Exemplo n.º 1
0
class GridSpec(dispatch.DispatchClient, serializers.Serializable):
    """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 = descriptors.WatchedProperty('GRID_UPDATE')
    max_vals = descriptors.WatchedProperty('GRID_UPDATE')
    var_count = descriptors.WatchedProperty('GRID_UPDATE')
    pt_counts = descriptors.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
Exemplo n.º 2
0
class StoredSweep(ParameterSweepBase, dispatch.DispatchClient,
                  serializers.Serializable):
    param_name = descriptors.WatchedProperty('PARAMETERSWEEP_UPDATE')
    param_vals = descriptors.WatchedProperty('PARAMETERSWEEP_UPDATE')
    param_count = descriptors.WatchedProperty('PARAMETERSWEEP_UPDATE')
    evals_count = descriptors.WatchedProperty('PARAMETERSWEEP_UPDATE')
    lookup = descriptors.ReadOnlyProperty()

    def __init__(self, param_name: str, param_vals: ndarray, evals_count: int,
                 hilbertspace: HilbertSpace, dressed_specdata: SpectrumData,
                 bare_specdata_list: List[SpectrumData]) -> None:
        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._lookup = spec_lookup.SpectrumLookup(hilbertspace,
                                                  dressed_specdata,
                                                  bare_specdata_list,
                                                  auto_run=False)

    # StoredSweep: file IO methods ---------------------------------------------------------------
    @classmethod
    def deserialize(cls, iodata: 'IOData') -> 'StoredSweep':
        """
        Take the given IOData and return an instance of the described class, initialized with the data stored in
        io_data.

        Parameters
        ----------
        iodata: IOData

        Returns
        -------
        StoredSweep
        """
        data_dict = iodata.as_kwargs()
        lookup = data_dict.pop('_lookup')
        data_dict['dressed_specdata'] = lookup._dressed_specdata
        data_dict['bare_specdata_list'] = lookup._bare_specdata_list
        new_storedsweep = StoredSweep(**data_dict)
        new_storedsweep._lookup = lookup
        new_storedsweep._lookup._hilbertspace = weakref.proxy(
            new_storedsweep._hilbertspace)
        return new_storedsweep

    # StoredSweep: other methods
    def get_hilbertspace(self) -> HilbertSpace:
        return self._hilbertspace

    def new_sweep(self,
                  subsys_update_list: List[QuantumSys],
                  update_hilbertspace: Callable,
                  num_cpus: int = settings.NUM_CPUS) -> ParameterSweep:
        return ParameterSweep(self.param_name, self.param_vals,
                              self.evals_count, self._hilbertspace,
                              subsys_update_list, update_hilbertspace,
                              num_cpus)
Exemplo n.º 3
0
class InteractionTerm(dispatch.DispatchClient, serializers.Serializable):
    """
    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 subsys_list,
    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 subsys_list involved in the interaction
    op1, op2: str or ndarray
        names of operators in the two subsys_list
    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 = descriptors.WatchedProperty('INTERACTIONTERM_UPDATE')
    subsys1 = descriptors.WatchedProperty('INTERACTIONTERM_UPDATE')
    subsys2 = descriptors.WatchedProperty('INTERACTIONTERM_UPDATE')
    op1 = descriptors.WatchedProperty('INTERACTIONTERM_UPDATE')
    op2 = descriptors.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

        self._init_params.remove('hilbertspace')

    def __repr__(self):
        init_dict = {name: getattr(self, name) for name in self._init_params}
        return type(self).__name__ + f'(**{init_dict!r})'

    def __str__(self):
        output = type(self).__name__.upper() + '\n ———— PARAMETERS ————'
        for param_name in self._init_params:
            output += '\n' + str(param_name) + '\t: ' + str(
                getattr(self, param_name))
        return output + '\n'
Exemplo n.º 4
0
class KerrOscillator(Oscillator, serializers.Serializable):
    r"""Class representing a nonlinear Kerr oscillator/resonator governed by a Hamiltonian
    :math:`H_\text{Kerr}=E_\text{osc} a^{\dagger} a - K a^{\dagger} a^{\dagger} a a`, with :math:`a`
    being the annihilation operator.

    Parameters
    ----------
    E_osc:
        energy of harmonic term
    K:
        energy of the Kerr term
    l_osc:
        oscillator length (used to define phi_operator and n_operator)
    truncated_dim:
        desired dimension of the truncated quantum system; expected: truncated_dim > 1
    """
    K = descriptors.WatchedProperty("QUANTUMSYSTEM_UPDATE")

    def __init__(
        self,
        E_osc: float,
        K: float,
        l_osc: Union[float, None] = None,
        truncated_dim: int = _default_evals_count,
    ) -> None:

        self.K: float = K

        Oscillator.__init__(self,
                            E_osc=E_osc,
                            omega=None,
                            l_osc=l_osc,
                            truncated_dim=truncated_dim)

        self._image_filename = os.path.join(
            os.path.dirname(os.path.abspath(__file__)),
            "qubit_img/kerr-oscillator.jpg")

    @staticmethod
    def default_params() -> Dict[str, Any]:
        return {
            "E_osc": 5.0,
            "K": 0.05,
            "l_osc": 1,
            "truncated_dim": _default_evals_count,
        }

    def eigenvals(self, evals_count: int = _default_evals_count) -> ndarray:
        """Returns array of eigenvalues.

        Parameters
        ----------
        evals_count:
            number of desired eigenvalues (default value = 6)
        """
        evals = [(self.E_osc + self.K) * n - self.K * n**2
                 for n in range(evals_count)]
        return np.asarray(evals)
Exemplo n.º 5
0
class ParameterSweepBase(ABC):
    """
    The ParameterSweepBase class is an abstract base class for ParameterSweep and StoredSweep
    """

    param_name = descriptors.WatchedProperty("PARAMETERSWEEP_UPDATE")
    param_vals = descriptors.WatchedProperty("PARAMETERSWEEP_UPDATE")
    param_count = descriptors.WatchedProperty("PARAMETERSWEEP_UPDATE")
    evals_count = descriptors.WatchedProperty("PARAMETERSWEEP_UPDATE")
    lookup = descriptors.ReadOnlyProperty()
    _hilbertspace: hspace.HilbertSpace

    def get_subsys(self, index: int) -> QuantumSys:
        return self._hilbertspace[index]

    def get_subsys_index(self, subsys: QuantumSys) -> int:
        return self._hilbertspace.get_subsys_index(subsys)

    @property
    def osc_subsys_list(self) -> List[Tuple[int, Oscillator]]:
        return self._hilbertspace.osc_subsys_list

    @property
    def qbt_subsys_list(self) -> List[Tuple[int, QubitBaseClass]]:
        return self._hilbertspace.qbt_subsys_list

    @property
    def subsystem_count(self) -> int:
        return self._hilbertspace.subsystem_count

    @property
    def bare_specdata_list(self) -> List[SpectrumData]:
        return self.lookup._bare_specdata_list

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

    def _lookup_bare_eigenstates(
        self,
        param_index: int,
        subsys: QuantumSys,
        bare_specdata_list: List[SpectrumData],
    ) -> Union[ndarray, List[QutipEigenstates]]:
        """
        Parameters
        ----------
        param_index:
            position index of parameter value in question
        subsys:
            Hilbert space subsystem for which bare eigendata is to be looked up
        bare_specdata_list:
            may be provided during partial generation of the lookup

        Returns
        -------
            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]  # type: ignore

    @property
    def system_params(self) -> Dict[str, Any]:
        return self._hilbertspace.get_initdata()

    def new_datastore(self, **kwargs) -> DataStore:
        """Return DataStore object with system/sweep information obtained from self."""
        return storage.DataStore(self.system_params, self.param_name,
                                 self.param_vals, **kwargs)
Exemplo n.º 6
0
class Fluxonium(base.QubitBaseClass1d, serializers.Serializable, NoisySystem):
    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:
        desired dimension of the truncated quantum system; expected: truncated_dim > 1
    """
    EJ = descriptors.WatchedProperty('QUANTUMSYSTEM_UPDATE')
    EC = descriptors.WatchedProperty('QUANTUMSYSTEM_UPDATE')
    EL = descriptors.WatchedProperty('QUANTUMSYSTEM_UPDATE')
    flux = descriptors.WatchedProperty('QUANTUMSYSTEM_UPDATE')
    cutoff = descriptors.WatchedProperty('QUANTUMSYSTEM_UPDATE')

    def __init__(self,
                 EJ: float,
                 EC: float,
                 EL: float,
                 flux: float,
                 cutoff: int,
                 truncated_dim: int = 6) -> None:
        self.EJ = EJ
        self.EC = EC
        self.EL = EL
        self.flux = flux
        self.cutoff = cutoff
        self.truncated_dim = truncated_dim
        self._sys_type = type(self).__name__
        self._evec_dtype = np.float_
        self._default_grid = discretization.Grid1d(-4.5 * np.pi, 4.5 * np.pi,
                                                   151)
        self._image_filename = os.path.join(
            os.path.dirname(os.path.abspath(__file__)),
            'qubit_img/fluxonium.jpg')

    @staticmethod
    def default_params() -> Dict[str, Any]:
        return {
            'EJ': 8.9,
            'EC': 2.5,
            'EL': 0.5,
            'flux': 0.0,
            'cutoff': 110,
            'truncated_dim': 10
        }

    def supported_noise_channels(self) -> List[str]:
        """Return a list of supported noise channels"""
        return [
            'tphi_1_over_f_cc', 'tphi_1_over_f_flux', 't1_capacitive',
            't1_charge_impedance', 't1_flux_bias_line', 't1_inductive',
            't1_quasiparticle_tunneling'
        ]

    def phi_osc(self) -> float:
        """
        Returns
        -------
            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) -> float:
        """
        Returns
        -------
            Returns the plasma oscillation frequency.
        """
        return math.sqrt(8.0 * self.EL *
                         self.EC)  # LC plasma oscillation energy

    def phi_operator(self) -> ndarray:
        """
        Returns
        -------
            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) -> ndarray:
        """
        Returns
        -------
            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,
                           alpha: float = 1.0,
                           beta: float = 0.0) -> ndarray:
        """
        Returns
        -------
            Returns the :math:`e^{i (\\alpha \\phi + \beta) }` operator in the LC harmonic oscillator basis,
            with :math:`\\alpha` and :math:`\\beta` being numbers
        """
        exponent = 1j * (alpha * self.phi_operator())
        return sp.linalg.expm(exponent) * cmath.exp(1j * beta)

    def cos_phi_operator(self,
                         alpha: float = 1.0,
                         beta: float = 0.0) -> ndarray:
        """
        Returns
        -------
            Returns the :math:`\\cos (\\alpha \\phi + \\beta)` operator in the LC harmonic oscillator basis,
            with :math:`\\alpha` and :math:`\\beta` being numbers
        """
        exp_matrix = self.exp_i_phi_operator(alpha, beta)
        return 0.5 * (exp_matrix + exp_matrix.conjugate().T)

    def sin_phi_operator(self,
                         alpha: float = 1.0,
                         beta: float = 0.0) -> ndarray:
        """
        Returns
        -------
            Returns the :math:`\\sin (\\alpha \\phi + \\beta)` operator in the LC harmonic oscillator basis
            with :math:`\\alpha` and :math:`\\beta` being numbers
        """
        exp_matrix = self.exp_i_phi_operator(alpha, beta)
        return -1j * 0.5 * (exp_matrix - exp_matrix.conjugate().T)

    def hamiltonian(
            self) -> ndarray:  # follow Zhu et al., PRB 87, 024510 (2013)
        """Construct Hamiltonian matrix in harmonic-oscillator basis, following Zhu et al., PRB 87, 024510 (2013)
        """
        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 d_hamiltonian_d_EJ(self) -> ndarray:
        """Returns operator representing a derivative of the Hamiltonian with respect to `EJ`.

        The flux is grouped as in the Hamiltonian. 
        """
        return -self.cos_phi_operator(1, 2 * np.pi * self.flux)

    def d_hamiltonian_d_flux(self) -> ndarray:
        """Returns operator representing a derivative of the Hamiltonian with respect to `flux`.

        Flux is grouped as in the Hamiltonian. 
        """
        return -2 * np.pi * self.EJ * self.sin_phi_operator(
            1, 2 * np.pi * self.flux)

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

    def potential(self, phi: Union[float, ndarray]) -> ndarray:
        """Fluxonium potential evaluated at `phi`.

        Parameters
        ----------
            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: Tuple[ndarray, ndarray],
                     which: int = 0,
                     phi_grid: 'Grid1d' = None) -> storage.WaveFunction:
        """Returns a fluxonium wave function in `phi` basis

        Parameters
        ----------
        esys:
            eigenvalues, eigenvectors
        which:
             index of desired wave function (default value = 0)
        phi_grid:
            used for setting a custom grid for phi; if None use self._default_grid
        """
        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] \
                                       * osc.harm_osc_wavefunction(n, phi_basis_labels, phi_osc)
        return storage.WaveFunction(basis_labels=phi_basis_labels,
                                    amplitudes=phi_wavefunc_amplitudes,
                                    energy=evals[which])

    def wavefunction1d_defaults(self, mode: str, evals: ndarray,
                                wavefunc_count: int) -> Dict[str, Any]:
        """Plot defaults for plotting.wavefunction1d.

        Parameters
        ----------
        mode:
            amplitude modifier, needed to give the correct default y label
        evals:
            eigenvalues to include in plot
        wavefunc_count:
            number of wave functions to be plotted
        """
        ylabel = r'$\psi_j(\varphi)$'
        ylabel = constants.MODE_STR_DICT[mode](ylabel)
        options = {'xlabel': r'$\varphi$', 'ylabel': ylabel}
        return options
Exemplo n.º 7
0
class ZeroPi(base.QubitBaseClass, serializers.Serializable):
    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; expected: truncated_dim > 1
   """
    EJ = descriptors.WatchedProperty('QUANTUMSYSTEM_UPDATE')
    EL = descriptors.WatchedProperty('QUANTUMSYSTEM_UPDATE')
    ECJ = descriptors.WatchedProperty('QUANTUMSYSTEM_UPDATE')
    EC = descriptors.WatchedProperty('QUANTUMSYSTEM_UPDATE')
    dEJ = descriptors.WatchedProperty('QUANTUMSYSTEM_UPDATE')
    dCJ = descriptors.WatchedProperty('QUANTUMSYSTEM_UPDATE')
    ng = descriptors.WatchedProperty('QUANTUMSYSTEM_UPDATE')
    ncut = descriptors.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 = type(self).__name__
        self._evec_dtype = np.complex_
        # for theta, needed for plotting wavefunction
        self._default_grid = discretization.Grid1d(-np.pi / 2, 3 * np.pi / 2,
                                                   100)
        self._init_params.remove(
            'ECS'
        )  # used in for file Serializable purposes; remove ECS as init parameter
        self._image_filename = os.path.join(
            os.path.dirname(os.path.abspath(__file__)),
            'qubit_pngs/zeropi.png')
        dispatch.CENTRAL_DISPATCH.register('GRID_UPDATE', self)

    @staticmethod
    def default_params():
        return {
            'EJ': 10.0,
            'EL': 0.04,
            'ECJ': 20.0,
            'EC': 0.04,
            'dEJ': 0.0,
            'dCJ': 0.0,
            'ng': 0.1,
            'flux': 0.23,
            'ncut': 30,
            'truncated_dim': 10
        }

    @staticmethod
    def nonfit_params():
        return ['ng', 'flux', 'ncut', 'truncated_dim']

    @classmethod
    def create(cls):
        phi_grid = discretization.Grid1d(-19.0, 19.0, 200)
        init_params = cls.default_params()
        zeropi = cls(**init_params, grid=phi_grid)
        zeropi.widget()
        return zeropi

    def widget(self, params=None):
        init_params = params or self.get_initdata()
        del init_params['grid']
        init_params['grid_max_val'] = self.grid.max_val
        init_params['grid_min_val'] = self.grid.min_val
        init_params['grid_pt_count'] = self.grid.pt_count
        ui.create_widget(self.set_params,
                         init_params,
                         image_filename=self._image_filename)

    def set_params(self, **kwargs):
        phi_grid = discretization.Grid1d(kwargs.pop('grid_min_val'),
                                         kwargs.pop('grid_max_val'),
                                         kwargs.pop('grid_pt_count'))
        self.grid = phi_grid
        for param_name, param_val in kwargs.items():
            setattr(self, param_name, param_val)

    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 = spec_utils.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 = spec_utils.standardize_phases(
            wavefunc_amplitudes)

        grid2d = discretization.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 storage.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)
Exemplo n.º 8
0
class Transmon(base.QubitBaseClass1d, serializers.Serializable, NoisySystem):
    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:
       Josephson energy
    EC:
        charging energy
    ng:
        offset charge
    ncut:
        charge basis cutoff, `n = -ncut, ..., ncut`
    truncated_dim:
        desired dimension of the truncated quantum system; expected: truncated_dim > 1
    """
    EJ = descriptors.WatchedProperty('QUANTUMSYSTEM_UPDATE')
    EC = descriptors.WatchedProperty('QUANTUMSYSTEM_UPDATE')
    ng = descriptors.WatchedProperty('QUANTUMSYSTEM_UPDATE')
    ncut = descriptors.WatchedProperty('QUANTUMSYSTEM_UPDATE')

    def __init__(self,
                 EJ: float,
                 EC: float,
                 ng: float,
                 ncut: int,
                 truncated_dim: int = 6) -> None:
        self.EJ = EJ
        self.EC = EC
        self.ng = ng
        self.ncut = ncut
        self.truncated_dim = truncated_dim
        self._sys_type = type(self).__name__
        self._evec_dtype = np.float_
        self._default_grid = discretization.Grid1d(-np.pi, np.pi, 151)
        self._default_n_range = (-5, 6)
        self._image_filename = os.path.join(
            os.path.dirname(os.path.abspath(__file__)),
            'qubit_img/fixed-transmon.jpg')

    @staticmethod
    def default_params() -> Dict[str, Any]:
        return {
            'EJ': 15.0,
            'EC': 0.3,
            'ng': 0.0,
            'ncut': 30,
            'truncated_dim': 10
        }

    def supported_noise_channels(self) -> List[str]:
        """Return a list of supported noise channels"""
        return [
            'tphi_1_over_f_cc', 'tphi_1_over_f_ng', 't1_capacitive',
            't1_charge_impedance'
        ]

    def effective_noise_channels(self) -> List[str]:
        """Return a default list of channels used when calculating effective t1 and t2 nosie."""
        noise_channels = self.supported_noise_channels()
        noise_channels.remove('t1_charge_impedance')
        return noise_channels

    def n_operator(self) -> ndarray:
        """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) -> ndarray:
        """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) -> ndarray:
        """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) -> ndarray:
        """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) -> ndarray:
        """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 d_hamiltonian_d_ng(self) -> ndarray:
        """Returns operator representing a derivative of the Hamiltonian with respect to charge offset `ng`."""
        return -8 * self.EC * self.n_operator()

    def d_hamiltonian_d_EJ(self) -> ndarray:
        """Returns operator representing a derivative of the Hamiltonian with respect to EJ."""
        return -self.cos_phi_operator()

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

    def potential(self, phi: Union[float, ndarray]) -> ndarray:
        """Transmon phase-basis potential evaluated at `phi`.

        Parameters
        ----------
        phi:
            phase variable value
        """
        return -self.EJ * np.cos(phi)

    def plot_n_wavefunction(self,
                            esys: Tuple[ndarray, ndarray] = None,
                            mode: str = 'real',
                            which: int = 0,
                            nrange: Tuple[int, int] = None,
                            **kwargs) -> Tuple[Figure, Axes]:
        """Plots transmon wave function in charge basis

        Parameters
        ----------
        esys:
            eigenvalues, eigenvectors
        mode:
            `'abs_sqr', 'abs', 'real', 'imag'`
        which:
             index or indices of wave functions to plot (default value = 0)
        nrange:
             range of `n` to be included on the x-axis (default value = (-5,6))
        **kwargs:
            plotting parameters
        """
        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: str, evals: ndarray,
                                wavefunc_count: int) -> Dict[str, Any]:
        """Plot defaults for plotting.wavefunction1d.

        Parameters
        ----------
        mode:
            amplitude modifier, needed to give the correct default y label
        evals:
            eigenvalues to include in plot
        wavefunc_count:
        """
        ylabel = r'$\psi_j(\varphi)$'
        ylabel = constants.MODE_STR_DICT[mode](ylabel)
        options = {'xlabel': r'$\varphi$', 'ylabel': ylabel}
        return options

    def plot_phi_wavefunction(self,
                              esys: Tuple[ndarray, ndarray] = None,
                              which: int = 0,
                              phi_grid: Grid1d = None,
                              mode: str = 'abs_sqr',
                              scaling: float = None,
                              **kwargs) -> Tuple[Figure, Axes]:
        """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: Tuple[ndarray, ndarray] = None,
                                 which: int = 0) -> WaveFunction:
        """Return the transmon wave function in number basis. The specific index of the wave function to be returned is
        `which`.

        Parameters
        ----------
        esys:
            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:
            eigenfunction index (default value = 0)
        """
        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 storage.WaveFunction(n_vals, evecs[:, which], evals[which])

    def wavefunction(self,
                     esys: Tuple[ndarray, ndarray] = None,
                     which: int = 0,
                     phi_grid: Grid1d = None) -> WaveFunction:
        """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:
            if None, the eigensystem is calculated on the fly; otherwise, the provided eigenvalue, eigenvector arrays
            as obtained from `.eigensystem()` are used
        which:
            eigenfunction index (default value = 0)
        phi_grid:
            used for setting a custom grid for phi; if None use self._default_grid
        """
        if esys is None:
            evals_count = max(which + 1, 3)
            evals, evecs = self.eigensys(evals_count)
        else:
            evals, evecs = 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 storage.WaveFunction(basis_labels=phi_basis_labels,
                                    amplitudes=phi_wavefunc_amplitudes,
                                    energy=evals[which])
Exemplo n.º 9
0
class CircuitFluxQubit(circuit.Circuit):
    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; expected: truncated_dim > 1
    """

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

    @staticmethod
    def default_params():
        return {
            'EJ1': 1.0,
            'EJ2': 1.0,
            'EJ3': 0.8,
            'ECJ1': 0.016,
            'ECJ2': 0.016,
            'ECJ3': 0.021,
            'ECg1': 0.83,
            'ECg2': 0.83,
            'ng1': 0.0,
            'ng2': 0.0,
            'flux': 0.4,
            'ncut': 10,
            'truncated_dim': 10
        }

    @staticmethod
    def nonfit_params():
        return ['ng1', 'ng2', 'flux', 'ncut', 'truncated_dim']

    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 = type(self).__name__
        self._evec_dtype = np.complex_
        self._default_grid = discretization.Grid1d(
            -np.pi / 2, 3 * np.pi / 2, 100)  # for plotting in phi_j basis
        self._image_filename = os.path.join(
            os.path.dirname(os.path.abspath(__file__)),
            'qubit_pngs/fluxqubit.png')

        super().__init__()

        self.add_element(circuit.Capacitance('Cg1'), ['g1', '1'])
        self.add_element(circuit.Capacitance('Cg2'), ['g2', '2'])
        self.add_element(circuit.Capacitance('CJ1'), ['GND', '1'])
        self.add_element(circuit.Capacitance('CJ2'), ['GND', '2'])
        self.add_element(circuit.Capacitance('CJ3'), ['1', '3'])
        self.add_element(circuit.JosephsonJunction('J1', use_offset=False),
                         ['GND', '1'])
        self.add_element(circuit.JosephsonJunction('J2', use_offset=False),
                         ['GND', '2'])
        self.add_element(circuit.JosephsonJunction('J3', use_offset=False),
                         ['1', '3'])

        self.phi1 = circuit.Variable('\\phi_1')
        self.phi2 = circuit.Variable('\\phi_2')
        self.f = circuit.Variable('f')
        self.g1 = circuit.Variable('g_1')
        self.g2 = circuit.Variable('g_2')

        self.add_variable(self.phi1)
        self.add_variable(self.phi2)
        self.add_variable(self.f)
        self.add_variable(self.g1)
        self.add_variable(self.g2)

        self.map_nodes_linear(['GND', '1', '2', '3', 'g1', 'g2'],
                              ['\\phi_1', '\\phi_2', 'f', 'g_1', 'g_2'],
                              np.asarray([[0, 0, 0, 0, 0], [1, 0, 0, 0, 0],
                                          [0, 1, 0, 0, 0], [0, 1, -1, 0, 0],
                                          [0, 0, 0, 1, 0], [0, 0, 0, 0, 1]]))

        self.set_parameters()

    def set_parameters(self):
        self.phi1.set_variable(self.ncut * 2 + 1,
                               1)  # 2pi wavefunction periodicity
        self.phi2.set_variable(self.ncut * 2 + 1,
                               1)  # 2pi wavefunction periodicity
        self.f.set_parameter(
            self.flux * 2 * np.pi,
            0)  # external flux: 0.4 quantum, external voltage: 0
        self.g1.set_parameter(
            0, self.ng1 * self.ECg1 /
            8)  # external flux: 0 quanta, external voltage: 0
        self.g2.set_parameter(
            0, self.ng2 * self.ECg2 /
            8)  # external flux: 0 quanta, external voltage: 0

        self.find_element('J1').set_critical_current(self.EJ1)
        self.find_element('J2').set_critical_current(self.EJ2)
        self.find_element('J3').set_critical_current(self.EJ3)
        self.find_element('CJ1').set_capacitance(1 / (8 * self.ECJ1))
        self.find_element('CJ2').set_capacitance(1 / (8 * self.ECJ2))
        self.find_element('CJ3').set_capacitance(1 / (8 * self.ECJ3))
        self.find_element('Cg1').set_capacitance(1 / (8 * self.ECg1))
        self.find_element('Cg2').set_capacitance(1 / (8 * self.ECg2))

    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 potential(self, *args):
        self.set_parameters()
        return super().potential(*args)

    def hamiltonian(self):
        self.set_parameters()
        return super().hamiltonian()

    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 = spec_utils.standardize_phases(
            wavefunc_amplitudes)

        grid2d = discretization.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 storage.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)
Exemplo n.º 10
0
class Transmon(base.QubitBaseClass1d, serializers.Serializable):
    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 = descriptors.WatchedProperty('QUANTUMSYSTEM_UPDATE')
    EC = descriptors.WatchedProperty('QUANTUMSYSTEM_UPDATE')
    ng = descriptors.WatchedProperty('QUANTUMSYSTEM_UPDATE')
    ncut = descriptors.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 = type(self).__name__
        self._evec_dtype = np.float_
        self._default_grid = discretization.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 = constants.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 storage.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 storage.WaveFunction(basis_labels=phi_basis_labels,
                                    amplitudes=phi_wavefunc_amplitudes,
                                    energy=evals[which])
Exemplo n.º 11
0
class Grid1d(dispatch.DispatchClient, serializers.Serializable):
    """Data structure and methods for setting up discretized 1d coordinate grid,
    generating corresponding derivative matrices.

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

    min_val = descriptors.WatchedProperty("GRID_UPDATE")
    max_val = descriptors.WatchedProperty("GRID_UPDATE")
    pt_count = descriptors.WatchedProperty("GRID_UPDATE")

    def __init__(self, min_val: float, max_val: float, pt_count: int) -> None:
        self.min_val = min_val
        self.max_val = max_val
        self.pt_count = pt_count

    def __repr__(self) -> str:
        init_dict = self.get_initdata()
        return type(self).__name__ + f"({init_dict!r})"

    def __str__(self) -> str:
        output = "Grid1d -----[ "
        for param_name, param_val in sorted(
            utils.drop_private_keys(self.__dict__).items()
        ):
            output += str(param_name) + ": " + str(param_val) + ",  "
        output = output[:-3] + " ]"
        return output

    def __eq__(self, other: Any) -> bool:
        if not isinstance(other, type(self)):
            return False
        return self.__dict__ == other.__dict__

    def __hash__(self):
        return super().__hash__()

    def get_initdata(self) -> Dict[str, Any]:
        """Returns dict appropriate for creating/initializing a new Grid1d object.
        Returns
        -------
        dict
        """
        return self.__dict__

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

    def make_linspace(self) -> ndarray:
        """Returns a numpy array of the grid points
        Returns
        -------
        ndarray
        """
        return np.linspace(self.min_val, self.max_val, self.pt_count)

    def first_derivative_matrix(
        self, prefactor: Union[float, complex] = 1.0, periodic: bool = False
    ) -> dia_matrix:
        """Generate sparse matrix for first derivative of the form :math:`\\partial_{x_i}`.
        Uses STENCIL setting to construct the matrix with a multi-point stencil.

        Parameters
        ----------
        prefactor:
            prefactor of the derivative matrix (default value: 1.0)
        periodic:
            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.grid_spacing()
        matrix_diagonals = [
            coefficient * prefactor / delta_x
            for coefficient in FIRST_STENCIL_COEFFS[settings.STENCIL]
        ]
        offset = [i - (settings.STENCIL - 1) // 2 for i in range(settings.STENCIL)]
        derivative_matrix = band_matrix(
            matrix_diagonals, offset, self.pt_count, dtype=dtp, has_corners=periodic
        )
        return derivative_matrix

    def second_derivative_matrix(
        self, prefactor: Union[float, complex] = 1.0, periodic: bool = False
    ) -> dia_matrix:
        """Generate sparse matrix for second derivative of the form :math:`\\partial^2_{x_i}`.
        Uses STENCIL setting to construct the matrix with a multi-point stencil.

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

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

        delta_x = self.grid_spacing()
        matrix_diagonals = [
            coefficient * prefactor / delta_x ** 2
            for coefficient in SECOND_STENCIL_COEFFS[settings.STENCIL]
        ]
        offset = [i - (settings.STENCIL - 1) // 2 for i in range(settings.STENCIL)]
        derivative_matrix = band_matrix(
            matrix_diagonals, offset, self.pt_count, dtype=dtp, has_corners=periodic
        )
        return derivative_matrix
Exemplo n.º 12
0
class HilbertSpace(dispatch.DispatchClient, serializers.Serializable):
    """Class holding information about the full Hilbert space, usually composed of multiple subsys_list.
    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 = descriptors.ReadOnlyProperty()
    qbt_subsys_list = descriptors.ReadOnlyProperty()
    lookup = descriptors.ReadOnlyProperty()
    interaction_list = descriptors.WatchedProperty('INTERACTIONLIST_UPDATE')

    def __init__(self,
                 subsystem_list: List[QuantumSys],
                 interaction_list: List[InteractionTerm] = None) -> None:
        self._subsystems: Tuple[QuantumSys, ...] = tuple(subsystem_list)
        if interaction_list:
            self.interaction_list = tuple(interaction_list)
        else:
            self.interaction_list = []

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

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

    def __getitem__(self, index: int) -> QuantumSys:
        return self._subsystems[index]

    def __iter__(self) -> Iterator[QuantumSys]:
        return iter(self._subsystems)

    def __repr__(self) -> str:
        init_dict = self.get_initdata()
        return type(self).__name__ + f'(**{init_dict!r})'

    def __str__(self) -> str:
        output = '====== HilbertSpace object ======\n'
        for subsystem in self:
            output += '\n' + str(subsystem) + '\n'
        if self.interaction_list:
            for interaction_term in self.interaction_list:
                output += '\n' + str(interaction_term) + '\n'
        return output

    ###############################################################################################
    # HilbertSpace: file IO methods
    ###############################################################################################
    @classmethod
    def deserialize(cls, io_data: 'IOData') -> 'HilbertSpace':
        """
        Take the given IOData and return an instance of the described class, initialized with the data stored in
        io_data.
        """
        alldata_dict = io_data.as_kwargs()
        lookup = alldata_dict.pop('_lookup', None)
        new_hilbertspace = cls(**alldata_dict)
        new_hilbertspace._lookup = lookup
        if lookup is not None:
            new_hilbertspace._lookup._hilbertspace = weakref.proxy(
                new_hilbertspace)
        return new_hilbertspace

    def serialize(self) -> 'IOData':
        """
        Convert the content of the current class instance into IOData format.
        """
        initdata = {name: getattr(self, name) for name in self._init_params}
        initdata['_lookup'] = self._lookup
        iodata = serializers.dict_serialize(initdata)
        iodata.typename = type(self).__name__
        return iodata

    def get_initdata(self) -> Dict[str, Any]:
        """Returns dict appropriate for creating/initializing a new HilbertSpace object.
        """
        return {
            'subsystem_list': self._subsystems,
            'interaction_list': self.interaction_list
        }

    ###############################################################################################
    # HilbertSpace: creation via GUI
    ###############################################################################################
    @classmethod
    def create(cls) -> 'HilbertSpace':
        hilbertspace = cls([])
        scqubits.ui.hspace_widget.create_hilbertspace_widget(
            hilbertspace.__init__)  # type: ignore
        return hilbertspace

    ###############################################################################################
    # HilbertSpace: methods for CentralDispatch
    ###############################################################################################
    def receive(self, event: str, sender: Any, **kwargs) -> None:
        if self._lookup is not None:
            if event == 'QUANTUMSYSTEM_UPDATE' and sender in self:
                self.broadcast('HILBERTSPACE_UPDATE')
                self._lookup._out_of_sync = True
            elif event == 'INTERACTIONTERM_UPDATE' and sender in self.interaction_list:
                self.broadcast('HILBERTSPACE_UPDATE')
                self._lookup._out_of_sync = True
            elif event == 'INTERACTIONLIST_UPDATE' and sender is self:
                self.broadcast('HILBERTSPACE_UPDATE')
                self._lookup._out_of_sync = True

    ###############################################################################################
    # HilbertSpace: subsystems, dimensions, etc.
    ###############################################################################################
    def get_subsys_index(self, subsys: QuantumSys) -> int:
        """
        Return the index of the given subsystem in the HilbertSpace.
        """
        return self._subsystems.index(subsys)

    @property
    def subsystem_list(self) -> Tuple[QuantumSys, ...]:
        return self._subsystems

    @property
    def subsystem_dims(self) -> List[int]:
        """Returns list of the Hilbert space dimensions of each subsystem"""
        return [subsystem.truncated_dim for subsystem in self]

    @property
    def dimension(self) -> int:
        """Returns total dimension of joint Hilbert space"""
        return np.prod(np.asarray(self.subsystem_dims))

    @property
    def subsystem_count(self) -> int:
        """Returns number of subsys_list composing the joint Hilbert space"""
        return len(self._subsystems)

    ###############################################################################################
    # HilbertSpace: generate SpectrumLookup
    ###############################################################################################
    def generate_lookup(self) -> None:
        bare_specdata_list = []
        for index, subsys in enumerate(self):
            evals, evecs = subsys.eigensys(evals_count=subsys.truncated_dim)
            bare_specdata_list.append(
                storage.SpectrumData(energy_table=[evals],
                                     state_table=[evecs],
                                     system_params=subsys.get_initdata()))

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

    ###############################################################################################
    # HilbertSpace: energy spectrum
    ###############################################################################################
    def eigenvals(self, evals_count: int = 6) -> ndarray:
        """Calculates eigenvalues of the full Hamiltonian using `qutip.Qob.eigenenergies()`.

        Parameters
        ----------
        evals_count:
            number of desired eigenvalues/eigenstates
        """
        hamiltonian_mat = self.hamiltonian()
        return hamiltonian_mat.eigenenergies(eigvals=evals_count)

    def eigensys(self,
                 evals_count: int = 6) -> Tuple[ndarray, QutipEigenstates]:
        """Calculates eigenvalues and eigenvectors of the full Hamiltonian using `qutip.Qob.eigenstates()`.

        Parameters
        ----------
        evals_count:
            number of desired eigenvalues/eigenstates

        Returns
        -------
            eigenvalues and eigenvectors
        """
        hamiltonian_mat = self.hamiltonian()
        evals, evecs = hamiltonian_mat.eigenstates(eigvals=evals_count)
        evecs = evecs.view(scqubits.io_utils.fileio_qutip.QutipEigenstates)
        return evals, evecs

    def _esys_for_paramval(
            self, paramval: float, update_hilbertspace: Callable,
            evals_count: int) -> Tuple[ndarray, QutipEigenstates]:
        update_hilbertspace(paramval)
        return self.eigensys(evals_count)

    def _evals_for_paramval(self, paramval: float,
                            update_hilbertspace: Callable,
                            evals_count: int) -> ndarray:
        update_hilbertspace(paramval)
        return self.eigenvals(evals_count)

    ###############################################################################################
    # HilbertSpace: Hamiltonian (bare, interaction, full)
    ###############################################################################################
    def hamiltonian(self) -> Qobj:
        """

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

    def bare_hamiltonian(self) -> Qobj:
        """
        Returns
        -------
            composite Hamiltonian composed of bare Hamiltonians of subsys_list 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 interaction_hamiltonian(self) -> Qobj:
        """
        Returns
        -------
            interaction Hamiltonian
        """
        if not self.interaction_list:
            return 0

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

    def interactionterm_hamiltonian(self,
                                    interactionterm: InteractionTerm,
                                    evecs1: ndarray = None,
                                    evecs2: ndarray = None) -> Qobj:
        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.dag()
        return hamiltonian

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

        Parameters
        ----------
        subsystem:
            Subsystem for which the Hamiltonian is to be provided.
        evals:
            Eigenenergies can be provided as `evals`; otherwise, they are calculated.
        """
        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 get_bare_hamiltonian(self) -> Qobj:
        """Deprecated, use `bare_hamiltonian()` instead."""
        warnings.warn(
            'get_bare_hamiltonian() is deprecated, use bare_hamiltonian() instead',
            FutureWarning)
        return self.bare_hamiltonian()

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

    ###############################################################################################
    # HilbertSpace: identity wrapping, operators
    ###############################################################################################
    def identity_wrap(self,
                      operator: Union[str, ndarray, csc_matrix, dia_matrix,
                                      Qobj],
                      subsystem: QuantumSys,
                      op_in_eigenbasis: bool = False,
                      evecs: ndarray = None) -> Qobj:
        """Wrap given operator in subspace `subsystem` in identity operators to form full Hilbert-space operator.

        Parameters
        ----------
        operator:
            operator acting in Hilbert space of `subsystem`; if str, then this should be an operator name in
            the subsystem, typically not in eigenbasis
        subsystem:
            subsystem where diagonal operator is defined
        op_in_eigenbasis:
            whether `operator` is given in the `subsystem` eigenbasis; otherwise, the internal QuantumSys basis is
            assumed
        evecs:
            internal QuantumSys eigenstates, used to convert `operator` into eigenbasis
        """
        subsys_operator = spec_utils.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 diag_operator(self, diag_elements: ndarray,
                      subsystem: QuantumSys) -> Qobj:
        """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 subsys_list).

        Parameters
        ----------
        diag_elements:
            diagonal elements of subsystem diagonal operator
        subsystem:
            subsystem where diagonal operator is defined
        """
        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 hubbard_operator(self, j: int, k: int, subsystem: QuantumSys) -> Qobj:
        """Hubbard operator :math:`|j\\rangle\\langle k|` for system `subsystem`

        Parameters
        ----------
        j,k:
            eigenstate indices for Hubbard operator
        subsystem:
            subsystem in which Hubbard operator acts
        """
        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: QuantumSys) -> Qobj:
        """Annihilation operator a for `subsystem`

        Parameters
        ----------
        subsystem:
            specifies subsystem in which annihilation operator acts
        """
        dim = subsystem.truncated_dim
        operator = (qt.destroy(dim))
        return self.identity_wrap(operator, subsystem)

    ###############################################################################################
    # HilbertSpace: spectrum sweep
    ###############################################################################################
    def get_spectrum_vs_paramvals(
            self,
            param_vals: ndarray,
            update_hilbertspace: Callable,
            evals_count: int = 10,
            get_eigenstates: bool = False,
            param_name: str = "external_parameter",
            num_cpus: int = settings.NUM_CPUS) -> SpectrumData:
        """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:
            array of parameter values
        update_hilbertspace:
            update_hilbertspace(param_val) specifies how a change in the external parameter affects
            the Hilbert space components
        evals_count:
            number of desired energy levels (default value = 10)
        get_eigenstates:
            set to true if eigenstates should be returned as well (default value = False)
        param_name:
            name for the parameter that is varied in `param_vals` (default value = "external_parameter")
        num_cpus:
            number of cores to be used for computation (default value: settings.NUM_CPUS)
        """
        target_map = cpu_switch.get_map_method(num_cpus)
        if get_eigenstates:
            func = functools.partial(self._esys_for_paramval,
                                     update_hilbertspace=update_hilbertspace,
                                     evals_count=evals_count)
            with utils.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 = spec_utils.recast_esys_mapdata(
                eigensystem_mapdata)
        else:
            func = functools.partial(self._evals_for_paramval,
                                     update_hilbertspace=update_hilbertspace,
                                     evals_count=evals_count)
            with utils.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  # type: ignore

        return storage.SpectrumData(eigenvalue_table,
                                    self.get_initdata(),
                                    param_name,
                                    param_vals,
                                    state_table=eigenstate_table)
Exemplo n.º 13
0
class GenericQubit(base.QuantumSystem, serializers.Serializable):
    """Class for a generic qubit (genuine two-level system). Create a class instance
    via::

        GenericQubit(E=4.3)

    Parameters
    ----------
    E:
       qubit energy splitting
    """

    truncated_dim = 2
    _evec_dtype: type
    _sys_type: str
    _init_params: list

    E = descriptors.WatchedProperty("QUANTUMSYSTEM_UPDATE")

    def __init__(self, E: float) -> None:
        self.E = E
        self._sys_type = type(self).__name__
        self._evec_dtype = np.float_

    @staticmethod
    def default_params() -> Dict[str, Any]:
        return {"E": 5.0}

    def hamiltonian(self):
        return 0.5 * self.E * self.sz_operator()

    def hilbertdim(self) -> int:
        """Returns Hilbert space dimension"""
        return 2

    def eigenvals(self, evals_count: int = 2) -> ndarray:
        hamiltonian_mat = self.hamiltonian()
        evals = sp.linalg.eigh(hamiltonian_mat, eigvals_only=True)
        return np.sort(evals)

    def eigensys(self, evals_count: int = 2) -> Tuple[ndarray, ndarray]:
        hamiltonian_mat = self.hamiltonian()
        evals, evecs = sp.linalg.eigh(hamiltonian_mat, eigvals_only=False)
        evals, evecs = order_eigensystem(evals, evecs)
        return evals, evecs

    def matrixelement_table(self, operator: str) -> ndarray:
        """Returns table of matrix elements for `operator` with respect to the
        eigenstates of the qubit. The operator is given as a string matching a class
        method returning an operator matrix.

        Parameters
        ----------
        operator:
            name of class method in string form, returning operator matrix in
            qubit-internal basis.
        """
        _, evecs = self.eigensys()
        operator_matrix = getattr(self, operator)()
        table = get_matrixelement_table(operator_matrix, evecs)
        return table

    @classmethod
    def create(cls) -> base.QuantumSystem:
        raise NotImplementedError

    def widget(self, params: Dict[str, Any] = None):
        raise NotImplementedError("GenericQubit does not support widget-based "
                                  "creation.")

    def sx_operator(self):
        return operators.sigma_x()

    def sy_operator(self):
        return operators.sigma_y()

    def sz_operator(self):
        return operators.sigma_z()

    def sp_operator(self):
        return operators.sigma_plus()

    def sm_operator(self):
        return operators.sigma_minus()
Exemplo n.º 14
0
class ParameterSweep(ParameterSweepBase, dispatch.DispatchClient,
                     serializers.Serializable):
    """
    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 = descriptors.WatchedProperty('PARAMETERSWEEP_UPDATE')
    param_vals = descriptors.WatchedProperty('PARAMETERSWEEP_UPDATE')
    param_count = descriptors.WatchedProperty('PARAMETERSWEEP_UPDATE')
    evals_count = descriptors.WatchedProperty('PARAMETERSWEEP_UPDATE')
    subsys_update_list = descriptors.WatchedProperty('PARAMETERSWEEP_UPDATE')
    update_hilbertspace = descriptors.WatchedProperty('PARAMETERSWEEP_UPDATE')
    lookup = descriptors.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

        # setup for file Serializable

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

        # generate the spectral data sweep
        if settings.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 = spec_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 _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 = cpu_switch.get_map_method(self.num_cpus)
        with utils.InfoBar(
                "Parallel compute bare eigensys [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 = cpu_switch.get_map_method(self.num_cpus)

        with utils.InfoBar(
                "Parallel compute dressed eigensys [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(
                storage.SpectrumData(energy_table,
                                     system_params={},
                                     param_name=self.param_name,
                                     param_vals=self.param_vals,
                                     state_table=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 = storage.SpectrumData(energy_table,
                                        system_params={},
                                        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)
        evals, evecs = hamiltonian.eigenstates(eigvals=self.evals_count)
        evecs = evecs.view(serializers.QutipEigenstates)
        return evals, evecs

    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]

    @classmethod
    def deserialize(cls, iodata):
        """
        Take the given IOData and return an instance of the described class, initialized with the data stored in
        io_data.

        Parameters
        ----------
        iodata: IOData

        Returns
        -------
        StoredSweep
        """
        return cls(**iodata.as_kwargs())

    def serialize(self):
        """
        Convert the content of the current class instance into IOData format.

        Returns
        -------
        IOData
        """
        initdata = {
            'param_name': self.param_name,
            'param_vals': self.param_vals,
            'evals_count': self.evals_count,
            'hilbertspace': self._hilbertspace,
            'dressed_specdata': self._lookup._dressed_specdata,
            'bare_specdata_list': self._lookup._bare_specdata_list
        }
        iodata = serializers.dict_serialize(initdata)
        iodata.typename = 'StoredSweep'
        return iodata

    def filewrite(self, filename):
        """Convenience method bound to the class. Simply accesses the `write` function.

        Parameters
        ----------
        filename: str
        """
        io.write(self, filename)
Exemplo n.º 15
0
class Bifluxon(base.QubitBaseClass, serializers.Serializable, NoisyBifluxon):
    r"""Bifluxon Qubit

    | [1] Kalashnikov et al., PRX Quantum 1, 010307 (2020). https://doi.org/10.1103/PRXQuantum.1.010307


    Bifluxon qubit without considering disorder in the small Josephson junctions, based Eq. (1) in [1],

    .. math::

        H &= 4E_{\text{C}}(-i\partial_\theta-n_g)^2-2E_\text{J}\cos\theta\cos(\phi/2) \\
        -4E_\text{CL}\partial_\phi^2+E_L(\phi -\varphi_\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:
        mean Josephson energy of the two junctions
    EL:
        inductive energy of the inductors
    EC:
        charging energy of the superconducting islond connected to the two junctions
    ECL:
        charging energy of the large superinductor
    dEJ:
        relative disorder in EJ, i.e., (EJ1-EJ2)/EJavg
    ng:
        offset charge at the small superconducting island
    flux:
        magnetic flux through the circuit loop, measured in units of flux quanta (h/2e)
    grid:
        specifies the range and spacing of the discretization lattice
    ncut:
        charge number cutoff for the superconducting island,  `n_theta = -ncut, ..., ncut`

    truncated_dim:
        desired dimension of the truncated quantum system; expected: truncated_dim > 1
   """
    EJ = descriptors.WatchedProperty("QUANTUMSYSTEM_UPDATE")
    EL = descriptors.WatchedProperty("QUANTUMSYSTEM_UPDATE")
    EC = descriptors.WatchedProperty("QUANTUMSYSTEM_UPDATE")
    ECL = descriptors.WatchedProperty("QUANTUMSYSTEM_UPDATE")
    dEJ = descriptors.WatchedProperty("QUANTUMSYSTEM_UPDATE")
    ng = descriptors.WatchedProperty("QUANTUMSYSTEM_UPDATE")
    ncut = descriptors.WatchedProperty("QUANTUMSYSTEM_UPDATE")

    def __init__(
        self,
        EJ: float,
        EL: float,
        EC: float,
        ECL: float,
        ng: float,
        flux: float,
        grid: Grid1d,
        ncut: int,
        dEJ: float = 0.0,
        truncated_dim: int = 6,
    ) -> None:
        self.EJ = EJ
        self.EL = EL
        self.EC = EC
        self.ECL = ECL
        self.dEJ = dEJ
        self.ng = ng
        self.flux = flux
        self.grid = grid
        self.ncut = ncut
        self.truncated_dim = truncated_dim
        self._sys_type = type(self).__name__
        self._evec_dtype = np.complex_

        # _default_grid is for *theta*, needed for plotting wavefunction
        self._default_grid = discretization.Grid1d(-np.pi / 2, 3 * np.pi / 2,
                                                   200)

        self._image_filename = os.path.join(
            os.path.dirname(os.path.abspath(__file__)),
            "qubit_img/bifluxon.jpg")
        dispatch.CENTRAL_DISPATCH.register("GRID_UPDATE", self)

    @staticmethod
    def default_params() -> Dict[str, Any]:
        return {
            "EJ": 27.2,
            "EL": 0.94,
            "EC": 7.7,
            "ECL": 10.0,
            "dEJ": 0.0,
            "ng": 0.5,
            "flux": 0.23,
            "ncut": 30,
            "truncated_dim": 10,
        }

    @classmethod
    def create(cls) -> "Bifluxon":
        phi_grid = discretization.Grid1d(-19.0, 19.0, 200)
        init_params = cls.default_params()
        init_params["grid"] = phi_grid
        bifluxon = cls(**init_params)
        bifluxon.widget()
        return bifluxon

    def supported_noise_channels(self) -> List[str]:
        """Return a list of supported noise channels"""
        return [
            "tphi_1_over_f_cc",
            "tphi_1_over_f_flux",
            "t1_flux_bias_line",
            # 't1_capacitive',
            "t1_inductive",
        ]

    def widget(self, params: Dict[str, Any] = None) -> None:
        init_params = params or self.get_initdata()
        del init_params["grid"]
        init_params["grid_max_val"] = self.grid.max_val
        init_params["grid_min_val"] = self.grid.min_val
        init_params["grid_pt_count"] = self.grid.pt_count
        ui.create_widget(self.set_params,
                         init_params,
                         image_filename=self._image_filename)

    def set_params(self, **kwargs) -> None:
        phi_grid = discretization.Grid1d(
            kwargs.pop("grid_min_val"),
            kwargs.pop("grid_max_val"),
            kwargs.pop("grid_pt_count"),
        )
        self.grid = phi_grid
        for param_name, param_val in kwargs.items():
            setattr(self, param_name, param_val)

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

    def _evals_calc(self, evals_count: int) -> ndarray:
        hamiltonian_mat = self.hamiltonian()
        evals = sparse.linalg.eigsh(
            hamiltonian_mat,
            k=evals_count,
            sigma=0.0,
            which="LM",
            return_eigenvectors=False,
        )
        return np.sort(evals)

    def _esys_calc(self, evals_count: int) -> Tuple[ndarray, ndarray]:
        hamiltonian_mat = self.hamiltonian()
        evals, evecs = sparse.linalg.eigsh(
            hamiltonian_mat,
            k=evals_count,
            sigma=0.0,
            which="LM",
            return_eigenvectors=True,
        )
        # TODO consider normalization of zeropi wavefunctions
        # evecs /= np.sqrt(self.grid.grid_spacing())
        evals, evecs = spec_utils.order_eigensystem(evals, evecs)
        return evals, evecs

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

    def potential(self, phi: ndarray, theta: ndarray) -> ndarray:
        """
        Returns
        -------
            value of the potential energy evaluated at phi, theta for Bifluxon
        """
        return (-2.0 * self.EJ * np.cos(theta) *
                np.cos(phi / 2.0 + 2.0 * np.pi * self.flux / 2.0) +
                (1 / 2.0) * self.EL * phi**2)

    def sparse_kinetic_mat(self) -> csc_matrix:
        """
        Kinetic energy portion of the Hamiltonian.

        Returns
        -------
            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")
        identity_theta = sparse.identity(dim_theta, format="csc")
        kinetic_matrix_phi = self.grid.second_derivative_matrix(
            prefactor=-4.0 * self.ECL)
        diag_elements = (4.0 * self.EC * 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")

        return kinetic_matrix

    def sparse_potential_mat(self) -> csc_matrix:
        """
        Potential energy portion of the Hamiltonian.

        Returns
        -------
            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 = (1 / 2.0) * self.EL * np.square(grid_linspace)
        phi_inductive_potential = sparse.dia_matrix(
            (phi_inductive_vals, [0]), shape=(pt_count, pt_count)).tocsc()
        phi_cosby2_vals = np.cos(grid_linspace / 2.0 +
                                 2.0 * np.pi * self.flux / 2.0)
        phi_cosby2_potential = sparse.dia_matrix(
            (phi_cosby2_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_cosby2_potential, theta_cos_potential, format="csc") +
            sparse.kron(
                phi_inductive_potential, self._identity_theta(), format="csc"))
        # if self.dEJ != 0:
        #     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) -> csc_matrix:
        """Calculates Hamiltonian in basis obtained by discretizing phi and employing charge basis for theta.

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

    def sparse_d_potential_d_flux_mat(self) -> csc_matrix:
        r"""Calculates derivative 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
        -------
            matrix representing the derivative of the potential energy.
            NEED TO UPDATE FOR BIFLUXON with phi/2 operators
        """
        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) -> csc_matrix:
        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
        -------
            matrix representing the derivative of the Hamiltonian
        """
        return self.sparse_d_potential_d_flux_mat()

    def sparse_d_potential_d_EJ_mat(self) -> csc_matrix:
        r"""Calculates a of the potential energy w.r.t EJ.

        Returns
        -------
            matrix representing the derivative of the potential energy
        """
        return -2.0 * sparse.kron(
            self._cos_phi_operator(x=-2.0 * np.pi * self.flux / 2.0),
            self._cos_theta_operator(),
            format="csc",
        )

    def d_hamiltonian_d_EJ(self) -> csc_matrix:
        r"""Calculates a derivative of the Hamiltonian w.r.t EJ.

        Returns
        -------
            matrix representing the derivative of the Hamiltonian
        """
        return self.sparse_d_potential_d_EJ_mat()

    def d_hamiltonian_d_ng(self) -> csc_matrix:
        r"""Calculates a derivative of the Hamiltonian w.r.t ng.
        as stored in the object.

        Returns
        -------
            matrix representing the derivative of the Hamiltonian
        """
        return -8 * self.EC * self.n_theta_operator()

    def _identity_phi(self) -> csc_matrix:
        r"""
        Identity operator acting only on the `\phi` Hilbert subspace.
        """
        pt_count = self.grid.pt_count
        return sparse.identity(pt_count, format="csc")

    def _identity_theta(self) -> csc_matrix:
        r"""
        Identity operator acting only on the `\theta` Hilbert subspace.
        """
        dim_theta = 2 * self.ncut + 1
        return sparse.identity(dim_theta, format="csc")

    def i_d_dphi_operator(self) -> csc_matrix:
        r"""
        Operator :math:`i d/d\phi`.
        """
        return sparse.kron(
            self.grid.first_derivative_matrix(prefactor=1j),
            self._identity_theta(),
            format="csc",
        )

    def _phi_operator(self) -> dia_matrix:
        r"""
        Operator :math:`\phi`, acting only on the `\phi` Hilbert subspace.
        """
        pt_count = self.grid.pt_count

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

    def phi_operator(self) -> csc_matrix:
        r"""
        Operator :math:`\phi`.
        """
        return sparse.kron(self._phi_operator(),
                           self._identity_theta(),
                           format="csc")

    def n_theta_operator(self) -> csc_matrix:
        r"""
        Operator :math:`n_\theta`.
        """
        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: float = 0) -> csc_matrix:
        r"""
        Operator :math:`\sin(\phi + x)`, acting only on the `\phi` Hilbert subspace.x
        """
        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: float = 0) -> csc_matrix:
        r"""
        Operator :math:`\cos(\phi + x)`, acting only on the `\phi` Hilbert subspace.
        """
        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 _sin_phiby2_operator(self, x: float = 0) -> csc_matrix:
        r"""
        Operator :math:`\sin(\phi/2 + x)`, acting only on the `\phi` Hilbert subspace.x
        """
        pt_count = self.grid.pt_count

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

    def _cos_phiby2_operator(self, x: float = 0) -> csc_matrix:
        r"""
        Operator :math:`\cos(\phi/2.0 + x)`, acting only on the `\phi` Hilbert subspace.
        """
        pt_count = self.grid.pt_count

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

    def _cos_theta_operator(self) -> csc_matrix:
        r"""
        Operator :math:`\cos(\theta)`, acting only on the `\theta` Hilbert subspace.
        """
        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) -> csc_matrix:
        r"""
        Operator :math:`\cos(\theta)`.
        """
        return sparse.kron(self._identity_phi(),
                           self._cos_theta_operator(),
                           format="csc")

    def _sin_theta_operator(self) -> csc_matrix:
        r"""
        Operator :math:`\sin(\theta)`, acting only on the `\theta` Hilbert space.
        """
        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) -> csc_matrix:
        r"""
        Operator :math:`\sin(\theta)`.
        """
        return sparse.kron(self._identity_phi(),
                           self._sin_theta_operator(),
                           format="csc")

    def plot_potential(self,
                       theta_grid: Grid1d = None,
                       contour_vals: Union[List[float], ndarray] = None,
                       **kwargs) -> Tuple[Figure, Axes]:
        """Draw contour plot of the potential energy.

        Parameters
        ----------
        theta_grid:
            used for setting a custom grid for theta; if None use self._default_grid
        contour_vals:
        **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,
                             xlabel=r"$\phi$",
                             ylabel=r"$\theta$",
                             **kwargs)

    def wavefunction(
        self,
        esys: Tuple[ndarray, ndarray] = None,
        which: int = 0,
        theta_grid: Grid1d = None,
    ) -> WaveFunctionOnGrid:
        """Returns a zero-pi wave function in `phi`, `theta` basis

        Parameters
        ----------
        esys:
            eigenvalues, eigenvectors
        which:
             index of desired wave function (default value = 0)
        theta_grid:
            used for setting a custom grid for theta; if None use self._default_grid
        """
        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 = spec_utils.standardize_phases(
            wavefunc_amplitudes)

        grid2d = discretization.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 storage.WaveFunctionOnGrid(grid2d, wavefunc_amplitudes)

    def plot_wavefunction(self,
                          esys: Tuple[ndarray, ndarray] = None,
                          which: int = 0,
                          theta_grid: Grid1d = None,
                          mode: str = "abs",
                          zero_calibrate: bool = True,
                          **kwargs) -> Tuple[Figure, Axes]:
        """Plots 2d phase-basis wave function.

        Parameters
        ----------
        esys:
            eigenvalues, eigenvectors as obtained from `.eigensystem()`
        which:
            index of wave function to be plotted (default value = (0)
        theta_grid:
            used for setting a custom grid for theta; if None use self._default_grid
        mode:
            choices as specified in `constants.MODE_FUNC_DICT` (default value = 'abs_sqr')
        zero_calibrate:
            if True, colors are adjusted to use zero wavefunction amplitude as the neutral color in the palette
        **kwargs:
            plot options
        """
        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,
                                   xlabel=r"$\phi$",
                                   ylabel=r"$\theta$",
                                   **kwargs)
Exemplo n.º 16
0
class FullZeroPi(base.QubitBaseClass, serializers.Serializable,
                 NoisyFullZeroPi):
    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-\varphi_\text{ext}/2)\\
        &H_\text{int} = 2E_{C\Sigma}dC\,\partial_\theta\partial_\zeta + E_L dE_L \phi\,\zeta\\
        &H_\zeta = E_{\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; expected: truncated_dim > 1
    """

    EJ = descriptors.WatchedProperty('QUANTUMSYSTEM_UPDATE',
                                     inner_object_name='_zeropi')
    EL = descriptors.WatchedProperty('QUANTUMSYSTEM_UPDATE',
                                     inner_object_name='_zeropi')
    ECJ = descriptors.WatchedProperty('QUANTUMSYSTEM_UPDATE',
                                      inner_object_name='_zeropi')
    EC = descriptors.WatchedProperty('QUANTUMSYSTEM_UPDATE',
                                     inner_object_name='_zeropi')
    ECS = descriptors.WatchedProperty('QUANTUMSYSTEM_UPDATE',
                                      inner_object_name='_zeropi')
    dEJ = descriptors.WatchedProperty('QUANTUMSYSTEM_UPDATE',
                                      inner_object_name='_zeropi')
    dCJ = descriptors.WatchedProperty('QUANTUMSYSTEM_UPDATE',
                                      inner_object_name='_zeropi')
    ng = descriptors.WatchedProperty('QUANTUMSYSTEM_UPDATE',
                                     inner_object_name='_zeropi')
    flux = descriptors.WatchedProperty('QUANTUMSYSTEM_UPDATE',
                                       inner_object_name='_zeropi')
    grid = descriptors.WatchedProperty('QUANTUMSYSTEM_UPDATE',
                                       inner_object_name='_zeropi')
    ncut = descriptors.WatchedProperty('QUANTUMSYSTEM_UPDATE',
                                       inner_object_name='_zeropi')
    zeropi_cutoff = descriptors.WatchedProperty('QUANTUMSYSTEM_UPDATE',
                                                inner_object_name='_zeropi',
                                                attr_name='truncated_dim')
    dC = descriptors.WatchedProperty('QUANTUMSYSTEM_UPDATE')
    dEL = descriptors.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 = scqubits.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 = type(self).__name__
        self.truncated_dim = truncated_dim
        self._evec_dtype = np.complex_
        self._init_params.remove(
            'ECS'
        )  # used for file IO Serializable purposes; remove ECS as init parameter
        self._image_filename = os.path.join(
            os.path.dirname(os.path.abspath(__file__)),
            'qubit_img/fullzeropi.jpg')

        dispatch.CENTRAL_DISPATCH.register('GRID_UPDATE', self)

    @staticmethod
    def default_params():
        return {
            'EJ': 10.0,
            'EL': 0.04,
            'ECJ': 20.0,
            'EC': 0.04,
            'dEJ': 0.05,
            'dCJ': 0.05,
            'dC': 0.08,
            'dEL': 0.05,
            'ng': 0.1,
            'flux': 0.23,
            'ncut': 30,
            'zeropi_cutoff': 10,
            'zeta_cutoff': 40,
            'truncated_dim': 10
        }

    @classmethod
    def create(cls):
        phi_grid = discretization.Grid1d(-25.0, 25.0, 360)
        init_params = cls.default_params()
        zeropi = cls(**init_params, grid=phi_grid)
        zeropi.widget()
        return zeropi

    def supported_noise_channels(self):
        """Return a list of supported noise channels"""
        return [
            'tphi_1_over_f_cc',
            'tphi_1_over_f_flux'
            't1_bias_flux_line'
            # 't1_capacitive_loss',
            't1_inductive_loss',
        ]

    def widget(self, params=None):
        init_params = params or self.get_initdata()
        del init_params['grid']
        init_params['grid_max_val'] = self.grid.max_val
        init_params['grid_min_val'] = self.grid.min_val
        init_params['grid_pt_count'] = self.grid.pt_count
        ui.create_widget(self.set_params,
                         init_params,
                         image_filename=self._image_filename)

    def set_params(self, **kwargs):
        phi_grid = discretization.Grid1d(kwargs.pop('grid_min_val'),
                                         kwargs.pop('grid_max_val'),
                                         kwargs.pop('grid_pt_count'))
        self.grid = phi_grid
        for param_name, param_val in kwargs.items():
            setattr(self, param_name, param_val)

    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 set_E_zeta(self, value):
        raise ValueError(
            "It's not possible to directly set `E_zeta`. Instead one can set its value through `EL` or `EC`."
        )

    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)) + sparse.kron(
                zeropi_coupling.conjugate().T, 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 d_hamiltonian_d_EJ(self, zeropi_evecs=None):
        r"""Calculates a derivative of the Hamiltonian w.r.t EJ. 

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

    def d_hamiltonian_d_ng(self):
        r"""Calculates a derivative of the Hamiltonian w.r.t ng.
        as stored in the object.

        Returns
        -------
        scipy.sparse.csc_matrix
            matrix representing the derivative of the Hamiltonian
        """
        return -8 * self.EC * self.n_theta_operator()

    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 = spec_utils.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\phi`.

        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:`\phi`.

        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

        Returns
        -------
        int
        """
        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,
                                    sigma=0.0,
                                    which='LM',
                                    return_eigenvectors=False)
        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,
                                           sigma=0.0,
                                           which='LM',
                                           return_eigenvectors=True)
        evals, evecs = spec_utils.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 / 2.0) * (8.0 * self.EC /
                                                  self.EL)**0.25
        return prefactor * spec_utils.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 * spec_utils.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)
Exemplo n.º 17
0
class TunableTransmon(Transmon, serializers.Serializable, NoisySystem):
    r"""Class for the flux-tunable 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{\mathcal{E}_\text{J}(\Phi)}{2}(|n\rangle\langle n+1|+\text{h.c.})`,
    Here, the effective Josephson energy is flux-tunable:
    :math:`\mathcal{E}_J(\Phi) = E_{J,\text{max}} \sqrt{\cos^2(\pi\Phi/\Phi_0) + d^2 \sin^2(\pi\Phi/\Phi_0)}`
    and :math:`d=(E_{J2}-E_{J1})(E_{J1}+E_{J2})` parametrizes th junction asymmetry.

    Initialize with, for example::

        TunableTransmon(EJmax=1.0, d=0.1, EC=2.0, flux=0.3, ng=0.2, ncut=30)

    Parameters
    ----------
    EJmax:
       maximum effective Josephson energy (sum of the Josephson energies of the two junctions)
    d:
        junction asymmetry parameter
    EC:
        charging energy
    flux:
        flux threading the SQUID loop, in units of the flux quantum
    ng:
        offset charge
    ncut:
        charge basis cutoff, `n = -ncut, ..., ncut`
    truncated_dim:
        desired dimension of the truncated quantum system; expected: truncated_dim > 1
    """
    EJmax = descriptors.WatchedProperty('QUANTUMSYSTEM_UPDATE')
    d = descriptors.WatchedProperty('QUANTUMSYSTEM_UPDATE')
    flux = descriptors.WatchedProperty('QUANTUMSYSTEM_UPDATE')

    def __init__(self,
                 EJmax: float,
                 EC: float,
                 d: float,
                 flux: float,
                 ng: float,
                 ncut: int,
                 truncated_dim: int = 6) -> None:
        self.EJmax = EJmax
        self.EC = EC
        self.d = d
        self.flux = flux
        self.ng = ng
        self.ncut = ncut
        self.truncated_dim = truncated_dim
        self._sys_type = type(self).__name__
        self._evec_dtype = np.float_
        self._default_grid = discretization.Grid1d(-np.pi, np.pi, 151)
        self._default_n_range = (-5, 6)
        self._image_filename = os.path.join(
            os.path.dirname(os.path.abspath(__file__)),
            'qubit_img/tunable-transmon.jpg')

    @property
    def EJ(self) -> float:  # type: ignore
        """This is the effective, flux dependent Josephson energy, playing the role of EJ
        in the parent class `Transmon`"""
        return self.EJmax * np.sqrt(
            np.cos(np.pi * self.flux)**2 +
            self.d**2 * np.sin(np.pi * self.flux)**2)

    @staticmethod
    def default_params() -> Dict[str, Any]:
        return {
            'EJmax': 20.0,
            'EC': 0.3,
            'd': 0.01,
            'flux': 0.0,
            'ng': 0.0,
            'ncut': 30,
            'truncated_dim': 10
        }

    def supported_noise_channels(self) -> List[str]:
        """Return a list of supported noise channels"""
        return [
            'tphi_1_over_f_flux', 'tphi_1_over_f_cc', 'tphi_1_over_f_ng',
            't1_capacitive', 't1_flux_bias_line', 't1_charge_impedance'
        ]

    def d_hamiltonian_d_flux(self) -> ndarray:
        """Returns operator representing a derivative of the Hamiltonian with respect to `flux`."""
        return np.pi * self.EJmax * np.cos(np.pi * self.flux) * np.sin(np.pi * self.flux) * (self.d**2 - 1) \
                / np.sqrt(np.cos(np.pi * self.flux)**2 + self.d**2 * np.sin(np.pi * self.flux)**2) \
                * self.cos_phi_operator()
Exemplo n.º 18
0
class StoredSweep(
        ParameterSweepBase,
        SpectrumLookupMixin,
        dispatch.DispatchClient,
        serializers.Serializable,
):
    _parameters = descriptors.WatchedProperty("PARAMETERSWEEP_UPDATE")
    _evals_count = descriptors.WatchedProperty("PARAMETERSWEEP_UPDATE")
    _data = descriptors.WatchedProperty("PARAMETERSWEEP_UPDATE")
    _hilbertspace: HilbertSpace

    def __init__(self, paramvals_by_name, hilbertspace, evals_count,
                 _data) -> None:
        self._parameters = Parameters(paramvals_by_name)
        self._hilbertspace = hilbertspace
        self._evals_count = evals_count
        self._data = _data

        self._out_of_sync = False
        self._current_param_indices = None

    @classmethod
    def deserialize(cls, iodata: "IOData") -> "StoredSweep":
        """
        Take the given IOData and return an instance of the described class, initialized
        with the data stored in io_data.

        Parameters
        ----------
        iodata: IOData

        Returns
        -------
        StoredSweep
        """
        return StoredSweep(**iodata.as_kwargs())

    def serialize(self) -> "IOData":
        pass

    # StoredSweep: other methods
    def get_hilbertspace(self) -> HilbertSpace:
        return self._hilbertspace

    def new_sweep(
        self,
        paramvals_by_name: Dict[str, ndarray],
        update_hilbertspace: Callable,
        evals_count: int = 6,
        subsys_update_info: Optional[Dict[str, List[QuantumSys]]] = None,
        autorun: bool = settings.AUTORUN_SWEEP,
        num_cpus: Optional[int] = None,
    ) -> ParameterSweep:
        return ParameterSweep(
            self._hilbertspace,
            paramvals_by_name,
            update_hilbertspace,
            evals_count=evals_count,
            subsys_update_info=subsys_update_info,
            autorun=autorun,
            num_cpus=num_cpus,
        )
Exemplo n.º 19
0
class QuantumSystem(DispatchClient, ABC):
    """Generic quantum system class"""

    truncated_dim = descriptors.WatchedProperty("QUANTUMSYSTEM_UPDATE")
    _init_params: List[str]
    _image_filename: str
    _evec_dtype: type
    _sys_type: str

    # To facilitate warnings in set_units, introduce a counter keeping track of the
    # number of QuantumSystem instances
    _quantumsystem_counter: int = 0

    subclasses: List[ABCMeta] = []

    def __new__(cls, *args, **kwargs) -> "QuantumSystem":
        QuantumSystem._quantumsystem_counter += 1
        return super().__new__(cls)

    def __del__(self) -> None:
        # The following if clause mitigates an issue where upon program exit calls to
        # this destructor fail because `QuantumSystem` is of NoneType. (Upon program
        # exit, does the class itself get deleted before class instances are calling
        # their destructor?)
        try:
            QuantumSystem._quantumsystem_counter -= 1
        except (NameError, AttributeError):
            pass

    def __init_subclass__(cls):
        """Used to register all non-abstract subclasses as a list in
        `QuantumSystem.subclasses`."""
        super().__init_subclass__()
        if not inspect.isabstract(cls):
            cls.subclasses.append(cls)

    def __repr__(self) -> str:
        if hasattr(self, "_init_params"):
            init_names = self._init_params
        else:
            init_names = list(inspect.signature(self.__init__).parameters.keys())[1:]  # type: ignore
        init_dict = {name: getattr(self, name) for name in init_names}
        return type(self).__name__ + f"(**{init_dict!r})"

    def __str__(self) -> str:
        indent_length = 20
        name_prepend = self._sys_type.ljust(indent_length, "-") + "|\n"

        output = ""
        for param_name in self.default_params().keys():
            output += "{0}| {1}: {2}\n".format(
                " " * indent_length, str(param_name), str(getattr(self, param_name))
            )
        output += "{0}|\n".format(" " * indent_length)
        output += "{0}| dim: {1}\n".format(" " * indent_length, str(self.hilbertdim()))

        return name_prepend + output

    def __eq__(self, other: Any):
        if not isinstance(other, type(self)):
            return False
        return self.__dict__ == other.__dict__

    def __hash__(self):
        return super().__hash__()

    def get_initdata(self) -> Dict[str, Any]:
        """Returns dict appropriate for creating/initializing a new Serializable
        object."""
        return {name: getattr(self, name) for name in self._init_params}

    @abstractmethod
    def hilbertdim(self) -> int:
        """Returns dimension of Hilbert space"""

    @classmethod
    def create(cls) -> "QuantumSystem":
        """Use ipywidgets to create a new class instance"""
        init_params = cls.default_params()
        instance = cls(**init_params)
        instance.widget()
        return instance

    def widget(self, params: Dict[str, Any] = None):
        """Use ipywidgets to modify parameters of class instance"""
        init_params = params or self.get_initdata()
        ui.create_widget(
            self.set_params, init_params, image_filename=self._image_filename
        )

    @staticmethod
    @abstractmethod
    def default_params():
        """Return dictionary with default parameter values for initialization of
        class instance"""

    def set_params(self, **kwargs):
        """
        Set new parameters through the provided dictionary.
        """
        for param_name, param_val in kwargs.items():
            setattr(self, param_name, param_val)

    def supported_noise_channels(self) -> List:
        """
        Returns a list of noise channels this QuantumSystem supports. If none,
        return an empty list.
        """
        return []
Exemplo n.º 20
0
class ParameterSweepBase(ABC):
    """
    The_ParameterSweepBase class is an abstract base class for ParameterSweep and
    StoredSweep
    """

    _parameters = descriptors.WatchedProperty("PARAMETERSWEEP_UPDATE")
    _evals_count = descriptors.WatchedProperty("PARAMETERSWEEP_UPDATE")
    _data = descriptors.WatchedProperty("PARAMETERSWEEP_UPDATE")
    _hilbertspace: HilbertSpace

    _out_of_sync = False
    _current_param_indices: Union[NpIndices, None]

    def get_subsys(self, index: int) -> QuantumSys:
        return self._hilbertspace[index]

    def get_subsys_index(self, subsys: QuantumSys) -> int:
        return self._hilbertspace.get_subsys_index(subsys)

    @property
    def osc_subsys_list(self) -> List[Oscillator]:
        return self._hilbertspace.osc_subsys_list

    @property
    def qbt_subsys_list(self) -> List[QubitBaseClass]:
        return self._hilbertspace.qbt_subsys_list

    @property
    def subsystem_count(self) -> int:
        return self._hilbertspace.subsystem_count

    @utils.check_sync_status
    def __getitem__(self, key):
        if isinstance(key, str):
            return self._data[key]

        # The following enables the pre-slicing syntax:
        # <Sweep>[p1, p2, ...].dressed_eigenstates()
        if isinstance(key, tuple):
            self._current_param_indices = convert_to_std_npindex(
                key, self._parameters)
        elif isinstance(key, (int, slice)):
            if key == slice(None) or key == slice(None, None, None):
                key = (key, ) * len(self._parameters)
            else:
                key = (key, )
            self._current_param_indices = convert_to_std_npindex(
                key, self._parameters)
        return self

    def receive(self, event: str, sender: object, **kwargs) -> None:
        """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:
            type of event being received
        sender:
            identity of sender announcing the event
        **kwargs
        """
        if self._data:
            if event == "HILBERTSPACE_UPDATE" and sender is self._hilbertspace:
                self._out_of_sync = True
            elif event == "PARAMETERSWEEP_UPDATE" and sender is self:
                self._out_of_sync = True

    @property
    def bare_specdata_list(self) -> List[SpectrumData]:
        """
        Wrap bare eigensystem data into a SpectrumData object. To be used with
        pre-slicing, e.g. `<ParameterSweep>[0, :].bare_specdata_list`

        Returns
        -------
            List of `SpectrumData` objects with bare eigensystem data, one per subsystem
        """
        multi_index = self._current_param_indices
        sweep_param_indices = self.get_sweep_indices(multi_index)
        if len(sweep_param_indices) != 1:
            raise ValueError(
                "All but one parameter must be fixed for `bare_specdata_list`."
            )
        sweep_param_name = self._parameters.name_by_index[
            sweep_param_indices[0]]
        specdata_list: List[SpectrumData] = []
        for subsys_index, subsystem in enumerate(self._hilbertspace):
            evals_swp = self["bare_evals"][subsys_index][multi_index]
            evecs_swp = self["bare_evecs"][subsys_index][multi_index]
            specdata_list.append(
                SpectrumData(
                    energy_table=evals_swp.toarray(),
                    state_table=evecs_swp.toarray(),
                    system_params=self._hilbertspace.get_initdata(),
                    param_name=sweep_param_name,
                    param_vals=self._parameters[sweep_param_name],
                ))
        self._current_param_indices = None
        return specdata_list

    @property
    def dressed_specdata(self) -> "SpectrumData":
        """
        Wrap dressed eigensystem data into a SpectrumData object. To be used with
        pre-slicing, e.g. `<ParameterSweep>[0, :].dressed_specdata`

        Returns
        -------
            `SpectrumData` object with bare eigensystem data
        """
        multi_index = self._current_param_indices
        sweep_param_indices = self.get_sweep_indices(multi_index)
        if len(sweep_param_indices) != 1:
            raise ValueError(
                "All but one parameter must be fixed for `dressed_specdata`.")
        sweep_param_name = self._parameters.name_by_index[
            sweep_param_indices[0]]

        specdata = SpectrumData(
            energy_table=self["evals"][multi_index].toarray(),
            state_table=self["evecs"][multi_index].toarray(),
            system_params=self._hilbertspace.get_initdata(),
            param_name=sweep_param_name,
            param_vals=self._parameters[sweep_param_name],
        )
        self._current_param_indices = None
        return specdata

    def get_sweep_indices(self, multi_index: GIndexTuple) -> List[int]:
        """
        For given generalized multi-index, return a list of the indices that are being
        swept.
        """
        std_multi_index = convert_to_std_npindex(multi_index, self._parameters)

        sweep_indices = [
            index for index, index_obj in enumerate(std_multi_index)
            if isinstance(
                self._parameters.paramvals_list[index][index_obj],
                (list, tuple, ndarray),
            )
        ]
        self._current_param_indices = None
        return sweep_indices

    @property
    def system_params(self) -> Dict[str, Any]:
        return self._hilbertspace.get_initdata()

    def _final_states_subsys(
            self, subsystem: QuantumSys,
            initial_tuple: Tuple[int, ...]) -> List[Tuple[int, ...]]:
        """For given initial statet of the composite quantum system, return the final
        states possible to reach by changing the energy level of the given
        `subsystem`"""
        subsys_index = self._hilbertspace.get_subsys_index(subsystem)
        final_tuples_list = []

        for level in range(subsystem.truncated_dim):
            final_state = list(initial_tuple)
            final_state[subsys_index] = level
            final_tuples_list.append(tuple(final_state))
        final_tuples_list.remove(initial_tuple)
        return final_tuples_list

    def _get_final_states(
        self,
        initial_state: Tuple[int],
        subsys_list: List[QuantumSys],
        final: Union[Tuple[int, ...], None],
        sidebands: bool,
    ) -> List[Tuple[int, ...]]:
        """Construct and return the possible final states as a list, based on the
        provided initial state, a list of active subsystems and flag for whether to
        include sideband transitions."""
        if final:
            return [final]

        if not sidebands:
            final_state_list = []
            for subsys in subsys_list:
                final_state_list += self._final_states_subsys(
                    subsys, initial_state)
            return final_state_list

        range_list = [range(dim) for dim in self._hilbertspace.subsystem_dims]
        for subsys_index, subsys in enumerate(self._hilbertspace):
            if subsys not in subsys_list:
                range_list[subsys_index] = [initial_state[subsys_index]]
        final_state_list = list(itertools.product(*range_list))
        return final_state_list

    def _complete_state(
        self,
        partial_state: Union[List[int], Tuple[int]],
        subsys_list: List[QuantumSys],
    ) -> List[int]:
        """A partial state only includes entries for active subsystems. Complete this
        state by inserting 0 entries for all inactive subsystems."""
        state_full = [0] * len(self._hilbertspace)
        for entry, subsys in zip(partial_state, subsys_list):
            subsys_index = self.get_subsys_index(subsys)
            state_full[subsys_index] = entry
        return state_full

    def transitions(
        self,
        subsystems: Optional[Union[QuantumSys, List[QuantumSys]]] = None,
        initial: Optional[Union[int, Tuple[int, ...]]] = None,
        final: Optional[Tuple[int, ...]] = None,
        sidebands: bool = False,
        make_positive: bool = False,
        as_specdata: bool = False,
        param_indices: Optional[NpIndices] = None,
    ) -> Union[Tuple[List[Tuple[int, ...]], List[NamedSlotsNdarray]],
               SpectrumData]:
        """
        Use dressed eigenenergy data and lookup based on bare product state labels to
        extract transition energy data. Usage is based on preslicing to select all or
        a subset of parameters to be involved in the sweep, e.g.,

        `<ParameterSweep>[0, :, 2].transitions()`

        produces all eigenenergy differences for transitions starting in the ground
        state (default when no initial state is specified) as a function of the middle
        parameter while parameters 1 and 3 are fixed by the indices 0 and 2.

        Parameters
        ----------
        subsystems:
            single subsystems or list of subsystems considered as "active" for the
            transitions to be generated; if omitted as a parameter, all subsystems
            are considered as actively participating in the transitions
        initial:
            initial state from which transitions originate, specified as a bare product
            state of either all subsystems the subset of active subsystems
            (default: ground state of the system)
        final:
            concrete final state for which the transition energy should be generated; if
            not provided, a list of allowed final states is generated
        sidebands:
            if set to true, sideband transitions with multiple subsystems changing
            excitation levels are included (default: False)
        make_positive:
            boolean option relevant if the initial state is an excited state;
            downwards transition energies would regularly be negative, but are
            converted to positive if this flag is set to True
        as_specdata:
            whether data is handed back in raw array form or wrapped into a SpectrumData
            object (default: False)
        param_indices:
            usually to be omitted, as param_indices will be set via pre-slicing

        Returns
        -------
            A tuple consisting of a list of all the transitions and a corresponding
            list of difference energies, e.g.
            ((0,0,0), (0,0,1)),    <energy array for transition 0,0,0 -> 0,0,1>.
            If as_specdata is set to True, a SpectrumData object is returned instead,
            saving transition label info in an attribute named `labels`.
        """
        param_indices = param_indices or self._current_param_indices

        if subsystems is None:
            subsys_list = self._hilbertspace.subsys_list
        elif isinstance(subsystems, (QubitBaseClass, Oscillator)):
            subsys_list = [subsystems]
        else:
            subsys_list = subsystems

        if initial is None:
            initial_state = (0, ) * len(self._hilbertspace)
        elif isinstance(initial, int):
            initial_state = (initial, )
        else:
            initial_state = initial

        if len(initial_state) not in [
                len(self._hilbertspace),
                len(subsys_list)
        ]:
            raise ValueError(
                "Initial state information provided is not compatible "
                "with the specified subsystems(s) provided.")

        if len(initial_state) < len(self._hilbertspace):
            initial_state = self._complete_initial_state(
                initial_state, subsys_list)

        final_states_list = self._get_final_states(initial_state, subsys_list,
                                                   final, sidebands)

        transitions = []
        transition_energies = []
        initial_energies = self[param_indices].energy_by_bare_index(
            initial_state)
        for final_state in final_states_list:
            final_energies = self[param_indices].energy_by_bare_index(
                final_state)
            diff_energies = (final_energies - initial_energies).astype(float)
            if make_positive:
                diff_energies = np.abs(diff_energies)
            if not np.isnan(diff_energies.toarray()).all():
                transitions.append((initial_state, final_state))
                transition_energies.append(diff_energies)

        self._current_param_indices = None

        if not as_specdata:
            return transitions, transition_energies

        reduced_parameters = self._parameters.create_sliced(param_indices)
        if len(reduced_parameters) == 1:
            name = reduced_parameters.names[0]
            vals = reduced_parameters[name]
            return SpectrumData(
                energy_table=np.asarray(transition_energies).T,
                system_params=self.system_params,
                param_name=name,
                param_vals=vals,
                labels=list(map(str, transitions)),
                subtract=np.asarray([initial_energies] * self._evals_count,
                                    dtype=float).T,
            )
        return SpectrumData(
            energy_table=np.asarray(transition_energies),
            system_params=self.system_params,
            label=list(map(str, transitions)),
        )

    def plot_transitions(
        self,
        subsystems: Optional[Union[QuantumSys, List[QuantumSys]]] = None,
        initial: Optional[Union[int, Tuple[int, ...]]] = None,
        final: Optional[Union[int, Tuple[int, ...]]] = None,
        sidebands: bool = False,
        make_positive: bool = True,
        coloring: Union[str, ndarray] = "transition",
        param_indices: Optional[NpIndices] = None,
        **kwargs,
    ) -> Tuple[Figure, Axes]:
        """
        Plot transition energies as a function of one external parameter. Usage is based
        on preslicing of the ParameterSweep object to select a single parameter to be
        involved in the sweep. E.g.,

        `<ParameterSweep>[0, :, 2].plot_transitions()`

        plots all eigenenergy differences for transitions starting in the ground
        state (default when no initial state is specified) as a function of the middle
        parameter while parameters 1 and 3 are fixed by the indices 0 and 2.

        Parameters
        ----------
        subsystems:
            single subsystems or list of subsystems considered as "active" for the
            transitions to be generated; if omitted as a parameter, all subsystems
            are considered as actively participating in the transitions
        initial:
            initial state from which transitions originate, specified as a bare product
            state of either all subsystems the subset of active subsystems
            (default: ground state of the system)
        final:
            concrete final state for which the transition energy should be generated; if
            not provided, a list of allowed final states is generated
        sidebands:
            if set to true, sideband transitions with multiple subsystems changing
            excitation levels are included (default: False)
        make_positive:
            boolean option relevant if the initial state is an excited state;
            downwards transition energies would regularly be negative, but are
            converted to positive if this flag is set to True (default: True)
        coloring:
            For `"transition"` (default), transitions are colored by their
            dispersive nature; "`plain`", curves are colored naively
        param_indices:
            usually to be omitted, as param_indices will be set via pre-slicing

        Returns
        -------
            A tuple consisting of a list of all the transitions and a corresponding
            list of difference energies, e.g.
            ((0,0,0), (0,0,1)),    <energy array for transition 0,0,0 -> 0,0,1>.
            If as_specdata is set to True, a SpectrumData object is returned instead,
            saving transition label info in an attribute named `labels`.
        """

        param_indices = param_indices or self._current_param_indices
        if len(self._parameters.create_sliced(param_indices)) > 1:
            raise ValueError(
                "Transition plots are only supported for a sweep over a "
                "single parameter. You can reduce a multidimensional "
                "sweep by pre-slicing, e.g.,  <ParameterSweep>[0, :, "
                "0].plot_transitions(...)")
        specdata = self.transitions(
            subsystems,
            initial,
            final,
            sidebands,
            make_positive,
            as_specdata=True,
            param_indices=param_indices,
        )
        specdata_all = copy.deepcopy(self[param_indices].dressed_specdata)
        specdata_all.energy_table -= specdata.subtract
        if make_positive:
            specdata_all.energy_table = np.abs(specdata_all.energy_table)
        self._current_param_indices = None  # reset from pre-slicing

        if coloring == "plain":
            return specdata_all.plot_evals_vs_paramvals()

        fig_ax = specdata_all.plot_evals_vs_paramvals(color="gainsboro",
                                                      linewidth=0.75)

        labellines_status = plot._LABELLINES_ENABLED
        plot._LABELLINES_ENABLED = False
        fig, axes = specdata.plot_evals_vs_paramvals(
            label_list=specdata.labels, fig_ax=fig_ax, **kwargs)
        plot._LABELLINES_ENABLED = labellines_status
        return fig, axes

    def add_sweep(
        self,
        sweep_function: Union[str, Callable],
        sweep_name: Optional[str] = None,
        **kwargs,
    ) -> None:
        """
        Add a new sweep to the ParameterSweep object. The generated data is
        subsequently accessible through <ParameterSweep>[<sweep_function>] or
        <ParameterSweep>[<sweep_name>]

        Parameters
        ----------
        sweep_function:
            name of a sweep function in scq.sweeps as str, or custom function (
            callable) provided by the user
        sweep_name:
            if given, the generated data is stored in <ParameterSweep>[<sweep_name>]
            rather than [<sweep_name>]
        kwargs:
            keyword arguments handed over to the sweep function

        Returns
        -------
            None
        """
        if callable(sweep_function):
            if not hasattr(sweep_function, "__name__") and not sweep_name:
                raise ValueError(
                    "Sweep function name cannot be accessed. Provide an "
                    "explicit `sweep_name` instead.")
            sweep_name = sweep_name or sweep_function.__name__
            func = sweep_function
            self._data[sweep_name] = sweeps.generator(self, func, **kwargs)
        else:
            sweep_name = sweep_name or sweep_function
            func = getattr(sweeps, sweep_function)
            self._data[sweep_name] = func(**kwargs)

    def matrix_elements(
        self,
        operator_name: str,
        sweep_name: str,
        subsystem: "QuantumSys",
    ) -> None:
        """Generate data for matrix elements with respect to a given operator, as a
        function of the sweep parameter(s)

        Parameters
        ----------
        operator_name:
            name of the operator in question
        sweep_name:
            The sweep data will be accessible as <ParameterSweep>[<sweep_name>]
        subsystem:
            subsystems for which to compute matrix elements.

        Returns
        -------
            None; results are saved as <ParameterSweep>[<sweep_name>]
        """
        def _matrix_elements(
            sweep: "ParameterSweep",
            param_indices: Tuple[int, ...],
            param_vals: Tuple[float, ...],
            operator_name: Union[str, None] = None,
            subsystem=None,
        ) -> np.ndarray:
            subsys_index = sweep.get_subsys_index(subsystem)
            bare_evecs = sweep["bare_evecs"][subsys_index][param_indices]
            return subsystem.matrixelement_table(
                operator=operator_name,
                evecs=bare_evecs,
                evals_count=subsystem.truncated_dim,
            )

        operator_func = functools.partial(
            _matrix_elements,
            sweep=self,
            operator_name=operator_name,
            subsystem=subsystem,
        )

        matrix_element_data = sweeps.generator(
            self,
            operator_func,
        )
        self._data[sweep_name] = matrix_element_data
Exemplo n.º 21
0
class FluxQubit(base.QubitBaseClass, serializers.Serializable):
    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; expected: truncated_dim > 1
    """

    EJ1 = descriptors.WatchedProperty('QUANTUMSYSTEM_UPDATE')
    EJ2 = descriptors.WatchedProperty('QUANTUMSYSTEM_UPDATE')
    EJ3 = descriptors.WatchedProperty('QUANTUMSYSTEM_UPDATE')
    ECJ1 = descriptors.WatchedProperty('QUANTUMSYSTEM_UPDATE')
    ECJ2 = descriptors.WatchedProperty('QUANTUMSYSTEM_UPDATE')
    ECJ3 = descriptors.WatchedProperty('QUANTUMSYSTEM_UPDATE')
    ECg1 = descriptors.WatchedProperty('QUANTUMSYSTEM_UPDATE')
    ECg2 = descriptors.WatchedProperty('QUANTUMSYSTEM_UPDATE')
    ng1 = descriptors.WatchedProperty('QUANTUMSYSTEM_UPDATE')
    ng2 = descriptors.WatchedProperty('QUANTUMSYSTEM_UPDATE')
    flux = descriptors.WatchedProperty('QUANTUMSYSTEM_UPDATE')
    ncut = descriptors.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 = type(self).__name__
        self._evec_dtype = np.complex_
        self._default_grid = discretization.Grid1d(
            -np.pi / 2, 3 * np.pi / 2, 100)  # for plotting in phi_j basis
        self._image_filename = os.path.join(
            os.path.dirname(os.path.abspath(__file__)),
            'qubit_pngs/fluxqubit.png')

    @staticmethod
    def default_params():
        return {
            'EJ1': 1.0,
            'EJ2': 1.0,
            'EJ3': 0.8,
            'ECJ1': 0.016,
            'ECJ2': 0.016,
            'ECJ3': 0.021,
            'ECg1': 0.83,
            'ECg2': 0.83,
            'ng1': 0.0,
            'ng2': 0.0,
            'flux': 0.4,
            'ncut': 10,
            'truncated_dim': 10
        }

    @staticmethod
    def nonfit_params():
        return ['ng1', 'ng2', 'flux', 'ncut', 'truncated_dim']

    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 = spec_utils.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 = spec_utils.standardize_phases(
            wavefunc_amplitudes)

        grid2d = discretization.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 storage.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)
Exemplo n.º 22
0
class InteractionTerm(dispatch.DispatchClient, serializers.Serializable):
    """
    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 C ..., where A, B,
    C... are Hermitian operators in subsystems in subsystem_list, 2. V = g A B C... +
    h.c., where A, B, C... may be non-Hermitian

    Parameters
    ----------
    g_strength:
        coefficient parametrizing the interaction strength.
    operator_list:
        list of tuples (subsys_index, operator)
    add_hc:
        If set to True, the interaction Hamiltonian is of type 2, and the Hermitian
        conjugate is added.
    """

    g_strength = descriptors.WatchedProperty("INTERACTIONTERM_UPDATE")
    operator_list = descriptors.WatchedProperty("INTERACTIONTERM_UPDATE")
    add_hc = descriptors.WatchedProperty("INTERACTIONTERM_UPDATE")

    def __new__(
        cls,
        *args,
        **kwargs,
    ) -> Union["InteractionTerm", InteractionTermLegacy]:
        """This takes care of legacy use of the InteractionTerm class"""
        if "subsys1" in kwargs:
            warnings.warn(
                "This use of `InteractionTerm` is deprecated and will cease "
                "to be supported in the future.",
                FutureWarning,
            )
            return InteractionTermLegacy(
                g_strength=kwargs["g_strength"],
                op1=kwargs["op1"],
                subsys1=kwargs["subsys1"],
                op2=kwargs["op2"],
                subsys2=kwargs["subsys2"],
                hilbertspace=kwargs.pop("hilbertspace", None),
                add_hc=kwargs.pop("add_hc", None),
            )
        else:
            return super().__new__(cls)

    def __init__(
        self,
        g_strength: Union[float, complex],
        operator_list: List[Tuple[int, Union[ndarray, csc_matrix]]],
        add_hc: bool = False,
    ) -> None:
        self.g_strength = g_strength
        self.operator_list = operator_list
        self.add_hc = add_hc

    def __repr__(self) -> str:
        init_dict = {name: getattr(self, name) for name in self._init_params}
        return type(self).__name__ + f"(**{init_dict!r})"

    def __str__(self) -> str:
        indent_length = 25
        name_prepend = "InteractionTerm".ljust(indent_length, "-") + "|\n"

        output = ""
        for param_name in self._init_params:
            param_content = getattr(self, param_name).__repr__()
            param_content = param_content.strip("\n")
            if len(param_content) > 50:
                param_content = param_content[:50]
                param_content += " ..."

            output += "{0}| {1}: {2}\n".format(" " * indent_length,
                                               str(param_name), param_content)
        return name_prepend + output

    def hamiltonian(
        self,
        subsystem_list: List[QuantumSys],
        bare_esys: Optional[Dict[int, ndarray]] = None,
    ) -> Qobj:
        """
        Returns the full Hamiltonian of the interacting quantum system described by the
        HilbertSpace object

        Parameters
        ----------
        subsystem_list:
            list of all quantum systems in HilbertSpace calling ``hamiltonian``,
            needed for identity wrapping
        bare_esys:
            optionally, the bare eigensystems for each subsystem can be provided to
            speed up computation; these are provided in dict form via <subsys>: esys)

        Returns
        -------
            Hamiltonian in `qutip.Qobj` format
        """
        hamiltonian = self.g_strength
        id_wrapped_ops = self.id_wrap_all_ops(self.operator_list,
                                              subsystem_list,
                                              bare_esys=bare_esys)
        for op in id_wrapped_ops:
            hamiltonian *= op
        if self.add_hc:
            hamiltonian += hamiltonian.dag()
        return hamiltonian

    @staticmethod
    def id_wrap_all_ops(
        operator_list: List[Tuple[int, Union[ndarray, csc_matrix]]],
        subsystem_list: List[QuantumSys],
        bare_esys: Optional[Dict[int, ndarray]] = None,
    ) -> list:
        id_wrapped_operators = []
        for subsys_index, operator in operator_list:
            if bare_esys is not None and subsys_index in bare_esys:
                evecs = bare_esys[subsys_index][1]
            else:
                evecs = None
            id_wrapped_operators.append(
                spec_utils.identity_wrap(operator,
                                         subsystem_list[subsys_index],
                                         subsystem_list,
                                         evecs=evecs))
        return id_wrapped_operators
Exemplo n.º 23
0
class ParameterSweep(ParameterSweepBase, dispatch.DispatchClient,
                     serializers.Serializable):
    """
    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:
        name of external parameter to be varied
    param_vals:
        array of parameter values
    evals_count:
        number of eigenvalues and eigenstates to be calculated for the composite Hilbert space
    hilbertspace:
        collects all data specifying the Hilbert space of interest
    subsys_update_list:
        list of subsys_list in the Hilbert space which get modified when the external parameter changes
    update_hilbertspace:
        update_hilbertspace(param_val) specifies how a change in the external parameter affects
        the Hilbert space components
    num_cpus:
        number of CPUS requested for computing the sweep (default value settings.NUM_CPUS)
    """

    param_name = descriptors.WatchedProperty("PARAMETERSWEEP_UPDATE")
    param_vals = descriptors.WatchedProperty("PARAMETERSWEEP_UPDATE")
    param_count = descriptors.WatchedProperty("PARAMETERSWEEP_UPDATE")
    evals_count = descriptors.WatchedProperty("PARAMETERSWEEP_UPDATE")
    subsys_update_list = descriptors.WatchedProperty("PARAMETERSWEEP_UPDATE")
    update_hilbertspace = descriptors.WatchedProperty("PARAMETERSWEEP_UPDATE")
    lookup = descriptors.ReadOnlyProperty()

    def __init__(
        self,
        param_name: str,
        param_vals: ndarray,
        evals_count: int,
        hilbertspace: HilbertSpace,
        subsys_update_list: List[QuantumSys],
        update_hilbertspace: Callable,
        num_cpus: int = settings.NUM_CPUS,
    ) -> None:
        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: Union[SpectrumLookup, None] = None
        self._bare_hamiltonian_constant: Qobj

        self.tqdm_disabled = settings.PROGRESSBAR_DISABLED or (num_cpus > 1)

        dispatch.CENTRAL_DISPATCH.register("PARAMETERSWEEP_UPDATE", self)
        dispatch.CENTRAL_DISPATCH.register("HILBERTSPACE_UPDATE", self)

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

    def run(self) -> None:
        """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 = spec_lookup.SpectrumLookup(self, dressed_specdata,
                                                  bare_specdata_list)
        settings.DISPATCH_ENABLED = True

    # HilbertSpace: methods for CentralDispatch ----------------------------------------------------
    def cause_dispatch(self) -> None:
        self.update_hilbertspace(self.param_vals[0])

    def receive(self, event: str, sender: object, **kwargs) -> None:
        """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:
            type of event being received
        sender:
            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')

    # ParameterSweep: file IO methods ---------------------------------------------------------------
    @classmethod
    def deserialize(cls, iodata: "IOData") -> "StoredSweep":
        """
        Take the given IOData and return an instance of the described class, initialized with the data stored in
        io_data.

        Parameters
        ----------
        iodata: IOData

        Returns
        -------
        StoredSweep
        """
        data_dict = iodata.as_kwargs()
        lookup = data_dict.pop("_lookup")
        data_dict["dressed_specdata"] = lookup._dressed_specdata
        data_dict["bare_specdata_list"] = lookup._bare_specdata_list
        new_storedsweep = StoredSweep(**data_dict)
        new_storedsweep._lookup = lookup
        return new_storedsweep

    def serialize(self) -> "IOData":
        """
        Convert the content of the current class instance into IOData format.

        Returns
        -------
        IOData
        """
        if self._lookup is None:
            raise ValueError(
                "Nothing to save - no lookup data has been generated yet.")

        initdata = {
            "param_name": self.param_name,
            "param_vals": self.param_vals,
            "evals_count": self.evals_count,
            "hilbertspace": self._hilbertspace,
            "_lookup": self._lookup,
        }
        iodata = serializers.dict_serialize(initdata)
        iodata.typename = "StoredSweep"
        return iodata

    # ParameterSweep: private methods for generating the sweep -------------------------------------------------
    def _compute_bare_specdata_sweep(self) -> List[SpectrumData]:
        """
        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 = cpu_switch.get_map_method(self.num_cpus)
        with utils.InfoBar(
                "Parallel compute bare eigensys [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.tqdm_disabled,
                    ),
                ))
        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: List[SpectrumData]) -> SpectrumData:
        """
        Calculates and returns all dressed spectral data.
        """
        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 = cpu_switch.get_map_method(self.num_cpus)

        with utils.InfoBar(
                "Parallel compute dressed eigensys [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.tqdm_disabled,
                    ),
                ))
        dressed_specdata = self._recast_dressed_eigendata(dressed_eigendata)
        del dressed_eigendata
        return dressed_specdata

    def _recast_bare_eigendata(
        self,
        static_eigendata: List[List[Tuple[ndarray, ndarray]]],
        bare_eigendata: List[List[Tuple[ndarray, ndarray]]],
    ) -> List[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(
                storage.SpectrumData(
                    energy_table,
                    system_params={},
                    param_name=self.param_name,
                    param_vals=self.param_vals,
                    state_table=state_table,
                ))
        return specdata_list

    def _recast_dressed_eigendata(
        self,
        dressed_eigendata: List[Tuple[ndarray,
                                      QutipEigenstates]]) -> 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] = np.real_if_close(dressed_eigendata[j][0])
            state_table.append(dressed_eigendata[j][1])
        specdata = storage.SpectrumData(
            energy_table,
            system_params={},
            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: List[SpectrumData]) -> Qobj:
        """
        Returns
        -------
            composite Hamiltonian composed of bare Hamiltonians of subsys_list 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: List[SpectrumData],
            param_index: int) -> Qobj:
        """
        Parameters
        ----------
        param_index:
            position index of current value of the external parameter

        Returns
        -------
            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) -> List[Tuple[ndarray, ndarray]]:
        """
        Returns
        -------
            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)  # type: ignore
        return eigendata

    def _compute_bare_spectrum_varying(
            self, param_val: float) -> List[Tuple[ndarray, ndarray]]:
        """
        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()

        Returns
        -------
            (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.get_subsys_index(subsys)
                eigendata.append(self._hilbertspace[subsys_index].eigensys(
                    evals_count=evals_count))
            else:
                eigendata.append(None)  # type: ignore
        return eigendata

    def _compute_dressed_eigensystem(
        self, param_index: int, bare_specdata_list: List[SpectrumData]
    ) -> Tuple[ndarray, QutipEigenstates]:
        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)
        evals, evecs = hamiltonian.eigenstates(eigvals=self.evals_count)
        evecs = evecs.view(qutip_serializer.QutipEigenstates)
        return evals, evecs

    def _lookup_bare_eigenstates(
        self,
        param_index: int,
        subsys: QuantumSys,
        bare_specdata_list: List[SpectrumData],
    ) -> ndarray:
        """
        Parameters
        ----------
        param_index:
            position index of parameter value in question
        subsys:
            Hilbert space subsystem for which bare eigendata is to be looked up
        bare_specdata_list:
            may be provided during partial generation of the lookup

        Returns
        -------
            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]  # type: ignore
Exemplo n.º 24
0
class InteractionTermStr(dispatch.DispatchClient, serializers.Serializable):
    """
    Class for specifying a term in the interaction Hamiltonian of a composite Hilbert
    space, and constructing the Hamiltonian in qutip.Qobj format. The form of the
    interaction is defined using the expr string. Each operator must be
    hermitian, unless add_hc = True in which case each operator my be non-hermitian.
    Acceptable functions inside of expr string include: cos(), sin(),
    dag(), conj(), exp(), sqrt(), trans(), cosm(), sinm(), expm(), and sqrtm() along
    with other operators allowed in Python expressions.

    Parameters
    ----------
    expr:
        string that defines the interaction.
    operator_list:
        list of tuples of operator names, operators, and subsystem indices
        eg. {name: (operator, subsystem)}.
    add_hc:
        If set to True, the interaction Hamiltonian is of type 2, and the Hermitian
        conjugate is added.

    """

    expr = descriptors.WatchedProperty("INTERACTIONTERM_UPDATE")
    operator_list = descriptors.WatchedProperty("INTERACTIONTERM_UPDATE")
    add_hc = descriptors.WatchedProperty("INTERACTIONTERM_UPDATE")

    def __init__(
        self,
        expr: str,
        operator_list: List[Tuple[int, str, Union[ndarray, csc_matrix,
                                                  dia_matrix]]],
        const: Optional[Dict[str, Union[float, complex]]] = None,
        add_hc: bool = False,
    ) -> None:
        self.qutip_dict = {
            "cosm(": "Qobj.cosm(",
            "expm(": "Qobj.expm(",
            "sinm(": "Qobj.sinm(",
            "sqrtm(": "Qobj.sqrtm(",
            "cos(": "Qobj.cosm(",
            "exp(": "Qobj.expm(",
            "sin(": "Qobj.sinm(",
            "sqrt(": "Qobj.sqrtm(",
        }
        self.expr = expr
        self.operator_list = operator_list
        self.const = const or {}
        self.add_hc = add_hc

    def __repr__(self) -> str:
        init_dict = {name: getattr(self, name) for name in self._init_params}
        return type(self).__name__ + f"(**{init_dict!r})"

    def __str__(self) -> str:
        indent_length = 25
        name_prepend = "InteractionTermStr".ljust(indent_length, "-") + "|\n"

        output = ""
        for param_name in self._init_params:
            param_content = getattr(self, param_name).__repr__()
            param_content = param_content.strip("\n")
            if len(param_content) > 50:
                param_content = param_content[:50]
                param_content += " ..."

            output += "{0}| {1}: {2}\n".format(" " * indent_length,
                                               str(param_name), param_content)
        return name_prepend + output

    def parse_qutip_functions(self, string: str) -> str:
        for item, value in self.qutip_dict.items():
            if item in string:
                string = string.replace(item, value)
        return string

    def run_string_code(self, expression: str,
                        idwrapped_ops_by_name: Dict[str, Qobj]) -> Qobj:
        expression = self.parse_qutip_functions(expression)
        idwrapped_ops_by_name["Qobj"] = Qobj

        main = importlib.import_module("__main__")
        answer = eval(expression, {
            **main.__dict__,
            **idwrapped_ops_by_name,
            **self.const
        })
        return answer

    def id_wrap_all_ops(
        self,
        subsys_list: List[QuantumSys],
        bare_esys: Optional[Dict[int, ndarray]] = None,
    ) -> Dict[str, Qobj]:
        idwrapped_ops_by_name = {}
        for subsys_index, name, op in self.operator_list:
            if bare_esys and subsys_index in bare_esys:
                evecs = bare_esys[subsys_index][1]
            else:
                evecs = None
            idwrapped_ops_by_name[name] = spec_utils.identity_wrap(
                op, subsys_list[subsys_index], subsys_list, evecs=evecs)
        return idwrapped_ops_by_name

    def hamiltonian(
        self,
        subsystem_list: List[QuantumSys],
        bare_esys: Optional[Dict[int, ndarray]] = None,
    ) -> Qobj:
        """
        Parameters
        ----------
        subsystem_list:
            list of all quantum systems in HilbertSpace calling ``hamiltonian``,
            needed for identity wrapping
        bare_esys:
            optionally, the bare eigensystems for each subsystem can be provided to
            speed up computation; these are provided in dict form via <subsys>: esys)
        """
        idwrapped_ops_by_name = self.id_wrap_all_ops(subsystem_list,
                                                     bare_esys=bare_esys)
        hamiltonian = self.run_string_code(self.expr, idwrapped_ops_by_name)
        if not self.add_hc:
            return hamiltonian
        else:
            return hamiltonian + hamiltonian.dag()
Exemplo n.º 25
0
class Cos2PhiQubit(base.QubitBaseClass, serializers.Serializable,
                   NoisyCos2PhiQubit):
    r"""Cosine Two Phi Qubit

    | [1] Smith et al., NPJ Quantum Inf. 6, 8 (2020) http://www.nature.com/articles/s41534-019-0231-2

    .. math::

        H = & \,2 E_\text{CJ}'n_\phi^2 + 2 E_\text{CJ}' (n_\theta - n_\text{g} - n_\zeta)^2 + 4 E_\text{C} n_\zeta^2\\
        & + E_\text{L}'(\phi - \pi\Phi_\text{ext}/\Phi_0)^2 + E_\text{L}' \zeta^2 - 2 E_\text{J}\cos{\theta}\cos{\phi} \\
        & + 2 dE_\text{J} E_\text{J}\sin{\theta}\sin{\phi} \\
        & - 4 dC_\text{J} E_\text{CJ}' n_\phi (n_\theta - n_\text{g}-n_\zeta) \\
        & + dL E_\text{L}'(2\phi - \varphi_\text{ext})\zeta ,

    where :math:`E_\text{CJ}' = E_\text{CJ} / (1 - dC_\text{J})^2` and
    :math:`E_\text{L}' = E_\text{L} / (1 - dL)^2`.

    Parameters
    ----------
    EJ:
        Josephson energy of the two junctions
    ECJ:
        charging energy of the two junctions
    EL:
        inductive energy of the two inductors
    EC:
        charging energy of the shunt capacitor
    dCJ:
        disorder in junction charging energy
    dL:
        disorder in inductive energy
    dEJ:
        disorder in junction energy
    flux:
        external magnetic flux in angular units, 1 corresponds to one flux
        quantum
    ng:
        offset charge
    ncut:
        cutoff of charge basis, -ncut <= :math:`n_\theta` <= ncut
    zeta_cut:
        number of harmonic oscillator basis for :math:`\zeta` variable
    phi_cut:
        number of harmonic oscillator basis for :math:`\phi` variable
    truncated_dim:
        desired dimension of the truncated quantum system; expected: truncated_dim > 1
    """
    EJ = descriptors.WatchedProperty("QUANTUMSYSTEM_UPDATE")
    ECJ = descriptors.WatchedProperty("QUANTUMSYSTEM_UPDATE")
    EL = descriptors.WatchedProperty("QUANTUMSYSTEM_UPDATE")
    EC = descriptors.WatchedProperty("QUANTUMSYSTEM_UPDATE")
    dCJ = descriptors.WatchedProperty("QUANTUMSYSTEM_UPDATE")
    dL = descriptors.WatchedProperty("QUANTUMSYSTEM_UPDATE")
    dEJ = descriptors.WatchedProperty("QUANTUMSYSTEM_UPDATE")
    flux = descriptors.WatchedProperty("QUANTUMSYSTEM_UPDATE")
    ng = descriptors.WatchedProperty("QUANTUMSYSTEM_UPDATE")
    ncut = descriptors.WatchedProperty("QUANTUMSYSTEM_UPDATE")
    zeta_cut = descriptors.WatchedProperty("QUANTUMSYSTEM_UPDATE")
    phi_cut = descriptors.WatchedProperty("QUANTUMSYSTEM_UPDATE")

    def __init__(
        self,
        EJ: float,
        ECJ: float,
        EL: float,
        EC: float,
        dL: float,
        dCJ: float,
        dEJ: float,
        flux: float,
        ng: float,
        ncut: int,
        zeta_cut: int,
        phi_cut: int,
        truncated_dim: int = 6,
    ) -> None:
        self.EJ = EJ
        self.ECJ = ECJ
        self.EL = EL
        self.EC = EC
        self.dL = dL
        self.dCJ = dCJ
        self.dEJ = dEJ
        self.flux = flux
        self.ng = ng
        self.ncut = ncut
        self.zeta_cut = zeta_cut
        self.phi_cut = phi_cut
        self.truncated_dim = truncated_dim
        self._sys_type = type(self).__name__
        self._evec_dtype = np.float_
        self._default_phi_grid = discretization.Grid1d(-4 * np.pi, 4 * np.pi,
                                                       100)
        self._default_zeta_grid = discretization.Grid1d(
            -4 * np.pi, 4 * np.pi, 100)
        self._default_theta_grid = discretization.Grid1d(
            -0.5 * np.pi, 1.5 * np.pi, 100)
        self._image_filename = os.path.join(
            os.path.dirname(os.path.abspath(__file__)),
            "qubit_img/cos2phi-qubit.jpg",
        )

    @staticmethod
    def default_params() -> Dict[str, Any]:
        return {
            "EJ": 15.0,
            "ECJ": 2.0,
            "EL": 1.0,
            "EC": 0.04,
            "dCJ": 0.0,
            "dL": 0.6,
            "dEJ": 0.0,
            "flux": 0.5,
            "ng": 0.0,
            "ncut": 7,
            "zeta_cut": 30,
            "phi_cut": 7,
        }

    @classmethod
    def create(cls) -> "Cos2PhiQubit":
        init_params = cls.default_params()
        cosinetwophiqubit = cls(**init_params)
        cosinetwophiqubit.widget()
        return cosinetwophiqubit

    def supported_noise_channels(self) -> List[str]:
        """Return a list of supported noise channels"""
        return [
            "tphi_1_over_f_cc",
            "tphi_1_over_f_flux",
            "tphi_1_over_f_ng",
            "t1_capacitive",
            "t1_inductive",
            "t1_purcell",
        ]

    def _dim_phi(self) -> int:
        """
        Returns Hilbert space dimension of :math:`\\phi` degree of freedom"""
        return self.phi_cut

    def _dim_zeta(self) -> int:
        """
        Returns Hilbert space dimension of :math:`\\zeta` degree of freedom"""
        return self.zeta_cut

    def _dim_theta(self) -> int:
        """
        Returns Hilbert space dimension of :math:`\\theta` degree of freedom"""
        return 2 * self.ncut + 1

    def hilbertdim(self) -> int:
        """
        Returns total Hilbert space dimension"""
        return self._dim_phi() * self._dim_zeta() * self._dim_theta()

    def _disordered_el(self) -> float:
        """
        Returns
        -------
            inductive energy renormalized by with disorder"""
        return self.EL / (1 - self.dL**2)

    def _disordered_ecj(self) -> float:
        """
        Returns
        -------
            junction capacitance energy renormalized by with disorder"""
        return self.ECJ / (1 - self.dCJ**2)

    def phi_osc(self) -> float:
        """
        Returns oscillator strength of :math:`\\phi` degree of freedom"""
        return (2 * self._disordered_ecj() / self._disordered_el())**0.25

    def zeta_osc(self) -> float:
        """
        Returns oscillator strength of :math:`\\zeta` degree of freedom"""
        return (4 * self.EC / self._disordered_el())**0.25

    def phi_plasma(self) -> float:
        """
        Returns plasma oscillation frequency of :math:`\\phi` degree of freedom"""
        return math.sqrt(8.0 * self._disordered_el() * self._disordered_ecj())

    def zeta_plasma(self) -> float:
        """
        Returns plasma oscillation frequency of :math:`\\zeta` degree of freedom"""
        return math.sqrt(16.0 * self.EC * self._disordered_el())

    def _phi_operator(self) -> csc_matrix:
        """
        Returns
        -------
            `phi` operator in the harmonic oscillator basis"""
        dimension = self._dim_phi()
        return ((op.creation_sparse(dimension) +
                 op.annihilation_sparse(dimension)) * self.phi_osc() /
                math.sqrt(2))

    def phi_operator(self) -> csc_matrix:
        """Returns :math:`\\phi` operator"""
        return self._kron3(self._phi_operator(), self._identity_zeta(),
                           self._identity_theta())

    def _n_phi_operator(self) -> csc_matrix:
        """
        Returns
        -------
            `n_\phi` operator in the harmonic oscillator basis"""
        dimension = self._dim_phi()
        return (1j * (op.creation_sparse(dimension) -
                      op.annihilation_sparse(dimension)) /
                (self.phi_osc() * math.sqrt(2)))

    def n_phi_operator(self) -> csc_matrix:
        """Returns :math:`n_\\phi` operator"""
        return self._kron3(self._n_phi_operator(), self._identity_zeta(),
                           self._identity_theta())

    def _zeta_operator(self) -> csc_matrix:
        """
        Returns
        -------
            `zeta` operator in the harmonic oscillator basis"""
        dimension = self._dim_zeta()
        return ((op.creation_sparse(dimension) +
                 op.annihilation_sparse(dimension)) * self.zeta_osc() /
                math.sqrt(2))

    def zeta_operator(self) -> csc_matrix:
        """Returns :math:`\\zeta` operator"""
        return self._kron3(self._identity_phi(), self._zeta_operator(),
                           self._identity_theta())

    def _n_zeta_operator(self) -> csc_matrix:
        """
        Returns
        -------
            `n_\zeta` operator in the harmonic oscillator basis"""
        dimension = self._dim_zeta()
        return (1j * (op.creation_sparse(dimension) -
                      op.annihilation_sparse(dimension)) /
                (self.zeta_osc() * math.sqrt(2)))

    def n_zeta_operator(self) -> csc_matrix:
        """Returns :math:`n_\\zeta` operator"""
        return self._kron3(self._identity_phi(), self._n_zeta_operator(),
                           self._identity_theta())

    def _exp_i_phi_operator(self) -> csc_matrix:
        """
        Returns
        -------
            `e^{i*phi}` operator in the  harmonic oscillator basis"""
        exponent = 1j * self._phi_operator()
        return sp.sparse.linalg.expm(exponent)

    def _cos_phi_operator(self) -> csc_matrix:
        """
        Returns
        -------
            `cos phi` operator in the harmonic oscillator basis"""
        cos_phi_op = 0.5 * self._exp_i_phi_operator()
        cos_phi_op += cos_phi_op.conj().T
        return cos_phi_op

    def _sin_phi_operator(self) -> csc_matrix:
        """
        Returns
        -------
            `sin phi/2` operator in the LC harmonic oscillator basis"""
        sin_phi_op = -1j * 0.5 * self._exp_i_phi_operator()
        sin_phi_op += sin_phi_op.conj().T
        return sin_phi_op

    def _n_theta_operator(self) -> csc_matrix:
        """
        Returns
        -------
            `n_theta` operator in the charge basis"""
        diag_elements = np.arange(-self.ncut, self.ncut + 1)
        return dia_matrix(
            (diag_elements, [0]),
            shape=(self._dim_theta(), self._dim_theta())).tocsc()

    def n_theta_operator(self) -> csc_matrix:
        """Returns :math:`n_\\theta` operator"""
        return self._kron3(self._identity_phi(), self._identity_zeta(),
                           self._n_theta_operator())

    def _cos_theta_operator(self) -> csc_matrix:
        """Returns operator :math:`\\cos \\theta` in the charge basis"""
        cos_op = (0.5 * sparse.dia_matrix(
            (np.ones(self._dim_theta()), [1]),
            shape=(self._dim_theta(), self._dim_theta()),
        ).tocsc())
        cos_op += (0.5 * sparse.dia_matrix(
            (np.ones(self._dim_theta()), [-1]),
            shape=(self._dim_theta(), self._dim_theta()),
        ).tocsc())
        return cos_op

    def _sin_theta_operator(self) -> csc_matrix:
        """Returns operator :math:`\\sin \\theta` in the charge basis"""
        sin_op = (0.5 * sparse.dia_matrix(
            (np.ones(self._dim_theta()), [1]),
            shape=(self._dim_theta(), self._dim_theta()),
        ).tocsc())
        sin_op -= (0.5 * sparse.dia_matrix(
            (np.ones(self._dim_theta()), [-1]),
            shape=(self._dim_theta(), self._dim_theta()),
        ).tocsc())
        return sin_op * (-1j)

    def _kron3(self, mat1, mat2, mat3) -> csc_matrix:
        """
        Returns Kronecker product of three matrices
        """
        return sparse.kron(sparse.kron(mat1, mat2), mat3)

    def _identity_phi(self) -> csc_matrix:
        """
        Returns Identity operator acting only on the :math:`\phi` Hilbert subspace.
        """
        dimension = self._dim_phi()
        return sparse.eye(dimension)

    def _identity_zeta(self) -> csc_matrix:
        """
        Returns Identity operator acting only on the :math:`\zeta` Hilbert subspace.
        """
        dimension = self._dim_zeta()
        return sparse.eye(dimension)

    def _identity_theta(self) -> csc_matrix:
        """
        Returns Identity operator acting only on the :math:`\theta` Hilbert subspace.
        """
        dimension = self._dim_theta()
        return sparse.eye(dimension)

    def total_identity(self) -> csc_matrix:
        """Returns Identity operator acting on the total Hilbert space."""
        return self._kron3(self._identity_phi(), self._identity_zeta(),
                           self._identity_theta())

    def hamiltonian(self) -> csc_matrix:
        """
        Returns Hamiltonian in basis obtained by employing harmonic basis for
        :math:`\\phi, \\zeta` and charge basis for :math:`\\theta`.
        """
        phi_osc_mat = self._kron3(
            op.number_sparse(self._dim_phi(), self.phi_plasma()),
            self._identity_zeta(),
            self._identity_theta(),
        )

        zeta_osc_mat = self._kron3(
            self._identity_phi(),
            op.number_sparse(self._dim_zeta(), self.zeta_plasma()),
            self._identity_theta(),
        )

        cross_kinetic_mat = (
            2 * self._disordered_ecj() *
            (self.n_theta_operator() - self.total_identity() * self.ng -
             self.n_zeta_operator())**2)

        phi_flux_term = self._cos_phi_operator() * np.cos(
            self.flux * np.pi) - self._sin_phi_operator() * np.sin(
                self.flux * np.pi)
        junction_mat = (-2 * self.EJ * self._kron3(
            phi_flux_term, self._identity_zeta(), self._cos_theta_operator()) +
                        2 * self.EJ * self.total_identity())

        disorder_l = (-2 * self._disordered_el() * self.dL *
                      self._kron3(self._phi_operator(), self._zeta_operator(),
                                  self._identity_theta()))

        dis_phi_flux_term = self._sin_phi_operator() * np.cos(
            self.flux * np.pi) + self._cos_phi_operator() * np.sin(
                self.flux * np.pi)
        disorder_j = (2 * self.EJ * self.dEJ *
                      self._kron3(dis_phi_flux_term, self._identity_zeta(),
                                  self._sin_theta_operator()))

        dis_c_opt = (
            self._kron3(self._n_phi_operator(), self._identity_zeta(),
                        self._n_theta_operator()) -
            self.n_phi_operator() * self.ng -
            self._kron3(self._n_phi_operator(), self._n_zeta_operator(),
                        self._identity_theta()))
        disorder_c = -4 * self._disordered_ecj() * self.dCJ * dis_c_opt

        return (phi_osc_mat + zeta_osc_mat + cross_kinetic_mat + junction_mat +
                disorder_l + disorder_j + disorder_c)

    def _evals_calc(self, evals_count) -> ndarray:
        hamiltonian_mat = self.hamiltonian()
        evals = sparse.linalg.eigsh(
            hamiltonian_mat,
            k=evals_count,
            return_eigenvectors=False,
            sigma=0.0,
            which="LM",
            v0=settings.RANDOM_ARRAY[:self.hilbertdim()],
        )
        return np.sort(evals)

    def _esys_calc(self, evals_count) -> Tuple[ndarray, ndarray]:
        hamiltonian_mat = self.hamiltonian()
        evals, evecs = sparse.linalg.eigsh(
            hamiltonian_mat,
            k=evals_count,
            return_eigenvectors=True,
            sigma=0.0,
            which="LM",
            v0=settings.RANDOM_ARRAY[:self.hilbertdim()],
        )
        evals, evecs = spec_utils.order_eigensystem(evals, evecs)
        return evals, evecs

    def potential(self, phi, zeta, theta) -> float:
        """
        Returns full potential evaluated at :math:`\\phi, \\zeta, \\theta`

        Parameters
        ----------
        phi: float or ndarray
            float value of the phase variable `phi`
        zeta: float or ndarray
            float value of the phase variable `zeta`
        theta: float or ndarray
            float value of the phase variable `theta`
        """
        return (self._disordered_el() * (phi * phi) + self._disordered_el() *
                (zeta * zeta) -
                2 * self.EJ * np.cos(theta) * np.cos(phi + np.pi * self.flux) +
                2 * self.dEJ * self.EJ * np.sin(phi + np.pi * self.flux) *
                np.sin(theta))

    def reduced_potential(self, phi, theta) -> float:
        """Returns reduced potential by setting :math:`zeta = 0`"""
        return self.potential(phi, 0, theta)

    def plot_potential(self,
                       phi_grid=None,
                       theta_grid=None,
                       contour_vals=None,
                       **kwargs) -> Tuple[Figure, Axes]:
        """
        Draw contour plot of the potential energy in :math:`\\theta, \\phi` basis, at :math:`\\zeta = 0`

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

        y_vals = theta_grid.make_linspace()
        x_vals = phi_grid.make_linspace()
        return plot.contours(x_vals,
                             y_vals,
                             self.reduced_potential,
                             contour_vals=contour_vals,
                             ylabel=r"$\theta$",
                             xlabel=r"$\phi$",
                             **kwargs)

    def wavefunction(self,
                     esys=None,
                     which=0,
                     phi_grid=None,
                     zeta_grid=None,
                     theta_grid=None) -> WaveFunctionOnGrid:
        """
        Return a 3D wave function in :math:`\\phi, \\zeta, \\theta` basis

        Parameters
        ----------
        esys: ndarray, ndarray
            eigenvalues, eigenvectors
        which: int, optional
            index of desired wave function (default value = 0)
        phi_grid: Grid1d, option
            used for setting a custom grid for phi; if None use self._default_phi_grid
        zeta_grid: Grid1d, option
            used for setting a custom grid for zeta; if None use
            self._default_zeta_grid
        theta_grid: Grid1d, option
            used for setting a custom grid for theta; if None use
            self._default_theta_grid
        """
        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_phi_grid
        zeta_grid = zeta_grid or self._default_zeta_grid
        theta_grid = theta_grid or self._default_theta_grid

        phi_basis_labels = phi_grid.make_linspace()
        zeta_basis_labels = zeta_grid.make_linspace()
        theta_basis_labels = theta_grid.make_linspace()

        wavefunc_basis_amplitudes = evecs[:, which].reshape(
            self._dim_phi(), self._dim_zeta(), self._dim_theta())
        wavefunc_amplitudes = np.zeros(
            (phi_grid.pt_count, zeta_grid.pt_count, theta_grid.pt_count),
            dtype=np.complex_,
        )
        for i in range(self._dim_phi()):
            for j in range(self._dim_zeta()):
                for k in range(self._dim_theta()):
                    n_phi, n_zeta, n_theta = i, j, k - self.ncut
                    phi_wavefunc_amplitudes = osc.harm_osc_wavefunction(
                        n_phi, phi_basis_labels, self.phi_osc())
                    zeta_wavefunc_amplitudes = osc.harm_osc_wavefunction(
                        n_zeta, zeta_basis_labels, self.zeta_osc())
                    theta_wavefunc_amplitudes = (
                        np.exp(-1j * n_theta * theta_basis_labels) /
                        (2 * np.pi)**0.5)
                    wavefunc_amplitudes += wavefunc_basis_amplitudes[
                        i, j, k] * np.tensordot(
                            np.tensordot(phi_wavefunc_amplitudes,
                                         zeta_wavefunc_amplitudes, 0),
                            theta_wavefunc_amplitudes,
                            0,
                        )

        grid3d = discretization.GridSpec(
            np.asarray([
                [phi_grid.min_val, phi_grid.max_val, phi_grid.pt_count],
                [zeta_grid.min_val, zeta_grid.max_val, zeta_grid.pt_count],
                [theta_grid.min_val, theta_grid.max_val, theta_grid.pt_count],
            ]))
        return storage.WaveFunctionOnGrid(grid3d, wavefunc_amplitudes)

    def plot_wavefunction(self,
                          esys=None,
                          which=0,
                          phi_grid=None,
                          theta_grid=None,
                          mode="abs",
                          zero_calibrate=True,
                          **kwargs) -> Tuple[Figure, Axes]:
        """
        Plots a 2D wave function in :math:`\\theta, \\phi` basis, at :math:`\\zeta = 0`

        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, option
            used for setting a custom grid for phi; if None use self._default_phi_grid
        theta_grid: Grid1d, option
            used for setting a custom grid for theta; if None use
            self._default_theta_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

        """
        phi_grid = phi_grid or self._default_phi_grid
        zeta_grid = discretization.Grid1d(0, 0, 1)
        theta_grid = theta_grid or self._default_theta_grid

        amplitude_modifier = constants.MODE_FUNC_DICT[mode]
        wavefunc = self.wavefunction(
            esys,
            phi_grid=phi_grid,
            zeta_grid=zeta_grid,
            theta_grid=theta_grid,
            which=which,
        )

        wavefunc.gridspec = discretization.GridSpec(
            np.asarray([
                [phi_grid.min_val, phi_grid.max_val, phi_grid.pt_count],
                [theta_grid.min_val, theta_grid.max_val, theta_grid.pt_count],
            ]))
        wavefunc.amplitudes = np.transpose(
            amplitude_modifier(
                spec_utils.standardize_phases(
                    wavefunc.amplitudes.reshape(phi_grid.pt_count,
                                                theta_grid.pt_count))))
        return plot.wavefunction2d(wavefunc,
                                   zero_calibrate=zero_calibrate,
                                   ylabel=r"$\theta$",
                                   xlabel=r"$\phi$",
                                   **kwargs)

    def phi_1_operator(self) -> csc_matrix:
        """Returns operator representing the phase across inductor 1"""
        return self.zeta_operator() - self.phi_operator()

    def phi_2_operator(self) -> csc_matrix:
        """Returns operator representing the phase across inductor 2"""
        return -self.zeta_operator() - self.phi_operator()

    def n_1_operator(self) -> csc_matrix:
        """Returns operator representing the charge difference across junction 1"""
        return 0.5 * self.n_phi_operator() + 0.5 * (self.n_theta_operator() -
                                                    self.n_zeta_operator())

    def n_2_operator(self) -> csc_matrix:
        """Returns operator representing the charge difference across junction 2"""
        return 0.5 * self.n_phi_operator() - 0.5 * (self.n_theta_operator() -
                                                    self.n_zeta_operator())

    def d_hamiltonian_d_flux(self) -> csc_matrix:
        phi_flux_term = self._sin_phi_operator() * np.cos(
            self.flux * np.pi) + self._cos_phi_operator() * np.sin(
                self.flux * np.pi)
        junction_mat = (2 * self.EJ *
                        self._kron3(phi_flux_term, self._identity_zeta(),
                                    self._cos_theta_operator()) * np.pi)

        dis_phi_flux_term = self._cos_phi_operator() * np.cos(
            self.flux * np.pi) - self._sin_phi_operator() * np.sin(
                self.flux * np.pi)
        dis_junction_mat = (
            2 * self.dEJ * self.EJ *
            self._kron3(dis_phi_flux_term, self._identity_zeta(),
                        self._sin_theta_operator()) * np.pi)
        return junction_mat + dis_junction_mat

    def d_hamiltonian_d_EJ(self) -> csc_matrix:
        phi_flux_term = self._cos_phi_operator() * np.cos(
            self.flux * np.pi) - self._sin_phi_operator() * np.sin(
                self.flux * np.pi)
        junction_mat = -2 * self._kron3(phi_flux_term, self._identity_zeta(),
                                        self._cos_theta_operator())

        dis_phi_flux_term = self._sin_phi_operator() * np.cos(
            self.flux * np.pi) + self._cos_phi_operator() * np.sin(
                self.flux * np.pi)
        dis_junction_mat = (
            2 * self.dEJ *
            self._kron3(dis_phi_flux_term, self._identity_zeta(),
                        self._sin_theta_operator()))
        return junction_mat + dis_junction_mat

    def d_hamiltonian_d_ng(self) -> csc_matrix:
        return (4 * self.dCJ * self._disordered_ecj() * self.n_phi_operator() -
                4 * self._disordered_ecj() *
                (self.n_theta_operator() - self.ng - self.n_zeta_operator()))
Exemplo n.º 26
0
class HilbertSpace(dispatch.DispatchClient, serializers.Serializable):
    """Class holding information about the full Hilbert space, usually composed of
    multiple subsys_list. 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 = descriptors.ReadOnlyProperty()
    qbt_subsys_list = descriptors.ReadOnlyProperty()
    lookup = descriptors.ReadOnlyProperty()
    interaction_list = descriptors.WatchedProperty("INTERACTIONLIST_UPDATE")

    def __init__(
        self,
        subsystem_list: List[QuantumSys],
        interaction_list: List[InteractionTerm] = None,
    ) -> None:
        self._subsystems: Tuple[QuantumSys, ...] = tuple(subsystem_list)
        self.subsys_list = subsystem_list
        if interaction_list:
            self.interaction_list = tuple(interaction_list)
        else:
            self.interaction_list = []

        self._lookup: Optional[spec_lookup.SpectrumLookup] = None
        self._osc_subsys_list = [
            subsys for subsys in self if isinstance(subsys, osc.Oscillator)
        ]
        self._qbt_subsys_list = [
            subsys for subsys in self
            if not isinstance(subsys, osc.Oscillator)
        ]

        dispatch.CENTRAL_DISPATCH.register("QUANTUMSYSTEM_UPDATE", self)
        dispatch.CENTRAL_DISPATCH.register("INTERACTIONTERM_UPDATE", self)
        dispatch.CENTRAL_DISPATCH.register("INTERACTIONLIST_UPDATE", self)

    def __getitem__(self, index: int) -> QuantumSys:
        return self._subsystems[index]

    def __iter__(self) -> Iterator[QuantumSys]:
        return iter(self._subsystems)

    def __repr__(self) -> str:
        init_dict = self.get_initdata()
        return type(self).__name__ + f"(**{init_dict!r})"

    def __str__(self) -> str:
        output = "HilbertSpace:  subsystems\n"
        output += "-------------------------\n"
        for subsystem in self:
            output += "\n" + str(subsystem) + "\n"
        if self.interaction_list:
            output += "\n\n"
            output += "HilbertSpace:  interaction terms\n"
            output += "--------------------------------\n"
            for interaction_term in self.interaction_list:
                output += "\n" + str(interaction_term) + "\n"
        return output

    def __len__(self):
        return len(self._subsystems)

    ###################################################################################
    # HilbertSpace: file IO methods
    ###################################################################################
    @classmethod
    def deserialize(cls, io_data: "IOData") -> "HilbertSpace":
        """
        Take the given IOData and return an instance of the described class,
        initialized with the data stored in io_data.
        """
        alldata_dict = io_data.as_kwargs()
        lookup = alldata_dict.pop("_lookup", None)
        new_hilbertspace = cls(**alldata_dict)
        new_hilbertspace._lookup = lookup
        if lookup is not None:
            new_hilbertspace._lookup._hilbertspace = weakref.proxy(
                new_hilbertspace)
        return new_hilbertspace

    def serialize(self) -> "IOData":
        """
        Convert the content of the current class instance into IOData format.
        """
        initdata = {name: getattr(self, name) for name in self._init_params}
        initdata["_lookup"] = self._lookup
        iodata = serializers.dict_serialize(initdata)
        iodata.typename = type(self).__name__
        return iodata

    def get_initdata(self) -> Dict[str, Any]:
        """Returns dict appropriate for creating/initializing a new HilbertSpace
        object."""
        return {
            "subsystem_list": self._subsystems,
            "interaction_list": self.interaction_list,
        }

    ###################################################################################
    # HilbertSpace: creation via GUI
    ###################################################################################
    @classmethod
    def create(cls) -> "HilbertSpace":
        hilbertspace = cls([])
        scqubits.ui.hspace_widget.create_hilbertspace_widget(
            hilbertspace.__init__)  # type: ignore
        return hilbertspace

    ###################################################################################
    # HilbertSpace: methods for CentralDispatch
    ###################################################################################
    def receive(self, event: str, sender: Any, **kwargs) -> None:
        if event == "QUANTUMSYSTEM_UPDATE" and sender in self:
            self.broadcast("HILBERTSPACE_UPDATE")
            if self._lookup:
                self._lookup._out_of_sync = True
        elif event == "INTERACTIONTERM_UPDATE" and sender in self.interaction_list:
            self.broadcast("HILBERTSPACE_UPDATE")
            if self._lookup:
                self._lookup._out_of_sync = True
        elif event == "INTERACTIONLIST_UPDATE" and sender is self:
            self.broadcast("HILBERTSPACE_UPDATE")
            if self._lookup:
                self._lookup._out_of_sync = True

    ###################################################################################
    # HilbertSpace: subsystems, dimensions, etc.
    ###################################################################################
    def get_subsys_index(self, subsys: QuantumSys) -> int:
        """
        Return the index of the given subsystem in the HilbertSpace.
        """
        return self._subsystems.index(subsys)

    @property
    def subsystem_list(self) -> Tuple[QuantumSys, ...]:
        return self._subsystems

    @property
    def subsystem_dims(self) -> List[int]:
        """Returns list of the Hilbert space dimensions of each subsystem"""
        return [subsystem.truncated_dim for subsystem in self]

    @property
    def dimension(self) -> int:
        """Returns total dimension of joint Hilbert space"""
        return np.prod(np.asarray(self.subsystem_dims))

    @property
    def subsystem_count(self) -> int:
        """Returns number of subsys_list composing the joint Hilbert space"""
        return len(self._subsystems)

    ###################################################################################
    # HilbertSpace: generate SpectrumLookup
    ###################################################################################
    def generate_lookup(self) -> None:
        bare_specdata_list = []
        for index, subsys in enumerate(self):
            evals, evecs = subsys.eigensys(evals_count=subsys.truncated_dim)
            bare_specdata_list.append(
                storage.SpectrumData(
                    energy_table=[evals],
                    state_table=[evecs],
                    system_params=subsys.get_initdata(),
                ))

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

    ###################################################################################
    # HilbertSpace: energy spectrum
    ##################################################################################
    def eigenvals(
        self,
        evals_count: int = 6,
        bare_esys: Optional[Dict[int, ndarray]] = None,
    ) -> ndarray:
        """Calculates eigenvalues of the full Hamiltonian using
        `qutip.Qob.eigenenergies()`.

        Parameters
        ----------
        evals_count:
            number of desired eigenvalues/eigenstates
        bare_esys:
            optionally, the bare eigensystems for each subsystem can be provided to
            speed up computation; these are provided in dict form via <subsys>: esys
        """
        hamiltonian_mat = self.hamiltonian(bare_esys=bare_esys)
        return hamiltonian_mat.eigenenergies(eigvals=evals_count)

    def eigensys(
        self,
        evals_count: int = 6,
        bare_esys: Optional[Dict[int, ndarray]] = None,
    ) -> Tuple[ndarray, QutipEigenstates]:
        """Calculates eigenvalues and eigenvectors of the full Hamiltonian using
        `qutip.Qob.eigenstates()`.

        Parameters
        ----------
        evals_count:
            number of desired eigenvalues/eigenstates
        bare_esys:
            optionally, the bare eigensystems for each subsystem can be provided to
            speed up computation; these are provided in dict form via <subsys>: esys

        Returns
        -------
            eigenvalues and eigenvectors
        """
        hamiltonian_mat = self.hamiltonian(bare_esys=bare_esys)
        evals, evecs = hamiltonian_mat.eigenstates(eigvals=evals_count)
        evecs = evecs.view(scqubits.io_utils.fileio_qutip.QutipEigenstates)
        return evals, evecs

    def _esys_for_paramval(
        self,
        paramval: float,
        update_hilbertspace: Callable,
        evals_count: int,
        bare_esys: Optional[Dict[int, ndarray]] = None,
    ) -> Tuple[ndarray, QutipEigenstates]:
        update_hilbertspace(paramval)
        return self.eigensys(evals_count, bare_esys=bare_esys)

    def _evals_for_paramval(
        self,
        paramval: float,
        update_hilbertspace: Callable,
        evals_count: int,
        bare_esys: Optional[Dict[int, ndarray]] = None,
    ) -> ndarray:
        update_hilbertspace(paramval)
        return self.eigenvals(evals_count, bare_esys=bare_esys)

    ###################################################################################
    # HilbertSpace: Hamiltonian (bare, interaction, full)
    #######################################################

    def hamiltonian(
        self,
        bare_esys: Optional[Dict[int, ndarray]] = None,
    ) -> Qobj:
        """
        Parameters
        ----------
        bare_esys:
            optionally, the bare eigensystems for each subsystem can be provided to
            speed up computation; these are provided in dict form via <subsys>: esys

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

    def bare_hamiltonian(self,
                         bare_esys: Optional[Dict[int,
                                                  ndarray]] = None) -> Qobj:
        """
        Parameters
        ----------
        bare_esys:
            optionally, the bare eigensystems for each subsystem can be provided to
            speed up computation; these are provided in dict form via <subsys>: esys

        Returns
        -------
            composite Hamiltonian composed of bare Hamiltonians of subsys_list
            independent of the external parameter
        """
        bare_hamiltonian = 0
        for subsys_index, subsys in enumerate(self):
            if bare_esys is not None and subsys_index in bare_esys:
                evals = bare_esys[subsys_index][0]
            else:
                evals = subsys.eigenvals(evals_count=subsys.truncated_dim)
            bare_hamiltonian += self.diag_hamiltonian(subsys, evals)
        return bare_hamiltonian

    def interaction_hamiltonian(self,
                                bare_esys: Optional[Dict[int, ndarray]] = None
                                ) -> Qobj:
        """
        Returns the interaction Hamiltonian, based on the interaction terms specified
        for the current HilbertSpace object

        Parameters
        ----------
        bare_esys:
            optionally, the bare eigensystems for each subsystem can be provided to
            speed up computation; these are provided in dict form via <subsys>: esys

        Returns
        -------
            interaction Hamiltonian
        """
        if not self.interaction_list:
            return 0

        operator_list = []
        for term in self.interaction_list:
            if isinstance(term, Qobj):
                operator_list.append(term)
            elif isinstance(term, (InteractionTerm, InteractionTermStr)):
                operator_list.append(
                    term.hamiltonian(self.subsys_list, bare_esys=bare_esys))
            # The following is to support the legacy version of InteractionTerm
            elif isinstance(term, InteractionTermLegacy):
                if bare_esys is not None:
                    subsys_index1 = self.get_subsys_index(term.subsys1)
                    subsys_index2 = self.get_subsys_index(term.subsys2)
                    if subsys_index1 in bare_esys:
                        evecs1 = bare_esys[subsys_index1][1]
                    if subsys_index2 in bare_esys:
                        evecs2 = bare_esys[subsys_index2][1]
                else:
                    evecs1 = evecs2 = None
                interactionlegacy_hamiltonian = self.interactionterm_hamiltonian(
                    term, evecs1=evecs1, evecs2=evecs2)
                operator_list.append(interactionlegacy_hamiltonian)
            else:
                raise TypeError(
                    "Expected an instance of InteractionTerm, InteractionTermStr, "
                    "or Qobj; got {} instead.".format(type(term)))
        hamiltonian = sum(operator_list)
        return hamiltonian

    def interactionterm_hamiltonian(
        self,
        interactionterm: InteractionTermLegacy,
        evecs1: Optional[ndarray] = None,
        evecs2: Optional[ndarray] = None,
    ) -> Qobj:
        """Deprecated, will not work in future versions."""
        interaction_op1 = spec_utils.identity_wrap(interactionterm.op1,
                                                   interactionterm.subsys1,
                                                   self.subsys_list,
                                                   evecs=evecs1)
        interaction_op2 = spec_utils.identity_wrap(interactionterm.op2,
                                                   interactionterm.subsys2,
                                                   self.subsys_list,
                                                   evecs=evecs2)
        hamiltonian = interactionterm.g_strength * interaction_op1 * interaction_op2
        if interactionterm.add_hc:
            return hamiltonian + hamiltonian.dag()
        return hamiltonian

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

        Parameters
        ----------
        subsystem:
            Subsystem for which the Hamiltonian is to be provided.
        evals:
            Eigenenergies can be provided as `evals`; otherwise, they are calculated.
        """
        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 spec_utils.identity_wrap(diag_qt_op, subsystem,
                                        self.subsys_list)

    ###################################################################################
    # HilbertSpace: identity wrapping, operators
    ###################################################################################

    def diag_operator(self, diag_elements: ndarray,
                      subsystem: QuantumSys) -> Qobj:
        """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 subsys_list).

        Parameters
        ----------
        diag_elements:
            diagonal elements of subsystem diagonal operator
        subsystem:
            subsystem where diagonal operator is defined
        """
        dim = subsystem.truncated_dim
        index = range(dim)
        diag_matrix = np.zeros((dim, dim), dtype=np.float_)
        diag_matrix[index, index] = diag_elements
        return spec_utils.identity_wrap(diag_matrix, subsystem,
                                        self.subsys_list)

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

        Parameters
        ----------
        j,k:
            eigenstate indices for Hubbard operator
        subsystem:
            subsystem in which Hubbard operator acts
        """
        dim = subsystem.truncated_dim
        operator = qt.states.basis(dim, j) * qt.states.basis(dim, k).dag()
        return spec_utils.identity_wrap(operator, subsystem, self.subsys_list)

    def annihilate(self, subsystem: QuantumSys) -> Qobj:
        """Annihilation operator a for `subsystem`

        Parameters
        ----------
        subsystem:
            specifies subsystem in which annihilation operator acts
        """
        dim = subsystem.truncated_dim
        operator = qt.destroy(dim)
        return spec_utils.identity_wrap(operator, subsystem, self.subsys_list)

    ###################################################################################
    # HilbertSpace: spectrum sweep
    ###################################################################################
    def get_spectrum_vs_paramvals(
        self,
        param_vals: ndarray,
        update_hilbertspace: Callable,
        evals_count: int = 10,
        get_eigenstates: bool = False,
        param_name: str = "external_parameter",
        num_cpus: Optional[int] = None,
    ) -> SpectrumData:
        """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:
            array of parameter values
        update_hilbertspace:
            update_hilbertspace(param_val) specifies how a change in the external
            parameter affects the Hilbert space components
        evals_count:
            number of desired energy levels (default value = 10)
        get_eigenstates:
            set to true if eigenstates should be returned as well
            (default value = False)
        param_name:
            name for the parameter that is varied in `param_vals`
            (default value = "external_parameter")
        num_cpus:
            number of cores to be used for computation
            (default value: settings.NUM_CPUS)
        """
        num_cpus = num_cpus or settings.NUM_CPUS
        target_map = cpu_switch.get_map_method(num_cpus)
        if get_eigenstates:
            func = functools.partial(
                self._esys_for_paramval,
                update_hilbertspace=update_hilbertspace,
                evals_count=evals_count,
            )
            with utils.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 = spec_utils.recast_esys_mapdata(
                eigensystem_mapdata)
        else:
            func = functools.partial(
                self._evals_for_paramval,
                update_hilbertspace=update_hilbertspace,
                evals_count=evals_count,
            )
            with utils.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  # type: ignore

        return storage.SpectrumData(
            eigenvalue_table,
            self.get_initdata(),
            param_name,
            param_vals,
            state_table=eigenstate_table,
        )

    ###################################################################################
    # HilbertSpace: add interaction and parsing arguments to .add_interaction
    ###################################################################################
    def add_interaction(self, check_validity=True, **kwargs) -> None:
        """
        Specify the interaction between subsystems making up the `HilbertSpace`
        instance. `add_interaction(...)` offers three different interfaces:

        * Simple interface for operator products
        * String-based interface for more general interaction operator expressions
        * General Qobj interface

        1. Simple interface for operator products
            Specify `ndarray`, `csc_matrix`, or `dia_matrix` (subsystem operator in
            subsystem-internal basis) along with the corresponding subsystem

            signature::

                .add_interation(g=<float>,
                                op1=(<ndarray>, <QuantumSystem>),
                                op2=(<csc_matrix>, <QuantumSystem>),
                                 …,
                                add_hc=<bool>)

            Alternatively, specify subsystem operators via callable methods.

            signature::

                .add_interaction(g=<float>,
                                 op1=<Callable>,
                                 op2=<Callable>,
                                 …,
                                 add_hc=<bool>)
        2. String-based interface for more general interaction operator expressions
                Specify a Python expression that generates the desired operator. The
                expression enables convenience use of basic qutip operations::

                    .add_interaction(expr=<str>,
                                     op1=(<str>, <ndarray>, <subsys>),
                                     op2=(<str>, <Callable>),
                                     …)
        3. General Qobj operator
            Specify a fully identity-wrapped `qutip.Qobj` operator. Signature::

                .add_interaction(qobj=<Qobj>)


        """
        if "expr" in kwargs:
            interaction = self._parse_interactiontermstr(**kwargs)
        elif "qobj" in kwargs:
            interaction = self._parse_qobj(**kwargs)
        elif "op1" in kwargs:
            interaction = self._parse_interactionterm(**kwargs)
        else:
            raise TypeError(
                "Invalid combination and/or types of arguments for `add_interaction`"
            )
        if self._lookup is not None:
            self._lookup._out_of_sync = True

        self.interaction_list.append(interaction)
        if not check_validity:
            return None
        try:
            _ = self.interaction_hamiltonian()
        except:
            self.interaction_list.pop()
            raise ValueError("Invalid Interaction Term")

    def _parse_interactiontermstr(self, **kwargs) -> InteractionTermStr:
        expr = kwargs.pop("expr")
        add_hc = kwargs.pop("add_hc", False)
        const = kwargs.pop("const", None)

        operator_list = []
        for key in kwargs.keys():
            if re.match(r"op\d+$", key) is None:
                raise TypeError("Unexpected keyword argument {}.".format(key))
            operator_list.append(self._parse_op_by_name(kwargs[key]))

        return InteractionTermStr(expr,
                                  operator_list,
                                  const=const,
                                  add_hc=add_hc)

    def _parse_interactionterm(self, **kwargs) -> InteractionTerm:
        g = kwargs.pop("g", None)
        if g is None:
            g = kwargs.pop("g_strength")
        add_hc = kwargs.pop("add_hc", False)

        operator_list = []
        for key in kwargs.keys():
            if re.match(r"op\d+$", key) is None:
                raise TypeError("Unexpected keyword argument {}.".format(key))
            subsys_index, op = self._parse_op(kwargs[key])
            operator_list.append(self._parse_op(kwargs[key]))

        return InteractionTerm(g, operator_list, add_hc=add_hc)

    @staticmethod
    def _parse_qobj(**kwargs) -> Qobj:
        op = kwargs["qobj"]
        if len(kwargs) > 1 or not isinstance(op, Qobj):
            raise TypeError(
                "Cannot interpret specified operator {}".format(op))
        return kwargs["qobj"]

    def _parse_op_by_name(
            self, op_by_name
    ) -> Tuple[int, str, Union[ndarray, csc_matrix, dia_matrix]]:
        if not isinstance(op_by_name, tuple):
            raise TypeError(
                "Cannot interpret specified operator {}".format(op_by_name))
        if len(op_by_name) == 3:
            # format expected:  (<op name as str>, <op as array>, <subsys as QuantumSystem>)
            return self.get_subsys_index(
                op_by_name[2]), op_by_name[0], op_by_name[1]
        # format expected (<op name as str)>, <QuantumSystem.method callable>)
        return (
            self.get_subsys_index(op_by_name[1].__self__),
            op_by_name[0],
            op_by_name[1](),
        )

    def _parse_op(
        self, op: Union[Callable, Tuple[Union[ndarray, csc_matrix],
                                        QuantumSys]]
    ) -> Tuple[int, Union[ndarray, csc_matrix]]:
        if callable(op):
            return self.get_subsys_index(op.__self__), op()
        if not isinstance(op, tuple):
            raise TypeError(
                "Cannot interpret specified operator {}".format(op))
        if len(op) == 2:
            # format expected:  (<op as array>, <subsys as QuantumSystem>)
            return self.get_subsys_index(op[1]), op[0]
        raise TypeError("Cannot interpret specified operator {}".format(op))
Exemplo n.º 27
0
class HilbertSpace(dispatch.DispatchClient, serializers.Serializable):
    """Class holding information about the full Hilbert space, usually composed of multiple subsys_list.
    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 = descriptors.ReadOnlyProperty()
    qbt_subsys_list = descriptors.ReadOnlyProperty()
    lookup = descriptors.ReadOnlyProperty()
    interaction_list = descriptors.WatchedProperty('INTERACTIONLIST_UPDATE')

    def __init__(self, subsystem_list, interaction_list=None):

        # Make sure all the given subsystems have required parameters set up.
        self._subsystems_check(subsystem_list)

        self._subsystems = tuple(subsystem_list)
        if interaction_list:
            self.interaction_list = tuple(interaction_list)
        else:
            self.interaction_list = []

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

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

    @classmethod
    def create(cls):
        hilbertspace = cls([])
        scqubits.ui.hspace_widget.create_hilbertspace_widget(
            hilbertspace.__init__)
        return hilbertspace

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

    def __repr__(self):
        init_dict = self.get_initdata()
        return type(self).__name__ + f'(**{init_dict!r})'

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

    def _subsystems_check(self, subsystems):
        """Check if all the subsystems have truncated_dim set, which 
        is required for HilbertSpace to work correctly.
        Raise an exception if not.

        Parameters
        ----------
        subsystems: list of QuantumSystems

        """
        bad_indices = [
            i for i, sub_sys in enumerate(subsystems)
            if sub_sys.truncated_dim is None
        ]

        if bad_indices:
            msg = "Subsystems with indices '{}' do".format(", ".join([str(i) for i in bad_indices]))  \
                if len(bad_indices) > 1 else "Subsystem with index '{:d}' does".format(bad_indices[0])

            raise RuntimeError("""{} not have `truncated_dim` set,
            which is required for `HilbertSpace` to operate correctly. This parameter can be
            set at object creation time, e.g.

            tmon = scqubits.Transmon(EJ=30.02, EC=1.2, ng=0.3, ncut=31, truncated_dim=4)

            or after the fact via tmon.truncated_dim = 4.
            """.format(msg))

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

    def get_initdata(self):
        """Returns dict appropriate for creating/initializing a new HilbertSpace object.

        Returns
        -------
        dict
        """
        return {
            'subsystem_list': self._subsystems,
            'interaction_list': self.interaction_list
        }

    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
            elif event == 'INTERACTIONTERM_UPDATE' and sender in self.interaction_list:
                self.broadcast('HILBERTSPACE_UPDATE')
                self._lookup._out_of_sync = True
            elif event == 'INTERACTIONLIST_UPDATE' and sender is self:
                self.broadcast('HILBERTSPACE_UPDATE')
                self._lookup._out_of_sync = True

    @property
    def subsystem_list(self):
        return self._subsystems

    @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 subsys_list 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(
                storage.SpectrumData(energy_table=[evals],
                                     state_table=[evecs],
                                     system_params=subsys.get_initdata()))

        evals, evecs = self.eigensys(evals_count=self.dimension)
        dressed_specdata = storage.SpectrumData(
            energy_table=[evals],
            state_table=[evecs],
            system_params=self.get_initdata())
        self._lookup = spec_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)
        evecs = evecs.view(scqubits.io_utils.fileio_qutip.QutipEigenstates)
        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 subsys_list).

        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 = spec_utils.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 subsys_list 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 not self.interaction_list:
            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.dag()
        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 = cpu_switch.get_map_method(num_cpus)
        if get_eigenstates:
            func = functools.partial(self._esys_for_paramval,
                                     update_hilbertspace=update_hilbertspace,
                                     evals_count=evals_count)
            with utils.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 = spec_utils.recast_esys_mapdata(
                eigensystem_mapdata)
        else:
            func = functools.partial(self._evals_for_paramval,
                                     update_hilbertspace=update_hilbertspace,
                                     evals_count=evals_count)
            with utils.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 storage.SpectrumData(eigenvalue_table,
                                    self.get_initdata(),
                                    param_name,
                                    param_vals,
                                    state_table=eigenstate_table)
Exemplo n.º 28
0
class InteractionTermLegacy(dispatch.DispatchClient, serializers.Serializable):
    """
    Deprecated, will not work in future versions. Please look into InteractionTerm
    instead.

    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 subsys_list, 2. V = g A B + h.c.,
    where A, B may be non-Hermitean

    Parameters
    ----------
    g_strength:
        coefficient parametrizing the interaction strength
    hilbertspace:
        specifies the Hilbert space components
    subsys1, subsys2:
        the two subsys_list involved in the interaction
    op1, op2:
        names of operators in the two subsys_list
    add_hc:
        If set to True, the interaction Hamiltonian is of type 2, and the Hermitean
        conjugate is added.
    """

    g_strength = descriptors.WatchedProperty("INTERACTIONTERM_UPDATE")
    subsys1 = descriptors.WatchedProperty("INTERACTIONTERM_UPDATE")
    subsys2 = descriptors.WatchedProperty("INTERACTIONTERM_UPDATE")
    op1 = descriptors.WatchedProperty("INTERACTIONTERM_UPDATE")
    op2 = descriptors.WatchedProperty("INTERACTIONTERM_UPDATE")

    def __init__(
        self,
        g_strength: Union[float, complex],
        subsys1: QuantumSys,
        op1: Union[str, ndarray, csc_matrix, dia_matrix],
        subsys2: QuantumSys,
        op2: Union[str, ndarray, csc_matrix, dia_matrix],
        add_hc: bool = False,
        hilbertspace: "HilbertSpace" = None,
    ) -> None:
        warnings.warn(
            "This use of `InteractionTerm` is deprecated and will cease "
            "to be supported in the future.",
            FutureWarning,
        )
        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

        self._init_params.remove("hilbertspace")

    def __repr__(self) -> str:
        init_dict = {name: getattr(self, name) for name in self._init_params}
        return type(self).__name__ + f"(**{init_dict!r})"

    def __str__(self) -> str:
        indent_length = 25
        name_prepend = "InteractionTermLegacy".ljust(indent_length,
                                                     "-") + "|\n"

        output = ""
        for param_name in self._init_params:
            param_content = getattr(self, param_name).__repr__()
            param_content = param_content.strip("\n")
            if len(param_content) > 50:
                param_content = param_content[:50]
                param_content += " ..."

            output += "{0}| {1}: {2}\n".format(" " * indent_length,
                                               str(param_name), param_content)
        return name_prepend + output
Exemplo n.º 29
0
class Grid1d(dispatch.DispatchClient, serializers.Serializable):
    """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 = descriptors.WatchedProperty('GRID_UPDATE')
    max_val = descriptors.WatchedProperty('GRID_UPDATE')
    pt_count = descriptors.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(
                utils.drop_private_keys(self.__dict__).items()):
            output += '\n' + str(param_name) + '\t: ' + str(param_val)
        return output

    def get_initdata(self):
        """Returns dict appropriate for creating/initializing a new Grid1d object.

        Returns
        -------
        dict
        """
        return self.__dict__

    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):
        """Returns a numpy array of the grid points

        Returns
        -------
        ndarray
        """
        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
Exemplo n.º 30
0
class Fluxonium(base.QubitBaseClass1d, serializers.Serializable):
    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; expected: truncated_dim > 1
    """
    EJ = descriptors.WatchedProperty('QUANTUMSYSTEM_UPDATE')
    EC = descriptors.WatchedProperty('QUANTUMSYSTEM_UPDATE')
    EL = descriptors.WatchedProperty('QUANTUMSYSTEM_UPDATE')
    flux = descriptors.WatchedProperty('QUANTUMSYSTEM_UPDATE')
    cutoff = descriptors.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 = type(self).__name__
        self._evec_dtype = np.float_
        self._default_grid = discretization.Grid1d(-4.5 * np.pi, 4.5 * np.pi,
                                                   151)
        self._image_filename = os.path.join(
            os.path.dirname(os.path.abspath(__file__)),
            'qubit_pngs/fluxonium.png')

    @staticmethod
    def default_params():
        return {
            'EJ': 8.9,
            'EC': 2.5,
            'EL': 0.5,
            'flux': 0.0,
            'cutoff': 110,
            'truncated_dim': 10
        }

    @staticmethod
    def nonfit_params():
        return ['flux', 'cutoff', 'truncated_dim']

    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] * osc.harm_osc_wavefunction(n, phi_basis_labels, phi_osc)
        return storage.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 = constants.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