pdft 0.2.1__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.
- pdft/__init__.py +86 -0
- pdft/bases/__init__.py +40 -0
- pdft/bases/base.py +470 -0
- pdft/bases/block/__init__.py +12 -0
- pdft/bases/block/block.py +261 -0
- pdft/bases/block/real_rich.py +164 -0
- pdft/bases/block/rich.py +265 -0
- pdft/bases/circuit/__init__.py +15 -0
- pdft/bases/circuit/entangled_qft.py +137 -0
- pdft/bases/circuit/mera.py +167 -0
- pdft/bases/circuit/qft.py +84 -0
- pdft/bases/circuit/tebd.py +130 -0
- pdft/circuit/__init__.py +34 -0
- pdft/circuit/builder.py +242 -0
- pdft/circuit/cache.py +52 -0
- pdft/io/__init__.py +39 -0
- pdft/io/compression.py +189 -0
- pdft/io/serialize.py +221 -0
- pdft/loss.py +159 -0
- pdft/manifolds.py +350 -0
- pdft/optimizers/__init__.py +19 -0
- pdft/optimizers/adam.py +86 -0
- pdft/optimizers/core.py +61 -0
- pdft/optimizers/gd.py +68 -0
- pdft/optimizers/loop.py +104 -0
- pdft/profiling.py +292 -0
- pdft/training/__init__.py +19 -0
- pdft/training/adam_step.py +131 -0
- pdft/training/batched.py +341 -0
- pdft/training/eval_loop.py +57 -0
- pdft/training/result.py +17 -0
- pdft/training/schedules.py +27 -0
- pdft/training/single.py +69 -0
- pdft/viz/__init__.py +24 -0
- pdft/viz/circuit.py +85 -0
- pdft/viz/loss.py +119 -0
- pdft-0.2.1.dist-info/METADATA +100 -0
- pdft-0.2.1.dist-info/RECORD +40 -0
- pdft-0.2.1.dist-info/WHEEL +4 -0
- pdft-0.2.1.dist-info/licenses/LICENSE +25 -0
pdft/__init__.py
ADDED
|
@@ -0,0 +1,86 @@
|
|
|
1
|
+
"""pdft: Python port of ParametricDFT.jl (faithful JAX reference).
|
|
2
|
+
|
|
3
|
+
Importing this package enables JAX's x64 mode globally. This is required
|
|
4
|
+
to match Julia's ComplexF64 numerical behavior; without it, parity
|
|
5
|
+
tolerances are unreachable. See docs/superpowers/specs/2026-04-24-pdft-migration-design.md
|
|
6
|
+
Section 2 and 8.1.
|
|
7
|
+
|
|
8
|
+
Also enables a persistent JAX compilation cache by default, so the
|
|
9
|
+
~20-second JIT-compile cost of the Adam step is paid once per basis
|
|
10
|
+
shape across all runs. Override the cache dir with JAX_COMPILATION_CACHE_DIR
|
|
11
|
+
or disable entirely by setting PDFT_DISABLE_COMPILE_CACHE=1.
|
|
12
|
+
|
|
13
|
+
Public API: import from subpackages directly, e.g.
|
|
14
|
+
|
|
15
|
+
from pdft.bases.circuit import QFTBasis
|
|
16
|
+
from pdft.training import train_basis, train_basis_batched
|
|
17
|
+
from pdft.optimizers import RiemannianGD, RiemannianAdam
|
|
18
|
+
from pdft.loss import L1Norm, MSELoss
|
|
19
|
+
from pdft.io import save_basis, load_basis, compress
|
|
20
|
+
|
|
21
|
+
The names re-exported at the package root below are kept only for the
|
|
22
|
+
small set most commonly used in interactive sessions and notebooks.
|
|
23
|
+
"""
|
|
24
|
+
|
|
25
|
+
import os as _os
|
|
26
|
+
from pathlib import Path as _Path
|
|
27
|
+
|
|
28
|
+
import jax as _jax
|
|
29
|
+
|
|
30
|
+
_jax.config.update("jax_enable_x64", True)
|
|
31
|
+
|
|
32
|
+
if not _os.environ.get("PDFT_DISABLE_COMPILE_CACHE"):
|
|
33
|
+
_cache_dir = _os.environ.get("JAX_COMPILATION_CACHE_DIR")
|
|
34
|
+
if not _cache_dir:
|
|
35
|
+
_xdg = _os.environ.get("XDG_CACHE_HOME") or str(_Path.home() / ".cache")
|
|
36
|
+
_cache_dir = str(_Path(_xdg) / "pdft" / "jax-compile-cache")
|
|
37
|
+
try:
|
|
38
|
+
_Path(_cache_dir).mkdir(parents=True, exist_ok=True)
|
|
39
|
+
_jax.config.update("jax_compilation_cache_dir", _cache_dir)
|
|
40
|
+
_jax.config.update("jax_persistent_cache_min_compile_time_secs", 0.0)
|
|
41
|
+
except OSError:
|
|
42
|
+
pass
|
|
43
|
+
|
|
44
|
+
__version__ = "0.2.1"
|
|
45
|
+
__upstream_ref__ = "nzy1997/ParametricDFT.jl@a201a27e47df2f0f3ab460f83d49b6e5f5d1e9ef"
|
|
46
|
+
|
|
47
|
+
# Slim public re-export hub: most-used names only. Anything else: import
|
|
48
|
+
# from the relevant subpackage directly.
|
|
49
|
+
from .bases import ( # noqa: E402
|
|
50
|
+
AbstractSparseBasis,
|
|
51
|
+
BlockedBasis,
|
|
52
|
+
EntangledQFTBasis,
|
|
53
|
+
MERABasis,
|
|
54
|
+
QFTBasis,
|
|
55
|
+
RealRichBasis,
|
|
56
|
+
RichBasis,
|
|
57
|
+
TEBDBasis,
|
|
58
|
+
bases_allclose,
|
|
59
|
+
)
|
|
60
|
+
from .loss import AbstractLoss, L1Norm, MSELoss, loss_function # noqa: E402
|
|
61
|
+
from .optimizers import RiemannianAdam, RiemannianGD, optimize # noqa: E402
|
|
62
|
+
from .training import TrainingResult, train_basis, train_basis_batched # noqa: E402
|
|
63
|
+
|
|
64
|
+
__all__ = [
|
|
65
|
+
"AbstractLoss",
|
|
66
|
+
"AbstractSparseBasis",
|
|
67
|
+
"BlockedBasis",
|
|
68
|
+
"EntangledQFTBasis",
|
|
69
|
+
"L1Norm",
|
|
70
|
+
"MERABasis",
|
|
71
|
+
"MSELoss",
|
|
72
|
+
"QFTBasis",
|
|
73
|
+
"RealRichBasis",
|
|
74
|
+
"RichBasis",
|
|
75
|
+
"RiemannianAdam",
|
|
76
|
+
"RiemannianGD",
|
|
77
|
+
"TEBDBasis",
|
|
78
|
+
"TrainingResult",
|
|
79
|
+
"__upstream_ref__",
|
|
80
|
+
"__version__",
|
|
81
|
+
"bases_allclose",
|
|
82
|
+
"loss_function",
|
|
83
|
+
"optimize",
|
|
84
|
+
"train_basis",
|
|
85
|
+
"train_basis_batched",
|
|
86
|
+
]
|
pdft/bases/__init__.py
ADDED
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
"""Sparse-basis subpackage.
|
|
2
|
+
|
|
3
|
+
Two families:
|
|
4
|
+
- bases.circuit — full circuit topologies (QFT, EntangledQFT, TEBD, MERA),
|
|
5
|
+
comparable to FFT/DCT.
|
|
6
|
+
- bases.block — parameter-efficient block-structured bases (Blocked,
|
|
7
|
+
Rich, RealRich) over arbitrary block partitions.
|
|
8
|
+
|
|
9
|
+
The abstract base class and bases_allclose helper live in bases.base.
|
|
10
|
+
"""
|
|
11
|
+
|
|
12
|
+
from .base import (
|
|
13
|
+
AbstractSparseBasis,
|
|
14
|
+
EntangledQFTBasis,
|
|
15
|
+
MERABasis,
|
|
16
|
+
QFTBasis,
|
|
17
|
+
TEBDBasis,
|
|
18
|
+
bases_allclose,
|
|
19
|
+
)
|
|
20
|
+
from .block import BlockedBasis, RealRichBasis, RichBasis, fit_to_dct
|
|
21
|
+
from .circuit import entangled_qft_code, ft_mat, ift_mat, mera_code, qft_code, tebd_code
|
|
22
|
+
|
|
23
|
+
__all__ = [
|
|
24
|
+
"AbstractSparseBasis",
|
|
25
|
+
"BlockedBasis",
|
|
26
|
+
"EntangledQFTBasis",
|
|
27
|
+
"MERABasis",
|
|
28
|
+
"QFTBasis",
|
|
29
|
+
"RealRichBasis",
|
|
30
|
+
"RichBasis",
|
|
31
|
+
"TEBDBasis",
|
|
32
|
+
"bases_allclose",
|
|
33
|
+
"entangled_qft_code",
|
|
34
|
+
"fit_to_dct",
|
|
35
|
+
"ft_mat",
|
|
36
|
+
"ift_mat",
|
|
37
|
+
"mera_code",
|
|
38
|
+
"qft_code",
|
|
39
|
+
"tebd_code",
|
|
40
|
+
]
|
pdft/bases/base.py
ADDED
|
@@ -0,0 +1,470 @@
|
|
|
1
|
+
"""Sparse basis abstraction + concrete bases.
|
|
2
|
+
|
|
3
|
+
Mirror of upstream src/basis.jl. Phase 1 shipped QFTBasis; Phase 3 adds
|
|
4
|
+
EntangledQFTBasis, TEBDBasis, and MERABasis.
|
|
5
|
+
"""
|
|
6
|
+
|
|
7
|
+
from __future__ import annotations
|
|
8
|
+
|
|
9
|
+
from collections.abc import Sequence
|
|
10
|
+
from dataclasses import dataclass, field
|
|
11
|
+
from typing import Protocol, runtime_checkable
|
|
12
|
+
|
|
13
|
+
import jax
|
|
14
|
+
import jax.numpy as jnp
|
|
15
|
+
from jax import tree_util
|
|
16
|
+
|
|
17
|
+
from .circuit.qft import qft_code
|
|
18
|
+
|
|
19
|
+
Array = jax.Array
|
|
20
|
+
|
|
21
|
+
|
|
22
|
+
@runtime_checkable
|
|
23
|
+
class AbstractSparseBasis(Protocol):
|
|
24
|
+
@property
|
|
25
|
+
def image_size(self) -> tuple[int, int]: ...
|
|
26
|
+
@property
|
|
27
|
+
def num_parameters(self) -> int: ...
|
|
28
|
+
def forward_transform(self, pic: Array) -> Array: ...
|
|
29
|
+
def inverse_transform(self, pic: Array) -> Array: ...
|
|
30
|
+
|
|
31
|
+
|
|
32
|
+
@dataclass
|
|
33
|
+
class QFTBasis:
|
|
34
|
+
"""QFT tensor-network basis. Mirror of `ParametricDFT.jl/src/basis.jl::QFTBasis`.
|
|
35
|
+
|
|
36
|
+
Stores ONE tensor list. `inverse_transform` applies `conj(tensors)` through
|
|
37
|
+
`inv_code` — exactly like Julia's `inverse_transform(basis::QFTBasis, ...)`
|
|
38
|
+
which does `basis.inverse_code(conj.(basis.tensors)..., ...)`.
|
|
39
|
+
|
|
40
|
+
`code` and `inv_code` are jit-compiled einsum closures and compare by
|
|
41
|
+
identity; they are marked `compare=False` so the dataclass-generated
|
|
42
|
+
`__eq__` doesn't consider them. Use `bases_allclose` for semantic
|
|
43
|
+
comparison.
|
|
44
|
+
|
|
45
|
+
Pytree contract:
|
|
46
|
+
leaves = tensors (one list)
|
|
47
|
+
aux data = (m, n, len(tensors), code, inv_code)
|
|
48
|
+
"""
|
|
49
|
+
|
|
50
|
+
m: int
|
|
51
|
+
n: int
|
|
52
|
+
tensors: list[Array]
|
|
53
|
+
code: object = field(compare=False, repr=False)
|
|
54
|
+
inv_code: object = field(compare=False, repr=False)
|
|
55
|
+
|
|
56
|
+
def __init__(
|
|
57
|
+
self,
|
|
58
|
+
m: int,
|
|
59
|
+
n: int,
|
|
60
|
+
tensors: Sequence[Array] | None = None,
|
|
61
|
+
code: object | None = None,
|
|
62
|
+
inv_code: object | None = None,
|
|
63
|
+
):
|
|
64
|
+
if m < 1 or n < 1:
|
|
65
|
+
raise ValueError(f"m and n must be >= 1, got m={m}, n={n}")
|
|
66
|
+
self.m = m
|
|
67
|
+
self.n = n
|
|
68
|
+
_code, init_tensors = qft_code(m, n)
|
|
69
|
+
_inv_code, _ = qft_code(m, n, inverse=True)
|
|
70
|
+
self.tensors = list(tensors) if tensors is not None else init_tensors
|
|
71
|
+
self.code = code if code is not None else _code
|
|
72
|
+
self.inv_code = inv_code if inv_code is not None else _inv_code
|
|
73
|
+
|
|
74
|
+
@property
|
|
75
|
+
def inv_tensors(self) -> list[Array]:
|
|
76
|
+
"""Back-compat alias: Julia stores one tensor list. The "inverse"
|
|
77
|
+
is computed by applying `conj(tensors)` through `inv_code`. We
|
|
78
|
+
expose this property so older callers that read `basis.inv_tensors`
|
|
79
|
+
keep working — they get the same arrays as `basis.tensors`.
|
|
80
|
+
"""
|
|
81
|
+
return self.tensors
|
|
82
|
+
|
|
83
|
+
# ------ AbstractSparseBasis interface ------
|
|
84
|
+
|
|
85
|
+
@property
|
|
86
|
+
def image_size(self) -> tuple[int, int]:
|
|
87
|
+
return (2**self.m, 2**self.n)
|
|
88
|
+
|
|
89
|
+
@property
|
|
90
|
+
def num_parameters(self) -> int:
|
|
91
|
+
return sum(int(t.size) for t in self.tensors)
|
|
92
|
+
|
|
93
|
+
def forward_transform(self, pic: Array) -> Array:
|
|
94
|
+
from .circuit.qft import ft_mat
|
|
95
|
+
|
|
96
|
+
return ft_mat(self.tensors, self.code, self.m, self.n, pic)
|
|
97
|
+
|
|
98
|
+
def inverse_transform(self, pic: Array) -> Array:
|
|
99
|
+
from .circuit.qft import ift_mat
|
|
100
|
+
|
|
101
|
+
return ift_mat(
|
|
102
|
+
[jnp.conj(t) for t in self.tensors],
|
|
103
|
+
self.inv_code,
|
|
104
|
+
self.m,
|
|
105
|
+
self.n,
|
|
106
|
+
pic,
|
|
107
|
+
)
|
|
108
|
+
|
|
109
|
+
|
|
110
|
+
# ---------------------------------------------------------------------------
|
|
111
|
+
# JAX pytree registration
|
|
112
|
+
# ---------------------------------------------------------------------------
|
|
113
|
+
|
|
114
|
+
|
|
115
|
+
def _qftbasis_flatten(b: QFTBasis):
|
|
116
|
+
leaves = tuple(b.tensors)
|
|
117
|
+
aux = (b.m, b.n, len(b.tensors), b.code, b.inv_code)
|
|
118
|
+
return leaves, aux
|
|
119
|
+
|
|
120
|
+
|
|
121
|
+
def _qftbasis_unflatten(aux, leaves) -> QFTBasis:
|
|
122
|
+
m, n, n_fwd, code, inv_code = aux
|
|
123
|
+
assert len(leaves) == n_fwd
|
|
124
|
+
tensors = list(leaves)
|
|
125
|
+
return QFTBasis(m=m, n=n, tensors=tensors, code=code, inv_code=inv_code)
|
|
126
|
+
|
|
127
|
+
|
|
128
|
+
tree_util.register_pytree_node(QFTBasis, _qftbasis_flatten, _qftbasis_unflatten)
|
|
129
|
+
|
|
130
|
+
|
|
131
|
+
# ---------------------------------------------------------------------------
|
|
132
|
+
# Equality helper
|
|
133
|
+
# ---------------------------------------------------------------------------
|
|
134
|
+
|
|
135
|
+
|
|
136
|
+
def bases_allclose(a, b, *, atol: float = 1e-10) -> bool:
|
|
137
|
+
"""Semantic equality across any of the registered basis types.
|
|
138
|
+
|
|
139
|
+
Checks same concrete type, same (m, n), and all `tensors` within `atol`
|
|
140
|
+
(rtol=0). `code` / `inv_code` are ignored. Per Julia's design we no
|
|
141
|
+
longer store a separate `inv_tensors` to compare.
|
|
142
|
+
"""
|
|
143
|
+
if type(a) is not type(b):
|
|
144
|
+
return False
|
|
145
|
+
if (a.m, a.n) != (b.m, b.n):
|
|
146
|
+
return False
|
|
147
|
+
if len(a.tensors) != len(b.tensors):
|
|
148
|
+
return False
|
|
149
|
+
for x, y in zip(a.tensors, b.tensors):
|
|
150
|
+
if not jnp.allclose(x, y, atol=atol, rtol=0.0):
|
|
151
|
+
return False
|
|
152
|
+
return True
|
|
153
|
+
|
|
154
|
+
|
|
155
|
+
# ---------------------------------------------------------------------------
|
|
156
|
+
# EntangledQFTBasis
|
|
157
|
+
# ---------------------------------------------------------------------------
|
|
158
|
+
|
|
159
|
+
|
|
160
|
+
@dataclass
|
|
161
|
+
class EntangledQFTBasis:
|
|
162
|
+
"""QFT + appended entanglement layer on `min(m, n)` row/col qubit pairs.
|
|
163
|
+
|
|
164
|
+
Mirror of upstream src/basis.jl:280-500 (entangle_position=:back only).
|
|
165
|
+
"""
|
|
166
|
+
|
|
167
|
+
m: int
|
|
168
|
+
n: int
|
|
169
|
+
n_entangle: int
|
|
170
|
+
tensors: list[Array]
|
|
171
|
+
code: object = field(compare=False, repr=False)
|
|
172
|
+
inv_code: object = field(compare=False, repr=False)
|
|
173
|
+
|
|
174
|
+
def __init__(
|
|
175
|
+
self,
|
|
176
|
+
m: int,
|
|
177
|
+
n: int,
|
|
178
|
+
tensors: Sequence[Array] | None = None,
|
|
179
|
+
entangle_phases: Sequence[float] | None = None,
|
|
180
|
+
entangle_position: str = "back",
|
|
181
|
+
code: object | None = None,
|
|
182
|
+
inv_code: object | None = None,
|
|
183
|
+
seed: int | None = None,
|
|
184
|
+
):
|
|
185
|
+
"""Mirror of `_init_circuit(::Type{EntangledQFTBasis}, ...)` from
|
|
186
|
+
ParametricDFT.jl/src/training.jl. When `seed` is provided AND
|
|
187
|
+
`entangle_phases` is None, draws phases from
|
|
188
|
+
`np.random.default_rng(seed).normal(0, 0.1, n_entangle)` — Julia's
|
|
189
|
+
`randn(n_gates) * 0.1` convention. This breaks the symmetry-collapse
|
|
190
|
+
where `EntangledQFTBasis` initialises identically to `QFTBasis` when
|
|
191
|
+
all entanglement phases are zero.
|
|
192
|
+
"""
|
|
193
|
+
import numpy as np
|
|
194
|
+
|
|
195
|
+
from .circuit.entangled_qft import entangled_qft_code
|
|
196
|
+
|
|
197
|
+
if m < 1 or n < 1:
|
|
198
|
+
raise ValueError(f"m and n must be >= 1, got m={m}, n={n}")
|
|
199
|
+
self.m = m
|
|
200
|
+
self.n = n
|
|
201
|
+
n_entangle = min(m, n)
|
|
202
|
+
if entangle_phases is None and seed is not None:
|
|
203
|
+
entangle_phases = list(np.random.default_rng(seed).normal(0.0, 0.1, n_entangle))
|
|
204
|
+
_code, init_tensors, self.n_entangle = entangled_qft_code(
|
|
205
|
+
m, n, entangle_phases=entangle_phases, entangle_position=entangle_position
|
|
206
|
+
)
|
|
207
|
+
_inv_code, _, _ = entangled_qft_code(
|
|
208
|
+
m, n, entangle_phases=entangle_phases, entangle_position=entangle_position, inverse=True
|
|
209
|
+
)
|
|
210
|
+
self.tensors = list(tensors) if tensors is not None else init_tensors
|
|
211
|
+
self.code = code if code is not None else _code
|
|
212
|
+
self.inv_code = inv_code if inv_code is not None else _inv_code
|
|
213
|
+
|
|
214
|
+
@property
|
|
215
|
+
def inv_tensors(self) -> list[Array]:
|
|
216
|
+
return self.tensors
|
|
217
|
+
|
|
218
|
+
@property
|
|
219
|
+
def image_size(self) -> tuple[int, int]:
|
|
220
|
+
return (2**self.m, 2**self.n)
|
|
221
|
+
|
|
222
|
+
@property
|
|
223
|
+
def num_parameters(self) -> int:
|
|
224
|
+
return sum(int(t.size) for t in self.tensors)
|
|
225
|
+
|
|
226
|
+
def forward_transform(self, pic: Array) -> Array:
|
|
227
|
+
from ..circuit.builder import apply_circuit
|
|
228
|
+
|
|
229
|
+
return apply_circuit(self.tensors, self.code, self.m, self.n, pic)
|
|
230
|
+
|
|
231
|
+
def inverse_transform(self, pic: Array) -> Array:
|
|
232
|
+
from ..circuit.builder import apply_circuit
|
|
233
|
+
|
|
234
|
+
return apply_circuit(
|
|
235
|
+
[jnp.conj(t) for t in self.tensors],
|
|
236
|
+
self.inv_code,
|
|
237
|
+
self.m,
|
|
238
|
+
self.n,
|
|
239
|
+
pic,
|
|
240
|
+
)
|
|
241
|
+
|
|
242
|
+
|
|
243
|
+
def _entangled_flatten(b: EntangledQFTBasis):
|
|
244
|
+
leaves = tuple(b.tensors)
|
|
245
|
+
aux = (b.m, b.n, b.n_entangle, len(b.tensors), b.code, b.inv_code)
|
|
246
|
+
return leaves, aux
|
|
247
|
+
|
|
248
|
+
|
|
249
|
+
def _entangled_unflatten(aux, leaves) -> EntangledQFTBasis:
|
|
250
|
+
m, n, n_entangle, n_fwd, code, inv_code = aux
|
|
251
|
+
tensors = list(leaves)
|
|
252
|
+
out = EntangledQFTBasis.__new__(EntangledQFTBasis)
|
|
253
|
+
out.m = m
|
|
254
|
+
out.n = n
|
|
255
|
+
out.n_entangle = n_entangle
|
|
256
|
+
out.tensors = tensors
|
|
257
|
+
out.code = code
|
|
258
|
+
out.inv_code = inv_code
|
|
259
|
+
return out
|
|
260
|
+
|
|
261
|
+
|
|
262
|
+
tree_util.register_pytree_node(EntangledQFTBasis, _entangled_flatten, _entangled_unflatten)
|
|
263
|
+
|
|
264
|
+
|
|
265
|
+
# ---------------------------------------------------------------------------
|
|
266
|
+
# TEBDBasis
|
|
267
|
+
# ---------------------------------------------------------------------------
|
|
268
|
+
|
|
269
|
+
|
|
270
|
+
@dataclass
|
|
271
|
+
class TEBDBasis:
|
|
272
|
+
"""2D TEBD basis with row + column rings of controlled-phase gates.
|
|
273
|
+
|
|
274
|
+
Mirror of upstream src/basis.jl:600-745.
|
|
275
|
+
"""
|
|
276
|
+
|
|
277
|
+
m: int
|
|
278
|
+
n: int
|
|
279
|
+
n_row_gates: int
|
|
280
|
+
n_col_gates: int
|
|
281
|
+
tensors: list[Array]
|
|
282
|
+
code: object = field(compare=False, repr=False)
|
|
283
|
+
inv_code: object = field(compare=False, repr=False)
|
|
284
|
+
|
|
285
|
+
def __init__(
|
|
286
|
+
self,
|
|
287
|
+
m: int,
|
|
288
|
+
n: int,
|
|
289
|
+
tensors: Sequence[Array] | None = None,
|
|
290
|
+
phases: Sequence[float] | None = None,
|
|
291
|
+
code: object | None = None,
|
|
292
|
+
inv_code: object | None = None,
|
|
293
|
+
seed: int | None = None,
|
|
294
|
+
):
|
|
295
|
+
"""When `seed` is provided AND `phases` is None, draws phases from
|
|
296
|
+
`np.random.default_rng(seed).normal(0, 0.1, n_row_gates + n_col_gates)`
|
|
297
|
+
— Julia's `randn(n_gates) * 0.1` convention from
|
|
298
|
+
`ParametricDFT.jl/src/training.jl::_init_circuit`.
|
|
299
|
+
"""
|
|
300
|
+
import numpy as np
|
|
301
|
+
|
|
302
|
+
from .circuit.tebd import tebd_code
|
|
303
|
+
|
|
304
|
+
if m < 1 or n < 1:
|
|
305
|
+
raise ValueError(f"m and n must be >= 1, got m={m}, n={n}")
|
|
306
|
+
self.m = m
|
|
307
|
+
self.n = n
|
|
308
|
+
if phases is None and seed is not None:
|
|
309
|
+
phases = list(np.random.default_rng(seed).normal(0.0, 0.1, m + n))
|
|
310
|
+
_code, init_tensors, self.n_row_gates, self.n_col_gates = tebd_code(m, n, phases=phases)
|
|
311
|
+
_inv_code, _, _, _ = tebd_code(m, n, phases=phases, inverse=True)
|
|
312
|
+
self.tensors = list(tensors) if tensors is not None else init_tensors
|
|
313
|
+
self.code = code if code is not None else _code
|
|
314
|
+
self.inv_code = inv_code if inv_code is not None else _inv_code
|
|
315
|
+
|
|
316
|
+
@property
|
|
317
|
+
def inv_tensors(self) -> list[Array]:
|
|
318
|
+
return self.tensors
|
|
319
|
+
|
|
320
|
+
@property
|
|
321
|
+
def image_size(self) -> tuple[int, int]:
|
|
322
|
+
return (2**self.m, 2**self.n)
|
|
323
|
+
|
|
324
|
+
@property
|
|
325
|
+
def num_parameters(self) -> int:
|
|
326
|
+
return sum(int(t.size) for t in self.tensors)
|
|
327
|
+
|
|
328
|
+
def forward_transform(self, pic: Array) -> Array:
|
|
329
|
+
from ..circuit.builder import apply_circuit
|
|
330
|
+
|
|
331
|
+
return apply_circuit(self.tensors, self.code, self.m, self.n, pic)
|
|
332
|
+
|
|
333
|
+
def inverse_transform(self, pic: Array) -> Array:
|
|
334
|
+
from ..circuit.builder import apply_circuit
|
|
335
|
+
|
|
336
|
+
return apply_circuit(
|
|
337
|
+
[jnp.conj(t) for t in self.tensors],
|
|
338
|
+
self.inv_code,
|
|
339
|
+
self.m,
|
|
340
|
+
self.n,
|
|
341
|
+
pic,
|
|
342
|
+
)
|
|
343
|
+
|
|
344
|
+
|
|
345
|
+
def _tebd_flatten(b: TEBDBasis):
|
|
346
|
+
leaves = tuple(b.tensors)
|
|
347
|
+
aux = (b.m, b.n, b.n_row_gates, b.n_col_gates, len(b.tensors), b.code, b.inv_code)
|
|
348
|
+
return leaves, aux
|
|
349
|
+
|
|
350
|
+
|
|
351
|
+
def _tebd_unflatten(aux, leaves) -> TEBDBasis:
|
|
352
|
+
m, n, nr, nc, n_fwd, code, inv_code = aux
|
|
353
|
+
tensors = list(leaves)
|
|
354
|
+
out = TEBDBasis.__new__(TEBDBasis)
|
|
355
|
+
out.m = m
|
|
356
|
+
out.n = n
|
|
357
|
+
out.n_row_gates = nr
|
|
358
|
+
out.n_col_gates = nc
|
|
359
|
+
out.tensors = tensors
|
|
360
|
+
out.code = code
|
|
361
|
+
out.inv_code = inv_code
|
|
362
|
+
return out
|
|
363
|
+
|
|
364
|
+
|
|
365
|
+
tree_util.register_pytree_node(TEBDBasis, _tebd_flatten, _tebd_unflatten)
|
|
366
|
+
|
|
367
|
+
|
|
368
|
+
# ---------------------------------------------------------------------------
|
|
369
|
+
# MERABasis
|
|
370
|
+
# ---------------------------------------------------------------------------
|
|
371
|
+
|
|
372
|
+
|
|
373
|
+
@dataclass
|
|
374
|
+
class MERABasis:
|
|
375
|
+
"""2D MERA basis. Each dimension with >=2 qubits must be a power of 2.
|
|
376
|
+
|
|
377
|
+
Mirror of upstream src/basis.jl:840-1070.
|
|
378
|
+
"""
|
|
379
|
+
|
|
380
|
+
m: int
|
|
381
|
+
n: int
|
|
382
|
+
n_row_gates: int
|
|
383
|
+
n_col_gates: int
|
|
384
|
+
tensors: list[Array]
|
|
385
|
+
code: object = field(compare=False, repr=False)
|
|
386
|
+
inv_code: object = field(compare=False, repr=False)
|
|
387
|
+
|
|
388
|
+
def __init__(
|
|
389
|
+
self,
|
|
390
|
+
m: int,
|
|
391
|
+
n: int,
|
|
392
|
+
tensors: Sequence[Array] | None = None,
|
|
393
|
+
phases: Sequence[float] | None = None,
|
|
394
|
+
code: object | None = None,
|
|
395
|
+
inv_code: object | None = None,
|
|
396
|
+
seed: int | None = None,
|
|
397
|
+
):
|
|
398
|
+
"""When `seed` is provided AND `phases` is None, draws phases from
|
|
399
|
+
`np.random.default_rng(seed).normal(0, 0.1, n_gates)` — Julia's
|
|
400
|
+
`randn(n_gates) * 0.1` convention from
|
|
401
|
+
`ParametricDFT.jl/src/training.jl::_init_circuit`.
|
|
402
|
+
"""
|
|
403
|
+
import numpy as np
|
|
404
|
+
|
|
405
|
+
from .circuit.mera import _n_mera_gates, mera_code
|
|
406
|
+
|
|
407
|
+
if m < 1 or n < 1:
|
|
408
|
+
raise ValueError(f"m and n must be >= 1, got m={m}, n={n}")
|
|
409
|
+
self.m = m
|
|
410
|
+
self.n = n
|
|
411
|
+
if phases is None and seed is not None:
|
|
412
|
+
n_row = _n_mera_gates(m) if m >= 2 else 0
|
|
413
|
+
n_col = _n_mera_gates(n) if n >= 2 else 0
|
|
414
|
+
phases = list(np.random.default_rng(seed).normal(0.0, 0.1, n_row + n_col))
|
|
415
|
+
_code, init_tensors, self.n_row_gates, self.n_col_gates = mera_code(m, n, phases=phases)
|
|
416
|
+
_inv_code, _, _, _ = mera_code(m, n, phases=phases, inverse=True)
|
|
417
|
+
self.tensors = list(tensors) if tensors is not None else init_tensors
|
|
418
|
+
self.code = code if code is not None else _code
|
|
419
|
+
self.inv_code = inv_code if inv_code is not None else _inv_code
|
|
420
|
+
|
|
421
|
+
@property
|
|
422
|
+
def inv_tensors(self) -> list[Array]:
|
|
423
|
+
return self.tensors
|
|
424
|
+
|
|
425
|
+
@property
|
|
426
|
+
def image_size(self) -> tuple[int, int]:
|
|
427
|
+
return (2**self.m, 2**self.n)
|
|
428
|
+
|
|
429
|
+
@property
|
|
430
|
+
def num_parameters(self) -> int:
|
|
431
|
+
return sum(int(t.size) for t in self.tensors)
|
|
432
|
+
|
|
433
|
+
def forward_transform(self, pic: Array) -> Array:
|
|
434
|
+
from ..circuit.builder import apply_circuit
|
|
435
|
+
|
|
436
|
+
return apply_circuit(self.tensors, self.code, self.m, self.n, pic)
|
|
437
|
+
|
|
438
|
+
def inverse_transform(self, pic: Array) -> Array:
|
|
439
|
+
from ..circuit.builder import apply_circuit
|
|
440
|
+
|
|
441
|
+
return apply_circuit(
|
|
442
|
+
[jnp.conj(t) for t in self.tensors],
|
|
443
|
+
self.inv_code,
|
|
444
|
+
self.m,
|
|
445
|
+
self.n,
|
|
446
|
+
pic,
|
|
447
|
+
)
|
|
448
|
+
|
|
449
|
+
|
|
450
|
+
def _mera_flatten(b: MERABasis):
|
|
451
|
+
leaves = tuple(b.tensors)
|
|
452
|
+
aux = (b.m, b.n, b.n_row_gates, b.n_col_gates, len(b.tensors), b.code, b.inv_code)
|
|
453
|
+
return leaves, aux
|
|
454
|
+
|
|
455
|
+
|
|
456
|
+
def _mera_unflatten(aux, leaves) -> MERABasis:
|
|
457
|
+
m, n, nr, nc, n_fwd, code, inv_code = aux
|
|
458
|
+
tensors = list(leaves)
|
|
459
|
+
out = MERABasis.__new__(MERABasis)
|
|
460
|
+
out.m = m
|
|
461
|
+
out.n = n
|
|
462
|
+
out.n_row_gates = nr
|
|
463
|
+
out.n_col_gates = nc
|
|
464
|
+
out.tensors = tensors
|
|
465
|
+
out.code = code
|
|
466
|
+
out.inv_code = inv_code
|
|
467
|
+
return out
|
|
468
|
+
|
|
469
|
+
|
|
470
|
+
tree_util.register_pytree_node(MERABasis, _mera_flatten, _mera_unflatten)
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
"""Block-structured bases: Blocked, Rich, RealRich."""
|
|
2
|
+
|
|
3
|
+
from .block import BlockedBasis
|
|
4
|
+
from .rich import RichBasis, fit_to_dct
|
|
5
|
+
from .real_rich import RealRichBasis
|
|
6
|
+
|
|
7
|
+
__all__ = [
|
|
8
|
+
"BlockedBasis",
|
|
9
|
+
"RealRichBasis",
|
|
10
|
+
"RichBasis",
|
|
11
|
+
"fit_to_dct",
|
|
12
|
+
]
|