Function to produce armchair heteroribbons.
See original GitHub issueI 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")
graphene_heteroribbon([7,8,9,8,7], [3,4,2,2,3], shifts=[0,0,-1,1,0]).plot(axes="xy")
graphene_heteroribbon([7,8,9,8,7], [2j,1,2,1j,2]).plot(axes="xy")
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")
and then you just cut the cell to get the graphene electrodes, for example.
Issue Analytics
- State:
- Created 2 years ago
- Comments:23 (23 by maintainers)
Top 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 >
Top Related Medium Post
No results found
Top Related StackOverflow Question
No results found
Troubleshoot Live Code
Lightrun enables developers to add logs, metrics and snapshots to live code - no restarts or redeploys required.
Start Free
Top Related Reddit Thread
No results found
Top Related Hackernoon Post
No results found
Top Related Tweet
No results found
Top Related Dev.to Post
No results found
Top Related Hashnode Post
No results found
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 (🤞 )
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 😃
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.