def prep_symmetry(atoms, symprec=1.0e-6, verbose=False): """ Prepare `at` for symmetry-preserving minimisation at precision `symprec` Returns a tuple `(rotations, translations, symm_map)` """ import spglib dataset = spglib.get_symmetry_dataset(atoms_to_spglib_cell(atoms), symprec=symprec) if verbose: print_symmetry(symprec, dataset) rotations = dataset['rotations'].copy() translations = dataset['translations'].copy() symm_map = [] scaled_pos = atoms.get_scaled_positions() for (rot, trans) in zip(rotations, translations): this_op_map = [-1] * len(atoms) for i_at in range(len(atoms)): new_p = rot @ scaled_pos[i_at, :] + trans dp = scaled_pos - new_p dp -= np.round(dp) i_at_map = np.argmin(np.linalg.norm(dp, axis=1)) this_op_map[i_at] = i_at_map symm_map.append(this_op_map) return (rotations, translations, symm_map)
def check_symmetry(atoms, symprec=1.0e-6, verbose=False): """ Check symmetry of `atoms` with precision `symprec` using `spglib` Prints a summary and returns result of `spglib.get_symmetry_dataset()` """ import spglib dataset = spglib.get_symmetry_dataset(atoms_to_spglib_cell(atoms), symprec=symprec) if verbose: print_symmetry(symprec, dataset) return dataset
def operate(self, atoms): # Do the operation sym_num = 1 sg = self.sym_goal while sym_num < sg: for _ in range(self.max_tries): for _ in range(2): permute2(atoms, rng=self.rng) self.dcf(atoms) sym_num = spglib.get_symmetry_dataset( atoms_to_spglib_cell(atoms))['number'] if sym_num >= sg: break sg -= 1 return atoms
def read_castep_cell(fd, index=None, calculator_args={}, find_spg=False, units=units_CODATA2002): """Read a .cell file and return an atoms object. Any value found that does not fit the atoms API will be stored in the atoms.calc attribute. By default, the Castep calculator will be tolerant and in the absence of a castep_keywords.json file it will just accept all keywords that aren't automatically parsed. """ from ase.calculators.castep import Castep cell_units = { # Units specifiers for CASTEP 'bohr': units_CODATA2002['a0'], 'ang': 1.0, 'm': 1e10, 'cm': 1e8, 'nm': 10, 'pm': 1e-2 } calc = Castep(**calculator_args) if calc.cell.castep_version == 0 and calc._kw_tol < 3: # No valid castep_keywords.json was found warnings.warn( 'read_cell: Warning - Was not able to validate CASTEP input. ' 'This may be due to a non-existing ' '"castep_keywords.json" ' 'file or a non-existing CASTEP installation. ' 'Parsing will go on but keywords will not be ' 'validated and may cause problems if incorrect during a CASTEP ' 'run.') celldict = read_freeform(fd) def parse_blockunit(line_tokens, blockname): u = 1.0 if len(line_tokens[0]) == 1: usymb = line_tokens[0][0].lower() u = cell_units.get(usymb, 1) if usymb not in cell_units: warnings.warn('read_cell: Warning - ignoring invalid ' 'unit specifier in %BLOCK {0} ' '(assuming Angstrom instead)'.format(blockname)) line_tokens = line_tokens[1:] return u, line_tokens # Arguments to pass to the Atoms object at the end aargs = {'pbc': True} # Start by looking for the lattice lat_keywords = [w in celldict for w in ('lattice_cart', 'lattice_abc')] if all(lat_keywords): warnings.warn('read_cell: Warning - two lattice blocks present in the' ' same file. LATTICE_ABC will be ignored') elif not any(lat_keywords): raise ValueError('Cell file must contain at least one between ' 'LATTICE_ABC and LATTICE_CART') if 'lattice_abc' in celldict: lines = celldict.pop('lattice_abc')[0].split('\n') line_tokens = [l.split() for l in lines] u, line_tokens = parse_blockunit(line_tokens, 'lattice_abc') if len(line_tokens) != 2: warnings.warn('read_cell: Warning - ignoring additional ' 'lines in invalid %BLOCK LATTICE_ABC') abc = [float(p) * u for p in line_tokens[0][:3]] angles = [float(phi) for phi in line_tokens[1][:3]] aargs['cell'] = cellpar_to_cell(abc + angles) if 'lattice_cart' in celldict: lines = celldict.pop('lattice_cart')[0].split('\n') line_tokens = [l.split() for l in lines] u, line_tokens = parse_blockunit(line_tokens, 'lattice_cart') if len(line_tokens) != 3: warnings.warn('read_cell: Warning - ignoring more than ' 'three lattice vectors in invalid %BLOCK ' 'LATTICE_CART') aargs['cell'] = [[float(x) * u for x in lt[:3]] for lt in line_tokens] # Now move on to the positions pos_keywords = [w in celldict for w in ('positions_abs', 'positions_frac')] if all(pos_keywords): warnings.warn('read_cell: Warning - two lattice blocks present in the' ' same file. POSITIONS_FRAC will be ignored') del celldict['positions_frac'] elif not any(pos_keywords): raise ValueError('Cell file must contain at least one between ' 'POSITIONS_FRAC and POSITIONS_ABS') aargs['symbols'] = [] pos_type = 'positions' pos_block = celldict.pop('positions_abs', [None])[0] if pos_block is None: pos_type = 'scaled_positions' pos_block = celldict.pop('positions_frac', [None])[0] aargs[pos_type] = [] lines = pos_block.split('\n') line_tokens = [l.split() for l in lines] if 'scaled' not in pos_type: u, line_tokens = parse_blockunit(line_tokens, 'positions_abs') else: u = 1.0 # Here we extract all the possible additional info # These are marked by their type add_info = { 'SPIN': (float, 0.0), # (type, default) 'MAGMOM': (float, 0.0), 'LABEL': (str, 'NULL') } add_info_arrays = dict((k, []) for k in add_info) def parse_info(raw_info): re_keys = (r'({0})\s*[=:\s]{{1}}\s' r'*([^\s]*)').format('|'.join(add_info.keys())) # Capture all info groups info = re.findall(re_keys, raw_info) info = {g[0]: add_info[g[0]][0](g[1]) for g in info} return info # Array for custom species (a CASTEP special thing) # Usually left unused custom_species = None for tokens in line_tokens: # Now, process the whole 'species' thing spec_custom = tokens[0].split(':', 1) elem = spec_custom[0] if len(spec_custom) > 1 and custom_species is None: # Add it to the custom info! custom_species = list(aargs['symbols']) if custom_species is not None: custom_species.append(tokens[0]) aargs['symbols'].append(elem) aargs[pos_type].append([float(p) * u for p in tokens[1:4]]) # Now for the additional information info = ' '.join(tokens[4:]) info = parse_info(info) for k in add_info: add_info_arrays[k] += [info.get(k, add_info[k][1])] # Now on to the species potentials... if 'species_pot' in celldict: lines = celldict.pop('species_pot')[0].split('\n') line_tokens = [l.split() for l in lines] for tokens in line_tokens: if len(tokens) == 1: # It's a library all_spec = (set(custom_species) if custom_species is not None else set(aargs['symbols'])) for s in all_spec: calc.cell.species_pot = (s, tokens[0]) else: calc.cell.species_pot = tuple(tokens[:2]) # Ionic constraints raw_constraints = {} if 'ionic_constraints' in celldict: lines = celldict.pop('ionic_constraints')[0].split('\n') line_tokens = [l.split() for l in lines] for tokens in line_tokens: if not len(tokens) == 6: continue _, species, nic, x, y, z = tokens # convert xyz to floats x = float(x) y = float(y) z = float(z) nic = int(nic) if (species, nic) not in raw_constraints: raw_constraints[(species, nic)] = [] raw_constraints[(species, nic)].append(np.array([x, y, z])) # Symmetry operations if 'symmetry_ops' in celldict: lines = celldict.pop('symmetry_ops')[0].split('\n') line_tokens = [l.split() for l in lines] # Read them in blocks of four blocks = np.array(line_tokens).astype(float) if (len(blocks.shape) != 2 or blocks.shape[1] != 3 or blocks.shape[0] % 4 != 0): warnings.warn('Warning: could not parse SYMMETRY_OPS' ' block properly, skipping') else: blocks = blocks.reshape((-1, 4, 3)) rotations = blocks[:, :3] translations = blocks[:, 3] # Regardless of whether we recognize them, store these calc.cell.symmetry_ops = (rotations, translations) # Anything else that remains, just add it to the cell object: for k, (val, otype) in celldict.items(): try: if otype == 'block': val = val.split('\n') # Avoids a bug for one-line blocks calc.cell.__setattr__(k, val) except Exception as e: raise RuntimeError('Problem setting calc.cell.%s = %s: %s' % (k, val, e)) # Get the relevant additional info aargs['magmoms'] = np.array(add_info_arrays['SPIN']) # SPIN or MAGMOM are alternative keywords aargs['magmoms'] = np.where(aargs['magmoms'] != 0, aargs['magmoms'], add_info_arrays['MAGMOM']) labels = np.array(add_info_arrays['LABEL']) aargs['calculator'] = calc atoms = ase.Atoms(**aargs) # Spacegroup... if find_spg: # Try importing spglib try: import spglib except ImportError: warnings.warn('spglib not found installed on this system - ' 'automatic spacegroup detection is not possible') spglib = None if spglib is not None: symmd = spglib.get_symmetry_dataset(atoms_to_spglib_cell(atoms)) atoms_spg = Spacegroup(int(symmd['number'])) atoms.info['spacegroup'] = atoms_spg atoms.new_array('castep_labels', labels) if custom_species is not None: atoms.new_array('castep_custom_species', np.array(custom_species)) fixed_atoms = [] constraints = [] for (species, nic), value in raw_constraints.items(): absolute_nr = atoms.calc._get_absolute_number(species, nic) if len(value) == 3: # Check if they are linearly independent if np.linalg.det(value) == 0: warnings.warn( 'Error: Found linearly dependent constraints attached ' 'to atoms %s' % (absolute_nr)) continue fixed_atoms.append(absolute_nr) elif len(value) == 2: direction = np.cross(value[0], value[1]) # Check if they are linearly independent if np.linalg.norm(direction) == 0: warnings.warn( 'Error: Found linearly dependent constraints attached ' 'to atoms %s' % (absolute_nr)) continue constraint = ase.constraints.FixedLine(a=absolute_nr, direction=direction) constraints.append(constraint) elif len(value) == 1: constraint = ase.constraints.FixedPlane(a=absolute_nr, direction=np.array( value[0], dtype=np.float32)) constraints.append(constraint) else: warnings.warn('Error: Found %s statements attached to atoms %s' % (len(value), absolute_nr)) # we need to sort the fixed atoms list in order not to raise an assertion # error in FixAtoms if fixed_atoms: constraints.append( ase.constraints.FixAtoms(indices=sorted(fixed_atoms))) if constraints: atoms.set_constraint(constraints) atoms.calc.atoms = atoms atoms.calc.push_oldstate() return atoms
def refine_symmetry(atoms, symprec=0.01, verbose=False): """ Refine symmetry of an Atoms object Parameters ---------- atoms - input Atoms object symprec - symmetry precicion verbose - if True, print out symmetry information before and after Returns ------- spglib dataset """ import spglib # test orig config with desired tol dataset = check_symmetry(atoms, symprec, verbose=verbose) # set actual cell to symmetrized cell vectors by copying # transformed and rotated standard cell std_cell = dataset['std_lattice'] trans_std_cell = dataset['transformation_matrix'].T @ std_cell rot_trans_std_cell = trans_std_cell @ dataset['std_rotation_matrix'] atoms.set_cell(rot_trans_std_cell, True) # get new dataset and primitive cell dataset = check_symmetry(atoms, symprec=symprec, verbose=verbose) res = spglib.find_primitive(atoms_to_spglib_cell(atoms), symprec=symprec) prim_cell, prim_scaled_pos, prim_types = res # calculate offset between standard cell and actual cell std_cell = dataset['std_lattice'] rot_std_cell = std_cell @ dataset['std_rotation_matrix'] rot_std_pos = dataset['std_positions'] @ rot_std_cell pos = atoms.get_positions() dp0 = (pos[list(dataset['mapping_to_primitive']).index(0)] - rot_std_pos[list(dataset['std_mapping_to_primitive']).index(0)]) # create aligned set of standard cell positions to figure out mapping rot_prim_cell = prim_cell @ dataset['std_rotation_matrix'] inv_rot_prim_cell = np.linalg.inv(rot_prim_cell) aligned_std_pos = rot_std_pos + dp0 # find ideal positions from position of corresponding std cell atom + # integer_vec . primitive cell vectors # here we are assuming that primitive vectors returned by find_primitive # are compatible with std_lattice returned by get_symmetry_dataset mapping_to_primitive = list(dataset['mapping_to_primitive']) std_mapping_to_primitive = list(dataset['std_mapping_to_primitive']) pos = atoms.get_positions() for i_at in range(len(atoms)): std_i_at = std_mapping_to_primitive.index(mapping_to_primitive[i_at]) dp = aligned_std_pos[std_i_at] - pos[i_at] dp_s = dp @ inv_rot_prim_cell pos[i_at] = (aligned_std_pos[std_i_at] - np.round(dp_s) @ rot_prim_cell) atoms.set_positions(pos) # test final config with tight tol return check_symmetry(atoms, symprec=1e-4, verbose=verbose)