Esempio n. 1
0
class StoredSweep(ParameterSweepBase, dispatch.DispatchClient,
                  serializers.Serializable):
    param_name = descriptors.WatchedProperty('PARAMETERSWEEP_UPDATE')
    param_vals = descriptors.WatchedProperty('PARAMETERSWEEP_UPDATE')
    param_count = descriptors.WatchedProperty('PARAMETERSWEEP_UPDATE')
    evals_count = descriptors.WatchedProperty('PARAMETERSWEEP_UPDATE')
    lookup = descriptors.ReadOnlyProperty()

    def __init__(self, param_name: str, param_vals: ndarray, evals_count: int,
                 hilbertspace: HilbertSpace, dressed_specdata: SpectrumData,
                 bare_specdata_list: List[SpectrumData]) -> None:
        self.param_name = param_name
        self.param_vals = param_vals
        self.param_count = len(param_vals)
        self.evals_count = evals_count
        self._hilbertspace = hilbertspace
        self._lookup = spec_lookup.SpectrumLookup(hilbertspace,
                                                  dressed_specdata,
                                                  bare_specdata_list,
                                                  auto_run=False)

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

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

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

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

    def new_sweep(self,
                  subsys_update_list: List[QuantumSys],
                  update_hilbertspace: Callable,
                  num_cpus: int = settings.NUM_CPUS) -> ParameterSweep:
        return ParameterSweep(self.param_name, self.param_vals,
                              self.evals_count, self._hilbertspace,
                              subsys_update_list, update_hilbertspace,
                              num_cpus)
Esempio n. 2
0
class HilbertSpace(dispatch.DispatchClient, serializers.Serializable):
    """Class holding information about the full Hilbert space, usually composed of multiple subsys_list.
    The class provides methods to turn subsystem operators into operators acting on the full Hilbert space, and
    establishes the interface to qutip. Returned operators are of the `qutip.Qobj` type. The class also provides methods
    for obtaining eigenvalues, absorption and emission spectra as a function of an external parameter.
    """
    osc_subsys_list = descriptors.ReadOnlyProperty()
    qbt_subsys_list = descriptors.ReadOnlyProperty()
    lookup = descriptors.ReadOnlyProperty()
    interaction_list = descriptors.WatchedProperty('INTERACTIONLIST_UPDATE')

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

    def receive(self, event, sender, **kwargs):
        if self.lookup is not None:
            if event == 'QUANTUMSYSTEM_UPDATE' and sender in self:
                self.broadcast('HILBERTSPACE_UPDATE')
                self._lookup._out_of_sync = True
            elif event == 'INTERACTIONTERM_UPDATE' and sender in self.interaction_list:
                self.broadcast('HILBERTSPACE_UPDATE')
                self._lookup._out_of_sync = True
            elif event == 'INTERACTIONLIST_UPDATE' and sender is self:
                self.broadcast('HILBERTSPACE_UPDATE')
                self._lookup._out_of_sync = True

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

    @property
    def subsystem_dims(self):
        """Returns list of the Hilbert space dimensions of each subsystem

        Returns
        -------
        list of int"""
        return [subsystem.truncated_dim for subsystem in self]

    @property
    def dimension(self):
        """Returns total dimension of joint Hilbert space

        Returns
        -------
        int"""
        return np.prod(np.asarray(self.subsystem_dims))

    @property
    def subsystem_count(self):
        """Returns number of subsys_list composing the joint Hilbert space

        Returns
        -------
        int"""
        return len(self._subsystems)

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

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

    def eigenvals(self, evals_count=6):
        """Calculates eigenvalues of the full Hamiltonian using `qutip.Qob.eigenenergies()`.

        Parameters
        ----------
        evals_count: int, optional
            number of desired eigenvalues/eigenstates

        Returns
        -------
        eigenvalues: ndarray of float
        """
        hamiltonian_mat = self.hamiltonian()
        return hamiltonian_mat.eigenenergies(eigvals=evals_count)

    def eigensys(self, evals_count):
        """Calculates eigenvalues and eigenvectore of the full Hamiltonian using `qutip.Qob.eigenstates()`.

        Parameters
        ----------
        evals_count: int, optional
            number of desired eigenvalues/eigenstates

        Returns
        -------
        evals: ndarray of float
        evecs: ndarray of Qobj kets
        """
        hamiltonian_mat = self.hamiltonian()
        evals, evecs = hamiltonian_mat.eigenstates(eigvals=evals_count)
        evecs = evecs.view(scqubits.io_utils.fileio_qutip.QutipEigenstates)
        return evals, evecs

    def diag_operator(self, diag_elements, subsystem):
        """For given diagonal elements of a diagonal operator in `subsystem`, return the `Qobj` operator for the
        full Hilbert space (perform wrapping in identities for other subsys_list).

        Parameters
        ----------
        diag_elements: ndarray of floats
            diagonal elements of subsystem diagonal operator
        subsystem: object derived from QuantumSystem
            subsystem where diagonal operator is defined

        Returns
        -------
        qutip.Qobj operator

        """
        dim = subsystem.truncated_dim
        index = range(dim)
        diag_matrix = np.zeros((dim, dim), dtype=np.float_)
        diag_matrix[index, index] = diag_elements
        return self.identity_wrap(diag_matrix, subsystem)

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

        Parameters
        ----------
        subsystem: object derived from `QuantumSystem`
            Subsystem for which the Hamiltonian is to be provided.
        evals: ndarray, optional
            Eigenenergies can be provided as `evals`; otherwise, they are calculated.

        Returns
        -------
        qutip.Qobj operator
        """
        evals_count = subsystem.truncated_dim
        if evals is None:
            evals = subsystem.eigenvals(evals_count=evals_count)
        diag_qt_op = qt.Qobj(inpt=np.diagflat(evals[0:evals_count]))
        return self.identity_wrap(diag_qt_op, subsystem)

    def identity_wrap(self,
                      operator,
                      subsystem,
                      op_in_eigenbasis=False,
                      evecs=None):
        """Wrap given operator in subspace `subsystem` in identity operators to form full Hilbert-space operator.

        Parameters
        ----------
        operator: ndarray or qutip.Qobj or str
            operator acting in Hilbert space of `subsystem`; if str, then this should be an operator name in
            the subsystem, typically not in eigenbasis
        subsystem: object derived from QuantumSystem
            subsystem where diagonal operator is defined
        op_in_eigenbasis: bool
            whether `operator` is given in the `subsystem` eigenbasis; otherwise, the internal QuantumSystem basis is
            assumed
        evecs: ndarray, optional
            internal QuantumSystem eigenstates, used to convert `operator` into eigenbasis

        Returns
        -------
        qutip.Qobj operator
        """
        subsys_operator = spec_utils.convert_operator_to_qobj(
            operator, subsystem, op_in_eigenbasis, evecs)
        operator_identitywrap_list = [
            qt.operators.qeye(the_subsys.truncated_dim) for the_subsys in self
        ]
        subsystem_index = self.get_subsys_index(subsystem)
        operator_identitywrap_list[subsystem_index] = subsys_operator
        return qt.tensor(operator_identitywrap_list)

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

        Parameters
        ----------
        j,k: int
            eigenstate indices for Hubbard operator
        subsystem: instance derived from QuantumSystem class
            subsystem in which Hubbard operator acts

        Returns
        -------
        qutip.Qobj operator
        """
        dim = subsystem.truncated_dim
        operator = (qt.states.basis(dim, j) * qt.states.basis(dim, k).dag())
        return self.identity_wrap(operator, subsystem)

    def annihilate(self, subsystem):
        """Annihilation operator a for `subsystem`

        Parameters
        ----------
        subsystem: object derived from QuantumSystem
            specifies subsystem in which annihilation operator acts

        Returns
        -------
        qutip.Qobj operator
        """
        dim = subsystem.truncated_dim
        operator = (qt.destroy(dim))
        return self.identity_wrap(operator, subsystem)

    def get_subsys_index(self, subsys):
        """
        Return the index of the given subsystem in the HilbertSpace.

        Parameters
        ----------
        subsys: QuantumSystem

        Returns
        -------
        int
        """
        return self.index(subsys)

    def bare_hamiltonian(self):
        """
        Returns
        -------
        qutip.Qobj operator
            composite Hamiltonian composed of bare Hamiltonians of subsys_list independent of the external parameter
        """
        bare_hamiltonian = 0
        for subsys in self:
            evals = subsys.eigenvals(evals_count=subsys.truncated_dim)
            bare_hamiltonian += self.diag_hamiltonian(subsys, evals)
        return bare_hamiltonian

    def get_bare_hamiltonian(self):
        """Deprecated, use `bare_hamiltonian()` instead."""
        warnings.warn(
            'bare_hamiltonian() is deprecated, use bare_hamiltonian() instead',
            FutureWarning)
        return self.bare_hamiltonian()

    def hamiltonian(self):
        """

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

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

    def interaction_hamiltonian(self):
        """
        Returns
        -------
        qutip.Qobj operator
            interaction Hamiltonian
        """
        if not self.interaction_list:
            return 0

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

    def interactionterm_hamiltonian(self,
                                    interactionterm,
                                    evecs1=None,
                                    evecs2=None):
        interaction_op1 = self.identity_wrap(interactionterm.op1,
                                             interactionterm.subsys1,
                                             evecs=evecs1)
        interaction_op2 = self.identity_wrap(interactionterm.op2,
                                             interactionterm.subsys2,
                                             evecs=evecs2)
        hamiltonian = interactionterm.g_strength * interaction_op1 * interaction_op2
        if interactionterm.add_hc:
            return hamiltonian + hamiltonian.dag()
        return hamiltonian

    def _esys_for_paramval(self, paramval, update_hilbertspace, evals_count):
        update_hilbertspace(paramval)
        return self.eigensys(evals_count)

    def _evals_for_paramval(self, paramval, update_hilbertspace, evals_count):
        update_hilbertspace(paramval)
        return self.eigenvals(evals_count)

    def get_spectrum_vs_paramvals(self,
                                  param_vals,
                                  update_hilbertspace,
                                  evals_count=10,
                                  get_eigenstates=False,
                                  param_name="external_parameter",
                                  num_cpus=settings.NUM_CPUS):
        """Return eigenvalues (and optionally eigenstates) of the full Hamiltonian as a function of a parameter.
        Parameter values are specified as a list or array in `param_vals`. The Hamiltonian `hamiltonian_func`
        must be a function of that particular parameter, and is expected to internally set subsystem parameters.
        If a `filename` string is provided, then eigenvalue data is written to that file.

        Parameters
        ----------
        param_vals: ndarray of floats
            array of parameter values
        update_hilbertspace: function
            update_hilbertspace(param_val) specifies how a change in the external parameter affects
            the Hilbert space components
        evals_count: int, optional
            number of desired energy levels (default value = 10)
        get_eigenstates: bool, optional
            set to true if eigenstates should be returned as well (default value = False)
        param_name: str, optional
            name for the parameter that is varied in `param_vals` (default value = "external_parameter")
        num_cpus: int, optional
            number of cores to be used for computation (default value: settings.NUM_CPUS)

        Returns
        -------
        SpectrumData object
        """
        target_map = cpu_switch.get_map_method(num_cpus)
        if get_eigenstates:
            func = functools.partial(self._esys_for_paramval,
                                     update_hilbertspace=update_hilbertspace,
                                     evals_count=evals_count)
            with utils.InfoBar(
                    "Parallel computation of eigenvalues [num_cpus={}]".format(
                        num_cpus), num_cpus):
                eigensystem_mapdata = list(
                    target_map(
                        func,
                        tqdm(param_vals,
                             desc='Spectral data',
                             leave=False,
                             disable=(num_cpus > 1))))
            eigenvalue_table, eigenstate_table = spec_utils.recast_esys_mapdata(
                eigensystem_mapdata)
        else:
            func = functools.partial(self._evals_for_paramval,
                                     update_hilbertspace=update_hilbertspace,
                                     evals_count=evals_count)
            with utils.InfoBar(
                    "Parallel computation of eigensystems [num_cpus={}]".
                    format(num_cpus), num_cpus):
                eigenvalue_table = list(
                    target_map(
                        func,
                        tqdm(param_vals,
                             desc='Spectral data',
                             leave=False,
                             disable=(num_cpus > 1))))
            eigenvalue_table = np.asarray(eigenvalue_table)
            eigenstate_table = None

        return storage.SpectrumData(eigenvalue_table,
                                    self.get_initdata(),
                                    param_name,
                                    param_vals,
                                    state_table=eigenstate_table)
Esempio n. 3
0
class ParameterSweepBase(ABC):
    """
    The ParameterSweepBase class is an abstract base class for ParameterSweep and StoredSweep
    """

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

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

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

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

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

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

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

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

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

        Returns
        -------
            bare eigenvectors for the specified subsystem and the external parameter fixed to the value indicated by
            its index
        """
        subsys_index = self.get_subsys_index(subsys)
        return bare_specdata_list[subsys_index].state_table[
            param_index]  # type: ignore

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

    def new_datastore(self, **kwargs) -> DataStore:
        """Return DataStore object with system/sweep information obtained from self."""
        return storage.DataStore(self.system_params, self.param_name,
                                 self.param_vals, **kwargs)
Esempio n. 4
0
class ParameterSweep(ParameterSweepBase, dispatch.DispatchClient,
                     serializers.Serializable):
    """
    The ParameterSweep class helps generate spectral and associated data for a composite quantum system, as an externa,
    parameter, such as flux, is swept over some given interval of values. Upon initialization, these data are calculated
    and stored internally, so that plots can be generated efficiently. This is of particular use for interactive
    displays used in the Explorer class.

    Parameters
    ----------
    param_name:
        name of external parameter to be varied
    param_vals:
        array of parameter values
    evals_count:
        number of eigenvalues and eigenstates to be calculated for the composite Hilbert space
    hilbertspace:
        collects all data specifying the Hilbert space of interest
    subsys_update_list:
        list of subsys_list in the Hilbert space which get modified when the external parameter changes
    update_hilbertspace:
        update_hilbertspace(param_val) specifies how a change in the external parameter affects
        the Hilbert space components
    num_cpus:
        number of CPUS requested for computing the sweep (default value settings.NUM_CPUS)
    """

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

    def __init__(
        self,
        param_name: str,
        param_vals: ndarray,
        evals_count: int,
        hilbertspace: HilbertSpace,
        subsys_update_list: List[QuantumSys],
        update_hilbertspace: Callable,
        num_cpus: int = settings.NUM_CPUS,
    ) -> None:
        self.param_name = param_name
        self.param_vals = param_vals
        self.param_count = len(param_vals)
        self.evals_count = evals_count
        self._hilbertspace = hilbertspace
        self.subsys_update_list = tuple(subsys_update_list)
        self.update_hilbertspace = update_hilbertspace
        self.num_cpus = num_cpus
        self._lookup: Union[SpectrumLookup, None] = None
        self._bare_hamiltonian_constant: Qobj

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

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

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

    def run(self) -> None:
        """Top-level method for generating all parameter sweep data"""
        self.cause_dispatch(
        )  # generate one dispatch before temporarily disabling CENTRAL_DISPATCH
        settings.DISPATCH_ENABLED = False
        bare_specdata_list = self._compute_bare_specdata_sweep()
        dressed_specdata = self._compute_dressed_specdata_sweep(
            bare_specdata_list)
        self._lookup = spec_lookup.SpectrumLookup(self, dressed_specdata,
                                                  bare_specdata_list)
        settings.DISPATCH_ENABLED = True

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

    def receive(self, event: str, sender: object, **kwargs) -> None:
        """Hook to CENTRAL_DISPATCH. This method is accessed by the global CentralDispatch instance whenever an event
        occurs that ParameterSweep is registered for. In reaction to update events, the lookup table is marked as out
        of sync.

        Parameters
        ----------
        event:
            type of event being received
        sender:
            identity of sender announcing the event
        **kwargs
        """
        if self._lookup is not None:
            if event == "HILBERTSPACE_UPDATE" and sender is self._hilbertspace:
                self._lookup._out_of_sync = True
                # print('Lookup table now out of sync')
            elif event == "PARAMETERSWEEP_UPDATE" and sender is self:
                self._lookup._out_of_sync = True
                # print('Lookup table now out of sync')

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

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

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

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

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

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

    # ParameterSweep: private methods for generating the sweep -------------------------------------------------
    def _compute_bare_specdata_sweep(self) -> List[SpectrumData]:
        """
        Pre-calculates all bare spectral data needed for the interactive explorer display.
        """
        bare_eigendata_constant = [self._compute_bare_spectrum_constant()
                                   ] * self.param_count
        target_map = cpu_switch.get_map_method(self.num_cpus)
        with utils.InfoBar(
                "Parallel compute bare eigensys [num_cpus={}]".format(
                    self.num_cpus),
                self.num_cpus,
        ):
            bare_eigendata_varying = list(
                target_map(
                    self._compute_bare_spectrum_varying,
                    tqdm(
                        self.param_vals,
                        desc="Bare spectra",
                        leave=False,
                        disable=self.tqdm_disabled,
                    ),
                ))
        bare_specdata_list = self._recast_bare_eigendata(
            bare_eigendata_constant, bare_eigendata_varying)
        del bare_eigendata_constant
        del bare_eigendata_varying
        return bare_specdata_list

    def _compute_dressed_specdata_sweep(
            self, bare_specdata_list: List[SpectrumData]) -> SpectrumData:
        """
        Calculates and returns all dressed spectral data.
        """
        self._bare_hamiltonian_constant = self._compute_bare_hamiltonian_constant(
            bare_specdata_list)
        param_indices = range(self.param_count)
        func = functools.partial(self._compute_dressed_eigensystem,
                                 bare_specdata_list=bare_specdata_list)
        target_map = cpu_switch.get_map_method(self.num_cpus)

        with utils.InfoBar(
                "Parallel compute dressed eigensys [num_cpus={}]".format(
                    self.num_cpus),
                self.num_cpus,
        ):
            dressed_eigendata = list(
                target_map(
                    func,
                    tqdm(
                        param_indices,
                        desc="Dressed spectrum",
                        leave=False,
                        disable=self.tqdm_disabled,
                    ),
                ))
        dressed_specdata = self._recast_dressed_eigendata(dressed_eigendata)
        del dressed_eigendata
        return dressed_specdata

    def _recast_bare_eigendata(
        self,
        static_eigendata: List[List[Tuple[ndarray, ndarray]]],
        bare_eigendata: List[List[Tuple[ndarray, ndarray]]],
    ) -> List[SpectrumData]:
        specdata_list = []
        for index, subsys in enumerate(self._hilbertspace):
            if subsys in self.subsys_update_list:
                eigendata = bare_eigendata
            else:
                eigendata = static_eigendata
            evals_count = subsys.truncated_dim
            dim = subsys.hilbertdim()
            esys_dtype = subsys._evec_dtype

            energy_table = np.empty(shape=(self.param_count, evals_count),
                                    dtype=np.float_)
            state_table = np.empty(shape=(self.param_count, dim, evals_count),
                                   dtype=esys_dtype)
            for j in range(self.param_count):
                energy_table[j] = eigendata[j][index][0]
                state_table[j] = eigendata[j][index][1]
            specdata_list.append(
                storage.SpectrumData(
                    energy_table,
                    system_params={},
                    param_name=self.param_name,
                    param_vals=self.param_vals,
                    state_table=state_table,
                ))
        return specdata_list

    def _recast_dressed_eigendata(
        self,
        dressed_eigendata: List[Tuple[ndarray,
                                      QutipEigenstates]]) -> SpectrumData:
        evals_count = self.evals_count
        energy_table = np.empty(shape=(self.param_count, evals_count),
                                dtype=np.float_)
        state_table = []  # for dressed states, entries are Qobj
        for j in range(self.param_count):
            energy_table[j] = np.real_if_close(dressed_eigendata[j][0])
            state_table.append(dressed_eigendata[j][1])
        specdata = storage.SpectrumData(
            energy_table,
            system_params={},
            param_name=self.param_name,
            param_vals=self.param_vals,
            state_table=state_table,
        )
        return specdata

    def _compute_bare_hamiltonian_constant(
            self, bare_specdata_list: List[SpectrumData]) -> Qobj:
        """
        Returns
        -------
            composite Hamiltonian composed of bare Hamiltonians of subsys_list independent of the external parameter
        """
        static_hamiltonian = 0
        for index, subsys in enumerate(self._hilbertspace):
            if subsys not in self.subsys_update_list:
                evals = bare_specdata_list[index].energy_table[0]
                static_hamiltonian += self._hilbertspace.diag_hamiltonian(
                    subsys, evals)
        return static_hamiltonian

    def _compute_bare_hamiltonian_varying(
            self, bare_specdata_list: List[SpectrumData],
            param_index: int) -> Qobj:
        """
        Parameters
        ----------
        param_index:
            position index of current value of the external parameter

        Returns
        -------
            composite Hamiltonian consisting of all bare Hamiltonians which depend on the external parameter
        """
        hamiltonian = 0
        for index, subsys in enumerate(self._hilbertspace):
            if subsys in self.subsys_update_list:
                evals = bare_specdata_list[index].energy_table[param_index]
                hamiltonian += self._hilbertspace.diag_hamiltonian(
                    subsys, evals)
        return hamiltonian

    def _compute_bare_spectrum_constant(self) -> List[Tuple[ndarray, ndarray]]:
        """
        Returns
        -------
            eigensystem data for each subsystem that is not affected by a change of the external parameter
        """
        eigendata = []
        for subsys in self._hilbertspace:
            if subsys not in self.subsys_update_list:
                evals_count = subsys.truncated_dim
                eigendata.append(subsys.eigensys(evals_count=evals_count))
            else:
                eigendata.append(None)  # type: ignore
        return eigendata

    def _compute_bare_spectrum_varying(
            self, param_val: float) -> List[Tuple[ndarray, ndarray]]:
        """
        For given external parameter value obtain the bare eigenspectra of each bare subsystem that is affected by
        changes in the external parameter. Formulated to be used with Pool.map()

        Returns
        -------
            (evals, evecs) bare eigendata for each subsystem that is parameter-dependent
        """
        eigendata = []
        self.update_hilbertspace(param_val)
        for subsys in self._hilbertspace:
            if subsys in self.subsys_update_list:
                evals_count = subsys.truncated_dim
                subsys_index = self._hilbertspace.get_subsys_index(subsys)
                eigendata.append(self._hilbertspace[subsys_index].eigensys(
                    evals_count=evals_count))
            else:
                eigendata.append(None)  # type: ignore
        return eigendata

    def _compute_dressed_eigensystem(
        self, param_index: int, bare_specdata_list: List[SpectrumData]
    ) -> Tuple[ndarray, QutipEigenstates]:
        hamiltonian = (self._bare_hamiltonian_constant +
                       self._compute_bare_hamiltonian_varying(
                           bare_specdata_list, param_index))

        for interaction_term in self._hilbertspace.interaction_list:
            evecs1 = self._lookup_bare_eigenstates(param_index,
                                                   interaction_term.subsys1,
                                                   bare_specdata_list)
            evecs2 = self._lookup_bare_eigenstates(param_index,
                                                   interaction_term.subsys2,
                                                   bare_specdata_list)
            hamiltonian += self._hilbertspace.interactionterm_hamiltonian(
                interaction_term, evecs1=evecs1, evecs2=evecs2)
        evals, evecs = hamiltonian.eigenstates(eigvals=self.evals_count)
        evecs = evecs.view(qutip_serializer.QutipEigenstates)
        return evals, evecs

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

        Returns
        -------
            bare eigenvectors for the specified subsystem and the external parameter fixed to the value indicated by
            its index
        """
        subsys_index = self.get_subsys_index(subsys)
        return bare_specdata_list[subsys_index].state_table[
            param_index]  # type: ignore
Esempio n. 5
0
class HilbertSpace(dispatch.DispatchClient, serializers.Serializable):
    """Class holding information about the full Hilbert space, usually composed of
    multiple subsys_list. The class provides methods to turn subsystem operators into
    operators acting on the full Hilbert space, and establishes the interface to
    qutip. Returned operators are of the `qutip.Qobj` type. The class also provides
    methods for obtaining eigenvalues, absorption and emission spectra as a function
    of an external parameter.
    """

    osc_subsys_list = descriptors.ReadOnlyProperty()
    qbt_subsys_list = descriptors.ReadOnlyProperty()
    lookup = descriptors.ReadOnlyProperty()
    interaction_list = descriptors.WatchedProperty("INTERACTIONLIST_UPDATE")

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

        Parameters
        ----------
        subsystem:
            Subsystem for which the Hamiltonian is to be provided.
        evals:
            Eigenenergies can be provided as `evals`; otherwise, they are calculated.
        """
        evals_count = subsystem.truncated_dim
        if evals is None:
            evals = subsystem.eigenvals(evals_count=evals_count)
        diag_qt_op = qt.Qobj(inpt=np.diagflat(evals[0:evals_count]))
        return spec_utils.identity_wrap(diag_qt_op, subsystem,
                                        self.subsys_list)

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

    def diag_operator(self, diag_elements: ndarray,
                      subsystem: QuantumSys) -> Qobj:
        """For given diagonal elements of a diagonal operator in `subsystem`, return
        the `Qobj` operator for the full Hilbert space (perform wrapping in
        identities for other subsys_list).

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

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

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

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

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

    ###################################################################################
    # HilbertSpace: spectrum sweep
    ###################################################################################
    def get_spectrum_vs_paramvals(
        self,
        param_vals: ndarray,
        update_hilbertspace: Callable,
        evals_count: int = 10,
        get_eigenstates: bool = False,
        param_name: str = "external_parameter",
        num_cpus: Optional[int] = None,
    ) -> SpectrumData:
        """Return eigenvalues (and optionally eigenstates) of the full Hamiltonian as
        a function of a parameter. Parameter values are specified as a list or array
        in `param_vals`. The Hamiltonian `hamiltonian_func` must be a function of
        that particular parameter, and is expected to internally set subsystem
        parameters. If a `filename` string is provided, then eigenvalue data is
        written to that file.

        Parameters
        ----------
        param_vals:
            array of parameter values
        update_hilbertspace:
            update_hilbertspace(param_val) specifies how a change in the external
            parameter affects the Hilbert space components
        evals_count:
            number of desired energy levels (default value = 10)
        get_eigenstates:
            set to true if eigenstates should be returned as well
            (default value = False)
        param_name:
            name for the parameter that is varied in `param_vals`
            (default value = "external_parameter")
        num_cpus:
            number of cores to be used for computation
            (default value: settings.NUM_CPUS)
        """
        num_cpus = num_cpus or settings.NUM_CPUS
        target_map = cpu_switch.get_map_method(num_cpus)
        if get_eigenstates:
            func = functools.partial(
                self._esys_for_paramval,
                update_hilbertspace=update_hilbertspace,
                evals_count=evals_count,
            )
            with utils.InfoBar(
                    "Parallel computation of eigenvalues [num_cpus={}]".format(
                        num_cpus),
                    num_cpus,
            ):
                eigensystem_mapdata = list(
                    target_map(
                        func,
                        tqdm(
                            param_vals,
                            desc="Spectral data",
                            leave=False,
                            disable=(num_cpus > 1),
                        ),
                    ))
            eigenvalue_table, eigenstate_table = spec_utils.recast_esys_mapdata(
                eigensystem_mapdata)
        else:
            func = functools.partial(
                self._evals_for_paramval,
                update_hilbertspace=update_hilbertspace,
                evals_count=evals_count,
            )
            with utils.InfoBar(
                    "Parallel computation of eigensystems [num_cpus={}]".
                    format(num_cpus),
                    num_cpus,
            ):
                eigenvalue_table = list(
                    target_map(
                        func,
                        tqdm(
                            param_vals,
                            desc="Spectral data",
                            leave=False,
                            disable=(num_cpus > 1),
                        ),
                    ))
            eigenvalue_table = np.asarray(eigenvalue_table)
            eigenstate_table = None  # type: ignore

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

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

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

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

            signature::

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

            Alternatively, specify subsystem operators via callable methods.

            signature::

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

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

                .add_interaction(qobj=<Qobj>)


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

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

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

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

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

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

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

        return InteractionTerm(g, operator_list, add_hc=add_hc)

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

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

    def _parse_op(
        self, op: Union[Callable, Tuple[Union[ndarray, csc_matrix],
                                        QuantumSys]]
    ) -> Tuple[int, Union[ndarray, csc_matrix]]:
        if callable(op):
            return self.get_subsys_index(op.__self__), op()
        if not isinstance(op, tuple):
            raise TypeError(
                "Cannot interpret specified operator {}".format(op))
        if len(op) == 2:
            # format expected:  (<op as array>, <subsys as QuantumSystem>)
            return self.get_subsys_index(op[1]), op[0]
        raise TypeError("Cannot interpret specified operator {}".format(op))
Esempio n. 6
0
class HilbertSpace(dispatch.DispatchClient, serializers.Serializable):
    """Class holding information about the full Hilbert space, usually composed of multiple subsys_list.
    The class provides methods to turn subsystem operators into operators acting on the full Hilbert space, and
    establishes the interface to qutip. Returned operators are of the `qutip.Qobj` type. The class also provides methods
    for obtaining eigenvalues, absorption and emission spectra as a function of an external parameter.
    """
    osc_subsys_list = descriptors.ReadOnlyProperty()
    qbt_subsys_list = descriptors.ReadOnlyProperty()
    lookup = descriptors.ReadOnlyProperty()
    interaction_list = descriptors.WatchedProperty('INTERACTIONLIST_UPDATE')

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

    def bare_hamiltonian(self) -> Qobj:
        """
        Returns
        -------
            composite Hamiltonian composed of bare Hamiltonians of subsys_list independent of the external parameter
        """
        bare_hamiltonian = 0
        for subsys in self:
            evals = subsys.eigenvals(evals_count=subsys.truncated_dim)
            bare_hamiltonian += self.diag_hamiltonian(subsys, evals)
        return bare_hamiltonian

    def interaction_hamiltonian(self) -> Qobj:
        """
        Returns
        -------
            interaction Hamiltonian
        """
        if not self.interaction_list:
            return 0

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

    def interactionterm_hamiltonian(self,
                                    interactionterm: InteractionTerm,
                                    evecs1: ndarray = None,
                                    evecs2: ndarray = None) -> Qobj:
        interaction_op1 = self.identity_wrap(interactionterm.op1,
                                             interactionterm.subsys1,
                                             evecs=evecs1)
        interaction_op2 = self.identity_wrap(interactionterm.op2,
                                             interactionterm.subsys2,
                                             evecs=evecs2)
        hamiltonian = interactionterm.g_strength * interaction_op1 * interaction_op2
        if interactionterm.add_hc:
            return hamiltonian + hamiltonian.dag()
        return hamiltonian

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

        Parameters
        ----------
        subsystem:
            Subsystem for which the Hamiltonian is to be provided.
        evals:
            Eigenenergies can be provided as `evals`; otherwise, they are calculated.
        """
        evals_count = subsystem.truncated_dim
        if evals is None:
            evals = subsystem.eigenvals(evals_count=evals_count)
        diag_qt_op = qt.Qobj(inpt=np.diagflat(evals[0:evals_count]))
        return self.identity_wrap(diag_qt_op, subsystem)

    def get_bare_hamiltonian(self) -> Qobj:
        """Deprecated, use `bare_hamiltonian()` instead."""
        warnings.warn(
            'get_bare_hamiltonian() is deprecated, use bare_hamiltonian() instead',
            FutureWarning)
        return self.bare_hamiltonian()

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

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

        Parameters
        ----------
        operator:
            operator acting in Hilbert space of `subsystem`; if str, then this should be an operator name in
            the subsystem, typically not in eigenbasis
        subsystem:
            subsystem where diagonal operator is defined
        op_in_eigenbasis:
            whether `operator` is given in the `subsystem` eigenbasis; otherwise, the internal QuantumSys basis is
            assumed
        evecs:
            internal QuantumSys eigenstates, used to convert `operator` into eigenbasis
        """
        subsys_operator = spec_utils.convert_operator_to_qobj(
            operator, subsystem, op_in_eigenbasis, evecs)
        operator_identitywrap_list = [
            qt.operators.qeye(the_subsys.truncated_dim) for the_subsys in self
        ]
        subsystem_index = self.get_subsys_index(subsystem)
        operator_identitywrap_list[subsystem_index] = subsys_operator
        return qt.tensor(operator_identitywrap_list)

    def diag_operator(self, diag_elements: ndarray,
                      subsystem: QuantumSys) -> Qobj:
        """For given diagonal elements of a diagonal operator in `subsystem`, return the `Qobj` operator for the
        full Hilbert space (perform wrapping in identities for other subsys_list).

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

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

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

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

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

    ###############################################################################################
    # HilbertSpace: spectrum sweep
    ###############################################################################################
    def get_spectrum_vs_paramvals(
            self,
            param_vals: ndarray,
            update_hilbertspace: Callable,
            evals_count: int = 10,
            get_eigenstates: bool = False,
            param_name: str = "external_parameter",
            num_cpus: int = settings.NUM_CPUS) -> SpectrumData:
        """Return eigenvalues (and optionally eigenstates) of the full Hamiltonian as a function of a parameter.
        Parameter values are specified as a list or array in `param_vals`. The Hamiltonian `hamiltonian_func`
        must be a function of that particular parameter, and is expected to internally set subsystem parameters.
        If a `filename` string is provided, then eigenvalue data is written to that file.

        Parameters
        ----------
        param_vals:
            array of parameter values
        update_hilbertspace:
            update_hilbertspace(param_val) specifies how a change in the external parameter affects
            the Hilbert space components
        evals_count:
            number of desired energy levels (default value = 10)
        get_eigenstates:
            set to true if eigenstates should be returned as well (default value = False)
        param_name:
            name for the parameter that is varied in `param_vals` (default value = "external_parameter")
        num_cpus:
            number of cores to be used for computation (default value: settings.NUM_CPUS)
        """
        target_map = cpu_switch.get_map_method(num_cpus)
        if get_eigenstates:
            func = functools.partial(self._esys_for_paramval,
                                     update_hilbertspace=update_hilbertspace,
                                     evals_count=evals_count)
            with utils.InfoBar(
                    "Parallel computation of eigenvalues [num_cpus={}]".format(
                        num_cpus), num_cpus):
                eigensystem_mapdata = list(
                    target_map(
                        func,
                        tqdm(param_vals,
                             desc='Spectral data',
                             leave=False,
                             disable=(num_cpus > 1))))
            eigenvalue_table, eigenstate_table = spec_utils.recast_esys_mapdata(
                eigensystem_mapdata)
        else:
            func = functools.partial(self._evals_for_paramval,
                                     update_hilbertspace=update_hilbertspace,
                                     evals_count=evals_count)
            with utils.InfoBar(
                    "Parallel computation of eigensystems [num_cpus={}]".
                    format(num_cpus), num_cpus):
                eigenvalue_table = list(
                    target_map(
                        func,
                        tqdm(param_vals,
                             desc='Spectral data',
                             leave=False,
                             disable=(num_cpus > 1))))
            eigenvalue_table = np.asarray(eigenvalue_table)
            eigenstate_table = None  # type: ignore

        return storage.SpectrumData(eigenvalue_table,
                                    self.get_initdata(),
                                    param_name,
                                    param_vals,
                                    state_table=eigenstate_table)
Esempio n. 7
0
class ParameterSweep(ParameterSweepBase, dispatch.DispatchClient,
                     serializers.Serializable):
    """
    The ParameterSweep class helps generate spectral and associated data for a composite quantum system, as an externa,
    parameter, such as flux, is swept over some given interval of values. Upon initialization, these data are calculated
    and stored internally, so that plots can be generated efficiently. This is of particular use for interactive
    displays used in the Explorer class.

    Parameters
    ----------
    param_name: str
        name of external parameter to be varied
    param_vals: ndarray
        array of parameter values
    evals_count: int
        number of eigenvalues and eigenstates to be calculated for the composite Hilbert space
    hilbertspace: HilbertSpace
        collects all data specifying the Hilbert space of interest
    subsys_update_list: list or iterable
        list of subsystems in the Hilbert space which get modified when the external parameter changes
    update_hilbertspace: function
        update_hilbertspace(param_val) specifies how a change in the external parameter affects
        the Hilbert space components
    num_cpus: int, optional
        number of CPUS requested for computing the sweep (default value settings.NUM_CPUS)
    """
    param_name = descriptors.WatchedProperty('PARAMETERSWEEP_UPDATE')
    param_vals = descriptors.WatchedProperty('PARAMETERSWEEP_UPDATE')
    param_count = descriptors.WatchedProperty('PARAMETERSWEEP_UPDATE')
    evals_count = descriptors.WatchedProperty('PARAMETERSWEEP_UPDATE')
    subsys_update_list = descriptors.WatchedProperty('PARAMETERSWEEP_UPDATE')
    update_hilbertspace = descriptors.WatchedProperty('PARAMETERSWEEP_UPDATE')
    lookup = descriptors.ReadOnlyProperty()

    def __init__(self,
                 param_name,
                 param_vals,
                 evals_count,
                 hilbertspace,
                 subsys_update_list,
                 update_hilbertspace,
                 num_cpus=settings.NUM_CPUS):
        self.param_name = param_name
        self.param_vals = param_vals
        self.param_count = len(param_vals)
        self.evals_count = evals_count
        self._hilbertspace = hilbertspace
        self.subsys_update_list = tuple(subsys_update_list)
        self.update_hilbertspace = update_hilbertspace
        self.num_cpus = num_cpus

        self._lookup = None
        self._bare_hamiltonian_constant = None

        # setup for file Serializable

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

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

    def run(self):
        """Top-level method for generating all parameter sweep data"""
        self.cause_dispatch(
        )  # generate one dispatch before temporarily disabling CENTRAL_DISPATCH
        settings.DISPATCH_ENABLED = False
        bare_specdata_list = self._compute_bare_specdata_sweep()
        dressed_specdata = self._compute_dressed_specdata_sweep(
            bare_specdata_list)
        self._lookup = spec_lookup.SpectrumLookup(self, dressed_specdata,
                                                  bare_specdata_list)
        settings.DISPATCH_ENABLED = True

    def cause_dispatch(self):
        self.update_hilbertspace(self.param_vals[0])

    def receive(self, event, sender, **kwargs):
        """Hook to CENTRAL_DISPATCH. This method is accessed by the global CentralDispatch instance whenever an event
        occurs that ParameterSweep is registered for. In reaction to update events, the lookup table is marked as out
        of sync.

        Parameters
        ----------
        event: str
            type of event being received
        sender: object
            identity of sender announcing the event
        **kwargs
        """
        if self.lookup is not None:
            if event == 'HILBERTSPACE_UPDATE' and sender is self._hilbertspace:
                self._lookup._out_of_sync = True
                # print('Lookup table now out of sync')
            elif event == 'PARAMETERSWEEP_UPDATE' and sender is self:
                self._lookup._out_of_sync = True
                # print('Lookup table now out of sync')

    def _compute_bare_specdata_sweep(self):
        """
        Pre-calculates all bare spectral data needed for the interactive explorer display.
        """
        bare_eigendata_constant = [self._compute_bare_spectrum_constant()
                                   ] * self.param_count
        target_map = cpu_switch.get_map_method(self.num_cpus)
        with utils.InfoBar(
                "Parallel compute bare eigensys [num_cpus={}]".format(
                    self.num_cpus), self.num_cpus):
            bare_eigendata_varying = list(
                target_map(
                    self._compute_bare_spectrum_varying,
                    tqdm(self.param_vals,
                         desc='Bare spectra',
                         leave=False,
                         disable=(self.num_cpus > 1))))
        bare_specdata_list = self._recast_bare_eigendata(
            bare_eigendata_constant, bare_eigendata_varying)
        del bare_eigendata_constant
        del bare_eigendata_varying
        return bare_specdata_list

    def _compute_dressed_specdata_sweep(self, bare_specdata_list):
        """
        Calculates and returns all dressed spectral data.

        Returns
        -------
        SpectrumData
        """
        self._bare_hamiltonian_constant = self._compute_bare_hamiltonian_constant(
            bare_specdata_list)
        param_indices = range(self.param_count)
        func = functools.partial(self._compute_dressed_eigensystem,
                                 bare_specdata_list=bare_specdata_list)
        target_map = cpu_switch.get_map_method(self.num_cpus)

        with utils.InfoBar(
                "Parallel compute dressed eigensys [num_cpus={}]".format(
                    self.num_cpus), self.num_cpus):
            dressed_eigendata = list(
                target_map(
                    func,
                    tqdm(param_indices,
                         desc='Dressed spectrum',
                         leave=False,
                         disable=(self.num_cpus > 1))))
        dressed_specdata = self._recast_dressed_eigendata(dressed_eigendata)
        del dressed_eigendata
        return dressed_specdata

    def _recast_bare_eigendata(self, static_eigendata, bare_eigendata):
        """
        Parameters
        ----------
        static_eigendata: list of eigensystem tuples
        bare_eigendata: list of eigensystem tuples

        Returns
        -------
        list of SpectrumData
        """
        specdata_list = []
        for index, subsys in enumerate(self._hilbertspace):
            if subsys in self.subsys_update_list:
                eigendata = bare_eigendata
            else:
                eigendata = static_eigendata
            evals_count = subsys.truncated_dim
            dim = subsys.hilbertdim()
            esys_dtype = subsys._evec_dtype

            energy_table = np.empty(shape=(self.param_count, evals_count),
                                    dtype=np.float_)
            state_table = np.empty(shape=(self.param_count, dim, evals_count),
                                   dtype=esys_dtype)
            for j in range(self.param_count):
                energy_table[j] = eigendata[j][index][0]
                state_table[j] = eigendata[j][index][1]
            specdata_list.append(
                storage.SpectrumData(energy_table,
                                     system_params={},
                                     param_name=self.param_name,
                                     param_vals=self.param_vals,
                                     state_table=state_table))
        return specdata_list

    def _recast_dressed_eigendata(self, dressed_eigendata):
        """
        Parameters
        ----------
        dressed_eigendata: list of tuple(evals, qutip evecs)

        Returns
        -------
        SpectrumData
        """
        evals_count = self.evals_count
        energy_table = np.empty(shape=(self.param_count, evals_count),
                                dtype=np.float_)
        state_table = []  # for dressed states, entries are Qobj
        for j in range(self.param_count):
            energy_table[j] = dressed_eigendata[j][0]
            state_table.append(dressed_eigendata[j][1])
        specdata = storage.SpectrumData(energy_table,
                                        system_params={},
                                        param_name=self.param_name,
                                        param_vals=self.param_vals,
                                        state_table=state_table)
        return specdata

    def _compute_bare_hamiltonian_constant(self, bare_specdata_list):
        """
        Returns
        -------
        qutip.Qobj operator
            composite Hamiltonian composed of bare Hamiltonians of subsystems independent of the external parameter
        """
        static_hamiltonian = 0
        for index, subsys in enumerate(self._hilbertspace):
            if subsys not in self.subsys_update_list:
                evals = bare_specdata_list[index].energy_table[0]
                static_hamiltonian += self._hilbertspace.diag_hamiltonian(
                    subsys, evals)
        return static_hamiltonian

    def _compute_bare_hamiltonian_varying(self, bare_specdata_list,
                                          param_index):
        """
        Parameters
        ----------
        param_index: int
            position index of current value of the external parameter

        Returns
        -------
        qutip.Qobj operator
            composite Hamiltonian consisting of all bare Hamiltonians which depend on the external parameter
        """
        hamiltonian = 0
        for index, subsys in enumerate(self._hilbertspace):
            if subsys in self.subsys_update_list:
                evals = bare_specdata_list[index].energy_table[param_index]
                hamiltonian += self._hilbertspace.diag_hamiltonian(
                    subsys, evals)
        return hamiltonian

    def _compute_bare_spectrum_constant(self):
        """
        Returns
        -------
        list of (ndarray, ndarray)
            eigensystem data for each subsystem that is not affected by a change of the external parameter
        """
        eigendata = []
        for subsys in self._hilbertspace:
            if subsys not in self.subsys_update_list:
                evals_count = subsys.truncated_dim
                eigendata.append(subsys.eigensys(evals_count=evals_count))
            else:
                eigendata.append(None)
        return eigendata

    def _compute_bare_spectrum_varying(self, param_val):
        """
        For given external parameter value obtain the bare eigenspectra of each bare subsystem that is affected by
        changes in the external parameter. Formulated to be used with Pool.map()

        Parameters
        ----------
        param_val: float

        Returns
        -------
        list of tuples(ndarray, ndarray)
            (evals, evecs) bare eigendata for each subsystem that is parameter-dependent
        """
        eigendata = []
        self.update_hilbertspace(param_val)
        for subsys in self._hilbertspace:
            if subsys in self.subsys_update_list:
                evals_count = subsys.truncated_dim
                subsys_index = self._hilbertspace.index(subsys)
                eigendata.append(self._hilbertspace[subsys_index].eigensys(
                    evals_count=evals_count))
            else:
                eigendata.append(None)
        return eigendata

    def _compute_dressed_eigensystem(self, param_index, bare_specdata_list):
        hamiltonian = (self._bare_hamiltonian_constant +
                       self._compute_bare_hamiltonian_varying(
                           bare_specdata_list, param_index))

        for interaction_term in self._hilbertspace.interaction_list:
            evecs1 = self._lookup_bare_eigenstates(param_index,
                                                   interaction_term.subsys1,
                                                   bare_specdata_list)
            evecs2 = self._lookup_bare_eigenstates(param_index,
                                                   interaction_term.subsys2,
                                                   bare_specdata_list)
            hamiltonian += self._hilbertspace.interactionterm_hamiltonian(
                interaction_term, evecs1=evecs1, evecs2=evecs2)
        evals, evecs = hamiltonian.eigenstates(eigvals=self.evals_count)
        evecs = evecs.view(serializers.QutipEigenstates)
        return evals, evecs

    def _lookup_bare_eigenstates(self, param_index, subsys,
                                 bare_specdata_list):
        """
        Parameters
        ----------
        self: ParameterSweep or HilbertSpace
        param_index: int
            position index of parameter value in question
        subsys: QuantumSystem
            Hilbert space subsystem for which bare eigendata is to be looked up
        bare_specdata_list: list of SpectrumData
            may be provided during partial generation of the lookup

        Returns
        -------
        ndarray
            bare eigenvectors for the specified subsystem and the external parameter fixed to the value indicated by
            its index
        """
        subsys_index = self.get_subsys_index(subsys)
        return bare_specdata_list[subsys_index].state_table[param_index]

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

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

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

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

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

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

        Parameters
        ----------
        filename: str
        """
        io.write(self, filename)