示例#1
0
class GridSpec(DispatchClient, MetaDataMixin):
    """Class for specifying a general discretized coordinate grid (arbitrary dimensions).

    Parameters
    ----------
    minmaxpts_array: ndarray
        array of with entries [minvalue, maxvalue, number of points]
    """

    min_vals = WatchedProperty('GRID_UPDATE')
    max_vals = WatchedProperty('GRID_UPDATE')
    var_count = WatchedProperty('GRID_UPDATE')
    pt_counts = WatchedProperty('GRID_UPDATE')

    def __init__(self, minmaxpts_array):
        self.min_vals = minmaxpts_array[:, 0]
        self.max_vals = minmaxpts_array[:, 1]
        self.var_count = len(self.min_vals)
        self.pt_counts = minmaxpts_array[:, 2].astype(
            np.int)  # these are used as indices; need to be whole numbers.

    def __str__(self):
        output = '    GridSpec ......'
        for param_name, param_val in sorted(self.__dict__.items()):
            output += '\n' + str(param_name) + '\t: ' + str(param_val)
        return output

    def unwrap(self):
        """Auxiliary routine that yields a tuple of the parameters specifying the grid."""
        return self.min_vals, self.max_vals, self.pt_counts, self.var_count
示例#2
0
class InteractionTerm(DispatchClient):
    """
    Class for specifying a term in the interaction Hamiltonian of a composite Hilbert space, and constructing
    the Hamiltonian in qutip.Qobj format. The expected form of the interaction term is of two possible types:
    1. V = g A B, where A, B are Hermitean operators in two specified subsystems,
    2. V = g A B + h.c., where A, B may be non-Hermitean
    
    Parameters
    ----------
    g_strength: float
        coefficient parametrizing the interaction strength
    hilbertspace: HilbertSpace
        specifies the Hilbert space components
    subsys1, subsys2: QuantumSystem
        the two subsystems involved in the interaction
    op1, op2: str or ndarray
        names of operators in the two subsystems
    add_hc: bool, optional (default=False)
        If set to True, the interaction Hamiltonian is of type 2, and the Hermitean conjugate is added.
    """
    g_strength = WatchedProperty('INTERACTIONTERM_UPDATE')
    subsys1 = WatchedProperty('INTERACTIONTERM_UPDATE')
    subsys2 = WatchedProperty('INTERACTIONTERM_UPDATE')
    op1 = WatchedProperty('INTERACTIONTERM_UPDATE')
    op2 = WatchedProperty('INTERACTIONTERM_UPDATE')

    def __init__(self,
                 g_strength,
                 subsys1,
                 op1,
                 subsys2,
                 op2,
                 add_hc=False,
                 hilbertspace=None):
        if hilbertspace:
            warnings.warn(
                "`hilbertspace` is no longer a parameter for initializing an InteractionTerm object.",
                FutureWarning)
        self.g_strength = g_strength
        self.subsys1 = subsys1
        self.op1 = op1
        self.subsys2 = subsys2
        self.op2 = op2
        self.add_hc = add_hc
示例#3
0
class Grid1d(DispatchClient, MetaDataMixin):
    """Data structure and methods for setting up discretized 1d coordinate grid, generating corresponding derivative
    matrices.

    Parameters
    ----------
    min_val: float
        minimum value of the discretized variable
    max_val: float
        maximum value of the discretized variable
    pt_count: int
        number of grid points
    """

    min_val = WatchedProperty('GRID_UPDATE')
    max_val = WatchedProperty('GRID_UPDATE')
    pt_count = WatchedProperty('GRID_UPDATE')

    def __init__(self, min_val, max_val, pt_count):
        self.min_val = min_val
        self.max_val = max_val
        self.pt_count = pt_count

    def __str__(self):
        output = '    Grid1d ......'
        for param_name, param_val in sorted(self.__dict__.items()):
            output += '\n' + str(param_name) + '\t: ' + str(param_val)
        return output

    def grid_spacing(self):
        """
        Returns
        -------
        float
            spacing between neighboring grid points
        """
        return (self.max_val - self.min_val) / self.pt_count

    def make_linspace(self):
        return np.linspace(self.min_val, self.max_val, self.pt_count)

    def first_derivative_matrix(self, prefactor=1.0, periodic=False):
        """Generate sparse matrix for first derivative of the form :math:`\\partial_{x_i}`.
        Uses :math:`f'(x) \\approx [f(x+h) - f(x-h)]/2h`.

        Parameters
        ----------
        prefactor: float or complex, optional
            prefactor of the derivative matrix (default value: 1.0)
        periodic: bool, optional
            set to True if variable is a periodic variable

        Returns
        -------
        sparse matrix in `dia` format
        """
        if isinstance(prefactor, complex):
            dtp = np.complex_
        else:
            dtp = np.float_

        delta_x = (self.max_val - self.min_val) / self.pt_count
        offdiag_element = prefactor / (2 * delta_x)

        derivative_matrix = sparse.dia_matrix((self.pt_count, self.pt_count),
                                              dtype=dtp)
        derivative_matrix.setdiag(
            offdiag_element, k=1)  # occupy first off-diagonal to the right
        derivative_matrix.setdiag(-offdiag_element, k=-1)  # and left

        if periodic:
            derivative_matrix.setdiag(-offdiag_element, k=self.pt_count - 1)
            derivative_matrix.setdiag(offdiag_element, k=-self.pt_count + 1)

        return derivative_matrix

    def second_derivative_matrix(self, prefactor=1.0, periodic=False):
        """Generate sparse matrix for second derivative of the form :math:`\\partial^2_{x_i}`.
        Uses :math:`f''(x) \\approx [f(x+h) - 2f(x) + f(x-h)]/h^2`.

        Parameters
        ----------
        prefactor: float, optional
            optional prefactor of the derivative matrix (default value = 1.0)
        periodic: bool, optional
            set to True if variable is a periodic variable (default value = False)

        Returns
        -------
        sparse matrix in `dia` format
        """
        delta_x = (self.max_val - self.min_val) / self.pt_count
        offdiag_element = prefactor / delta_x**2

        derivative_matrix = sparse.dia_matrix((self.pt_count, self.pt_count),
                                              dtype=np.float_)
        derivative_matrix.setdiag(-2.0 * offdiag_element, k=0)
        derivative_matrix.setdiag(offdiag_element, k=1)
        derivative_matrix.setdiag(offdiag_element, k=-1)

        if periodic:
            derivative_matrix.setdiag(offdiag_element, k=self.pt_count - 1)
            derivative_matrix.setdiag(offdiag_element, k=-self.pt_count + 1)

        return derivative_matrix

    @classmethod
    def create_from_dict(cls, meta_dict):
        """
        Create and initialize a new grid object from metadata dictionary
        Parameters
        ----------
        meta_dict: dict

        Returns
        -------
        Grid1d
        """
        return cls(min_val=meta_dict['min_val'],
                   max_val=meta_dict['max_val'],
                   pt_count=meta_dict['pt_count'])
示例#4
0
class FullZeroPi(QubitBaseClass):
    r"""Zero-Pi qubit [Brooks2013]_ [Dempster2014]_ including coupling to the zeta mode. The circuit is described by the
    Hamiltonian :math:`H = H_{0-\pi} + H_\text{int} + H_\zeta`, where

    .. math::

        &H_{0-\pi} = -2E_\text{CJ}\partial_\phi^2+2E_{\text{C}\Sigma}(i\partial_\theta-n_g)^2
                     +2E_{C\Sigma}dC_J\,\partial_\phi\partial_\theta\\
        &\qquad\qquad\qquad+2E_{C\Sigma}(\delta C_J/C_J)\partial_\phi\partial_\theta
                     +2\,\delta E_J \sin\theta\sin(\phi-\phi_\text{ext}/2)\\
        &H_\text{int} = 2E_{C\Sigma}dC\,\partial_\theta\partial_\zeta + E_L dE_L \phi\,\zeta\\
        &H_\zeta = \omega_\zeta a^\dagger a

    expressed in phase basis. The definition of the relevant charging energies :math:`E_\text{CJ}`,
    :math:`E_{\text{C}\Sigma}`,     Josephson energies :math:`E_\text{J}`, inductive energies :math:`E_\text{L}`,
    and relative amounts of disorder :math:`dC_\text{J}`, :math:`dE_\text{J}`, :math:`dC`, :math:`dE_\text{L}`
    follows [Groszkowski2018]_. Internally, the ``FullZeroPi`` class formulates the Hamiltonian matrix via the
    product basis of the decoupled Zero-Pi qubit (see ``ZeroPi``)  on one hand, and the zeta LC oscillator on the other
    hand.

    Parameters
    ----------
    EJ: float
        mean Josephson energy of the two junctions
    EL: float
        inductive energy of the two (super-)inductors
    ECJ: float
        charging energy associated with the two junctions
    EC: float or None
        charging energy of the large shunting capacitances; set to `None` if `ECS` is provided instead
    dEJ: float
        relative disorder in EJ, i.e., (EJ1-EJ2)/EJavg
    dEL: float
        relative disorder in EL, i.e., (EL1-EL2)/ELavg
    dCJ: float
        relative disorder of the junction capacitances, i.e., (CJ1-CJ2)/CJavg
    dC: float
        relative disorder in large capacitances, i.e., (C1-C2)/Cavg
    ng: float
        offset charge associated with theta
    zeropi_cutoff: int
        cutoff in the number of states of the disordered zero-pi qubit
    zeta_cutoff: int
        cutoff in the zeta oscillator basis (Fock state basis)
    flux: float
        magnetic flux through the circuit loop, measured in units of flux quanta (h/2e)
    grid: Grid1d object
        specifies the range and spacing of the discretization lattice
    ncut: int
        charge number cutoff for `n_theta`,  `n_theta = -ncut, ..., ncut`
    ECS: float, optional
        total charging energy including large shunting capacitances and junction capacitances; may be provided instead
        of EC
    truncated_dim: int, optional
        desired dimension of the truncated quantum system
    """

    EJ = WatchedProperty('QUANTUMSYSTEM_UPDATE', inner_object_name='_zeropi')
    EL = WatchedProperty('QUANTUMSYSTEM_UPDATE', inner_object_name='_zeropi')
    ECJ = WatchedProperty('QUANTUMSYSTEM_UPDATE', inner_object_name='_zeropi')
    EC = WatchedProperty('QUANTUMSYSTEM_UPDATE', inner_object_name='_zeropi')
    ECS = WatchedProperty('QUANTUMSYSTEM_UPDATE', inner_object_name='_zeropi')
    dEJ = WatchedProperty('QUANTUMSYSTEM_UPDATE', inner_object_name='_zeropi')
    dCJ = WatchedProperty('QUANTUMSYSTEM_UPDATE', inner_object_name='_zeropi')
    ng = WatchedProperty('QUANTUMSYSTEM_UPDATE', inner_object_name='_zeropi')
    flux = WatchedProperty('QUANTUMSYSTEM_UPDATE', inner_object_name='_zeropi')
    grid = WatchedProperty('QUANTUMSYSTEM_UPDATE', inner_object_name='_zeropi')
    ncut = WatchedProperty('QUANTUMSYSTEM_UPDATE', inner_object_name='_zeropi')
    zeropi_cutoff = WatchedProperty('QUANTUMSYSTEM_UPDATE',
                                    inner_object_name='_zeropi',
                                    attr_name='truncated_dim')
    dC = WatchedProperty('QUANTUMSYSTEM_UPDATE')
    dEL = WatchedProperty('QUANTUMSYSTEM_UPDATE')

    def __init__(self,
                 EJ,
                 EL,
                 ECJ,
                 EC,
                 dEJ,
                 dCJ,
                 dC,
                 dEL,
                 flux,
                 ng,
                 zeropi_cutoff,
                 zeta_cutoff,
                 grid,
                 ncut,
                 ECS=None,
                 truncated_dim=None):
        self._zeropi = ZeroPi(
            EJ=EJ,
            EL=EL,
            ECJ=ECJ,
            EC=EC,
            ng=ng,
            flux=flux,
            grid=grid,
            ncut=ncut,
            dEJ=dEJ,
            dCJ=dCJ,
            ECS=ECS,
            # the zeropi_cutoff defines the truncated_dim of the "base" zeropi object
            truncated_dim=zeropi_cutoff)
        self.dC = dC
        self.dEL = dEL
        self.zeta_cutoff = zeta_cutoff
        self._sys_type = 'full 0-pi'
        self.truncated_dim = truncated_dim
        self._evec_dtype = np.complex_

        CENTRAL_DISPATCH.register('GRID_UPDATE', self)

    def receive(self, event, sender, **kwargs):
        if sender is self._zeropi.grid:
            self.broadcast('QUANTUMSYSTEM_UPDATE')

    def __str__(self):
        output_str = super().__str__() + '\n\n'
        output_str += 'INTERNAL 0-Pi object: ' + self._zeropi.__str__()
        return output_str

    def set_EC_via_ECS(self, ECS):
        """Helper function to set `EC` by providing `ECS`, keeping `ECJ` constant."""
        self._zeropi.set_EC_via_ECS(ECS)

    @property
    def E_zeta(self):
        """Returns energy quantum of the zeta mode"""
        return (8.0 * self.EL * self.EC)**0.5

    def hamiltonian(self, return_parts=False):
        """Returns Hamiltonian in basis obtained by discretizing phi, employing charge basis for theta, and Fock
        basis for zeta.

        Parameters
        ----------
        return_parts: bool, optional
            If set to true, `hamiltonian` returns [hamiltonian, evals, evecs, g_coupling_matrix]

        Returns
        -------
        scipy.sparse.csc_matrix or list
        """
        zeropi_dim = self.zeropi_cutoff
        zeropi_evals, zeropi_evecs = self._zeropi.eigensys(
            evals_count=zeropi_dim)
        zeropi_diag_hamiltonian = sparse.dia_matrix((zeropi_dim, zeropi_dim),
                                                    dtype=np.complex_)
        zeropi_diag_hamiltonian.setdiag(zeropi_evals)

        zeta_dim = self.zeta_cutoff
        prefactor = self.E_zeta
        zeta_diag_hamiltonian = op.number_sparse(zeta_dim, prefactor)

        hamiltonian_mat = sparse.kron(
            zeropi_diag_hamiltonian,
            sparse.identity(zeta_dim, format='dia', dtype=np.complex_))
        hamiltonian_mat += sparse.kron(
            sparse.identity(zeropi_dim, format='dia', dtype=np.complex_),
            zeta_diag_hamiltonian)

        gmat = self.g_coupling_matrix(zeropi_evecs)
        zeropi_coupling = sparse.dia_matrix((zeropi_dim, zeropi_dim),
                                            dtype=np.complex_)
        for l1 in range(zeropi_dim):
            for l2 in range(zeropi_dim):
                zeropi_coupling += gmat[l1, l2] * op.hubbard_sparse(
                    l1, l2, zeropi_dim)
        hamiltonian_mat += sparse.kron(
            zeropi_coupling,
            op.annihilation_sparse(zeta_dim) + op.creation_sparse(zeta_dim))

        if return_parts:
            return [hamiltonian_mat.tocsc(), zeropi_evals, zeropi_evecs, gmat]

        return hamiltonian_mat.tocsc()

    def d_hamiltonian_d_flux(self, zeropi_evecs=None):
        r"""Calculates a derivative of the Hamiltonian w.r.t flux, at the current value of flux,
        as stored in the object. The returned operator is in the product basis

        The flux is assumed to be given in the units of the ratio \Phi_{ext}/\Phi_0. 
        So if \frac{\partial H}{ \partial \Phi_{\rm ext}}, is needed, the expression returned 
        by this function, needs to be multiplied by 1/\Phi_0.

        Returns
        -------
        scipy.sparse.csc_matrix
            matrix representing the derivative of the Hamiltonian 
        """
        return self._zeropi_operator_in_product_basis(
            self._zeropi.d_hamiltonian_d_flux(), zeropi_evecs=zeropi_evecs)

    def _zeropi_operator_in_product_basis(self,
                                          zeropi_operator,
                                          zeropi_evecs=None):
        """Helper method that converts a zeropi operator into one in the product basis.

        Returns
        -------
        scipy.sparse.csc_matrix
            operator written in the product basis
        """
        zeropi_dim = self.zeropi_cutoff
        zeta_dim = self.zeta_cutoff

        if zeropi_evecs is None:
            _, zeropi_evecs = self._zeropi.eigensys(evals_count=zeropi_dim)

        op_eigen_basis = sparse.dia_matrix(
            (zeropi_dim, zeropi_dim),
            dtype=np.complex_)  # is this guaranteed to be zero?

        op_zeropi = get_matrixelement_table(zeropi_operator, zeropi_evecs)
        for n in range(zeropi_dim):
            for m in range(zeropi_dim):
                op_eigen_basis += op_zeropi[n, m] * op.hubbard_sparse(
                    n, m, zeropi_dim)

        return sparse.kron(op_eigen_basis,
                           sparse.identity(zeta_dim,
                                           format='csc',
                                           dtype=np.complex_),
                           format='csc')

    def i_d_dphi_operator(self, zeropi_evecs=None):
        r"""
        Operator :math:`i d/d\varphi`.

        Returns
        -------
            scipy.sparse.csc_matrix
        """
        return self._zeropi_operator_in_product_basis(
            self._zeropi.i_d_dphi_operator(), zeropi_evecs=zeropi_evecs)

    def n_theta_operator(self, zeropi_evecs=None):
        r"""
        Operator :math:`n_\theta`.

        Returns
        -------
        scipy.sparse.csc_matrix
        """
        return self._zeropi_operator_in_product_basis(
            self._zeropi.n_theta_operator(), zeropi_evecs=zeropi_evecs)

    def phi_operator(self, zeropi_evecs=None):
        r"""
        Operator :math:`\varphi`.

        Returns
        -------
            scipy.sparse.csc_matrix
        """
        return self._zeropi_operator_in_product_basis(
            self._zeropi.phi_operator(), zeropi_evecs=zeropi_evecs)

    def hilbertdim(self):
        """Returns Hilbert space dimension"""
        return self.zeropi_cutoff * self.zeta_cutoff

    def _evals_calc(self, evals_count, hamiltonian_mat=None):
        if hamiltonian_mat is None:
            hamiltonian_mat = self.hamiltonian()
        evals = sparse.linalg.eigsh(hamiltonian_mat,
                                    k=evals_count,
                                    return_eigenvectors=False,
                                    which='SA')
        return np.sort(evals)

    def _esys_calc(self, evals_count, hamiltonian_mat=None):
        if hamiltonian_mat is None:
            hamiltonian_mat = self.hamiltonian()
        evals, evecs = sparse.linalg.eigsh(hamiltonian_mat,
                                           k=evals_count,
                                           return_eigenvectors=True,
                                           which='SA')
        evals, evecs = order_eigensystem(evals, evecs)
        return evals, evecs

    def g_phi_coupling_matrix(self, zeropi_states):
        """Returns a matrix of coupling strengths g^\\phi_{ll'} [cmp. Dempster et al., Eq. (18)], using the states
        from the list `zeropi_states`. Most commonly, `zeropi_states` will contain eigenvectors of the
        `DisorderedZeroPi` type.
        """
        # prefactor = self.EL * self.dEL * (8.0 * self.EC / self.EL)**0.25
        prefactor = self.EL * (self.dEL / 2.0) * (8.0 * self.EC /
                                                  self.EL)**0.25
        return prefactor * get_matrixelement_table(self._zeropi.phi_operator(),
                                                   zeropi_states)

    def g_theta_coupling_matrix(self, zeropi_states):
        """Returns a matrix of coupling strengths i*g^\\theta_{ll'} [cmp. Dempster et al., Eq. (17)], using the states
        from the list 'zeropi_states'.
        """
        prefactor = 1j * self.ECS * (self.dC / 2.0) * (32.0 * self.EL /
                                                       self.EC)**0.25
        return prefactor * get_matrixelement_table(
            self._zeropi.n_theta_operator(), zeropi_states)

    def g_coupling_matrix(self, zeropi_states=None, evals_count=None):
        """Returns a matrix of coupling strengths g_{ll'} [cmp. Dempster et al., text above Eq. (17)], using the states
        from 'zeropi_states'. If `zeropi_states==None`, then a set of `self.zeropi` eigenstates is calculated. Only in
        that case is `which` used for the eigenstate number (and hence the coupling matrix size).
        """
        if evals_count is None:
            evals_count = self._zeropi.truncated_dim
        if zeropi_states is None:
            _, zeropi_states = self._zeropi.eigensys(evals_count=evals_count)
        return self.g_phi_coupling_matrix(
            zeropi_states) + self.g_theta_coupling_matrix(zeropi_states)

    def set_params_from_dict(self, meta_dict):
        """Set object parameters by given metadata dictionary

        Parameters
        ----------
        meta_dict: dict
        """
        for param_name, param_value in meta_dict.items():
            if key_in_grid1d(param_name):
                setattr(self.grid, param_name, param_value)
            elif is_numerical(param_value):
                setattr(self, param_name, param_value)

        self._zeropi = ZeroPi(EJ=self.EJ,
                              EL=self.EL,
                              ECJ=self.ECJ,
                              EC=self.EC,
                              dEJ=self.dEJ,
                              dCJ=self.dCJ,
                              flux=self.flux,
                              ng=self.ng,
                              grid=self.grid,
                              ncut=self.ncut,
                              truncated_dim=self.zeropi_cutoff)

    @classmethod
    def create_from_dict(cls, meta_dict):
        """Set object parameters by given metadata dictionary

        Parameters
        ----------
        meta_dict: dict
        """
        filtered_dict = {}
        grid_dict = {}
        for param_name, param_value in meta_dict.items():
            if key_in_grid1d(param_name):
                grid_dict[param_name] = param_value
            elif is_numerical(param_value):
                filtered_dict[param_name] = param_value

        grid = Grid1d(**grid_dict)
        filtered_dict['grid'] = grid
        return cls(**filtered_dict)
示例#5
0
class Fluxonium(QubitBaseClass1d):
    r"""Class for the fluxonium qubit. Hamiltonian
    :math:`H_\text{fl}=-4E_\text{C}\partial_\phi^2-E_\text{J}\cos(\phi-\varphi_\text{ext}) +\frac{1}{2}E_L\phi^2`
    is represented in dense form. The employed basis is the EC-EL harmonic oscillator basis. The cosine term in the
    potential is handled via matrix exponentiation. Initialize with, for example::

        qubit = Fluxonium(EJ=1.0, EC=2.0, EL=0.3, flux=0.2, cutoff=120)

    Parameters
    ----------
    EJ: float
        Josephson energy
    EC: float
        charging energy
    EL: float
        inductive energy
    flux: float
        external magnetic flux in angular units, 2pi corresponds to one flux quantum
    cutoff: int
        number of harm. osc. basis states used in diagonalization
    truncated_dim: int, optional
        desired dimension of the truncated quantum system
    """

    EJ = WatchedProperty('QUANTUMSYSTEM_UPDATE')
    EC = WatchedProperty('QUANTUMSYSTEM_UPDATE')
    EL = WatchedProperty('QUANTUMSYSTEM_UPDATE')
    flux = WatchedProperty('QUANTUMSYSTEM_UPDATE')
    cutoff = WatchedProperty('QUANTUMSYSTEM_UPDATE')

    def __init__(self, EJ, EC, EL, flux, cutoff, truncated_dim=None):
        self.EJ = EJ
        self.EC = EC
        self.EL = EL
        self.flux = flux
        self.cutoff = cutoff
        self.truncated_dim = truncated_dim
        self._sys_type = 'fluxonium'
        self._evec_dtype = np.float_
        self._default_grid = Grid1d(-4.5 * np.pi, 4.5 * np.pi, 151)

    def phi_osc(self):
        """
        Returns
        -------
        float
            Returns oscillator length for the LC oscillator composed of the fluxonium inductance and capacitance.
        """
        return (8.0 * self.EC / self.EL)**0.25  # LC oscillator length

    def E_plasma(self):
        """
        Returns
        -------
        float
            Returns the plasma oscillation frequency.
        """
        return math.sqrt(8.0 * self.EL *
                         self.EC)  # LC plasma oscillation energy

    def phi_operator(self):
        """
        Returns
        -------
        ndarray
            Returns the phi operator in the LC harmonic oscillator basis
        """
        dimension = self.hilbertdim()
        return (op.creation(dimension) +
                op.annihilation(dimension)) * self.phi_osc() / math.sqrt(2)

    def n_operator(self):
        """
        Returns
        -------
        ndarray
            Returns the :math:`n = - i d/d\\phi` operator in the LC harmonic oscillator basis
        """
        dimension = self.hilbertdim()
        return 1j * (op.creation(dimension) - op.annihilation(dimension)) / (
            self.phi_osc() * math.sqrt(2))

    def exp_i_phi_operator(self):
        """
        Returns
        -------
        ndarray
            Returns the :math:`e^{i\\phi}` operator in the LC harmonic oscillator basis
        """
        exponent = 1j * self.phi_operator()
        return sp.linalg.expm(exponent)

    def cos_phi_operator(self):
        """
        Returns
        -------
        ndarray
            Returns the :math:`\\cos \\phi` operator in the LC harmonic oscillator basis
        """
        cos_phi_op = 0.5 * self.exp_i_phi_operator()
        cos_phi_op += cos_phi_op.conjugate().T
        return cos_phi_op

    def sin_phi_operator(self):
        """
        Returns
        -------
        ndarray
            Returns the :math:`\\sin \\phi` operator in the LC harmonic oscillator basis
        """
        sin_phi_op = -1j * 0.5 * self.exp_i_phi_operator()
        sin_phi_op += sin_phi_op.conjugate().T
        return sin_phi_op

    def hamiltonian(self):  # follow Zhu et al., PRB 87, 024510 (2013)
        """Construct Hamiltonian matrix in harmonic-oscillator basis, following Zhu et al., PRB 87, 024510 (2013)

        Returns
        -------
        ndarray
        """
        dimension = self.hilbertdim()
        diag_elements = [i * self.E_plasma() for i in range(dimension)]
        lc_osc_matrix = np.diag(diag_elements)

        exp_matrix = self.exp_i_phi_operator() * cmath.exp(
            1j * 2 * np.pi * self.flux)
        cos_matrix = 0.5 * (exp_matrix + exp_matrix.conjugate().T)

        hamiltonian_mat = lc_osc_matrix - self.EJ * cos_matrix
        return np.real(
            hamiltonian_mat
        )  # use np.real to remove rounding errors from matrix exponential,
        # fluxonium Hamiltonian in harm. osc. basis is real-valued

    def hilbertdim(self):
        """
        Returns
        -------
        int
            Returns the Hilbert space dimension."""
        return self.cutoff

    def potential(self, phi):
        """Fluxonium potential evaluated at `phi`.

        Parameters
        ----------
        phi: float or ndarray
            float value of the phase variable `phi`

        Returns
        -------
        float or ndarray
        """
        return 0.5 * self.EL * phi * phi - self.EJ * np.cos(phi + 2.0 * np.pi *
                                                            self.flux)

    def wavefunction(self, esys, which=0, phi_grid=None):
        """Returns a fluxonium wave function in `phi` basis

        Parameters
        ----------
        esys: ndarray, ndarray
            eigenvalues, eigenvectors
        which: int, optional
             index of desired wave function (default value = 0)
        phi_grid: Grid1d, optional
            used for setting a custom grid for phi; if None use self._default_grid

        Returns
        -------
        WaveFunction object
        """
        if esys is None:
            evals_count = max(which + 1, 3)
            evals, evecs = self.eigensys(evals_count)
        else:
            evals, evecs = esys
        dim = self.hilbertdim()

        phi_grid = phi_grid or self._default_grid

        phi_basis_labels = phi_grid.make_linspace()
        wavefunc_osc_basis_amplitudes = evecs[:, which]
        phi_wavefunc_amplitudes = np.zeros(phi_grid.pt_count,
                                           dtype=np.complex_)
        phi_osc = self.phi_osc()
        for n in range(dim):
            phi_wavefunc_amplitudes += wavefunc_osc_basis_amplitudes[
                n] * harm_osc_wavefunction(n, phi_basis_labels, phi_osc)
        return WaveFunction(basis_labels=phi_basis_labels,
                            amplitudes=phi_wavefunc_amplitudes,
                            energy=evals[which])

    def wavefunction1d_defaults(self, mode, evals, wavefunc_count):
        """Plot defaults for plotting.wavefunction1d.

        Parameters
        ----------
        mode: str
            amplitude modifier, needed to give the correct default y label
        evals: ndarray
            eigenvalues to include in plot
        wavefunc_count: int
            number of wave functions to be plotted
        """
        ylabel = r'$\psi_j(\varphi)$'
        ylabel = MODE_STR_DICT[mode](ylabel)
        options = {'xlabel': r'$\varphi$', 'ylabel': ylabel}
        if wavefunc_count > 1:
            ymin = -1.025 * self.EJ
            ymax = max(1.8 * self.EJ, evals[-1] + 0.1 * (evals[-1] - evals[0]))
            options['ylim'] = (ymin, ymax)
        return options
示例#6
0
class ZeroPi(QubitBaseClass):
    r"""Zero-Pi Qubit

    | [1] Brooks et al., Physical Review A, 87(5), 052306 (2013). http://doi.org/10.1103/PhysRevA.87.052306
    | [2] Dempster et al., Phys. Rev. B, 90, 094518 (2014). http://doi.org/10.1103/PhysRevB.90.094518
    | [3] Groszkowski et al., New J. Phys. 20, 043053 (2018). https://doi.org/10.1088/1367-2630/aab7cd

    Zero-Pi qubit without coupling to the `zeta` mode, i.e., no disorder in `EC` and `EL`,
    see Eq. (4) in Groszkowski et al., New J. Phys. 20, 043053 (2018),

    .. math::

        H &= -2E_\text{CJ}\partial_\phi^2+2E_{\text{C}\Sigma}(i\partial_\theta-n_g)^2
               +2E_{C\Sigma}dC_J\,\partial_\phi\partial_\theta
               -2E_\text{J}\cos\theta\cos(\phi-\varphi_\text{ext}/2)+E_L\phi^2\\
          &\qquad +2E_\text{J} + E_J dE_J \sin\theta\sin(\phi-\phi_\text{ext}/2).

    Formulation of the Hamiltonian matrix proceeds by discretization of the `phi` variable, and using charge basis for
    the `theta` variable.

    Parameters
    ----------
    EJ: float
        mean Josephson energy of the two junctions
    EL: float
        inductive energy of the two (super-)inductors
    ECJ: float
        charging energy associated with the two junctions
    EC: float or None
        charging energy of the large shunting capacitances; set to `None` if `ECS` is provided instead
    dEJ: float
        relative disorder in EJ, i.e., (EJ1-EJ2)/EJavg
    dCJ: float
        relative disorder of the junction capacitances, i.e., (CJ1-CJ2)/CJavg
    ng: float
        offset charge associated with theta
    flux: float
        magnetic flux through the circuit loop, measured in units of flux quanta (h/2e)
    grid: Grid1d object
        specifies the range and spacing of the discretization lattice
    ncut: int
        charge number cutoff for `n_theta`,  `n_theta = -ncut, ..., ncut`
    ECS: float, optional
        total charging energy including large shunting capacitances and junction capacitances; may be provided instead
        of EC
    truncated_dim: int, optional
        desired dimension of the truncated quantum system
   """
    EJ = WatchedProperty('QUANTUMSYSTEM_UPDATE')
    EL = WatchedProperty('QUANTUMSYSTEM_UPDATE')
    ECJ = WatchedProperty('QUANTUMSYSTEM_UPDATE')
    EC = WatchedProperty('QUANTUMSYSTEM_UPDATE')
    dEJ = WatchedProperty('QUANTUMSYSTEM_UPDATE')
    dCJ = WatchedProperty('QUANTUMSYSTEM_UPDATE')
    ng = WatchedProperty('QUANTUMSYSTEM_UPDATE')
    ncut = WatchedProperty('QUANTUMSYSTEM_UPDATE')

    def __init__(self,
                 EJ,
                 EL,
                 ECJ,
                 EC,
                 ng,
                 flux,
                 grid,
                 ncut,
                 dEJ=0,
                 dCJ=0,
                 ECS=None,
                 truncated_dim=None):
        self.EJ = EJ
        self.EL = EL
        self.ECJ = ECJ

        if EC is None and ECS is None:
            raise ValueError("Argument missing: must either provide EC or ECS")
        if EC and ECS:
            raise ValueError(
                "Argument error: can only provide either EC or ECS")
        if EC:
            self.EC = EC
        else:
            self.EC = 1 / (1 / ECS - 1 / self.ECJ)

        self.dEJ = dEJ
        self.dCJ = dCJ
        self.ng = ng
        self.flux = flux
        self.grid = grid
        self.ncut = ncut
        self.truncated_dim = truncated_dim
        self._sys_type = '0-pi'
        self._evec_dtype = np.complex_
        self._default_grid = Grid1d(
            -np.pi / 2, 3 * np.pi / 2,
            100)  # for theta, needed for plotting wavefunction

        CENTRAL_DISPATCH.register('GRID_UPDATE', self)

    def receive(self, event, sender, **kwargs):
        if sender is self.grid:
            self.broadcast('QUANTUMSYSTEM_UPDATE')

    def _evals_calc(self, evals_count):
        hamiltonian_mat = self.hamiltonian()
        evals = sparse.linalg.eigsh(hamiltonian_mat,
                                    k=evals_count,
                                    return_eigenvectors=False,
                                    which='SA')
        return np.sort(evals)

    def _esys_calc(self, evals_count):
        hamiltonian_mat = self.hamiltonian()
        evals, evecs = sparse.linalg.eigsh(hamiltonian_mat,
                                           k=evals_count,
                                           return_eigenvectors=True,
                                           which='SA')
        # TODO consider normalization of zeropi wavefunctions
        # evecs /= np.sqrt(self.grid.grid_spacing())
        evals, evecs = order_eigensystem(evals, evecs)
        return evals, evecs

    def get_ECS(self):
        return 1 / (1 / self.EC + 1 / self.ECJ)

    def set_ECS(self, value):
        warnings.warn(
            "It is not possible to directly set ECS (except in initialization). Instead, set EC or ECJ, "
            "or use set_EC_via_ECS() to update EC indirectly.", Warning)

    ECS = property(get_ECS, set_ECS)

    def set_EC_via_ECS(self, ECS):
        """Helper function to set `EC` by providing `ECS`, keeping `ECJ` constant."""
        self.EC = 1 / (1 / ECS - 1 / self.ECJ)

    def hilbertdim(self):
        """Returns Hilbert space dimension"""
        return self.grid.pt_count * (2 * self.ncut + 1)

    def potential(self, phi, theta):
        """
        Parameters
        ----------
        phi: float
        theta: float

        Returns
        -------
        float
            value of the potential energy evaluated at phi, theta
        """
        return (-2.0 * self.EJ * np.cos(theta) *
                np.cos(phi - 2.0 * np.pi * self.flux / 2.0) +
                self.EL * phi**2 + 2.0 * self.EJ + self.EJ * self.dEJ *
                np.sin(theta) * np.sin(phi - 2.0 * np.pi * self.flux / 2.0))

    def sparse_kinetic_mat(self):
        """
        Kinetic energy portion of the Hamiltonian.
        TODO: update this method to use single-variable operator methods

        Returns
        -------
        scipy.sparse.csc_matrix
            matrix representing the kinetic energy operator
        """
        pt_count = self.grid.pt_count
        dim_theta = 2 * self.ncut + 1
        identity_phi = sparse.identity(pt_count,
                                       format='csc',
                                       dtype=np.complex_)
        identity_theta = sparse.identity(dim_theta,
                                         format='csc',
                                         dtype=np.complex_)

        kinetic_matrix_phi = self.grid.second_derivative_matrix(
            prefactor=-2.0 * self.ECJ)

        diag_elements = 2.0 * self.ECS * np.square(
            np.arange(-self.ncut + self.ng, self.ncut + 1 + self.ng))
        kinetic_matrix_theta = sparse.dia_matrix(
            (diag_elements, [0]), shape=(dim_theta, dim_theta)).tocsc()

        kinetic_matrix = (
            sparse.kron(kinetic_matrix_phi, identity_theta, format='csc') +
            sparse.kron(identity_phi, kinetic_matrix_theta, format='csc'))

        kinetic_matrix -= 2.0 * self.ECS * self.dCJ * self.i_d_dphi_operator(
        ) * self.n_theta_operator()
        return kinetic_matrix

    def sparse_potential_mat(self):
        """
        Potential energy portion of the Hamiltonian.
        TODO: update this method to use single-variable operator methods

        Returns
        -------
        scipy.sparse.csc_matrix
            matrix representing the potential energy operator
        """
        pt_count = self.grid.pt_count
        grid_linspace = self.grid.make_linspace()
        dim_theta = 2 * self.ncut + 1

        phi_inductive_vals = self.EL * np.square(grid_linspace)
        phi_inductive_potential = sparse.dia_matrix(
            (phi_inductive_vals, [0]), shape=(pt_count, pt_count)).tocsc()
        phi_cos_vals = np.cos(grid_linspace - 2.0 * np.pi * self.flux / 2.0)
        phi_cos_potential = sparse.dia_matrix(
            (phi_cos_vals, [0]), shape=(pt_count, pt_count)).tocsc()
        phi_sin_vals = np.sin(grid_linspace - 2.0 * np.pi * self.flux / 2.0)
        phi_sin_potential = sparse.dia_matrix(
            (phi_sin_vals, [0]), shape=(pt_count, pt_count)).tocsc()

        theta_cos_potential = (-self.EJ * (sparse.dia_matrix(
            ([1.0] * dim_theta, [-1]),
            shape=(dim_theta, dim_theta)) + sparse.dia_matrix(
                ([1.0] * dim_theta, [1]), shape=(dim_theta, dim_theta)))
                               ).tocsc()
        potential_mat = (
            sparse.kron(phi_cos_potential, theta_cos_potential, format='csc') +
            sparse.kron(
                phi_inductive_potential, self._identity_theta(), format='csc')
            + 2 * self.EJ * sparse.kron(
                self._identity_phi(), self._identity_theta(), format='csc'))
        potential_mat += (self.EJ * self.dEJ * sparse.kron(
            phi_sin_potential, self._identity_theta(), format='csc') *
                          self.sin_theta_operator())
        return potential_mat

    def hamiltonian(self):
        """Calculates Hamiltonian in basis obtained by discretizing phi and employing charge basis for theta.

        Returns
        -------
        scipy.sparse.csc_matrix
            matrix representing the potential energy operator
        """
        return self.sparse_kinetic_mat() + self.sparse_potential_mat()

    def sparse_d_potential_d_flux_mat(self):
        r"""Calculates a of the potential energy w.r.t flux, at the current value of flux,
        as stored in the object.

        The flux is assumed to be given in the units of the ratio \Phi_{ext}/\Phi_0.
        So if \frac{\partial U}{ \partial \Phi_{\rm ext}}, is needed, the expression returned
        by this function, needs to be multiplied by 1/\Phi_0.

        Returns
        -------
        scipy.sparse.csc_matrix
            matrix representing the derivative of the potential energy
        """
        op_1 = sparse.kron(self._sin_phi_operator(x=-2.0 * np.pi * self.flux /
                                                  2.0),
                           self._cos_theta_operator(),
                           format='csc')
        op_2 = sparse.kron(self._cos_phi_operator(x=-2.0 * np.pi * self.flux /
                                                  2.0),
                           self._sin_theta_operator(),
                           format='csc')
        return -2.0 * np.pi * self.EJ * op_1 - np.pi * self.EJ * self.dEJ * op_2

    def d_hamiltonian_d_flux(self):
        r"""Calculates a derivative of the Hamiltonian w.r.t flux, at the current value of flux,
        as stored in the object.

        The flux is assumed to be given in the units of the ratio \Phi_{ext}/\Phi_0.
        So if \frac{\partial H}{ \partial \Phi_{\rm ext}}, is needed, the expression returned
        by this function, needs to be multiplied by 1/\Phi_0.

        Returns
        -------
        scipy.sparse.csc_matrix
            matrix representing the derivative of the Hamiltonian
        """
        return self.sparse_d_potential_d_flux_mat()

    def _identity_phi(self):
        r"""
        Identity operator acting only on the `\phi` Hilbert subspace.

        Returns
        -------
            scipy.sparse.csc_matrix
        """
        pt_count = self.grid.pt_count
        return sparse.identity(pt_count, format='csc')

    def _identity_theta(self):
        r"""
        Identity operator acting only on the `\theta` Hilbert subspace.

        Returns
        -------
            scipy.sparse.csc_matrix
        """
        dim_theta = 2 * self.ncut + 1
        return sparse.identity(dim_theta, format='csc')

    def i_d_dphi_operator(self):
        r"""
        Operator :math:`i d/d\varphi`.

        Returns
        -------
            scipy.sparse.csc_matrix
        """
        return sparse.kron(self.grid.first_derivative_matrix(prefactor=1j),
                           self._identity_theta(),
                           format='csc')

    def _phi_operator(self):
        r"""
        Operator :math:`\varphi`, acting only on the `\varphi` Hilbert subspace.


        Returns
        -------
            scipy.sparse.csc_matrix
        """
        pt_count = self.grid.pt_count

        phi_matrix = sparse.dia_matrix((pt_count, pt_count), dtype=np.complex_)
        diag_elements = self.grid.make_linspace()
        phi_matrix.setdiag(diag_elements)
        return phi_matrix

    def phi_operator(self):
        r"""
        Operator :math:`\varphi`.

        Returns
        -------
            scipy.sparse.csc_matrix
        """
        return sparse.kron(self._phi_operator(),
                           self._identity_theta(),
                           format='csc')

    def n_theta_operator(self):
        r"""
        Operator :math:`n_\theta`.

        Returns
        -------
            scipy.sparse.csc_matrix
        """
        dim_theta = 2 * self.ncut + 1
        diag_elements = np.arange(-self.ncut, self.ncut + 1)
        n_theta_matrix = sparse.dia_matrix(
            (diag_elements, [0]), shape=(dim_theta, dim_theta)).tocsc()
        return sparse.kron(self._identity_phi(), n_theta_matrix, format='csc')

    def _sin_phi_operator(self, x=0):
        r"""
        Operator :math:`\sin(\phi + x)`, acting only on the `\phi` Hilbert subspace.

        Returns
        -------
            scipy.sparse.csc_matrix
        """
        pt_count = self.grid.pt_count

        vals = np.sin(self.grid.make_linspace() + x)
        sin_phi_matrix = sparse.dia_matrix((vals, [0]),
                                           shape=(pt_count, pt_count)).tocsc()
        return sin_phi_matrix

    def _cos_phi_operator(self, x=0):
        r"""
        Operator :math:`\cos(\phi + x)`, acting only on the `\phi` Hilbert subspace.

        Returns
        -------
            scipy.sparse.csc_matrix
        """
        pt_count = self.grid.pt_count

        vals = np.cos(self.grid.make_linspace() + x)
        cos_phi_matrix = sparse.dia_matrix((vals, [0]),
                                           shape=(pt_count, pt_count)).tocsc()
        return cos_phi_matrix

    def _cos_theta_operator(self):
        r"""
        Operator :math:`\cos(\theta)`, acting only on the `\theta` Hilbert subspace.

        Returns
        -------
            scipy.sparse.csc_matrix
        """
        dim_theta = 2 * self.ncut + 1
        cos_theta_matrix = 0.5 * (sparse.dia_matrix(
            ([1.0] * dim_theta, [-1]), shape=(dim_theta, dim_theta)) +
                                  sparse.dia_matrix(
                                      ([1.0] * dim_theta, [1]),
                                      shape=(dim_theta, dim_theta))).tocsc()
        return cos_theta_matrix

    def cos_theta_operator(self):
        r"""
        Operator :math:`\cos(\theta)`.

        Returns
        -------
            scipy.sparse.csc_matrix
        """
        return sparse.kron(self._identity_phi(),
                           self._cos_phi_operator(),
                           format='csc')

    def _sin_theta_operator(self):
        r"""
        Operator :math:`\sin(\theta)`, acting only on the `\theta` Hilbert space.

        Returns
        -------
            scipy.sparse.csc_matrix
        """
        dim_theta = 2 * self.ncut + 1
        sin_theta_matrix = (-0.5 * 1j * (sparse.dia_matrix(
            ([1.0] * dim_theta, [1]), shape=(dim_theta, dim_theta)
        ) - sparse.dia_matrix(
            ([1.0] * dim_theta, [-1]), shape=(dim_theta, dim_theta))).tocsc())
        return sin_theta_matrix

    def sin_theta_operator(self):
        r"""
        Operator :math:`\sin(\theta)`.

        Returns
        -------
            scipy.sparse.csc_matrix
        """
        return sparse.kron(self._identity_phi(),
                           self._sin_theta_operator(),
                           format='csc')

    def plot_potential(self, theta_grid=None, contour_vals=None, **kwargs):
        """Draw contour plot of the potential energy.

        Parameters
        ----------
        theta_grid: Grid1d, optional
            used for setting a custom grid for theta; if None use self._default_grid
        contour_vals: list, optional
        **kwargs:
            plotting parameters
        """
        theta_grid = theta_grid or self._default_grid

        x_vals = self.grid.make_linspace()
        y_vals = theta_grid.make_linspace()
        return plot.contours(x_vals,
                             y_vals,
                             self.potential,
                             contour_vals=contour_vals,
                             **kwargs)

    def wavefunction(self, esys=None, which=0, theta_grid=None):
        """Returns a zero-pi wave function in `phi`, `theta` basis

        Parameters
        ----------
        esys: ndarray, ndarray
            eigenvalues, eigenvectors
        which: int, optional
             index of desired wave function (default value = 0)
        theta_grid: Grid1d, optional
            used for setting a custom grid for theta; if None use self._default_grid

        Returns
        -------
        WaveFunctionOnGrid object
        """
        evals_count = max(which + 1, 3)
        if esys is None:
            _, evecs = self.eigensys(evals_count)
        else:
            _, evecs = esys

        theta_grid = theta_grid or self._default_grid
        dim_theta = 2 * self.ncut + 1
        state_amplitudes = evecs[:, which].reshape(self.grid.pt_count,
                                                   dim_theta)

        # Calculate psi_{phi, theta} = sum_n state_amplitudes_{phi, n} A_{n, theta}
        # where a_{n, theta} = 1/sqrt(2 pi) e^{i n theta}
        n_vec = np.arange(-self.ncut, self.ncut + 1)
        theta_vec = theta_grid.make_linspace()
        a_n_theta = np.exp(1j * np.outer(n_vec, theta_vec)) / (2 * np.pi)**0.5
        wavefunc_amplitudes = np.matmul(state_amplitudes, a_n_theta).T
        wavefunc_amplitudes = standardize_phases(wavefunc_amplitudes)

        grid2d = GridSpec(
            np.asarray(
                [[self.grid.min_val, self.grid.max_val, self.grid.pt_count],
                 [theta_grid.min_val, theta_grid.max_val,
                  theta_grid.pt_count]]))
        return WaveFunctionOnGrid(grid2d, wavefunc_amplitudes)

    def plot_wavefunction(self,
                          esys=None,
                          which=0,
                          theta_grid=None,
                          mode='abs',
                          zero_calibrate=True,
                          **kwargs):
        """Plots 2d phase-basis wave function.

        Parameters
        ----------
        esys: ndarray, ndarray
            eigenvalues, eigenvectors as obtained from `.eigensystem()`
        which: int, optional
            index of wave function to be plotted (default value = (0)
        theta_grid: Grid1d, optional
            used for setting a custom grid for theta; if None use self._default_grid
        mode: str, optional
            choices as specified in `constants.MODE_FUNC_DICT` (default value = 'abs_sqr')
        zero_calibrate: bool, optional
            if True, colors are adjusted to use zero wavefunction amplitude as the neutral color in the palette
        **kwargs:
            plot options

        Returns
        -------
        Figure, Axes
        """
        theta_grid = theta_grid or self._default_grid

        amplitude_modifier = constants.MODE_FUNC_DICT[mode]
        wavefunc = self.wavefunction(esys, theta_grid=theta_grid, which=which)
        wavefunc.amplitudes = amplitude_modifier(wavefunc.amplitudes)
        return plot.wavefunction2d(wavefunc,
                                   zero_calibrate=zero_calibrate,
                                   **kwargs)

    def set_params_from_dict(self, meta_dict):
        """Set object parameters by given metadata dictionary

        Parameters
        ----------
        meta_dict: dict
        """
        for param_name, param_value in meta_dict.items():
            if key_in_grid1d(param_name):
                setattr(self.grid, param_name, param_value)
            elif is_numerical(param_value):
                setattr(self, param_name, param_value)

    @classmethod
    def create_from_dict(cls, meta_dict):
        """Set object parameters by given metadata dictionary

        Parameters
        ----------
        meta_dict: dict
        """
        filtered_dict = {}
        grid_dict = {}
        for param_name, param_value in meta_dict.items():
            if key_in_grid1d(param_name):
                grid_dict[param_name] = param_value
            elif is_numerical(param_value):
                filtered_dict[param_name] = param_value

        grid = Grid1d(**grid_dict)
        filtered_dict['grid'] = grid
        return cls(**filtered_dict)
示例#7
0
class HilbertSpace(DispatchClient):
    """Class holding information about the full Hilbert space, usually composed of multiple subsystems.
    The class provides methods to turn subsystem operators into operators acting on the full Hilbert space, and
    establishes the interface to qutip. Returned operators are of the `qutip.Qobj` type. The class also provides methods
    for obtaining eigenvalues, absorption and emission spectra as a function of an external parameter.
    """
    osc_subsys_list = ReadOnlyProperty()
    qbt_subsys_list = ReadOnlyProperty()
    lookup = ReadOnlyProperty()
    interaction_list = WatchedProperty('INTERACTIONLIST_UPDATE')

    def __init__(self, subsystem_list, interaction_list=None):
        self._subsystems = tuple(subsystem_list)
        if interaction_list:
            self.interaction_list = tuple(interaction_list)
        else:
            self.interaction_list = None

        self._lookup = None
        self._osc_subsys_list = [(index, subsys)
                                 for (index, subsys) in enumerate(self)
                                 if isinstance(subsys, Oscillator)]
        self._qbt_subsys_list = [(index, subsys)
                                 for (index, subsys) in enumerate(self)
                                 if not isinstance(subsys, Oscillator)]

        CENTRAL_DISPATCH.register('QUANTUMSYSTEM_UPDATE', self)
        CENTRAL_DISPATCH.register('INTERACTIONTERM_UPDATE', self)
        CENTRAL_DISPATCH.register('INTERACTIONLIST_UPDATE', self)

    def __getitem__(self, index):
        return self._subsystems[index]

    def __str__(self):
        output = '====== HilbertSpace object ======\n'
        for subsystem in self:
            output += '\n' + str(subsystem) + '\n'
        return output

    def index(self, item):
        return self._subsystems.index(item)

    def _get_metadata_dict(self):
        meta_dict = {}
        for index, subsystem in enumerate(self):
            subsys_meta = subsystem._get_metadata_dict()
            renamed_subsys_meta = {}
            for key in subsys_meta.keys():
                renamed_subsys_meta[type(subsystem).__name__ + str(index) +
                                    '_' + key] = subsys_meta[key]
            meta_dict.update(renamed_subsys_meta)
        return meta_dict

    def receive(self, event, sender, **kwargs):
        if self.lookup is not None:
            if event == 'QUANTUMSYSTEM_UPDATE' and sender in self:
                self.broadcast('HILBERTSPACE_UPDATE')
                self._lookup._out_of_sync = True
                # print('Lookup table now out of sync')
            elif event == 'INTERACTIONTERM_UPDATE' and sender in self.interaction_list:
                self.broadcast('HILBERTSPACE_UPDATE')
                self._lookup._out_of_sync = True
                # print('Lookup table now out of sync')
            elif event == 'INTERACTIONLIST_UPDATE' and sender is self:
                self.broadcast('HILBERTSPACE_UPDATE')
                self._lookup._out_of_sync = True
                # print('Lookup table now out of sync')

    @property
    def subsystem_dims(self):
        """Returns list of the Hilbert space dimensions of each subsystem

        Returns
        -------
        list of int"""
        return [subsystem.truncated_dim for subsystem in self]

    @property
    def dimension(self):
        """Returns total dimension of joint Hilbert space

        Returns
        -------
        int"""
        return np.prod(np.asarray(self.subsystem_dims))

    @property
    def subsystem_count(self):
        """Returns number of subsystems composing the joint Hilbert space

        Returns
        -------
        int"""
        return len(self._subsystems)

    def generate_lookup(self):
        bare_specdata_list = []
        for index, subsys in enumerate(self):
            evals, evecs = subsys.eigensys(evals_count=subsys.truncated_dim)
            bare_specdata_list.append(
                SpectrumData(energy_table=[evals],
                             state_table=[evecs],
                             system_params=subsys.__dict__))

        evals, evecs = self.eigensys(evals_count=self.dimension)
        dressed_specdata = SpectrumData(
            energy_table=[evals],
            state_table=[evecs],
            system_params=self._get_metadata_dict())
        self._lookup = SpectrumLookup(self,
                                      bare_specdata_list=bare_specdata_list,
                                      dressed_specdata=dressed_specdata)

    def eigenvals(self, evals_count=6):
        """Calculates eigenvalues of the full Hamiltonian using `qutip.Qob.eigenenergies()`.

        Parameters
        ----------
        evals_count: int, optional
            number of desired eigenvalues/eigenstates

        Returns
        -------
        eigenvalues: ndarray of float
        """
        hamiltonian_mat = self.hamiltonian()
        return hamiltonian_mat.eigenenergies(eigvals=evals_count)

    def eigensys(self, evals_count):
        """Calculates eigenvalues and eigenvectore of the full Hamiltonian using `qutip.Qob.eigenstates()`.

        Parameters
        ----------
        evals_count: int, optional
            number of desired eigenvalues/eigenstates

        Returns
        -------
        evals: ndarray of float
        evecs: ndarray of Qobj kets
        """
        hamiltonian_mat = self.hamiltonian()
        evals, evecs = hamiltonian_mat.eigenstates(eigvals=evals_count)
        return evals, evecs

    def diag_operator(self, diag_elements, subsystem):
        """For given diagonal elements of a diagonal operator in `subsystem`, return the `Qobj` operator for the
        full Hilbert space (perform wrapping in identities for other subsystems).

        Parameters
        ----------
        diag_elements: ndarray of floats
            diagonal elements of subsystem diagonal operator
        subsystem: object derived from QuantumSystem
            subsystem where diagonal operator is defined

        Returns
        -------
        qutip.Qobj operator

        """
        dim = subsystem.truncated_dim
        index = range(dim)
        diag_matrix = np.zeros((dim, dim), dtype=np.float_)
        diag_matrix[index, index] = diag_elements
        return self.identity_wrap(diag_matrix, subsystem)

    def diag_hamiltonian(self, subsystem, evals=None):
        """Returns a `qutip.Qobj` which has the eigenenergies of the object `subsystem` on the diagonal.

        Parameters
        ----------
        subsystem: object derived from `QuantumSystem`
            Subsystem for which the Hamiltonian is to be provided.
        evals: ndarray, optional
            Eigenenergies can be provided as `evals`; otherwise, they are calculated.

        Returns
        -------
        qutip.Qobj operator
        """
        evals_count = subsystem.truncated_dim
        if evals is None:
            evals = subsystem.eigenvals(evals_count=evals_count)
        diag_qt_op = qt.Qobj(inpt=np.diagflat(evals[0:evals_count]))
        return self.identity_wrap(diag_qt_op, subsystem)

    def identity_wrap(self,
                      operator,
                      subsystem,
                      op_in_eigenbasis=False,
                      evecs=None):
        """Wrap given operator in subspace `subsystem` in identity operators to form full Hilbert-space operator.

        Parameters
        ----------
        operator: ndarray or qutip.Qobj or str
            operator acting in Hilbert space of `subsystem`; if str, then this should be an operator name in
            the subsystem, typically not in eigenbasis
        subsystem: object derived from QuantumSystem
            subsystem where diagonal operator is defined
        op_in_eigenbasis: bool
            whether `operator` is given in the `subsystem` eigenbasis; otherwise, the internal QuantumSystem basis is
            assumed
        evecs: ndarray, optional
            internal QuantumSystem eigenstates, used to convert `operator` into eigenbasis

        Returns
        -------
        qutip.Qobj operator
        """
        subsys_operator = convert_operator_to_qobj(operator, subsystem,
                                                   op_in_eigenbasis, evecs)
        operator_identitywrap_list = [
            qt.operators.qeye(the_subsys.truncated_dim) for the_subsys in self
        ]
        subsystem_index = self.get_subsys_index(subsystem)
        operator_identitywrap_list[subsystem_index] = subsys_operator
        return qt.tensor(operator_identitywrap_list)

    def hubbard_operator(self, j, k, subsystem):
        """Hubbard operator :math:`|j\\rangle\\langle k|` for system `subsystem`

        Parameters
        ----------
        j,k: int
            eigenstate indices for Hubbard operator
        subsystem: instance derived from QuantumSystem class
            subsystem in which Hubbard operator acts

        Returns
        -------
        qutip.Qobj operator
        """
        dim = subsystem.truncated_dim
        operator = (qt.states.basis(dim, j) * qt.states.basis(dim, k).dag())
        return self.identity_wrap(operator, subsystem)

    def annihilate(self, subsystem):
        """Annihilation operator a for `subsystem`

        Parameters
        ----------
        subsystem: object derived from QuantumSystem
            specifies subsystem in which annihilation operator acts

        Returns
        -------
        qutip.Qobj operator
        """
        dim = subsystem.truncated_dim
        operator = (qt.destroy(dim))
        return self.identity_wrap(operator, subsystem)

    def get_subsys_index(self, subsys):
        """
        Return the index of the given subsystem in the HilbertSpace.

        Parameters
        ----------
        subsys: QuantumSystem

        Returns
        -------
        int
        """
        return self.index(subsys)

    def bare_hamiltonian(self):
        """
        Returns
        -------
        qutip.Qobj operator
            composite Hamiltonian composed of bare Hamiltonians of subsystems independent of the external parameter
        """
        bare_hamiltonian = 0
        for subsys in self:
            evals = subsys.eigenvals(evals_count=subsys.truncated_dim)
            bare_hamiltonian += self.diag_hamiltonian(subsys, evals)
        return bare_hamiltonian

    def get_bare_hamiltonian(self):
        """Deprecated, use `bare_hamiltonian()` instead."""
        warnings.warn(
            'bare_hamiltonian() is deprecated, use bare_hamiltonian() instead',
            FutureWarning)
        return self.bare_hamiltonian()

    def hamiltonian(self):
        """

        Returns
        -------
        qutip.qobj
            Hamiltonian of the composite system, including the interaction between components
        """
        return self.bare_hamiltonian() + self.interaction_hamiltonian()

    def get_hamiltonian(self):
        """Deprecated, use `hamiltonian()` instead."""
        return self.hamiltonian()

    def interaction_hamiltonian(self):
        """
        Returns
        -------
        qutip.Qobj operator
            interaction Hamiltonian
        """
        if self.interaction_list is None:
            return 0

        hamiltonian = [
            self.interactionterm_hamiltonian(term)
            for term in self.interaction_list
        ]
        return sum(hamiltonian)

    def interactionterm_hamiltonian(self,
                                    interactionterm,
                                    evecs1=None,
                                    evecs2=None):
        interaction_op1 = self.identity_wrap(interactionterm.op1,
                                             interactionterm.subsys1,
                                             evecs=evecs1)
        interaction_op2 = self.identity_wrap(interactionterm.op2,
                                             interactionterm.subsys2,
                                             evecs=evecs2)
        hamiltonian = interactionterm.g_strength * interaction_op1 * interaction_op2
        if interactionterm.add_hc:
            return hamiltonian + hamiltonian.conj()
        return hamiltonian

    def _esys_for_paramval(self, paramval, update_hilbertspace, evals_count):
        update_hilbertspace(paramval)
        return self.eigensys(evals_count)

    def _evals_for_paramval(self, paramval, update_hilbertspace, evals_count):
        update_hilbertspace(paramval)
        return self.eigenvals(evals_count)

    def get_spectrum_vs_paramvals(self,
                                  param_vals,
                                  update_hilbertspace,
                                  evals_count=10,
                                  get_eigenstates=False,
                                  param_name="external_parameter",
                                  num_cpus=settings.NUM_CPUS):
        """Return eigenvalues (and optionally eigenstates) of the full Hamiltonian as a function of a parameter.
        Parameter values are specified as a list or array in `param_vals`. The Hamiltonian `hamiltonian_func`
        must be a function of that particular parameter, and is expected to internally set subsystem parameters.
        If a `filename` string is provided, then eigenvalue data is written to that file.

        Parameters
        ----------
        param_vals: ndarray of floats
            array of parameter values
        update_hilbertspace: function
            update_hilbertspace(param_val) specifies how a change in the external parameter affects
            the Hilbert space components
        evals_count: int, optional
            number of desired energy levels (default value = 10)
        get_eigenstates: bool, optional
            set to true if eigenstates should be returned as well (default value = False)
        param_name: str, optional
            name for the parameter that is varied in `param_vals` (default value = "external_parameter")
        num_cpus: int, optional
            number of cores to be used for computation (default value: settings.NUM_CPUS)

        Returns
        -------
        SpectrumData object
        """
        target_map = get_map_method(num_cpus)
        if get_eigenstates:
            func = functools.partial(self._esys_for_paramval,
                                     update_hilbertspace=update_hilbertspace,
                                     evals_count=evals_count)
            with InfoBar(
                    "Parallel computation of eigenvalues [num_cpus={}]".format(
                        num_cpus), num_cpus):
                eigensystem_mapdata = list(
                    target_map(
                        func,
                        tqdm(param_vals,
                             desc='Spectral data',
                             leave=False,
                             disable=(num_cpus > 1))))
            eigenvalue_table, eigenstate_table = recast_esys_mapdata(
                eigensystem_mapdata)
        else:
            func = functools.partial(self._evals_for_paramval,
                                     update_hilbertspace=update_hilbertspace,
                                     evals_count=evals_count)
            with InfoBar(
                    "Parallel computation of eigensystems [num_cpus={}]".
                    format(num_cpus), num_cpus):
                eigenvalue_table = list(
                    target_map(
                        func,
                        tqdm(param_vals,
                             desc='Spectral data',
                             leave=False,
                             disable=(num_cpus > 1))))
            eigenvalue_table = np.asarray(eigenvalue_table)
            eigenstate_table = None

        return SpectrumData(eigenvalue_table,
                            self._get_metadata_dict(),
                            param_name,
                            param_vals,
                            state_table=eigenstate_table)
示例#8
0
class FluxQubit(QubitBaseClass):
    r"""Flux Qubit

    | [1] Orlando et al., Physical Review B, 60, 15398 (1999). https://link.aps.org/doi/10.1103/PhysRevB.60.15398

    The original flux qubit as defined in [1], where the junctions are allowed to have varying junction
    energies and capacitances to allow for junction asymmetry. Typically, one takes :math:`E_{J1}=E_{J2}=E_J`, and
    :math:`E_{J3}=\alpha E_J` where :math:`0\le \alpha \le 1`. The same relations typically hold
    for the junction capacitances. The Hamiltonian is given by

    .. math::

       H_\text{flux}=&(n_{i}-n_{gi})4(E_\text{C})_{ij}(n_{j}-n_{gj}) \\
                    -&E_{J}\cos\phi_{1}-E_{J}\cos\phi_{2}-\alpha E_{J}\cos(2\pi f + \phi_{1} - \phi_{2}),

    where :math:`i,j\in\{1,2\}` is represented in the charge basis for both degrees of freedom.
    Initialize with, for example::

        EJ = 35.0
        alpha = 0.6
        flux_qubit = qubit.FluxQubit(EJ1 = EJ, EJ2 = EJ, EJ3 = alpha*EJ,
                                     ECJ1 = 1.0, ECJ2 = 1.0, ECJ3 = 1.0/alpha,
                                     ECg1 = 50.0, ECg2 = 50.0, ng1 = 0.0, ng2 = 0.0,
                                     flux = 0.5, ncut = 10)

    Parameters
    ----------
    EJ1, EJ2, EJ3: float
        Josephson energy of the ith junction
        `EJ1 = EJ2`, with `EJ3 = alpha * EJ1` and `alpha <= 1`
    ECJ1, ECJ2, ECJ3: float
        charging energy associated with the ith junction
    ECg1, ECg2: float
        charging energy associated with the capacitive coupling to ground for the two islands
    ng1, ng2: float
        offset charge associated with island i
    flux: float
        magnetic flux through the circuit loop, measured in units of the flux quantum
    ncut: int
        charge number cutoff for the charge on both islands `n`,  `n = -ncut, ..., ncut`
    truncated_dim: int, optional
        desired dimension of the truncated quantum system
    """

    EJ1 = WatchedProperty('QUANTUMSYSTEM_UPDATE')
    EJ2 = WatchedProperty('QUANTUMSYSTEM_UPDATE')
    EJ3 = WatchedProperty('QUANTUMSYSTEM_UPDATE')
    ECJ1 = WatchedProperty('QUANTUMSYSTEM_UPDATE')
    ECJ2 = WatchedProperty('QUANTUMSYSTEM_UPDATE')
    ECJ3 = WatchedProperty('QUANTUMSYSTEM_UPDATE')
    ECg1 = WatchedProperty('QUANTUMSYSTEM_UPDATE')
    ECg2 = WatchedProperty('QUANTUMSYSTEM_UPDATE')
    ng1 = WatchedProperty('QUANTUMSYSTEM_UPDATE')
    ng2 = WatchedProperty('QUANTUMSYSTEM_UPDATE')
    flux = WatchedProperty('QUANTUMSYSTEM_UPDATE')
    ncut = WatchedProperty('QUANTUMSYSTEM_UPDATE')

    def __init__(self, EJ1, EJ2, EJ3, ECJ1, ECJ2, ECJ3, ECg1, ECg2, ng1, ng2, flux, ncut,
                 truncated_dim=None):
        self.EJ1 = EJ1
        self.EJ2 = EJ2
        self.EJ3 = EJ3
        self.ECJ1 = ECJ1
        self.ECJ2 = ECJ2
        self.ECJ3 = ECJ3
        self.ECg1 = ECg1
        self.ECg2 = ECg2
        self.ng1 = ng1
        self.ng2 = ng2
        self.flux = flux
        self.ncut = ncut
        self.truncated_dim = truncated_dim
        self._sys_type = 'flux qubit'
        self._evec_dtype = np.complex_
        self._default_grid = Grid1d(-np.pi / 2, 3 * np.pi / 2, 100)    # for plotting in phi_j basis

    def EC_matrix(self):
        """Return the charging energy matrix"""
        Cmat = np.zeros((2, 2))
        CJ1 = 1. / (2 * self.ECJ1)  # capacitances in units where e is set to 1
        CJ2 = 1. / (2 * self.ECJ2)
        CJ3 = 1. / (2 * self.ECJ3)
        Cg1 = 1. / (2 * self.ECg1)
        Cg2 = 1. / (2 * self.ECg2)

        Cmat[0, 0] = CJ1 + CJ3 + Cg1
        Cmat[1, 1] = CJ2 + CJ3 + Cg2
        Cmat[0, 1] = -CJ3
        Cmat[1, 0] = -CJ3

        return np.linalg.inv(Cmat) / 2.

    def _evals_calc(self, evals_count):
        hamiltonian_mat = self.hamiltonian()
        evals = sp.linalg.eigh(hamiltonian_mat, eigvals=(0, evals_count - 1), eigvals_only=True)
        return np.sort(evals)

    def _esys_calc(self, evals_count):
        hamiltonian_mat = self.hamiltonian()
        evals, evecs = sp.linalg.eigh(hamiltonian_mat, eigvals=(0, evals_count - 1), eigvals_only=False)
        evals, evecs = order_eigensystem(evals, evecs)
        return evals, evecs

    def hilbertdim(self):
        """Return Hilbert space dimension."""
        return (2 * self.ncut + 1) ** 2

    def potential(self, phi1, phi2):
        """Return value of the potential energy at phi1 and phi2, disregarding constants."""
        return (-self.EJ1 * np.cos(phi1) - self.EJ2 * np.cos(phi2)
                - self.EJ3 * np.cos(2.0 * np.pi * self.flux + phi1 - phi2))

    def kineticmat(self):
        """Return the kinetic energy matrix."""
        ECmat = self.EC_matrix()

        kinetic_mat = 4.0 * ECmat[0, 0] * np.kron(np.matmul(self._n_operator() - self.ng1 * self._identity(),
                                                            self._n_operator() - self.ng1 * self._identity()),
                                                  self._identity())
        kinetic_mat += 4.0 * ECmat[1, 1] * np.kron(self._identity(),
                                                   np.matmul(self._n_operator() - self.ng2 * self._identity(),
                                                             self._n_operator() - self.ng2 * self._identity()))
        kinetic_mat += 4.0 * (ECmat[0, 1] + ECmat[1, 0]) * np.kron(self._n_operator() - self.ng1 * self._identity(),
                                                                   self._n_operator() - self.ng2 * self._identity())
        return kinetic_mat

    def potentialmat(self):
        """Return the potential energy matrix for the potential."""
        potential_mat = -0.5 * self.EJ1 * np.kron(self._exp_i_phi_operator() + self._exp_i_phi_operator().T,
                                                  self._identity())
        potential_mat += -0.5 * self.EJ2 * np.kron(self._identity(),
                                                   self._exp_i_phi_operator() + self._exp_i_phi_operator().T)
        potential_mat += -0.5 * self.EJ3 * (np.exp(1j * 2 * np.pi * self.flux)
                                            * np.kron(self._exp_i_phi_operator(), self._exp_i_phi_operator().T))
        potential_mat += -0.5 * self.EJ3 * (np.exp(-1j * 2 * np.pi * self.flux)
                                            * np.kron(self._exp_i_phi_operator().T, self._exp_i_phi_operator()))
        return potential_mat

    def hamiltonian(self):
        """Return Hamiltonian in basis obtained by employing charge basis for both degrees of freedom"""
        return self.kineticmat() + self.potentialmat()

    def _n_operator(self):
        diag_elements = np.arange(-self.ncut, self.ncut + 1, dtype=np.complex_)
        return np.diag(diag_elements)

    def _exp_i_phi_operator(self):
        dim = 2 * self.ncut + 1
        off_diag_elements = np.ones(dim - 1, dtype=np.complex_)
        e_iphi_matrix = np.diag(off_diag_elements, k=1)
        return e_iphi_matrix

    def _identity(self):
        dim = 2 * self.ncut + 1
        return np.eye(dim)

    def n_1_operator(self):
        r"""Return charge number operator conjugate to :math:`\phi_1`"""
        return np.kron(self._n_operator(), self._identity())

    def n_2_operator(self):
        r"""Return charge number operator conjugate to :math:`\phi_2`"""
        return np.kron(self._identity(), self._n_operator())

    def exp_i_phi_1_operator(self):
        r"""Return operator :math:`e^{i\phi_1}` in the charge basis."""
        return np.kron(self._exp_i_phi_operator(), self._identity())

    def exp_i_phi_2_operator(self):
        r"""Return operator :math:`e^{i\phi_2}` in the charge basis."""
        return np.kron(self._identity(), self._exp_i_phi_operator())

    def cos_phi_1_operator(self):
        """Return operator :math:`\\cos \\phi_1` in the charge basis"""
        cos_op = 0.5 * self.exp_i_phi_1_operator()
        cos_op += cos_op.T
        return cos_op

    def cos_phi_2_operator(self):
        """Return operator :math:`\\cos \\phi_2` in the charge basis"""
        cos_op = 0.5 * self.exp_i_phi_2_operator()
        cos_op += cos_op.T
        return cos_op

    def sin_phi_1_operator(self):
        """Return operator :math:`\\sin \\phi_1` in the charge basis"""
        sin_op = -1j * 0.5 * self.exp_i_phi_1_operator()
        sin_op += sin_op.conj().T
        return sin_op

    def sin_phi_2_operator(self):
        """Return operator :math:`\\sin \\phi_2` in the charge basis"""
        sin_op = -1j * 0.5 * self.exp_i_phi_2_operator()
        sin_op += sin_op.conj().T
        return sin_op

    def plot_potential(self, phi_grid=None, contour_vals=None, **kwargs):
        """
        Draw contour plot of the potential energy.

        Parameters
        ----------
        phi_grid: Grid1d, optional
            used for setting a custom grid for phi; if None use self._default_grid
        contour_vals: list of float, optional
            specific contours to draw
        **kwargs:
            plot options
        """
        phi_grid = phi_grid or self._default_grid
        x_vals = y_vals = phi_grid.make_linspace()
        if 'figsize' not in kwargs:
            kwargs['figsize'] = (5, 5)
        return plot.contours(x_vals, y_vals, self.potential, contour_vals=contour_vals, **kwargs)

    def wavefunction(self, esys=None, which=0, phi_grid=None):
        """
        Return a flux qubit wave function in phi1, phi2 basis

        Parameters
        ----------
        esys: ndarray, ndarray
            eigenvalues, eigenvectors
        which: int, optional
            index of desired wave function (default value = 0)
        phi_grid: Grid1d, optional
            used for setting a custom grid for phi; if None use self._default_grid

        Returns
        -------
        WaveFunctionOnGrid object
        """
        evals_count = max(which + 1, 3)
        if esys is None:
            _, evecs = self.eigensys(evals_count)
        else:
            _, evecs = esys
        phi_grid = phi_grid or self._default_grid

        dim = 2 * self.ncut + 1
        state_amplitudes = np.reshape(evecs[:, which], (dim, dim))

        n_vec = np.arange(-self.ncut, self.ncut + 1)
        phi_vec = phi_grid.make_linspace()
        a_1_phi = np.exp(1j * np.outer(phi_vec, n_vec)) / (2 * np.pi) ** 0.5
        a_2_phi = a_1_phi.T
        wavefunc_amplitudes = np.matmul(a_1_phi, state_amplitudes)
        wavefunc_amplitudes = np.matmul(wavefunc_amplitudes, a_2_phi)
        wavefunc_amplitudes = standardize_phases(wavefunc_amplitudes)

        grid2d = GridSpec(np.asarray([[phi_grid.min_val, phi_grid.max_val, phi_grid.pt_count],
                                      [phi_grid.min_val, phi_grid.max_val, phi_grid.pt_count]]))
        return WaveFunctionOnGrid(grid2d, wavefunc_amplitudes)

    def plot_wavefunction(self, esys=None, which=0, phi_grid=None, mode='abs', zero_calibrate=True, **kwargs):
        """Plots 2d phase-basis wave function.

        Parameters
        ----------
        esys: ndarray, ndarray
            eigenvalues, eigenvectors as obtained from `.eigensystem()`
        which: int, optional
            index of wave function to be plotted (default value = (0)
        phi_grid: Grid1d, optional
            used for setting a custom grid for phi; if None use self._default_grid
        mode: str, optional
            choices as specified in `constants.MODE_FUNC_DICT` (default value = 'abs_sqr')
        zero_calibrate: bool, optional
            if True, colors are adjusted to use zero wavefunction amplitude as the neutral color in the palette
        **kwargs:
            plot options

        Returns
        -------
        Figure, Axes
        """
        amplitude_modifier = constants.MODE_FUNC_DICT[mode]
        wavefunc = self.wavefunction(esys, phi_grid=phi_grid, which=which)
        wavefunc.amplitudes = amplitude_modifier(wavefunc.amplitudes)
        if 'figsize' not in kwargs:
            kwargs['figsize'] = (5, 5)
        return plot.wavefunction2d(wavefunc, zero_calibrate=zero_calibrate, **kwargs)
示例#9
0
class Transmon(QubitBaseClass1d):
    r"""Class for the Cooper-pair-box and transmon qubit. The Hamiltonian is represented in dense form in the number
    basis, :math:`H_\text{CPB}=4E_\text{C}(\hat{n}-n_g)^2+\frac{E_\text{J}}{2}(|n\rangle\langle n+1|+\text{h.c.})`.
    Initialize with, for example::

        Transmon(EJ=1.0, EC=2.0, ng=0.2, ncut=30)

    Parameters
    ----------
    EJ: float
       Josephson energy
    EC: float
        charging energy
    ng: float
        offset charge
    ncut: int
        charge basis cutoff, `n = -ncut, ..., ncut`
    truncated_dim: int, optional
        desired dimension of the truncated quantum system
    """

    EJ = WatchedProperty('QUANTUMSYSTEM_UPDATE')
    EC = WatchedProperty('QUANTUMSYSTEM_UPDATE')
    ng = WatchedProperty('QUANTUMSYSTEM_UPDATE')
    ncut = WatchedProperty('QUANTUMSYSTEM_UPDATE')

    def __init__(self, EJ, EC, ng, ncut, truncated_dim=None):
        self.EJ = EJ
        self.EC = EC
        self.ng = ng
        self.ncut = ncut
        self.truncated_dim = truncated_dim
        self._sys_type = 'transmon'
        self._evec_dtype = np.float_
        self._default_grid = Grid1d(-np.pi, np.pi, 151)
        self._default_n_range = (-5, 6)

    def n_operator(self):
        """Returns charge operator `n` in the charge basis"""
        diag_elements = np.arange(-self.ncut, self.ncut + 1, 1)
        return np.diag(diag_elements)

    def exp_i_phi_operator(self):
        """Returns operator :math:`e^{i\\varphi}` in the charge basis"""
        dimension = self.hilbertdim()
        entries = np.repeat(1.0, dimension - 1)
        exp_op = np.diag(entries, -1)
        return exp_op

    def cos_phi_operator(self):
        """Returns operator :math:`\\cos \\varphi` in the charge basis"""
        cos_op = 0.5 * self.exp_i_phi_operator()
        cos_op += cos_op.T
        return cos_op

    def sin_phi_operator(self):
        """Returns operator :math:`\\sin \\varphi` in the charge basis"""
        sin_op = -1j * 0.5 * self.exp_i_phi_operator()
        sin_op += sin_op.conjugate().T
        return sin_op

    def hamiltonian(self):
        """Returns Hamiltonian in charge basis"""
        dimension = self.hilbertdim()
        hamiltonian_mat = np.diag([
            4.0 * self.EC * (ind - self.ncut - self.ng)**2
            for ind in range(dimension)
        ])
        ind = np.arange(dimension - 1)
        hamiltonian_mat[ind, ind + 1] = -self.EJ / 2.0
        hamiltonian_mat[ind + 1, ind] = -self.EJ / 2.0
        return hamiltonian_mat

    def hilbertdim(self):
        """Returns Hilbert space dimension"""
        return 2 * self.ncut + 1

    def potential(self, phi):
        """Transmon phase-basis potential evaluated at `phi`.

        Parameters
        ----------
        phi: float
            phase variable value

        Returns
        -------
        float
        """
        return -self.EJ * np.cos(phi)

    def plot_n_wavefunction(self,
                            esys=None,
                            mode='real',
                            which=0,
                            nrange=None,
                            **kwargs):
        """Plots transmon wave function in charge basis

        Parameters
        ----------
        esys: tuple(ndarray, ndarray), optional
            eigenvalues, eigenvectors
        mode: str from MODE_FUNC_DICT, optional
            `'abs_sqr', 'abs', 'real', 'imag'`
        which: int or tuple of ints, optional
             index or indices of wave functions to plot (default value = 0)
        nrange: tuple of two ints, optional
             range of `n` to be included on the x-axis (default value = (-5,6))
        **kwargs:
            plotting parameters

        Returns
        -------
        Figure, Axes
        """
        if nrange is None:
            nrange = self._default_n_range
        n_wavefunc = self.numberbasis_wavefunction(esys, which=which)
        amplitude_modifier = constants.MODE_FUNC_DICT[mode]
        n_wavefunc.amplitudes = amplitude_modifier(n_wavefunc.amplitudes)
        kwargs = {
            **defaults.wavefunction1d_discrete(mode),
            **kwargs
        }  # if any duplicates, later ones survive
        return plot.wavefunction1d_discrete(n_wavefunc, xlim=nrange, **kwargs)

    def wavefunction1d_defaults(self, mode, evals, wavefunc_count):
        """Plot defaults for plotting.wavefunction1d.

        Parameters
        ----------
        mode: str
            amplitude modifier, needed to give the correct default y label
        evals: ndarray
            eigenvalues to include in plot
        wavefunc_count: int
        """
        ylabel = r'$\psi_j(\varphi)$'
        ylabel = MODE_STR_DICT[mode](ylabel)
        options = {'xlabel': r'$\varphi$', 'ylabel': ylabel}
        if wavefunc_count > 1:
            ymin = -1.05 * self.EJ
            ymax = max(1.1 * self.EJ,
                       evals[-1] + 0.05 * (evals[-1] - evals[0]))
            options['ylim'] = (ymin, ymax)
        return options

    def plot_phi_wavefunction(self,
                              esys=None,
                              which=0,
                              phi_grid=None,
                              mode='abs_sqr',
                              scaling=None,
                              **kwargs):
        """Alias for plot_wavefunction"""
        return self.plot_wavefunction(esys=esys,
                                      which=which,
                                      phi_grid=phi_grid,
                                      mode=mode,
                                      scaling=scaling,
                                      **kwargs)

    def numberbasis_wavefunction(self, esys=None, which=0):
        """Return the transmon wave function in number basis. The specific index of the wave function to be returned is
        `which`.

        Parameters
        ----------
        esys: ndarray, ndarray, optional
            if `None`, the eigensystem is calculated on the fly; otherwise, the provided eigenvalue, eigenvector arrays
            as obtained from `.eigensystem()`, are used (default value = None)
        which: int, optional
            eigenfunction index (default value = 0)

        Returns
        -------
        WaveFunction object
        """
        if esys is None:
            evals_count = max(which + 1, 3)
            esys = self.eigensys(evals_count)
        evals, evecs = esys

        n_vals = np.arange(-self.ncut, self.ncut + 1)
        return WaveFunction(n_vals, evecs[:, which], evals[which])

    def wavefunction(self, esys=None, which=0, phi_grid=None):
        """Return the transmon wave function in phase basis. The specific index of the wavefunction is `which`.
        `esys` can be provided, but if set to `None` then it is calculated on the fly.

        Parameters
        ----------
        esys: tuple(ndarray, ndarray), optional
            if None, the eigensystem is calculated on the fly; otherwise, the provided eigenvalue, eigenvector arrays
            as obtained from `.eigensystem()` are used
        which: int, optional
            eigenfunction index (default value = 0)
        phi_grid: Grid1d, optional
            used for setting a custom grid for phi; if None use self._default_grid

        Returns
        -------
        WaveFunction object
        """
        if esys is None:
            evals_count = max(which + 1, 3)
            esys = self.eigensys(evals_count)
        evals, _ = esys
        n_wavefunc = self.numberbasis_wavefunction(esys, which=which)

        phi_grid = phi_grid or self._default_grid

        phi_basis_labels = phi_grid.make_linspace()
        phi_wavefunc_amplitudes = np.empty(phi_grid.pt_count,
                                           dtype=np.complex_)
        for k in range(phi_grid.pt_count):
            phi_wavefunc_amplitudes[k] = (
                (1j**which / math.sqrt(2 * np.pi)) *
                np.sum(n_wavefunc.amplitudes * np.exp(
                    1j * phi_basis_labels[k] * n_wavefunc.basis_labels)))
        return WaveFunction(basis_labels=phi_basis_labels,
                            amplitudes=phi_wavefunc_amplitudes,
                            energy=evals[which])
示例#10
0
class ParameterSweep(DispatchClient):
    """
    The ParameterSweep class helps generate spectral and associated data for a composite quantum system, as an externa,
    parameter, such as flux, is swept over some given interval of values. Upon initialization, these data are calculated
    and stored internally, so that plots can be generated efficiently. This is of particular use for interactive
    displays used in the Explorer class.

    Parameters
    ----------
    param_name: str
        name of external parameter to be varied
    param_vals: ndarray
        array of parameter values
    evals_count: int
        number of eigenvalues and eigenstates to be calculated for the composite Hilbert space
    hilbertspace: HilbertSpace
        collects all data specifying the Hilbert space of interest
    subsys_update_list: list or iterable
        list of subsystems in the Hilbert space which get modified when the external parameter changes
    update_hilbertspace: function
        update_hilbertspace(param_val) specifies how a change in the external parameter affects
        the Hilbert space components
    num_cpus: int, optional
        number of CPUS requested for computing the sweep (default value settings.NUM_CPUS)
    """

    param_name = WatchedProperty('PARAMETERSWEEP_UPDATE')
    param_vals = WatchedProperty('PARAMETERSWEEP_UPDATE')
    param_count = WatchedProperty('PARAMETERSWEEP_UPDATE')
    evals_count = WatchedProperty('PARAMETERSWEEP_UPDATE')
    subsys_update_list = WatchedProperty('PARAMETERSWEEP_UPDATE')
    update_hilbertspace = WatchedProperty('PARAMETERSWEEP_UPDATE')
    lookup = ReadOnlyProperty()

    def __init__(self, param_name, param_vals, evals_count, hilbertspace, subsys_update_list, update_hilbertspace,
                 num_cpus=settings.NUM_CPUS):
        self.param_name = param_name
        self.param_vals = param_vals
        self.param_count = len(param_vals)
        self.evals_count = evals_count
        self._hilbertspace = hilbertspace
        self.subsys_update_list = tuple(subsys_update_list)
        self.update_hilbertspace = update_hilbertspace
        self.num_cpus = num_cpus

        self._lookup = None
        self._bare_hamiltonian_constant = None

        CENTRAL_DISPATCH.register('PARAMETERSWEEP_UPDATE', self)
        CENTRAL_DISPATCH.register('HILBERTSPACE_UPDATE', self)

        # generate the spectral data sweep
        if AUTORUN_SWEEP:
            self.run()

    def run(self):
        """Top-level method for generating all parameter sweep data"""
        self.cause_dispatch()   # generate one dispatch before temporarily disabling CENTRAL_DISPATCH
        settings.DISPATCH_ENABLED = False
        bare_specdata_list = self._compute_bare_specdata_sweep()
        dressed_specdata = self._compute_dressed_specdata_sweep(bare_specdata_list)
        self._lookup = SpectrumLookup(self, dressed_specdata, bare_specdata_list)
        settings.DISPATCH_ENABLED = True

    def cause_dispatch(self):
        self.update_hilbertspace(self.param_vals[0])

    def receive(self, event, sender, **kwargs):
        """Hook to CENTRAL_DISPATCH. This method is accessed by the global CentralDispatch instance whenever an event
        occurs that ParameterSweep is registered for. In reaction to update events, the lookup table is marked as out
        of sync.

        Parameters
        ----------
        event: str
            type of event being received
        sender: object
            identity of sender announcing the event
        **kwargs
        """
        if self.lookup is not None:
            if event == 'HILBERTSPACE_UPDATE' and sender is self._hilbertspace:
                self._lookup._out_of_sync = True
                # print('Lookup table now out of sync')
            elif event == 'PARAMETERSWEEP_UPDATE' and sender is self:
                self._lookup._out_of_sync = True
                # print('Lookup table now out of sync')

    def get_subsys(self, index):
        return self._hilbertspace[index]

    def get_subsys_index(self, subsys):
        return self._hilbertspace.get_subsys_index(subsys)

    @property
    def osc_subsys_list(self):
        return self._hilbertspace.osc_subsys_list

    @property
    def qbt_subsys_list(self):
        return self._hilbertspace.qbt_subsys_list

    @property
    def subsystem_count(self):
        return self._hilbertspace.subsystem_count

    @property
    def bare_specdata_list(self):
        return self.lookup._bare_specdata_list

    @property
    def dressed_specdata(self):
        return self.lookup._dressed_specdata

    def _compute_bare_specdata_sweep(self):
        """
        Pre-calculates all bare spectral data needed for the interactive explorer display.
        """
        bare_eigendata_constant = [self._compute_bare_spectrum_constant()] * self.param_count
        target_map = get_map_method(self.num_cpus)
        with InfoBar("Parallel computation of bare eigensystem [num_cpus={}]".format(self.num_cpus), self.num_cpus):
            bare_eigendata_varying = list(
                target_map(self._compute_bare_spectrum_varying,
                           tqdm(self.param_vals, desc='Bare spectra', leave=False, disable=(self.num_cpus > 1)))
            )
        bare_specdata_list = self._recast_bare_eigendata(bare_eigendata_constant, bare_eigendata_varying)
        del bare_eigendata_constant
        del bare_eigendata_varying
        return bare_specdata_list

    def _compute_dressed_specdata_sweep(self, bare_specdata_list):
        """
        Calculates and returns all dressed spectral data.

        Returns
        -------
        SpectrumData
        """
        self._bare_hamiltonian_constant = self._compute_bare_hamiltonian_constant(bare_specdata_list)
        param_indices = range(self.param_count)
        func = functools.partial(self._compute_dressed_eigensystem, bare_specdata_list=bare_specdata_list)
        target_map = get_map_method(self.num_cpus)

        with InfoBar("Parallel computation of dressed eigensystem [num_cpus={}]".format(self.num_cpus), self.num_cpus):
            dressed_eigendata = list(target_map(func, tqdm(param_indices, desc='Dressed spectrum', leave=False,
                                                           disable=(self.num_cpus > 1))))
        dressed_specdata = self._recast_dressed_eigendata(dressed_eigendata)
        del dressed_eigendata
        return dressed_specdata

    def _recast_bare_eigendata(self, static_eigendata, bare_eigendata):
        """
        Parameters
        ----------
        static_eigendata: list of eigensystem tuples
        bare_eigendata: list of eigensystem tuples

        Returns
        -------
        list of SpectrumData
        """
        specdata_list = []
        for index, subsys in enumerate(self._hilbertspace):
            if subsys in self.subsys_update_list:
                eigendata = bare_eigendata
            else:
                eigendata = static_eigendata
            evals_count = subsys.truncated_dim
            dim = subsys.hilbertdim()
            esys_dtype = subsys._evec_dtype

            energy_table = np.empty(shape=(self.param_count, evals_count), dtype=np.float_)
            state_table = np.empty(shape=(self.param_count, dim, evals_count), dtype=esys_dtype)
            for j in range(self.param_count):
                energy_table[j] = eigendata[j][index][0]
                state_table[j] = eigendata[j][index][1]
            specdata_list.append(SpectrumData(energy_table, subsys.__dict__, self.param_name, self.param_vals,
                                              state_table))
        return specdata_list

    def _recast_dressed_eigendata(self, dressed_eigendata):
        """
        Parameters
        ----------
        dressed_eigendata: list of tuple(evals, qutip evecs)

        Returns
        -------
        SpectrumData
        """
        evals_count = self.evals_count
        energy_table = np.empty(shape=(self.param_count, evals_count), dtype=np.float_)
        state_table = []  # for dressed states, entries are Qobj
        for j in range(self.param_count):
            energy_table[j] = dressed_eigendata[j][0]
            state_table.append(dressed_eigendata[j][1])
        specdata = SpectrumData(energy_table, system_params=self._hilbertspace._get_metadata_dict(),
                                param_name=self.param_name, param_vals=self.param_vals, state_table=state_table)
        return specdata

    def _compute_bare_hamiltonian_constant(self, bare_specdata_list):
        """
        Returns
        -------
        qutip.Qobj operator
            composite Hamiltonian composed of bare Hamiltonians of subsystems independent of the external parameter
        """
        static_hamiltonian = 0
        for index, subsys in enumerate(self._hilbertspace):
            if subsys not in self.subsys_update_list:
                evals = bare_specdata_list[index].energy_table[0]
                static_hamiltonian += self._hilbertspace.diag_hamiltonian(subsys, evals)
        return static_hamiltonian

    def _compute_bare_hamiltonian_varying(self, bare_specdata_list, param_index):
        """
        Parameters
        ----------
        param_index: int
            position index of current value of the external parameter

        Returns
        -------
        qutip.Qobj operator
            composite Hamiltonian consisting of all bare Hamiltonians which depend on the external parameter
        """
        hamiltonian = 0
        for index, subsys in enumerate(self._hilbertspace):
            if subsys in self.subsys_update_list:
                evals = bare_specdata_list[index].energy_table[param_index]
                hamiltonian += self._hilbertspace.diag_hamiltonian(subsys, evals)
        return hamiltonian

    def _compute_bare_spectrum_constant(self):
        """
        Returns
        -------
        list of (ndarray, ndarray)
            eigensystem data for each subsystem that is not affected by a change of the external parameter
        """
        eigendata = []
        for subsys in self._hilbertspace:
            if subsys not in self.subsys_update_list:
                evals_count = subsys.truncated_dim
                eigendata.append(subsys.eigensys(evals_count=evals_count))
            else:
                eigendata.append(None)
        return eigendata

    def _compute_bare_spectrum_varying(self, param_val):
        """
        For given external parameter value obtain the bare eigenspectra of each bare subsystem that is affected by
        changes in the external parameter. Formulated to be used with Pool.map()

        Parameters
        ----------
        param_val: float

        Returns
        -------
        list of tuples(ndarray, ndarray)
            (evals, evecs) bare eigendata for each subsystem that is parameter-dependent
        """
        eigendata = []
        self.update_hilbertspace(param_val)
        for subsys in self._hilbertspace:
            if subsys in self.subsys_update_list:
                evals_count = subsys.truncated_dim
                subsys_index = self._hilbertspace.index(subsys)
                eigendata.append(self._hilbertspace[subsys_index].eigensys(evals_count=evals_count))
            else:
                eigendata.append(None)
        return eigendata

    def _compute_dressed_eigensystem(self, param_index, bare_specdata_list):
        hamiltonian = (self._bare_hamiltonian_constant +
                       self._compute_bare_hamiltonian_varying(bare_specdata_list, param_index))

        for interaction_term in self._hilbertspace.interaction_list:
            evecs1 = self._lookup_bare_eigenstates(param_index, interaction_term.subsys1, bare_specdata_list)
            evecs2 = self._lookup_bare_eigenstates(param_index, interaction_term.subsys2, bare_specdata_list)
            hamiltonian += self._hilbertspace.interactionterm_hamiltonian(interaction_term,
                                                                          evecs1=evecs1, evecs2=evecs2)
        return hamiltonian.eigenstates(eigvals=self.evals_count)

    def _lookup_bare_eigenstates(self, param_index, subsys, bare_specdata_list):
        """
        Parameters
        ----------
        self: ParameterSweep or HilbertSpace
        param_index: int
            position index of parameter value in question
        subsys: QuantumSystem
            Hilbert space subsystem for which bare eigendata is to be looked up
        bare_specdata_list: list of SpectrumData
            may be provided during partial generation of the lookup

        Returns
        -------
        ndarray
            bare eigenvectors for the specified subsystem and the external parameter fixed to the value indicated by
            its index
        """
        subsys_index = self.get_subsys_index(subsys)
        return bare_specdata_list[subsys_index].state_table[param_index]

    @property
    def system_params(self):
        return self._hilbertspace.__dict__

    def new_datastore(self, **kwargs):
        """Return DataStore object with system/sweep information obtained from self."""
        return DataStore(self.system_params, self.param_name, self.param_vals, **kwargs)