Exemplo n.º 1
0
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)
Exemplo n.º 2
0
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
Exemplo n.º 3
0
 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
Exemplo n.º 4
0
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
Exemplo n.º 5
0
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)