11import numpy as np
22
3+ from math import acos , pi
34from sisl import geom , Atom , Cuboid
45
56__all__ = ['bilayer' ]
@@ -28,7 +29,7 @@ def bilayer(bond=1.42, bottom_atom=None, top_atom=None, stacking='AB',
2829 twist : tuple of int, optional
2930 integer coordinates (m, n) defining a commensurate twist angle
3031 separation : float, optional
31- distance between the two layers (in Angstrom)
32+ distance between the two layers
3233 ret_angle : bool, optional
3334 return the twist angle (in degrees) in addition to the geometry instance
3435 layer : {'both', 'bottom', 'top'}
@@ -50,12 +51,15 @@ def bilayer(bond=1.42, bottom_atom=None, top_atom=None, stacking='AB',
5051 top = geom .honeycomb (bond , top_atom )
5152 ref_cell = bottom .cell .copy ()
5253
53- if stacking .lower () == 'aa' :
54+ stacking = stacking .lower ()
55+ if stacking == 'aa' :
5456 top = top .move ([0 , 0 , separation ])
55- elif stacking . lower () == 'ab' :
57+ elif stacking == 'ab' :
5658 top = top .move ([- bond , 0 , separation ])
57- elif stacking . lower () == 'ba' :
59+ elif stacking == 'ba' :
5860 top = top .move ([bond , 0 , separation ])
61+ else :
62+ raise ValueError ("bilayer: stacking must be one of {AA, AB, BA}" )
5963
6064 # Compute twist angle
6165 m , n = twist
@@ -75,48 +79,69 @@ def bilayer(bond=1.42, bottom_atom=None, top_atom=None, stacking='AB',
7579 else :
7680 # Twisting
7781 cos_theta = (n ** 2 + 4 * n * m + m ** 2 ) / (2 * (n ** 2 + n * m + m ** 2 ))
78- theta = np . arccos (cos_theta ) * 180 / np . pi
82+ theta = acos (cos_theta ) * 180 / pi
7983 rep = 4 * (n + m )
8084 natoms = 2 * (n ** 2 + n * m + m ** 2 )
8185
8286 if rep > 1 :
8387 # Set origo through an A atom near the middle of the geometry
84- bottom = bottom .tile (rep , axis = 0 ).tile (rep , axis = 1 )
85- top = top .tile (rep , axis = 0 ).tile (rep , axis = 1 )
86- tvec = rep * (ref_cell [0 ] + ref_cell [1 ]) / 2
87- bottom = bottom .move (- tvec )
88- top = top .move (- tvec )
88+ align_vec = - rep * (ref_cell [0 ] + ref_cell [1 ]) / 2
89+
90+ bottom = (bottom
91+ .tile (rep , axis = 0 )
92+ .tile (rep , axis = 1 )
93+ .move (align_vec ))
8994
9095 # Set new lattice vectors
9196 bottom .cell [0 ] = n * ref_cell [0 ] + m * ref_cell [1 ]
9297 bottom .cell [1 ] = - m * ref_cell [0 ] + (n + m ) * ref_cell [1 ]
9398
99+ # Remove atoms outside cell
100+ cell_box = Cuboid (bottom .cell , center = [- bond * 1e-4 ] * 3 )
101+
102+ # Reduce atoms in bottom
103+ inside_idx = cell_box .within_index (bottom .xyz )
104+ bottom = bottom .sub (inside_idx )
105+
94106 # Rotate top layer around A atom in bottom layer
95- top = top .rotate (theta , [0 , 0 , 1 ])
107+ top = (top
108+ .tile (rep , axis = 0 )
109+ .tile (rep , axis = 1 )
110+ .move (align_vec )
111+ .rotate (theta , [0 , 0 , 1 ]))
96112
113+ inside_idx = cell_box .within_index (top .xyz )
114+ top = top .sub (inside_idx )
115+
116+ # Ensure the cells are commensurate
97117 top .cell [:] = bottom .cell [:]
98118
99119 # Which layers to be returned
100- if layer .lower () == 'bottom' :
120+ layer = layer .lower ()
121+ if layer == 'bottom' :
101122 bilayer = bottom
102- elif layer . lower () == 'top' :
123+ elif layer == 'top' :
103124 bilayer = top
104- else :
125+ elif layer == 'both' :
105126 bilayer = bottom .add (top )
106127 natoms *= 2
128+ else :
129+ raise ValueError ("bilayer: layer must be one of {both, bottom, top}" )
107130
108131 if rep > 1 :
109- # Remove atoms outside cell
110- cell_box = Cuboid (bilayer .cell )
111- cell_box .set_center ([- 0.0001 ] * 3 )
112- inside_idx = cell_box .within_index (bilayer .xyz )
113- bilayer = bilayer .sub (inside_idx )
114-
115- # Rotate whole cell
132+ # Rotate and shift unit cell back
133+ fxyz_min = bilayer .fxyz .min (axis = 0 )
134+ fxyz_min [2 ] = 0.
135+ # This is a small hack since rotate is not numerically
136+ # precise.
137+ # TODO We could consider using mpfmath in Quaternion for increased
138+ # precision...
139+ fxyz_min [np .fabs (fxyz_min ) > 1.e-7 ] *= 0.49
140+ offset = fxyz_min .dot (bilayer .cell )
116141 vec = bilayer .cell [0 ] + bilayer .cell [1 ]
117142 vec_costh = vec [0 ] / vec .dot (vec ) ** 0.5
118- vec_th = np . arccos (vec_costh ) * 180 / np . pi
119- bilayer = bilayer .rotate (vec_th , [0 , 0 , 1 ])
143+ vec_th = acos (vec_costh ) * 180 / pi
144+ bilayer = bilayer .move ( - offset ). rotate (vec_th , [0 , 0 , 1 ])
120145
121146 # Sanity check
122147 assert len (bilayer ) == natoms
0 commit comments