question-mark
Stuck on an issue?

Lightrun Answers was designed to reduce the constant googling that comes with debugging 3rd party libraries. It collects links to all the places you might be looking at while hunting down a tough bug.

And, if you’re still stuck at the end, we’re happy to hop on a call to see how we can help out.

Function to produce armchair heteroribbons.

See original GitHub issue

I was tired of having to think about how to build each heteroribbon and I’ve written a function to produce only valid heteroribbons (with no dangling bonds) specifying the width, units and shift of each section. Initially, I thought that it might be a useful thing to put in sisl_toolbox, but seeing that these structures are getting popular I thought that maybe it’s worth it to add it to sisl.geom.

Here's the (pretty long) code.
import sisl
import math
import numpy as np

def graphene_heteroribbon(Ws, units, shifts=None, **kwargs):
    """Builds a graphene nanoribbon with multiple widths.
    
    The function makes sure that only valid ribbons, that is
    ribbons with no dangling bonds, are being produced.
    Explanatory errors are raised if that's not possible. Only the 
    connection at the periodic boundary remains unchecked.
    
    It only works for armchair ribbons for now.
    
    Parameters
    ----------
    Ws: array-like of int
        The width of each section of the heteroribbon.
    units: array-like of int
        The number of units of each section. Note that a "unit" is
        not a unit cell, but half of it. I.e. a zigzag string.
        
        There are two situations in which you can choose the starting
        configuration of your border ("open", "closed"). At the beggining of
        the heteroribbon and on an even section after an odd one. In those
        cases, an integer imaginary number here will indicate that you want
        the open configuration (e.g. 2j indicates that you want 2 units, starting
        with an open configuration). If you pass an imaginary number on an invalid
        situation, an error will be raised.
    shifts: array-like of int, optional
        How each section must be shifted from the previous one.
        
        The exact meaning of the value depends on the sections that are
        being joined so that only valid ribbons are produced:
            - If both ribbons are odd, ribbons are aligned on the center
            and then shifted:
              ^ (shift) zig zag atoms if the incoming section is thinner than the
                previous one.
              ^ (2 * shift) zig zag atoms if they differ in a multiple of 4 atoms
                and both configurations are closed or if they differ in (2 + a
                multiple of 4 atoms) and configurations are different (one open
                and one closed.)
              ^ (1 + 2 * shift) zig zag atoms otherwise.
            - If both ribbons are even, they are aligned by the open end of the last
            section, not the incoming one, and then they are shifted:
              ^ (0) zig zag atoms if the shift is 0. In this case both sections are 
              forced to have the same edge open.
              ^ (-1 + 2 * abs(shift)) zig zag atoms in the appropiate direction if abs(shift) is
              bigger than 1. Notice that you can only shift the open edges toward the center.
            - If the ribbons have different parities, they are aligned on the open edge
            of the even ribbon and then shifted:
              ^ (0) zig zag atoms if shift is 0 in the particular cases that (i) the incoming
              odd section is thinner than the last even section or (ii) the last section, 
              being odd, has a border in the open configuration. Only in those cases does it make
              sense to exactly match the edges.
              ^ (bias + 2 * abs(shift)) zig zag atoms in the appropiate direction, where `bias` is
              either -1 or 1 depending on the exact situation and always ensures that the minimum
              shift is always `abs(shift) == 1`.
              
            
        
        If not provided, it defaults to an array full of zeros.
    **kwargs
        Keyword arguments are directly passed to `sisl.geom.graphene_nanoribbon`.
              
    Returns
    ----------
    sisl.Geometry:
        The final structure of the heteroribbon.
    """
    if kwargs.get('kind', 'armchair') == "zigzag":
        raise ValueError("Zigzag heteroribbons are not implemented yet.")
    
    # Initialize all variables that are going to be updated through the
    # iterations
    geom = None
    last_addition = None
    last_open = False
    last_W = -1
    
    # Get the distance of an atom shift.
    atom_shift = kwargs.get('bond', 1.42) * math.sin(math.radians(60))
    
    # If shifts were not provided it means the user wants no shifts,
    # so just initialize the array to all zeros.
    if shifts is None:
        shifts = np.zeros(len(Ws))
    
    # Helper function to manipulate ribbons.
    def _open_start(geometry):
        """Changes the border used for a ribbon.
        
        It does so by shifting half unit cell. This must be done before any
        tiling of the geometry.
        """
        geometry = geometry.move(geometry.cell[0] / 2)
        geometry.xyz = (geometry.fxyz % 1).dot(geometry.cell)
        return geometry
    
    # Loop through all the sections that compose the heteroribbon.
    for i, (W, W_units, shift) in enumerate(zip(Ws, units, shifts)):
        # Check that we are not joining an open odd ribbon with
        # a smaller ribbon, since no matter what the shift is there will
        # always be dangling bonds.
        if last_W % 2 == 1 and W < last_W:
            assert not last_open, (f"At the interface between {last_W} and {W}, {last_W} has an open end."
                    "If the wider ribbon is odd, it must always have a closed end.")
        
        # Get the unit cell of the ribbon that we are going to add to the
        # heteroribbon.
        new_addition = sisl.geom.graphene_nanoribbon(W, **kwargs)
        # Unit cells returned by sisl are always in the "closed" configuration.
        # In even width ribbons, there will always be an open edge and a closed
        # edge. We use "open" in the situation where the top edge is open.
        open_start = False
        
        # If the number of units has been provided as an imaginary number, this means that
        # the user wants the open configuration of the ribbon.
        if W_units.imag > 0:
            # However this is only possible in very specific cases, since in all other
            # cases the border of the incoming section is fully determined by the border
            # of the previous section and the shift.
            if i == 0 or W % 2 == 0 and last_W % 2 == 1:
                new_addition = _open_start(new_addition)
                open_start = not open_start
                W_units = int(W_units.imag)
            else:
                raise ValueError("This section does not allow choosing the configuration, but an imaginary",
                    f" number {W_units} was passed as the number of units."
                )
        
        # If this is not the first ribbon section in the heteroribbon, we are going to
        # calculate the shifts.
        if not i == 0:
            # Get the difference in width between the previous and this ribbon section
            W_diff = W - last_W
            # And also the mod(4) because there are certain differences if the width differs
            # on 1, 2, 3 or 4 atoms. After that, the cycle just repeats (e.g. 5 == 1, etc).
            diff_mod = W_diff % 4
            
            # Now, we need to calculate the offset that we have to apply to the incoming
            # section depending on several factors.
            if diff_mod % 2 == 0 and W % 2 == 1:
                # Both sections are odd
                
                # In this case, ribbons are aligned based on their centers.
                offset = (last_addition.center() - new_addition.center())[1]
                
                different_configs = last_open != open_start
                
                if W < last_W:
                    # The incoming section is thinner than the last one. Note that at this point
                    # we are sure that the last section has a closed border, otherwise we
                    # would have raised an error. At this point, centers can differ by any
                    # integer number of atoms, but we might need to open the start depending
                    # on the shift so that the sections match. 
                    if shift % 2 == 0 and diff_mod == 2 or shift % 2 == 1 and diff_mod == 0:
                        # We must open the start
                        new_addition = _open_start(new_addition)
                        open_start = not open_start
                    
                    # Shift limits are a bit complicated and are different for even and odd shifts.
                    # This is because a closed incoming section can shift until there is no connection
                    # between ribbons, while an open one needs to stop before its edge goes outside
                    # the previous section.
                    shift_lims = {
                        "closed": last_W // 2 + W // 2 - 2,
                        "open": last_W // 2 - W // 2 - 1
                    }
                    
                    shift_pars = {
                        lim % 2: lim for k, lim in shift_lims.items()
                    }
                    
                    assert (shift % 2 == 0 and abs(shift) <= shift_pars[0])\
                        or (shift % 2 == 1 and abs(shift) <= shift_pars[1]),\
                        (f"Shift must be an even number between {-shift_pars[0]} and"
                         f" {shift_pars[0]} or an odd number between {-shift_pars[1]}"
                         f" and {shift_pars[1]}")
                    
                    offset += shift * atom_shift
                else:
                    # At this point, we already know that the incoming section is wider and
                    # therefore it MUST have a closed start, otherwise there will be dangling bonds.
                    if diff_mod == 2 and last_open or diff_mod == 0 and not last_open:
                        # In these cases, centers must match or differ by an even number of atoms.
                        offset += (2*shift) * atom_shift
                        
                        # And these are the limits for the shift
                        if last_open:
                            # To avoid the current open section leaving dangling bonds.
                            shift_lim = (W - last_W) // 4
                        else:
                            # To avoid sections being disconnected.
                            shift_lim = max((last_W - 3) // 2, 0) + (W - last_W) // 4
                        
                        min_shift, max_shift = - shift_lim, shift_lim                    
                    else:
                        # Otherwise, centers must differ by an odd number of atoms.
                        offset += (1 + 2*shift) * atom_shift
                        
                        # And these are the limits for the shift
                        if last_open:
                            # To avoid the current open section leaving dangling bonds.
                            min_shift = - max(0, (W - last_W) // 4)
                            max_shift = max(0, (W - last_W) // 4 - 1)
                        else:
                            # To avoid sections being disconnected.
                            shift_lim = max((last_W - 3) // 2, 0) + (W - last_W) // 4
                            min_shift, max_shift = -shift_lim - 1, shift_lim
                    
                    assert min_shift <= shift <= max_shift, (f"Shift must be between {min_shift}"
                        f" and {max_shift}, but {shift} was provided."
                    )
                    
            else:
                # There is at least one even section.
                
                # In this case ribbons are aligned based on one of their edges.
                # We choose the edge of the even ribbon that is open (i.e. has
                # dangling bonds). If both ribbons are even, we align it based
                # on the last section, not the incoming one.
                if last_W % 2 == 0 and last_open or W % 2 == 0 and open_start:
                    # Align them on the top edge
                    offset = last_addition.xyz[:, 1].max() - new_addition.xyz[:, 1].max()
                else:
                    # Align them on the bottom edge
                    offset = last_addition.xyz[:, 1].min() - new_addition.xyz[:, 1].min()
                    
                # We have to make sure that the open edge of the even ribbons (however
                # many there are) is always shifted towards the center. Shifting in the
                # opposite direction would result in dangling bonds.
                no_shift = False

                if diff_mod % 2 == 0:
                    min_shift = max(0, (W - last_W) - 1)
                    max_shift = W // 2
                        
                    # Both ribbons are even
                    if (last_open and shift == 0) or (not last_open and shift != 0):
                        # If there is no shift, we must make sure that both ribbons are open or close.
                        # If there is shift, both ribbons must have different configurations to avoid
                        # dangling bonds.
                        new_addition = _open_start(new_addition)
                        open_start = not open_start

                    if shift == 0:
                        no_shift = True
                    else:
                        bias, shift_sign = {
                            True: (-1, 1),
                            False: (1, -1)
                        }[last_open]
                        
                elif W % 2 == 1:
                    # Last section was even, incoming section is odd.
                    min_shift = 0
                    max_shift = last_W // 2

                    if W < last_W and shift == 0:
                        # Incoming odd section is thinner than current section.
                        # We can make the ends match if we ensure that the odd section is open.Make ends match (shift = 0): The odd ribbon must be open.
                        new_addition = _open_start(new_addition)
                        open_start = not open_start

                        no_shift = True
                    else:
                        bias, shift_sign = {
                            (True, True): (-1, 1),
                            (True, False): (1, -1),
                            (False, True): (1, 1),
                            (False, False): (-1, -1)
                        }[(W < last_W, last_open)]

                else:
                    # Last section was odd, incoming section is even.
                    min_shift = 0
                    max_shift = W // 2
                    
                    if last_open:
                        assert shift == 0, ("The shift between an open odd ribbon and an incoming"
                            "even ribbon must be always 0, otherwise the odd section will have a dangling bond"
                        )
                        no_shift = True
                    else:
                        bias, shift_sign = {
                            True: (-1, -1),
                            False: (1, 1)
                        }[open_start]

                if not no_shift:
                    # Check that the shift value is correct. There are two things to check here:
                    # Provided shift is in the right direction
                    right_shift_sign = shift*shift_sign - abs(shift) == 0
                    # Provided shift falls within the bounds.
                    shift_in_bounds = min_shift <= abs(shift) <= max_shift
                    
                    assert (right_shift_sign and shift_in_bounds), (
                        f" Shift must be between {min_shift*shift_sign} and {max_shift*shift_sign}, but {shift} was provided."
                    )

                    offset += (bias + 2*shift) * atom_shift
                
            # Apply the offset that we have calculated.
            new_addition = new_addition.move([0, offset, 0])
        
        # Check how many times we have to tile the unit cell (tiles) and whether
        # we have to cut the last string of atoms (cut_last)
        tiles, res = divmod(W_units + 1, 2)
        cut_last = res == 0
        
        # Tile the current section unit cell
        new_addition = new_addition.tile(tiles, 0)
        # Cut the last string of atoms.
        if cut_last:
            new_addition.cell[0,0] *= W_units / (W_units + 1)
            new_addition = new_addition.remove({"x": (new_addition.cell[0,0] - 0.01, None)})
        
        # Initialize the heterorribon or append to it if it's already initialized.
        if i == 0:
            geom = new_addition
        else:
            geom = geom.append(new_addition, 0)
        
        # Update the state for the next iteration
        last_addition = new_addition
        last_open = open_start != cut_last
        last_W = W
            
    return geom

And some examples of structures you can get:

graphene_heteroribbon([7,9,6], units=[2,4,2]).plot(axes="xy")

newplot - 2022-01-12T042342 537

graphene_heteroribbon([7,8,9,8,7], [3,4,2,2,3], shifts=[0,0,-1,1,0]).plot(axes="xy")

newplot - 2022-01-12T042418 632

graphene_heteroribbon([7,8,9,8,7], [2j,1,2,1j,2]).plot(axes="xy")

newplot - 2022-01-12T042459 816

And this is just trying random numbers, you always get the valid ribbons or an error if that’s not possible.

Also, this might be useful to design TranSIESTA setups with graphene electrodes? Something like:

graphene_heteroribbon([31, 9, 7, 5, 7, 9, 31], [4,9,5,3,6,9,4]).plot(axes="xy")

newplot - 2022-01-12T043341 061

and then you just cut the cell to get the graphene electrodes, for example.

Issue Analytics

  • State:open
  • Created 2 years ago
  • Comments:23 (23 by maintainers)

github_iconTop GitHub Comments

1reaction
zerothicommented, Jan 12, 2022

Now I just really want to clarify the units in #418, and then I really want it out. But after that, I should be able to do releases more often (🤞 )

0reactions
pfebrercommented, Jan 12, 2022

Yes, but the point is that you had to find in your ribbon where can you start to build it and then understand how much you need to shift. Anyway, if you have the possibility to start “open” you can choose to shift it yourself if you want as well 😃

Note that the “open/closed” notation is not clear for even-numbered widths. In my example above the first column is of width 8 - should this be considered one or the other case?

Yes, I know. I consider “closed” the one that sisl produces. Just to be consistent with the odd widths. That is, I consider “closed” the one with the bottom edge open.

Read more comments on GitHub >

github_iconTop Results From Across the Web

First principles study on the structural, electronic, and transport ...
We investigate the structural, electronic, and transport properties of the Armchair graphene heterostructure nanoribbons terminated by H and F atoms, ...
Read more >
Recent Advances in 2D Lateral Heterostructures - PMC - NCBI
The electronic properties of 2D materials can be tuned by the ribbon width. The armchair graphene nanoribbons can behave as semiconductors, and the...
Read more >
Addressing Electron Spins Embedded in Metallic Graphene ...
The boron heteroatoms turn the ribbon metallic and, at the same time, acquire a net magnetic moment. Density functional theory (DFT) ...
Read more >
First-principles investigation of armchair stanene nanoribbons
The electronic properties of these nanoribbons strongly depend on their ribbon width. In general, band gap opens and increases with decreasing nanoribbon width...
Read more >
arXiv:2109.07533v1 [cond-mat.mtrl-sci] 15 Sep 2021
(a–c) Energy gaps, εg, of hydrogen-terminated armchair graphene nanoribbons as a function of an increasing electric field, Eext. Depending.
Read more >

github_iconTop Related Medium Post

No results found

github_iconTop Related StackOverflow Question

No results found

github_iconTroubleshoot Live Code

Lightrun enables developers to add logs, metrics and snapshots to live code - no restarts or redeploys required.
Start Free

github_iconTop Related Reddit Thread

No results found

github_iconTop Related Hackernoon Post

No results found

github_iconTop Related Tweet

No results found

github_iconTop Related Dev.to Post

No results found

github_iconTop Related Hashnode Post

No results found