forked from MRHemmati/pythTB
-
Notifications
You must be signed in to change notification settings - Fork 0
/
pythtb.py
3644 lines (3122 loc) · 154 KB
/
pythtb.py
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
584
585
586
587
588
589
590
591
592
593
594
595
596
597
598
599
600
601
602
603
604
605
606
607
608
609
610
611
612
613
614
615
616
617
618
619
620
621
622
623
624
625
626
627
628
629
630
631
632
633
634
635
636
637
638
639
640
641
642
643
644
645
646
647
648
649
650
651
652
653
654
655
656
657
658
659
660
661
662
663
664
665
666
667
668
669
670
671
672
673
674
675
676
677
678
679
680
681
682
683
684
685
686
687
688
689
690
691
692
693
694
695
696
697
698
699
700
701
702
703
704
705
706
707
708
709
710
711
712
713
714
715
716
717
718
719
720
721
722
723
724
725
726
727
728
729
730
731
732
733
734
735
736
737
738
739
740
741
742
743
744
745
746
747
748
749
750
751
752
753
754
755
756
757
758
759
760
761
762
763
764
765
766
767
768
769
770
771
772
773
774
775
776
777
778
779
780
781
782
783
784
785
786
787
788
789
790
791
792
793
794
795
796
797
798
799
800
801
802
803
804
805
806
807
808
809
810
811
812
813
814
815
816
817
818
819
820
821
822
823
824
825
826
827
828
829
830
831
832
833
834
835
836
837
838
839
840
841
842
843
844
845
846
847
848
849
850
851
852
853
854
855
856
857
858
859
860
861
862
863
864
865
866
867
868
869
870
871
872
873
874
875
876
877
878
879
880
881
882
883
884
885
886
887
888
889
890
891
892
893
894
895
896
897
898
899
900
901
902
903
904
905
906
907
908
909
910
911
912
913
914
915
916
917
918
919
920
921
922
923
924
925
926
927
928
929
930
931
932
933
934
935
936
937
938
939
940
941
942
943
944
945
946
947
948
949
950
951
952
953
954
955
956
957
958
959
960
961
962
963
964
965
966
967
968
969
970
971
972
973
974
975
976
977
978
979
980
981
982
983
984
985
986
987
988
989
990
991
992
993
994
995
996
997
998
999
1000
from __future__ import print_function
# PythTB python tight binding module.
# August 25th, 2017
__version__='1.7.2'
# Copyright 2010, 2012, 2016, 2017 by Sinisa Coh and David Vanderbilt
#
# This file is part of PythTB. PythTB is free software: you can
# redistribute it and/or modify it under the terms of the GNU General
# Public License as published by the Free Software Foundation, either
# version 3 of the License, or (at your option) any later version.
#
# PythTB is distributed in the hope that it will be useful, but WITHOUT
# ANY WARRANTY; without even the implied warranty of MERCHANTABILITY
# or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public
# License for more details.
#
# A copy of the GNU General Public License should be available
# alongside this source in a file named gpl-3.0.txt. If not,
# see <http://www.gnu.org/licenses/>.
#
# PythTB is availabe at http://www.physics.rutgers.edu/pythtb/
import numpy as np # numerics for matrices
import sys # for exiting
import copy # for deepcopying
class tb_model(object):
r"""
This is the main class of the PythTB package which contains all
information for the tight-binding model.
:param dim_k: Dimensionality of reciprocal space, i.e., specifies how
many directions are considered to be periodic.
:param dim_r: Dimensionality of real space, i.e., specifies how many
real space lattice vectors there are and how many coordinates are
needed to specify the orbital coordinates.
.. note:: Parameter *dim_r* can be larger than *dim_k*! For example,
a polymer is a three-dimensional molecule (one needs three
coordinates to specify orbital positions), but it is periodic
along only one direction. For a polymer, therefore, we should
have *dim_k* equal to 1 and *dim_r* equal to 3. See similar example
here: :ref:`trestle-example`.
:param lat: Array containing lattice vectors in Cartesian
coordinates (in arbitrary units). In example the below, the first
lattice vector has coordinates [1.0,0.5] while the second
one has coordinates [0.0,2.0]. By default, lattice vectors
are an identity matrix.
:param orb: Array containing reduced coordinates of all
tight-binding orbitals. In the example below, the first
orbital is defined with reduced coordinates [0.2,0.3]. Its
Cartesian coordinates are therefore 0.2 times the first
lattice vector plus 0.3 times the second lattice vector.
If *orb* is an integer code will assume that there are these many
orbitals all at the origin of the unit cell. By default
the code will assume a single orbital at the origin.
:param per: This is an optional parameter giving a list of lattice
vectors which are considered to be periodic. In the example below,
only the vector [0.0,2.0] is considered to be periodic (since
per=[1]). By default, all lattice vectors are assumed to be
periodic. If dim_k is smaller than dim_r, then by default the first
dim_k vectors are considered to be periodic.
:param nspin: Number of explicit spin components assumed for each
orbital in *orb*. Allowed values of *nspin* are *1* and *2*. If
*nspin* is 1 then the model is spinless, if *nspin* is 2 then it
is explicitly a spinfull model and each orbital is assumed to
have two spin components. Default value of this parameter is
*1*. Of course one can make spinfull calculation even with
*nspin* set to 1, but then the user must keep track of which
orbital corresponds to which spin component.
Example usage::
# Creates model that is two-dimensional in real space but only
# one-dimensional in reciprocal space. Second lattice vector is
# chosen to be periodic (since per=[1]). Three orbital
# coordinates are specified.
tb = tb_model(1, 2,
lat=[[1.0, 0.5], [0.0, 2.0]],
orb=[[0.2, 0.3], [0.1, 0.1], [0.2, 0.2]],
per=[1])
"""
def __init__(self,dim_k,dim_r,lat=None,orb=None,per=None,nspin=1):
# initialize _dim_k = dimensionality of k-space (integer)
if type(dim_k).__name__!='int':
raise Exception("\n\nArgument dim_k not an integer")
if dim_k < 0 or dim_k > 4:
raise Exception("\n\nArgument dim_k out of range. Must be between 0 and 4.")
self._dim_k=dim_k
# initialize _dim_r = dimensionality of r-space (integer)
if type(dim_r).__name__!='int':
raise Exception("\n\nArgument dim_r not an integer")
if dim_r < dim_k or dim_r > 4:
raise Exception("\n\nArgument dim_r out of range. Must be dim_r>=dim_k and dim_r<=4.")
self._dim_r=dim_r
# initialize _lat = lattice vectors, array of dim_r*dim_r
# format is _lat(lat_vec_index,cartesian_index)
# special option: 'unit' implies unit matrix, also default value
if lat is 'unit' or lat is None:
self._lat=np.identity(dim_r,float)
print(" Lattice vectors not specified! I will use identity matrix.")
elif type(lat).__name__ not in ['list','ndarray']:
raise Exception("\n\nArgument lat is not a list.")
else:
self._lat=np.array(lat,dtype=float)
if self._lat.shape!=(dim_r,dim_r):
raise Exception("\n\nWrong lat array dimensions")
# check that volume is not zero and that have right handed system
if dim_r>0:
if np.abs(np.linalg.det(self._lat))<1.0E-6:
raise Exception("\n\nLattice vectors length/area/volume too close to zero, or zero.")
if np.linalg.det(self._lat)<0.0:
raise Exception("\n\nLattice vectors need to form right handed system.")
# initialize _norb = number of basis orbitals per cell
# and _orb = orbital locations, in reduced coordinates
# format is _orb(orb_index,lat_vec_index)
# special option: 'bravais' implies one atom at origin
if orb is 'bravais' or orb is None:
self._norb=1
self._orb=np.zeros((1,dim_r))
print(" Orbital positions not specified. I will assume a single orbital at the origin.")
elif type(orb).__name__=='int':
self._norb=orb
self._orb=np.zeros((orb,dim_r))
print(" Orbital positions not specified. I will assume ",orb," orbitals at the origin")
elif type(orb).__name__ not in ['list','ndarray']:
raise Exception("\n\nArgument orb is not a list or an integer")
else:
self._orb=np.array(orb,dtype=float)
if len(self._orb.shape)!=2:
raise Exception("\n\nWrong orb array rank")
self._norb=self._orb.shape[0] # number of orbitals
if self._orb.shape[1]!=dim_r:
raise Exception("\n\nWrong orb array dimensions")
# choose which self._dim_k out of self._dim_r dimensions are
# to be considered periodic.
if per==None:
# by default first _dim_k dimensions are periodic
self._per=list(range(self._dim_k))
else:
if len(per)!=self._dim_k:
raise Exception("\n\nWrong choice of periodic/infinite direction!")
# store which directions are the periodic ones
self._per=per
# remember number of spin components
if nspin not in [1,2]:
raise Exception("\n\nWrong value of nspin, must be 1 or 2!")
self._nspin=nspin
# by default, assume model did not come from w90 object and that
# position operator is diagonal
self._assume_position_operator_diagonal=True
# compute number of electronic states at each k-point
self._nsta=self._norb*self._nspin
# Initialize onsite energies to zero
if self._nspin==1:
self._site_energies=np.zeros((self._norb),dtype=float)
elif self._nspin==2:
self._site_energies=np.zeros((self._norb,2,2),dtype=complex)
# remember which onsite energies user has specified
self._site_energies_specified=np.zeros(self._norb,dtype=bool)
self._site_energies_specified[:]=False
# Initialize hoppings to empty list
self._hoppings=[]
# The onsite energies and hoppings are not specified
# when creating a 'tb_model' object. They are speficied
# subsequently by separate function calls defined below.
def set_onsite(self,onsite_en,ind_i=None,mode="set"):
r"""
Defines on-site energies for tight-binding orbitals. One can
either set energy for one tight-binding orbital, or all at
once.
.. warning:: In previous version of PythTB this function was
called *set_sites*. For backwards compatibility one can still
use that name but that feature will be removed in future
releases.
:param onsite_en: Either a list of on-site energies (in
arbitrary units) for each orbital, or a single on-site
energy (in this case *ind_i* parameter must be given). In
the case when *nspin* is *1* (spinless) then each on-site
energy is a single number. If *nspin* is *2* then on-site
energy can be given either as a single number, or as an
array of four numbers, or 2x2 matrix. If a single number is
given, it is interpreted as on-site energy for both up and
down spin component. If an array of four numbers is given,
these are the coefficients of I, sigma_x, sigma_y, and
sigma_z (that is, the 2x2 identity and the three Pauli spin
matrices) respectively. Finally, full 2x2 matrix can be
given as well. If this function is never called, on-site
energy is assumed to be zero.
:param ind_i: Index of tight-binding orbital whose on-site
energy you wish to change. This parameter should be
specified only when *onsite_en* is a single number (not a
list).
:param mode: Similar to parameter *mode* in function set_hop*.
Speficies way in which parameter *onsite_en* is
used. It can either set value of on-site energy from scratch,
reset it, or add to it.
* "set" -- Default value. On-site energy is set to value of
*onsite_en* parameter. One can use "set" on each
tight-binding orbital only once.
* "reset" -- Specifies on-site energy to given value. This
function can be called multiple times for the same
orbital(s).
* "add" -- Adds to the previous value of on-site
energy. This function can be called multiple times for the
same orbital(s).
Example usage::
# Defines on-site energy of first orbital to be 0.0,
# second 1.0, and third 2.0
tb.set_onsite([0.0, 1.0, 2.0])
# Increases value of on-site energy for second orbital
tb.set_onsite(100.0, 1, mode="add")
# Changes on-site energy of second orbital to zero
tb.set_onsite(0.0, 1, mode="reset")
# Sets all three on-site energies at once
tb.set_onsite([2.0, 3.0, 4.0], mode="reset")
"""
if ind_i==None:
if (len(onsite_en)!=self._norb):
raise Exception("\n\nWrong number of site energies")
# make sure ind_i is not out of scope
if ind_i!=None:
if ind_i<0 or ind_i>=self._norb:
raise Exception("\n\nIndex ind_i out of scope.")
# make sure that onsite terms are real/hermitian
if ind_i!=None:
to_check=[onsite_en]
else:
to_check=onsite_en
for ons in to_check:
if np.array(ons).shape==():
if np.abs(np.array(ons)-np.array(ons).conjugate())>1.0E-8:
raise Exception("\n\nOnsite energy should not have imaginary part!")
elif np.array(ons).shape==(4,):
if np.max(np.abs(np.array(ons)-np.array(ons).conjugate()))>1.0E-8:
raise Exception("\n\nOnsite energy or Zeeman field should not have imaginary part!")
elif np.array(ons).shape==(2,2):
if np.max(np.abs(np.array(ons)-np.array(ons).T.conjugate()))>1.0E-8:
raise Exception("\n\nOnsite matrix should be Hermitian!")
# specifying onsite energies from scratch, can be called only once
if mode.lower()=="set":
# specifying only one site at a time
if ind_i!=None:
# make sure we specify things only once
if self._site_energies_specified[ind_i]==True:
raise Exception("\n\nOnsite energy for this site was already specified! Use mode=\"reset\" or mode=\"add\".")
else:
self._site_energies[ind_i]=self._val_to_block(onsite_en)
self._site_energies_specified[ind_i]=True
# specifying all sites at once
else:
# make sure we specify things only once
if True in self._site_energies_specified[ind_i]:
raise Exception("\n\nSome or all onsite energies were already specified! Use mode=\"reset\" or mode=\"add\".")
else:
for i in range(self._norb):
self._site_energies[i]=self._val_to_block(onsite_en[i])
self._site_energies_specified[:]=True
# reset values of onsite terms, without adding to previous value
elif mode.lower()=="reset":
# specifying only one site at a time
if ind_i!=None:
self._site_energies[ind_i]=self._val_to_block(onsite_en)
self._site_energies_specified[ind_i]=True
# specifying all sites at once
else:
for i in range(self._norb):
self._site_energies[i]=self._val_to_block(onsite_en[i])
self._site_energies_specified[:]=True
# add to previous value
elif mode.lower()=="add":
# specifying only one site at a time
if ind_i!=None:
self._site_energies[ind_i]+=self._val_to_block(onsite_en)
self._site_energies_specified[ind_i]=True
# specifying all sites at once
else:
for i in range(self._norb):
self._site_energies[i]+=self._val_to_block(onsite_en[i])
self._site_energies_specified[:]=True
else:
raise Exception("\n\nWrong value of mode parameter")
def set_hop(self,hop_amp,ind_i,ind_j,ind_R=None,mode="set",allow_conjugate_pair=False):
r"""
Defines hopping parameters between tight-binding orbitals. In
the notation used in section 3.1 equation 3.6 of
:download:`notes on tight-binding formalism
<misc/pythtb-formalism.pdf>` this function specifies the
following object
.. math::
H_{ij}({\bf R})= \langle \phi_{{\bf 0} i} \vert H \vert \phi_{{\bf R},j} \rangle
Where :math:`\langle \phi_{{\bf 0} i} \vert` is i-th
tight-binding orbital in the home unit cell and
:math:`\vert \phi_{{\bf R},j} \rangle` is j-th tight-binding orbital in
unit cell shifted by lattice vector :math:`{\bf R}`. :math:`H`
is the Hamiltonian.
(Strictly speaking, this term specifies hopping amplitude
for hopping from site *j+R* to site *i*, not vice-versa.)
Hopping in the opposite direction is automatically included by
the code since
.. math::
H_{ji}(-{\bf R})= \left[ H_{ij}({\bf R}) \right]^{*}
.. warning::
There is no need to specify hoppings in both :math:`i
\rightarrow j+R` direction and opposite :math:`j
\rightarrow i-R` direction since that is done
automatically. If you want to specifiy hoppings in both
directions, see description of parameter
*allow_conjugate_pair*.
.. warning:: In previous version of PythTB this function was
called *add_hop*. For backwards compatibility one can still
use that name but that feature will be removed in future
releases.
:param hop_amp: Hopping amplitude; can be real or complex
number, equals :math:`H_{ij}({\bf R})`. If *nspin* is *2*
then hopping amplitude can be given either as a single
number, or as an array of four numbers, or as 2x2 matrix. If
a single number is given, it is interpreted as hopping
amplitude for both up and down spin component. If an array
of four numbers is given, these are the coefficients of I,
sigma_x, sigma_y, and sigma_z (that is, the 2x2 identity and
the three Pauli spin matrices) respectively. Finally, full
2x2 matrix can be given as well.
:param ind_i: Index of bra orbital from the bracket :math:`\langle
\phi_{{\bf 0} i} \vert H \vert \phi_{{\bf R},j} \rangle`. This
orbital is assumed to be in the home unit cell.
:param ind_j: Index of ket orbital from the bracket :math:`\langle
\phi_{{\bf 0} i} \vert H \vert \phi_{{\bf R},j} \rangle`. This
orbital does not have to be in the home unit cell; its unit cell
position is determined by parameter *ind_R*.
:param ind_R: Specifies, in reduced coordinates, the shift of
the ket orbital. The number of coordinates must equal the
dimensionality in real space (*dim_r* parameter) for consistency,
but only the periodic directions of ind_R will be considered. If
reciprocal space is zero-dimensional (as in a molecule),
this parameter does not need to be specified.
:param mode: Similar to parameter *mode* in function *set_onsite*.
Speficies way in which parameter *hop_amp* is
used. It can either set value of hopping term from scratch,
reset it, or add to it.
* "set" -- Default value. Hopping term is set to value of
*hop_amp* parameter. One can use "set" for each triplet of
*ind_i*, *ind_j*, *ind_R* only once.
* "reset" -- Specifies on-site energy to given value. This
function can be called multiple times for the same triplet
*ind_i*, *ind_j*, *ind_R*.
* "add" -- Adds to the previous value of hopping term This
function can be called multiple times for the same triplet
*ind_i*, *ind_j*, *ind_R*.
If *set_hop* was ever called with *allow_conjugate_pair* set
to True, then it is possible that user has specified both
:math:`i \rightarrow j+R` and conjugate pair :math:`j
\rightarrow i-R`. In this case, "set", "reset", and "add"
parameters will treat triplet *ind_i*, *ind_j*, *ind_R* and
conjugate triplet *ind_j*, *ind_i*, *-ind_R* as distinct.
:param allow_conjugate_pair: Default value is *False*. If set
to *True* code will allow user to specify hopping
:math:`i \rightarrow j+R` even if conjugate-pair hopping
:math:`j \rightarrow i-R` has been
specified. If both terms are specified, code will
still count each term two times.
Example usage::
# Specifies complex hopping amplitude between first orbital in home
# unit cell and third orbital in neigbouring unit cell.
tb.set_hop(0.3+0.4j, 0, 2, [0, 1])
# change value of this hopping
tb.set_hop(0.1+0.2j, 0, 2, [0, 1], mode="reset")
# add to previous value (after this function call below,
# hopping term amplitude is 100.1+0.2j)
tb.set_hop(100.0, 0, 2, [0, 1], mode="add")
"""
#
if self._dim_k!=0 and (ind_R is None):
raise Exception("\n\nNeed to specify ind_R!")
# if necessary convert from integer to array
if self._dim_k==1 and type(ind_R).__name__=='int':
tmpR=np.zeros(self._dim_r,dtype=int)
tmpR[self._per]=ind_R
ind_R=tmpR
# check length of ind_R
if self._dim_k!=0:
if len(ind_R)!=self._dim_r:
raise Exception("\n\nLength of input ind_R vector must equal dim_r! Even if dim_k<dim_r.")
# make sure ind_i and ind_j are not out of scope
if ind_i<0 or ind_i>=self._norb:
raise Exception("\n\nIndex ind_i out of scope.")
if ind_j<0 or ind_j>=self._norb:
raise Exception("\n\nIndex ind_j out of scope.")
# do not allow onsite hoppings to be specified here because then they
# will be double-counted
if self._dim_k==0:
if ind_i==ind_j:
raise Exception("\n\nDo not use set_hop for onsite terms. Use set_onsite instead!")
else:
if ind_i==ind_j:
all_zer=True
for k in self._per:
if int(ind_R[k])!=0:
all_zer=False
if all_zer==True:
raise Exception("\n\nDo not use set_hop for onsite terms. Use set_onsite instead!")
#
# make sure that if <i|H|j+R> is specified that <j|H|i-R> is not!
if allow_conjugate_pair==False:
for h in self._hoppings:
if ind_i==h[2] and ind_j==h[1]:
if self._dim_k==0:
raise Exception(\
"""\n
Following matrix element was already implicitely specified:
i="""+str(ind_i)+" j="+str(ind_j)+"""
Remember, specifying <i|H|j> automatically specifies <j|H|i>. For
consistency, specify all hoppings for a given bond in the same
direction. (Or, alternatively, see the documentation on the
'allow_conjugate_pair' flag.)
""")
elif False not in (np.array(ind_R)[self._per]==(-1)*np.array(h[3])[self._per]):
raise Exception(\
"""\n
Following matrix element was already implicitely specified:
i="""+str(ind_i)+" j="+str(ind_j)+" R="+str(ind_R)+"""
Remember,specifying <i|H|j+R> automatically specifies <j|H|i-R>. For
consistency, specify all hoppings for a given bond in the same
direction. (Or, alternatively, see the documentation on the
'allow_conjugate_pair' flag.)
""")
# convert to 2by2 matrix if needed
hop_use=self._val_to_block(hop_amp)
# hopping term parameters to be stored
if self._dim_k==0:
new_hop=[hop_use,int(ind_i),int(ind_j)]
else:
new_hop=[hop_use,int(ind_i),int(ind_j),np.array(ind_R)]
#
# see if there is a hopping term with same i,j,R
use_index=None
for iih,h in enumerate(self._hoppings):
# check if the same
same_ijR=False
if ind_i==h[1] and ind_j==h[2]:
if self._dim_k==0:
same_ijR=True
else:
if False not in (np.array(ind_R)[self._per]==np.array(h[3])[self._per]):
same_ijR=True
# if they are the same then store index of site at which they are the same
if same_ijR==True:
use_index=iih
#
# specifying hopping terms from scratch, can be called only once
if mode.lower()=="set":
# make sure we specify things only once
if use_index!=None:
raise Exception("\n\nHopping energy for this site was already specified! Use mode=\"reset\" or mode=\"add\".")
else:
self._hoppings.append(new_hop)
# reset value of hopping term, without adding to previous value
elif mode.lower()=="reset":
if use_index!=None:
self._hoppings[use_index]=new_hop
else:
self._hoppings.append(new_hop)
# add to previous value
elif mode.lower()=="add":
if use_index!=None:
self._hoppings[use_index][0]+=new_hop[0]
else:
self._hoppings.append(new_hop)
else:
raise Exception("\n\nWrong value of mode parameter")
def _val_to_block(self,val):
"""If nspin=2 then returns a 2 by 2 matrix from the input
parameters. If only one real number is given in the input then
assume that this is the diagonal term. If array with four
elements is given then first one is the diagonal term, and
other three are Zeeman field direction. If given a 2 by 2
matrix, just return it. If nspin=1 then just returns val."""
# spinless case
if self._nspin==1:
return val
# spinfull case
elif self._nspin==2:
# matrix to return
ret=np.zeros((2,2),dtype=complex)
#
use_val=np.array(val)
# only one number is given
if use_val.shape==():
ret[0,0]+=use_val
ret[1,1]+=use_val
# if four numbers are given
elif use_val.shape==(4,):
# diagonal
ret[0,0]+=use_val[0]
ret[1,1]+=use_val[0]
# sigma_x
ret[0,1]+=use_val[1]
ret[1,0]+=use_val[1] # sigma_y
ret[0,1]+=use_val[2]*(-1.0j)
ret[1,0]+=use_val[2]*( 1.0j)
# sigma_z
ret[0,0]+=use_val[3]
ret[1,1]+=use_val[3]*(-1.0)
# if 2 by 2 matrix is given
elif use_val.shape==(2,2):
return use_val
else:
raise Exception(\
"""\n
Wrong format of the on-site or hopping term. Must be single number, or
in the case of a spinfull model can be array of four numbers or 2x2
matrix.""")
return ret
def display(self):
r"""
Prints on the screen some information about this tight-binding
model. This function doesn't take any parameters.
"""
print('---------------------------------------')
print('report of tight-binding model')
print('---------------------------------------')
print('k-space dimension =',self._dim_k)
print('r-space dimension =',self._dim_r)
print('number of spin components =',self._nspin)
print('periodic directions =',self._per)
print('number of orbitals =',self._norb)
print('number of electronic states =',self._nsta)
print('lattice vectors:')
for i,o in enumerate(self._lat):
print(" #",_nice_int(i,2)," ===> [", end=' ')
for j,v in enumerate(o):
print(_nice_float(v,7,4), end=' ')
if j!=len(o)-1:
print(",", end=' ')
print("]")
print('positions of orbitals:')
for i,o in enumerate(self._orb):
print(" #",_nice_int(i,2)," ===> [", end=' ')
for j,v in enumerate(o):
print(_nice_float(v,7,4), end=' ')
if j!=len(o)-1:
print(",", end=' ')
print("]")
print('site energies:')
for i,site in enumerate(self._site_energies):
print(" #",_nice_int(i,2)," ===> ", end=' ')
if self._nspin==1:
print(_nice_float(site,7,4))
elif self._nspin==2:
print(str(site).replace("\n"," "))
print('hoppings:')
for i,hopping in enumerate(self._hoppings):
print("<",_nice_int(hopping[1],2),"| H |",_nice_int(hopping[2],2), end=' ')
if len(hopping)==4:
print("+ [", end=' ')
for j,v in enumerate(hopping[3]):
print(_nice_int(v,2), end=' ')
if j!=len(hopping[3])-1:
print(",", end=' ')
else:
print("]", end=' ')
print("> ===> ", end=' ')
if self._nspin==1:
print(_nice_complex(hopping[0],7,4))
elif self._nspin==2:
print(str(hopping[0]).replace("\n"," "))
print('hopping distances:')
for i,hopping in enumerate(self._hoppings):
print("| pos(",_nice_int(hopping[1],2),") - pos(",_nice_int(hopping[2],2), end=' ')
if len(hopping)==4:
print("+ [", end=' ')
for j,v in enumerate(hopping[3]):
print(_nice_int(v,2), end=' ')
if j!=len(hopping[3])-1:
print(",", end=' ')
else:
print("]", end=' ')
print(") | = ", end=' ')
pos_i=np.dot(self._orb[hopping[1]],self._lat)
pos_j=np.dot(self._orb[hopping[2]],self._lat)
if len(hopping)==4:
pos_j+=np.dot(hopping[3],self._lat)
dist=np.linalg.norm(pos_j-pos_i)
print (_nice_float(dist,7,4))
print()
def visualize(self,dir_first,dir_second=None,eig_dr=None,draw_hoppings=True,ph_color="black"):
r"""
Rudimentary function for visualizing tight-binding model geometry,
hopping between tight-binding orbitals, and electron eigenstates.
If eigenvector is not drawn, then orbitals in home cell are drawn
as red circles, and those in neighboring cells are drawn with
different shade of red. Hopping term directions are drawn with
green lines connecting two orbitals. Origin of unit cell is
indicated with blue dot, while real space unit vectors are drawn
with blue lines.
If eigenvector is drawn, then electron eigenstate on each orbital
is drawn with a circle whose size is proportional to wavefunction
amplitude while its color depends on the phase. There are various
coloring schemes for the phase factor; see more details under
*ph_color* parameter. If eigenvector is drawn and coloring scheme
is "red-blue" or "wheel", all other elements of the picture are
drawn in gray or black.
:param dir_first: First index of Cartesian coordinates used for
plotting.
:param dir_second: Second index of Cartesian coordinates used for
plotting. For example if dir_first=0 and dir_second=2, and
Cartesian coordinates of some orbital is [2.0,4.0,6.0] then it
will be drawn at coordinate [2.0,6.0]. If dimensionality of real
space (*dim_r*) is zero or one then dir_second should not be
specified.
:param eig_dr: Optional parameter specifying eigenstate to
plot. If specified, this should be one-dimensional array of
complex numbers specifying wavefunction at each orbital in
the tight-binding basis. If not specified, eigenstate is not
drawn.
:param draw_hoppings: Optional parameter specifying whether to
draw all allowed hopping terms in the tight-binding
model. Default value is True.
:param ph_color: Optional parameter determining the way
eigenvector phase factors are translated into color. Default
value is "black". Convention of the wavefunction phase is as
in convention 1 in section 3.1 of :download:`notes on
tight-binding formalism <misc/pythtb-formalism.pdf>`. In
other words, these wavefunction phases are in correspondence
with cell-periodic functions :math:`u_{n {\bf k}} ({\bf r})`
not :math:`\Psi_{n {\bf k}} ({\bf r})`.
* "black" -- phase of eigenvectors are ignored and wavefunction
is always colored in black.
* "red-blue" -- zero phase is drawn red, while phases or pi or
-pi are drawn blue. Phases in between are interpolated between
red and blue. Some phase information is lost in this coloring
becase phase of +phi and -phi have same color.
* "wheel" -- each phase is given unique color. In steps of pi/3
starting from 0, colors are assigned (in increasing hue) as:
red, yellow, green, cyan, blue, magenta, red.
:returns:
* **fig** -- Figure object from matplotlib.pyplot module
that can be used to save the figure in PDF, EPS or similar
format, for example using fig.savefig("name.pdf") command.
* **ax** -- Axes object from matplotlib.pyplot module that can be
used to tweak the plot, for example by adding a plot title
ax.set_title("Title goes here").
Example usage::
# Draws x-y projection of tight-binding model
# tweaks figure and saves it as a PDF.
(fig, ax) = tb.visualize(0, 1)
ax.set_title("Title goes here")
fig.savefig("model.pdf")
See also these examples: :ref:`edge-example`,
:ref:`visualize-example`.
"""
# check the format of eig_dr
if not (eig_dr is None):
if eig_dr.shape!=(self._norb,):
raise Exception("\n\nWrong format of eig_dr! Must be array of size norb.")
# check that ph_color is correct
if ph_color not in ["black","red-blue","wheel"]:
raise Exception("\n\nWrong value of ph_color parameter!")
# check if dir_second had to be specified
if dir_second==None and self._dim_r>1:
raise Exception("\n\nNeed to specify index of second coordinate for projection!")
# start a new figure
import matplotlib.pyplot as plt
fig=plt.figure(figsize=[plt.rcParams["figure.figsize"][0],
plt.rcParams["figure.figsize"][0]])
ax=fig.add_subplot(111, aspect='equal')
def proj(v):
"Project vector onto drawing plane"
coord_x=v[dir_first]
if dir_second==None:
coord_y=0.0
else:
coord_y=v[dir_second]
return [coord_x,coord_y]
def to_cart(red):
"Convert reduced to Cartesian coordinates"
return np.dot(red,self._lat)
# define colors to be used in plotting everything
# except eigenvectors
if (eig_dr is None) or ph_color=="black":
c_cell="b"
c_orb="r"
c_nei=[0.85,0.65,0.65]
c_hop="g"
else:
c_cell=[0.4,0.4,0.4]
c_orb=[0.0,0.0,0.0]
c_nei=[0.6,0.6,0.6]
c_hop=[0.0,0.0,0.0]
# determine color scheme for eigenvectors
def color_to_phase(ph):
if ph_color=="black":
return "k"
if ph_color=="red-blue":
ph=np.abs(ph/np.pi)
return [1.0-ph,0.0,ph]
if ph_color=="wheel":
if ph<0.0:
ph=ph+2.0*np.pi
ph=6.0*ph/(2.0*np.pi)
x_ph=1.0-np.abs(ph%2.0-1.0)
if ph>=0.0 and ph<1.0: ret_col=[1.0 ,x_ph,0.0 ]
if ph>=1.0 and ph<2.0: ret_col=[x_ph,1.0 ,0.0 ]
if ph>=2.0 and ph<3.0: ret_col=[0.0 ,1.0 ,x_ph]
if ph>=3.0 and ph<4.0: ret_col=[0.0 ,x_ph,1.0 ]
if ph>=4.0 and ph<5.0: ret_col=[x_ph,0.0 ,1.0 ]
if ph>=5.0 and ph<=6.0: ret_col=[1.0 ,0.0 ,x_ph]
return ret_col
# draw origin
ax.plot([0.0],[0.0],"o",c=c_cell,mec="w",mew=0.0,zorder=7,ms=4.5)
# first draw unit cell vectors which are considered to be periodic
for i in self._per:
# pick a unit cell vector and project it down to the drawing plane
vec=proj(self._lat[i])
ax.plot([0.0,vec[0]],[0.0,vec[1]],"-",c=c_cell,lw=1.5,zorder=7)
# now draw all orbitals
for i in range(self._norb):
# find position of orbital in cartesian coordinates
pos=to_cart(self._orb[i])
pos=proj(pos)
ax.plot([pos[0]],[pos[1]],"o",c=c_orb,mec="w",mew=0.0,zorder=10,ms=4.0)
# draw hopping terms
if draw_hoppings==True:
for h in self._hoppings:
# draw both i->j+R and i-R->j hop
for s in range(2):
# get "from" and "to" coordinates
pos_i=np.copy(self._orb[h[1]])
pos_j=np.copy(self._orb[h[2]])
# add also lattice vector if not 0-dim
if self._dim_k!=0:
if s==0:
pos_j[self._per]=pos_j[self._per]+h[3][self._per]
if s==1:
pos_i[self._per]=pos_i[self._per]-h[3][self._per]
# project down vector to the plane
pos_i=np.array(proj(to_cart(pos_i)))
pos_j=np.array(proj(to_cart(pos_j)))
# add also one point in the middle to bend the curve
prcnt=0.05 # bend always by this ammount
pos_mid=(pos_i+pos_j)*0.5
dif=pos_j-pos_i # difference vector
orth=np.array([dif[1],-1.0*dif[0]]) # orthogonal to difference vector
orth=orth/np.sqrt(np.dot(orth,orth)) # normalize
pos_mid=pos_mid+orth*prcnt*np.sqrt(np.dot(dif,dif)) # shift mid point in orthogonal direction
# draw hopping
all_pnts=np.array([pos_i,pos_mid,pos_j]).T
ax.plot(all_pnts[0],all_pnts[1],"-",c=c_hop,lw=0.75,zorder=8)
# draw "from" and "to" sites
ax.plot([pos_i[0]],[pos_i[1]],"o",c=c_nei,zorder=9,mew=0.0,ms=4.0,mec="w")
ax.plot([pos_j[0]],[pos_j[1]],"o",c=c_nei,zorder=9,mew=0.0,ms=4.0,mec="w")
# now draw the eigenstate
if not (eig_dr is None):
for i in range(self._norb):
# find position of orbital in cartesian coordinates
pos=to_cart(self._orb[i])
pos=proj(pos)
# find norm of eigenfunction at this point
nrm=(eig_dr[i]*eig_dr[i].conjugate()).real
# rescale and get size of circle
nrm_rad=2.0*nrm*float(self._norb)
# get color based on the phase of the eigenstate
phase=np.angle(eig_dr[i])
c_ph=color_to_phase(phase)
ax.plot([pos[0]],[pos[1]],"o",c=c_ph,mec="w",mew=0.0,ms=nrm_rad,zorder=11,alpha=0.8)
# center the image
# first get the current limit, which is probably tight
xl=ax.set_xlim()
yl=ax.set_ylim()
# now get the center of current limit
centx=(xl[1]+xl[0])*0.5
centy=(yl[1]+yl[0])*0.5
# now get the maximal size (lengthwise or heightwise)
mx=max([xl[1]-xl[0],yl[1]-yl[0]])
# set new limits
extr=0.05 # add some boundary as well
ax.set_xlim(centx-mx*(0.5+extr),centx+mx*(0.5+extr))
ax.set_ylim(centy-mx*(0.5+extr),centy+mx*(0.5+extr))
# return a figure and axes to the user
return (fig,ax)
def get_num_orbitals(self):
"Returns number of orbitals in the model."
return self._norb
def get_orb(self):
"Returns reduced coordinates of orbitals in format [orbital,coordinate.]"
return self._orb.copy()
def get_lat(self):
"Returns lattice vectors in format [vector,coordinate]."
return self._lat.copy()
def _gen_ham(self,k_input=None):
"""Generate Hamiltonian for a certain k-point,
K-point is given in reduced coordinates!"""
kpnt=np.array(k_input)
if not (k_input is None):
# if kpnt is just a number then convert it to an array
if len(kpnt.shape)==0:
kpnt=np.array([kpnt])
# check that k-vector is of corect size
if kpnt.shape!=(self._dim_k,):
raise Exception("\n\nk-vector of wrong shape!")
else:
if self._dim_k!=0:
raise Exception("\n\nHave to provide a k-vector!")
# zero the Hamiltonian matrix
if self._nspin==1:
ham=np.zeros((self._norb,self._norb),dtype=complex)
elif self._nspin==2:
ham=np.zeros((self._norb,2,self._norb,2),dtype=complex)
# modify diagonal elements
for i in range(self._norb):
if self._nspin==1:
ham[i,i]=self._site_energies[i]
elif self._nspin==2:
ham[i,:,i,:]=self._site_energies[i]
# go over all hoppings
for hopping in self._hoppings:
# get all data for the hopping parameter
if self._nspin==1:
amp=complex(hopping[0])
elif self._nspin==2:
amp=np.array(hopping[0],dtype=complex)
i=hopping[1]
j=hopping[2]
# in 0-dim case there is no phase factor
if self._dim_k>0:
ind_R=np.array(hopping[3],dtype=float)
# vector from one site to another
rv=-self._orb[i,:]+self._orb[j,:]+ind_R
# Take only components of vector which are periodic
rv=rv[self._per]
# Calculate the hopping, see details in info/tb/tb.pdf
phase=np.exp((2.0j)*np.pi*np.dot(kpnt,rv))
amp=amp*phase
# add this hopping into a matrix and also its conjugate
if self._nspin==1:
ham[i,j]+=amp
ham[j,i]+=amp.conjugate()
elif self._nspin==2:
ham[i,:,j,:]+=amp
ham[j,:,i,:]+=amp.T.conjugate()
return ham
def _sol_ham(self,ham,eig_vectors=False):
"""Solves Hamiltonian and returns eigenvectors, eigenvalues"""
# reshape matrix first
if self._nspin==1:
ham_use=ham
elif self._nspin==2:
ham_use=ham.reshape((2*self._norb,2*self._norb))
# check that matrix is hermitian
if np.max(ham_use-ham_use.T.conj())>1.0E-9:
raise Exception("\n\nHamiltonian matrix is not hermitian?!")
#solve matrix
if eig_vectors==False: # only find eigenvalues
eval=np.linalg.eigvalsh(ham_use)
# sort eigenvalues and convert to real numbers
eval=_nicefy_eig(eval)
return np.array(eval,dtype=float)
else: # find eigenvalues and eigenvectors
(eval,eig)=np.linalg.eigh(ham_use)
# transpose matrix eig since otherwise it is confusing
# now eig[i,:] is eigenvector for eval[i]-th eigenvalue
eig=eig.T
# sort evectors, eigenvalues and convert to real numbers
(eval,eig)=_nicefy_eig(eval,eig)
# reshape eigenvectors if doing a spinfull calculation
if self._nspin==2:
eig=eig.reshape((self._nsta,self._norb,2))
return (eval,eig)
def solve_all(self,k_list=None,eig_vectors=False):
r"""
Solves for eigenvalues and (optionally) eigenvectors of the
tight-binding model on a given one-dimensional list of k-vectors.
.. note::
Eigenvectors (wavefunctions) returned by this
function and used throughout the code are exclusively given
in convention 1 as described in section 3.1 of
:download:`notes on tight-binding formalism
<misc/pythtb-formalism.pdf>`. In other words, they
are in correspondence with cell-periodic functions
:math:`u_{n {\bf k}} ({\bf r})` not
:math:`\Psi_{n {\bf k}} ({\bf r})`.
.. note::
In some cases class :class:`pythtb.wf_array` provides a more
elegant way to deal with eigensolutions on a regular mesh of
k-vectors.
:param k_list: One-dimensional array of k-vectors. Each k-vector
is given in reduced coordinates of the reciprocal space unit
cell. For example, for real space unit cell vectors [1.0,0.0]
and [0.0,2.0] and associated reciprocal space unit vectors
[2.0*pi,0.0] and [0.0,pi], k-vector with reduced coordinates
[0.25,0.25] corresponds to k-vector [0.5*pi,0.25*pi].
Dimensionality of each vector must equal to the number of
periodic directions (i.e. dimensionality of reciprocal space,
*dim_k*).
This parameter shouldn't be specified for system with
zero-dimensional k-space (*dim_k* =0).
:param eig_vectors: Optional boolean parameter, specifying whether
eigenvectors should be returned. If *eig_vectors* is True, then
both eigenvalues and eigenvectors are returned, otherwise only