Source code for qutip_qip.device.spinchain

from copy import deepcopy

import numpy as np

from qutip import sigmax, sigmay, sigmaz, tensor
from ..circuit import QubitCircuit
from .processor import Model
from .modelprocessor import ModelProcessor, _to_array
from ..pulse import Pulse
from ..compiler import SpinChainCompiler
from ..transpiler import to_chain_structure


__all__ = ["SpinChain", "LinearSpinChain", "CircularSpinChain"]


class SpinChain(ModelProcessor):
    """
    The processor based on the physical implementation of
    a spin chain qubits system.
    The available Hamiltonian of the system is predefined.
    The processor can simulate the evolution under the given
    control pulses either numerically or analytically.
    This is the base class and should not be used directly.
    Please use :class:`.LinearSpinChain` or :class:`.CircularSpinChain`.

    Parameters
    ----------
    num_qubits: int
        The number of qubits in the system.

    correct_global_phase: float, optional
        Save the global phase, the analytical solution
        will track the global phase.
        It has no effect on the numerical solution.

    **params:
        Hardware parameters. See :obj:`.SpinChainModel`.
    """

    def __init__(
        self, num_qubits, correct_global_phase=True, model=None, **params
    ):
        super(SpinChain, self).__init__(
            num_qubits=num_qubits,
            correct_global_phase=correct_global_phase,
            model=model,
            **params,
        )
        self.correct_global_phase = correct_global_phase
        self.spline_kind = "step_func"
        self.native_gates = ["SQRTISWAP", "ISWAP", "RX", "RZ"]

    @property
    def sx_ops(self):
        """list: A list of sigmax Hamiltonians for each qubit."""
        return self.ctrls[: self.num_qubits]

    @property
    def sz_ops(self):
        """list: A list of sigmaz Hamiltonians for each qubit."""
        return self.ctrls[self.num_qubits : 2 * self.num_qubits]

    @property
    def sxsy_ops(self):
        """
        list: A list of tensor(sigmax, sigmay)
        interacting Hamiltonians for each qubit.
        """
        return self.ctrls[2 * self.num_qubits :]

    @property
    def sx_u(self):
        """array-like: Pulse coefficients for sigmax Hamiltonians."""
        return self.coeffs[: self.num_qubits]

    @property
    def sz_u(self):
        """array-like: Pulse coefficients for sigmaz Hamiltonians."""
        return self.coeffs[self.num_qubits : 2 * self.num_qubits]

    @property
    def sxsy_u(self):
        """
        array-like: Pulse coefficients for tensor(sigmax, sigmay)
        interacting Hamiltonians.
        """
        return self.coeffs[2 * self.num_qubits :]

    def load_circuit(self, qc, setup, schedule_mode="ASAP", compiler=None):
        if compiler is None:
            compiler = SpinChainCompiler(
                self.num_qubits, self.params, setup=setup
            )
        tlist, coeffs = super().load_circuit(
            qc, schedule_mode=schedule_mode, compiler=compiler
        )
        self.global_phase = compiler.global_phase
        return tlist, coeffs


[docs]class LinearSpinChain(SpinChain): """ Spin chain model with open-end topology. For the control Hamiltonian please refer to :obj:`SpinChainModel`. Parameters ---------- num_qubits: int The number of qubits in the system. correct_global_phase: float, optional Save the global phase, the analytical solution will track the global phase. It has no effect on the numerical solution. **params: Hardware parameters. See :obj:`.SpinChainModel`. Examples -------- .. testcode:: import numpy as np import qutip from qutip_qip.circuit import QubitCircuit from qutip_qip.device import LinearSpinChain qc = QubitCircuit(2) qc.add_gate("RX", 0, arg_value=np.pi) qc.add_gate("RY", 1, arg_value=np.pi) qc.add_gate("ISWAP", [1, 0]) processor = LinearSpinChain(2, g=0.1, t1=300) processor.load_circuit(qc) init_state = qutip.basis([2, 2], [0, 0]) result = processor.run_state(init_state) print(round(qutip.fidelity(result.states[-1], qc.run(init_state)), 4)) .. testoutput:: 0.994 """ def __init__( self, num_qubits=None, correct_global_phase=True, **params, ): model = SpinChainModel(num_qubits=num_qubits, setup="linear", **params) super(LinearSpinChain, self).__init__( num_qubits, correct_global_phase=correct_global_phase, model=model, ) @property def sxsy_ops(self): """ list: A list of tensor(sigmax, sigmay) interacting Hamiltonians for each qubit. """ return self.ctrls[2 * self.num_qubits : 3 * self.num_qubits - 1] @property def sxsy_u(self): """ array-like: Pulse coefficients for tensor(sigmax, sigmay) interacting Hamiltonians. """ return self.coeffs[2 * self.num_qubits : 3 * self.num_qubits - 1]
[docs] def load_circuit(self, qc, schedule_mode="ASAP", compiler=None): return super(LinearSpinChain, self).load_circuit( qc, "linear", schedule_mode=schedule_mode, compiler=compiler )
[docs] def topology_map(self, qc): return to_chain_structure(qc, "linear")
[docs]class CircularSpinChain(SpinChain): """ Spin chain model with circular topology. See :class:`.SpinChain` for details. For the control Hamiltonian please refer to :obj:`SpinChainModel`. Parameters ---------- num_qubits : int The number of qubits in the system. correct_global_phase : float, optional Save the global phase, the analytical solution will track the global phase. It has no effect on the numerical solution. **params: Hardware parameters. See :obj:`.SpinChainModel`. Examples -------- .. testcode:: import numpy as np import qutip from qutip_qip.circuit import QubitCircuit from qutip_qip.device import CircularSpinChain qc = QubitCircuit(2) qc.add_gate("RX", 0, arg_value=np.pi) qc.add_gate("RY", 1, arg_value=np.pi) qc.add_gate("ISWAP", [1, 0]) processor = CircularSpinChain(2, g=0.1, t1=300) processor.load_circuit(qc) init_state = qutip.basis([2, 2], [0, 0]) result = processor.run_state(init_state) print(round(qutip.fidelity(result.states[-1], qc.run(init_state)), 4)) .. testoutput:: 0.994 """ def __init__( self, num_qubits=None, correct_global_phase=True, **params, ): if num_qubits <= 1: raise ValueError( "Circuit spin chain must have at least 2 qubits. " "The number of qubits is increased to 2." ) model = SpinChainModel( num_qubits=num_qubits, setup="circular", **params ) super(CircularSpinChain, self).__init__( num_qubits, correct_global_phase=correct_global_phase, model=model, ) @property def sxsy_ops(self): """ list: A list of tensor(sigmax, sigmay) interacting Hamiltonians for each qubit. """ return self.ctrls[2 * self.num_qubits : 3 * self.num_qubits] @property def sxsy_u(self): """ array-like: Pulse coefficients for tensor(sigmax, sigmay) interacting Hamiltonians. """ return self.coeffs[2 * self.num_qubits : 3 * self.num_qubits]
[docs] def load_circuit(self, qc, schedule_mode="ASAP", compiler=None): return super(CircularSpinChain, self).load_circuit( qc, "circular", schedule_mode=schedule_mode, compiler=compiler )
[docs] def topology_map(self, qc): return to_chain_structure(qc, "circular")
[docs]class SpinChainModel(Model): """ The physical model for the spin chian processor (:obj:`CircularSpinChain` and :obj:`LinearSpinChain`). The interaction is only possible between adjacent qubits. The single-qubit control Hamiltonians are :math:`\\sigma_j^x`$`, :math:`\\sigma_j^z`, while the interaction is realized by the exchange Hamiltonian :math:`\\sigma^x_{j}\\sigma^x_{j+1}+\\sigma^y_{j}\\sigma^y_{j+1}`. The overall Hamiltonian model is written as: .. math:: H= \\sum_{j=0}^{N-1} \\Omega^x_{j}(t) \\sigma^x_{j} + \\Omega^z_{j}(t) \\sigma^z_{j} + \\sum_{j=0}^{N-2} g_{j}(t) (\\sigma^x_{j}\\sigma^x_{j+1}+ \\sigma^y_{j}\\sigma^y_{j+1}). Parameters ---------- num_qubits: int The number of qubits, :math:`N`. setup : str "linear" for an open end and "circular" for a closed end chain. **params : Keyword arguments for hardware parameters, in the unit of frequency (MHz, GHz etc, the unit of time list needs to be adjusted accordingly). Parameters can either be a float or list with parameters for each qubits. - sx : float or list, optional The pulse strength of sigma-x control, :math:`\\Omega^x`, default ``0.25``. - sz : float or list, optional The pulse strength of sigma-z control, :math:`\\Omega^z`, default ``1.0``. - sxsy : float or list, optional The pulse strength for the exchange interaction, :math:`g`, default ``0.1``. It should be either a float or an array of the length :math:`N-1` for the linear setup or :math:`N` for the circular setup. - t1 : float or list, optional Characterize the amplitude damping for each qubit. - t2 : list of list, optional Characterize the total dephasing for each qubit. """ def __init__(self, num_qubits, setup, **params): self.num_qubits = num_qubits self.dims = num_qubits * [2] self.setup = setup self.params = { # default parameters, in the unit of frequency "sx": 0.25, "sz": 1.0, "sxsy": 0.1, } self._drift = [] self.params.update(deepcopy(params)) self.params.update(self._compute_params()) self._controls = self._set_up_controls(self.num_qubits) self._noise = [] @property def _old_index_label_map(self): num_qubits = self.num_qubits return ( ["sx" + str(i) for i in range(num_qubits)] + ["sz" + str(i) for i in range(num_qubits)] + ["g" + str(i) for i in range(num_qubits)] ) def _get_num_coupling(self): if self.setup == "linear": num_coupling = self.num_qubits - 1 elif self.setup == "circular": num_coupling = self.num_qubits else: raise ValueError( "Parameter setup needs to be linear or circular, " f"not {self.params['setup']}" ) return num_coupling def _set_up_controls(self, num_qubits): """ Generate the Hamiltonians for the spinchain model and save them in the attribute `ctrls`. Parameters ---------- num_qubits: int The number of qubits in the system. """ controls = {} # sx_controls for m in range(num_qubits): controls["sx" + str(m)] = (2 * np.pi * sigmax(), m) # sz_controls for m in range(num_qubits): controls["sz" + str(m)] = (2 * np.pi * sigmaz(), m) # sxsy_controls num_coupling = self._get_num_coupling() if num_coupling == 0: return controls operator = tensor([sigmax(), sigmax()]) + tensor([sigmay(), sigmay()]) for n in range(num_coupling): controls["g" + str(n)] = ( 2 * np.pi * operator, [n, (n + 1) % num_qubits], ) return controls def _compute_params(self): num_qubits = self.num_qubits computed_params = {} computed_params["sx"] = _to_array(self.params["sx"], num_qubits) computed_params["sz"] = _to_array(self.params["sz"], num_qubits) num_coupling = self._get_num_coupling() computed_params["sxsy"] = _to_array(self.params["sxsy"], num_coupling) return computed_params
[docs] def get_control_latex(self): """ Get the labels for each Hamiltonian. It is used in the method method :meth:`.Processor.plot_pulses`. It is a 2-d nested list, in the plot, a different color will be used for each sublist. """ num_qubits = self.num_qubits num_coupling = self._get_num_coupling() return [ {f"sx{m}": r"$\sigma_x^{}$".format(m) for m in range(num_qubits)}, {f"sz{m}": r"$\sigma_z^{}$".format(m) for m in range(num_qubits)}, { f"g{m}": r"$\sigma_x^{}\sigma_x^{} +" r" \sigma_y^{}\sigma_y^{}$".format( m, (m + 1) % num_qubits, m, (m + 1) % num_qubits ) for m in range(num_coupling) }, ]