Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

[dev] allow MPS states for testing purposes #56

Merged
merged 1 commit into from
Jan 31, 2025
Merged

Conversation

mrossinek
Copy link
Member

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.

@mrossinek
Copy link
Member Author

A full comparison of all backends with MPS or MPO internal states against exact statevector simulations can be done using the following script:

Comparison Script
import numpy as np

# Qiskit imports
from qiskit.circuit import QuantumCircuit
from qiskit.circuit.library import XXPlusYYGate
from qiskit.quantum_info import Statevector

# MPF quimb-based imports
from qiskit_addon_mpf.backends.quimb_layers import (
    LayerModel as QuimbLayerModel,
)
from qiskit_addon_mpf.backends.quimb_layers import (
    LayerwiseEvolver as QuimbLayerwiseEvolver,
)
from qiskit_addon_mpf.backends.quimb_tebd import MPOState as QuimbMPOState

# MPF tenpy-based imports
from qiskit_addon_mpf.backends.tenpy_layers import (
    LayerModel as TenpyLayerModel,
)
from qiskit_addon_mpf.backends.tenpy_layers import (
    LayerwiseEvolver as TenpyLayerwiseEvolver,
)
from qiskit_addon_mpf.backends.tenpy_tebd import (
    MPOState as TenpyMPOState,
)
from qiskit_addon_mpf.backends.tenpy_tebd import (
    MPS_neel_state as tenpy_MPS_neel_state,
)

# qiskit-quimb imports
from qiskit_quimb import quimb_circuit

# quimb imports
from quimb.tensor import TEBD as QuimbTEBDEngine
from quimb.tensor import CircuitMPS, MPO_identity, SpinHam1D, TensorNetwork
from quimb.tensor import MPS_neel_state as quimb_MPS_neel_state

# tenpy imports
from tenpy.algorithms import TEBDEngine as TenpyTEBDEngine
from tenpy.models import XXZChain2
from tenpy.networks.site import SpinHalfSite

############################
##### Circuit Builders #####
############################


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


############################
##### Model Parameters #####
############################

np.random.seed(0)

# constants
L = 4
W = 0.5
epsilon = 0.5

J = np.random.rand(L - 1) + W * np.ones(L - 1)
# ZZ couplings
Jz = 1.0
# XX and YY couplings
Jxx = epsilon

# base coupling
# external field
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)

############################
##### Tenpy Parameters #####
############################

tenpy_model_opts = {
    "bc_MPS": "finite",
    "conserve": "Sz",
    "sort_charge": False,
}


class ConserveXXZChain2(XXZChain2):
    """TeNPy's XXZChain2 hard-codes Sz conservation. This subclass makes it configurable."""

    def init_sites(self, model_params):
        conserve = model_params.get("conserve", "Sz", str)
        sort_charge = model_params.get("sort_charge", True, bool)
        return SpinHalfSite(conserve=conserve, sort_charge=sort_charge)  # use predefined Site


# This is the full model that we want to simulate. It is used for the "exact" time evolution
# (which is approximated via a fourth-order Suzuki-Trotter formula).
tenpy_exact_model = ConserveXXZChain2(
    {
        "L": L,
        "Jz": 4.0 * Jz * J,
        "Jxx": 4.0 * Jxx * J,
        "hz": 2.0 * hz,
        **tenpy_model_opts,
    }
)

tenpy_layers = [
    TenpyLayerModel.from_quantum_circuit(odd_coupling_layer, **tenpy_model_opts),
    TenpyLayerModel.from_quantum_circuit(even_coupling_layer, **tenpy_model_opts),
    TenpyLayerModel.from_quantum_circuit(ext_field_layer, keep_only_odd=True, **tenpy_model_opts),
    TenpyLayerModel.from_quantum_circuit(ext_field_layer, keep_only_odd=False, **tenpy_model_opts),
    TenpyLayerModel.from_quantum_circuit(ext_field_layer, keep_only_odd=False, **tenpy_model_opts),
    TenpyLayerModel.from_quantum_circuit(ext_field_layer, keep_only_odd=True, **tenpy_model_opts),
    TenpyLayerModel.from_quantum_circuit(even_coupling_layer, **tenpy_model_opts),
    TenpyLayerModel.from_quantum_circuit(odd_coupling_layer, **tenpy_model_opts),
]

############################
##### Quimb Parameters #####
############################

# 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"

# Build the local Hamiltonian
quimb_exact_model = builder.build_local_ham(L)

quimb_layers = [
    QuimbLayerModel.from_quantum_circuit(odd_coupling_layer, cyclic=False),
    QuimbLayerModel.from_quantum_circuit(even_coupling_layer, cyclic=False),
    QuimbLayerModel.from_quantum_circuit(ext_field_layer, keep_only_odd=True, cyclic=False),
    QuimbLayerModel.from_quantum_circuit(ext_field_layer, keep_only_odd=False, cyclic=False),
    QuimbLayerModel.from_quantum_circuit(ext_field_layer, keep_only_odd=False, cyclic=False),
    QuimbLayerModel.from_quantum_circuit(ext_field_layer, keep_only_odd=True, cyclic=False),
    QuimbLayerModel.from_quantum_circuit(even_coupling_layer, cyclic=False),
    QuimbLayerModel.from_quantum_circuit(odd_coupling_layer, cyclic=False),
]

############################
##### Reference Values #####
############################

### Qiskit ###

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)
print("Qiskit Statevector", full_state_vec.inner(init_state_vec))

quimb_init_circ = quimb_circuit(init_circ)
quimb_full_circ = quimb_circuit(full_circ.decompose())
print("Quimb Circuit     ", TensorNetwork((quimb_full_circ.psi.H, quimb_init_circ.psi)).contract())

quimb_circ_mps = CircuitMPS(L)
quimb_circ_mps.apply_gates(quimb_full_circ.gates)
print("Quimb Circuit MPS ", TensorNetwork((quimb_circ_mps.psi.H, quimb_init_circ.psi)).contract())

### Tenpy ###

tenpy_trunc_options = {
    "trunc_params": {
        "chi_max": 100,
        "svd_min": 1e-15,
        "trunc_cut": None,
    },
    "preserve_norm": False,
    "order": 2,
}

tenpy_initial_state = tenpy_MPS_neel_state(tenpy_exact_model.lat)

tenpy_ref_state = tenpy_initial_state.copy()

tenpy_ref_evo = TenpyTEBDEngine(tenpy_ref_state, tenpy_exact_model, tenpy_trunc_options)
for _ in range(N):
    tenpy_ref_evo.run_evolution(1, dt)

print("Builtin TeNPy MPS ", tenpy_ref_state.overlap(tenpy_initial_state))

### Quimb ###

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

quimb_initial_state = quimb_MPS_neel_state(L)

quimb_ref_state = quimb_initial_state.copy()

quimb_ref_evo = QuimbTEBDEngine(
    quimb_ref_state, quimb_exact_model, dt=dt, split_opts=quimb_trunc_options
)
for _ in range(N):
    quimb_ref_evo.step(order=2)

# NOTE: quimb's Tensor.overlap method computes <other|self> while tenpy does <self|other>. Thus, we
# need to conjugate the result below, where we have kept the LHS and RHS objects identical to tenpy.
print("Builtin Quimb MPS ", quimb_ref_evo.pt.overlap(quimb_initial_state).conjugate())

############################
##### Tenpy  Results   #####
############################

### MPS ###

tenpy_mps_state = tenpy_initial_state.copy()
tenpy_mps_evo = TenpyLayerwiseEvolver(
    evolution_state=tenpy_mps_state, layers=tenpy_layers, options=tenpy_trunc_options
)
for _ in range(N):
    tenpy_mps_evo.run_evolution(1, dt)
print("Custom  TeNPy MPS ", tenpy_mps_state.overlap(tenpy_initial_state))

### MPO ###

tenpy_mpo_state = TenpyMPOState.initialize_from_lattice(tenpy_exact_model.lat)
tenpy_mpo_evo = TenpyLayerwiseEvolver(
    evolution_state=tenpy_mpo_state, layers=tenpy_layers, options=tenpy_trunc_options
)
tenpy_mpo_evo.conjugate = True
for _ in range(N):
    tenpy_mpo_evo.run_evolution(1, dt)
print("Custom  TeNPy MPO ", tenpy_mpo_state.overlap(tenpy_initial_state))

############################
##### Quimb  Results   #####
############################

### MPS ###

quimb_mps_state = quimb_initial_state.copy()
quimb_mps_evo = QuimbLayerwiseEvolver(
    evolution_state=quimb_mps_state, layers=quimb_layers, dt=dt, split_opts=quimb_trunc_options
)
for _ in range(N):
    quimb_mps_evo.step()
# NOTE: quimb's Tensor.overlap method computes <other|self> while tenpy does <self|other>. Thus, we
# need to conjugate the result below, where we have kept the LHS and RHS objects identical to tenpy.
print("Custom  Quimb MPS ", quimb_mps_evo.pt.overlap(quimb_initial_state).conjugate())

### MPO ###

quimb_mpo_state = QuimbMPOState(MPO_identity(L))
quimb_mpo_evo = QuimbLayerwiseEvolver(
    evolution_state=quimb_mpo_state, layers=quimb_layers, dt=dt, split_opts=quimb_trunc_options
)
quimb_mpo_evo.conjugate = True
for _ in range(N):
    quimb_mpo_evo.step()
print("Custom  Quimb MPO ", quimb_mpo_evo.pt.overlap(quimb_initial_state))

@coveralls
Copy link

coveralls commented Jan 31, 2025

Pull Request Test Coverage Report for Build 13073186785

Details

  • 40 of 40 (100.0%) changed or added relevant lines in 3 files are covered.
  • No unchanged relevant lines lost coverage.
  • Overall coverage remained the same at 100.0%

Totals Coverage Status
Change from base Build 13054930004: 0.0%
Covered Lines: 601
Relevant Lines: 601

💛 - Coveralls

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.
@mrossinek mrossinek merged commit 468c2f4 into main Jan 31, 2025
15 checks passed
@mrossinek mrossinek deleted the mps-evolution branch January 31, 2025 13:10
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

2 participants