def __init__( self, hilbertspace: HilbertSpace, paramvals_by_name: Dict[str, ndarray], update_hilbertspace: Callable, evals_count: int = 20, subsys_update_info: Optional[Dict[str, List[QuantumSys]]] = None, bare_only: bool = False, autorun: bool = settings.AUTORUN_SWEEP, num_cpus: Optional[int] = None, ) -> None: num_cpus = num_cpus or settings.NUM_CPUS self._parameters = Parameters(paramvals_by_name) self._hilbertspace = hilbertspace self._evals_count = evals_count self._update_hilbertspace = update_hilbertspace self._subsys_update_info = subsys_update_info self._data: Dict[str, Optional[NamedSlotsNdarray]] = {} self._bare_only = bare_only self._num_cpus = num_cpus self.tqdm_disabled = settings.PROGRESSBAR_DISABLED or (num_cpus > 1) self._out_of_sync = False self._current_param_indices = None dispatch.CENTRAL_DISPATCH.register("PARAMETERSWEEP_UPDATE", self) dispatch.CENTRAL_DISPATCH.register("HILBERTSPACE_UPDATE", self) if autorun: self.run()
def __init__(self, paramvals_by_name, hilbertspace, evals_count, _data) -> None: self._parameters = Parameters(paramvals_by_name) self._hilbertspace = hilbertspace self._evals_count = evals_count self._data = _data self._out_of_sync = False self._current_param_indices = None
class ParameterSweep( ParameterSweepBase, SpectrumLookupMixin, dispatch.DispatchClient, serializers.Serializable, ): """ `ParameterSweep` supports array-like access ("pre-slicing") and dict-like access. With dict-like access via string-keywords `<ParameterSweep>[<str>]`, the following data is returned: `"evals"` and `"evecs"` dressed eigenenergies and eigenstates as `NamedSlotsNdarray`; eigenstates are decomposed in the bare product-state basis of the non-interacting subsystems' eigenbases `"bare_evals"` and `"bare_evecs"` bare eigenenergies and eigenstates as `NamedSlotsNdarray` `"lamb"`, `"chi"`, and `"kerr"` dispersive energy coefficients `"<custom sweep>"` NamedSlotsNdarray for custom data generated with `add_sweep`. Array-like access is responsible for "pre-slicing", enable lookup functionality such as `<Sweep>[p1, p2, ...].eigensys()` Parameters ---------- hilbertspace: HilbertSpace object describing the quantum system of interest paramvals_by_name: Dictionary that, for each set of parameter values, specifies a parameter name and the set of values to be used in the sweep. update_hilbertspace: function that updates the associated `hilbertspace` object with a given set of parameters evals_count: number of dressed eigenvalues/eigenstates to keep. (The number of bare eigenvalues/eigenstates is determined for each subsystems by `truncated_dim`.) [default: 20] subsys_update_info: To speed up calculations, the user may provide information that specifies which subsystems are being updated for each of the given parameter sweeps. This information is specified by a dictionary of the following form:: { "<parameter name 1>": [<subsystem a>], "<parameter name 2>": [<subsystem b>, <subsystem c>, ...], ... } This indicates that changes in `<parameter name 1>` only require updates of `<subsystem a>` while leaving other subsystems unchanged. Similarly, sweeping `<parameter name 2>` affects `<subsystem b>`, `<subsystem c>` etc. bare_only: if set to True, only bare eigendata is calculated; useful when performing a sweep for a single quantum system, no interaction (default: False) autorun: Determines whether to directly run the sweep or delay it until `.run()` is called manually. (Default: `settings.AUTORUN_SWEEP=True`) num_cpus: number of CPU cores requested for computing the sweep (default value `settings.NUM_CPUS`) """ def __new__(cls, *args, **kwargs) -> "Union[ParameterSweep, _ParameterSweep]": if args and isinstance(args[0], str) or "param_name" in kwargs: # old-style ParameterSweep interface is being used warnings.warn( "The implementation of the `ParameterSweep` class has changed and this " "old-style interface will cease to be supported in the future.", FutureWarning, ) return _ParameterSweep(*args, **kwargs) else: return super().__new__(cls, *args, **kwargs) def __init__( self, hilbertspace: HilbertSpace, paramvals_by_name: Dict[str, ndarray], update_hilbertspace: Callable, evals_count: int = 20, subsys_update_info: Optional[Dict[str, List[QuantumSys]]] = None, bare_only: bool = False, autorun: bool = settings.AUTORUN_SWEEP, num_cpus: Optional[int] = None, ) -> None: num_cpus = num_cpus or settings.NUM_CPUS self._parameters = Parameters(paramvals_by_name) self._hilbertspace = hilbertspace self._evals_count = evals_count self._update_hilbertspace = update_hilbertspace self._subsys_update_info = subsys_update_info self._data: Dict[str, Optional[NamedSlotsNdarray]] = {} self._bare_only = bare_only self._num_cpus = num_cpus self.tqdm_disabled = settings.PROGRESSBAR_DISABLED or (num_cpus > 1) self._out_of_sync = False self._current_param_indices = None dispatch.CENTRAL_DISPATCH.register("PARAMETERSWEEP_UPDATE", self) dispatch.CENTRAL_DISPATCH.register("HILBERTSPACE_UPDATE", self) if autorun: self.run() def cause_dispatch(self) -> None: initial_parameters = tuple(paramvals[0] for paramvals in self._parameters) self._update_hilbertspace(*initial_parameters) @classmethod def deserialize(cls, iodata: "IOData") -> "StoredSweep": pass def serialize(self) -> "IOData": """ Convert the content of the current class instance into IOData format. Returns ------- IOData """ initdata = { "paramvals_by_name": self._parameters.ordered_dict, "hilbertspace": self._hilbertspace, "evals_count": self._evals_count, "_data": self._data, } iodata = serializers.dict_serialize(initdata) iodata.typename = "StoredSweep" return iodata @property def param_info(self) -> Dict[str, ndarray]: """Return a dictionary of the parameter names and values used in this sweep.""" return self._parameters.paramvals_by_name def run(self) -> None: """Create all sweep data: bare spectral data, dressed spectral data, lookup data and custom sweep data.""" # generate one dispatch before temporarily disabling CENTRAL_DISPATCH self.cause_dispatch() settings.DISPATCH_ENABLED = False self._data["bare_evals"], self._data[ "bare_evecs"] = self._bare_spectrum_sweep() if not self._bare_only: self._data["evals"], self._data[ "evecs"] = self._dressed_spectrum_sweep() self._data["dressed_indices"] = self.generate_lookup() ( self._data["lamb"], self._data["chi"], self._data["kerr"], ) = self._dispersive_coefficients() settings.DISPATCH_ENABLED = True def _bare_spectrum_sweep( self) -> Tuple[NamedSlotsNdarray, NamedSlotsNdarray]: """ The bare energy spectra are computed according to the following scheme. 1. Perform a loop over all subsystems to separately obtain the bare energy eigenvalues and eigenstates for each subsystems. 2. If `update_subsystem_info` is given, remove those sweeps that leave the subsystems fixed. 3. If self._num_cpus > 1, parallelize. Returns ------- NamedSlotsNdarray[<paramname1>, <paramname2>, ..., "subsys"] for evals, likewise for evecs; here, "subsys": 0, 1, ... enumerates subsystems and """ bare_evals = np.empty((self.subsystem_count, ), dtype=object) bare_evecs = np.empty((self.subsystem_count, ), dtype=object) for subsys_index, subsystem in enumerate(self._hilbertspace): bare_esys = self._subsys_bare_spectrum_sweep(subsystem) bare_evals[subsys_index] = NamedSlotsNdarray( np.asarray(bare_esys[..., 0].tolist()), self._parameters.paramvals_by_name, ) bare_evecs[subsys_index] = NamedSlotsNdarray( np.asarray(bare_esys[..., 1].tolist()), self._parameters.paramvals_by_name, ) return ( NamedSlotsNdarray(bare_evals, {"subsys": np.arange(self.subsystem_count)}), NamedSlotsNdarray(bare_evecs, {"subsys": np.arange(self.subsystem_count)}), ) def _update_subsys_compute_esys(self, update_func: Callable, subsystem: QuantumSys, paramval_tuple: Tuple[float]) -> ndarray: update_func(*paramval_tuple) evals, evecs = subsystem.eigensys(evals_count=subsystem.truncated_dim) esys_array = np.empty(shape=(2, ), dtype=object) esys_array[0] = evals esys_array[1] = evecs return esys_array def _paramnames_no_subsys_update(self, subsystem) -> List[str]: if self._subsys_update_info is None: return [] updating_parameters = [ name for name in self._subsys_update_info.keys() if subsystem in self._subsys_update_info[name] ] return list(set(self._parameters.names) - set(updating_parameters)) def _subsys_bare_spectrum_sweep(self, subsystem) -> ndarray: """ Parameters ---------- subsystem: subsystem for which the bare spectrum sweep is to be computed Returns ------- multidimensional array of the format array[p1, p2, p3, ..., pN] = np.asarray[[evals, evecs]] """ fixed_paramnames = self._paramnames_no_subsys_update(subsystem) reduced_parameters = self._parameters.create_reduced(fixed_paramnames) total_count = np.prod( [len(param_vals) for param_vals in reduced_parameters]) multi_cpu = self._num_cpus > 1 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, ) as p: bare_eigendata = tqdm( target_map( functools.partial( self._update_subsys_compute_esys, self._update_hilbertspace, subsystem, ), itertools.product(*reduced_parameters.paramvals_list), ), total=total_count, desc="Bare spectra", leave=False, disable=multi_cpu, ) bare_eigendata = np.asarray(list(bare_eigendata), dtype=object) bare_eigendata = bare_eigendata.reshape( (*reduced_parameters.counts, 2)) # Bare spectral data was only computed once for each parameter that has no # update effect on the subsystems. Now extend the array to reflect this # for the full parameter array by repeating for name in fixed_paramnames: index = self._parameters.index_by_name[name] param_count = self._parameters.counts[index] bare_eigendata = np.repeat(bare_eigendata, param_count, axis=index) return bare_eigendata def _update_and_compute_dressed_esys( self, hilbertspace: HilbertSpace, evals_count: int, update_func: Callable, paramindex_tuple: Tuple[int], ) -> ndarray: paramval_tuple = self._parameters[paramindex_tuple] update_func(*paramval_tuple) assert self._data is not None bare_esys = { subsys_index: [ self._data["bare_evals"][subsys_index][paramindex_tuple], self._data["bare_evecs"][subsys_index][paramindex_tuple], ] for subsys_index, _ in enumerate(self._hilbertspace) } evals, evecs = hilbertspace.eigensys(evals_count=evals_count, bare_esys=bare_esys) esys_array = np.empty(shape=(2, ), dtype=object) esys_array[0] = evals esys_array[1] = evecs return esys_array def _dressed_spectrum_sweep( self, ) -> Tuple[NamedSlotsNdarray, NamedSlotsNdarray]: """ Returns ------- NamedSlotsNdarray[<paramname1>, <paramname2>, ...] of eigenvalues, likewise for eigenvectors """ multi_cpu = self._num_cpus > 1 target_map = cpu_switch.get_map_method(self._num_cpus) total_count = np.prod(self._parameters.counts) with utils.InfoBar( "Parallel compute dressed eigensys [num_cpus={}]".format( self._num_cpus), self._num_cpus, ) as p: spectrum_data = list( tqdm( target_map( functools.partial( self._update_and_compute_dressed_esys, self._hilbertspace, self._evals_count, self._update_hilbertspace, ), itertools.product(*self._parameters.ranges), ), total=total_count, desc="Dressed spectrum", leave=False, disable=multi_cpu, )) spectrum_data = np.asarray(spectrum_data, dtype=object) spectrum_data = spectrum_data.reshape((*self._parameters.counts, 2)) slotparamvals_by_name = OrderedDict( self._parameters.ordered_dict.copy()) evals = np.asarray(spectrum_data[..., 0].tolist()) evecs = spectrum_data[..., 1] return NamedSlotsNdarray(evals, slotparamvals_by_name), NamedSlotsNdarray( evecs, slotparamvals_by_name) def _energies_1(self, subsys): bare_label = np.zeros(len(self._hilbertspace)) bare_label[self.get_subsys_index(subsys)] = 1 energies_all_l = np.empty(self._parameters.counts + (subsys.truncated_dim, )) for l in range(subsys.truncated_dim): energies_all_l[..., l] = self[:].energy_by_bare_index( tuple(l * bare_label)) return energies_all_l def _energies_2(self, subsys1, subsys2): bare_label1 = np.zeros(len(self._hilbertspace)) bare_label1[self.get_subsys_index(subsys1)] = 1 bare_label2 = np.zeros(len(self._hilbertspace)) bare_label2[self.get_subsys_index(subsys2)] = 1 energies_all_l1_l2 = np.empty(self._parameters.counts + (subsys1.truncated_dim, ) + (subsys2.truncated_dim, )) for l1 in range(subsys1.truncated_dim): for l2 in range(subsys2.truncated_dim): energies_all_l1_l2[..., l1, l2] = self[:].energy_by_bare_index( tuple(l1 * bare_label1 + l2 * bare_label2)) return energies_all_l1_l2 def _dispersive_coefficients( self, ) -> Tuple[NamedSlotsNdarray, NamedSlotsNdarray, NamedSlotsNdarray]: energy_0 = self[:].energy_by_dressed_index(0).toarray() lamb_data = np.empty(self.subsystem_count, dtype=object) kerr_data = np.empty((self.subsystem_count, self.subsystem_count), dtype=object) for subsys_index1, subsys1 in enumerate(self._hilbertspace): energy_subsys1_all_l1 = self._energies_1(subsys1) bare_energy_subsys1_all_l1 = self["bare_evals"][ subsys_index1].toarray() lamb_subsys1_all_l1 = ( energy_subsys1_all_l1 - energy_0[..., None] - bare_energy_subsys1_all_l1 + bare_energy_subsys1_all_l1[..., 0][..., None]) lamb_data[subsys_index1] = NamedSlotsNdarray( lamb_subsys1_all_l1, self._parameters.paramvals_by_name) for subsys_index2, subsys2 in enumerate(self._hilbertspace): energy_subsys2_all_l2 = self._energies_1(subsys2) energy_subsys1_subsys2_all_l1_l2 = self._energies_2( subsys1, subsys2) kerr_subsys1_subsys2_all_l1_l2 = ( energy_subsys1_subsys2_all_l1_l2 + energy_0[..., None, None] - energy_subsys1_all_l1[..., :, None] - energy_subsys2_all_l2[..., None, :]) if subsys1 is subsys2: kerr_subsys1_subsys2_all_l1_l2 /= 2.0 # self-Kerr needs factor 1/2 kerr_data[subsys_index1, subsys_index2] = NamedSlotsNdarray( kerr_subsys1_subsys2_all_l1_l2, self._parameters.paramvals_by_name) sys_indices = np.arange(self.subsystem_count) lamb_data = NamedSlotsNdarray(lamb_data, {"subsys": sys_indices}) kerr_data = NamedSlotsNdarray(kerr_data, { "subsys1": sys_indices, "subsys2": sys_indices }) chi_data = kerr_data.copy() for subsys_index1, subsys1 in enumerate(self._hilbertspace): for subsys_index2, subsys2 in enumerate(self._hilbertspace): if subsys1 in self.osc_subsys_list: if subsys2 in self.qbt_subsys_list: chi_data[subsys_index1, subsys_index2] = chi_data[subsys_index1, subsys_index2][..., 1, :] kerr_data[subsys_index1, subsys_index2] = np.asarray([]) else: chi_data[subsys_index1, subsys_index2] = np.asarray([]) elif subsys1 in self.qbt_subsys_list: if subsys2 in self.osc_subsys_list: chi_data[subsys_index1, subsys_index2] = chi_data[ subsys_index1, subsys_index2][..., :, 1] kerr_data[subsys_index1, subsys_index2] = np.asarray([]) else: chi_data[subsys_index1, subsys_index2] = np.asarray([]) return lamb_data, chi_data, kerr_data
def tst_name(): tst = Parameters(paramvals_by_name) assert tst.names[0] == "p1"
def test_paravals_list(): tst = Parameters(paramvals_by_name) assert tst.paramvals_list == [paramvals1, paramvals2]
def test_iterate(): tst = Parameters(paramvals_by_name) lst = [paramvals1, paramvals2] for index, paramvals in enumerate(tst): assert np.allclose(paramvals, lst[index])
def test_params_count(): tst = Parameters(paramvals_by_name) assert tst.counts[1] == len(paramvals2)
def test_get_by_index(): tst = Parameters(paramvals_by_name) assert np.allclose(tst[1], paramvals2)
def test_get_by_name(): tst = Parameters(paramvals_by_name) assert np.allclose(tst["p1"], paramvals1)
def test_initialize_parameters(): tst = Parameters(paramvals_by_name)