Source code for supervillain.observable.observable

#!/usr/bin/env python

from functools import partial
import inspect

import supervillain.action
import supervillain.ensemble
from supervillain.performance import Timer
import supervillain.h5.extendable

import logging
logger = logging.getLogger(__name__)
from tqdm.contrib.logging import logging_redirect_tqdm

registry=dict()

[docs]class Observable: def __init_subclass__(cls, intermediate=False): # This registers every subclass that inherits from Observable. # Upon registration, Ensemble gets an attribute with the appropriate name. name = cls.__name__ registry[name] = cls cls._logger = (logger.debug if name[0] == '_' else logger.info) cls._debug = logger.debug cls._logger(f'Observable registered: {name}') setattr(supervillain.ensemble.Ensemble, name, cls()) def __get__(self, obj, objtype=None): # The __get__ method is the workhorse of the Descriptor protocol. name = self.__class__.__name__ # Cache: if name in obj.__dict__: # What's nice about this is that the cache is in the object's dictionary itself, # rather than associated with the observable class. This avoids the issue of a # class level cache discussed in https://github.com/evanberkowitz/two-dimensional-gasses/issues/12 # in that there's no extra reference to the object at all with this strategy. # So, when it goes out of scope with no reference, it will be deleted. self._debug(f'{name} already cached.') return obj.__dict__[name] # Observable might have been measured inline. try: return obj.configuration.fields[name] except Exception as e: self._debug(f'{name} was not measured inline.') # Just call the measurement and cache the result. class_name = obj.Action.__class__.__name__ try: # Observables can have action-dependent implementations # and a fall-back default which is convenient for observables which # depend simply on others. For example, a density might not # need the field variables but the global charge (or vice-versa). try: measure = getattr(self, class_name) except AttributeError as e: if hasattr(self, 'default'): measure = getattr(self, 'default') else: raise e from None # All observables must take the action as the first argument. measure = partial(measure, obj.Action) # Observables can depend on field variables and other Observables. # We look up the arguments as attributes of the ensemble. with Timer(self._logger, f'Measurement of {name}', per=len(obj)): with logging_redirect_tqdm(): obj.__dict__[name]= supervillain.h5.extendable.array([ measure(*obs) for obs in supervillain.observable.progress( zip(*[getattr(obj, o) for o in inspect.getfullargspec(measure).args]), desc=f'{name:{max([len(k) for k in registry])}s}', leave=True, total=len(obj)) ]) return obj.__dict__[name] except Exception as exception: raise NotImplementedError(f'{name} not implemented for {class_name}') from exception raise NotImplementedError() def __set__(self, obj, value): obj.__dict__[self.__class__.__name__] = value
[docs] @classmethod def autocorrelation(cls, ensemble): r''' Deciding whether an observable is included in an ensemble's :py:meth:`~.Ensemble.autocorrelation_time` computation is ensemble-dependent. For example, if $W=1$ then certain vortex observables are independent of configuration and thus look like they have an infinite autocorrelation time. However, that's expected and not an ergodicity problem. That's real physics! So, to decide whether an observable should be included in the ensemble's autocorrelation time requires in general evaluating a function on the observable itself and the ensemble. By default observables just return ``False`` but observables can override this function to make more clever decisions. ''' return False
[docs]class Scalar:
[docs] @classmethod def autocorrelation(cls, ensemble): r''' Scalars are simple to understand and can be included in the autocorrelation computation. Returns ``True``. ''' return True
[docs]class Constrained:
[docs] @classmethod def autocorrelation(cls, ensemble): r''' If $W=1$ the observable should not be included in the autocorrelation computation. If $W\neq 1$ then use all other considerations to decide. ''' return (ensemble.Action.W != 1) and super().autocorrelation(ensemble)
[docs]class OnlyVillain:
[docs] @classmethod def autocorrelation(cls, ensemble): r''' True if the ensemble's action is Villain and all other considerations are true. ''' return (isinstance(ensemble.Action, supervillain.action.Villain)) and super().autocorrelation(ensemble)
[docs]class OnlyWorldline:
[docs] @classmethod def autocorrelation(cls, ensemble): r''' True if the ensemble's action is :class:`~.action.Worldline` and all other considerations are true. ''' return (isinstance(ensemble.Action, supervillain.action.Worldline)) and super().autocorrelation(ensemble)
[docs]class NotVillain:
[docs] @classmethod def autocorrelation(cls, ensemble): r''' False if the ensemble's action is Villain, otherwise use all other considerations. ''' return (not isinstance(ensemble.Action, supervillain.action.Villain)) and super().autocorrelation(ensemble)
[docs]class NotWorldline:
[docs] @classmethod def autocorrelation(cls, ensemble): r''' False if the ensemble's action is :class:`~.action.Worldline`, otherwise use all other considerations. ''' return (not isinstance(ensemble.Action, supervillain.action.Worldline)) and super().autocorrelation(ensemble)