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 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 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 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 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: 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 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)