Skip to content

Commit

Permalink
[dev] allow MPS states for testing purposes
Browse files Browse the repository at this point in the history
This commit adds support for MPS states within the custom time evolver
implementations. This is useful for testing purposes when comparing the
algorithms against the backend specific default implementations (which
only allow MPS) or (e.g.) exact statevector simulations of
`QuantumCircuit` objects.

This feature is not meant for end-user consumption and, thus, not
advertised as such.
  • Loading branch information
mrossinek committed Jan 31, 2025
1 parent 8918dd1 commit ec220d7
Show file tree
Hide file tree
Showing 5 changed files with 360 additions and 24 deletions.
18 changes: 16 additions & 2 deletions qiskit_addon_mpf/backends/quimb_layers/evolver.py
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,8 @@

from __future__ import annotations

from quimb.tensor import MatrixProductState

from .. import quimb_tebd
from .model import LayerModel

Expand All @@ -34,7 +36,11 @@ class LayerwiseEvolver(quimb_tebd.TEBDEvolver):
"""

def __init__(
self, evolution_state: quimb_tebd.MPOState, layers: list[LayerModel], *args, **kwargs
self,
evolution_state: quimb_tebd.MPOState | MatrixProductState,
layers: list[LayerModel],
*args,
**kwargs,
) -> None:
"""Initialize a :class:`LayerwiseEvolver` instance.
Expand Down Expand Up @@ -62,14 +68,22 @@ def step(self) -> None:
"""
dt = self._dt

# NOTE: support for MatrixProductState objects is only added for testing/debugging purposes!
# This is not meant for consumption by end-users of the `qiskit_addon_mpf.dynamic` module
# and its use is highly discouraged.
is_mps = isinstance(self._pt, MatrixProductState)

for layer in self.layers:
self.H = layer
for i in range(self.L):
sites = (i, (i + 1) % self.L)
gate = self._get_gate_from_ham(1.0, sites)
if gate is None:
continue
self._pt.gate_split_(gate, sites, conj=self.conjugate, **self.split_opts)
if is_mps:
self._pt.gate_split_(gate, sites, **self.split_opts)
else:
self._pt.gate_split_(gate, sites, conj=self.conjugate, **self.split_opts)

self.t += dt
self._err += float("NaN")
23 changes: 19 additions & 4 deletions qiskit_addon_mpf/backends/quimb_tebd/evolver.py
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,7 @@

from typing import Literal, cast

from quimb.tensor import TEBD
from quimb.tensor import TEBD, MatrixProductState

from .. import Evolver
from .state import MPOState
Expand All @@ -37,7 +37,9 @@ class TEBDEvolver(TEBD, Evolver):
that API here.
"""

def __init__(self, evolution_state: MPOState, *args, order: int = 2, **kwargs) -> None:
def __init__(
self, evolution_state: MPOState | MatrixProductState, *args, order: int = 2, **kwargs
) -> None:
"""Initialize a :class:`TEBDEvolver` instance.
Args:
Expand Down Expand Up @@ -139,18 +141,31 @@ def sweep(
if dt is not None:
dt_frac *= dt / self._dt # pragma: no cover

# NOTE: support for MatrixProductState objects is only added for testing/debugging purposes!
# This is not meant for consumption by end-users of the `qiskit_addon_mpf.dynamic` module
# and its use is highly discouraged.
is_mps = isinstance(self._pt, MatrixProductState)

final_site_ind = self.L - 1
if direction == "right":
for i in range(0, final_site_ind, 2):
sites = (i, (i + 1) % self.L)
gate = self._get_gate_from_ham(dt_frac, sites)
self._pt.gate_split_(gate, sites, conj=self.conjugate, **self.split_opts)
if is_mps:
# NOTE: we ignore coverage here because the logic is tested via the LayerEvolver
self._pt.gate_split_(gate, sites, **self.split_opts) # pragma: no cover
else:
self._pt.gate_split_(gate, sites, conj=self.conjugate, **self.split_opts)

elif direction == "left":
for i in range(1, final_site_ind, 2):
sites = (i, (i + 1) % self.L)
gate = self._get_gate_from_ham(dt_frac, sites)
self._pt.gate_split_(gate, sites, conj=self.conjugate, **self.split_opts)
if is_mps:
# NOTE: we ignore coverage here because the logic is tested via the LayerEvolver
self._pt.gate_split_(gate, sites, **self.split_opts) # pragma: no cover
else:
self._pt.gate_split_(gate, sites, conj=self.conjugate, **self.split_opts)

else:
# NOTE: it should not be possible to reach this but we do a sanity check to ensure that
Expand Down
64 changes: 47 additions & 17 deletions qiskit_addon_mpf/backends/tenpy_tebd/evolver.py
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@

from tenpy import TEBDEngine, svd_theta
from tenpy.linalg import np_conserved as npc
from tenpy.networks import MPS

from .. import Evolver

Expand Down Expand Up @@ -96,35 +97,51 @@ def update_bond(self, i: int, U_bond: npc.Array) -> float:
"""
i0, i1 = i - 1, i
LOGGER.debug("Update sites (%d, %d)", i0, i1)

# NOTE: support for MatrixProductState objects is only added for testing/debugging purposes!
# This is not meant for consumption by end-users of the `qiskit_addon_mpf.dynamic` module
# and its use is highly discouraged.
is_mps = isinstance(self.psi, MPS)

leg_lbl = "v" if is_mps else "w"
left_leg = f"{leg_lbl}L"
right_leg = f"{leg_lbl}R"
p0s = ("p0",) if is_mps else ("p0", "p0*")
p1s = ("p1",) if is_mps else ("p1", "p1*")
ps = ("p",) if is_mps else ("p", "p*")

# Construct the theta matrix
C0 = self.psi.get_W(i0)
C1 = self.psi.get_W(i1)
C0 = self.psi.get_B(i0) if is_mps else self.psi.get_W(i0)
C1 = self.psi.get_B(i1) if is_mps else self.psi.get_W(i1)

C = npc.tensordot(C0, C1, axes=(["wR"], ["wL"]))
new_labels = ["wL", "p0", "p0*", "p1", "p1*", "wR"]
C = npc.tensordot(C0, C1, axes=([right_leg], [left_leg]))
new_labels = [left_leg, *p0s, *p1s, right_leg]
C.iset_leg_labels(new_labels)

# apply U to C
if self.conjugate:
C = npc.tensordot(U_bond.conj(), C, axes=(["p0", "p1"], ["p0*", "p1*"])) # apply U
else:
C = npc.tensordot(U_bond, C, axes=(["p0*", "p1*"], ["p0", "p1"])) # apply U
C.itranspose(["wL", "p0", "p0*", "p1", "p1*", "wR"])
theta = C.scale_axis(self.psi.Ss[i0], "wL")

C.itranspose([left_leg, *p0s, *p1s, right_leg])

theta = C.scale_axis(self.psi.get_SL(i0) if is_mps else self.psi.Ss[i0], left_leg)
# now theta is the same as if we had done
# theta = self.psi.get_theta(i0, n=2)
# theta = npc.tensordot(U_bond, theta, axes=(['p0*', 'p1*'], ['p0', 'p1'])) # apply U
# but also have C which is the same except the missing "S" on the left
# so we don't have to apply inverses of S (see below)

# theta = theta.combine_legs([("wL", "p0", "p0*"), ("p1", "p1*", "wR")], qconj=[+1, -1])
theta = theta.combine_legs([[0, 1, 2], [3, 4, 5]], qconj=[+1, -1])
theta = theta.combine_legs([(left_leg, *p0s), (*p1s, right_leg)], qconj=[+1, -1])

# Perform the SVD and truncate the wavefunction
U, S, V, trunc_err, renormalize = svd_theta(
theta, self.trunc_params, [None, None], inner_labels=["wR", "wL"]
theta, self.trunc_params, [None, None], inner_labels=[right_leg, left_leg]
)

# Split tensor and update matrices
B_R = V.split_legs(1).ireplace_labels(["p1", "p1*"], ["p", "p*"])
B_R = V.split_legs(1).ireplace_labels(p1s, ps)

# In general, we want to do the following:
# U = U.iscale_axis(S, 'vR')
Expand All @@ -137,16 +154,29 @@ def update_bond(self, i: int, U_bond: npc.Array) -> float:
# such that we obtain ``B_L = SL**-1 U S = SL**-1 U S V V^dagger = C V^dagger``
# here, C is the same as theta, but without the `S` on the very left
# (Note: this requires no inverse if the MPS is initially in 'B' canonical form)

def conj(labels: tuple[str, ...]):
"""Conjugates a tuple of leg labels."""
return tuple(lbl[:-1] if lbl[-1] == "*" else lbl + "*" for lbl in labels)

B_L = npc.tensordot(
C.combine_legs(("p1", "p1*", "wR"), pipes=theta.legs[1]),
C.combine_legs((*p1s, right_leg), pipes=theta.legs[1]),
V.conj(),
axes=["(p1.p1*.wR)", "(p1*.p1.wR*)"],
axes=[f"({'.'.join(p1s)}.{right_leg})", f"({'.'.join(conj(p1s))}.{right_leg}*)"],
)
B_L.ireplace_labels(["wL*", "p0", "p0*"], ["wR", "p", "p*"])
B_L.ireplace_labels([f"{left_leg}*", *p0s], [right_leg, *ps])
B_L /= renormalize # re-normalize to <psi|psi> = 1

self.psi.Ss[i1] = S
self.psi.set_W(i0, B_L)
self.psi.set_W(i1, B_R)
self._trunc_err_bonds[i] = self._trunc_err_bonds[i] + trunc_err
if is_mps:
self.psi.norm *= renormalize
self.psi.set_SR(i0, S)
self.psi.set_B(i0, B_L, form="B")
self.psi.set_B(i1, B_R, form="B")
else:
self.psi.Ss[i1] = S
self.psi.set_W(i0, B_L)
self.psi.set_W(i1, B_R)

self._trunc_err_bonds[i0] = self._trunc_err_bonds[i0] + trunc_err

return cast(float, trunc_err)
155 changes: 155 additions & 0 deletions test/backends/quimb_layers/test_evolver.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,155 @@
# This code is a Qiskit project.
#
# (C) Copyright IBM 2025.
#
# This code is licensed under the Apache License, Version 2.0. You may
# obtain a copy of this license in the LICENSE.txt file in the root directory
# of this source tree or at http://www.apache.org/licenses/LICENSE-2.0.
#
# Any modifications or derivative works of this code must retain this
# copyright notice, and modified files need to carry a notice indicating
# that they have been altered from the originals.


import numpy as np
import pytest
from qiskit.circuit import QuantumCircuit
from qiskit.circuit.library import XXPlusYYGate
from qiskit_addon_mpf.backends import HAS_QUIMB

if HAS_QUIMB:
from qiskit_addon_mpf.backends.quimb_layers import LayerModel, LayerwiseEvolver
from quimb.tensor import MPS_neel_state, SpinHam1D


def gen_ext_field_layer(n, hz):
qc = QuantumCircuit(n)
for q in range(n):
qc.rz(-hz[q], q)
return qc


def trotter_step(qc, q0, q1, Jxx, Jz):
qc.rzz(Jz, q0, q1)
qc.append(XXPlusYYGate(2.0 * Jxx), [q0, q1])


def gen_odd_coupling_layer(n, Jxx, Jz, J):
qc = QuantumCircuit(n)
for q in range(0, n, 2):
trotter_step(qc, q, q + 1, J[q] * Jxx, J[q] * Jz)
return qc


def gen_even_coupling_layer(n, Jxx, Jz, J):
qc = QuantumCircuit(n)
for q in range(1, n - 1, 2):
q0 = q
q1 = (q + 1) % n
if q1 < q0:
qc.barrier()
trotter_step(qc, q0, q1, J[q0] * Jxx, J[q0] * Jz)
return qc


@pytest.mark.skipif(not HAS_QUIMB, reason="Quimb is required for these unittests")
class TestLayerwiseEvolver:
def test_compare_statevector(self):
"""Test the time-evolution logic by comparing against an exact statevector simulation.
The reference value against which is being compared here can be obtained from:
.. code-block:: python
odd_coupling_layer = gen_odd_coupling_layer(L, dt * Jxx, dt * Jz, J)
even_coupling_layer = gen_even_coupling_layer(L, dt * Jxx, dt * Jz, J)
onsite_layer = gen_ext_field_layer(L, dt * hz)
layers = [
odd_coupling_layer,
even_coupling_layer,
onsite_layer,
onsite_layer,
even_coupling_layer,
odd_coupling_layer,
]
trotter_circ = QuantumCircuit(L)
for layer in layers:
trotter_circ = trotter_circ.compose(layer)
trotter_circ = trotter_circ.repeat(N)
init_circ = QuantumCircuit(L)
init_circ.x(1)
init_circ.x(3)
full_circ = init_circ.copy()
full_circ = full_circ.compose(trotter_circ)
init_state_vec = Statevector(init_circ)
full_state_vec = Statevector(full_circ)
reference = full_state_vec.inner(init_state_vec)
"""

np.random.seed(0)

L = 4
W = 0.5
epsilon = 0.5
J = np.random.rand(L - 1) + W * np.ones(L - 1)
Jz = 1.0
Jxx = epsilon
hz = 0.000000001 * np.array([(-1) ** i for i in range(L)])

N = 10
dt = 0.05

odd_coupling_layer = gen_odd_coupling_layer(L, Jxx, Jz, J)
even_coupling_layer = gen_even_coupling_layer(L, Jxx, Jz, J)
ext_field_layer = gen_ext_field_layer(L, hz)

# Initialize the builder for a spin 1/2 chain
builder = SpinHam1D(S=1 / 2)

# Add XX and YY couplings for neighboring sites
for i in range(L - 1):
builder[i, i + 1] += 2.0 * Jxx * J[i], "-", "+"
builder[i, i + 1] += 2.0 * Jxx * J[i], "+", "-"

# Add ZZ couplings for neighboring sites
for i in range(L - 1):
builder[i, i + 1] += 4.0 * Jz * J[i], "Z", "Z"

# Add the external Z-field (hz) to each site
for i in range(L):
builder[i] += -2.0 * hz[i], "Z"

layers = [
LayerModel.from_quantum_circuit(odd_coupling_layer, cyclic=False),
LayerModel.from_quantum_circuit(even_coupling_layer, cyclic=False),
LayerModel.from_quantum_circuit(ext_field_layer, keep_only_odd=True, cyclic=False),
LayerModel.from_quantum_circuit(ext_field_layer, keep_only_odd=False, cyclic=False),
LayerModel.from_quantum_circuit(ext_field_layer, keep_only_odd=False, cyclic=False),
LayerModel.from_quantum_circuit(ext_field_layer, keep_only_odd=True, cyclic=False),
LayerModel.from_quantum_circuit(even_coupling_layer, cyclic=False),
LayerModel.from_quantum_circuit(odd_coupling_layer, cyclic=False),
]

trunc_options = {
"max_bond": 100,
"cutoff": 1e-15,
"cutoff_mode": "rel",
"method": "svd",
"renorm": False,
}

initial_state = MPS_neel_state(L)
mps_state = initial_state.copy()
mps_evo = LayerwiseEvolver(
evolution_state=mps_state, layers=layers, dt=dt, split_opts=trunc_options
)
for _ in range(N):
mps_evo.step()

np.testing.assert_almost_equal(
initial_state.overlap(mps_evo.pt), -0.2607402383827852 - 0.6343830867298741j
)
Loading

0 comments on commit ec220d7

Please sign in to comment.