/
lattice.py
1480 lines (1312 loc) · 63.3 KB
/
lattice.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
"""Classes to define the lattice structure of a model.
The base class :class:`Lattice` defines the general structure of a lattice,
you can subclass this to define you own lattice.
The :class:`SimpleLattice` is a slight simplification for lattices with a single-site unit cell.
Further, we have some predefined lattices, namely
:class:`Chain`, :class:`Ladder` in 1D and
:class:`Square`, :class:`Triangular`, :class:`Honeycomb`, and :class:`Kagome` in 2D.
See also the :doc:`/intro_model`.
"""
# Copyright 2018-2019 TeNPy Developers, GNU GPLv3
import numpy as np
import itertools
from ..networks.site import Site
from ..tools.misc import to_iterable, inverse_permutation
from ..networks.mps import MPS # only to check boundary conditions
__all__ = [
'Lattice', 'TrivialLattice', 'IrregularLattice', 'SimpleLattice', 'Chain', 'Ladder', 'Square',
'Triangular', 'Honeycomb', 'Kagome', 'get_lattice', 'get_order', 'get_order_grouped'
]
# (update module doc string if you add further lattices)
bc_choices = {'open': True, 'periodic': False}
"""dict: maps possible choices of boundary conditions in a lattice to bool/int."""
class Lattice:
r"""A general, regular lattice.
The lattice consists of a **unit cell** which is repeated in `dim` different directions.
A site of the lattice is thus identified by **lattice indices** ``(x_0, ..., x_{dim-1}, u)``,
where ``0 <= x_l < Ls[l]`` pick the position of the unit cell in the lattice and
``0 <= u < len(unit_cell)`` picks the site within the unit cell. The site is located
in 'space' at ``sum_l x_l*basis[l] + unit_cell_positions[u]`` (see :meth:`position`).
(Note that the position in space is only used for plotting, not for defining the couplings.)
In addition to the pure geometry, this class also defines an `order` of all sites.
This order maps the lattice to a finite 1D chain and defines the geometry of MPSs and MPOs.
The **MPS index** `i` corresponds thus to the lattice sites given by
``(x_0, ..., x_{dim-1}, u) = tuple(self.order[i])``.
Infinite boundary conditions of the MPS repeat in the first spatial direction of the lattice,
i.e., if the site at (x_0, x_1, ..., x_{dim-1},u)`` has MPS index `i`, the site at
at ``(x_0 + a*Ls[0], x_1 ..., x_{dim-1}, u)`` corresponds to MPS index ``i + N_sites``.
Use :meth:`mps2lat_idx` and :meth:`lat2mps_idx` for conversion of indices.
The function :meth:`mps2lat_values` performs the necessary reshaping and re-ordering from
arrays indexed in MPS form to arrays indexed in lattice form.
Parameters
----------
Ls : list of int
the length in each direction
unit_cell : list of :class:`~tenpy.networks.site.Site`
The sites making up a unit cell of the lattice.
If you want to specify it only after initialization, use ``None`` entries in the list.
order : str | ``('standard', snake_winding, priority)`` | ``('grouped', groups)``
A string or tuple specifying the order, given to :meth:`ordering`.
bc : (iterable of) {'open' | 'periodic' | int}
Boundary conditions in each direction of the lattice.
A single string holds for all directions.
An integer `shift` means that we have periodic boundary conditions along this direction,
but shift/tilt by ``-shift*lattice.basis[0]`` (~cylinder axis for ``bc_MPS='infinite'``)
when going around the boundary along this direction.
bc_MPS : 'finite' | 'segment' | 'infinite'
Boundary conditions for an MPS/MPO living on the ordered lattice.
If the system is ``'infinite'``, the infinite direction is always along the first basis
vector (justifying the definition of `N_rings` and `N_sites_per_ring`).
basis : iterable of 1D arrays
For each direction one translation vectors shifting the unit cell.
Defaults to the standard ONB ``np.eye(dim)``.
positions : iterable of 1D arrays
For each site of the unit cell the position within the unit cell.
Defaults to ``np.zeros((len(unit_cell), dim))``.
nearest_neighbors : ``None`` | list of ``(u1, u2, dx)``
May be unspecified (``None``), otherwise it gives a list of parameters `u1`, `u2`, `dx`
as needed for the :meth:`~tenpy.models.model.CouplingModel` to generate nearest-neighbor
couplings.
Note that we include each coupling only in one direction; to get both directions, use
``nearest_neighbors + [(u2, u1, -dx) for (u1, u2, dx) in nearest_neighbors]``.
next_nearest_neighbors : ``None`` | list of ``(u1, u2, dx)``
Same as `nearest_neighbors`, but for the next-nearest neighbors.
next_next_nearest_neighbors : ``None`` | list of ``(u1, u2, dx)``
Same as `nearest_neighbors`, but for the next-next-nearest neighbors.
Attributes
----------
dim : int
order : ndarray (N_sites, dim+1)
N_cells : int
the number of unit cells in the lattice, ``np.prod(self.Ls)``.
N_sites : int
the number of sites in the lattice, ``np.prod(self.shape)``.
N_sites_per_ring : int
Defined as ``N_sites / Ls[0]``, for an infinite system the number of cites per "ring".
N_rings : int
Alias for ``Ls[0]``, for an infinite system the number of "rings" in the unit cell.
Ls : tuple of int
the length in each direction.
shape : tuple of int
the 'shape' of the lattice, same as ``Ls + (len(unit_cell), )``
unit_cell : list of :class:`~tenpy.networks.site.Site`
the lattice sites making up a unit cell of the lattice.
bc : bool ndarray
Boundary conditions of the couplings in each direction of the lattice,
translated into a bool array with the global `bc_choices`.
bc_shift : None | ndarray(int)
The shift in x-direction when going around periodic boundaries in other directions.
bc_MPS : 'finite' | 'segment' | 'infinite'
Boundary conditions for an MPS/MPO living on the ordered lattice.
If the system is ``'infinite'``, the infinite direction is always along the first basis
vector (justifying the definition of `N_rings` and `N_sites_per_ring`).
basis : ndarray (dim, Dim)
translation vectors shifting the unit cell. The row `i` gives the vector shifting in
direction `i`.
unit_cell_positions : ndarray, shape (len(unit_cell), Dim)
for each site in the unit cell a vector giving its position within the unit cell.
nearest_neighbors : ``None`` | list of ``(u1, u2, dx)``
May be unspecified (``None``), otherwise it gives a list of parameters `u1`, `u2`, `dx`
as needed for the :meth:`~tenpy.models.model.CouplingModel` to generate nearest-neighbor
couplings.
Note that we include each coupling only in one direction; to get both directions, use
``nearest_neighbors + [(u2, u1, -dx) for (u1, u2, dx) in nearest_neighbors]``.
next_nearest_neighbors : ``None`` | list of ``(u1, u2, dx)``
Same as :attr:`nearest_neighbors`, but for the next-nearest neighbors.
next_next_nearest_neighbors : ``None`` | list of ``(u1, u2, dx)``
Same as :attr:`nearest_neighbors`, but for the next-next-nearest neighbors.
_order : ndarray (N_sites, dim+1)
The place where :attr:`order` is stored.
_strides : ndarray (dim, )
necessary for :meth:`mps2lat_idx`
_perm : ndarray (N, )
permutation needed to make `order` lexsorted.
_mps2lat_vals_idx : ndarray `shape`
index array for reshape/reordering in :meth:`mps2lat_vals`
_mps_fix_u : tuple of ndarray (N_cells, ) np.intp
for each site of the unit cell an index array selecting the mps indices of that site.
_mps2lat_vals_idx_fix_u : tuple of ndarray of shape `Ls`
similar as `_mps2lat_vals_idx`, but for a fixed `u` picking a site from the unit cell.
"""
def __init__(self,
Ls,
unit_cell,
order='default',
bc='open',
bc_MPS='finite',
basis=None,
positions=None,
nearest_neighbors=None,
next_nearest_neighbors=None,
next_next_nearest_neighbors=None):
self.Ls = tuple([int(L) for L in Ls])
self.unit_cell = list(unit_cell)
self.N_cells = int(np.prod(self.Ls))
self.shape = self.Ls + (len(unit_cell), )
self.N_sites = int(np.prod(self.shape))
self.N_rings = self.Ls[0]
self.N_sites_per_ring = int(self.N_sites // self.N_rings)
if positions is None:
positions = np.zeros((len(self.unit_cell), self.dim))
if basis is None:
basis = np.eye(self.dim)
self.unit_cell_positions = np.array(positions)
self.basis = np.array(basis)
self._set_bc(bc)
self.bc_MPS = bc_MPS
# calculate order for MPS
self.order = self.ordering(order)
# uses attribute setter to calculte _mps2lat_vals_idx_fix_u etc and lat2mps
# calculate _strides
strides = [1]
for L in self.Ls:
strides.append(strides[-1] * L)
self._strides = np.array(strides, np.intp)
self.nearest_neighbors = nearest_neighbors
self.next_nearest_neighbors = next_nearest_neighbors
self.next_next_nearest_neighbors = next_next_nearest_neighbors
self.test_sanity() # check consistency
def test_sanity(self):
"""Sanity check.
Raises ValueErrors, if something is wrong.
"""
assert self.dim == len(self.Ls)
assert self.shape == self.Ls + (len(self.unit_cell), )
assert self.N_cells == np.prod(self.Ls)
assert self.N_sites == np.prod(self.shape)
if self.bc.shape != (self.dim, ):
raise ValueError("Wrong len of bc")
assert self.bc.dtype == np.bool
chinfo = None
for site in self.unit_cell:
if site is None:
continue
if chinfo is None:
chinfo = site.leg.chinfo
if not isinstance(site, Site):
raise ValueError("element of Unit cell is not Site.")
if site.leg.chinfo != chinfo:
raise ValueError("All sites must have the same ChargeInfo!")
if self.basis.shape[0] != self.dim:
raise ValueError("Need one basis vector for each direction!")
if self.unit_cell_positions.shape[0] != len(self.unit_cell):
raise ValueError("Need one position for each site in the unit cell.")
if self.basis.shape[1] != self.unit_cell_positions.shape[1]:
raise ValueError("Different space dimensions of `basis` and `unit_cell_positions`")
# if one of the following assert fails, the `ordering` function returned an invalid array
assert np.all(self._order >= 0) and np.all(self._order <= self.shape) # entries of `order`
assert np.all(
np.sum(self._order * self._strides,
axis=1)[self._perm] == np.arange(self.N_sites)) # rows of `order` unique?
if self.bc_MPS not in MPS._valid_bc:
raise ValueError("invalid MPS boundary conditions")
if self.bc[0] and self.bc_MPS == 'infinite':
raise ValueError("Need periodic boundary conditions along the x-direction "
"for 'infinite' `bc_MPS`")
@property
def dim(self):
"""The dimension of the lattice."""
return len(self.Ls)
@property
def order(self):
"""Defines an ordering of the lattice sites, thus mapping the lattice to a 1D chain.
This order defines how an MPS/MPO winds through the lattice.
"""
return self._order
@order.setter
def order(self, order_):
# update the value itself
self._order = order_
# and the other stuff which is cached
self._perm = np.lexsort(order_.T)
# use advanced numpy indexing...
self._mps2lat_vals_idx = np.empty(self.shape, np.intp)
self._mps2lat_vals_idx[tuple(order_.T)] = np.arange(self.N_sites)
# versions for fixed u
self._mps_fix_u = []
self._mps2lat_vals_idx_fix_u = []
for u in range(len(self.unit_cell)):
mps_fix_u = np.nonzero(order_[:, -1] == u)[0]
self._mps_fix_u.append(mps_fix_u)
mps2lat_vals_idx = np.empty(self.Ls, np.intp)
mps2lat_vals_idx[tuple(order_[mps_fix_u, :-1].T)] = np.arange(self.N_cells)
self._mps2lat_vals_idx_fix_u.append(mps2lat_vals_idx)
self._mps_fix_u = tuple(self._mps_fix_u)
def ordering(self, order):
"""Provide possible orderings of the `N` lattice sites.
This function can be overwritten by derived lattices to define additional orderings.
The following orders are defined in this method:
================== =========================== =============================
`order` equivalent `priority` equivalent ``snake_winding``
================== =========================== =============================
``'Cstyle'`` (0, 1, ..., dim-1, dim) (False, ..., False, False)
``'default'``
``'snake'`` (0, 1, ..., dim-1, dim) (True, ..., True, True)
``'snakeCstyle'``
``'Fstyle'`` (dim-1, ..., 1, 0, dim) (False, ..., False, False)
``'snakeFstyle'`` (dim-1, ..., 1, 0, dim) (False, ..., False, False)
================== =========================== =============================
Parameters
----------
order : str | ``('standard', snake_winding, priority)`` | ``('grouped', groups)``
Specifies the desired ordering using one of the strings of the above tables.
Alternatively, an ordering is specified by a tuple with first entry specifying a
function, ``'standard'`` for :func:`get_order` and ``'grouped'`` for
:func:`get_order_grouped`, and other arguments in the tuple as specified in the
documentation of these functions.
Returns
-------
order : array, shape (N, D+1), dtype np.intp
the order to be used for :attr:`order`.
See also
--------
get_order : generates the `order` from equivalent `priority` and `snake_winding`.
get_order_grouped : variant of `get_order`.
plot_order : visualizes the resulting `order`.
"""
if isinstance(order, str):
if order in ["default", "Cstyle"]:
priority = None
snake_winding = (False, ) * (self.dim + 1)
elif order == "Fstyle":
priority = range(self.dim, -1, -1)
snake_winding = (False, ) * (self.dim + 1)
elif order in ["snake", "snakeCstyle"]:
priority = None
snake_winding = (True, ) * (self.dim + 1)
elif order == "snakeFstyle":
priority = range(self.dim, -1, -1)
snake_winding = (True, ) * (self.dim + 1)
else:
# in a derived lattice use ``return super().ordering(order)`` as last option
# such that the derived lattice also has the orderings defined in this function.
raise ValueError("unknown ordering " + repr(order))
else:
descr = order[0]
if descr == 'standard':
snake_winding, priority = order[1], order[2]
elif descr == 'grouped':
return get_order_grouped(self.shape, order[1])
else:
raise ValueError("unknown ordering " + repr(order))
return get_order(self.shape, snake_winding, priority)
def position(self, lat_idx):
"""return 'space' position of one or multiple sites.
Parameters
----------
lat_idx : ndarray, ``(... , dim+1)``
Lattice indices.
Returns
-------
pos : ndarray, ``(..., dim)``
The position of the lattice sites specified by `lat_idx` in real-space.
"""
idx = self._asvalid_latidx(lat_idx)
res = np.take(self.unit_cell_positions, idx[..., -1], axis=0)
for i in range(self.dim):
res += idx[..., i, np.newaxis] * self.basis[i]
return res
def site(self, i):
"""return :class:`~tenpy.networks.site.Site` instance corresponding to an MPS index `i`"""
return self.unit_cell[self.order[i, -1]]
def mps_sites(self):
"""Return a list of sites for all MPS indices.
Equivalent to ``[self.site(i) for i in range(self.N_sites)]``.
This should be used for `sites` of 1D tensor networks (MPS, MPO,...).
"""
return [self.unit_cell[u] for u in self.order[:, -1]]
def mps2lat_idx(self, i):
"""Translate MPS index `i` to lattice indices ``(x_0, ..., x_{dim-1}, u)``.
Parameters
----------
i : int | array_like of int
MPS index/indices.
Returns
-------
lat_idx : array
First dimensions like `i`, last dimension has len `dim`+1 and contains the lattice
indices ``(x_0, ..., x_{dim-1}, u)`` corresponding to `i`.
For `i` accross the MPS unit cell and "infinite" `bc_MPS`, we shift `x_0` accordingly.
"""
if self.bc_MPS == 'infinite':
# allow `i` outsit of MPS unit cell for bc_MPS infinite
i0 = i
i = np.mod(i, self.N_sites)
if np.any(i0 != i):
lat = self.order[i].copy()
lat[..., 0] += (i0 - i) // self.N_sites * self.N_rings
return lat
return self.order[i].copy()
def lat2mps_idx(self, lat_idx):
"""Translate lattice indices ``(x_0, ..., x_{D-1}, u)`` to MPS index `i`.
Parameters
----------
lat_idx : array_like [..., dim+1]
The last dimension corresponds to lattice indices ``(x_0, ..., x_{D-1}, u)``.
All lattice indices should be positive and smaller than the corresponding entry in
``self.shape``. Exception: for "infinite" `bc_MPS`, an `x_0` outside indicates shifts
accross the boundary.
Returns
-------
i : array_like
MPS index/indices corresponding to `lat_idx`.
Has the same shape as `lat_idx` without the last dimension.
"""
idx = self._asvalid_latidx(lat_idx)
if self.bc_MPS == 'infinite':
i_shift = idx[..., 0] - np.mod(idx[..., 0], self.N_rings)
idx[..., 0] -= i_shift
i = np.sum(np.mod(idx, self.shape) * self._strides, axis=-1) # before permutation
i = np.take(self._perm, i) # after permutation
if self.bc_MPS == 'infinite':
i += i_shift * (self.N_sites // self.N_rings)
return i
def mps_idx_fix_u(self, u=None):
"""return an index array of MPS indices for which the site within the unit cell is `u`.
If you have multiple sites in your unit-cell, an onsite operator is in general not defined
for all sites. This functions returns an index array of the mps indices which belong to
sites given by ``self.unit_cell[u]``.
Parameters
----------
u : None | int
Selects a site of the unit cell. ``None`` (default) means all sites.
Returns
-------
mps_idx : array
MPS indices for which ``self.site(i) is self.unit_cell[u]``. Ordered ascending.
"""
if u is not None:
return self._mps_fix_u[u]
return self._perm
def mps_lat_idx_fix_u(self, u=None):
"""Similar as :meth:`mps_idx_fix_u`, but return also the corresponding lattice indices.
Parameters
----------
u : None | int
Selects a site of the unit cell. ``None`` (default) means all sites.
Returns
-------
mps_idx : array
MPS indices `i` for which ``self.site(i) is self.unit_cell[u]``.
lat_idx : 2D array
The row `j` contains the lattice index (without `u`) corresponding to ``mps_idx[j]``.
"""
mps_idx = self.mps_idx_fix_u(u)
return mps_idx, self.order[mps_idx, :-1]
def mps2lat_values(self, A, axes=0, u=None):
"""Reshape/reorder `A` to replace an MPS index by lattice indices.
Parameters
----------
A : ndarray
Some values. Must have ``A.shape[axes] = self.N_sites`` if `u` is ``None``, or
``A.shape[axes] = self.N_cells`` if `u` is an int.
axes : (iterable of) int
chooses the axis which should be replaced.
u : ``None`` | int
Optionally choose a subset of MPS indices present in the axes of `A`, namely the
indices corresponding to ``self.unit_cell[u]``, as returned by :meth:`mps_idx_fix_u`.
The resulting array will not have the additional dimension(s) of `u`.
Returns
-------
res_A : ndarray
Reshaped and reordered verions of A. Such that an MPS index `j` is replaced by
``res_A[..., self.order, ...] = A[..., np.arange(self.N_sites), ...]``
Examples
--------
Say you measure expection values of an onsite term for an MPS, which gives you an 1D array
`A`, where `A[i]` is the expectation value of the site given by ``self.mps2lat_idx(i)``.
Then this function gives you the expectation values ordered by the lattice:
>>> print(lat.shape, A.shape)
(10, 3, 2) (60,)
>>> A_res = lat.mps2lat_values(A)
>>> A_res.shape
(10, 3, 2)
>>> A_res[lat.mps2lat_idx(5)] == A[5]
True
If you have a correlation function ``C[i, j]``, it gets just slightly more complicated:
>>> print(lat.shape, C.shape)
(10, 3, 2) (60, 60)
>>> lat.mps2lat_values(C, axes=[0, 1]).shape
(10, 3, 2, 10, 3, 2)
If the unit cell consists of different physical sites, an onsite operator might be defined
only on one of the sites in the unit cell. Then you can use :meth:`mps_idx_fix_u` to get
the indices of sites it is defined on, measure the operator on these sites, and use
the argument `u` of this function.
>>> u = 0
>>> idx_subset = lat.mps_idx_fix_u(u)
>>> A_u = A[idx_subset]
>>> A_u_res = lat.mps2lat_values(A_u, u=u)
>>> A_u_res.shape
(10, 3)
>>> np.all(A_res[:, :, u] == A_u_res[:, :])
True
.. todo ::
make sure this function is used for expectation values...
"""
axes = to_iterable(axes)
if len(axes) > 1:
axes = [(ax + A.ndim if ax < 0 else ax) for ax in axes]
for ax in reversed(sorted(axes)): # need to start with largest axis!
A = self.mps2lat_values(A, ax, u) # recursion with single axis
return A
# choose the appropriate index arrays
if u is None:
idx = self._mps2lat_vals_idx
else:
idx = self._mps2lat_vals_idx_fix_u[u]
return np.take(A, idx, axis=axes[0])
def number_nearest_neighbors(self, u=0):
"""Count the number of nearest neighbors for a site in the bulk.
Requires :attr:`nearest_neighbors` to be set.
Parameters
----------
u : int
Specifies the site in the unit cell.
Returns
-------
number_NN : int
Number of nearest neighbors of the `u`-th site in the unit cell in the bulk of the
lattice. Note that it might be different at the edges of the lattice for open boundary
conditions.
"""
if self.nearest_neighbors is None:
raise ValueError("self.nearest_neighbors were not specified")
count = 0
for u1, u2, dx in self.nearest_neighbors:
if u1 == u:
count += 1
if u2 == u:
count += 1
return count
def number_next_nearest_neighbors(self, u=0):
"""Count the number of next nearest neighbors for a site in the bulk.
Requires :attr:`next_nearest_neighbors` to be set.
Parameters
----------
u : int
Specifies the site in the unit cell.
Returns
-------
number_NNN : int
Number of next nearest neighbors of the `u`-th site in the unit cell in the bulk of the
lattice. Note that it might be different at the edges of the lattice for open boundary
conditions.
"""
if self.next_nearest_neighbors is None:
raise ValueError("self.next_nearest_neighbors were not specified")
count = 0
for u1, u2, dx in self.next_nearest_neighbors:
if u1 == u:
count += 1
if u2 == u:
count += 1
return count
def possible_couplings(self, u1, u2, dx):
"""Find possible MPS indices for two-site couplings.
For periodic boundary conditions (``bc[a] == False``)
the index ``x_a`` is taken modulo ``Ls[a]`` and runs through ``range(Ls[a])``.
For open boundary conditions, ``x_a`` is limited to ``0 <= x_a < Ls[a]`` and
``0 <= x_a+dx[a] < lat.Ls[a]``.
Parameters
----------
u1, u2 : int
Indices within the unit cell; the `u1` and `u2` of
:meth:`~tenpy.models.model.CouplingModel.add_coupling`
dx : array
Length :attr:`dim`. The translation in terms of basis vectors for the coupling.
Returns
-------
mps1, mps2 : array
For each possible two-site coupling the MPS indices for the `u1` and `u2`.
lat_indices : 2D int array
Rows of `lat_indices` correspond to rows of `mps_ijkl` and contain the lattice indices
of the "lower left corner" of the box containing the coupling.
coupling_shape : tuple of int
Len :attr:`dim`. The correct shape for an array specifying the coupling strength.
`lat_indices` has only rows within this shape.
"""
coupling_shape, shift_lat_indices = self.coupling_shape(dx)
if any([s == 0 for s in coupling_shape]):
return [], [], np.zeros([0, self.dim]), coupling_shape
Ls = np.array(self.Ls)
N_sites = self.N_sites
mps_i, lat_i = self.mps_lat_idx_fix_u(u1)
lat_j_shifted = lat_i + dx
lat_j = np.mod(lat_j_shifted, Ls) # assuming PBC
if self.bc_shift is not None:
shift = np.sum(((lat_j_shifted - lat_j) // Ls)[:, 1:] * self.bc_shift, axis=1)
lat_j_shifted[:, 0] -= shift
lat_j[:, 0] = np.mod(lat_j_shifted[:, 0], Ls[0])
keep = np.all(
np.logical_or(
lat_j_shifted == lat_j, # not accross the boundary
np.logical_not(self.bc)), # direction has PBC
axis=1)
mps_i = mps_i[keep]
lat_indices = lat_i[keep] + shift_lat_indices[np.newaxis, :]
lat_indices = np.mod(lat_indices, coupling_shape)
lat_j = lat_j[keep]
lat_j_shifted = lat_j_shifted[keep]
mps_j = self.lat2mps_idx(np.concatenate([lat_j, [[u2]] * len(lat_j)], axis=1))
if self.bc_MPS == 'infinite':
# shift j by whole MPS unit cells for couplings along the infinite direction
mps_j_shift = (lat_j_shifted[:, 0] - lat_j[:, 0]) * (N_sites // Ls[0])
mps_j += mps_j_shift
# finally, ensure 0 <= min(i, j) < N_sites.
mps_ij_shift = np.where(mps_j_shift < 0, -mps_j_shift, 0)
mps_i += mps_ij_shift
mps_j += mps_ij_shift
return mps_i, mps_j, lat_indices, coupling_shape
def possible_multi_couplings(self, u0, other_us, dx):
"""Generalization of :meth:`possible_couplings` to couplings with more than 2 sites.
Given the arguments of :meth:`~tenpy.models.model.MultiCouplingModel.add_coupling`
determine the necessary shape of `strength`.
Parameters
----------
u0 : int
Argument `u0` of :meth:`~tenpy.models.model.MultiCouplingModel.add_multi_coupling`.
other_us : list of int
The `u` of the `other_ops` in
:meth:`~tenpy.models.model.MultiCouplingModel.add_multi_coupling`.
dx : array, shape (len(other_us), lat.dim+1)
The `dx` specifying relative operator positions of the `other_ops` in
:meth:`~tenpy.models.model.MultiCouplingModel.add_multi_coupling`.
Returns
-------
mps_ijkl : 2D int array
Each row contains MPS indices `i,j,k,l,...`` for each of the operators positions.
The positions are defined by `dx` (j,k,l,... relative to `i`) and boundary coundary
conditions of `self` (how much the `box` for given `dx` can be shifted around without
hitting a boundary - these are the different rows).
lat_indices : 2D int array
Rows of `lat_indices` correspond to rows of `mps_ijkl` and contain the lattice indices
of the "lower left corner" of the box containing the coupling.
coupling_shape : tuple of int
Len :attr:`dim`. The correct shape for an array specifying the coupling strength.
`lat_indices` has only rows within this shape.
"""
coupling_shape, shift_lat_indices = self.multi_coupling_shape(dx)
if any([s == 0 for s in coupling_shape]):
return [], [], [], coupling_shape
Ls = np.array(self.Ls)
N_sites = self.N_sites
mps_i, lat_i = self.mps_lat_idx_fix_u(u0)
lat_jkl_shifted = lat_i[:, np.newaxis, :] + dx[np.newaxis, :, :]
# lat_jkl* has 3 axes "initial site", "other_op", "spatial directions"
lat_jkl = np.mod(lat_jkl_shifted, Ls) # assuming PBC
if self.bc_shift is not None:
shift = np.sum(((lat_jkl_shifted - lat_jkl) // Ls)[:, :, 1:] * self.bc_shift, axis=2)
lat_jkl_shifted[:, :, 0] -= shift
lat_jkl[:, :, 0] = np.mod(lat_jkl_shifted[:, :, 0], Ls[0])
keep = np.all(
np.logical_or(
lat_jkl_shifted == lat_jkl, # not accross the boundary
np.logical_not(self.bc)), # direction has PBC
axis=(1, 2))
mps_i = mps_i[keep]
lat_indices = lat_i[keep, :] + shift_lat_indices[np.newaxis, :]
lat_indices = np.mod(lat_indices, coupling_shape)
lat_jkl = lat_jkl[keep, :, :]
lat_jkl_shifted = lat_jkl_shifted[keep, :, :]
latu_jkl = np.concatenate((lat_jkl, np.array([other_us] * len(lat_jkl))[:, :, np.newaxis]),
axis=2)
mps_jkl = self.lat2mps_idx(latu_jkl)
if self.bc_MPS == 'infinite':
# shift by whole MPS unit cells for couplings along the infinite direction
mps_jkl += (lat_jkl_shifted[:, :, 0] - lat_jkl[:, :, 0]) * (N_sites // Ls[0])
mps_ijkl = np.concatenate((mps_i[:, np.newaxis], mps_jkl), axis=1)
return mps_ijkl, lat_indices, coupling_shape
def coupling_shape(self, dx):
"""Calculate correct shape of the `strengths` for a coupling.
Parameters
----------
dx : tuple of int
Translation vector in the lattice for a coupling of two operators.
Returns
-------
coupling_shape : tuple of int
Len :attr:`dim`. The correct shape for an array specifying the coupling strength.
`lat_indices` has only rows within this shape.
shift_lat_indices : array
Translation vector from lower left corner of box spanned by `dx` to the origin.
"""
shape = [La - abs(dxa) * int(bca) for La, dxa, bca in zip(self.Ls, dx, self.bc)]
shift_strength = [min(0, dxa) for dxa in dx]
return tuple(shape), np.array(shift_strength)
def multi_coupling_shape(self, dx):
"""Calculate correct shape of the `strengths` for a multi_coupling.
Parameters
----------
dx : tuple of int
Translation vector in the lattice for a coupling of two operators.
Returns
-------
coupling_shape : tuple of int
Len :attr:`dim`. The correct shape for an array specifying the coupling strength.
`lat_indices` has only rows within this shape.
shift_lat_indices : array
Translation vector from lower left corner of box spanned by `dx` to the origin.
"""
Ls = self.Ls
shape = [None] * len(Ls)
shift_strength = [None] * len(Ls)
for a in range(len(Ls)):
max_dx, min_dx = np.max(dx[:, a]), np.min(dx[:, a])
box_dx = max(max_dx, 0) - min(min_dx, 0)
shape[a] = Ls[a] - box_dx * int(self.bc[a])
shift_strength[a] = min(0, min_dx)
return tuple(shape), np.array(shift_strength)
def plot_sites(self, ax, markers=['o', '^', 's', 'p', 'h', 'D'], **kwargs):
"""Plot the sites of the lattice with markers.
Parameters
----------
ax : :class:`matplotlib.axes.Axes`
The axes on which we should plot.
markers : list
List of values for the keywork `marker` of ``ax.plot()`` to distinguish the different
sites in the unit cell, a site `u` in the unit cell is plotted with a marker
``markers[u % len(markers)]``.
**kwargs :
Further keyword arguments given to ``ax.plot()``.
"""
kwargs.setdefault("linestyle", 'None')
use_marker = ('marker' not in kwargs)
for u in range(len(self.unit_cell)):
pos = self.position(self.order[self.mps_idx_fix_u(u), :])
if pos.shape[1] == 1:
pos = pos * np.array([[1., 0]]) # use broadcasting to add a column with zeros
if pos.shape[1] != 2:
raise ValueError("can only plot in 2 dimensions.")
if use_marker:
kwargs['marker'] = markers[u % len(markers)]
ax.plot(pos[:, 0], pos[:, 1], **kwargs)
def plot_order(self, ax, order=None, textkwargs={}, **kwargs):
"""Plot a line connecting sites in the specified "order" and text labels enumerating them.
Parameters
----------
ax : :class:`matplotlib.axes.Axes`
The axes on which we should plot.
order : None | 2D array (self.N_sites, self.dim+1)
The order as returned by :meth:`ordering`; by default (``None``) use :attr:`order`.
textkwargs: ``None`` | dict
If not ``None``, we add text labels enumerating the sites in the plot. The dictionary
can contain keyword arguments for ``ax.text()``.
**kwargs :
Further keyword arguments given to ``ax.plot()``.
"""
if order is None:
order = self.order
pos = self.position(order)
kwargs.setdefault('color', 'r')
if pos.shape[1] == 1:
pos = pos * np.array([[1., 0]]) # use broadcasting to add a column with zeros
if pos.shape[1] != 2:
raise ValueError("can only plot in 2 dimensions.")
ax.plot(pos[:, 0], pos[:, 1], **kwargs)
if textkwargs is not None:
textkwargs.setdefault('color', kwargs['color'])
for i, p in enumerate(pos):
ax.text(p[0], p[1], str(i), **textkwargs)
def plot_coupling(self, ax, coupling=None, **kwargs):
"""Plot lines connecting nearest neighbors of the lattice.
Parameters
----------
ax : :class:`matplotlib.axes.Axes`
The axes on which we should plot.
coupling : list of (u1, u2, dx)
By default (``None``), use :attr:``nearest_neighbors``.
Specifies the connections to be plotted; iteating over lattice indices `(i0, i1, ...)`,
we plot a connection from the site ``(i0, i1, ..., u1)`` to the site
``(i0+dx[0], i1+dx[1], ..., u2)``, taking into account the boundary conditions.
**kwargs :
Further keyword arguments given to ``ax.plot()``.
"""
if coupling is None:
coupling = self.nearest_neighbors
kwargs.setdefault('color', 'k')
Ls = np.array(self.Ls)
for u1, u2, dx in coupling:
# TODO: should use `possible_couplings` somehow,
# but then beriodic boundary conditions screew up the image
# should plot couplings of periodic boundary conditions
dx = np.r_[np.array(dx), u2 - u1] # append the difference in u to dx
lat_idx_1 = self.order[self._mps_fix_u[u1], :]
lat_idx_2 = lat_idx_1 + dx[np.newaxis, :]
lat_idx_2_mod = np.mod(lat_idx_2[:, :-1], Ls)
# handle boundary conditions
if self.bc_shift is not None:
shift = np.sum(((lat_idx_2[:, :-1] - lat_idx_2_mod) // Ls)[:, 1:] * self.bc_shift,
axis=1)
lat_idx_2[:, 0] -= shift
lat_idx_2_mod[:, 0] = np.mod(lat_idx_2[:, 0], self.Ls[0])
keep = np.all(
np.logical_or(
lat_idx_2_mod == lat_idx_2[:, :-1], # not accross the boundary
np.logical_not(self.bc)), # direction has PBC
axis=1)
# get positions
pos1 = self.position(lat_idx_1[keep, :])
pos2 = self.position(lat_idx_2[keep, :])
pos = np.stack((pos1, pos2), axis=0)
# ax.plot connects columns of 2D array by lines
if pos.shape[2] == 1:
pos = pos * np.array([[[1., 0]]]) # use broadcasting to add a column with zeros
if pos.shape[2] != 2:
raise ValueError("can only plot in 2 dimensions.")
ax.plot(pos[:, :, 0], pos[:, :, 1], **kwargs)
def plot_basis(self, ax, **kwargs):
"""Plot arrows indicating the basis vectors of the lattice.
Parameters
----------
ax : :class:`matplotlib.axes.Axes`
The axes on which we should plot.
**kwargs :
Keyword arguments specifying the "arrowprops" of ``ax.annotate``.
"""
kwargs.setdefault("arrowstyle", "->")
for i in range(self.dim):
vec = self.basis[i]
if vec.shape[0] == 1:
vec = vec * np.array([1., 0])
if vec.shape[0] != 2:
raise ValueError("can only plot in 2 dimensions.")
ax.annotate("", vec, [0., 0.], arrowprops=kwargs)
def plot_bc_identified(self, ax, direction=-1, shift=None, **kwargs):
"""Mark two sites indified by periodic boundary conditions.
Works only for lattice with a 2-dimensional basis.
Parameters
----------
ax : :class:`matplotlib.axes.Axes`
The axes on which we should plot.
direction : int
The direction of the lattice along which we should mark the idenitified sites.
If ``None``, mark it along all directions with periodic boundary conditions.
shift : None | np.ndarray
The origin starting from where we mark the identified sites.
Defaults to the first entry of :attr:`unit_cell_positions`.
**kwargs :
Keyword arguments for the used ``ax.plot``.
"""
if direction is None:
dirs = [i for i in range(self.dim) if not self.bc[i]]
else:
if direction < 0:
direction += self.dim
dirs = [direction]
shift = self.unit_cell_positions[0]
kwargs.setdefault("marker", "o")
kwargs.setdefault("markersize", 10)
kwargs.setdefault("color", "orange")
x_y = []
for i in dirs:
if self.bc[i]:
raise ValueError("Boundary conditons are not periodic for given direction")
x_y.append(shift)
x_y.append(shift + self.Ls[i] * self.basis[i])
if self.bc_shift is not None and i > 0:
x_y[-1] = x_y[-1] - self.bc_shift[i - 1] * self.basis[0]
x_y = np.array(x_y)
if x_y.shape[1] == 1:
x_y = np.hstack([x_y, np.zeros_like(x_y)])
if x_y.shape[1] != 2:
raise ValueError("can only plot in 2D")
ax.plot(x_y[:, 0], x_y[:, 1], **kwargs)
def _asvalid_latidx(self, lat_idx):
"""convert lat_idx to an ndarray with correct last dimension."""
lat_idx = np.asarray(lat_idx, dtype=np.intp)
if lat_idx.shape[-1] != len(self.shape):
raise ValueError("wrong len of last dimension of lat_idx: " + str(lat_idx.shape))
return lat_idx
def _set_bc(self, bc):
global bc_choices
if bc in list(bc_choices.keys()):
bc = [bc_choices[bc]] * self.dim
self.bc_shift = None
else:
bc = list(bc) # we modify entries...
self.bc_shift = np.zeros(self.dim - 1, np.int_)
for i, bc_i in enumerate(bc):
if isinstance(bc_i, int):
if i == 0:
raise ValueError("Invalid bc: first entry can't be a shift")
self.bc_shift[i - 1] = bc_i
bc[i] = bc_choices['periodic']
else:
bc[i] = bc_choices[bc_i]
if not np.any(self.bc_shift != 0):
self.bc_shift = None
self.bc = np.array(bc)
class TrivialLattice(Lattice):
"""Trivial lattice consisting of a single (possibly large) unit cell in 1D.
This is usefull if you need a valid :class:`Lattice` given just the :meth:`mps_sites`.
Parameters
----------
mps_sites : list of :class:`~tenpy.networks.site.Site`
The sites making up a unit cell of the lattice.
**kwargs :
Further keyword arguments given to :class:`Lattice`.
"""
def __init__(self, mps_sites, **kwargs):
Lattice.__init__(self, [1], mps_sites, **kwargs)
class IrregularLattice(Lattice):
"""A variant of a regular lattice, where we might have extra sites or sites missing.
.. todo ::
- this doesn't fully work yet...
"""
def __init__(self, mps_sites, based_on, order=None):
self.based_on = based_on
self._mps_sites = mps_sites
Lattice.__init__(self,
based_on.Ls,
based_on.unit_cell,
order='default',
bc=based_on.bc,
bc_MPS=based_on.bc_MPS)
# don't copy nearest_neighbors, basis, positions etc: no longer valid
self.N_sites = len(mps_sites)
self._order = order
@classmethod
def from_mps_sites(cls, mps_sites, based_on=None):
if based_on is None:
based_on = k
return cls(mps_sites, based_on)
@classmethod
def from_add_sites(self, M):
raise NotImplementedError()
def mps_sites(self):
return self._mps_sites
class SimpleLattice(Lattice):
"""A lattice with a unit cell consiting of just a single site.
In many cases, the unit cell consists just of a single site, such that the the last entry of
`u` of an 'lattice index' can only be ``0``.
From the point of internal algorithms, we handle this class like a :class:`Lattice` --
in that way we don't need to distinguish special cases in the algorithms.
Yet, from the point of a tenpy user, for example if you measure an expectation value
on each site in a `SimpleLattice`, you expect to get an ndarray of dimensions ``self.Ls``,
not ``self.shape``. To avoid that problem, `SimpleLattice` overwrites just the meaning of
``u=None`` in :meth:`mps2lat_values` to be the same as ``u=0``.
Parameters
----------
Ls : list of int
the length in each direction
site : :class:`~tenpy.networks.site.Site`