class GridSpec(DispatchClient, MetaDataMixin): """Class for specifying a general discretized coordinate grid (arbitrary dimensions). Parameters ---------- minmaxpts_array: ndarray array of with entries [minvalue, maxvalue, number of points] """ min_vals = WatchedProperty('GRID_UPDATE') max_vals = WatchedProperty('GRID_UPDATE') var_count = WatchedProperty('GRID_UPDATE') pt_counts = WatchedProperty('GRID_UPDATE') def __init__(self, minmaxpts_array): self.min_vals = minmaxpts_array[:, 0] self.max_vals = minmaxpts_array[:, 1] self.var_count = len(self.min_vals) self.pt_counts = minmaxpts_array[:, 2].astype( np.int) # these are used as indices; need to be whole numbers. def __str__(self): output = ' GridSpec ......' for param_name, param_val in sorted(self.__dict__.items()): output += '\n' + str(param_name) + '\t: ' + str(param_val) return output def unwrap(self): """Auxiliary routine that yields a tuple of the parameters specifying the grid.""" return self.min_vals, self.max_vals, self.pt_counts, self.var_count
class InteractionTerm(DispatchClient): """ Class for specifying a term in the interaction Hamiltonian of a composite Hilbert space, and constructing the Hamiltonian in qutip.Qobj format. The expected form of the interaction term is of two possible types: 1. V = g A B, where A, B are Hermitean operators in two specified subsystems, 2. V = g A B + h.c., where A, B may be non-Hermitean Parameters ---------- g_strength: float coefficient parametrizing the interaction strength hilbertspace: HilbertSpace specifies the Hilbert space components subsys1, subsys2: QuantumSystem the two subsystems involved in the interaction op1, op2: str or ndarray names of operators in the two subsystems add_hc: bool, optional (default=False) If set to True, the interaction Hamiltonian is of type 2, and the Hermitean conjugate is added. """ g_strength = WatchedProperty('INTERACTIONTERM_UPDATE') subsys1 = WatchedProperty('INTERACTIONTERM_UPDATE') subsys2 = WatchedProperty('INTERACTIONTERM_UPDATE') op1 = WatchedProperty('INTERACTIONTERM_UPDATE') op2 = WatchedProperty('INTERACTIONTERM_UPDATE') def __init__(self, g_strength, subsys1, op1, subsys2, op2, add_hc=False, hilbertspace=None): if hilbertspace: warnings.warn( "`hilbertspace` is no longer a parameter for initializing an InteractionTerm object.", FutureWarning) self.g_strength = g_strength self.subsys1 = subsys1 self.op1 = op1 self.subsys2 = subsys2 self.op2 = op2 self.add_hc = add_hc
class Grid1d(DispatchClient, MetaDataMixin): """Data structure and methods for setting up discretized 1d coordinate grid, generating corresponding derivative matrices. Parameters ---------- min_val: float minimum value of the discretized variable max_val: float maximum value of the discretized variable pt_count: int number of grid points """ min_val = WatchedProperty('GRID_UPDATE') max_val = WatchedProperty('GRID_UPDATE') pt_count = WatchedProperty('GRID_UPDATE') def __init__(self, min_val, max_val, pt_count): self.min_val = min_val self.max_val = max_val self.pt_count = pt_count def __str__(self): output = ' Grid1d ......' for param_name, param_val in sorted(self.__dict__.items()): output += '\n' + str(param_name) + '\t: ' + str(param_val) return output def grid_spacing(self): """ Returns ------- float spacing between neighboring grid points """ return (self.max_val - self.min_val) / self.pt_count def make_linspace(self): return np.linspace(self.min_val, self.max_val, self.pt_count) def first_derivative_matrix(self, prefactor=1.0, periodic=False): """Generate sparse matrix for first derivative of the form :math:`\\partial_{x_i}`. Uses :math:`f'(x) \\approx [f(x+h) - f(x-h)]/2h`. Parameters ---------- prefactor: float or complex, optional prefactor of the derivative matrix (default value: 1.0) periodic: bool, optional set to True if variable is a periodic variable Returns ------- sparse matrix in `dia` format """ if isinstance(prefactor, complex): dtp = np.complex_ else: dtp = np.float_ delta_x = (self.max_val - self.min_val) / self.pt_count offdiag_element = prefactor / (2 * delta_x) derivative_matrix = sparse.dia_matrix((self.pt_count, self.pt_count), dtype=dtp) derivative_matrix.setdiag( offdiag_element, k=1) # occupy first off-diagonal to the right derivative_matrix.setdiag(-offdiag_element, k=-1) # and left if periodic: derivative_matrix.setdiag(-offdiag_element, k=self.pt_count - 1) derivative_matrix.setdiag(offdiag_element, k=-self.pt_count + 1) return derivative_matrix def second_derivative_matrix(self, prefactor=1.0, periodic=False): """Generate sparse matrix for second derivative of the form :math:`\\partial^2_{x_i}`. Uses :math:`f''(x) \\approx [f(x+h) - 2f(x) + f(x-h)]/h^2`. Parameters ---------- prefactor: float, optional optional prefactor of the derivative matrix (default value = 1.0) periodic: bool, optional set to True if variable is a periodic variable (default value = False) Returns ------- sparse matrix in `dia` format """ delta_x = (self.max_val - self.min_val) / self.pt_count offdiag_element = prefactor / delta_x**2 derivative_matrix = sparse.dia_matrix((self.pt_count, self.pt_count), dtype=np.float_) derivative_matrix.setdiag(-2.0 * offdiag_element, k=0) derivative_matrix.setdiag(offdiag_element, k=1) derivative_matrix.setdiag(offdiag_element, k=-1) if periodic: derivative_matrix.setdiag(offdiag_element, k=self.pt_count - 1) derivative_matrix.setdiag(offdiag_element, k=-self.pt_count + 1) return derivative_matrix @classmethod def create_from_dict(cls, meta_dict): """ Create and initialize a new grid object from metadata dictionary Parameters ---------- meta_dict: dict Returns ------- Grid1d """ return cls(min_val=meta_dict['min_val'], max_val=meta_dict['max_val'], pt_count=meta_dict['pt_count'])
class FullZeroPi(QubitBaseClass): r"""Zero-Pi qubit [Brooks2013]_ [Dempster2014]_ including coupling to the zeta mode. The circuit is described by the Hamiltonian :math:`H = H_{0-\pi} + H_\text{int} + H_\zeta`, where .. math:: &H_{0-\pi} = -2E_\text{CJ}\partial_\phi^2+2E_{\text{C}\Sigma}(i\partial_\theta-n_g)^2 +2E_{C\Sigma}dC_J\,\partial_\phi\partial_\theta\\ &\qquad\qquad\qquad+2E_{C\Sigma}(\delta C_J/C_J)\partial_\phi\partial_\theta +2\,\delta E_J \sin\theta\sin(\phi-\phi_\text{ext}/2)\\ &H_\text{int} = 2E_{C\Sigma}dC\,\partial_\theta\partial_\zeta + E_L dE_L \phi\,\zeta\\ &H_\zeta = \omega_\zeta a^\dagger a expressed in phase basis. The definition of the relevant charging energies :math:`E_\text{CJ}`, :math:`E_{\text{C}\Sigma}`, Josephson energies :math:`E_\text{J}`, inductive energies :math:`E_\text{L}`, and relative amounts of disorder :math:`dC_\text{J}`, :math:`dE_\text{J}`, :math:`dC`, :math:`dE_\text{L}` follows [Groszkowski2018]_. Internally, the ``FullZeroPi`` class formulates the Hamiltonian matrix via the product basis of the decoupled Zero-Pi qubit (see ``ZeroPi``) on one hand, and the zeta LC oscillator on the other hand. Parameters ---------- EJ: float mean Josephson energy of the two junctions EL: float inductive energy of the two (super-)inductors ECJ: float charging energy associated with the two junctions EC: float or None charging energy of the large shunting capacitances; set to `None` if `ECS` is provided instead dEJ: float relative disorder in EJ, i.e., (EJ1-EJ2)/EJavg dEL: float relative disorder in EL, i.e., (EL1-EL2)/ELavg dCJ: float relative disorder of the junction capacitances, i.e., (CJ1-CJ2)/CJavg dC: float relative disorder in large capacitances, i.e., (C1-C2)/Cavg ng: float offset charge associated with theta zeropi_cutoff: int cutoff in the number of states of the disordered zero-pi qubit zeta_cutoff: int cutoff in the zeta oscillator basis (Fock state basis) flux: float magnetic flux through the circuit loop, measured in units of flux quanta (h/2e) grid: Grid1d object specifies the range and spacing of the discretization lattice ncut: int charge number cutoff for `n_theta`, `n_theta = -ncut, ..., ncut` ECS: float, optional total charging energy including large shunting capacitances and junction capacitances; may be provided instead of EC truncated_dim: int, optional desired dimension of the truncated quantum system """ EJ = WatchedProperty('QUANTUMSYSTEM_UPDATE', inner_object_name='_zeropi') EL = WatchedProperty('QUANTUMSYSTEM_UPDATE', inner_object_name='_zeropi') ECJ = WatchedProperty('QUANTUMSYSTEM_UPDATE', inner_object_name='_zeropi') EC = WatchedProperty('QUANTUMSYSTEM_UPDATE', inner_object_name='_zeropi') ECS = WatchedProperty('QUANTUMSYSTEM_UPDATE', inner_object_name='_zeropi') dEJ = WatchedProperty('QUANTUMSYSTEM_UPDATE', inner_object_name='_zeropi') dCJ = WatchedProperty('QUANTUMSYSTEM_UPDATE', inner_object_name='_zeropi') ng = WatchedProperty('QUANTUMSYSTEM_UPDATE', inner_object_name='_zeropi') flux = WatchedProperty('QUANTUMSYSTEM_UPDATE', inner_object_name='_zeropi') grid = WatchedProperty('QUANTUMSYSTEM_UPDATE', inner_object_name='_zeropi') ncut = WatchedProperty('QUANTUMSYSTEM_UPDATE', inner_object_name='_zeropi') zeropi_cutoff = WatchedProperty('QUANTUMSYSTEM_UPDATE', inner_object_name='_zeropi', attr_name='truncated_dim') dC = WatchedProperty('QUANTUMSYSTEM_UPDATE') dEL = WatchedProperty('QUANTUMSYSTEM_UPDATE') def __init__(self, EJ, EL, ECJ, EC, dEJ, dCJ, dC, dEL, flux, ng, zeropi_cutoff, zeta_cutoff, grid, ncut, ECS=None, truncated_dim=None): self._zeropi = ZeroPi( EJ=EJ, EL=EL, ECJ=ECJ, EC=EC, ng=ng, flux=flux, grid=grid, ncut=ncut, dEJ=dEJ, dCJ=dCJ, ECS=ECS, # the zeropi_cutoff defines the truncated_dim of the "base" zeropi object truncated_dim=zeropi_cutoff) self.dC = dC self.dEL = dEL self.zeta_cutoff = zeta_cutoff self._sys_type = 'full 0-pi' self.truncated_dim = truncated_dim self._evec_dtype = np.complex_ CENTRAL_DISPATCH.register('GRID_UPDATE', self) def receive(self, event, sender, **kwargs): if sender is self._zeropi.grid: self.broadcast('QUANTUMSYSTEM_UPDATE') def __str__(self): output_str = super().__str__() + '\n\n' output_str += 'INTERNAL 0-Pi object: ' + self._zeropi.__str__() return output_str def set_EC_via_ECS(self, ECS): """Helper function to set `EC` by providing `ECS`, keeping `ECJ` constant.""" self._zeropi.set_EC_via_ECS(ECS) @property def E_zeta(self): """Returns energy quantum of the zeta mode""" return (8.0 * self.EL * self.EC)**0.5 def hamiltonian(self, return_parts=False): """Returns Hamiltonian in basis obtained by discretizing phi, employing charge basis for theta, and Fock basis for zeta. Parameters ---------- return_parts: bool, optional If set to true, `hamiltonian` returns [hamiltonian, evals, evecs, g_coupling_matrix] Returns ------- scipy.sparse.csc_matrix or list """ zeropi_dim = self.zeropi_cutoff zeropi_evals, zeropi_evecs = self._zeropi.eigensys( evals_count=zeropi_dim) zeropi_diag_hamiltonian = sparse.dia_matrix((zeropi_dim, zeropi_dim), dtype=np.complex_) zeropi_diag_hamiltonian.setdiag(zeropi_evals) zeta_dim = self.zeta_cutoff prefactor = self.E_zeta zeta_diag_hamiltonian = op.number_sparse(zeta_dim, prefactor) hamiltonian_mat = sparse.kron( zeropi_diag_hamiltonian, sparse.identity(zeta_dim, format='dia', dtype=np.complex_)) hamiltonian_mat += sparse.kron( sparse.identity(zeropi_dim, format='dia', dtype=np.complex_), zeta_diag_hamiltonian) gmat = self.g_coupling_matrix(zeropi_evecs) zeropi_coupling = sparse.dia_matrix((zeropi_dim, zeropi_dim), dtype=np.complex_) for l1 in range(zeropi_dim): for l2 in range(zeropi_dim): zeropi_coupling += gmat[l1, l2] * op.hubbard_sparse( l1, l2, zeropi_dim) hamiltonian_mat += sparse.kron( zeropi_coupling, op.annihilation_sparse(zeta_dim) + op.creation_sparse(zeta_dim)) if return_parts: return [hamiltonian_mat.tocsc(), zeropi_evals, zeropi_evecs, gmat] return hamiltonian_mat.tocsc() def d_hamiltonian_d_flux(self, zeropi_evecs=None): r"""Calculates a derivative of the Hamiltonian w.r.t flux, at the current value of flux, as stored in the object. The returned operator is in the product basis The flux is assumed to be given in the units of the ratio \Phi_{ext}/\Phi_0. So if \frac{\partial H}{ \partial \Phi_{\rm ext}}, is needed, the expression returned by this function, needs to be multiplied by 1/\Phi_0. Returns ------- scipy.sparse.csc_matrix matrix representing the derivative of the Hamiltonian """ return self._zeropi_operator_in_product_basis( self._zeropi.d_hamiltonian_d_flux(), zeropi_evecs=zeropi_evecs) def _zeropi_operator_in_product_basis(self, zeropi_operator, zeropi_evecs=None): """Helper method that converts a zeropi operator into one in the product basis. Returns ------- scipy.sparse.csc_matrix operator written in the product basis """ zeropi_dim = self.zeropi_cutoff zeta_dim = self.zeta_cutoff if zeropi_evecs is None: _, zeropi_evecs = self._zeropi.eigensys(evals_count=zeropi_dim) op_eigen_basis = sparse.dia_matrix( (zeropi_dim, zeropi_dim), dtype=np.complex_) # is this guaranteed to be zero? op_zeropi = get_matrixelement_table(zeropi_operator, zeropi_evecs) for n in range(zeropi_dim): for m in range(zeropi_dim): op_eigen_basis += op_zeropi[n, m] * op.hubbard_sparse( n, m, zeropi_dim) return sparse.kron(op_eigen_basis, sparse.identity(zeta_dim, format='csc', dtype=np.complex_), format='csc') def i_d_dphi_operator(self, zeropi_evecs=None): r""" Operator :math:`i d/d\varphi`. Returns ------- scipy.sparse.csc_matrix """ return self._zeropi_operator_in_product_basis( self._zeropi.i_d_dphi_operator(), zeropi_evecs=zeropi_evecs) def n_theta_operator(self, zeropi_evecs=None): r""" Operator :math:`n_\theta`. Returns ------- scipy.sparse.csc_matrix """ return self._zeropi_operator_in_product_basis( self._zeropi.n_theta_operator(), zeropi_evecs=zeropi_evecs) def phi_operator(self, zeropi_evecs=None): r""" Operator :math:`\varphi`. Returns ------- scipy.sparse.csc_matrix """ return self._zeropi_operator_in_product_basis( self._zeropi.phi_operator(), zeropi_evecs=zeropi_evecs) def hilbertdim(self): """Returns Hilbert space dimension""" return self.zeropi_cutoff * self.zeta_cutoff def _evals_calc(self, evals_count, hamiltonian_mat=None): if hamiltonian_mat is None: hamiltonian_mat = self.hamiltonian() evals = sparse.linalg.eigsh(hamiltonian_mat, k=evals_count, return_eigenvectors=False, which='SA') return np.sort(evals) def _esys_calc(self, evals_count, hamiltonian_mat=None): if hamiltonian_mat is None: hamiltonian_mat = self.hamiltonian() evals, evecs = sparse.linalg.eigsh(hamiltonian_mat, k=evals_count, return_eigenvectors=True, which='SA') evals, evecs = order_eigensystem(evals, evecs) return evals, evecs def g_phi_coupling_matrix(self, zeropi_states): """Returns a matrix of coupling strengths g^\\phi_{ll'} [cmp. Dempster et al., Eq. (18)], using the states from the list `zeropi_states`. Most commonly, `zeropi_states` will contain eigenvectors of the `DisorderedZeroPi` type. """ # prefactor = self.EL * self.dEL * (8.0 * self.EC / self.EL)**0.25 prefactor = self.EL * (self.dEL / 2.0) * (8.0 * self.EC / self.EL)**0.25 return prefactor * get_matrixelement_table(self._zeropi.phi_operator(), zeropi_states) def g_theta_coupling_matrix(self, zeropi_states): """Returns a matrix of coupling strengths i*g^\\theta_{ll'} [cmp. Dempster et al., Eq. (17)], using the states from the list 'zeropi_states'. """ prefactor = 1j * self.ECS * (self.dC / 2.0) * (32.0 * self.EL / self.EC)**0.25 return prefactor * get_matrixelement_table( self._zeropi.n_theta_operator(), zeropi_states) def g_coupling_matrix(self, zeropi_states=None, evals_count=None): """Returns a matrix of coupling strengths g_{ll'} [cmp. Dempster et al., text above Eq. (17)], using the states from 'zeropi_states'. If `zeropi_states==None`, then a set of `self.zeropi` eigenstates is calculated. Only in that case is `which` used for the eigenstate number (and hence the coupling matrix size). """ if evals_count is None: evals_count = self._zeropi.truncated_dim if zeropi_states is None: _, zeropi_states = self._zeropi.eigensys(evals_count=evals_count) return self.g_phi_coupling_matrix( zeropi_states) + self.g_theta_coupling_matrix(zeropi_states) def set_params_from_dict(self, meta_dict): """Set object parameters by given metadata dictionary Parameters ---------- meta_dict: dict """ for param_name, param_value in meta_dict.items(): if key_in_grid1d(param_name): setattr(self.grid, param_name, param_value) elif is_numerical(param_value): setattr(self, param_name, param_value) self._zeropi = ZeroPi(EJ=self.EJ, EL=self.EL, ECJ=self.ECJ, EC=self.EC, dEJ=self.dEJ, dCJ=self.dCJ, flux=self.flux, ng=self.ng, grid=self.grid, ncut=self.ncut, truncated_dim=self.zeropi_cutoff) @classmethod def create_from_dict(cls, meta_dict): """Set object parameters by given metadata dictionary Parameters ---------- meta_dict: dict """ filtered_dict = {} grid_dict = {} for param_name, param_value in meta_dict.items(): if key_in_grid1d(param_name): grid_dict[param_name] = param_value elif is_numerical(param_value): filtered_dict[param_name] = param_value grid = Grid1d(**grid_dict) filtered_dict['grid'] = grid return cls(**filtered_dict)
class Fluxonium(QubitBaseClass1d): r"""Class for the fluxonium qubit. Hamiltonian :math:`H_\text{fl}=-4E_\text{C}\partial_\phi^2-E_\text{J}\cos(\phi-\varphi_\text{ext}) +\frac{1}{2}E_L\phi^2` is represented in dense form. The employed basis is the EC-EL harmonic oscillator basis. The cosine term in the potential is handled via matrix exponentiation. Initialize with, for example:: qubit = Fluxonium(EJ=1.0, EC=2.0, EL=0.3, flux=0.2, cutoff=120) Parameters ---------- EJ: float Josephson energy EC: float charging energy EL: float inductive energy flux: float external magnetic flux in angular units, 2pi corresponds to one flux quantum cutoff: int number of harm. osc. basis states used in diagonalization truncated_dim: int, optional desired dimension of the truncated quantum system """ EJ = WatchedProperty('QUANTUMSYSTEM_UPDATE') EC = WatchedProperty('QUANTUMSYSTEM_UPDATE') EL = WatchedProperty('QUANTUMSYSTEM_UPDATE') flux = WatchedProperty('QUANTUMSYSTEM_UPDATE') cutoff = WatchedProperty('QUANTUMSYSTEM_UPDATE') def __init__(self, EJ, EC, EL, flux, cutoff, truncated_dim=None): self.EJ = EJ self.EC = EC self.EL = EL self.flux = flux self.cutoff = cutoff self.truncated_dim = truncated_dim self._sys_type = 'fluxonium' self._evec_dtype = np.float_ self._default_grid = Grid1d(-4.5 * np.pi, 4.5 * np.pi, 151) def phi_osc(self): """ Returns ------- float Returns oscillator length for the LC oscillator composed of the fluxonium inductance and capacitance. """ return (8.0 * self.EC / self.EL)**0.25 # LC oscillator length def E_plasma(self): """ Returns ------- float Returns the plasma oscillation frequency. """ return math.sqrt(8.0 * self.EL * self.EC) # LC plasma oscillation energy def phi_operator(self): """ Returns ------- ndarray Returns the phi operator in the LC harmonic oscillator basis """ dimension = self.hilbertdim() return (op.creation(dimension) + op.annihilation(dimension)) * self.phi_osc() / math.sqrt(2) def n_operator(self): """ Returns ------- ndarray Returns the :math:`n = - i d/d\\phi` operator in the LC harmonic oscillator basis """ dimension = self.hilbertdim() return 1j * (op.creation(dimension) - op.annihilation(dimension)) / ( self.phi_osc() * math.sqrt(2)) def exp_i_phi_operator(self): """ Returns ------- ndarray Returns the :math:`e^{i\\phi}` operator in the LC harmonic oscillator basis """ exponent = 1j * self.phi_operator() return sp.linalg.expm(exponent) def cos_phi_operator(self): """ Returns ------- ndarray Returns the :math:`\\cos \\phi` operator in the LC harmonic oscillator basis """ cos_phi_op = 0.5 * self.exp_i_phi_operator() cos_phi_op += cos_phi_op.conjugate().T return cos_phi_op def sin_phi_operator(self): """ Returns ------- ndarray Returns the :math:`\\sin \\phi` operator in the LC harmonic oscillator basis """ sin_phi_op = -1j * 0.5 * self.exp_i_phi_operator() sin_phi_op += sin_phi_op.conjugate().T return sin_phi_op def hamiltonian(self): # follow Zhu et al., PRB 87, 024510 (2013) """Construct Hamiltonian matrix in harmonic-oscillator basis, following Zhu et al., PRB 87, 024510 (2013) Returns ------- ndarray """ dimension = self.hilbertdim() diag_elements = [i * self.E_plasma() for i in range(dimension)] lc_osc_matrix = np.diag(diag_elements) exp_matrix = self.exp_i_phi_operator() * cmath.exp( 1j * 2 * np.pi * self.flux) cos_matrix = 0.5 * (exp_matrix + exp_matrix.conjugate().T) hamiltonian_mat = lc_osc_matrix - self.EJ * cos_matrix return np.real( hamiltonian_mat ) # use np.real to remove rounding errors from matrix exponential, # fluxonium Hamiltonian in harm. osc. basis is real-valued def hilbertdim(self): """ Returns ------- int Returns the Hilbert space dimension.""" return self.cutoff def potential(self, phi): """Fluxonium potential evaluated at `phi`. Parameters ---------- phi: float or ndarray float value of the phase variable `phi` Returns ------- float or ndarray """ return 0.5 * self.EL * phi * phi - self.EJ * np.cos(phi + 2.0 * np.pi * self.flux) def wavefunction(self, esys, which=0, phi_grid=None): """Returns a fluxonium wave function in `phi` basis Parameters ---------- esys: ndarray, ndarray eigenvalues, eigenvectors which: int, optional index of desired wave function (default value = 0) phi_grid: Grid1d, optional used for setting a custom grid for phi; if None use self._default_grid Returns ------- WaveFunction object """ if esys is None: evals_count = max(which + 1, 3) evals, evecs = self.eigensys(evals_count) else: evals, evecs = esys dim = self.hilbertdim() phi_grid = phi_grid or self._default_grid phi_basis_labels = phi_grid.make_linspace() wavefunc_osc_basis_amplitudes = evecs[:, which] phi_wavefunc_amplitudes = np.zeros(phi_grid.pt_count, dtype=np.complex_) phi_osc = self.phi_osc() for n in range(dim): phi_wavefunc_amplitudes += wavefunc_osc_basis_amplitudes[ n] * harm_osc_wavefunction(n, phi_basis_labels, phi_osc) return WaveFunction(basis_labels=phi_basis_labels, amplitudes=phi_wavefunc_amplitudes, energy=evals[which]) def wavefunction1d_defaults(self, mode, evals, wavefunc_count): """Plot defaults for plotting.wavefunction1d. Parameters ---------- mode: str amplitude modifier, needed to give the correct default y label evals: ndarray eigenvalues to include in plot wavefunc_count: int number of wave functions to be plotted """ ylabel = r'$\psi_j(\varphi)$' ylabel = MODE_STR_DICT[mode](ylabel) options = {'xlabel': r'$\varphi$', 'ylabel': ylabel} if wavefunc_count > 1: ymin = -1.025 * self.EJ ymax = max(1.8 * self.EJ, evals[-1] + 0.1 * (evals[-1] - evals[0])) options['ylim'] = (ymin, ymax) return options
class ZeroPi(QubitBaseClass): r"""Zero-Pi Qubit | [1] Brooks et al., Physical Review A, 87(5), 052306 (2013). http://doi.org/10.1103/PhysRevA.87.052306 | [2] Dempster et al., Phys. Rev. B, 90, 094518 (2014). http://doi.org/10.1103/PhysRevB.90.094518 | [3] Groszkowski et al., New J. Phys. 20, 043053 (2018). https://doi.org/10.1088/1367-2630/aab7cd Zero-Pi qubit without coupling to the `zeta` mode, i.e., no disorder in `EC` and `EL`, see Eq. (4) in Groszkowski et al., New J. Phys. 20, 043053 (2018), .. math:: H &= -2E_\text{CJ}\partial_\phi^2+2E_{\text{C}\Sigma}(i\partial_\theta-n_g)^2 +2E_{C\Sigma}dC_J\,\partial_\phi\partial_\theta -2E_\text{J}\cos\theta\cos(\phi-\varphi_\text{ext}/2)+E_L\phi^2\\ &\qquad +2E_\text{J} + E_J dE_J \sin\theta\sin(\phi-\phi_\text{ext}/2). Formulation of the Hamiltonian matrix proceeds by discretization of the `phi` variable, and using charge basis for the `theta` variable. Parameters ---------- EJ: float mean Josephson energy of the two junctions EL: float inductive energy of the two (super-)inductors ECJ: float charging energy associated with the two junctions EC: float or None charging energy of the large shunting capacitances; set to `None` if `ECS` is provided instead dEJ: float relative disorder in EJ, i.e., (EJ1-EJ2)/EJavg dCJ: float relative disorder of the junction capacitances, i.e., (CJ1-CJ2)/CJavg ng: float offset charge associated with theta flux: float magnetic flux through the circuit loop, measured in units of flux quanta (h/2e) grid: Grid1d object specifies the range and spacing of the discretization lattice ncut: int charge number cutoff for `n_theta`, `n_theta = -ncut, ..., ncut` ECS: float, optional total charging energy including large shunting capacitances and junction capacitances; may be provided instead of EC truncated_dim: int, optional desired dimension of the truncated quantum system """ EJ = WatchedProperty('QUANTUMSYSTEM_UPDATE') EL = WatchedProperty('QUANTUMSYSTEM_UPDATE') ECJ = WatchedProperty('QUANTUMSYSTEM_UPDATE') EC = WatchedProperty('QUANTUMSYSTEM_UPDATE') dEJ = WatchedProperty('QUANTUMSYSTEM_UPDATE') dCJ = WatchedProperty('QUANTUMSYSTEM_UPDATE') ng = WatchedProperty('QUANTUMSYSTEM_UPDATE') ncut = WatchedProperty('QUANTUMSYSTEM_UPDATE') def __init__(self, EJ, EL, ECJ, EC, ng, flux, grid, ncut, dEJ=0, dCJ=0, ECS=None, truncated_dim=None): self.EJ = EJ self.EL = EL self.ECJ = ECJ if EC is None and ECS is None: raise ValueError("Argument missing: must either provide EC or ECS") if EC and ECS: raise ValueError( "Argument error: can only provide either EC or ECS") if EC: self.EC = EC else: self.EC = 1 / (1 / ECS - 1 / self.ECJ) self.dEJ = dEJ self.dCJ = dCJ self.ng = ng self.flux = flux self.grid = grid self.ncut = ncut self.truncated_dim = truncated_dim self._sys_type = '0-pi' self._evec_dtype = np.complex_ self._default_grid = Grid1d( -np.pi / 2, 3 * np.pi / 2, 100) # for theta, needed for plotting wavefunction CENTRAL_DISPATCH.register('GRID_UPDATE', self) def receive(self, event, sender, **kwargs): if sender is self.grid: self.broadcast('QUANTUMSYSTEM_UPDATE') def _evals_calc(self, evals_count): hamiltonian_mat = self.hamiltonian() evals = sparse.linalg.eigsh(hamiltonian_mat, k=evals_count, return_eigenvectors=False, which='SA') return np.sort(evals) def _esys_calc(self, evals_count): hamiltonian_mat = self.hamiltonian() evals, evecs = sparse.linalg.eigsh(hamiltonian_mat, k=evals_count, return_eigenvectors=True, which='SA') # TODO consider normalization of zeropi wavefunctions # evecs /= np.sqrt(self.grid.grid_spacing()) evals, evecs = order_eigensystem(evals, evecs) return evals, evecs def get_ECS(self): return 1 / (1 / self.EC + 1 / self.ECJ) def set_ECS(self, value): warnings.warn( "It is not possible to directly set ECS (except in initialization). Instead, set EC or ECJ, " "or use set_EC_via_ECS() to update EC indirectly.", Warning) ECS = property(get_ECS, set_ECS) def set_EC_via_ECS(self, ECS): """Helper function to set `EC` by providing `ECS`, keeping `ECJ` constant.""" self.EC = 1 / (1 / ECS - 1 / self.ECJ) def hilbertdim(self): """Returns Hilbert space dimension""" return self.grid.pt_count * (2 * self.ncut + 1) def potential(self, phi, theta): """ Parameters ---------- phi: float theta: float Returns ------- float value of the potential energy evaluated at phi, theta """ return (-2.0 * self.EJ * np.cos(theta) * np.cos(phi - 2.0 * np.pi * self.flux / 2.0) + self.EL * phi**2 + 2.0 * self.EJ + self.EJ * self.dEJ * np.sin(theta) * np.sin(phi - 2.0 * np.pi * self.flux / 2.0)) def sparse_kinetic_mat(self): """ Kinetic energy portion of the Hamiltonian. TODO: update this method to use single-variable operator methods Returns ------- scipy.sparse.csc_matrix matrix representing the kinetic energy operator """ pt_count = self.grid.pt_count dim_theta = 2 * self.ncut + 1 identity_phi = sparse.identity(pt_count, format='csc', dtype=np.complex_) identity_theta = sparse.identity(dim_theta, format='csc', dtype=np.complex_) kinetic_matrix_phi = self.grid.second_derivative_matrix( prefactor=-2.0 * self.ECJ) diag_elements = 2.0 * self.ECS * np.square( np.arange(-self.ncut + self.ng, self.ncut + 1 + self.ng)) kinetic_matrix_theta = sparse.dia_matrix( (diag_elements, [0]), shape=(dim_theta, dim_theta)).tocsc() kinetic_matrix = ( sparse.kron(kinetic_matrix_phi, identity_theta, format='csc') + sparse.kron(identity_phi, kinetic_matrix_theta, format='csc')) kinetic_matrix -= 2.0 * self.ECS * self.dCJ * self.i_d_dphi_operator( ) * self.n_theta_operator() return kinetic_matrix def sparse_potential_mat(self): """ Potential energy portion of the Hamiltonian. TODO: update this method to use single-variable operator methods Returns ------- scipy.sparse.csc_matrix matrix representing the potential energy operator """ pt_count = self.grid.pt_count grid_linspace = self.grid.make_linspace() dim_theta = 2 * self.ncut + 1 phi_inductive_vals = self.EL * np.square(grid_linspace) phi_inductive_potential = sparse.dia_matrix( (phi_inductive_vals, [0]), shape=(pt_count, pt_count)).tocsc() phi_cos_vals = np.cos(grid_linspace - 2.0 * np.pi * self.flux / 2.0) phi_cos_potential = sparse.dia_matrix( (phi_cos_vals, [0]), shape=(pt_count, pt_count)).tocsc() phi_sin_vals = np.sin(grid_linspace - 2.0 * np.pi * self.flux / 2.0) phi_sin_potential = sparse.dia_matrix( (phi_sin_vals, [0]), shape=(pt_count, pt_count)).tocsc() theta_cos_potential = (-self.EJ * (sparse.dia_matrix( ([1.0] * dim_theta, [-1]), shape=(dim_theta, dim_theta)) + sparse.dia_matrix( ([1.0] * dim_theta, [1]), shape=(dim_theta, dim_theta))) ).tocsc() potential_mat = ( sparse.kron(phi_cos_potential, theta_cos_potential, format='csc') + sparse.kron( phi_inductive_potential, self._identity_theta(), format='csc') + 2 * self.EJ * sparse.kron( self._identity_phi(), self._identity_theta(), format='csc')) potential_mat += (self.EJ * self.dEJ * sparse.kron( phi_sin_potential, self._identity_theta(), format='csc') * self.sin_theta_operator()) return potential_mat def hamiltonian(self): """Calculates Hamiltonian in basis obtained by discretizing phi and employing charge basis for theta. Returns ------- scipy.sparse.csc_matrix matrix representing the potential energy operator """ return self.sparse_kinetic_mat() + self.sparse_potential_mat() def sparse_d_potential_d_flux_mat(self): r"""Calculates a of the potential energy w.r.t flux, at the current value of flux, as stored in the object. The flux is assumed to be given in the units of the ratio \Phi_{ext}/\Phi_0. So if \frac{\partial U}{ \partial \Phi_{\rm ext}}, is needed, the expression returned by this function, needs to be multiplied by 1/\Phi_0. Returns ------- scipy.sparse.csc_matrix matrix representing the derivative of the potential energy """ op_1 = sparse.kron(self._sin_phi_operator(x=-2.0 * np.pi * self.flux / 2.0), self._cos_theta_operator(), format='csc') op_2 = sparse.kron(self._cos_phi_operator(x=-2.0 * np.pi * self.flux / 2.0), self._sin_theta_operator(), format='csc') return -2.0 * np.pi * self.EJ * op_1 - np.pi * self.EJ * self.dEJ * op_2 def d_hamiltonian_d_flux(self): r"""Calculates a derivative of the Hamiltonian w.r.t flux, at the current value of flux, as stored in the object. The flux is assumed to be given in the units of the ratio \Phi_{ext}/\Phi_0. So if \frac{\partial H}{ \partial \Phi_{\rm ext}}, is needed, the expression returned by this function, needs to be multiplied by 1/\Phi_0. Returns ------- scipy.sparse.csc_matrix matrix representing the derivative of the Hamiltonian """ return self.sparse_d_potential_d_flux_mat() def _identity_phi(self): r""" Identity operator acting only on the `\phi` Hilbert subspace. Returns ------- scipy.sparse.csc_matrix """ pt_count = self.grid.pt_count return sparse.identity(pt_count, format='csc') def _identity_theta(self): r""" Identity operator acting only on the `\theta` Hilbert subspace. Returns ------- scipy.sparse.csc_matrix """ dim_theta = 2 * self.ncut + 1 return sparse.identity(dim_theta, format='csc') def i_d_dphi_operator(self): r""" Operator :math:`i d/d\varphi`. Returns ------- scipy.sparse.csc_matrix """ return sparse.kron(self.grid.first_derivative_matrix(prefactor=1j), self._identity_theta(), format='csc') def _phi_operator(self): r""" Operator :math:`\varphi`, acting only on the `\varphi` Hilbert subspace. Returns ------- scipy.sparse.csc_matrix """ pt_count = self.grid.pt_count phi_matrix = sparse.dia_matrix((pt_count, pt_count), dtype=np.complex_) diag_elements = self.grid.make_linspace() phi_matrix.setdiag(diag_elements) return phi_matrix def phi_operator(self): r""" Operator :math:`\varphi`. Returns ------- scipy.sparse.csc_matrix """ return sparse.kron(self._phi_operator(), self._identity_theta(), format='csc') def n_theta_operator(self): r""" Operator :math:`n_\theta`. Returns ------- scipy.sparse.csc_matrix """ dim_theta = 2 * self.ncut + 1 diag_elements = np.arange(-self.ncut, self.ncut + 1) n_theta_matrix = sparse.dia_matrix( (diag_elements, [0]), shape=(dim_theta, dim_theta)).tocsc() return sparse.kron(self._identity_phi(), n_theta_matrix, format='csc') def _sin_phi_operator(self, x=0): r""" Operator :math:`\sin(\phi + x)`, acting only on the `\phi` Hilbert subspace. Returns ------- scipy.sparse.csc_matrix """ pt_count = self.grid.pt_count vals = np.sin(self.grid.make_linspace() + x) sin_phi_matrix = sparse.dia_matrix((vals, [0]), shape=(pt_count, pt_count)).tocsc() return sin_phi_matrix def _cos_phi_operator(self, x=0): r""" Operator :math:`\cos(\phi + x)`, acting only on the `\phi` Hilbert subspace. Returns ------- scipy.sparse.csc_matrix """ pt_count = self.grid.pt_count vals = np.cos(self.grid.make_linspace() + x) cos_phi_matrix = sparse.dia_matrix((vals, [0]), shape=(pt_count, pt_count)).tocsc() return cos_phi_matrix def _cos_theta_operator(self): r""" Operator :math:`\cos(\theta)`, acting only on the `\theta` Hilbert subspace. Returns ------- scipy.sparse.csc_matrix """ dim_theta = 2 * self.ncut + 1 cos_theta_matrix = 0.5 * (sparse.dia_matrix( ([1.0] * dim_theta, [-1]), shape=(dim_theta, dim_theta)) + sparse.dia_matrix( ([1.0] * dim_theta, [1]), shape=(dim_theta, dim_theta))).tocsc() return cos_theta_matrix def cos_theta_operator(self): r""" Operator :math:`\cos(\theta)`. Returns ------- scipy.sparse.csc_matrix """ return sparse.kron(self._identity_phi(), self._cos_phi_operator(), format='csc') def _sin_theta_operator(self): r""" Operator :math:`\sin(\theta)`, acting only on the `\theta` Hilbert space. Returns ------- scipy.sparse.csc_matrix """ dim_theta = 2 * self.ncut + 1 sin_theta_matrix = (-0.5 * 1j * (sparse.dia_matrix( ([1.0] * dim_theta, [1]), shape=(dim_theta, dim_theta) ) - sparse.dia_matrix( ([1.0] * dim_theta, [-1]), shape=(dim_theta, dim_theta))).tocsc()) return sin_theta_matrix def sin_theta_operator(self): r""" Operator :math:`\sin(\theta)`. Returns ------- scipy.sparse.csc_matrix """ return sparse.kron(self._identity_phi(), self._sin_theta_operator(), format='csc') def plot_potential(self, theta_grid=None, contour_vals=None, **kwargs): """Draw contour plot of the potential energy. Parameters ---------- theta_grid: Grid1d, optional used for setting a custom grid for theta; if None use self._default_grid contour_vals: list, optional **kwargs: plotting parameters """ theta_grid = theta_grid or self._default_grid x_vals = self.grid.make_linspace() y_vals = theta_grid.make_linspace() return plot.contours(x_vals, y_vals, self.potential, contour_vals=contour_vals, **kwargs) def wavefunction(self, esys=None, which=0, theta_grid=None): """Returns a zero-pi wave function in `phi`, `theta` basis Parameters ---------- esys: ndarray, ndarray eigenvalues, eigenvectors which: int, optional index of desired wave function (default value = 0) theta_grid: Grid1d, optional used for setting a custom grid for theta; if None use self._default_grid Returns ------- WaveFunctionOnGrid object """ evals_count = max(which + 1, 3) if esys is None: _, evecs = self.eigensys(evals_count) else: _, evecs = esys theta_grid = theta_grid or self._default_grid dim_theta = 2 * self.ncut + 1 state_amplitudes = evecs[:, which].reshape(self.grid.pt_count, dim_theta) # Calculate psi_{phi, theta} = sum_n state_amplitudes_{phi, n} A_{n, theta} # where a_{n, theta} = 1/sqrt(2 pi) e^{i n theta} n_vec = np.arange(-self.ncut, self.ncut + 1) theta_vec = theta_grid.make_linspace() a_n_theta = np.exp(1j * np.outer(n_vec, theta_vec)) / (2 * np.pi)**0.5 wavefunc_amplitudes = np.matmul(state_amplitudes, a_n_theta).T wavefunc_amplitudes = standardize_phases(wavefunc_amplitudes) grid2d = GridSpec( np.asarray( [[self.grid.min_val, self.grid.max_val, self.grid.pt_count], [theta_grid.min_val, theta_grid.max_val, theta_grid.pt_count]])) return WaveFunctionOnGrid(grid2d, wavefunc_amplitudes) def plot_wavefunction(self, esys=None, which=0, theta_grid=None, mode='abs', zero_calibrate=True, **kwargs): """Plots 2d phase-basis wave function. Parameters ---------- esys: ndarray, ndarray eigenvalues, eigenvectors as obtained from `.eigensystem()` which: int, optional index of wave function to be plotted (default value = (0) theta_grid: Grid1d, optional used for setting a custom grid for theta; if None use self._default_grid mode: str, optional choices as specified in `constants.MODE_FUNC_DICT` (default value = 'abs_sqr') zero_calibrate: bool, optional if True, colors are adjusted to use zero wavefunction amplitude as the neutral color in the palette **kwargs: plot options Returns ------- Figure, Axes """ theta_grid = theta_grid or self._default_grid amplitude_modifier = constants.MODE_FUNC_DICT[mode] wavefunc = self.wavefunction(esys, theta_grid=theta_grid, which=which) wavefunc.amplitudes = amplitude_modifier(wavefunc.amplitudes) return plot.wavefunction2d(wavefunc, zero_calibrate=zero_calibrate, **kwargs) def set_params_from_dict(self, meta_dict): """Set object parameters by given metadata dictionary Parameters ---------- meta_dict: dict """ for param_name, param_value in meta_dict.items(): if key_in_grid1d(param_name): setattr(self.grid, param_name, param_value) elif is_numerical(param_value): setattr(self, param_name, param_value) @classmethod def create_from_dict(cls, meta_dict): """Set object parameters by given metadata dictionary Parameters ---------- meta_dict: dict """ filtered_dict = {} grid_dict = {} for param_name, param_value in meta_dict.items(): if key_in_grid1d(param_name): grid_dict[param_name] = param_value elif is_numerical(param_value): filtered_dict[param_name] = param_value grid = Grid1d(**grid_dict) filtered_dict['grid'] = grid return cls(**filtered_dict)
class HilbertSpace(DispatchClient): """Class holding information about the full Hilbert space, usually composed of multiple subsystems. The class provides methods to turn subsystem operators into operators acting on the full Hilbert space, and establishes the interface to qutip. Returned operators are of the `qutip.Qobj` type. The class also provides methods for obtaining eigenvalues, absorption and emission spectra as a function of an external parameter. """ osc_subsys_list = ReadOnlyProperty() qbt_subsys_list = ReadOnlyProperty() lookup = ReadOnlyProperty() interaction_list = WatchedProperty('INTERACTIONLIST_UPDATE') def __init__(self, subsystem_list, interaction_list=None): self._subsystems = tuple(subsystem_list) if interaction_list: self.interaction_list = tuple(interaction_list) else: self.interaction_list = None self._lookup = None self._osc_subsys_list = [(index, subsys) for (index, subsys) in enumerate(self) if isinstance(subsys, Oscillator)] self._qbt_subsys_list = [(index, subsys) for (index, subsys) in enumerate(self) if not isinstance(subsys, Oscillator)] CENTRAL_DISPATCH.register('QUANTUMSYSTEM_UPDATE', self) CENTRAL_DISPATCH.register('INTERACTIONTERM_UPDATE', self) CENTRAL_DISPATCH.register('INTERACTIONLIST_UPDATE', self) def __getitem__(self, index): return self._subsystems[index] def __str__(self): output = '====== HilbertSpace object ======\n' for subsystem in self: output += '\n' + str(subsystem) + '\n' return output def index(self, item): return self._subsystems.index(item) def _get_metadata_dict(self): meta_dict = {} for index, subsystem in enumerate(self): subsys_meta = subsystem._get_metadata_dict() renamed_subsys_meta = {} for key in subsys_meta.keys(): renamed_subsys_meta[type(subsystem).__name__ + str(index) + '_' + key] = subsys_meta[key] meta_dict.update(renamed_subsys_meta) return meta_dict def receive(self, event, sender, **kwargs): if self.lookup is not None: if event == 'QUANTUMSYSTEM_UPDATE' and sender in self: self.broadcast('HILBERTSPACE_UPDATE') self._lookup._out_of_sync = True # print('Lookup table now out of sync') elif event == 'INTERACTIONTERM_UPDATE' and sender in self.interaction_list: self.broadcast('HILBERTSPACE_UPDATE') self._lookup._out_of_sync = True # print('Lookup table now out of sync') elif event == 'INTERACTIONLIST_UPDATE' and sender is self: self.broadcast('HILBERTSPACE_UPDATE') self._lookup._out_of_sync = True # print('Lookup table now out of sync') @property def subsystem_dims(self): """Returns list of the Hilbert space dimensions of each subsystem Returns ------- list of int""" return [subsystem.truncated_dim for subsystem in self] @property def dimension(self): """Returns total dimension of joint Hilbert space Returns ------- int""" return np.prod(np.asarray(self.subsystem_dims)) @property def subsystem_count(self): """Returns number of subsystems composing the joint Hilbert space Returns ------- int""" return len(self._subsystems) def generate_lookup(self): bare_specdata_list = [] for index, subsys in enumerate(self): evals, evecs = subsys.eigensys(evals_count=subsys.truncated_dim) bare_specdata_list.append( SpectrumData(energy_table=[evals], state_table=[evecs], system_params=subsys.__dict__)) evals, evecs = self.eigensys(evals_count=self.dimension) dressed_specdata = SpectrumData( energy_table=[evals], state_table=[evecs], system_params=self._get_metadata_dict()) self._lookup = SpectrumLookup(self, bare_specdata_list=bare_specdata_list, dressed_specdata=dressed_specdata) def eigenvals(self, evals_count=6): """Calculates eigenvalues of the full Hamiltonian using `qutip.Qob.eigenenergies()`. Parameters ---------- evals_count: int, optional number of desired eigenvalues/eigenstates Returns ------- eigenvalues: ndarray of float """ hamiltonian_mat = self.hamiltonian() return hamiltonian_mat.eigenenergies(eigvals=evals_count) def eigensys(self, evals_count): """Calculates eigenvalues and eigenvectore of the full Hamiltonian using `qutip.Qob.eigenstates()`. Parameters ---------- evals_count: int, optional number of desired eigenvalues/eigenstates Returns ------- evals: ndarray of float evecs: ndarray of Qobj kets """ hamiltonian_mat = self.hamiltonian() evals, evecs = hamiltonian_mat.eigenstates(eigvals=evals_count) return evals, evecs def diag_operator(self, diag_elements, subsystem): """For given diagonal elements of a diagonal operator in `subsystem`, return the `Qobj` operator for the full Hilbert space (perform wrapping in identities for other subsystems). Parameters ---------- diag_elements: ndarray of floats diagonal elements of subsystem diagonal operator subsystem: object derived from QuantumSystem subsystem where diagonal operator is defined Returns ------- qutip.Qobj operator """ dim = subsystem.truncated_dim index = range(dim) diag_matrix = np.zeros((dim, dim), dtype=np.float_) diag_matrix[index, index] = diag_elements return self.identity_wrap(diag_matrix, subsystem) def diag_hamiltonian(self, subsystem, evals=None): """Returns a `qutip.Qobj` which has the eigenenergies of the object `subsystem` on the diagonal. Parameters ---------- subsystem: object derived from `QuantumSystem` Subsystem for which the Hamiltonian is to be provided. evals: ndarray, optional Eigenenergies can be provided as `evals`; otherwise, they are calculated. Returns ------- qutip.Qobj operator """ evals_count = subsystem.truncated_dim if evals is None: evals = subsystem.eigenvals(evals_count=evals_count) diag_qt_op = qt.Qobj(inpt=np.diagflat(evals[0:evals_count])) return self.identity_wrap(diag_qt_op, subsystem) def identity_wrap(self, operator, subsystem, op_in_eigenbasis=False, evecs=None): """Wrap given operator in subspace `subsystem` in identity operators to form full Hilbert-space operator. Parameters ---------- operator: ndarray or qutip.Qobj or str operator acting in Hilbert space of `subsystem`; if str, then this should be an operator name in the subsystem, typically not in eigenbasis subsystem: object derived from QuantumSystem subsystem where diagonal operator is defined op_in_eigenbasis: bool whether `operator` is given in the `subsystem` eigenbasis; otherwise, the internal QuantumSystem basis is assumed evecs: ndarray, optional internal QuantumSystem eigenstates, used to convert `operator` into eigenbasis Returns ------- qutip.Qobj operator """ subsys_operator = convert_operator_to_qobj(operator, subsystem, op_in_eigenbasis, evecs) operator_identitywrap_list = [ qt.operators.qeye(the_subsys.truncated_dim) for the_subsys in self ] subsystem_index = self.get_subsys_index(subsystem) operator_identitywrap_list[subsystem_index] = subsys_operator return qt.tensor(operator_identitywrap_list) def hubbard_operator(self, j, k, subsystem): """Hubbard operator :math:`|j\\rangle\\langle k|` for system `subsystem` Parameters ---------- j,k: int eigenstate indices for Hubbard operator subsystem: instance derived from QuantumSystem class subsystem in which Hubbard operator acts Returns ------- qutip.Qobj operator """ dim = subsystem.truncated_dim operator = (qt.states.basis(dim, j) * qt.states.basis(dim, k).dag()) return self.identity_wrap(operator, subsystem) def annihilate(self, subsystem): """Annihilation operator a for `subsystem` Parameters ---------- subsystem: object derived from QuantumSystem specifies subsystem in which annihilation operator acts Returns ------- qutip.Qobj operator """ dim = subsystem.truncated_dim operator = (qt.destroy(dim)) return self.identity_wrap(operator, subsystem) def get_subsys_index(self, subsys): """ Return the index of the given subsystem in the HilbertSpace. Parameters ---------- subsys: QuantumSystem Returns ------- int """ return self.index(subsys) def bare_hamiltonian(self): """ Returns ------- qutip.Qobj operator composite Hamiltonian composed of bare Hamiltonians of subsystems independent of the external parameter """ bare_hamiltonian = 0 for subsys in self: evals = subsys.eigenvals(evals_count=subsys.truncated_dim) bare_hamiltonian += self.diag_hamiltonian(subsys, evals) return bare_hamiltonian def get_bare_hamiltonian(self): """Deprecated, use `bare_hamiltonian()` instead.""" warnings.warn( 'bare_hamiltonian() is deprecated, use bare_hamiltonian() instead', FutureWarning) return self.bare_hamiltonian() def hamiltonian(self): """ Returns ------- qutip.qobj Hamiltonian of the composite system, including the interaction between components """ return self.bare_hamiltonian() + self.interaction_hamiltonian() def get_hamiltonian(self): """Deprecated, use `hamiltonian()` instead.""" return self.hamiltonian() def interaction_hamiltonian(self): """ Returns ------- qutip.Qobj operator interaction Hamiltonian """ if self.interaction_list is None: return 0 hamiltonian = [ self.interactionterm_hamiltonian(term) for term in self.interaction_list ] return sum(hamiltonian) def interactionterm_hamiltonian(self, interactionterm, evecs1=None, evecs2=None): interaction_op1 = self.identity_wrap(interactionterm.op1, interactionterm.subsys1, evecs=evecs1) interaction_op2 = self.identity_wrap(interactionterm.op2, interactionterm.subsys2, evecs=evecs2) hamiltonian = interactionterm.g_strength * interaction_op1 * interaction_op2 if interactionterm.add_hc: return hamiltonian + hamiltonian.conj() return hamiltonian def _esys_for_paramval(self, paramval, update_hilbertspace, evals_count): update_hilbertspace(paramval) return self.eigensys(evals_count) def _evals_for_paramval(self, paramval, update_hilbertspace, evals_count): update_hilbertspace(paramval) return self.eigenvals(evals_count) def get_spectrum_vs_paramvals(self, param_vals, update_hilbertspace, evals_count=10, get_eigenstates=False, param_name="external_parameter", num_cpus=settings.NUM_CPUS): """Return eigenvalues (and optionally eigenstates) of the full Hamiltonian as a function of a parameter. Parameter values are specified as a list or array in `param_vals`. The Hamiltonian `hamiltonian_func` must be a function of that particular parameter, and is expected to internally set subsystem parameters. If a `filename` string is provided, then eigenvalue data is written to that file. Parameters ---------- param_vals: ndarray of floats array of parameter values update_hilbertspace: function update_hilbertspace(param_val) specifies how a change in the external parameter affects the Hilbert space components evals_count: int, optional number of desired energy levels (default value = 10) get_eigenstates: bool, optional set to true if eigenstates should be returned as well (default value = False) param_name: str, optional name for the parameter that is varied in `param_vals` (default value = "external_parameter") num_cpus: int, optional number of cores to be used for computation (default value: settings.NUM_CPUS) Returns ------- SpectrumData object """ target_map = get_map_method(num_cpus) if get_eigenstates: func = functools.partial(self._esys_for_paramval, update_hilbertspace=update_hilbertspace, evals_count=evals_count) with InfoBar( "Parallel computation of eigenvalues [num_cpus={}]".format( num_cpus), num_cpus): eigensystem_mapdata = list( target_map( func, tqdm(param_vals, desc='Spectral data', leave=False, disable=(num_cpus > 1)))) eigenvalue_table, eigenstate_table = recast_esys_mapdata( eigensystem_mapdata) else: func = functools.partial(self._evals_for_paramval, update_hilbertspace=update_hilbertspace, evals_count=evals_count) with InfoBar( "Parallel computation of eigensystems [num_cpus={}]". format(num_cpus), num_cpus): eigenvalue_table = list( target_map( func, tqdm(param_vals, desc='Spectral data', leave=False, disable=(num_cpus > 1)))) eigenvalue_table = np.asarray(eigenvalue_table) eigenstate_table = None return SpectrumData(eigenvalue_table, self._get_metadata_dict(), param_name, param_vals, state_table=eigenstate_table)
class FluxQubit(QubitBaseClass): r"""Flux Qubit | [1] Orlando et al., Physical Review B, 60, 15398 (1999). https://link.aps.org/doi/10.1103/PhysRevB.60.15398 The original flux qubit as defined in [1], where the junctions are allowed to have varying junction energies and capacitances to allow for junction asymmetry. Typically, one takes :math:`E_{J1}=E_{J2}=E_J`, and :math:`E_{J3}=\alpha E_J` where :math:`0\le \alpha \le 1`. The same relations typically hold for the junction capacitances. The Hamiltonian is given by .. math:: H_\text{flux}=&(n_{i}-n_{gi})4(E_\text{C})_{ij}(n_{j}-n_{gj}) \\ -&E_{J}\cos\phi_{1}-E_{J}\cos\phi_{2}-\alpha E_{J}\cos(2\pi f + \phi_{1} - \phi_{2}), where :math:`i,j\in\{1,2\}` is represented in the charge basis for both degrees of freedom. Initialize with, for example:: EJ = 35.0 alpha = 0.6 flux_qubit = qubit.FluxQubit(EJ1 = EJ, EJ2 = EJ, EJ3 = alpha*EJ, ECJ1 = 1.0, ECJ2 = 1.0, ECJ3 = 1.0/alpha, ECg1 = 50.0, ECg2 = 50.0, ng1 = 0.0, ng2 = 0.0, flux = 0.5, ncut = 10) Parameters ---------- EJ1, EJ2, EJ3: float Josephson energy of the ith junction `EJ1 = EJ2`, with `EJ3 = alpha * EJ1` and `alpha <= 1` ECJ1, ECJ2, ECJ3: float charging energy associated with the ith junction ECg1, ECg2: float charging energy associated with the capacitive coupling to ground for the two islands ng1, ng2: float offset charge associated with island i flux: float magnetic flux through the circuit loop, measured in units of the flux quantum ncut: int charge number cutoff for the charge on both islands `n`, `n = -ncut, ..., ncut` truncated_dim: int, optional desired dimension of the truncated quantum system """ EJ1 = WatchedProperty('QUANTUMSYSTEM_UPDATE') EJ2 = WatchedProperty('QUANTUMSYSTEM_UPDATE') EJ3 = WatchedProperty('QUANTUMSYSTEM_UPDATE') ECJ1 = WatchedProperty('QUANTUMSYSTEM_UPDATE') ECJ2 = WatchedProperty('QUANTUMSYSTEM_UPDATE') ECJ3 = WatchedProperty('QUANTUMSYSTEM_UPDATE') ECg1 = WatchedProperty('QUANTUMSYSTEM_UPDATE') ECg2 = WatchedProperty('QUANTUMSYSTEM_UPDATE') ng1 = WatchedProperty('QUANTUMSYSTEM_UPDATE') ng2 = WatchedProperty('QUANTUMSYSTEM_UPDATE') flux = WatchedProperty('QUANTUMSYSTEM_UPDATE') ncut = WatchedProperty('QUANTUMSYSTEM_UPDATE') def __init__(self, EJ1, EJ2, EJ3, ECJ1, ECJ2, ECJ3, ECg1, ECg2, ng1, ng2, flux, ncut, truncated_dim=None): self.EJ1 = EJ1 self.EJ2 = EJ2 self.EJ3 = EJ3 self.ECJ1 = ECJ1 self.ECJ2 = ECJ2 self.ECJ3 = ECJ3 self.ECg1 = ECg1 self.ECg2 = ECg2 self.ng1 = ng1 self.ng2 = ng2 self.flux = flux self.ncut = ncut self.truncated_dim = truncated_dim self._sys_type = 'flux qubit' self._evec_dtype = np.complex_ self._default_grid = Grid1d(-np.pi / 2, 3 * np.pi / 2, 100) # for plotting in phi_j basis def EC_matrix(self): """Return the charging energy matrix""" Cmat = np.zeros((2, 2)) CJ1 = 1. / (2 * self.ECJ1) # capacitances in units where e is set to 1 CJ2 = 1. / (2 * self.ECJ2) CJ3 = 1. / (2 * self.ECJ3) Cg1 = 1. / (2 * self.ECg1) Cg2 = 1. / (2 * self.ECg2) Cmat[0, 0] = CJ1 + CJ3 + Cg1 Cmat[1, 1] = CJ2 + CJ3 + Cg2 Cmat[0, 1] = -CJ3 Cmat[1, 0] = -CJ3 return np.linalg.inv(Cmat) / 2. def _evals_calc(self, evals_count): hamiltonian_mat = self.hamiltonian() evals = sp.linalg.eigh(hamiltonian_mat, eigvals=(0, evals_count - 1), eigvals_only=True) return np.sort(evals) def _esys_calc(self, evals_count): hamiltonian_mat = self.hamiltonian() evals, evecs = sp.linalg.eigh(hamiltonian_mat, eigvals=(0, evals_count - 1), eigvals_only=False) evals, evecs = order_eigensystem(evals, evecs) return evals, evecs def hilbertdim(self): """Return Hilbert space dimension.""" return (2 * self.ncut + 1) ** 2 def potential(self, phi1, phi2): """Return value of the potential energy at phi1 and phi2, disregarding constants.""" return (-self.EJ1 * np.cos(phi1) - self.EJ2 * np.cos(phi2) - self.EJ3 * np.cos(2.0 * np.pi * self.flux + phi1 - phi2)) def kineticmat(self): """Return the kinetic energy matrix.""" ECmat = self.EC_matrix() kinetic_mat = 4.0 * ECmat[0, 0] * np.kron(np.matmul(self._n_operator() - self.ng1 * self._identity(), self._n_operator() - self.ng1 * self._identity()), self._identity()) kinetic_mat += 4.0 * ECmat[1, 1] * np.kron(self._identity(), np.matmul(self._n_operator() - self.ng2 * self._identity(), self._n_operator() - self.ng2 * self._identity())) kinetic_mat += 4.0 * (ECmat[0, 1] + ECmat[1, 0]) * np.kron(self._n_operator() - self.ng1 * self._identity(), self._n_operator() - self.ng2 * self._identity()) return kinetic_mat def potentialmat(self): """Return the potential energy matrix for the potential.""" potential_mat = -0.5 * self.EJ1 * np.kron(self._exp_i_phi_operator() + self._exp_i_phi_operator().T, self._identity()) potential_mat += -0.5 * self.EJ2 * np.kron(self._identity(), self._exp_i_phi_operator() + self._exp_i_phi_operator().T) potential_mat += -0.5 * self.EJ3 * (np.exp(1j * 2 * np.pi * self.flux) * np.kron(self._exp_i_phi_operator(), self._exp_i_phi_operator().T)) potential_mat += -0.5 * self.EJ3 * (np.exp(-1j * 2 * np.pi * self.flux) * np.kron(self._exp_i_phi_operator().T, self._exp_i_phi_operator())) return potential_mat def hamiltonian(self): """Return Hamiltonian in basis obtained by employing charge basis for both degrees of freedom""" return self.kineticmat() + self.potentialmat() def _n_operator(self): diag_elements = np.arange(-self.ncut, self.ncut + 1, dtype=np.complex_) return np.diag(diag_elements) def _exp_i_phi_operator(self): dim = 2 * self.ncut + 1 off_diag_elements = np.ones(dim - 1, dtype=np.complex_) e_iphi_matrix = np.diag(off_diag_elements, k=1) return e_iphi_matrix def _identity(self): dim = 2 * self.ncut + 1 return np.eye(dim) def n_1_operator(self): r"""Return charge number operator conjugate to :math:`\phi_1`""" return np.kron(self._n_operator(), self._identity()) def n_2_operator(self): r"""Return charge number operator conjugate to :math:`\phi_2`""" return np.kron(self._identity(), self._n_operator()) def exp_i_phi_1_operator(self): r"""Return operator :math:`e^{i\phi_1}` in the charge basis.""" return np.kron(self._exp_i_phi_operator(), self._identity()) def exp_i_phi_2_operator(self): r"""Return operator :math:`e^{i\phi_2}` in the charge basis.""" return np.kron(self._identity(), self._exp_i_phi_operator()) def cos_phi_1_operator(self): """Return operator :math:`\\cos \\phi_1` in the charge basis""" cos_op = 0.5 * self.exp_i_phi_1_operator() cos_op += cos_op.T return cos_op def cos_phi_2_operator(self): """Return operator :math:`\\cos \\phi_2` in the charge basis""" cos_op = 0.5 * self.exp_i_phi_2_operator() cos_op += cos_op.T return cos_op def sin_phi_1_operator(self): """Return operator :math:`\\sin \\phi_1` in the charge basis""" sin_op = -1j * 0.5 * self.exp_i_phi_1_operator() sin_op += sin_op.conj().T return sin_op def sin_phi_2_operator(self): """Return operator :math:`\\sin \\phi_2` in the charge basis""" sin_op = -1j * 0.5 * self.exp_i_phi_2_operator() sin_op += sin_op.conj().T return sin_op def plot_potential(self, phi_grid=None, contour_vals=None, **kwargs): """ Draw contour plot of the potential energy. Parameters ---------- phi_grid: Grid1d, optional used for setting a custom grid for phi; if None use self._default_grid contour_vals: list of float, optional specific contours to draw **kwargs: plot options """ phi_grid = phi_grid or self._default_grid x_vals = y_vals = phi_grid.make_linspace() if 'figsize' not in kwargs: kwargs['figsize'] = (5, 5) return plot.contours(x_vals, y_vals, self.potential, contour_vals=contour_vals, **kwargs) def wavefunction(self, esys=None, which=0, phi_grid=None): """ Return a flux qubit wave function in phi1, phi2 basis Parameters ---------- esys: ndarray, ndarray eigenvalues, eigenvectors which: int, optional index of desired wave function (default value = 0) phi_grid: Grid1d, optional used for setting a custom grid for phi; if None use self._default_grid Returns ------- WaveFunctionOnGrid object """ evals_count = max(which + 1, 3) if esys is None: _, evecs = self.eigensys(evals_count) else: _, evecs = esys phi_grid = phi_grid or self._default_grid dim = 2 * self.ncut + 1 state_amplitudes = np.reshape(evecs[:, which], (dim, dim)) n_vec = np.arange(-self.ncut, self.ncut + 1) phi_vec = phi_grid.make_linspace() a_1_phi = np.exp(1j * np.outer(phi_vec, n_vec)) / (2 * np.pi) ** 0.5 a_2_phi = a_1_phi.T wavefunc_amplitudes = np.matmul(a_1_phi, state_amplitudes) wavefunc_amplitudes = np.matmul(wavefunc_amplitudes, a_2_phi) wavefunc_amplitudes = standardize_phases(wavefunc_amplitudes) grid2d = GridSpec(np.asarray([[phi_grid.min_val, phi_grid.max_val, phi_grid.pt_count], [phi_grid.min_val, phi_grid.max_val, phi_grid.pt_count]])) return WaveFunctionOnGrid(grid2d, wavefunc_amplitudes) def plot_wavefunction(self, esys=None, which=0, phi_grid=None, mode='abs', zero_calibrate=True, **kwargs): """Plots 2d phase-basis wave function. Parameters ---------- esys: ndarray, ndarray eigenvalues, eigenvectors as obtained from `.eigensystem()` which: int, optional index of wave function to be plotted (default value = (0) phi_grid: Grid1d, optional used for setting a custom grid for phi; if None use self._default_grid mode: str, optional choices as specified in `constants.MODE_FUNC_DICT` (default value = 'abs_sqr') zero_calibrate: bool, optional if True, colors are adjusted to use zero wavefunction amplitude as the neutral color in the palette **kwargs: plot options Returns ------- Figure, Axes """ amplitude_modifier = constants.MODE_FUNC_DICT[mode] wavefunc = self.wavefunction(esys, phi_grid=phi_grid, which=which) wavefunc.amplitudes = amplitude_modifier(wavefunc.amplitudes) if 'figsize' not in kwargs: kwargs['figsize'] = (5, 5) return plot.wavefunction2d(wavefunc, zero_calibrate=zero_calibrate, **kwargs)
class Transmon(QubitBaseClass1d): r"""Class for the Cooper-pair-box and transmon qubit. The Hamiltonian is represented in dense form in the number basis, :math:`H_\text{CPB}=4E_\text{C}(\hat{n}-n_g)^2+\frac{E_\text{J}}{2}(|n\rangle\langle n+1|+\text{h.c.})`. Initialize with, for example:: Transmon(EJ=1.0, EC=2.0, ng=0.2, ncut=30) Parameters ---------- EJ: float Josephson energy EC: float charging energy ng: float offset charge ncut: int charge basis cutoff, `n = -ncut, ..., ncut` truncated_dim: int, optional desired dimension of the truncated quantum system """ EJ = WatchedProperty('QUANTUMSYSTEM_UPDATE') EC = WatchedProperty('QUANTUMSYSTEM_UPDATE') ng = WatchedProperty('QUANTUMSYSTEM_UPDATE') ncut = WatchedProperty('QUANTUMSYSTEM_UPDATE') def __init__(self, EJ, EC, ng, ncut, truncated_dim=None): self.EJ = EJ self.EC = EC self.ng = ng self.ncut = ncut self.truncated_dim = truncated_dim self._sys_type = 'transmon' self._evec_dtype = np.float_ self._default_grid = Grid1d(-np.pi, np.pi, 151) self._default_n_range = (-5, 6) def n_operator(self): """Returns charge operator `n` in the charge basis""" diag_elements = np.arange(-self.ncut, self.ncut + 1, 1) return np.diag(diag_elements) def exp_i_phi_operator(self): """Returns operator :math:`e^{i\\varphi}` in the charge basis""" dimension = self.hilbertdim() entries = np.repeat(1.0, dimension - 1) exp_op = np.diag(entries, -1) return exp_op def cos_phi_operator(self): """Returns operator :math:`\\cos \\varphi` in the charge basis""" cos_op = 0.5 * self.exp_i_phi_operator() cos_op += cos_op.T return cos_op def sin_phi_operator(self): """Returns operator :math:`\\sin \\varphi` in the charge basis""" sin_op = -1j * 0.5 * self.exp_i_phi_operator() sin_op += sin_op.conjugate().T return sin_op def hamiltonian(self): """Returns Hamiltonian in charge basis""" dimension = self.hilbertdim() hamiltonian_mat = np.diag([ 4.0 * self.EC * (ind - self.ncut - self.ng)**2 for ind in range(dimension) ]) ind = np.arange(dimension - 1) hamiltonian_mat[ind, ind + 1] = -self.EJ / 2.0 hamiltonian_mat[ind + 1, ind] = -self.EJ / 2.0 return hamiltonian_mat def hilbertdim(self): """Returns Hilbert space dimension""" return 2 * self.ncut + 1 def potential(self, phi): """Transmon phase-basis potential evaluated at `phi`. Parameters ---------- phi: float phase variable value Returns ------- float """ return -self.EJ * np.cos(phi) def plot_n_wavefunction(self, esys=None, mode='real', which=0, nrange=None, **kwargs): """Plots transmon wave function in charge basis Parameters ---------- esys: tuple(ndarray, ndarray), optional eigenvalues, eigenvectors mode: str from MODE_FUNC_DICT, optional `'abs_sqr', 'abs', 'real', 'imag'` which: int or tuple of ints, optional index or indices of wave functions to plot (default value = 0) nrange: tuple of two ints, optional range of `n` to be included on the x-axis (default value = (-5,6)) **kwargs: plotting parameters Returns ------- Figure, Axes """ if nrange is None: nrange = self._default_n_range n_wavefunc = self.numberbasis_wavefunction(esys, which=which) amplitude_modifier = constants.MODE_FUNC_DICT[mode] n_wavefunc.amplitudes = amplitude_modifier(n_wavefunc.amplitudes) kwargs = { **defaults.wavefunction1d_discrete(mode), **kwargs } # if any duplicates, later ones survive return plot.wavefunction1d_discrete(n_wavefunc, xlim=nrange, **kwargs) def wavefunction1d_defaults(self, mode, evals, wavefunc_count): """Plot defaults for plotting.wavefunction1d. Parameters ---------- mode: str amplitude modifier, needed to give the correct default y label evals: ndarray eigenvalues to include in plot wavefunc_count: int """ ylabel = r'$\psi_j(\varphi)$' ylabel = MODE_STR_DICT[mode](ylabel) options = {'xlabel': r'$\varphi$', 'ylabel': ylabel} if wavefunc_count > 1: ymin = -1.05 * self.EJ ymax = max(1.1 * self.EJ, evals[-1] + 0.05 * (evals[-1] - evals[0])) options['ylim'] = (ymin, ymax) return options def plot_phi_wavefunction(self, esys=None, which=0, phi_grid=None, mode='abs_sqr', scaling=None, **kwargs): """Alias for plot_wavefunction""" return self.plot_wavefunction(esys=esys, which=which, phi_grid=phi_grid, mode=mode, scaling=scaling, **kwargs) def numberbasis_wavefunction(self, esys=None, which=0): """Return the transmon wave function in number basis. The specific index of the wave function to be returned is `which`. Parameters ---------- esys: ndarray, ndarray, optional if `None`, the eigensystem is calculated on the fly; otherwise, the provided eigenvalue, eigenvector arrays as obtained from `.eigensystem()`, are used (default value = None) which: int, optional eigenfunction index (default value = 0) Returns ------- WaveFunction object """ if esys is None: evals_count = max(which + 1, 3) esys = self.eigensys(evals_count) evals, evecs = esys n_vals = np.arange(-self.ncut, self.ncut + 1) return WaveFunction(n_vals, evecs[:, which], evals[which]) def wavefunction(self, esys=None, which=0, phi_grid=None): """Return the transmon wave function in phase basis. The specific index of the wavefunction is `which`. `esys` can be provided, but if set to `None` then it is calculated on the fly. Parameters ---------- esys: tuple(ndarray, ndarray), optional if None, the eigensystem is calculated on the fly; otherwise, the provided eigenvalue, eigenvector arrays as obtained from `.eigensystem()` are used which: int, optional eigenfunction index (default value = 0) phi_grid: Grid1d, optional used for setting a custom grid for phi; if None use self._default_grid Returns ------- WaveFunction object """ if esys is None: evals_count = max(which + 1, 3) esys = self.eigensys(evals_count) evals, _ = esys n_wavefunc = self.numberbasis_wavefunction(esys, which=which) phi_grid = phi_grid or self._default_grid phi_basis_labels = phi_grid.make_linspace() phi_wavefunc_amplitudes = np.empty(phi_grid.pt_count, dtype=np.complex_) for k in range(phi_grid.pt_count): phi_wavefunc_amplitudes[k] = ( (1j**which / math.sqrt(2 * np.pi)) * np.sum(n_wavefunc.amplitudes * np.exp( 1j * phi_basis_labels[k] * n_wavefunc.basis_labels))) return WaveFunction(basis_labels=phi_basis_labels, amplitudes=phi_wavefunc_amplitudes, energy=evals[which])
class ParameterSweep(DispatchClient): """ The ParameterSweep class helps generate spectral and associated data for a composite quantum system, as an externa, parameter, such as flux, is swept over some given interval of values. Upon initialization, these data are calculated and stored internally, so that plots can be generated efficiently. This is of particular use for interactive displays used in the Explorer class. Parameters ---------- param_name: str name of external parameter to be varied param_vals: ndarray array of parameter values evals_count: int number of eigenvalues and eigenstates to be calculated for the composite Hilbert space hilbertspace: HilbertSpace collects all data specifying the Hilbert space of interest subsys_update_list: list or iterable list of subsystems in the Hilbert space which get modified when the external parameter changes update_hilbertspace: function update_hilbertspace(param_val) specifies how a change in the external parameter affects the Hilbert space components num_cpus: int, optional number of CPUS requested for computing the sweep (default value settings.NUM_CPUS) """ param_name = WatchedProperty('PARAMETERSWEEP_UPDATE') param_vals = WatchedProperty('PARAMETERSWEEP_UPDATE') param_count = WatchedProperty('PARAMETERSWEEP_UPDATE') evals_count = WatchedProperty('PARAMETERSWEEP_UPDATE') subsys_update_list = WatchedProperty('PARAMETERSWEEP_UPDATE') update_hilbertspace = WatchedProperty('PARAMETERSWEEP_UPDATE') lookup = ReadOnlyProperty() def __init__(self, param_name, param_vals, evals_count, hilbertspace, subsys_update_list, update_hilbertspace, num_cpus=settings.NUM_CPUS): self.param_name = param_name self.param_vals = param_vals self.param_count = len(param_vals) self.evals_count = evals_count self._hilbertspace = hilbertspace self.subsys_update_list = tuple(subsys_update_list) self.update_hilbertspace = update_hilbertspace self.num_cpus = num_cpus self._lookup = None self._bare_hamiltonian_constant = None CENTRAL_DISPATCH.register('PARAMETERSWEEP_UPDATE', self) CENTRAL_DISPATCH.register('HILBERTSPACE_UPDATE', self) # generate the spectral data sweep if AUTORUN_SWEEP: self.run() def run(self): """Top-level method for generating all parameter sweep data""" self.cause_dispatch() # generate one dispatch before temporarily disabling CENTRAL_DISPATCH settings.DISPATCH_ENABLED = False bare_specdata_list = self._compute_bare_specdata_sweep() dressed_specdata = self._compute_dressed_specdata_sweep(bare_specdata_list) self._lookup = SpectrumLookup(self, dressed_specdata, bare_specdata_list) settings.DISPATCH_ENABLED = True def cause_dispatch(self): self.update_hilbertspace(self.param_vals[0]) def receive(self, event, sender, **kwargs): """Hook to CENTRAL_DISPATCH. This method is accessed by the global CentralDispatch instance whenever an event occurs that ParameterSweep is registered for. In reaction to update events, the lookup table is marked as out of sync. Parameters ---------- event: str type of event being received sender: object identity of sender announcing the event **kwargs """ if self.lookup is not None: if event == 'HILBERTSPACE_UPDATE' and sender is self._hilbertspace: self._lookup._out_of_sync = True # print('Lookup table now out of sync') elif event == 'PARAMETERSWEEP_UPDATE' and sender is self: self._lookup._out_of_sync = True # print('Lookup table now out of sync') def get_subsys(self, index): return self._hilbertspace[index] def get_subsys_index(self, subsys): return self._hilbertspace.get_subsys_index(subsys) @property def osc_subsys_list(self): return self._hilbertspace.osc_subsys_list @property def qbt_subsys_list(self): return self._hilbertspace.qbt_subsys_list @property def subsystem_count(self): return self._hilbertspace.subsystem_count @property def bare_specdata_list(self): return self.lookup._bare_specdata_list @property def dressed_specdata(self): return self.lookup._dressed_specdata def _compute_bare_specdata_sweep(self): """ Pre-calculates all bare spectral data needed for the interactive explorer display. """ bare_eigendata_constant = [self._compute_bare_spectrum_constant()] * self.param_count target_map = get_map_method(self.num_cpus) with InfoBar("Parallel computation of bare eigensystem [num_cpus={}]".format(self.num_cpus), self.num_cpus): bare_eigendata_varying = list( target_map(self._compute_bare_spectrum_varying, tqdm(self.param_vals, desc='Bare spectra', leave=False, disable=(self.num_cpus > 1))) ) bare_specdata_list = self._recast_bare_eigendata(bare_eigendata_constant, bare_eigendata_varying) del bare_eigendata_constant del bare_eigendata_varying return bare_specdata_list def _compute_dressed_specdata_sweep(self, bare_specdata_list): """ Calculates and returns all dressed spectral data. Returns ------- SpectrumData """ self._bare_hamiltonian_constant = self._compute_bare_hamiltonian_constant(bare_specdata_list) param_indices = range(self.param_count) func = functools.partial(self._compute_dressed_eigensystem, bare_specdata_list=bare_specdata_list) target_map = get_map_method(self.num_cpus) with InfoBar("Parallel computation of dressed eigensystem [num_cpus={}]".format(self.num_cpus), self.num_cpus): dressed_eigendata = list(target_map(func, tqdm(param_indices, desc='Dressed spectrum', leave=False, disable=(self.num_cpus > 1)))) dressed_specdata = self._recast_dressed_eigendata(dressed_eigendata) del dressed_eigendata return dressed_specdata def _recast_bare_eigendata(self, static_eigendata, bare_eigendata): """ Parameters ---------- static_eigendata: list of eigensystem tuples bare_eigendata: list of eigensystem tuples Returns ------- list of SpectrumData """ specdata_list = [] for index, subsys in enumerate(self._hilbertspace): if subsys in self.subsys_update_list: eigendata = bare_eigendata else: eigendata = static_eigendata evals_count = subsys.truncated_dim dim = subsys.hilbertdim() esys_dtype = subsys._evec_dtype energy_table = np.empty(shape=(self.param_count, evals_count), dtype=np.float_) state_table = np.empty(shape=(self.param_count, dim, evals_count), dtype=esys_dtype) for j in range(self.param_count): energy_table[j] = eigendata[j][index][0] state_table[j] = eigendata[j][index][1] specdata_list.append(SpectrumData(energy_table, subsys.__dict__, self.param_name, self.param_vals, state_table)) return specdata_list def _recast_dressed_eigendata(self, dressed_eigendata): """ Parameters ---------- dressed_eigendata: list of tuple(evals, qutip evecs) Returns ------- SpectrumData """ evals_count = self.evals_count energy_table = np.empty(shape=(self.param_count, evals_count), dtype=np.float_) state_table = [] # for dressed states, entries are Qobj for j in range(self.param_count): energy_table[j] = dressed_eigendata[j][0] state_table.append(dressed_eigendata[j][1]) specdata = SpectrumData(energy_table, system_params=self._hilbertspace._get_metadata_dict(), param_name=self.param_name, param_vals=self.param_vals, state_table=state_table) return specdata def _compute_bare_hamiltonian_constant(self, bare_specdata_list): """ Returns ------- qutip.Qobj operator composite Hamiltonian composed of bare Hamiltonians of subsystems independent of the external parameter """ static_hamiltonian = 0 for index, subsys in enumerate(self._hilbertspace): if subsys not in self.subsys_update_list: evals = bare_specdata_list[index].energy_table[0] static_hamiltonian += self._hilbertspace.diag_hamiltonian(subsys, evals) return static_hamiltonian def _compute_bare_hamiltonian_varying(self, bare_specdata_list, param_index): """ Parameters ---------- param_index: int position index of current value of the external parameter Returns ------- qutip.Qobj operator composite Hamiltonian consisting of all bare Hamiltonians which depend on the external parameter """ hamiltonian = 0 for index, subsys in enumerate(self._hilbertspace): if subsys in self.subsys_update_list: evals = bare_specdata_list[index].energy_table[param_index] hamiltonian += self._hilbertspace.diag_hamiltonian(subsys, evals) return hamiltonian def _compute_bare_spectrum_constant(self): """ Returns ------- list of (ndarray, ndarray) eigensystem data for each subsystem that is not affected by a change of the external parameter """ eigendata = [] for subsys in self._hilbertspace: if subsys not in self.subsys_update_list: evals_count = subsys.truncated_dim eigendata.append(subsys.eigensys(evals_count=evals_count)) else: eigendata.append(None) return eigendata def _compute_bare_spectrum_varying(self, param_val): """ For given external parameter value obtain the bare eigenspectra of each bare subsystem that is affected by changes in the external parameter. Formulated to be used with Pool.map() Parameters ---------- param_val: float Returns ------- list of tuples(ndarray, ndarray) (evals, evecs) bare eigendata for each subsystem that is parameter-dependent """ eigendata = [] self.update_hilbertspace(param_val) for subsys in self._hilbertspace: if subsys in self.subsys_update_list: evals_count = subsys.truncated_dim subsys_index = self._hilbertspace.index(subsys) eigendata.append(self._hilbertspace[subsys_index].eigensys(evals_count=evals_count)) else: eigendata.append(None) return eigendata def _compute_dressed_eigensystem(self, param_index, bare_specdata_list): hamiltonian = (self._bare_hamiltonian_constant + self._compute_bare_hamiltonian_varying(bare_specdata_list, param_index)) for interaction_term in self._hilbertspace.interaction_list: evecs1 = self._lookup_bare_eigenstates(param_index, interaction_term.subsys1, bare_specdata_list) evecs2 = self._lookup_bare_eigenstates(param_index, interaction_term.subsys2, bare_specdata_list) hamiltonian += self._hilbertspace.interactionterm_hamiltonian(interaction_term, evecs1=evecs1, evecs2=evecs2) return hamiltonian.eigenstates(eigvals=self.evals_count) def _lookup_bare_eigenstates(self, param_index, subsys, bare_specdata_list): """ Parameters ---------- self: ParameterSweep or HilbertSpace param_index: int position index of parameter value in question subsys: QuantumSystem Hilbert space subsystem for which bare eigendata is to be looked up bare_specdata_list: list of SpectrumData may be provided during partial generation of the lookup Returns ------- ndarray bare eigenvectors for the specified subsystem and the external parameter fixed to the value indicated by its index """ subsys_index = self.get_subsys_index(subsys) return bare_specdata_list[subsys_index].state_table[param_index] @property def system_params(self): return self._hilbertspace.__dict__ def new_datastore(self, **kwargs): """Return DataStore object with system/sweep information obtained from self.""" return DataStore(self.system_params, self.param_name, self.param_vals, **kwargs)