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
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)
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'
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)
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)
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
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)
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])
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)
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])
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
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)
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()
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)
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)
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)
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()
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, )
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 []
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
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)
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
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
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()
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()))
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))
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)
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
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
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