Beispiel #1
0
def get_primitive_cell(atoms, tol=1e-8):
    """Atoms object interface with spglib primitive cell finder:
    https://atztogo.github.io/spglib/python-spglib.html#python-spglib

    Parameters
    ----------
    atoms : object
        Atoms object to search for a primitive unit cell.
    tol : float
        Tolerance for floating point rounding errors.

    Returns
    -------
    primitive cell : object
        The primitive unit cell returned by spglib if one is found.
    """
    lattice = atoms.cell
    positions = atoms.get_scaled_positions()
    numbers = atoms.get_atomic_numbers()

    cell = (lattice, positions, numbers)
    cell = spglib.find_primitive(cell, symprec=tol)

    if cell is None:
        return None

    _lattice, _positions, _numbers = cell
    atoms = Gratoms(symbols=_numbers, cell=_lattice, pbc=atoms.pbc)
    atoms.set_scaled_positions(_positions)

    return atoms
Beispiel #2
0
    def molecule_search(self,
                        element_pool={
                            'C': 2,
                            'H': 6
                        },
                        load_molecules=True,
                        multiple_bond_search=False):
        """Return the enumeration of molecules which can be produced from
        the specified atoms.

        Parameters
        ----------
        element_pool : dict
            Atomic symbols keys paired with the maximum number of that atom.
        load_molecules : bool
            Load any existing molecules from the database.
        multiple_bond_search : bool
            Allow atoms to form bonds with other atoms in the molecule.
        """
        numbers = np.zeros(len(self.base_valence))
        for k, v in element_pool.items():
            numbers[chemical_symbols.index(k)] = v

        self.element_pool = numbers
        self.multiple_bond_search = multiple_bond_search

        molecules = {}
        if load_molecules:
            self.molecules = self.load_molecules(binned=True)

        search_molecules = []
        for el, cnt in enumerate(self.element_pool):

            if cnt == 0:
                continue

            molecule = Gratoms(symbols=[el])
            molecule.nodes[0]['valence'] = self.base_valence[el]

            search_molecules += [molecule]

            comp_tag, bond_tag = molecule.get_chemical_tags()
            if comp_tag not in self.molecules:
                molecules[comp_tag] = {bond_tag: [molecule]}

        for molecule in search_molecules:

            # Recusive use of molecules
            new_molecules, molecules = self._branch_molecule(
                molecule, molecules)

            search_molecules += new_molecules

        self.save_molecules(molecules)
Beispiel #3
0
def to_gratoms(atoms):
    """Convert and atom object to a gratoms object."""
    gratoms = Gratoms(numbers=atoms.numbers,
                      positions=atoms.positions,
                      pbc=atoms.pbc,
                      cell=atoms.cell)

    if atoms.constraints:
        gratoms.set_constraint(atoms.constraints)

    return gratoms
Beispiel #4
0
    def root_surface(self, slab, root, vector=None):
        """Creates a cell from a primitive cell that repeats along the x and y
        axis in a way consistent with the primitive cell, that has been cut
        to have a side length of *root*.

        *primitive cell* should be a primitive 2d cell of your slab, repeated
        as needed in the z direction.

        *root* should be determined using an analysis tool such as the
        root_surface_analysis function, or prior knowledge. It should always
        be a whole number as it represents the number of repetitions.
        """
        atoms = slab.copy()

        xynorm = norm(atoms.cell[0:2, 0:2], axis=1)
        vectors = atoms.cell[0:2, 0:2] / xynorm[0]
        mvectors = norm(vectors, axis=1)

        vnorm = norm(vector)
        angle = -np.arctan2(vector[1], vector[0])
        scale = vnorm / mvectors[0]

        maxn = int(np.ceil(scale)) * 2
        atoms *= (maxn, maxn, 1)
        atoms.cell[0:2, 0:2] = slab.cell[0:2, 0:2] * scale
        atoms.rotate((angle * 180) / np.pi, 'z')

        coords = atoms.get_scaled_positions()
        original_index = np.arange(coords.shape[0])
        periodic_match = original_index.copy()
        for i, j in enumerate(periodic_match):
            if i != j:
                continue

            matched = geometry.matching_sites(coords[i], coords)
            periodic_match[matched] = i

        repeated = np.where(periodic_match != original_index)
        del atoms[repeated]

        atoms.wrap()
        ind = np.lexsort(
            (atoms.positions[:, 0], atoms.positions[:, 1], atoms.positions[:,
                                                                           2]))
        atoms = Gratoms(positions=atoms.positions[ind],
                        numbers=atoms.numbers[ind],
                        cell=atoms.cell,
                        pbc=atoms.pbc,
                        tags=atoms.get_tags())

        assert (len(atoms) == len(slab) * root)

        return atoms
Beispiel #5
0
    def id_adsorbates(self, classifier='radial', return_atoms=False):
        """Return a list of Gratoms objects for each adsorbate
        classified on a surface. Requires classification of adsorbate
        atoms.

        Parameters
        ----------
        classifier : str
            Classification technique to identify individual adsorbates.

            'radial':
            Use standard cutoff distances to identify neighboring atoms.

        return_atoms : bool
            Return Gratoms objects instead of adsorbate indices.

        Returns
        -------
        adsorabtes : list (n,)
            Adsorbate indices of adsorbates in unit cell.
        """
        atoms = self.atoms.copy()

        # Remove the slab atoms
        ads_atoms = self.ads_atoms
        if ads_atoms is None:
            ads_atoms = self.id_adsorbate_atoms()

        if classifier == 'radial':
            con = utils.get_cutoff_neighbors(atoms)
            ads_con = con[ads_atoms][:, ads_atoms]
            G = nx.Graph()
            G.add_nodes_from(ads_atoms)
            edges = utils.connectivity_to_edges(ads_con, indices=ads_atoms)
            G.add_weighted_edges_from(edges, weight='bonds')
            SG = nx.connected_component_subgraphs(G)

            adsorabtes = []
            for sg in SG:
                nodes = list(sg.nodes)
                if return_atoms:
                    edges = list(sg.edges)
                    ads = Gratoms(numbers=atoms.numbers[nodes],
                                  positions=atoms.positions[nodes],
                                  edges=edges)
                    ads.center(vacuum=5)
                else:
                    ads = nodes
                adsorabtes += [ads]

        return adsorabtes
Beispiel #6
0
def test_gratoms():
    edges = [(0, 1), (0, 2)]
    atoms = Gratoms(edges=edges)

    for n in atoms.edges:
        assert (n in edges)

    mol = molecule('H2O')
    atoms = to_gratoms(mol)
    atoms.graph.add_edges_from([(0, 1), (0, 2)])

    sym_test = atoms.get_neighbor_symbols(0)
    assert (sym_test.tolist() == ['H', 'H'])

    test_tags = atoms.get_chemical_tags(rank=1)
    assert (test_tags == '2,0,0,0,0,0,0,1')

    test_comp, test_bonds = atoms.get_chemical_tags()
    assert (test_comp == '2,0,0,0,0,0,0,1')
    assert (test_bonds == '4,0,0,0,0,0,0,3')

    atoms.set_constraint(FixAtoms(indices=[0]))
    del atoms[2]
    assert (len(atoms) == 2)

    nx.set_node_attributes(atoms.graph, name='valence', values={0: 1, 1: 0})

    test_nodes = atoms.get_unsaturated_nodes(screen=1)
    assert (test_nodes == [0])
Beispiel #7
0
def rdkit_to_gratoms(rdkG, name=None, confid=-1):
    """TODO: conserve 3D positions if present."""
    block = Chem.MolToMolBlock(rdkG, confId=confid)

    positions = np.empty((rdkG.GetNumAtoms(), 3))
    n = rdkG.GetNumAtoms()

    symbols, edges, valence = [], [], {}
    for i, atom in enumerate(block.split('\n')[4:n + 4]):
        data = atom.split()
        positions[i] = np.array(data[:3], dtype=float)
        symbols += [data[3]]
        valence.update({i: int(data[9])})

    for i, bond in enumerate(block.split('\n')[n + 4:]):
        data = bond.split()
        if data[0] == 'M':
            break
        data = np.array(data, dtype=int)

        edges += [(data[0] - 1, data[1] - 1, {'bonds': data[2]})]

    gratoms = Gratoms(symbols, positions)
    gratoms.graph.name = name
    nx.set_node_attributes(gratoms.graph, values=valence, name='valence')
    gratoms.graph.add_edges_from(edges)

    return gratoms
Beispiel #8
0
    def test_edge_addition(self):
        """Test that edges are added correctly."""
        edges = [(0, 1), (0, 2)]
        atoms = Gratoms(edges=edges)

        for n in atoms.edges:
            assert (n in edges)
Beispiel #9
0
    def _build_basis(self, bulk):
        """Get the basis unit cell from bulk unit cell. This
        basis is effectively the same as the bulk, but rotated such
        that the z-axis is aligned with the surface termination.

        The basis is stored separately from the slab generated and is
        only intended for internal use.

        Returns
        -------
        basis : atoms object
            The basis slab corresponding to the provided bulk.
        """
        del bulk.constraints

        if len(np.nonzero(self.miller_index)[0]) == 1:
            mi = max(np.abs(self.miller_index))
            basis = circulant(self.miller_index[::-1] / mi).astype(int)
        else:
            h, k, l = self.miller_index
            p, q = ext_gcd(k, l)
            a1, a2, a3 = bulk.cell

            k1 = np.dot(p * (k * a1 - h * a2) + q * (l * a1 - h * a3),
                        l * a2 - k * a3)
            k2 = np.dot(l * (k * a1 - h * a2) - k * (l * a1 - h * a3),
                        l * a2 - k * a3)

            if abs(k2) > self.tol:
                i = -int(np.round(k1 / k2))
                p, q = p + i * l, q - i * k

            a, b = ext_gcd(p * k + q * l, h)

            c1 = (p * k + q * l, -p * h, -q * h)
            c2 = np.array((0, l, -k)) // abs(gcd(l, k))
            c3 = (b, a * p, a * q)

            basis = np.array([c1, c2, c3])

        basis_atoms = Gratoms(positions=bulk.positions,
                              numbers=bulk.get_atomic_numbers(),
                              cell=bulk.cell,
                              pbc=True)

        scaled = solve(basis.T, basis_atoms.get_scaled_positions().T).T
        scaled -= np.floor(scaled + self.tol)
        basis_atoms.set_scaled_positions(scaled)
        basis_atoms.set_cell(np.dot(basis, basis_atoms.cell), scale_atoms=True)

        a1, a2, a3 = basis_atoms.cell
        n1 = np.cross(a1, a2)
        a3 = n1 / norm(n1)
        rotate(basis_atoms, a3, (0, 0, 1), a1, (1, 0, 0))

        return basis_atoms
Beispiel #10
0
def get_spglib_cell(atoms, primitive=False, idealize=True, tol=1e-5):
    """Atoms object interface with spglib primitive cell finder:
    https://atztogo.github.io/spglib/python-spglib.html#python-spglib

    Parameters
    ----------
    atoms : object
        Atoms object to search for a primitive unit cell.
    primitive : bool
        Reduce the atoms object into a primitive form.
    idealize : bool
        Convert the cell into the spglib standardized form.
    tol : float
        Tolerance for floating point rounding errors.

    Returns
    -------
    primitive cell : object
        The primitive unit cell returned by spglib if one is found.
    """
    lattice = atoms.cell
    positions = atoms.get_scaled_positions()
    numbers = atoms.get_atomic_numbers()

    cell = (lattice, positions, numbers)
    cell = spglib.standardize_cell(cell,
                                   to_primitive=primitive,
                                   no_idealize=~idealize,
                                   symprec=tol)

    if cell is None:
        return atoms

    _lattice, _positions, _numbers = cell
    atoms = Gratoms(symbols=_numbers, cell=_lattice, pbc=atoms.pbc)
    atoms.set_scaled_positions(_positions)

    return atoms
Beispiel #11
0
def hydrogenate(atoms, bins, copy=True):
    """Add hydrogens to a gratoms object via provided bins"""
    h_index = len(atoms)

    edges = []
    for i, j in enumerate(bins):
        for _ in range(j):
            edges += [(i, h_index)]
            h_index += 1

    if copy:
        atoms = atoms.copy()
    atoms += Gratoms('H{}'.format(sum(bins)))
    atoms.graph.add_edges_from(edges)

    return atoms
Beispiel #12
0
    def structure_to_atoms(
            self,
            structure,
            parameters=None):
        """Return an atoms object for a given structure from the
        database.

        Parameters
        ----------
        structure : Structure object
            A structure from the CatFlow database.
        parameters : dict
            Parameters used to perform the calculation. If None, they
            will be retrieved from the database.

        Returns
        -------
        atoms : Gratoms object
            Atomic structure.
        """
        if parameters is None:
            calculator_name, parameters = self.cursor.query(
                Calculator.name, Calculator.parameters).\
                filter(Calculator.id == structure.calculator_id).one()
        else:
            calculator_name = parameters.pop('calculator_name')
        calculator = utils.str_to_class(calculator_name)

        atoms = Gratoms(
            numbers=structure.numbers,
            positions=structure.positions,
            pbc=structure.pbc,
            cell=structure.cell)

        results = {}
        for prop_name in utils.supported_properties:
            prop = getattr(structure, prop_name)
            if isinstance(prop, list):
                prop = np.array(prop)
            results[prop_name] = prop

        calculator = calculator(atoms, **parameters)
        calculator.set_results(results)

        return atoms
Beispiel #13
0
    def load_molecules(self, ids=None, binned=False):
        """Load 2D molecule graphs from the database.

        Parameters
        ----------
        binned : bool
            Return the molecules in sub-dictionaries of their
            corresponding composition and bonding tags.

        Returns
        -------
        molecules : dict
            All molecules present in the database.
        """
        if isinstance(ids, list):
            ids = ','.join([str(_) for _ in ids])

        cmd = """SELECT m.molecule_pid,
                        m.comp_tag,
                        m.bond_tag,
                        nodes,
                        bonds
        FROM molecules m LEFT JOIN
        (
         SELECT molecule_id,
         GROUP_CONCAT(node_id || ',' || atom_num || ',' || valence, ';')
         as nodes
         FROM atoms
         GROUP BY molecule_id
        ) a
          ON a.molecule_id = m.molecule_pid LEFT JOIN
        (
         SELECT molecule_id,
         GROUP_CONCAT(node_id1 || ',' || node_id2 || ',' || nbonds, ';')
         as bonds
         FROM bonds
         GROUP BY molecule_id
        ) b
          ON b.molecule_id = m.molecule_pid
        """
        if ids:
            cmd += """WHERE m.molecule_pid IN ({})""".format(ids)

        self.c.execute(cmd)
        fetch = self.c.fetchall()

        molecules = {}

        for index, comp_tag, bond_tag, node_data, edge_data in fetch:

            # Unpacks node, number, and valence
            node_data = np.array([_.split(',') for _ in node_data.split(';')],
                                 dtype=int)

            data, symbols = {}, []
            for node, n, valence in node_data:
                data.update({node: valence})
                symbols += [n]

            molecule = Gratoms(symbols)
            molecule.graph.name = index
            nx.set_node_attributes(molecule.graph, data, 'valence')

            if edge_data:
                edges = np.array([_.split(',') for _ in edge_data.split(';')],
                                 dtype=int)
                molecule.graph.add_weighted_edges_from(edges, weight='bonds')

            if binned:
                if comp_tag not in molecules:
                    molecules[comp_tag] = {}

                if bond_tag not in molecules[comp_tag]:
                    molecules[comp_tag][bond_tag] = []

                molecules[comp_tag][bond_tag] += [molecule]
            else:
                molecules[index] = molecule

        return molecules
Beispiel #14
0
    def align_crystal(self, bulk, miller_index):
        """Return an aligned unit cell from bulk unit cell. This alignment
        rotates the a and b basis vectors to be parallel to the Miller index.

        Parameters
        ----------
        bulk : Atoms object
            Bulk system to be standardized.
        miller_index : list (3,)
            Miller indices to align with the basis vectors.

        Returns
        -------
        new_bulk : Gratoms object
            Standardized bulk unit cell.
        """
        del bulk.constraints

        if len(np.nonzero(miller_index)[0]) == 1:
            mi = max(np.abs(miller_index))
            new_lattice = scipy.linalg.circulant(miller_index[::-1] /
                                                 mi).astype(int)
        else:
            h, k, l = miller_index
            p, q = utils.ext_gcd(k, l)
            a1, a2, a3 = bulk.cell

            k1 = np.dot(p * (k * a1 - h * a2) + q * (l * a1 - h * a3),
                        l * a2 - k * a3)
            k2 = np.dot(l * (k * a1 - h * a2) - k * (l * a1 - h * a3),
                        l * a2 - k * a3)

            if abs(k2) > self.tol:
                i = -int(np.round(k1 / k2))
                p, q = p + i * l, q - i * k

            a, b = utils.ext_gcd(p * k + q * l, h)

            c1 = (p * k + q * l, -p * h, -q * h)
            c2 = np.array((0, l, -k)) // abs(gcd(l, k))
            c3 = (b, a * p, a * q)
            new_lattice = np.array([c1, c2, c3])

        scaled = np.linalg.solve(new_lattice.T,
                                 bulk.get_scaled_positions().T).T
        scaled -= np.floor(scaled + self.tol)

        new_bulk = Gratoms(positions=bulk.positions,
                           numbers=bulk.get_atomic_numbers(),
                           pbc=True)

        if not self.attach_graph:
            del new_bulk._graph

        new_bulk.set_scaled_positions(scaled)
        new_bulk.set_cell(np.dot(new_lattice, bulk.cell), scale_atoms=True)

        # Align the longest of the ab basis vectors with x
        d = np.linalg.norm(new_bulk.cell[:2], axis=1)
        if d[1] > d[0]:
            new_bulk.cell[[0, 1]] = new_bulk.cell[[1, 0]]
        a = new_bulk.cell[0]
        a3 = np.cross(a, new_bulk.cell[1]) / np.max(d)
        rotate(new_bulk, a3, (0, 0, 1), a, (1, 0, 0))

        # Ensure the remaining basis vectors are positive in their
        # corresponding axis
        for i in range(1, 3):
            if new_bulk.cell[i][i] < 0:
                new_bulk.cell[i] *= -1
        new_bulk.wrap(eps=1e-3)

        return new_bulk
Beispiel #15
0
def get_topologies(symbols, saturate=False):
    """Return the possible topologies of a given chemical species

    Parameters
    ----------
    symbols : str
        Atomic symbols to construct the topologies from.
    saturate : bool
        Saturate the molecule with hydrogen based on the
        default.radicals set.

    Returns
    -------
    """
    num, cnt = utils.get_atomic_numbers(symbols, True)
    mcnt = cnt[num != 1]
    mnum = num[num != 1]

    if cnt[num == 1]:
        hcnt = cnt[num == 1][0]
    else:
        hcnt = 0

    elements = np.repeat(mnum, mcnt)
    max_degree = defaults.get('radicals')[elements]
    n = mcnt.sum()

    hmax = int(max_degree.sum() - (n - 1) * 2)
    if hcnt > hmax:
        hcnt = hmax

    if saturate:
        hcnt = hmax

    if n == 1:
        atoms = Gratoms(elements, cell=[1, 1, 1])
        hatoms = hydrogenate(atoms, np.array([hcnt]))
        return [hatoms]
    elif n == 0:
        hatoms = Gratoms('H{}'.format(hcnt))
        if hcnt == 2:
            hatoms.graph.add_edge(0, 1, bonds=1)
        return [hatoms]

    ln = np.arange(n).sum()
    il = np.tril_indices(n, -1)

    backbones, molecules = [], []
    combos = itertools.combinations(np.arange(ln), n - 1)
    for c in combos:
        # Construct the connectivity matrix
        ltm = np.zeros(ln)
        ltm[[c]] = 1

        connectivity = np.zeros((n, n))
        connectivity[il] = ltm
        connectivity = np.maximum(connectivity, connectivity.T)

        degree = connectivity.sum(axis=0)

        # Not fully connected (cyclical subgraph)
        if np.any(degree == 0):
            continue

        # Overbonded atoms.
        remaining_bonds = (max_degree - degree).astype(int)
        if np.any(remaining_bonds < 0):
            continue

        atoms = Gratoms(numbers=elements, edges=connectivity, cell=[1, 1, 1])

        isomorph = False
        for G0 in backbones:
            if atoms.is_isomorph(G0):
                isomorph = True
                break

        if not isomorph:
            backbones += [atoms]

            # The backbone is saturated, do not enumerate
            if hcnt == hmax:
                hatoms = hydrogenate(atoms, remaining_bonds)
                molecules += [hatoms]
                continue

            # Enumerate hydrogens across backbone
            for bins in bin_hydrogen(hcnt, n):
                if not np.all(bins <= remaining_bonds):
                    continue

                hatoms = hydrogenate(atoms, bins)

                isomorph = False
                for G0 in molecules:
                    if hatoms.is_isomorph(G0):
                        isomorph = True
                        break

                if not isomorph:
                    molecules += [hatoms]

    return molecules
Beispiel #16
0
    def load_3d_structures(self, ids=None):
        """Return Gratoms objects from the ReactionNetwork database.

        Parameters
        ----------
        ids : int or list of int
            Identifier of the molecule in the database. If None, return all
            structure.

        Returns
        -------
        images : list
            All Gratoms objects in the database.
        """
        if isinstance(ids, list):
            ids = ','.join([str(_) for _ in ids])

        if ids is None:
            cmd = """SELECT
             GROUP_CONCAT(x_coord || ',' || y_coord || ',' || z_coord, ';'),
             GROUP_CONCAT(symbol, ';')
             FROM positions
             GROUP BY molecule_id
            """

            self.c.execute(cmd)
            fetch = self.c.fetchall()

            images = []
            for i, out in enumerate(fetch):

                symbols = out[1].split(';')
                positions = np.array([_.split(',') for _ in out[0].split(';')],
                                     dtype=float)

                gratoms = Gratoms(symbols, positions)
                gratoms.graph.name = i

                images += [gratoms]

            return images

        else:
            cmd = """SELECT
             GROUP_CONCAT(x_coord || ',' || y_coord || ',' || z_coord, ';'),
             GROUP_CONCAT(symbol, ';')
             FROM positions
             WHERE molecule_id IN ({})
             GROUP BY molecule_id
            """.format(ids)

            self.c.execute(cmd)
            out = self.c.fetchone()

            if out is None:
                raise ValueError('No matching index found')

            symbols = out[1].split(';')
            positions = np.array([_.split(',') for _ in out[0].split(';')],
                                 dtype=float)

            gratoms = Gratoms(symbols, positions)
            gratoms.graph.name = int(ids)

            return gratoms
Beispiel #17
0
    def get_slab(self, size=(1, 1), root=None, iterm=None, primitive=False):
        """Generate a slab object with a certain number of layers.

        Parameters
        ----------
        size : tuple (2,)
            Repeat the x and y lattice vectors by the indicated
            dimensions
        root : int
            Produce a slab with a primitive a1 basis vector multiplied
            by the square root of a provided value. Uses primitive unit cell.
        iterm : int
            A termination index in reference to the list of possible
            terminations.
        primitive : bool
            Whether to reduce the unit cell to its primitive form.

        Returns
        -------
        slab : atoms object
            The modified basis slab produced based on the layer specifications
            given.
        """
        slab = self._basis.copy()

        if iterm:
            if self.unique_terminations is None:
                terminations = self.get_unique_terminations()
            else:
                terminations = self.unique_terminations
            zshift = terminations[iterm]

            slab.translate([0, 0, -zshift])
            slab.wrap(pbc=True)

        # Get the minimum number of layers needed
        zlayers = utils.get_unique_coordinates(slab,
                                               direct=False,
                                               tol=self.tol)

        if self.min_width:
            width = slab.cell[2][2]
            z_repetitions = np.ceil(width / len(zlayers) * self.min_width)
        else:
            z_repetitions = np.ceil(self.layers / len(zlayers))

        slab *= (1, 1, int(z_repetitions))

        if primitive or root:
            if self.vacuum:
                slab.center(vacuum=self.vacuum, axis=2)
            else:
                raise ValueError('Primitive slab generation requires vacuum')

            nslab = utils.get_primitive_cell(slab)

            if nslab is not None:
                slab = nslab

                # spglib occasionally returns a split slab
                zpos = slab.get_scaled_positions()
                if zpos[:, 2].max() > 0.9 or zpos[:, 2].min() < 0.1:
                    zpos[:, 2] -= 0.5
                    zpos[:, 2] %= 1
                    slab.set_scaled_positions(zpos)
                    slab.center(vacuum=self.vacuum, axis=2)

                # For hcp(1, 1, 0), primitive alters z-axis
                d = norm(slab.cell, axis=0)
                maxd = np.argwhere(d == d.max())[0][0]
                if maxd != 2:
                    slab.rotate(slab.cell[maxd], 'z', rotate_cell=True)
                    slab.cell[[maxd, 2]] = slab.cell[[2, maxd]]
                    slab.cell[maxd] = -slab.cell[maxd]
                    slab.wrap(pbc=True)

                slab.rotate(slab.cell[0], 'x', rotate_cell=True)

        # Orthogonalize the z-coordinate
        # Warning: bulk symmetry is lost at this point
        a, b, c = slab.cell
        nab = np.cross(a, b)
        c = (nab * np.dot(c, nab) / norm(nab)**2)
        slab.cell[2] = c

        # Align the longest remaining basis vector with x
        vdist = norm(slab.cell[:2], axis=1)
        if vdist[1] > vdist[0]:
            slab.rotate(slab.cell[1], 'x', rotate_cell=True)
            slab.cell[0] *= -1
            slab.cell[[0, 1]] = slab.cell[[1, 0]]

        # Enforce that the angle between basis vectors is acute.
        if slab.cell[1][0] < 0:
            slab.rotate(slab.cell[2], '-z')
            slab.cell *= [[1, 0, 0], [-1, 1, 0], [0, 0, 1]]

        if root:
            roots, vectors = root_surface_analysis(slab, return_vectors=True)

            if root not in roots:
                raise ValueError(
                    'Requested root structure unavailable for this system.'
                    'Try: {}'.format(roots))

            vect = vectors[np.where(root == roots)][0]
            slab = self.root_surface(slab, root, vect)

        # Get the direct z-coordinate of the requested layer
        zlayers = utils.get_unique_coordinates(slab,
                                               direct=False,
                                               tag=True,
                                               tol=self.tol)

        if not self.fix_stoichiometry:
            reverse_sort = np.sort(zlayers)[::-1]

            if self.min_width:
                n = np.where(zlayers < self.min_width, 1, 0).sum()
                ncut = reverse_sort[n]
            else:
                ncut = reverse_sort[:self.layers][-1]

            zpos = slab.positions[:, 2]
            index = np.arange(len(slab))
            del slab[index[zpos - ncut < -self.tol]]

            slab.cell[2][2] -= ncut
            slab.translate([0, 0, -ncut])

        slab *= (size[0], size[1], 1)
        tags = slab.get_tags()

        m = np.where(tags == 1)[0][0]
        translation = slab[m].position.copy()
        translation[2] = 0
        slab.translate(-translation)
        slab.wrap()

        ind = np.lexsort(
            (slab.positions[:, 0], slab.positions[:, 1], slab.positions[:, 2]))

        slab = Gratoms(positions=slab.positions[ind],
                       numbers=slab.numbers[ind],
                       cell=slab.cell,
                       pbc=[1, 1, 0],
                       tags=tags[ind])

        fix = tags.max() - self.fixed
        constraints = FixAtoms(indices=[a.index for a in slab if a.tag > fix])
        slab.set_constraint(constraints)

        if self.vacuum:
            slab.center(vacuum=self.vacuum, axis=2)

        self.slab = slab

        return slab