# param_sweep.py
#
# This file is part of scqubits.
#
# Copyright (c) 2019, Jens Koch and Peter Groszkowski
# All rights reserved.
#
# This source code is licensed under the BSD-style license found in the
# LICENSE file in the root directory of this source tree.
############################################################################
import functools
from abc import ABC, abstractmethod
import numpy as np
import scqubits.core.central_dispatch as dispatch
import scqubits.core.descriptors as descriptors
import scqubits.core.hilbert_space as hspace
import scqubits.core.spec_lookup as spec_lookup
import scqubits.core.storage as storage
import scqubits.io_utils.fileio as io
import scqubits.io_utils.fileio_qutip as qutip_serializer
import scqubits.io_utils.fileio_serializers as serializers
import scqubits.settings as settings
import scqubits.utils.cpu_switch as cpu_switch
import scqubits.utils.misc as utils
if settings.IN_IPYTHON:
from tqdm.notebook import tqdm
else:
from tqdm import tqdm
class ParameterSweepBase(ABC):
"""
The ParameterSweepBase class is an abstract base class for ParameterSweep and StoredSweep
"""
param_name: str
param_vals: np.ndarray
param_count: int
evals_count: int
lookup: spec_lookup.SpectrumLookup
_hilbertspace: hspace.HilbertSpace
@abstractmethod
def lookup(self):
pass
def get_subsys(self, index):
return self._hilbertspace[index]
def get_subsys_index(self, subsys):
return self._hilbertspace.get_subsys_index(subsys)
@property
def osc_subsys_list(self):
return self._hilbertspace.osc_subsys_list
@property
def qbt_subsys_list(self):
return self._hilbertspace.qbt_subsys_list
@property
def subsystem_count(self):
return self._hilbertspace.subsystem_count
@property
def bare_specdata_list(self):
return self.lookup._bare_specdata_list
@property
def dressed_specdata(self):
return self.lookup._dressed_specdata
def _lookup_bare_eigenstates(self, param_index, subsys, bare_specdata_list):
"""
Parameters
----------
self: ParameterSweep or HilbertSpace
param_index: int
position index of parameter value in question
subsys: QuantumSystem
Hilbert space subsystem for which bare eigendata is to be looked up
bare_specdata_list: list of SpectrumData
may be provided during partial generation of the lookup
Returns
-------
ndarray
bare eigenvectors for the specified subsystem and the external parameter fixed to the value indicated by
its index
"""
subsys_index = self.get_subsys_index(subsys)
return bare_specdata_list[subsys_index].state_table[param_index]
@property
def system_params(self):
return self._hilbertspace.get_initdata()
def new_datastore(self, **kwargs):
"""Return DataStore object with system/sweep information obtained from self."""
return storage.DataStore(self.system_params, self.param_name, self.param_vals, **kwargs)
[docs]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 subsys_list 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.tqdm_disabled = settings.PROGRESSBAR_DISABLED or (num_cpus > 1)
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()
[docs] 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])
[docs] 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.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):
"""
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.tqdm_disabled)))
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] = 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):
"""
Returns
-------
qutip.Qobj operator
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, 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(qutip_serializer.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]
[docs] @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())
[docs] 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
[docs] def filewrite(self, filename):
"""Convenience method bound to the class. Simply accesses the `write` function.
Parameters
----------
filename: str
"""
io.write(self, filename)
class StoredSweep(ParameterSweepBase, serializers.Serializable):
def __init__(self, param_name, param_vals, evals_count, hilbertspace, dressed_specdata, bare_specdata_list):
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)
@property
def lookup(self):
return self._lookup
def get_hilbertspace(self):
return self._hilbertspace
def new_sweep(self, subsys_update_list, update_hilbertspace, num_cpus=settings.NUM_CPUS):
return ParameterSweep(
self.param_name,
self.param_vals,
self.evals_count,
self._hilbertspace,
subsys_update_list,
update_hilbertspace,
num_cpus
)