def _get_spacegroup(atoms, symprec=1e-5, center=None): """ASE implementation of get_spacegroup. """ # we try all available spacegroups from 1 to 230, backwards # a Space group is the collection of all symmetry operations which lets the # unit cell invariant. found = None positions = atoms.get_scaled_positions(wrap=True) # in the lattice frame # make sure we are insensitive to translation. this choice is arbitrary and # could lead to a 'slightly' wrong guess for the Space group, e.g. do not # guess centro-symmetry. if center: try: positions -= positions[center] except IndexError: pass # search space groups from the highest symmetry to the lowest # retain the first match for nb in range(230, 0, -1): sg = Spacegroup(nb) # # now we scan all atoms in the cell and look for equivalent sites try: sites, kinds = sg.equivalent_sites(positions, onduplicates='keep', symprec=symprec) except TypeError: # ASE <= 3.9 sites, kinds = sg.equivalent_sites(positions, ondublicates='keep', symprec=symprec) # # the equivalent sites should match all other atom locations in the cell # as the spacegroup transforms the unit cell in itself if len(sites) == len(positions): # store the space group into the list found = sg break return found
def read(self): """ Read data from the CIF file """ with open(self.filename, 'r') as f: ciffile = ReadCif(f) for block in ciffile: spacegroup = Spacegroup(int(block['_space_group_IT_number'])) cellpar = np.array([ block['_cell_length_a'], block['_cell_length_b'], block['_cell_length_c'], block['_cell_angle_alpha'], block['_cell_angle_beta'], block['_cell_angle_gamma'], ], dtype=float) try: site_labels = block['_atom_site_label'] except KeyError: print('Could not get site labels from cif file') try: occupancies = np.array(block['_atom_site_occupancy'], dtype=float) except KeyError: print('Could not get occupancies from cif file') try: fract_x = np.array(block['_atom_site_fract_x'], dtype=float) fract_y = np.array(block['_atom_site_fract_y'], dtype=float) fract_z = np.array(block['_atom_site_fract_z'], dtype=float) except KeyError: warn( 'Could not get fractional coordinates from cif file, getting absolute coordinates instead.' ) try: x = np.array(block['_atom_site_cartn_x'], dtype=float) y = np.array(block['_atom_site_cartn_y'], dtype=float) z = np.array(block['_atom_site_cartn_z'], dtype=float) except KeyError: warn( 'Could not get absolute coordinates from cif file') x = [np.nan] * len(block) y = [np.nan] * len(block) z = [np.nan] * len(block) finally: fract_x = x / cellpar[0] fract_y = y / cellpar[1] fract_z = z / cellpar[2] else: x = fract_x * cellpar[0] y = fract_y * cellpar[1] z = fract_y * cellpar[2] finally: x = x.T y = y.T z = z.T fract_x = fract_x.T fract_y = fract_y.T fract_z = fract_z.T positions = np.array([x, y, z]) fractional_positions = np.array( [fract_x, fract_y, fract_z]) try: symbols = block['_atom_site_type_symbol'] except KeyError: print( 'Could not get atom site chemical symbols from cif file' ) try: dwf = block['_atom_site_B_iso_or_equiv'] except KeyError: print('Could not get Debye-Waller factors from cif file') basis_atoms = [] for label, occ, fx, fy, fz, symbol, B in zip( site_labels, occupancies, fractional_positions[0], fractional_positions[1], fractional_positions[2], symbols, dwf): atom = CIFAtom(cellpar, symbol=symbol, occupancy=occ, fractional_position=(fx, fy, fz), dwf=B, site_label=label) basis_atoms.append(atom) atoms = [] for atom in basis_atoms: equivalent_sites, kinds = spacegroup.equivalent_sites( atom.fractional_position, onduplicates='warn', occupancies=atom.occupancy) for site in equivalent_sites: position = site * cellpar[:3] equivalent_atom = CIFAtom(cellpar, fractional_position=site, site_label=atom.site_label, symbol=atom.symbol, dwf=atom.dwf, occupancy=atom.occupancy) atoms.append(equivalent_atom) self.atoms = atoms self.crystal = Crystal(atoms, cellpar)
def _analyze(self): """Analyze the topology to cut the fragments out.""" # separate the dummies from the rest logger.debug("Analyzing fragments of topology {0}.".format(self.name)) numbers = numpy.asarray(self.atoms.get_atomic_numbers()) Xis = numpy.where(numbers == 0)[0] Ais = numpy.where(numbers > 0)[0] # setup the tags tags = numpy.zeros(len(self.atoms)) tags[Xis] = Xis + 1 self.atoms.set_tags(tags) tags = self.atoms.get_tags() # analyze # first build the neighborlist cutoffs = self._get_cutoffs(Xis=Xis, Ais=Ais) neighborlist = NeighborList(cutoffs=cutoffs, bothways=True, self_interaction=False, skin=0.0) neighborlist.update(self.atoms) # iterate over non-dummies to find dummy neighbors for ai in Ais: # get indices and offsets of dummies only! ni, no = neighborlist.get_neighbors(ai) ni, no = zip(*[(idx, off) for idx, off in list(zip(ni, no)) if idx in Xis]) ni = numpy.asarray(ni) no = numpy.asarray(no) # get absolute positions, no offsets positions = self.atoms.positions[ni] + no.dot(self.atoms.cell) # create the Atoms object fragment = Atoms("X" * len(ni), positions, tags=tags[ni]) # calculate the point group properties max_order = min(8, len(ni)) shape = symmetry.get_symmetry_elements(mol=fragment.copy(), max_order=max_order) pg = symmetry.PointGroup(mol=fragment.copy(), tol=0.1) # save that info self.fragments[ai] = fragment self.shapes[ai] = shape self.pointgroups[ai] = pg.schoenflies # now getting the equivalent sites using the Spacegroup object sg = self.atoms.info["spacegroup"] if not isinstance(sg, Spacegroup): sg = Spacegroup(sg) scaled_positions = self.atoms.get_scaled_positions() seen_indices = [] symbols = numpy.array(self.atoms.get_chemical_symbols()) for ai in Ais: if ai in seen_indices: continue sites, _ = sg.equivalent_sites(scaled_positions[ai]) these_indices = [] for site in sites: norms = numpy.linalg.norm(scaled_positions - site, axis=1) if norms.min() < 1e-6: these_indices.append(norms.argmin()) # take pbc into account norms = numpy.abs(norms - 1.0) if norms.min() < 1e-6: these_indices.append(norms.argmin()) these_indices = [idx for idx in these_indices if idx in Ais] seen_indices += these_indices self.equivalent_sites.append(these_indices) logger.info("{es} equivalent sites kinds.".format( es=len(self.equivalent_sites))) return None
def crystal(symbols=None, basis=None, occupancies=None, spacegroup=1, setting=1, cell=None, cellpar=None, ab_normal=(0, 0, 1), a_direction=None, size=(1, 1, 1), onduplicates='warn', symprec=0.001, pbc=True, primitive_cell=False, **kwargs): """Create an Atoms instance for a conventional unit cell of a space group. Parameters: symbols : str | sequence of str | sequence of Atom | Atoms Element symbols of the unique sites. Can either be a string formula or a sequence of element symbols. E.g. ('Na', 'Cl') and 'NaCl' are equivalent. Can also be given as a sequence of Atom objects or an Atoms object. basis : list of scaled coordinates Positions of the unique sites corresponding to symbols given either as scaled positions or through an atoms instance. Not needed if *symbols* is a sequence of Atom objects or an Atoms object. occupancies : list of site occupancies Occupancies of the unique sites. Defaults to 1.0 and thus no mixed occupancies are considered if not explicitly asked for. If occupancies are given, the most dominant species will yield the atomic number. spacegroup : int | string | Spacegroup instance Space group given either as its number in International Tables or as its Hermann-Mauguin symbol. setting : 1 | 2 Space group setting. cell : 3x3 matrix Unit cell vectors. cellpar : [a, b, c, alpha, beta, gamma] Cell parameters with angles in degree. Is not used when `cell` is given. ab_normal : vector Is used to define the orientation of the unit cell relative to the Cartesian system when `cell` is not given. It is the normal vector of the plane spanned by a and b. a_direction : vector Defines the orientation of the unit cell a vector. a will be parallel to the projection of `a_direction` onto the a-b plane. size : 3 positive integers How many times the conventional unit cell should be repeated in each direction. onduplicates : 'keep' | 'replace' | 'warn' | 'error' Action if `basis` contain symmetry-equivalent positions: 'keep' - ignore additional symmetry-equivalent positions 'replace' - replace 'warn' - like 'keep', but issue an UserWarning 'error' - raises a SpacegroupValueError symprec : float Minimum "distance" betweed two sites in scaled coordinates before they are counted as the same site. pbc : one or three bools Periodic boundary conditions flags. Examples: True, False, 0, 1, (1, 1, 0), (True, False, False). Default is True. primitive_cell : bool Wheter to return the primitive instead of the conventional unit cell. Keyword arguments: All additional keyword arguments are passed on to the Atoms constructor. Currently, probably the most useful additional keyword arguments are `info`, `constraint` and `calculator`. Examples: Two diamond unit cells (space group number 227) >>> diamond = crystal('C', [(0,0,0)], spacegroup=227, ... cellpar=[3.57, 3.57, 3.57, 90, 90, 90], size=(2,1,1)) >>> ase.view(diamond) # doctest: +SKIP A CoSb3 skutterudite unit cell containing 32 atoms >>> skutterudite = crystal(('Co', 'Sb'), ... basis=[(0.25,0.25,0.25), (0.0, 0.335, 0.158)], ... spacegroup=204, cellpar=[9.04, 9.04, 9.04, 90, 90, 90]) >>> len(skutterudite) 32 """ sg = Spacegroup(spacegroup, setting) if (not isinstance(symbols, basestring) and hasattr(symbols, '__getitem__') and len(symbols) > 0 and isinstance(symbols[0], ase.Atom)): symbols = ase.Atoms(symbols) if isinstance(symbols, ase.Atoms): basis = symbols symbols = basis.get_chemical_symbols() if isinstance(basis, ase.Atoms): basis_coords = basis.get_scaled_positions() if cell is None and cellpar is None: cell = basis.cell if symbols is None: symbols = basis.get_chemical_symbols() else: basis_coords = np.array(basis, dtype=float, copy=False, ndmin=2) if occupancies is not None: occupancies_dict = {} for index, coord in enumerate(basis_coords): # Compute all distances and get indices of nearest atoms dist = spatial.distance.cdist(coord.reshape(1, 3), basis_coords) indices_dist = np.flatnonzero(dist < symprec) occ = {symbols[index]: occupancies[index]} # Check nearest and update occupancy for index_dist in indices_dist: if index == index_dist: continue else: occ.update({symbols[index_dist]: occupancies[index_dist]}) occupancies_dict[index] = occ.copy() sites, kinds = sg.equivalent_sites(basis_coords, onduplicates=onduplicates, symprec=symprec) # this is needed to handle deuterium masses masses = None if 'masses' in kwargs: masses = kwargs['masses'][kinds] del kwargs['masses'] symbols = parse_symbols(symbols) if occupancies is None: symbols = [symbols[i] for i in kinds] else: # make sure that we put the dominant species there symbols = [ sorted(occupancies_dict[i].items(), key=lambda x: x[1])[-1][0] for i in kinds ] if cell is None: cell = cellpar_to_cell(cellpar, ab_normal, a_direction) info = dict(spacegroup=sg) if primitive_cell: info['unit_cell'] = 'primitive' else: info['unit_cell'] = 'conventional' if 'info' in kwargs: info.update(kwargs['info']) if occupancies is not None: info['occupancy'] = occupancies_dict kwargs['info'] = info atoms = ase.Atoms(symbols, scaled_positions=sites, cell=cell, pbc=pbc, masses=masses, **kwargs) # if all occupancies are 1, no partial occupancy present if occupancies: if not all([occ == 1 for occ in occupancies]): # use tags to identify sites, and in particular the occupancy atoms.set_tags(kinds) if isinstance(basis, ase.Atoms): for name in basis.arrays: if not atoms.has(name): array = basis.get_array(name) atoms.new_array(name, [array[i] for i in kinds], dtype=array.dtype, shape=array.shape[1:]) if primitive_cell: from ase.build import cut prim_cell = sg.scaled_primitive_cell # Preserve calculator if present: calc = atoms.calc atoms = cut(atoms, a=prim_cell[0], b=prim_cell[1], c=prim_cell[2]) atoms.calc = calc if size != (1, 1, 1): atoms = atoms.repeat(size) return atoms
def crystal(symbols=None, basis=None, spacegroup=1, setting=1, cell=None, cellpar=None, ab_normal=(0, 0, 1), a_direction=None, size=(1, 1, 1), onduplicates='warn', symprec=0.001, pbc=True, primitive_cell=False, **kwargs): """Create an Atoms instance for a conventional unit cell of a space group. Parameters: symbols : str | sequence of str | sequence of Atom | Atoms Element symbols of the unique sites. Can either be a string formula or a sequence of element symbols. E.g. ('Na', 'Cl') and 'NaCl' are equivalent. Can also be given as a sequence of Atom objects or an Atoms object. basis : list of scaled coordinates Positions of the unique sites corresponding to symbols given either as scaled positions or through an atoms instance. Not needed if *symbols* is a sequence of Atom objects or an Atoms object. spacegroup : int | string | Spacegroup instance Space group given either as its number in International Tables or as its Hermann-Mauguin symbol. setting : 1 | 2 Space group setting. cell : 3x3 matrix Unit cell vectors. cellpar : [a, b, c, alpha, beta, gamma] Cell parameters with angles in degree. Is not used when `cell` is given. ab_normal : vector Is used to define the orientation of the unit cell relative to the Cartesian system when `cell` is not given. It is the normal vector of the plane spanned by a and b. a_direction : vector Defines the orientation of the unit cell a vector. a will be parallel to the projection of `a_direction` onto the a-b plane. size : 3 positive integers How many times the conventional unit cell should be repeated in each direction. onduplicates : 'keep' | 'replace' | 'warn' | 'error' Action if `basis` contain symmetry-equivalent positions: 'keep' - ignore additional symmetry-equivalent positions 'replace' - replace 'warn' - like 'keep', but issue an UserWarning 'error' - raises a SpacegroupValueError symprec : float Minimum "distance" betweed two sites in scaled coordinates before they are counted as the same site. pbc : one or three bools Periodic boundary conditions flags. Examples: True, False, 0, 1, (1, 1, 0), (True, False, False). Default is True. primitive_cell : bool Wheter to return the primitive instead of the conventional unit cell. Keyword arguments: All additional keyword arguments are passed on to the Atoms constructor. Currently, probably the most useful additional keyword arguments are `info`, `constraint` and `calculator`. Examples: Two diamond unit cells (space group number 227) >>> diamond = crystal('C', [(0,0,0)], spacegroup=227, ... cellpar=[3.57, 3.57, 3.57, 90, 90, 90], size=(2,1,1)) >>> ase.view(diamond) # doctest: +SKIP A CoSb3 skutterudite unit cell containing 32 atoms >>> skutterudite = crystal(('Co', 'Sb'), ... basis=[(0.25,0.25,0.25), (0.0, 0.335, 0.158)], ... spacegroup=204, cellpar=[9.04, 9.04, 9.04, 90, 90, 90]) >>> len(skutterudite) 32 """ sg = Spacegroup(spacegroup, setting) if (not isinstance(symbols, str) and hasattr(symbols, '__getitem__') and len(symbols) > 0 and isinstance(symbols[0], ase.Atom)): symbols = ase.Atoms(symbols) if isinstance(symbols, ase.Atoms): basis = symbols symbols = basis.get_chemical_symbols() if isinstance(basis, ase.Atoms): basis_coords = basis.get_scaled_positions() if cell is None and cellpar is None: cell = basis.cell if symbols is None: symbols = basis.get_chemical_symbols() else: basis_coords = np.array(basis, dtype=float, copy=False, ndmin=2) sites, kinds = sg.equivalent_sites(basis_coords, onduplicates=onduplicates, symprec=symprec) symbols = parse_symbols(symbols) symbols = [symbols[i] for i in kinds] if cell is None: cell = cellpar_to_cell(cellpar, ab_normal, a_direction) info = dict(spacegroup=sg) if primitive_cell: info['unit_cell'] = 'primitive' else: info['unit_cell'] = 'conventional' if 'info' in kwargs: info.update(kwargs['info']) kwargs['info'] = info atoms = ase.Atoms(symbols, scaled_positions=sites, cell=cell, pbc=pbc, **kwargs) if isinstance(basis, ase.Atoms): for name in basis.arrays: if not atoms.has(name): array = basis.get_array(name) atoms.new_array(name, [array[i] for i in kinds], dtype=array.dtype, shape=array.shape[1:]) if primitive_cell: from ase.build import cut prim_cell = sg.scaled_primitive_cell atoms = cut(atoms, a=prim_cell[0], b=prim_cell[1], c=prim_cell[2]) if size != (1, 1, 1): atoms = atoms.repeat(size) return atoms