/
lattice.py
2509 lines (2224 loc) · 110 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.
The :class:`IrregularLattice` provides a way to remove or add sites to an existing, regular
lattice.
See also the :doc:`/intro/model` and :doc:`/intro/lattices`.
"""
# Copyright 2018-2021 TeNPy Developers, GNU GPLv3
import numpy as np
import itertools
import warnings
from ..networks.site import Site
from ..tools.misc import (to_iterable, to_array, to_iterable_of_len, inverse_permutation,
get_close, find_subclass)
from ..networks.mps import MPS # only to check boundary conditions
__all__ = [
'Lattice', 'TrivialLattice', 'IrregularLattice', 'HelicalLattice', '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 + 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.
.. deprecated :: 0.5.0
The parameters and attributes `nearest_neighbors`, `next_nearest_neighbors` and
`next_next_nearest_neighbors` are deprecated. Instead, we use a dictionary `pairs`
with those names as keys and the corresponding values as specified before.
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)``
Deprecated. Specify as ``pairs['nearest_neighbors']`` instead.
next_nearest_neighbors : ``None`` | list of ``(u1, u2, dx)``
Deprecated. Specify as ``pairs['next_nearest_neighbors']`` instead.
next_next_nearest_neighbors : ``None`` | list of ``(u1, u2, dx)``
Deprecated. Specify as ``pairs['next_next_nearest_neighbors']`` instead.
pairs : dict
Of the form ``{'nearest_neighbors': [(u1, u2, dx), ...], ...}``.
Typical keys are ``'nearest_neighbors', 'next_nearest_neighbors'``.
For each of them, it specifies a list of tuples ``(u1, u2, dx)`` which can
be used as parameters for :meth:`~tenpy.models.model.CouplingModel.add_coupling`
to generate couplings over each pair of ,e.g., ``'nearest_neighbors'``.
Note that this adds couplings for each pair *only in one direction*!
Attributes
----------
Ls : tuple of int
the length in each direction.
shape : tuple of int
the 'shape' of the lattice, same as ``Ls + (len(unit_cell), )``
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.
unit_cell : list of :class:`~tenpy.networks.site.Site`
the 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.
pairs : dict
See above.
_order : ndarray (N_sites, dim+1)
The place where :attr:`order` is stored.
_strides : ndarray (dim, )
necessary for :meth:`lat2mps_idx`.
_perm : ndarray (N, )
permutation needed to make `order` lexsorted, ``_perm = np.lexsort(_order.T)``.
_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.
"""
Lu = None #: the (expected) number of sites in the unit cell, ``len(unit_cell)``.
dim = None #: the dimension of the lattice
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,
pairs=None):
self.unit_cell = list(unit_cell)
self._set_Ls(Ls) # after setting unit_cell
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.boundary_conditions = bc # property setter for self.bc and self.bc_shift
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
self.pairs = pairs if pairs is not None else {}
for name, NN in [('nearest_neighbors', nearest_neighbors),
('next_nearest_neighbors', next_nearest_neighbors),
('next_next_nearest_neighbors', next_next_nearest_neighbors)]:
if NN is None:
continue # no value set
msg = "Lattice.__init__() got argument `{0!s}`.\nSet as `neighbors['{0!s}'] instead!"
msg = msg.format(name)
warnings.warn(msg, FutureWarning)
if name in self.pairs:
raise ValueError("{0!s} sepcified twice!".format(name))
self.pairs[name] = NN
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), )
if not isinstance(self, HelicalLattice):
assert self.N_cells == np.prod(self.Ls)
if self.bc.shape != (self.dim, ):
raise ValueError("Wrong len of bc")
assert self.bc.dtype == bool
chinfo = None
for site in self.unit_cell:
if not isinstance(site, Site):
continue
if chinfo is None:
chinfo = site.leg.chinfo
if site.leg.chinfo != chinfo:
raise ValueError("All sites in the lattice must have the same ChargeInfo!"
" Call tenpy.networks.site.set_common_charges() before "
"giving them to the lattice!")
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 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`")
if not isinstance(self, (IrregularLattice, HelicalLattice)):
assert self.N_sites == np.prod(self.shape)
# if one of the following assert fails,
# the `ordering` function might have returned an invalid array
assert np.all(self._order >= 0) and np.all(self._order <= self.shape)
# rows of `order` unique and _perm correct?
assert np.all(
np.sum(self._order * self._strides, axis=1)[self._perm] == np.arange(self.N_sites))
def save_hdf5(self, hdf5_saver, h5gr, subpath):
"""Export `self` into a HDF5 file.
This method saves all the data it needs to reconstruct `self` with :meth:`from_hdf5`.
Specifically, it saves
:attr:`unit_cell`, :attr:`Ls`, :attr:`unit_cell_positions`, :attr:`basis`,
:attr:`boundary_conditions`, :attr:`pairs` under their name,
:attr:`bc_MPS` as ``"boundary_conditions_MPS"``, and
:attr:`order` as ``"order_for_MPS"``.
Moreover, it saves :attr:`dim` and :attr:`N_sites` as HDF5 attributes.
Parameters
----------
hdf5_saver : :class:`~tenpy.tools.hdf5_io.Hdf5Saver`
Instance of the saving engine.
h5gr : :class`Group`
HDF5 group which is supposed to represent `self`.
subpath : str
The `name` of `h5gr` with a ``'/'`` in the end.
"""
hdf5_saver.save(self.unit_cell, subpath + "unit_cell")
hdf5_saver.save(np.array(self.Ls, int), subpath + "lengths")
hdf5_saver.save(self.unit_cell_positions, subpath + "unit_cell_positions")
hdf5_saver.save(self.basis, subpath + "basis")
hdf5_saver.save(self.boundary_conditions, subpath + "boundary_conditions")
hdf5_saver.save(self.bc_MPS, subpath + "boundary_condition_MPS")
hdf5_saver.save(self.order, subpath + "order_for_MPS")
hdf5_saver.save(self.pairs, subpath + "pairs")
# not necessary for loading, but still usefull
h5gr.attrs["dim"] = self.dim
h5gr.attrs["N_sites"] = self.N_sites
@classmethod
def from_hdf5(cls, hdf5_loader, h5gr, subpath):
"""Load instance from a HDF5 file.
This method reconstructs a class instance from the data saved with :meth:`save_hdf5`.
Parameters
----------
hdf5_loader : :class:`~tenpy.tools.hdf5_io.Hdf5Loader`
Instance of the loading engine.
h5gr : :class:`Group`
HDF5 group which is represent the object to be constructed.
subpath : str
The `name` of `h5gr` with a ``'/'`` in the end.
Returns
-------
obj : cls
Newly generated class instance containing the required data.
"""
obj = cls.__new__(cls) # create class instance, no __init__() call
hdf5_loader.memorize_load(h5gr, obj)
obj.unit_cell = hdf5_loader.load(subpath + "unit_cell")
Ls = hdf5_loader.load(subpath + "lengths")
obj._set_Ls(Ls)
obj.unit_cell_positions = hdf5_loader.load(subpath + "unit_cell_positions")
obj.basis = hdf5_loader.load(subpath + "basis")
obj.boundary_conditions = hdf5_loader.load(subpath + "boundary_conditions")
obj.bc_MPS = hdf5_loader.load(subpath + "boundary_condition_MPS")
obj.order = hdf5_loader.load(subpath + "order_for_MPS") # property setter!
obj.pairs = hdf5_loader.load(subpath + "pairs")
obj.test_sanity()
return obj
@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.
Each row of the array contains the lattice indices for one site,
the order of the rows thus specifies a path through the lattice,
along which an MPS will wind through through the lattice.
You can visualize the order with :meth:`plot_order`.
"""
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)
================== =========================== =============================
.. plot ::
import matplotlib.pyplot as plt
from tenpy.models import lattice
fig, axes = plt.subplots(2, 2, sharex=True, sharey=True, figsize=(8, 6))
orders = ['Cstyle', 'snakeCstyle', 'Fstyle', 'snakeFstyle']
lat = lattice.Square(5, 3, None, bc='periodic')
for order, ax in zip(orders, axes.flatten()):
lat.order = lat.ordering(order)
lat.plot_order(ax, linestyle=':')
lat.plot_sites(ax)
lat.plot_basis(ax, origin=-0.25*(lat.basis[0] + lat.basis[1]))
ax.set_title(repr(order))
ax.set_aspect('equal')
ax.set_xlim(-1)
ax.set_ylim(-1)
plt.show()
.. note ::
For lattices with a non-trivial unit cell (e.g. Honeycomb, Kagome), the
grouped order might be more appropriate, see :func:`get_order_grouped`
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:]
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)
@property
def boundary_conditions(self):
"""Human-readable list of boundary conditions from :attr:`bc` and :attr:`bc_shift`.
Returns
-------
boundary_conditions : list of str
List of ``"open"`` or ``"periodic"``, one entry for each direction of the lattice.
"""
global bc_choices
bc_choices_reverse = dict([(v, k) for (k, v) in bc_choices.items()])
bc = [bc_choices_reverse[bc] for bc in self.bc]
if self.bc_shift is not None:
for i, shift in enumerate(self.bc_shift):
assert bc[i + 1] == "periodic"
bc[i + 1] = int(shift)
return bc
@boundary_conditions.setter
def boundary_conditions(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)
def enlarge_mps_unit_cell(self, factor=2):
"""Repeat the unit cell for infinite MPS boundary conditions; in place.
Parameters
----------
factor : int
The new number of sites in the MPS unit cell will be increased from `N_sites` to
``factor*N_sites_per_ring``. Since MPS unit cells are repeated in the `x`-direction
in our convetion, the lattice shape goes from
``(Lx, Ly, ..., Lu)`` to ``(Lx*factor, Ly, ..., Lu)``.
"""
if self.bc_MPS != "infinite":
raise ValueError("can only enlarge the MPS unit cell for infinite MPS.")
new_Ls = list(self.Ls)
old_Lx = new_Ls[0]
new_Ls[0] = old_Lx * factor
old_order = self.order
new_order = []
for i in range(factor):
order = old_order.copy()
shift_x = i * old_Lx
order[:, 0] += shift_x
new_order.append(order)
new_order = np.vstack(new_order)
# now update the contents of `self`
self._set_Ls(new_Ls)
self.order = new_order # property setter
self.test_sanity()
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_rings // self.N_sites
# N_sites_per_ring might not be set for IrregularLattice
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
# N_sites_per_ring might not be set for IrregularLattice
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 MPS indices along the specified axes
are replaced by lattice indices, i.e., if MPS index `j` maps to lattice site
`(x0, x1, x2)`, then ``res_A[..., x0, x1, x2, ...] = A[..., j, ...]``.
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:
.. testsetup :: mps2lat_values
lat = tenpy.models.lattice.Honeycomb(10, 3, None)
A = np.arange(60)
C = np.arange(60*60).reshape(60, 60)
A_res = lat.mps2lat_values(A)
.. doctest :: mps2lat_values
>>> print(lat.shape, A.shape)
(10, 3, 2) (60,)
>>> A_res = lat.mps2lat_values(A)
>>> A_res.shape
(10, 3, 2)
>>> A_res[tuple(lat.mps2lat_idx(5))] == A[5]
True
If you have a correlation function ``C[i, j]``, it gets just slightly more complicated:
.. doctest :: mps2lat_values
>>> 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.
.. doctest :: mps2lat_values
>>> 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
"""
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 mps2lat_values_masked(self, A, axes=-1, mps_inds=None, include_u=None):
"""Reshape/reorder an array `A` to replace an MPS index by lattice indices.
This is a generalization of :meth:`mps2lat_values` allowing for the case of an arbitrary
set of MPS indices present in each axis of `A`.
Parameters
----------
A : ndarray
Some values.
axes : (iterable of) int
Chooses the axis of `A` which should be replaced.
If multiple axes are given, you also need to give multiple index arrays as `mps_inds`.
mps_inds : (list of) 1D ndarray
Specifies for each `axis` in `axes`, for which MPS indices we have values in the
corresponding `axis` of `A`.
Defaults to ``[np.arange(A.shape[ax]) for ax in axes]``.
For indices accross the MPS unit cell and "infinite" `bc_MPS`,
we shift `x_0` accordingly.
include_u : (list of) bool
Specifies for each `axis` in `axes`, whether the `u` index of the lattice should be
included into the output array `res_A`. Defaults to ``len(self.unit_cell) > 1``.
Returns
-------
res_A : np.ma.MaskedArray
Reshaped and reordered copy of A. Such that MPS indices along the specified axes
are replaced by lattice indices, i.e., if MPS index `j` maps to lattice site
`(x0, x1, x2)`, then ``res_A[..., x0, x1, x2, ...] = A[..., mps_inds[j], ...]``.
"""
try:
iter(axes)
except TypeError: # axes is single int
axes = [axes]
mps_inds = [mps_inds]
include_u = [include_u]
else: # iterable axes
if mps_inds is None:
mps_inds = [None] * len(axes)
if include_u is None:
include_u = [None] * len(axes)
if len(axes) != len(mps_inds) or len(axes) != len(include_u):
raise ValueError("Lenght of `axes`, `mps_inds` and `include_u` different")
# sort axes ascending
axes = [(ax + A.ndim if ax < 0 else ax) for ax in axes]
# goal: use numpy advanced indexing for the data copy
lat_inds = [] # lattice indices to be used
new_shapes = [] # shape to be
for ax, mps_inds_ax, include_u_ax in zip(axes, mps_inds, include_u):
if mps_inds_ax is None: # default
mps_inds_ax = np.arange(A.shape[ax])
if include_u_ax is None: # default
include_u_ax = (len(self.unit_cell) > 1)
if mps_inds_ax.ndim != 1:
raise ValueError("got non-1D array in `mps_inds` " + str(mps_inds_ax.shape))
lat_inds_ax = self.mps2lat_idx(mps_inds_ax)
shape = list(self.shape)
max_i = np.max(mps_inds_ax)
if max_i >= self.N_sites:
shape[0] += (max_i - self.N_sites) * self.N_rings // self.N_sites + 1
min_i = np.min(mps_inds_ax)
if min_i < 0:
# we use numpy indexing to simply wrap around negative indices
shape[0] += (abs(min_i) - 1) * self.N_rings // self.N_sites + 1
if not include_u_ax:
shape = shape[:-1]
lat_inds_ax = lat_inds_ax[:, :-1]
new_shapes.append(shape)
lat_inds.append(lat_inds_ax)
res_A_ndim = A.ndim - len(axes) + sum([len(s) for s in new_shapes])
res_A_shape = []
res_A_inds = []
dim_before = 0
dim_after = A.ndim - 1
for ax in range(A.ndim):
if ax in axes:
i = axes.index(ax)
inds_ax = lat_inds[i].T
res_A_shape.extend(new_shapes[i])
for inds in lat_inds[i].T:
inds = inds.reshape([1] * dim_before + [len(inds)] + [1] * dim_after)
res_A_inds.append(inds)
else:
d = A.shape[ax]
res_A_shape.append(d)
inds = np.arange(d).reshape([1] * dim_before + [d] + [1] * dim_after)
res_A_inds.append(inds)
dim_before += 1
dim_after -= 1
# and finally we are in the position to create the masked Array
fill_value = np.ma.array([0, 1], dtype=A.dtype).get_fill_value()
res_A_data = np.full(res_A_shape, fill_value, dtype=A.dtype)
res_A = np.ma.array(res_A_data, mask=True, fill_value=fill_value)
res_A[tuple(res_A_inds)] = A # copy data, automatically unmasks entries
return res_A
def count_neighbors(self, u=0, key='nearest_neighbors'):
"""Count e.g. the number of nearest neighbors for a site in the bulk.
Parameters
----------
u : int
Specifies the site in the unit cell, for which we should count the number
of neighbors (or whatever `key` specifies).
key : str
Key of :attr:`pairs` to select what to count.
Returns
-------
number : int
Number of nearest neighbors (or whatever `key` specified) for the `u`-th site in the
unit cell, somewhere in the bulk of the lattice. Note that it might not be the correct
value at the edges of a lattice with open boundary conditions.
"""
pairs = self.pairs[key]
count = 0
for u1, u2, dx in pairs:
if u1 == u:
count += 1
if u2 == u:
count += 1
return count
def number_nearest_neighbors(self, u=0):
"""Deprecated.
.. deprecated :: 0.5.0
Use :meth:`count_neighbors` instead.
"""
msg = "Use ``count_neighbors(u, 'nearest_neighbors')`` instead."
warnings.warn(msg, FutureWarning)
return self.count_neighbors(u, 'nearest_neighbors')
def number_next_nearest_neighbors(self, u=0):
"""Deprecated.
.. deprecated :: 0.5.0
Use :meth:`count_neighbors` instead.
"""
msg = "Use ``count_neighbors(u, 'next_nearest_neighbors')`` instead."
warnings.warn(msg, FutureWarning)
return self.count_neighbors(u, 'next_nearest_neighbors')
def distance(self, u1, u2, dx):
"""Get the distance for a given coupling between two sites in the lattice.
The `u1`, `u2`, `dx` parameters are defined in analogy with
:meth:`~tenpy.models.model.CouplingModel.add_coupling`, i.e., this function
calculates the distance between a pair of operators added with `add_coupling` (using the
:attr:`basis` and :attr:`unit_cell_positions` of the lattice).
.. warning ::
This function ignores "wrapping" arround the cylinder in the case of periodic boundary
conditions.
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
-------
distance : float
The distance between site at lattice indices ``[x, y, u1]`` and
``[x + dx[0], y + dx[1], u2]``, **ignoring** any boundary effects.
"""
vec_dist = self.unit_cell_positions[u2] - self.unit_cell_positions[u1]
for ax in range(self.dim):
vec_dist = vec_dist + dx[..., ax, np.newaxis] * self.basis[ax]
return np.linalg.norm(vec_dist, axis=-1)
def find_coupling_pairs(self, max_dx=3, cutoff=None, eps=1.e-10):
"""Automatically find coupling pairs grouped by distances.
Given the :attr:`unit_cell_positions` and :attr:`basis`, the coupling :attr:`pairs` of
`nearest_neighbors`, `next_nearest_neighbors` etc at a given distance are basically
fixed (although not uniquely, since we take out half of them to avoid double-counting
couplings in both directions ``A_i B_j + B_i A_i``).
This function iterates through all possible couplings up to a given `cutoff` distance and
then determines the possible :attr:`pairs` at fixed distances (up to round-off errors).
Parameters
----------
max_dx : int
Maximal index for each index of `dx` to iterate over. You need large enough values
to include every possible coupling up to the desired distance, but choosing it too
large might make this function run for a long time.
cutoff : float
Maximal distance (in the units in which :attr:`basis` and :attr:`unit_cell_positions`
is given).
eps : float
Tolerance up to which to distances are considered the same.
Returns
-------
coupling_pairs : dict
Keys are distances of nearest-neighbors, next-nearest-neighbors etc.
Values are ``[(u1, u2, dx), ...]`` as in :attr:`pairs`.
"""
if cutoff is None:
cutoff = max_dx - eps
assert cutoff < max_dx * min(np.linalg.norm(self.basis, axis=-1))
Lu = len(self.unit_cell)
dist_pairs = {}
for u1, u2 in itertools.product(range(Lu), repeat=2):
for dx in itertools.product(range(max_dx, -max_dx - 1, -1), repeat=self.dim):
dist = self.distance(u1, u2, np.array(dx))
if dist > cutoff or dist < eps:
continue
d0 = get_close(dist_pairs.keys(), dist)
if d0 is None:
dist_pairs[dist] = []
d0 = dist
pairs = dist_pairs[d0]
if (u2, u1, tuple(-i for i in dx)) in pairs:
continue # avoid double-counting for existing pairs
pairs.append((u1, u2, dx))
# finally sort the keys of the dict by distances
result = {}
for key in sorted(dist_pairs.keys()):
result[key] = [(u1, u2, np.array(dx)) for u1, u2, dx in dist_pairs[key]]
return result
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.
Corresponds to `dx` argument of
:meth:`tenpy.models.model.CouplingModel.add_multi_coupling`.
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 origin to the lower left corner of box spanned by `dx`.
"""
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 possible_couplings(self, u1, u2, dx, strength=None):
"""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.
strength : array_like | None
If given, instead of returning `lat_indices` and `coupling_shape`
directly return the correct `strength_12`.
Returns
-------
mps1, mps2 : 1D array
For each possible two-site coupling the MPS indices for the `u1` and `u2`.
strength_vals : 1D array
(Only returend if `strength` is not None.)
Such that ``for (i, j, s) in zip(mps1, mps2, strength_vals):`` iterates over all
possible couplings with `s` being the strength of that coupling.
Couplings where ``strength_vals == 0.`` are filtered out.
lat_indices : 2D int array
(Only returend if `strength` is None.)
Rows of `lat_indices` correspond to entries of `mps1` and `mps2` and contain the
lattice indices of the "lower left corner" of the box containing the coupling.
coupling_shape : tuple of int
(Only returend if `strength` is None.)
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]):
if strength is None:
return [], [], np.zeros([0, self.dim]), coupling_shape
else:
return [], [], np.array([])
Ls = np.array(self.Ls)
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: