temfpy 0.1.0__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
temfpy/__init__.py ADDED
@@ -0,0 +1,14 @@
1
+ import logging as _logging
2
+
3
+ # Try to get version from hatch-vcs generated file first, then fallback
4
+ try:
5
+ from ._version import __version__
6
+ except ImportError:
7
+ from importlib.metadata import version
8
+ __version__ = version("temfpy")
9
+
10
+
11
+ def setup_logging(level=_logging.INFO):
12
+ _logging.basicConfig(
13
+ level=level,
14
+ )
temfpy/_version.py ADDED
@@ -0,0 +1,34 @@
1
+ # file generated by setuptools-scm
2
+ # don't change, don't track in version control
3
+
4
+ __all__ = [
5
+ "__version__",
6
+ "__version_tuple__",
7
+ "version",
8
+ "version_tuple",
9
+ "__commit_id__",
10
+ "commit_id",
11
+ ]
12
+
13
+ TYPE_CHECKING = False
14
+ if TYPE_CHECKING:
15
+ from typing import Tuple
16
+ from typing import Union
17
+
18
+ VERSION_TUPLE = Tuple[Union[int, str], ...]
19
+ COMMIT_ID = Union[str, None]
20
+ else:
21
+ VERSION_TUPLE = object
22
+ COMMIT_ID = object
23
+
24
+ version: str
25
+ __version__: str
26
+ __version_tuple__: VERSION_TUPLE
27
+ version_tuple: VERSION_TUPLE
28
+ commit_id: COMMIT_ID
29
+ __commit_id__: COMMIT_ID
30
+
31
+ __version__ = version = '0.1.0'
32
+ __version_tuple__ = version_tuple = (0, 1, 0)
33
+
34
+ __commit_id__ = commit_id = None
temfpy/gutzwiller.py ADDED
@@ -0,0 +1,186 @@
1
+ # Copyright (C) TeMFPy Developers, MIT license
2
+ r"Tools for Gutzwiller projecting MPS to a smaller on-site Hilbert space."
3
+
4
+ import logging
5
+ import warnings
6
+
7
+ import numpy as np
8
+
9
+ from tenpy import networks
10
+ import tenpy.linalg as npc
11
+
12
+ logger = logging.getLogger(__name__)
13
+
14
+
15
+ def abrikosov_ph(
16
+ mps: networks.MPS,
17
+ *,
18
+ inplace: bool = False,
19
+ return_canonical: bool = True,
20
+ cutoff: float = 1e-12,
21
+ ) -> None | networks.MPS:
22
+ r"""Gutzwiller projection from Abrikosov fermions to a spin-1/2 Hilbert space.
23
+
24
+ The input MPS is assumed to describe Abrikosov fermions, with the down
25
+ spins particle-hole transformed; i.e.:
26
+
27
+ .. math::
28
+
29
+ c_{i,\uparrow} := f_{i,\uparrow},
30
+ \qquad \qquad
31
+ c_{i,\downarrow} := f_{i,\downarrow}^\dagger.
32
+
33
+ Therefore, it must contain an even number of spinless fermion sites:
34
+ sites :math:`2i` and :math:`2i+1` represent modes :math:`c_{i\uparrow}` and
35
+ :math:`c_{i\downarrow}`, respectively.
36
+ These pairs are projected to a spin-1/2 Hilbert space using the following rules:
37
+
38
+ - Zero occupation → spin-down state
39
+ - Single occupation → unphysical states, dropped
40
+ - Double occupation → spin-up state
41
+
42
+ Therefore, depending on the conserved charge of the input MPS, only the following charge blocks
43
+ of the virtual legs are kept:
44
+
45
+ - ``'N'`` (particle number) → even ``'N'`` blocks → ``'S_z'`` conserved
46
+ - ``'parity'`` → even ``'parity'`` blocks → no conserved charge
47
+
48
+ Parameters
49
+ ----------
50
+ mps:
51
+ MPS representing the wave function to be projected.
52
+ Must be of even length and every site must be an instance
53
+ of :class:`~tenpy.networks.site.FermionSite`.
54
+ inplace:
55
+ Whether to transform the original MPS in place.
56
+ return_canonical:
57
+ Whether to transform the output MPS to right canonical form.
58
+ cutoff:
59
+ Cutoff for Schmidt values to keep in the canonical form.
60
+
61
+ Returns
62
+ -------
63
+ The Gutzwiller projected ``mps``, if ``inplace`` is :obj:`False`.
64
+
65
+ Note
66
+ ----
67
+ Currently, no symmetry quantum numbers other than fermion
68
+ number or parity can be handled.
69
+ """
70
+
71
+ assert (
72
+ mps.L % 2 == 0
73
+ ), "Odd-length MPS cannot represent an Abrikosov fermion Hilbert space"
74
+ # TODO: allow grouped sites which include a FermionicSite
75
+ assert isinstance(
76
+ mps.sites[0], networks.FermionSite
77
+ ), f"All sites must be fermionic, found: {mps.sites[0]}"
78
+
79
+ def gen_leg_mask(leg: npc.charges.LegCharge) -> np.ndarray:
80
+ """Generates a mask selecting the physical charge blocks for a given
81
+ fermionic leg.
82
+
83
+ The physical charge blocks depend on the conserved charge of the
84
+ given ``leg``:
85
+
86
+ - ``'N'`` → even particle charge blocks
87
+ - ``'parity'`` → even parity charge block
88
+
89
+
90
+ Parameters
91
+ ----------
92
+ leg:
93
+ The fermionic leg for which the mask is generated.
94
+
95
+ Returns
96
+ -------
97
+ A boolean mask selecting the physical charge blocks
98
+ that can be used by :class:`~tenpy.networks.Array.iproject`.
99
+
100
+ """
101
+ mask = (leg.to_qflat() % 2 == 0).ravel()
102
+
103
+ return mask
104
+
105
+ if not inplace:
106
+ mps = mps.copy()
107
+ logger.debug(f"Deep copied MPS before Gutzwiller projection.")
108
+
109
+ conserved_fermion = mps.sites[0].conserve
110
+ if conserved_fermion == "N":
111
+ conserved_spin = "Sz"
112
+ elif conserved_fermion == "parity":
113
+ conserved_spin = None
114
+ else:
115
+ raise ValueError(
116
+ f"FermionSite must conserve either 'N' or 'parity', found {conserved_fermion}"
117
+ )
118
+
119
+ # TeNPy bindings
120
+ spin_site = networks.SpinHalfSite(conserved_spin)
121
+ spin_leg = spin_site.leg
122
+ chinfo_s = spin_leg.chinfo
123
+
124
+ # We start by grouping neighboring sites
125
+ # This will result in LegPipe objects for all physical legs
126
+ mps.group_sites(2)
127
+
128
+ # The mask for the physical leg is independent of the site
129
+ mask_p = gen_leg_mask(mps._B[0].get_leg("p"))
130
+
131
+ for idx, B in enumerate(mps._B):
132
+ # Remove LegPipe structure
133
+ B.legs[B.get_leg_index("p")] = B.get_leg("p").to_LegCharge()
134
+
135
+ mask_vL = gen_leg_mask(B.get_leg("vL"))
136
+ mask_vR = gen_leg_mask(B.get_leg("vR"))
137
+
138
+ # Change the occupation number leg charges to spin charges
139
+ # --------------------------------------------------------
140
+
141
+ B.iproject([mask_vL, mask_p, mask_vR], ["vL", "p", "vR"])
142
+
143
+ # Change the occupation number leg charges to spin charges,
144
+ # if conserved.
145
+ # ------------------------------------------------------------
146
+ if conserved_spin is "Sz":
147
+ B.chinfo = chinfo_s
148
+
149
+ leg_vL, leg_p, leg_vR = [B.get_leg(label) for label in ["vL", "p", "vR"]]
150
+
151
+ leg_p.chinfo = chinfo_s
152
+ leg_p.charges = spin_leg.charges
153
+
154
+ leg_vL.chinfo = chinfo_s
155
+ leg_vL.charges -= idx
156
+
157
+ leg_vR.chinfo = chinfo_s
158
+ leg_vR.charges -= idx + 1
159
+
160
+ else: # None
161
+ B = B.drop_charge(charge="parity_N", chinfo=chinfo_s)
162
+
163
+ mps.chinfo = chinfo_s
164
+ mps.grouped = 1
165
+ mps.sites = [spin_site] * mps.L
166
+
167
+ # Transform into right canoncial form
168
+ mps.form = [None] * mps.L
169
+ mps._S = [None] * (mps.L + 1)
170
+
171
+ logger.info(
172
+ "Completed projection to spin-1/2 space. Conserved charge is now %s",
173
+ conserved_spin,
174
+ )
175
+
176
+ if return_canonical:
177
+ mps.canonical_form(cutoff=cutoff)
178
+ logger.info("Transformed MPS to right canonical form")
179
+ else:
180
+ warnings.warn(
181
+ "The MPS is not in canonical form after Gutzwiller projection.\n"
182
+ "Consider setting 'return_canonical=True'",
183
+ )
184
+
185
+ if not inplace:
186
+ return mps
temfpy/iMPS.py ADDED
@@ -0,0 +1,304 @@
1
+ # Copyright (C) TeMFPy Developers, MIT license
2
+ r"""Tools for converting finite to infinite MPS."""
3
+
4
+ import logging
5
+ import warnings
6
+ from typing import NamedTuple
7
+
8
+ import numpy as np
9
+
10
+ import tenpy.linalg.np_conserved as npc
11
+ from tenpy import networks as nw
12
+
13
+ logger = logging.getLogger(__name__)
14
+
15
+ _UNITARY_TOL = 1e-6
16
+ _SCHMIDT_TOL = 1e-6
17
+
18
+
19
+ def overlap_schmidt(bra: nw.mps.MPS, ket: nw.mps.MPS, mode: str) -> npc.Array:
20
+ r"""Overlap or optimal basis rotation between two sets of Schmidt vectors.
21
+
22
+ Parameters
23
+ ----------
24
+ bra:
25
+ Bra Schmidt vectors.
26
+ ket:
27
+ Ket Schmidt vectors.
28
+ mode:
29
+ If the overlap is between "left" or "right" Schmidt vectors.
30
+
31
+ Returns
32
+ -------
33
+ overlap: :class:`~tenpy.linalg.np_conserved.Array`
34
+ The overlaps :math:`(C_0)_{\alpha\beta} = \langle L_\alpha'|L_\beta \rangle`
35
+ or :math:`(D_0)_{\alpha\beta} = \langle R_\beta'|R_\alpha \rangle`,
36
+ depending on ``mode``.
37
+
38
+ Legs are labelled ``"vL"`` (incoming) and ``"vR"`` (outgoing).
39
+ """
40
+ assert bra.L == ket.L, "The two MPS have different lengths."
41
+ mode = mode.lower()
42
+
43
+ # ---- Compute overlap ----
44
+ if mode == "left":
45
+ TM = nw.TransferMatrix(bra, ket, transpose=True, form="A")
46
+ elif mode == "right":
47
+ TM = nw.TransferMatrix(bra, ket, transpose=False, form="B", charge_sector=None)
48
+ else:
49
+ raise ValueError("`mode` must be either 'left' or 'right', got " + repr(mode))
50
+
51
+ # Identity matrix with legs matching the input legs of the transfer matrix
52
+ Id = npc.Array.from_ndarray([[1.0]], TM.pipe.legs, labels=TM.label_split)
53
+
54
+ # nw.TransferMatrix makes sure that the first legtwo_leg is always ingoing and
55
+ # the second leg is always outgoing.
56
+ # Therefore, it is safe to just set the labels here instead of using
57
+ # ireplace_labels.
58
+ overlap = TM.matvec(Id).iset_leg_labels(["vL", "vR"])
59
+
60
+ return overlap
61
+
62
+
63
+ def basis_rotation(
64
+ overlap: npc.Array,
65
+ Schmidt_bra: np.ndarray,
66
+ Schmidt_ket: np.ndarray,
67
+ mode: str,
68
+ form: str = "B",
69
+ unitary_tol: float = _UNITARY_TOL,
70
+ schmidt_tol: float = _SCHMIDT_TOL,
71
+ ) -> tuple[npc.Array, float, float]:
72
+ r"""Overlap or optimal basis rotation between two sets of Schmidt vectors.
73
+
74
+ Parameters
75
+ ----------
76
+ overlap:
77
+ The overlaps :math:`(C_0)_{\alpha\beta} = \langle L_\alpha'|L_\beta \rangle`
78
+ or :math:`(D_0)_{\alpha\beta} = \langle R_\beta'|R_\alpha \rangle`,
79
+ depending on ``mode``.
80
+
81
+ Legs should be labelled ``"vL"`` (incoming) and ``"vR"`` (outgoing).
82
+ Schmidt_bra:
83
+ Schmidt values corresponding to the bra Schmidt vectors :math:`\langle L'|`
84
+ or :math:`\langle R'|`.
85
+ Schmidt_ket:
86
+ Schmidt values corresponding to the ket Schmidt vectors :math:`|L \rangle`
87
+ or :math:`|R \rangle`.
88
+ mode:
89
+ If the overlap is between "left" or "right" Schmidt vectors.
90
+ form:
91
+ Whether the basis rotation is to be used for a left ("A")
92
+ or a right ("B", default) canonical MPS tensor.
93
+ unitary_tol:
94
+ Highest allowed deviation from unitarity (weighted with Schmidt values)
95
+ in the overlaps before a warning is raised.
96
+ schmidt_tol:
97
+ Highest allowed mixing between unequal Schmidt value sectors
98
+ before a warning is raised.
99
+
100
+ Returns
101
+ -------
102
+ rotation_matrix: :class:`~tenpy.linalg.np_conserved.Array`
103
+ If :obj:`True`, the optimal unitary basis rotation matrix :math:`C` that
104
+ minimises the iMPS conversion error.
105
+
106
+ Legs are labelled ``"vL"`` (incoming) and ``"vR"`` (outgoing).
107
+ unitary_error: float
108
+ Deviation of the overlap matrix from unitarity,
109
+ measured as the square root of the trace of
110
+ :math:`S_{\rm ket} (C_0^\dagger C_0 - \mathbb{I}) S_{\rm ket}`.
111
+ schmidt_error: float
112
+ Degree of mixing of Schmidt vectors with unequal Schmidt values,
113
+ measured as the norm of either :math:`S_{\rm bra} C - C_0 S_{\rm ket}`
114
+ or :math:`(C - C_0) S_{\rm ket}`, depending on ``mode`` and ``form``.
115
+ """
116
+ mode = mode.lower()
117
+ err = f"`mode` must be either 'left' or 'right', got {mode!r}"
118
+ assert mode in ["left", "right"], err
119
+
120
+ form = form.upper()
121
+ assert form in ["A", "B"], f"`form` must be either 'A' or 'B', got {form!r}"
122
+
123
+ # make what follows independent of mode
124
+ v_bra, v_ket = ("vL", "vR") if mode == "left" else ("vR", "vL")
125
+
126
+ # ---- Test unitarity ----
127
+ # C @ S_ket
128
+ C_Sk = overlap.scale_axis(Schmidt_ket, v_ket)
129
+ # unitary_error^2 = tr(S_ket^2 - S_ket @ C^dagger @ C @ S_ket)
130
+ unitary_error = (
131
+ np.sum(Schmidt_ket**2) - npc.inner(C_Sk, C_Sk, do_conj=True)
132
+ ) ** 0.5
133
+ logging.info(f"{mode.capitalize()} deviation from unitary: {unitary_error:.4e}")
134
+ if unitary_error > unitary_tol:
135
+ warnings.warn(
136
+ f"\n{mode.capitalize()} overlap matrix deviates from unitarity by "
137
+ f"{unitary_error}.\n"
138
+ "Increasing the bond dimension may be useful."
139
+ )
140
+
141
+ # ---- Convert to unitary rotation matrix ----
142
+ if (mode, form) in [("left", "A"), ("right", "B")]:
143
+ # Schmidt values are inserted into the mixed canonical form
144
+ # at this entanglement cut. => Orthogonal Procustes for S_bra @ C @ S_ket
145
+ U, _, V = npc.svd(C_Sk.scale_axis(Schmidt_bra, v_bra))
146
+ else:
147
+ # Schmidt values are inserted into the mixed canonical form
148
+ # far from this entanglement cut. => Orthogonal Procustes for C @ S_ket^2
149
+ U, _, V = npc.svd(C_Sk.scale_axis(Schmidt_ket, v_ket))
150
+ overlap = npc.tensordot(U, V, 1)
151
+
152
+ # ---- Test Schmidt value deviations ----
153
+ # As above, the difference is due to whether Schmidt values appear in
154
+ # the mixed canonical form at this cut or far away
155
+ if (mode, form) in [("left", "A"), ("right", "B")]:
156
+ Sb_C = overlap.scale_axis(Schmidt_bra, v_bra)
157
+ else:
158
+ # not just C_Sk bc `overlap` has changed
159
+ Sb_C = overlap.scale_axis(Schmidt_ket, v_ket)
160
+
161
+ schmidt_error = npc.norm(Sb_C - C_Sk)
162
+ logging.info(f"{mode.capitalize()} Schmidt value mixing: {schmidt_error:.4e}")
163
+ if schmidt_error > schmidt_tol:
164
+ warnings.warn(
165
+ f"\nMixing between unequal Schmidt value sectors on the {mode} side is\n"
166
+ f"{schmidt_error}. Increasing the number of sites may help."
167
+ )
168
+
169
+ return overlap, unitary_error, schmidt_error
170
+
171
+
172
+ class iMPSError(NamedTuple):
173
+ """Container of the approximation errors accrued by :func:`MPS_to_iMPS`."""
174
+
175
+ left_unitary: float
176
+ """Deviation of left environment from unitarity."""
177
+ left_schmidt: float
178
+ """Mixing between unequal Schmidt values by the left environment."""
179
+ right_unitary: float
180
+ """Deviation of left environment from unitarity."""
181
+ right_schmidt: float
182
+ """Mixing between unequal Schmidt values by the right environment."""
183
+
184
+ @property
185
+ def left_total(self) -> float:
186
+ """Total approximation error of the left environment."""
187
+ return (self.left_schmidt**2 + self.left_unitary**2) ** 0.5
188
+
189
+ @property
190
+ def right_total(self) -> float:
191
+ """Total approximation error of the right environment."""
192
+ return (self.right_schmidt**2 + self.right_unitary**2) ** 0.5
193
+
194
+ @property
195
+ def total_error(self) -> float:
196
+ """Total approximation error."""
197
+ return np.linalg.norm(self)
198
+
199
+ def __repr__(self) -> str:
200
+ fields = [f" {f}={x:.8e}" for f, x in zip(self._fields, self) if x != 0]
201
+ if len(fields) == 0:
202
+ return "iMPSError()"
203
+ else:
204
+ return "iMPSError(\n" + (",\n".join(fields)) + "\n)"
205
+
206
+
207
+ def MPS_to_iMPS(
208
+ mps_short: nw.MPS,
209
+ mps_long: nw.MPS,
210
+ sites_per_cell: int,
211
+ cut: int,
212
+ unitary_tol: float = _UNITARY_TOL,
213
+ schmidt_tol: float = _SCHMIDT_TOL,
214
+ ) -> tuple[nw.MPS, iMPSError]:
215
+ """Constructs an iMPS by comparing two finite MPS.
216
+
217
+ The two MPS are expected to represent the ground states of a gapped,
218
+ translation invariant Hamiltonian on two system sizes that differ by
219
+ one repeating unit cell.
220
+
221
+ For sufficiently large systems, therefore, they are of the form
222
+
223
+ .. code::
224
+
225
+ ...(A...B)(A...B)...
226
+ ...(A...B)(A...B)(A...B)...
227
+
228
+ up to gauge transformations. The repeating unit cell ``(A...B)`` is
229
+ extracted from the longer chain, and its gauge is fixed by comparing its
230
+ left and right environments to the Schmidt vectors of the shorter chain.
231
+
232
+ Parameters
233
+ ----------
234
+ mps_short:
235
+ MPS of the shorter chain.
236
+ mps_long:
237
+ MPS of the longer chain.
238
+ sites_per_cell:
239
+ Size of the iMPS unit cell.
240
+ cut:
241
+ First site of the repeating unit cell in ``mps_long``.
242
+ unitary_tol:
243
+ Maximum deviation of the gauge rotation matrices from unitarity
244
+ before a warning is raised.
245
+ schmidt_tol:
246
+ Maximum mixing of unequal Schmidt values by the gauge rotation matrices
247
+ before a warning is raised.
248
+
249
+ Returns
250
+ -------
251
+ iMPS: :class:`~tenpy.networks.mps.MPS`
252
+ iMPS with unit cell size ``sites_per_cell``, constructed from the
253
+ additional unit cell of ``mps_long``.
254
+ validation_metric: :class:`iMPSError`
255
+ Errors introduced during the conversion.
256
+ """
257
+ # preliminary checks
258
+ L_short, L_long = mps_short.L, mps_long.L
259
+ if L_short + sites_per_cell != L_long:
260
+ raise ValueError(
261
+ "The given two MPS must differ by one unit cell, got "
262
+ f"{L_long} - {L_short} != {sites_per_cell}"
263
+ )
264
+ if mps_short.chinfo != mps_long.chinfo:
265
+ raise ValueError("Incompatible ChargeInfo in the two MPS")
266
+ assert all(x is not None for x in mps_short.form), "mps_short is not canonical"
267
+ assert all(x is not None for x in mps_long.form), "mps_long is not canonical"
268
+
269
+ # Schmidt values in the short chain at the reference cut
270
+ S0 = mps_short.get_SL(cut)
271
+
272
+ # Left gauge fixing matrix C
273
+ bra = mps_short.extract_segment(0, cut - 1)
274
+ ket = mps_long.extract_segment(0, cut - 1)
275
+ S_ket = mps_long.get_SL(cut)
276
+ C = overlap_schmidt(bra, ket, mode="left")
277
+ C, left_unitary, left_schmidt = basis_rotation(
278
+ C, S0, S_ket, mode="left", unitary_tol=unitary_tol, schmidt_tol=schmidt_tol
279
+ )
280
+
281
+ # Right gauge fixing matrix D
282
+ bra = mps_short.extract_segment(cut, L_short - 1)
283
+ ket = mps_long.extract_segment(cut + sites_per_cell, L_long - 1)
284
+ S_ket = mps_long.get_SL(cut + sites_per_cell)
285
+ D = overlap_schmidt(bra, ket, mode="right")
286
+ D, right_unitary, right_schmidt = basis_rotation(
287
+ D, S0, S_ket, mode="right", unitary_tol=unitary_tol, schmidt_tol=schmidt_tol
288
+ )
289
+
290
+ # Extract middle section of MPS in right canonical form
291
+ sites = mps_long.sites[cut : cut + sites_per_cell]
292
+ tensors = [mps_long.get_B(cut + i, form="B") for i in range(sites_per_cell)]
293
+ schmidt_values = mps_long._S[cut + 1 : cut + sites_per_cell]
294
+
295
+ # Apply gauge unitaries to first and last tensor
296
+ tensors[0] = npc.tensordot(C, tensors[0], axes=["vR", "vL"])
297
+ tensors[-1] = npc.tensordot(tensors[-1], D, axes=["vR", "vL"])
298
+
299
+ # Set Schmidt values on both ends to that of the reference MPS
300
+ schmidt_values = [S0] + schmidt_values + [S0]
301
+
302
+ iMPS = nw.MPS(sites, tensors, schmidt_values, bc="infinite", form="B")
303
+ error = iMPSError(left_unitary, left_schmidt, right_unitary, right_schmidt)
304
+ return iMPS, error