r"""
Contains two classes used for sweep functionalities.
NOTE : Both of these classes are not intended to be directly instanciated by the user.
:class:`Simulation <quanguru.classes.Simulation.Simulation>` objects **has** ``Sweep/s`` as their attributes, and
``_sweep/s`` are intended to be created by calling the relevant methods over ``Simulation.Sweep``.
.. currentmodule:: quanguru.classes.QSweep
.. autosummary::
_sweep
Sweep
.. |c| unicode:: U+2705
.. |x| unicode:: U+274C
.. |w| unicode:: U+2000
======================= ================== ================ ===============
**Function Name** **Docstrings** **Unit Tests** **Tutorials**
======================= ================== ================ ===============
`_sweep` |w| |w| |w| |c| |w| |w| |x| |w| |w| |x|
`Sweep` |w| |w| |w| |c| |w| |w| |x| |w| |w| |x|
======================= ================== ================ ===============
"""
from functools import reduce
from numpy import arange, logspace
from .base import qBase, _recurseIfList
from .baseClasses import updateBase
__all__ = [
'Sweep'
]
[docs]class _sweep(updateBase): # pylint: disable=too-many-instance-attributes
r"""
Implements methods and attributes to sweep the value of an attribute for some objects for a list of values.
The default sweep :meth:`~_defSweep` sweeps the value for a given attribute
(a string stored in :py:attr:`~_sweep.key`) of objects in ``subSys`` dictionary.
The list of values (stored in :py:attr:`_sweepList`) to be swept are set either directly
by giving a ``list`` or the ``sweepMin-sweepMax-sweepStep`` with ``logSweep``.
Default sweep function can be replaced with any custom method by re-assigning the :py:attr:`~sweepFunction` to the
function reference. The default sweep method requires the index of the value from the list of values to set the next
value, this index is provided by the modularSweep and useful for multi-parameter sweeps. It keeps a value fixed
by re-assigning it using the same index, and the :class:`~paramBoundBase` and other relevant classes uses the
custom setattr methods (see :meth:`~setAttr` and :meth:`~setAttrParam`) to make sure that ``paramUpdated`` boolean
is not set to ``True`` for the same value. This class implements a single sweep, and multi parameter sweep is
achieved by the :class:`~Sweep` class.
"""
#: (**class attribute**) class label used in default naming
label = '_sweep'
#: (**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__ = ['sweepMax', 'sweepMin', 'sweepStep', '_sweepList', 'logSweep', 'combinatorial', '_sweepIndex']
#@sweepInitError
def __init__(self, **kwargs):
super().__init__(_internal=kwargs.pop('_internal', False))
#: protected attribute pointing to a sweep function, by default :meth:`~_defSweep`. This attribute get&set
#: using the sweepFunction property to replace default with a customized sweep method.
self._updateBase__function = self._defSweep # pylint: disable=assigning-non-slot
#: maximum value for the swept parameter, used with other attributes to create the sweepList
self.sweepMax = None
#: minimum value for the swept parameter, used with other attributes to create the sweepList
self.sweepMin = None
#: corresponds to the step size in a linearly spaced sweepList, or number of steps in logarithmic case,
#: used with other attributes to create the sweepList
self.sweepStep = None
#: protected attribute to store a list of values for the swept parameter. Can be given a full list or
#: be created using sweepMin-sweepMax-sweepStep values.
self._sweepList = None
#: boolean to create either linearly or logarithmically spaced list values (from sweepMin-sweepMax-sweepStep).
self.logSweep = False
#: boolean to determine, if two different sweeps are swept simultaneously (same length of list and pair of
#: values at the same index are swept) or a multi-parameter (combinatorial) sweep,
#: ie fix one sweep the other & repeat.
self.combinatorial = False
#: stores the index of the value (from the _sweepList) currently being assigned by the sweep function. Used by
#: the default methods but also useful for custom methods. It is calculated by the modular arithmetic in
#: modularSweep and passed to here by :class:`~Sweep` object containing self in its subSys. It starts from -1
#: and the correspoding property returns _sweepIndex+1, while the :meth:`~runSweep` sets it to ind+1 for a given
#: ind from modularSweep. This whole ordeal is due to make sure that python list indexing and modular arithmetic
#: properly agrees for the sweep functionality. I feel it can be improved but will leave as it is for now.
self._sweepIndex = -1
self._named__setKwargs(**kwargs) # pylint: disable=no-member
@property
def index(self):
r"""
returns ``self._sweepIndex + 1``. reason for +1 is explained in :py:attr:`~_sweepIndex`. There is no setter,
the value of _sweepIndex is updated by the :meth:`~runSweep` and is an internal process.
"""
return self._sweepIndex + 1
@property
def sweepFunction(self):
r"""
gets and set :py:attr:`~_updateBase__function`, which should point to a Callable.
"""
return self._updateBase__function # pylint: disable=no-member
@sweepFunction.setter
def sweepFunction(self, func):
self._updateBase__function = func # pylint: disable=assigning-non-slot
@property
def sweepKey(self):
r"""
gets and sets :py:attr:`~_updateBase__key`, which should be string.
"""
return self._updateBase__key # pylint: disable=no-member
@sweepKey.setter
def sweepKey(self, keyStr):
self._updateBase__key = keyStr # pylint: disable=assigning-non-slot
@property
def sweepList(self):
r"""
gets and sets :py:attr:`~_sweepList`. Setter requires a list input, if it is not set, getter tries creating the
list (and setting :py:attr:`~_sweepList`) using sweepMin-sweepMax-sweepStep attributes.
"""
if self._sweepList is None:
try:
if self.logSweep is False:
self._sweepList = arange(self.sweepMin, self.sweepMax + self.sweepStep, # pylint: disable=no-member
self.sweepStep) # pylint: disable=no-member
elif self.logSweep is True:
self._sweepList = logspace(self.sweepMin, self.sweepMax,
num=self.sweepStep, base=10.0) # pylint: disable=no-member
except: #pylint:disable=bare-except # noqa: E722
pass
return self._sweepList
@sweepList.setter
def sweepList(self, sList):
self._sweepList = sList
[docs] @staticmethod
def _defSweep(self): # pylint: disable=bad-staticmethod-argument
r"""
This is the default sweep function, and it just calls the
:meth:`_runUpdate <quanguru.classes.updateBase.updateBase._runUpdate>` by feeding it the value from the
``sweepList`` at the position ``ind``. :meth:`_runUpdate <quanguru.classes.updateBase.updateBase._runUpdate>`
function just sets the attribute (for the given key) of every ``subSys`` to a given value (``val``).
The modularSweep methods uses multiplication of length of ``sweepList/s`` (stored in
__inds attribute of :class:`Sweep` instances) as a loop range, and the current loop counter is used by the
:meth:`~_indicesForSweep` to calculate which indices of multi _sweep is currently needed.
Parameters
----------
ind : int
Index of the value from ``sweepList``
"""
val = self.sweepList[self.index]
self._runUpdate(val)
[docs] def runSweep(self, ind):
r"""
Wraps the ``_updateBase__function``, so that this will be the function that is always called to run the
sweeps. This is not essential and could be removed, but it kind of creates a duck-typing with ``Sweep`` class,
when we might want to use a nested sweep.
"""
self._sweepIndex = ind-1 # pylint: disable=assigning-non-slot
self._updateBase__function(self) # pylint: disable=no-member
[docs]class Sweep(qBase):
r"""
A container class for :class:`_sweep` objects and relevant methods for creating/removing and carrying
multi-parameter sweeps. It stores :class:`_sweep` objects in its ``subSys``
dictionary, and it has two additional private attributes to store sweep lengths and their multiplications, which are
used in modularSweep and by :meth:`~_indicesForSweep` to carry multi parameter sweeps.
Instances of this
class are used as attributes of :class:`Simulation <quanguru.classes.Simulation.Simulation>` objects, and those are
intended to be used for ``_sweep`` creations.
"""
#: Used in default naming of objects. See :attr:`label <quanguru.classes.QUni.qUniversal.label>`.
label = 'Sweep'
#: (**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__ = ['__inds', '__indMultip']
# TODO init errors
def __init__(self, **kwargs):
super().__init__(_internal=kwargs.pop('_internal', False))
self.__inds = []
r"""
a list of ``sweepList`` length/s of multi-parameter ``_sweep`` object/s in ``subSys`` dictionary, meaning the
length for simultaneously swept ``_sweep`` objects are not repeated. the values are
appended to the list, if it is the first ``sweep`` to be included into ``subSys`` or ``combinatorial is True``.
"""
self.__indMultip = 1
r"""
the multiplication of all the indices in ``inds``. This value is used as the loop range by modularSweep.
"""
self._named__setKwargs(**kwargs) # pylint: disable=no-member
@property
def inds(self):
r"""
``returns _Sweep__inds`` and there is no setter
"""
return self._Sweep__inds
@property
def indMultip(self):
r"""
``returns _Sweep__indMultip``, and there is no setter
NOTE : The reason this property returns a pre-assingned value rather than calculating from the ``inds`` is to
avoid calculating it over and over again, which could be avoided by checking if ``_Sweep__indMultip is None``,
but that might create other issues, such as re-running the same simulation after a change in ``sweepList``
length/s. It still can be improved, and it is possible to avoid such issues and get rid of :meth:`prepare`,
which is called in ``run`` methods of ``Simulations``, by some modifications in these properties.
"""
return self._Sweep__indMultip
@property
def sweeps(self):
r"""
The sweeps property wraps ``subSys`` dictionary to create new terminology, it works exactly as
:meth:`subSys <quanguru.classes.base.qBase.subSys>`.
"""
return self._qBase__subSys # pylint: disable=no-member
@sweeps.setter
def sweeps(self, sysDict):
super().addSubSys(sysDict)
[docs] @_recurseIfList
def removeSweep(self, sys):
r"""
Removes a ``_sweep`` it self, or all the ``_sweep`` objects that contain a particular ``sys`` in it.
Since, it uses :meth:`removeSubSys <quanguru.classes.base.qBase.removeSubSys>`, it works exactly the same,
meaning
names/aliases/objects/listOfObjects can be used to remove.
If the argument ``sys`` is an :class:`_sweep` object, this method calls
:meth:`removeSubSys <quanguru.classes.base.qBase.removeSubSys>` (since ``_sweep`` objects are stored in
``subSys`` dictionary of ``Sweep`` objects).
Else, it calls the :meth:`removeSubSys <quanguru.classes.base.qBase.removeSubSys>` on every ``_sweep`` in its
``subSys`` dictionary (since ``systems`` are stored in ``subSys`` dictionary of ``_sweep`` objects).
"""
if isinstance(sys, _sweep):
super()._removeSubSysExc(sys, _exclude=[])
else:
sweeps = list(self.subSys.values())
for sweep in sweeps:
sweep._removeSubSysExc(sys, _exclude=[]) #pylint:disable=protected-access
if len(sweep.subSys) == 0:
super()._removeSubSysExc(sweep, _exclude=[])
[docs] def createSweep(self, system=None, sweepKey=None, **kwargs):
r"""
Creates a instance of ``_sweep`` and assing its ``system`` and ``sweepKey`` to given system
and sweepKey arguments of this method. Keyworded arguments are used to set the other attributes of the newly
created ``_sweep`` object.
Parameters
----------
system : Any
Since ``system`` property setter of ``_sweep`` behaves exactly as
:meth:`subSys <quanguru.classes.base.qBase.subSys>` setter, this can be various things, from a single
system to name/alias of the system, or from a class to a list/tuple contaning any combination
of these.
sweepKey : str
Name of the attribute of system/s that will be swept
:returns: The new ``_sweep`` instance.
"""
if system is None:
system = self.superSys.superSys
if system is None:
raise ValueError('?')
newSweep = _sweep(superSys=self, subSys=system, sweepKey=sweepKey, **kwargs)
if system is not self.auxObj:
if not isinstance(sweepKey, str):
raise ValueError("key")
# newSweep._aux = True #pylint: disable=protected-access
if hasattr(list(newSweep.subSys.values())[0], sweepKey):
for sys in newSweep.subSys.values():
if not hasattr(sys, sweepKey):
raise AttributeError("?")
else:
# FIXME if the system does not have the given attribute and that is an error
# (eg wrong attr name/typo given), this still works without an error. this should be an explicit setting
newSweep._aux = True #pylint: disable=protected-access
# ignores when object is given with a key it does not have
#elif not hasattr(list(newSweep.subSys.values())[0], sweepKey):
# newSweep._aux = True #pylint: disable=protected-access
super().addSubSys(newSweep)
return newSweep
[docs] def prepare(self):
r"""
This method is called inside ``run`` method of ``Simulation`` object/s to update ``inds`` and ``indMultip``
attributes/properties. The reason for this a bit argued in :meth:`indMultip`, but it is basically to ensure that
any changes to ``sweepList/s`` or ``combinatorial/s`` are accurately used/reflected (especially on re-runs).
"""
if len(self.subSys) > 0:
self._Sweep__inds = [] # pylint: disable=assigning-non-slot
for indx, sweep in enumerate(self.subSys.values()):
if ((sweep.combinatorial is True) or (indx == 0)):
self._Sweep__inds.insert(0, len(sweep.sweepList))
self._Sweep__indMultip = reduce(lambda x, y: x*y, self._Sweep__inds) # pylint: disable=assigning-non-slot
[docs] def runSweep(self, indList):
r"""
called in modularSweep to run all the ``_sweep``
objects in a ``Sweep``. indices from a given list ``indList`` are used by the ``runSweep`` method of ``_sweep``
objects, and it switches to a new index, if the ``combinatorial is True``. This means that the ``_sweeps``
**should be created in an order** such that ``_sweep`` objects that run simultaneously **have to be** added to
``subSys`` one after the other. Also, for nested Sweeps, the indList should be a properly nested list.
"""
indx = 0
for sweep in self.sweeps.values():
if sweep.combinatorial is True:
indx += 1
sweep.runSweep(indList[indx])
# function used in modular sweep
[docs] @staticmethod
def _indicesForSweep(ind, *args):
r"""
method used in modularSweep to calculate indices for each sweepList from the loop counter ``ìnd`` using the
total lengths ``*args``. It is hard to describe the exact calculation in words, but it is trivial to see from
the math (TODO) which i will do later.
the loop counter can at max be :math:`(\prod_{i = 1}^{len(args)} args[i]) - 1`, and multi-parameter
sweeps loops the first sweepList while fixing the others. So, at each inp = args[0] the first list should
start from zero, and the second list moves to next item, and this relation goes up in the chain, e.g. at each
inp = args[0]*args[1], the index of the third need to be increased, and so on. Therefore, the current index for
the first sweepList simply is the reminder of inp with args[0].
"""
indices = []
for arg in args:
remain = ind%arg
ind = (ind-remain)/arg
indices.insert(0, int(remain))
return indices