Source code for quanguru.classes.QRes

r"""
    Contains qResults, qResBase, and qResBlank classes used in storing various types of simulation results.

    .. currentmodule:: quanguru.classes.QRes

    .. autosummary::

        qResBlank
        qResBase
        qResults

    .. |c| unicode:: U+2705
    .. |x| unicode:: U+274C
    .. |w| unicode:: U+2000

    =======================    ==================    ================   ===============
       **Function Name**        **Docstrings**        **Unit Tests**     **Tutorials**
    =======================    ==================    ================   ===============
      `qResBlank`                |w| |w| |w| |c|       |w| |w| |x|        |w| |w| |x|
      `qResBase`                 |w| |w| |w| |c|       |w| |w| |x|        |w| |w| |x|
      `qResults`                 |w| |w| |w| |c|       |w| |w| |x|        |w| |w| |x|
    =======================    ==================    ================   ===============

"""
from collections import defaultdict

from .base import qBase, aliasDict

__all__ = [
    'qResults'
]

#: used as the slots in :class:`~qResBlank` and :class:`~qResBase`.
resKeys = ['__results', '__states', '__resultsLast', '__statesLast', '__average', '__calculated']

[docs]class qResBlank: r""" This is a simplified version of :class:`~qResBase`, and it is used in time evolution returns of multi-processing (pool.map()). Since :class:`~qResBase` inherits from :class:`~named`, they contain a dictionary with :class:`~named` instances (incl. quantum system and protocol with large matrices), and multi-processing returns a duplicate/copy of each object in the dictionary. Introduced to save memory, this class is introduced and :meth:`~qResults._copyAllResBlank` is used in multi-process returns to write the results into qResBlank instances, which does not inherit from :class:`~named` (thus no duplicate of instances). # NOTE There must be a better solution, but enough for the time being """ __slots__ = resKeys def __init__(self): super().__init__() #: Stores the quantities calculated at each step of the time evolution self.__results = defaultdict(list) #: Stores the time averaged quantities self.__average = defaultdict(list) #: Stores the last list of results when sweeping parameters self.__resultsLast = defaultdict(list) #: Stores the time-evolved states self.__states = defaultdict(list) #: Stores the last list of time-evolved states when sweeping parameters self.__statesLast = defaultdict(list) #: Stores the quantities calculated by the calculate methods (see :class:`~computeBase`) self.__calculated = defaultdict(list) @property def resultsDict(self): r""" returns the protected attribute ``self._qResBlank__resultsLast`` """ return self._qResBlank__resultsLast # pylint: disable=no-member @property def states(self): r""" returns the protected attribute ``self._qResBlank__statesLast`` """ return self._qResBlank__statesLast # pylint: disable=no-member
[docs]class qResBase(qBase): r""" Base class for qResults implements the defaultdict attributes (used to store various quantities) and property/method to store quantities in them. `Last` ones store a single time trace (the very last one) and are reset in the beginning of each time evolution. At the each time trace, the Last are either moved into full dictionary or copied into a :class:`~qResBlank` depending, respectively, on single or multi-process. This class is extended by :class:`~qResults` to implement the method dealing with the details of moving the Last into full. """ #: (**class attribute**) class label used in default naming label = 'qResBase' #: (**class attribute**) number of instances created internally by the library _internalInstances: int = 0 #: (**class attribute**) number of instances created explicitly by the user _externalInstances: int = 0 #: (**class attribute**) number of total instances = _internalInstances + _externalInstances _instances: int = 0 __slots__ = resKeys def __init__(self, **kwargs): super().__init__(_internal=kwargs.pop('_internal', False)) #: Stores the quantities calculated at each step of the time evolution self.__results = defaultdict(list) #: Stores the time averaged quantities self.__average = defaultdict(list) #: Stores the last list of results when sweeping parameters self.__resultsLast = defaultdict(list) #: Stores the time-evolved states self.__states = defaultdict(list) #: Stores the last list of time-evolved states when sweeping parameters self.__statesLast = defaultdict(list) #: Stores the quantities calculated by the calculate methods (see :class:`~computeBase`) self.__calculated = defaultdict(list) self._named__setKwargs(**kwargs) # pylint: disable=no-member @property def calculated(self): r""" returns the protected attribute ``self._qResBase__calculated`` and the setter appends the given value into the defaultdict from the given (key, val) list/tuple. """ return self._qResBase__calculated # pylint: disable=no-member @calculated.setter def calculated(self, keyValList): self._qResBase__calculated[keyValList[0]].append(keyValList[1]) # pylint: disable=no-member @property def resultsDict(self): r""" returns the protected attribute ``self._qResBase__resultsLast`` and the setter appends the given value into the defaultdict from the given (key, val) list/tuple. Uses the Last, so that it stores the results into proper dictionary during time evolution, and other methods in the :class:`~qResults` are called at the end of time evolutions to make sure that (by equating this to the regular results dictionary) this returns the full results. """ return self._qResBase__resultsLast # pylint: disable=no-member @resultsDict.setter def singleResult(self, keyValList): self._qResBase__resultsLast[keyValList[0]].append(keyValList[1]) # pylint: disable=no-member @resultsDict.setter def resAverage(self, keyValList): r""" stores the averaged value of given (key, value) pairs. # FIXME does not work if storing say ndarray """ valCountPair = self._qResBase__average.pop(keyValList[0], None) # pylint: disable=no-member if valCountPair is not None: val = valCountPair[0] counter = valCountPair[1] avg = ((counter*val) + keyValList[1])/(counter + 1) self._qResBase__average[keyValList[0]] = [avg, counter+1] # pylint: disable=no-member self._qResBase__resultsLast[keyValList[0]] = avg # pylint: disable=no-member else: val = keyValList[1] self._qResBase__average[keyValList[0]] = [val, 1] # pylint: disable=no-member self._qResBase__resultsLast[keyValList[0]] = val # pylint: disable=no-member
[docs] def resultsMethod(self, key, value, average=False): r""" This method also stores results and is an alternative to :py:attr:`~results` property. It is aimed to provide enriched syntax. Might also implement average. """ if not average: if isinstance(key, str): self._qResBase__resultsLast[key].append(value) # pylint: disable=no-member elif isinstance(value, str): self._qResBase__resultsLast[value].append(key) # pylint: disable=no-member
#else: # valCountPair = self._qResBase__average.pop(key, None) # if valCountPair is not None: # val = valCountPair[0] # counter = valCountPair[1] # # FIXME does not work if storing say ndarray # avg = ((counter*val) + value)/(counter + 1) # self._qResBase__average @property def states(self): r""" returns the _qResBase__statesLast. The states are stored by the simulation objects, so there is no setter. Uses the Last, so that it stores the results into proper dictionary during time evolution, and other methods in the :class:`~qResults` are called at the end of time evolutions to make sure that (by equating this to the regular results dictionary) this returns the full results. """ return self._qResBase__statesLast # pylint: disable=no-member
[docs]class qResults(qResBase): r""" Extends the :class:`~qResBase` and introduces methods to reset, organise, copy, etc. for the results/states/etc. The results/states/etc are stored in the correspoding Last during time evolution and are appended into a single list ,which, at the end of the simulation, is re-shaped by :meth:`~_reShape` method using the sweep.indices. The time-evolution methods do not need to return anything (in single process) since every thing is stored in qResults objects. However, pool.map() need to return the relevant results out of the pickled program, and it is easier and consistent to return the objects storing the results. Due to the, named._allInstacesDict dictionary in their attributes, returning qResults out of the map causes duplication of all the named instances incl. quantum system and protocols with large matrices. Therefore, an alternative method :meth:`~_copyAllResBlank` is used to copy the dictionaries containing the results into :class:`~qResBlank` instances, and duplication of named instances is avoided (since qResBlank does not inherit from named, ie. does not contain the _allInstacesDict). """ #: (**class attribute**) class label used in default naming label = 'qResults' #: (**class attribute**) number of instances created internally by the library _internalInstances: int = 0 #: (**class attribute**) number of instances created explicitly by the user _externalInstances: int = 0 #: (**class attribute**) number of total instances = _internalInstances + _externalInstances _instances: int = 0 #: (**class attribute**) dictionary to store all the qResults instances, used in several places. _allResults = aliasDict() __slots__ = ['allResults'] def __init__(self, **kwargs): super().__init__(_internal=kwargs.pop('_internal', False)) kwargs.pop('allResults', None) #: required to properly pickle _allResults dictionary self.allResults = qResults._allResults self.allResults[self.name] = self # pylint: disable=no-member self._named__setKwargs(**kwargs) # pylint: disable=no-member
[docs] def _copyAllResBlank(self): r""" method to copy the dictionaries of all the :class:`~qResults` instances into new :class:`~qResBlank` instances, and store them into another dictionary with the name of the corresponding qResults instance. The returned dictionary mimics the _allResults dictionary of qResults and is used as the return of the pool.map(). At the end of the simulation, these dictionaries are used to write back the results into actual _allResults by :meth:`~_organiseMultiProcRes`. """ allResCopy = {} for sys in self.allResults.values(): newQRes = qResBlank() newQRes._qResBlank__results = sys._qResBase__results # pylint: disable=assigning-non-slot newQRes._qResBlank__average = sys._qResBase__average # pylint: disable=assigning-non-slot newQRes._qResBlank__states = sys._qResBase__states # pylint: disable=assigning-non-slot newQRes._qResBlank__resultsLast = sys._qResBase__resultsLast # pylint: disable=assigning-non-slot newQRes._qResBlank__statesLast = sys._qResBase__statesLast # pylint: disable=assigning-non-slot newQRes._qResBlank__calculated = sys._qResBase__calculated # pylint: disable=assigning-non-slot allResCopy[sys.name] = newQRes return allResCopy
[docs] def _reset(self): r""" Method to empty (creates and assigned them to new defaultdicts) all the previous results/states dicts. """ for qRes in self.allResults.values(): qRes._qResBase__results = defaultdict(list) qRes._qResBase__average = defaultdict(list) qRes._qResBase__states = defaultdict(list) qRes._qResBase__resultsLast = defaultdict(list) qRes._qResBase__statesLast = defaultdict(list) qRes._qResBase__calculated = defaultdict(list)
[docs] def _resetLast(self): r""" Method to reset the Last dictionaries only, and it is called before the time evolution to empty the Last. After the time evolution (depending on single/multi-process), these are moved to regular results by the corresponding methods below. At the end of the simulation, these are made equal to regular, so that the setter/getter properties of results/states (returning Last ones) works during and after the simulation to set/get the results. """ for qRes in self.allResults.values(): qRes._qResBase__resultsLast = defaultdict(list) qRes._qResBase__statesLast = defaultdict(list) qRes._qResBase__average = defaultdict(list)
[docs] def _organiseMultiProcRes(self, results, inds): r""" multi-processing returns are dictionaries containing :class:`~qResBlank` instances with single time trace (see :meth:`~_copyAllResBlank`), this method re-writes the results into corresponding :class:`~qResults` objects and re-shapes them into correct sweep and time trace structure. Calls other methods of the class for these. """ for res in results: for keyUni, valUni in res.items(): self._organise(keyUni, valUni) self._finaliseAll(inds)
[docs] @classmethod def _finaliseAll(cls, inds): r""" Calls the :meth:`~_finalise` method, which reshapes the results/states list using the given list of indices (ie. length of each sweep), on every qResults instances in the _allResults dictionary. """ for qres in cls._allResults.values(): qres._finalise(inds)
[docs] @staticmethod def _organise(keyUni, valUni): r""" At the end of time evolution, :meth:`~_organise` creates a single list containing all the results/states for each key. """ for key, val in valUni.resultsDict.items(): qResults._allResults[keyUni]._qResBase__results[key].append(val) # pylint: disable=no-member for key1, val1, in valUni.states.items(): qResults._allResults[keyUni]._qResBase__states[key1].append(val1) # pylint: disable=no-member
[docs] def _organiseSingleProcRes(self): r""" organising the single-process results by simply calling :meth:`~_organise` on every qResults instance in allResults dictionary. This needs to be finalised, ie. creates a single list containing every sweep, so the list has to be reshaped. """ for keyUni, valUni in self.allResults.items(): self._organise(keyUni, valUni)
[docs] def _finalise(self, inds): r""" method to reshape the results/states list using the given list of indices (ie. length of each sweep). """ for key, val in self._qResBase__results.items(): # pylint: disable=no-member if inds != []: self._qResBase__results[key], _ = self._reShape(val, list(reversed(inds))) # pylint: disable=no-member else: self._qResBase__results[key] = val[0] # pylint: disable=no-member for key1, val1 in self._qResBase__states.items(): # pylint: disable=no-member if inds != []: self._qResBase__states[key1], _ = self._reShape(val1, list(reversed(inds))) # pylint: disable=no-member else: self._qResBase__states[key1] = val1[0] # pylint: disable=no-member self._qResBase__resultsLast = self._qResBase__results # pylint: disable=assigning-non-slot, no-member self._qResBase__statesLast = self._qResBase__states # pylint: disable=assigning-non-slot, no-member
[docs] @staticmethod def _reShape(lis, inds, counter=0, totalCount=0): r""" a recursive method to reshape a list/listOfLists into given indices form. """ newList = [] if counter < (len(inds)-1): counter += 1 for _ in range(inds[counter-1]): nList, totalCount = qResults._reShape(lis, inds, counter, totalCount) newList.append(nList) else: for _ in range(inds[counter]): lisToAppend = lis[totalCount] if isinstance(lisToAppend, list): if len(lisToAppend) == 1: lisToAppend = lisToAppend[0] newList.append(lisToAppend) totalCount += 1 return (newList, totalCount) return (newList, totalCount)