def testDFTB(self): try: yaml_file = os.path.join(_TESTDATA_DIR, "Si2-muairss-dftb.yaml") cell_file = os.path.join(_TESTDATA_DIR, "Si2.cell") input_params = load_input_file(yaml_file, MuAirssSchema) with silence_stdio(True, True): input_atoms = io.read(cell_file) # Run Muairss write: sys.argv[1:] = ["-tw", cell_file, yaml_file] os.chdir(_TESTDATA_DIR) run_muairss() # Check all folders contain a dftb_in.hsd and geo_end.gen for rootDir, subDirs, files in os.walk( os.path.abspath("muon-airss-out-dftb/dftb+") ): expected_files = ["geo_end.gen", "dftb_in.hsd"] for s in subDirs: count = 0 for f in expected_files: f = os.path.join("muon-airss-out-dftb/dftb+/" + s, f) self.assertTrue(os.path.exists(f)) if count == 0: with silence_stdio(True, True): atoms = io.read(f) equal = atoms.cell == input_atoms.cell self.assertTrue(equal.all()) count += 1 # Run DFTB if _RUN_DFTB: subprocess.call(os.path.join(_TESTDATA_DIR, "script-dftb")) else: yaml_file = os.path.join(_TESTDATA_DIR, "Si2-muairss-dftb-read.yaml") input_params = load_input_file(yaml_file, MuAirssSchema) sys.argv[1:] = [cell_file, yaml_file] run_muairss() clust_folder = "Si2_clusters/dftb+" self.assertTrue(os.path.exists(clust_folder)) self.assertTrue( len( glob.glob( os.path.join( clust_folder, "*.{0}".format(input_params["clustering_save_format"]), ) ) ) > 0 ) self.assertTrue(os.path.exists("Si2_clusters.txt")) self.assertTrue(os.path.exists("Si2_Si2_dftb+_clusters.dat")) finally: # Remove all created files and folders _clean_testdata_dir()
def main(task=None): parser = ap.ArgumentParser() parser.add_argument('structures', type=str, default=None, help="A structure file or a folder of files in an ASE " "readable format") parser.add_argument('parameter_file', type=str, default=None, help="""YAML formatted file with generation parameters. The arguments can be overridden by structure-specific YAML files if a folder is passed as the first argument.""") parser.add_argument('-t', type=str, default='r', choices=['r', 'w'], dest='task', help="""Task to be run by muairss. Can be either 'w' (=generate and WRITE structures) or 'r' (=READ and cluster results). Default is READ.""") args = parser.parse_args() params = load_input_file(args.parameter_file, MuAirssSchema) if task is None: task = args.task if task == 'w': if os.path.isdir(args.structures): muairss_batch_io(args, params, True) elif os.path.isfile(args.structures): with silence_stdio(): struct = io.read(args.structures) save_muairss_collection(struct, params) else: raise RuntimeError("{} is neither a file or a directory".format( args.structures)) elif task == 'r': if os.path.isdir(args.structures): all_coll = muairss_batch_io(args, params) clusters = {} for name, data in all_coll.items(): clusters[name] = muairss_cluster(data['struct'], data['collection'], params) elif os.path.isfile(args.structures): with silence_stdio(): struct = io.read(args.structures) collection = load_muairss_collection(struct, params) clusters = { params['name']: muairss_cluster(struct, collection, params) } else: raise RuntimeError("{} is neither a file or a directory".format( args.structures)) write_cluster_report(args, params, clusters)
def _read_castep(self, folder, sname=None): try: if sname is not None: cfile = os.path.join(folder, sname + ".castep") else: cfile = glob.glob(os.path.join(folder, "*.castep"))[0] sname = seedname(cfile) with silence_stdio(): atoms = io.read(cfile) atoms.info["name"] = sname return atoms except IndexError: raise IOError("ERROR: No .castep files found in {}.".format( os.path.abspath(folder))) except OSError as e: raise IOError("ERROR: {}".format(e)) except ( io.formats.UnknownFileTypeError, ValueError, TypeError, Exception, ) as e: raise IOError( "ERROR: Invalid file: {file}, due to error: {error}".format( file=sname + ".castep", error=e))
def main(): parser = ap.ArgumentParser() parser.add_argument( "structure", type=str, default=None, help="A structure file in an ASE readable format", ) parser.add_argument( "-sp", "--symprec", type=float, default=1e-3, help="Symmetry precision to use in spglib", ) args = parser.parse_args() with silence_stdio(): a = io.read(args.structure) symdata = SymmetryDataset.get(a, symprec=args.symprec) wpoints = WyckoffPoints.get(a, symprec=args.symprec) fpos = a.get_scaled_positions() write_symmetry_report(args, symdata, wpoints, fpos)
def print_symmetry_report(): parser = ap.ArgumentParser() parser.add_argument('structure', type=str, default=None, help="A structure file in an ASE readable format") args = parser.parse_args() with silence_stdio(): a = io.read(args.structure) symdata = SymmetryDataset.get(a) wpoints = WyckoffPoints.get(a) fpos = a.get_scaled_positions() print("Wyckoff points symmetry report for {0}".format(args.structure)) print("Space Group International Symbol: " "{0}".format(symdata['international'])) print("Space Group Hall Number: " "{0}".format(symdata['hall_number'])) print("Absolute\t\t\tFractional\t\tHessian constraints") # List any Wyckoff point that does not already have an atom in it for wp in wpoints: if np.any(np.isclose(np.linalg.norm(fpos - wp.fpos, axis=1), 0)): continue print("{0}\t{1}\t{2}".format(wp.pos, wp.fpos, wp.hessian))
def test_write(self): # read in cell file to get atom try: input_folder = _TESTDATA_DIR + "/Si2" os.chdir(input_folder) output_folder = "test_save" os.mkdir(output_folder) with silence_stdio(): atoms = io.read("Si2.cell") # test writing geom_opt output param_file = "Si2-muairss-uep.yaml" params = load_input_file(param_file, MuAirssSchema) reader = ReadWriteUEP(params=params) reader.write(atoms, output_folder) self.assertTrue( os.path.exists(os.path.join(output_folder, "test_save.yaml"))) params["charged"] = False reader = ReadWriteUEP(params=params) reader.write(atoms, output_folder) except Exception as e: print(e) finally: shutil.rmtree("test_save")
def _create_calculator(self, calc_type=None): with silence_stdio(): if self._calc is not None and isinstance(self._calc, Castep): calc = deepcopy(self._calc) else: calc = Castep() mu_symbol = self.params.get('mu_symbol', 'H:mu') # Start by ensuring that the muon mass and gyromagnetic ratios are # included gamma_block = calc.cell.species_gamma.value if gamma_block is None: calc.cell.species_gamma = add_to_castep_block( gamma_block, mu_symbol, constants.m_gamma, 'gamma') mass_block = calc.cell.species_mass.value calc.cell.species_mass = add_to_castep_block( mass_block, mu_symbol, constants.m_mu_amu, 'mass') # Now assign the k-points k_points_param = self.params.get('k_points_grid') if k_points_param is not None: calc.cell.kpoint_mp_grid = list_to_string(k_points_param) else: if calc.cell.kpoint_mp_grid is None: calc.cell.kpoint_mp_grid = list_to_string([1, 1, 1]) # Read the parameters pfile = self.params.get('castep_param', None) if pfile is not None: with silence_stdio(): calc.param = read_param(self.params['castep_param']).param self._calc = calc if calc_type == "MAGRES": calc = self._create_hfine_castep_calculator() elif calc_type == "GEOM_OPT": calc = self._create_geom_opt_castep_calculator() return self._calc
def save_muairss_collection(struct, params, batch_path=""): """Generate input files for a single structure and configuration file""" dc = generate_muairss_collection(struct, params) # Just to keep track, add the parameters used to the collection dc.info["muairss_params"] = dict(params) # Output folder out_path = safe_create_folder(os.path.join(batch_path, params["out_folder"])) if not out_path: raise RuntimeError("Could not create folder {0}") io_formats = { "castep": ReadWriteCastep, "dftb+": ReadWriteDFTB, "uep": ReadWriteUEP, } calcs = [s.strip().lower() for s in params["calculator"].split(",")] if "all" in calcs: calcs = io_formats.keys() # Save LICENSE file for DFTB+ parameters if "dftb+" in calcs: from pymuonsuite.data.dftb_pars import get_license with open(os.path.join(out_path, "dftb.LICENSE"), "w") as f: f.write(get_license()) for cname in calcs: rw = io_formats[cname](params, script=params.get("script_file")) calc_path = os.path.join(out_path, cname) dc.save_tree( calc_path, rw.write, name_root=params["name"], opt_args={"calc_type": "GEOM_OPT"}, safety_check=2, ) # Do we also save a collective structure? allf = params["allpos_filename"] if allf is not None: alls = struct.copy() csp = struct.get_chemical_symbols() for atoms in dc: # We rely on the fact the muon is always put at the end mu = atoms[-1] alls.append(mu) csp += [params["mu_symbol"]] alls.set_array("castep_custom_species", np.array(csp)) with silence_stdio(True, True): io.write(allf, alls)
def write(self, a, folder, sname=None, calc_type="GEOM_OPT"): """Writes input files for an Atoms object with a Castep calculator. | Args: | a (ase.Atoms): Atoms object to write. Can have a Castep | calculator attached to carry cell/param | keywords. | folder (str): Path to save the input files to. | sname (str): Seedname to save the files with. If not | given, use the name of the folder. | calc_type (str): Castep task which will be performed: | "GEOM_OPT" or "MAGRES" """ if calc_type == "GEOM_OPT" or calc_type == "MAGRES": if sname is None: sname = os.path.split(folder)[-1] # Same as folder name self._calc = deepcopy(self._calc) # We only use the calculator attached to the atoms object if a calc # has not been set when initialising the ReadWrite object OR we # have not called write() and made a calculator before. if self._calc is None: if isinstance(a.calc, Castep): self._calc = deepcopy(a.calc) self._create_calculator(calc_type=calc_type) else: self._update_calculator(calc_type) a.calc = self._calc with silence_stdio(): io.write( os.path.join(folder, sname + ".cell"), a, magnetic_moments="initial", ) write_param( os.path.join(folder, sname + ".param"), a.calc.param, force_write=True, ) if self.script is not None: stxt = open(self.script).read() stxt = stxt.format(seedname=sname) with open(os.path.join(folder, "script.sh"), "w") as sf: sf.write(stxt) else: raise (NotImplementedError( "Calculation type {} is not implemented." " Please choose 'GEOM_OPT' or 'MAGRES'".format(calc_type)))
def muairss_batch_io(args, global_params, save=False): structures_path = args.structures all_files = glob.glob(os.path.join(structures_path, "*")) structure_files = [ path for path in all_files if not os.path.splitext(path)[1] == ".yaml" ] if save: global_params["out_folder"] = safe_create_folder(global_params["out_folder"]) print( "Beginning {0} of {1} structures".format( "creation" if save else "loading", len(structure_files) ) ) bpath = global_params["out_folder"] loaded = {} for path in structure_files: name = parse_structure_name(path) parameter_file = os.path.join(structures_path, "{}.yaml".format(name)) if not os.path.isfile(parameter_file): parameter_file = None with silence_stdio(): struct = io.read(path) params = dict(global_params) # Copy params["name"] = name if parameter_file is not None: params = load_input_file(parameter_file, MuAirssSchema, merge=params) params["out_folder"] = params["name"] if save: print("Making {} ---------------------".format(name)) save_muairss_collection(struct, params, batch_path=bpath) else: print("Loading {} ---------------------".format(name)) coll = load_muairss_collection(struct, params, batch_path=bpath) loaded[name] = {"struct": struct, "collection": coll} print("Done!") if not save: return loaded
def test_create_calc(self): folder = os.path.join(_TESTDATA_DIR, "Si2") def check_geom_opt_params(calc, params): self.assertEqual(calc.label, params["name"]) self.assertEqual(calc.geom_steps, params["geom_steps"]) self.assertEqual(calc.gw_factor, params["uep_gw_factor"]) self.assertEqual(calc.opt_tol, params["geom_force_tol"]) self.assertEqual(calc.save_structs, params["uep_save_structs"]) # In the case that a params dict is provided, the values for the # parameters should be taken from here: params = { "name": "Si2", "charged": True, "geom_steps": 300, "uep_gw_factor": 4.0, "geom_force_tol": 0.05, "uep_save_structs": False, "uep_chden": "Si2.den_fmt", } reader = ReadWriteUEP(params=params) with silence_stdio(): a = io.read(os.path.join(folder, "Si2.cell")) calc = reader._create_calculator(a, folder, "Si2") check_geom_opt_params(calc, params) # In the case that we do not supply a params dict or a calculator, # the new calculator should get the default settings: reader = ReadWriteUEP() params = { "name": "Si2", "geom_steps": 30, "uep_gw_factor": 5.0, "geom_force_tol": 1e-5, "uep_save_structs": True, } calc = reader._create_calculator(a, folder, "Si2") check_geom_opt_params(calc, params)
def _create_hfine_castep_calculator(self): """Update calculator to contain all the necessary parameters for a hyperfine calculation.""" # Remove settings for geom_opt calculator: self._calc.param.geom_max_iter = None self._calc.param.geom_force_tol = None self._calc.param.max_scf_cycles = None self._calc.param.write_cell_structure = None self._calc.param.charge = None self._calc.cell.fix_all_cell = None pfile = self.params.get('castep_param', None) if pfile is not None: with silence_stdio(): self._calc.param = read_param( self.params['castep_param']).param self._calc.param.task = 'Magres' self._calc.param.magres_task = 'Hyperfine' return self._calc
def test_create_calc(self): folder = os.path.join(_TESTDATA_DIR, "Si2") def check_geom_opt_params(calc, params): self.assertEqual(calc.label, params['name']) self.assertEqual(calc.geom_steps, params['geom_steps']) self.assertEqual(calc.gw_factor, params['uep_gw_factor']) self.assertEqual(calc.opt_tol, params['geom_force_tol']) self.assertEqual(calc.save_structs, params['uep_save_structs']) # In the case that a params dict is provided, the values for the # parameters should be taken from here: params = {'name': 'Si2', 'charged': True, 'geom_steps': 300, 'uep_gw_factor': 4.0, 'geom_force_tol': 0.05, 'uep_save_structs': False, 'uep_chden': 'Si2.den_fmt'} reader = ReadWriteUEP(params=params) with silence_stdio(): a = io.read(os.path.join(folder, "Si2.cell")) calc = reader._create_calculator(a, folder, "Si2") check_geom_opt_params(calc, params) # In the case that we do not supply a params dict or a calculator, # the new calculator should get the default settings: reader = ReadWriteUEP() params = {'name': 'Si2', 'geom_steps': 30, 'uep_gw_factor': 5.0, 'geom_force_tol': 1e-5, 'uep_save_structs': True} calc = reader._create_calculator(a, folder, 'Si2') check_geom_opt_params(calc, params)
def muon_vibrational_average_write(structure, method="independent", mu_index=-1, mu_symbol="H:mu", grid_n=20, sigma_n=3, avgprop="hyperfine", calculator="castep", displace_T=0, phonon_source_file=None, phonon_source_type="castep", **kwargs): """ Write input files to compute a vibrational average for a quantity on a muon in a given system. | Pars: | structure (str): Filename for input structure file | method (str): Method to use for the average. Options are | 'independent', 'montecarlo'. | Default is 'independent'. | mu_index (int): Position of the muon in the given cell file. | Default is -1. | mu_symbol (str): Use this symbol to look for the muon among | CASTEP custom species. Overrides muon_index if | present in cell. | grid_n (int): Number of configurations used for sampling. | Applies slightly | differently to different schemes. | sigma_n (int): Number of sigmas of the harmonic wavefunction used | for sampling. | avgprop (str): Property to calculate and average. Default is | 'hyperfine'. | calculator (str): Source of the property to calculate and average. | Can be 'castep' or 'dftb+'. Default is 'castep'. | phonon_source (str):Source of the phonon data. Can be 'castep' or | 'asedftbp'. Default is 'castep'. | **kwargs: Other arguments (such as specific arguments for | the given phonon method) """ # Open the structure file with silence_stdio(): cell = io.read(structure) path = os.path.split(structure)[0] sname = seedname(structure) cell.info["name"] = sname # Fetch species try: species = cell.get_array("castep_custom_species") except KeyError: species = np.array(cell.get_chemical_symbols()) mu_indices = np.where(species == mu_symbol)[0] if len(mu_indices) > 1: raise MuonAverageError("More than one muon found in the system") elif len(mu_indices) == 1: mu_index = mu_indices[0] else: species = list(species) species[mu_index] = mu_symbol species = np.array(species) cell.set_array("castep_custom_species", species) io_formats = {"castep": ReadWriteCastep, "dftb+": ReadWriteDFTB} # Load the phonons if phonon_source_file is not None: phpath, phfile = os.path.split(phonon_source_file) phfile = seedname(seedname(phfile)) # have to do twice for dftb case else: phpath = path phfile = sname try: rw = io_formats[phonon_source_type]() atoms = rw.read(phpath, phfile, read_phonons=True) ph_evals = atoms.info["ph_evals"] ph_evecs = atoms.info["ph_evecs"] except IOError: raise return except KeyError: phonon_source_file = os.path.join(phpath, phfile + ".phonon") if phonon_source_type == "dftb+": phonon_source_file = phonon_source_file + "s.pkl" raise (IOError( "Phonon file {0} could not be read.".format(phonon_source_file))) return # Fetch masses try: masses = parse_castep_masses(cell) except AttributeError: # Just fall back on ASE standard masses if not available masses = cell.get_masses() masses[mu_index] = constants.m_mu_amu cell.set_masses(masses) # Now create the distribution scheme if method == "independent": displsch = IndependentDisplacements(ph_evals, ph_evecs, masses, mu_index, sigma_n) elif method == "montecarlo": # Set seed np.random.seed(kwargs["random_seed"]) displsch = MonteCarloDisplacements(ph_evals, ph_evecs, masses) displsch.recalc_displacements(n=grid_n, T=displace_T) # Make it a collection pos = cell.get_positions() displaced_cells = [] for i, d in enumerate(displsch.displacements): dcell = cell.copy() dcell.set_positions(pos + d) if calculator == "dftb" and not kwargs["dftb_pbc"]: dcell.set_pbc(False) dcell.info["name"] = sname + "_displaced_{0}".format(i) displaced_cells.append(dcell) if kwargs["write_allconf"]: # Write a global configuration structure allconf = sum(displaced_cells, cell.copy()) with silence_stdio(): if all(allconf.get_pbc()): io.write(sname + "_allconf.cell", allconf) else: io.write(sname + "_allconf.xyz", allconf) # Get a calculator if calculator == "castep": params = { "castep_param": kwargs["castep_param"], "k_points_grid": kwargs["k_points_grid"], "mu_symbol": mu_symbol, } io_format = ReadWriteCastep(params=params, calc=cell.calc, script=kwargs["script_file"]) opt_args = {"calc_type": "MAGRES"} elif calculator == "dftb+": params = { "dftb_set": kwargs["dftb_set"], "dftb_pbc": kwargs["dftb_pbc"], "k_points_grid": kwargs["k_points_grid"] if kwargs["dftb_pbc"] else None, } io_format = ReadWriteDFTB(params=params, calc=cell.calc, script=kwargs["script_file"]) opt_args = {"calc_type": "SPINPOL"} displaced_coll = AtomsCollection(displaced_cells) displaced_coll.info["displacement_scheme"] = displsch displaced_coll.info["muon_index"] = mu_index displaced_coll.save_tree(sname + "_displaced", io_format.write, opt_args=opt_args)
def __init__(self, structures=[], info={}, cell_reduce=False, progress=False, suppress_ase_warnings=True): """ Initialize the AtomsCollection | Args: | structures (list[str] or list[ase.Atoms]): list of file names or | Atoms that will form | the collection | info (dict): dictionary of general information to attach | to this collection | cell_reduce (bool): if True, perform a Niggli cell reduction on | all loaded structures | progress (bool): visualize a progress bar for the loading process | suppress_ase_warnings (bool): suppress annoying ASE warnings when | loading files (default is True) """ # Start by parsing out the structures self.structures = [] if isinstance(structures, ase.Atoms): # Well, it's just one... structures = [structures] elif inspect.isgenerator(structures): # Let's unravel it iter_structs = structures structures = [] for s in iter_structs: structures.append(s) if progress: sys.stdout.write("Loading collection...\n") s_n = len(structures) for s_i, struct in enumerate(structures): if progress: # Progress bar sys.stdout.write("\rLoading: {0}".format(utils.progbar(s_i+1, s_n))) # Is it an Atoms object? if type(struct) is ase.Atoms: self.structures.append(ase.Atoms(struct)) # Copy all arrays for k in struct.arrays.keys(): if not self.structures[-1].has(k): self.structures[-1].new_array(k, struct.get_array(k)) if struct.calc is not None: # Prevents pointless attempts at re-calculating self.structures[-1].calc._old_atoms = self.structures[-1] # Or is it a string? elif utils.is_string(struct): with utils.silence_stdio(suppress_ase_warnings, suppress_ase_warnings): self.structures.append(ase_io.read(str(struct))) # If there's no name, give it the filename if 'name' not in self.structures[-1].info: self.structures[-1].info['name'] = utils.seedname(struct) else: raise TypeError('Structures must be Atoms objects or valid ' 'file names,' ' not {0}'.format(type(struct).__name__)) if cell_reduce: # Here we must keep the energy if it was present # We do this by hand because ASE has its good reasons # for severing the atoms-calculator connection when changing # the unit cell. try: _E = self.structures[-1].calc.results['energy'] except (KeyError, AttributeError): _E = None niggli_reduce(self.structures[-1]) if _E is not None: _calc = SinglePointCalculator(self.structures[-1], energy=_E) self.structures[-1].set_calculator(_calc) if progress: sys.stdout.write('\nLoaded {0} structures\n'.format(s_n)) self._all = _AllCaller(self.structures, ase.Atoms) self._arrays = {} # Now assign the info if type(info) is not dict: raise TypeError('Info must be dict,' ' not {0}'.format(type(info).__name__)) else: self.info = info.copy()
def asephonons_entry(): from ase import io from ase.calculators.dftb import Dftb from pymuonsuite.data.dftb_pars import DFTBArgs parser = ap.ArgumentParser( description="Compute phonon modes with ASE and" " DFTB+ for reuse in quantum effects " "calculations." ) parser.add_argument( "structure_file", type=str, help="Structure for which to compute the phonons", ) parser.add_argument( "parameter_file", type=str, help="YAML file containing relevant input parameters", ) args = parser.parse_args() # Load parameters params = load_input_file(args.parameter_file, AsePhononsSchema) fname, fext = os.path.splitext(args.structure_file) if params["name"] is None: params["name"] = fname # Load structure with silence_stdio(): a = io.read(args.structure_file) # Create a Dftb calculator dargs = DFTBArgs(params["dftb_set"]) # Is it periodic? if params["pbc"]: a.set_pbc(True) calc = Dftb( atoms=a, label="asephonons", kpts=params["kpoint_grid"], **dargs.args ) ph_kpts = params["phonon_kpoint_grid"] else: a.set_pbc(False) calc = Dftb(atoms=a, label="asephonons", **dargs.args) ph_kpts = None a.calc = calc try: phdata = ase_phonon_calc( a, kpoints=ph_kpts, ftol=params["force_tol"], force_clean=params["force_clean"], name=params["name"], ) except Exception as e: print(e) print("Error: Could not write phonons file, see asephonons.out for" " details.") sys.exit(1) # Save optimised structure with silence_stdio(): io.write(params["name"] + "_opt" + fext, phdata.structure) # And write out the phonons outf = params["name"] + "_opt.phonons.pkl" pickle.dump(phdata, open(outf, "wb")) write_phonon_report(args, params, phdata)
def testCASTEP(self): try: yaml_file = os.path.join(_TESTDATA_DIR, "Si2-muairss-castep.yaml") cell_file = os.path.join(_TESTDATA_DIR, "Si2.cell") param_file = os.path.join(_TESTDATA_DIR, "Si2.param") input_params = load_input_file(yaml_file, MuAirssSchema) with silence_stdio(True, True): castep_param = read_param(param_file).param # Run Muairss write: sys.argv[1:] = ["-tw", cell_file, yaml_file] os.chdir(_TESTDATA_DIR) run_muairss() # Check all folders contain a yaml file for (rootDir, subDirs, files) in os.walk("muon-airss-out-castep/castep/"): for s in subDirs: expected_file = os.path.join( "muon-airss-out-castep/castep/" + s, s + ".cell" ) script_file = input_params["script_file"] if script_file is not None: expected_script = os.path.join( "muon-airss-out-castep/castep/" + s, "script.sh" ) self.assertTrue(os.path.exists(expected_script)) self.assertTrue(os.path.exists(expected_file)) with silence_stdio(): atoms = io.read(expected_file) self.assertEqual( atoms.calc.cell.kpoint_mp_grid.value, list_to_string(input_params["k_points_grid"]), ) expected_param_file = os.path.join( "muon-airss-out-castep/castep/" + s, s + ".param" ) self.assertTrue(os.path.exists(expected_param_file)) with silence_stdio(): output_castep_param = read_param(expected_param_file).param self.assertEqual( output_castep_param.cut_off_energy, castep_param.cut_off_energy, ) self.assertEqual( output_castep_param.elec_energy_tol, castep_param.elec_energy_tol, ) # below test didn't work as cell positions get rounded... # equal = atoms.cell == input_atoms.cell # self.assertTrue(equal.all()) yaml_file = os.path.join(_TESTDATA_DIR, "Si2-muairss-castep-read.yaml") sys.argv[1:] = [cell_file, yaml_file] run_muairss() self.assertTrue(os.path.exists("Si2_clusters.txt")) self.assertTrue(os.path.exists("Si2_Si2_castep_clusters.dat")) # Test clustering_write_input has produced files we expect: self.assertTrue(os.path.exists("Si2_clusters")) calc_folder = "Si2_clusters/castep/" for (rootDir, subDirs, files) in os.walk(calc_folder): for s in subDirs: expected_file = os.path.join(calc_folder + s, s + ".cell") self.assertTrue(os.path.exists(expected_file)) with silence_stdio(): atoms = io.read(expected_file) self.assertEqual( atoms.calc.cell.kpoint_mp_grid.value, list_to_string(input_params["k_points_grid"]), ) expected_param_file = os.path.join(calc_folder + s, s + ".param") self.assertTrue(os.path.exists(expected_param_file)) with silence_stdio(): output_castep_param = read_param(expected_param_file).param self.assertEqual( output_castep_param.cut_off_energy, castep_param.cut_off_energy, ) self.assertEqual( output_castep_param.elec_energy_tol, castep_param.elec_energy_tol, ) finally: # Remove all created files and folders _clean_testdata_dir()
def read(self, folder, sname=None, read_spinpol=False, read_phonons=False, **kwargs): ''' Read a DFTB+ output non-destructively. | | Args: | folder (str) : path to a directory to load DFTB+ results | sname (str): name to label the atoms with and/or of the | .phonons.pkl file to be read | Returns: | atoms (ase.Atoms): an atomic structure with the results | attached in a SinglePointCalculator ''' try: with silence_stdio(): atoms = io.read(os.path.join(folder, 'geo_end.gen')) except IOError: raise IOError("ERROR: No geo_end.gen file found in {}." .format(os.path.abspath(folder))) except Exception as e: raise IOError("ERROR: Could not read {file}, due to error: {error}" .format(file='geo_end.gen', error=e)) if sname is None: atoms.info['name'] = os.path.split(folder)[-1] else: atoms.info['name'] = sname results_file = os.path.join(folder, "results.tag") if os.path.isfile(results_file): # DFTB+ was used to perform the optimisation temp_file = os.path.join(folder, "results.tag.bak") # We need to backup the results file here because # .read_results() will remove the results file with BackupFile(results_file, temp_file): calc = Dftb(atoms=atoms) calc.atoms_input = atoms calc.directory = folder calc.do_forces = True calc.read_results() energy = calc.get_potential_energy() forces = calc.get_forces() charges = calc.get_charges(atoms) calc = SinglePointCalculator(atoms, energy=energy, forces=forces, charges=charges) atoms.calc = calc if read_spinpol: try: pops = parse_spinpol_dftb(folder) hfine = [] for i in range(len(atoms)): hf = compute_hfine_mullpop(atoms, pops, self_i=i, fermi=True, fermi_neigh=True) hfine.append(hf) atoms.set_array('hyperfine', np.array(hfine)) except (IndexError, IOError) as e: raise IOError('Could not read hyperfine details due to error: ' '{0}'.format(e)) if read_phonons: try: if sname is not None: phonon_source_file = os.path.join(folder, sname + '.phonons.pkl') else: print("Phonons filename was not given, searching for any" " .phonons.pkl file.") phonon_source_file = glob.glob( os.path.join(folder, '*.phonons.pkl'))[0] self._read_dftb_phonons(atoms, phonon_source_file) except IndexError: raise IOError("No .phonons.pkl files found in {}." .format(os.path.abspath(folder))) except IOError: raise IOError("{} could not be found." .format(phonon_source_file)) except Exception as e: raise IOError('Could not read {file} due to error: {error}' .format(file=phonon_source_file, error=e)) return atoms
def __init__(self, seedname, gw_fac=3, path=""): """Initialise a ChargeDistrbution object. Initialise a ChargeDistribution object with CASTEP output files. Arguments: seedname {str} -- The seedname of the CASTEP output files (.den_fmt and .castep) used to load the data. Keyword Arguments: gw_fac {number} -- Gaussian width factor. The Gaussian width used for each ion will be the radius of its pseudopotential divided by this factor. (default: {3}) path {str} -- Path in which the CASTEP output files can be found. (default: {''}) Raises: RuntimeError -- CASTEP pseudopotentials were not found """ # CASTEP SPECIFIC CODE - reading files # Load the electronic density seedpath = os.path.join(path, seedname) self._elec_den = FMTReader(seedpath + ".den_fmt") with silence_stdio(): self._struct = io.read(seedpath + ".castep") ppots = parse_castep_ppots(seedpath + ".castep") # Override by also grabbing any pseudopotentials found in the .cell # file cppot = None try: with silence_stdio(): cppot = io.read(seedpath + ".cell").calc.cell.species_pot.value except IOError: pass # If not available, ignore this if cppot is not None: ppf = [lpp.split() for lpp in cppot.split("\n") if lpp] for el, pppath in ppf: f = os.path.join(path, pppath) try: ppots.update(parse_castep_ppots(f)) except IOError: # File not found print("WARNING: pseudopotential file " "{0} not found".format(f)) # END OF CASTEP SPECIFIC CODE # unit cell and FFT grid lattice = np.array(self._elec_den.real_lattice) # Ang grid = np.array(self._elec_den.grid) # dx = [np.linalg.norm(lattice[i]) / grid[i] for i in range(3)] inv_latt = np.linalg.inv(lattice.T) * 2 * np.pi # Ang^-1 fft_grid = np.array( np.meshgrid(*[np.fft.fftfreq(grid[i]) * grid[i] for i in range(3)], indexing="ij")) # Uses the g-vector convention in formulas used # this should match CASTEP's grid in reciprocal space self._g_grid = np.tensordot(inv_latt, fft_grid, axes=(0, 0)) # Ang^-1 # Information for the elements, and guarantee zero net charge elems = self._struct.get_chemical_symbols() pos = self._struct.get_positions() try: # CASTEP SPECIFIC CODE (maybe) - this way of getting charges & # gaussian widths from pseudopotentials self._q = np.array([ppots[el][0] for el in elems]) # e self._gw = np.array([ppots[el][1] / gw_fac for el in elems]) # Ang # END OF CASTEP SPECIFIC CODE except KeyError: raise RuntimeError("""Some or all CASTEP pseudopotentials were not found. UEP calculation can not go on. Please notice that at the moment only ultrasoft pseudopotentials are supported, and if not generated automatically, they must be possible to retrieve using the paths in the SPECIES_POT block of the .cell file.""") # Here we find the Fourier components of the potential due to # the valence electrons self._rho = self._elec_den.data[:, :, :, 0] if not np.isclose(np.average(self._rho), sum(self._q), 1e-4): raise RuntimeError("Cell is not neutral") # Put the minus sign for electrons # CASTEP SPECIFIC CODE - unit conversion. Needs standardising. self._rho *= -sum(self._q) / np.sum( self._rho) # Normalise charge. Now it's e/grid points # END OF CASTEP SPECIFIC CODE self._rhoe_G = np.fft.fftn(self._rho) Gnorm = np.linalg.norm(self._g_grid, axis=0) # remove 0-values as we will use 1/G_norm in calculations # (0-values are special cases and we set the results manually elsewhere) Gnorm_fixed = np.where(Gnorm > 0, Gnorm, np.inf) cell = np.array(self._elec_den.real_lattice) vol = abs(np.dot(np.cross(cell[:, 0], cell[:, 1]), cell[:, 2])) # Ang^3 self._vol = vol # Core formula: reciprocal space potential contribution FROM ELECTRONS self._Ve_G = 4 * np.pi / Gnorm_fixed**2 * (self._rhoe_G / vol) # e/Ang # Now on to doing the same for ionic components self._rhoi_G = self._g_grid[0] * 0.0j for i, p in enumerate(pos): self._rhoi_G += self._q[i] * np.exp( -1.0j # phase term corresponding to translation in direct space * np.sum(self._g_grid[:, :, :, :] * p[:, None, None, None], axis=0) - 0.5 * (self._gw[i] * Gnorm)**2) # Reciprocal space potential contributions FROM IONS # (approximated as Gaussian charges) self._Vi_G = 4 * np.pi / Gnorm_fixed**2 * (self._rhoi_G / vol) # Is there any data on spin polarization? self._spinpol = False if self._elec_den.data.shape[-1] >= 2: self._spinpol = True self._spin = self._elec_den.data[:, :, :, 1] self._spin_G = np.fft.fftn(self._spin) # Dipolar tensor FFT dyad_G = self._g_grid[:, None] * self._g_grid[None, :] dyad_G /= Gnorm_fixed**2 self._dip_G = ( 4.0 / 3.0 * np.pi * (3 * dyad_G - np.eye(3)[:, :, None, None, None] + 0j)) self._dip_G[:, :, 0, 0, 0] = 0 self._dip_G *= self._spin_G / (self._vol * np.prod(self._spin.shape)) # Convert to Tesla. self._dip_G *= _dipT # Now, Thomas-Fermi energy tfint = np.sum(abs(self._rho / self._vol)**(5 / 3) * self._vol) C = (0.3 * cnst.hbar**2 / cnst.m_e * (3 * np.pi**2)**(2 / 3) / cnst.e * 1e20) self._thomasFermiE = C * tfint
def test_write(self): # read in cell file to get atom try: input_folder = _TESTDATA_DIR + "/Si2" output_folder = os.path.join(_TESTDATA_DIR, "test_save") os.mkdir(output_folder) os.chdir(input_folder) yaml_file = os.path.join(input_folder, "Si2-muairss-castep.yaml") cell_file = os.path.join(input_folder, "Si2.cell") param_file = os.path.join(input_folder, "Si2.param") input_params = load_input_file(yaml_file, MuAirssSchema) with silence_stdio(): castep_param = read_param(param_file).param atoms = io.read(cell_file) # test writing geom_opt output reader = ReadWriteCastep(params=input_params) reader.write( atoms, output_folder, sname="Si2_geom_opt", calc_type="GEOM_OPT", ) reader.write(atoms, output_folder, sname="Si2_magres", calc_type="MAGRES") # # read back in and check that atom locations are preserved with silence_stdio(): geom_opt_atoms = io.read( os.path.join(output_folder, "Si2_geom_opt.cell") ) magres_atoms = io.read(os.path.join(output_folder, "Si2_magres.cell")) equal = atoms.positions == geom_opt_atoms.positions # self.assertTrue(equal.all()) # is not true due to to rounding equal = geom_opt_atoms.positions == magres_atoms.positions self.assertTrue(equal.all()) self.assertEqual( geom_opt_atoms.calc.cell.kpoint_mp_grid.value, list_to_string(input_params["k_points_grid"]), ) self.assertEqual( magres_atoms.calc.cell.kpoint_mp_grid.value, list_to_string(input_params["k_points_grid"]), ) # Test if parameters file have correct tasks: with silence_stdio(): geom_params = read_param( os.path.join(output_folder, "Si2_geom_opt.param") ).param magres_params = read_param( os.path.join(output_folder, "Si2_magres.param") ).param self.assertEqual(geom_params.task.value, "GeometryOptimization") self.assertEqual(magres_params.task.value, "Magres") self.assertEqual(magres_params.magres_task.value, "Hyperfine") # These are only set in the param file only so should equal # the value in the param file: self.assertEqual(geom_params.geom_max_iter, castep_param.geom_max_iter) self.assertEqual(geom_params.cut_off_energy, castep_param.cut_off_energy) self.assertEqual(geom_params.elec_energy_tol, castep_param.elec_energy_tol) # This is set in the input yaml and param file so should equal # the value in the yaml file: self.assertEqual( geom_params.geom_force_tol.value, str(input_params["geom_force_tol"]), ) self.assertEqual(magres_params.cut_off_energy, castep_param.cut_off_energy) self.assertEqual( magres_params.elec_energy_tol, castep_param.elec_energy_tol ) finally: shutil.rmtree(output_folder)
def write_cluster_report(args, params, clusters): if params['clustering_method'] == 'hier': clustinfo = """ Clustering method: Hierarchical t = {t} """.format(t=params['clustering_hier_t']) elif params['clustering_method'] == 'kmeans': clustinfo = """ Clustering method: k-Means k = {k} """.format(k=params['clustering_kmeans_k']) with open(params['name'] + '_clusters.txt', 'w') as f: f.write(""" **************************** | | | MUAIRSS | | Clustering report | | | **************************** Name: {name} Date: {date} Structure file(s): {structs} Parameter file: {param} {clustinfo} ******************* """.format(name=params['name'], date=datetime.now(), structs=args.structures, param=args.parameter_file, clustinfo=clustinfo)) for name, cdata in clusters.items(): f.write('Clusters for {0}:\n'.format(name)) if params['clustering_save_min'] is not None or \ params['clustering_save_type'] is not None: if params['clustering_save_folder'] is not None: clustering_save_path = safe_create_folder( params['clustering_save_folder']) else: clustering_save_path = safe_create_folder( '{0}_clusters'.format(params['name'])) if not clustering_save_path: raise RuntimeError('Could not create folder {0}') for calc, clusts in cdata.items(): # Computer readable fdat = open( params['name'] + '_{0}_{1}_clusters.dat'.format(name, calc), 'w') f.write('CALCULATOR: {0}\n'.format(calc)) (cinds, cgroups), ccolls, gvecs = clusts f.write('\t{0} clusters found\n'.format(max(cinds))) min_energy_structs = [] for i, g in enumerate(cgroups): f.write('\n\n\t-----------\n\tCluster ' '{0}\n\t-----------\n'.format(i + 1)) f.write('\tStructures: {0}\n'.format(len(g))) coll = ccolls[i + 1] E = gvecs[g, 0] Emin = np.amin(E) Eavg = np.average(E) Estd = np.std(E) f.write('\n\tEnergy (eV):\n') f.write('\tMinimum\t\tAverage\t\tStDev\n') f.write('\t{0:.2f}\t\t{1:.2f}\t\t{2:.2f}\n'.format( Emin, Eavg, Estd)) fdat.write('\t'.join( map(str, [ i + 1, len(g), Emin, Eavg, Estd, coll[np.argmin( E)].structures[0].positions[-1][0], coll[np.argmin(E)].structures[0].positions[-1][1], coll[np.argmin(E)].structures[0].positions[-1][2] ])) + '\n') f.write('\n\tMinimum energy structure: {0}\n'.format( coll[np.argmin(E)].structures[0].info['name'])) # Save minimum energy structure if params['clustering_save_type'] == 'structures' or \ params['clustering_save_min']: # For backwards-compatability with old pymuonsuite # versions if params['clustering_save_min']: if params['clustering_save_format'] is None: params['clustering_save_format'] = 'cif' try: calc_path = os.path.join(clustering_save_path, calc) if not os.path.exists(calc_path): os.mkdir(calc_path) fname = ('{0}_{1}_min_cluster_' '{2}.{3}'.format( params['name'], calc, i + 1, params['clustering_save_format'])) with silence_stdio(): io.write(os.path.join(calc_path, fname), coll[np.argmin(E)].structures[0]) except (io.formats.UnknownFileTypeError) as e: print("ERROR: File format '{0}' is not " "recognised. Modify 'clustering_save_format'" " and try again.".format(e)) return except ValueError as e: print("ERROR: {0}. Modify 'clustering_save_format'" "and try again.".format(e)) return min_energy_structs.append(coll[np.argmin(E)].structures[0]) f.write('\n\n\tStructure list:') for j, s in enumerate(coll): if j % 4 == 0: f.write('\n\t') f.write('{0}\t'.format(s.info['name'])) fdat.close() if params['clustering_save_type'] == 'input': calc_path = os.path.join(clustering_save_path, calc) sname = "{0}_min_cluster".format(params['name']) io_formats = { 'castep': ReadWriteCastep, 'dftb+': ReadWriteDFTB, } try: write_method = io_formats[ params['clustering_save_format']](params).write except KeyError as e: print("ERROR: Calculator type {0} is not " "recognised. Modify 'clustering_save_format'" " to be one of: {1}".format( e, list(io_formats.keys()))) return if params['clustering_save_format'] == 'dftb+': from pymuonsuite.data.dftb_pars import get_license with open( os.path.join(clustering_save_path, 'dftb.LICENSE'), 'w') as license_file: license_file.write(get_license()) min_energy_structs = AtomsCollection(min_energy_structs) # here we remove the structure's name so the original # numbering of the structs is removed: for i, a in enumerate(min_energy_structs): min_energy_structs.structures[i].info.pop('name', None) min_energy_structs.save_tree( calc_path, write_method, name_root=sname, opt_args={'calc_type': 'GEOM_OPT'}, safety_check=2) # Print distance matrix f.write('\n\n\t----------\n\n\tSimilarity (ranked):\n') centers = np.array( [np.average(gvecs[g], axis=0) for g in cgroups]) dmat = np.linalg.norm(centers[:, None] - centers[None, :], axis=-1) inds = np.triu_indices(len(cgroups), k=1) for i in np.argsort(dmat[inds]): c1 = inds[0][i] c2 = inds[1][i] d = dmat[c1, c2] f.write('\t{0} <--> {1} (distance = {2:.3f})\n'.format( c1 + 1, c2 + 1, d)) f.write('\n--------------------------\n\n') f.write('\n==========================\n\n')
def __init__(self, seedname, gw_fac=3, path=''): """Initialise a ChargeDistrbution object. Initialise a ChargeDistribution object with CASTEP output files. Arguments: seedname {str} -- The seedname of the CASTEP output files (.den_fmt and .castep) used to load the data. Keyword Arguments: gw_fac {number} -- Factor used to divide the Gaussian width used for the ions. The final width will be the radius of the pseudopotential divided by this. (default: {3}) path {str} -- Path in which the CASTEP output files can be found. (default: {''}) Raises: RuntimeError -- CASTEP pseudopotentials were not found """ # Load the electronic density seedpath = os.path.join(path, seedname) self._elec_den = FMTReader(seedpath + '.den_fmt') with silence_stdio(): self._struct = io.read(seedpath + '.castep') ppots = parse_castep_ppots(seedpath + '.castep') # Override by also grabbing any pseudopotentials found in the .cell # file cppot = None try: with silence_stdio(): cppot = io.read(seedpath + '.cell').calc.cell.species_pot.value except IOError: pass # If not available, ignore this if cppot is not None: ppf = [l.split() for l in cppot.split('\n') if l] for el, pppath in ppf: f = os.path.join(path, pppath) try: ppots.update(parse_castep_ppots(f)) except IOError: # File not found print('WARNING: pseudopotential file ' '{0} not found'.format(f)) # FFT grid lattice = np.array(self._elec_den.real_lattice) grid = np.array(self._elec_den.grid) dx = [np.linalg.norm(lattice[i]) / grid[i] for i in range(3)] inv_latt = np.linalg.inv(lattice.T) * 2 * np.pi fft_grid = np.array( np.meshgrid(*[np.fft.fftfreq(grid[i]) * grid[i] for i in range(3)], indexing='ij')) # Uses the g-vector convention in formulas used self._g_grid = np.tensordot(inv_latt, fft_grid, axes=(0, 0)) # Information for the elements, and guarantee zero net charge elems = self._struct.get_chemical_symbols() pos = self._struct.get_positions() try: self._q = np.array([ppots[el][0] for el in elems]) self._gw = np.array([ppots[el][1] / gw_fac for el in elems]) except KeyError: raise RuntimeError("""Some or all CASTEP pseudopotentials were not found. UEP calculation can not go on. Please notice that at the moment only ultrasoft pseudopotentials are supported, and if not generated automatically, they must be possible to retrieve using the paths in the SPECIES_POT block of the .cell file.""") # Here we find the Fourier components of the potential due to # the valence electrons self._rho = self._elec_den.data[:, :, :, 0] if not np.isclose(np.average(self._rho), sum(self._q), 1e-4): raise RuntimeError('Cell is not neutral') # Put the minus sign for electrons self._rho *= -sum(self._q) / np.sum(self._rho) # Normalise charge self._rhoe_G = np.fft.fftn(self._rho) Gnorm = np.linalg.norm(self._g_grid, axis=0) Gnorm_fixed = np.where(Gnorm > 0, Gnorm, np.inf) cell = np.array(self._elec_den.real_lattice) vol = abs(np.dot(np.cross(cell[:, 0], cell[:, 1]), cell[:, 2])) self._vol = vol self._Ve_G = 4 * np.pi / Gnorm_fixed**2 * (self._rhoe_G / vol) # Now on to doing the same for ionic components self._rhoi_G = self._g_grid[0] * 0.j for i, p in enumerate(pos): self._rhoi_G += (self._q[i] * np.exp(-1.0j * np.sum( self._g_grid[:, :, :, :] * p[:, None, None, None], axis=0) - 0.5 * (self._gw[i] * Gnorm)**2)) pregrid = (4 * np.pi / Gnorm_fixed**2 * 1.0 / vol) self._Vi_G = (pregrid * self._rhoi_G) # Is there any data on spin polarization? self._spinpol = False if self._elec_den.data.shape[-1] >= 2: self._spinpol = True self._spin = self._elec_den.data[:, :, :, 1] self._spin_G = np.fft.fftn(self._spin) # Dipolar tensor FFT dyad_G = self._g_grid[:, None] * self._g_grid[None, :] dyad_G /= Gnorm_fixed**2 self._dip_G = 4.0 / 3.0 * np.pi * ( 3 * dyad_G - np.eye(3)[:, :, None, None, None] + 0j) self._dip_G[:, :, 0, 0, 0] = 0 self._dip_G *= self._spin_G / (self._vol * np.prod(self._spin.shape)) # Convert to Tesla. self._dip_G *= _dipT # Now, Thomas-Fermi energy tfint = np.sum(abs(self._rho / self._vol)**(5 / 3) * self._vol) C = 0.3 * cnst.hbar**2 / cnst.m_e * (3 * np.pi**2)**(2 / 3) / cnst.e * 1e20 self._thomasFermiE = C * tfint