Package csb :: Package statistics :: Package pdf :: Module parameterized |
[frames] | no frames]

# Source Code for Module csb.statistics.pdf.parameterized

      1  """
      2  Probability density functions with support for shared and computed parameters.
      3
      4  This module extends the functionality of L{csb.statistics.pdf} with a specialized
      5  and more sophisticated L{AbstractDensity} -- the L{ParameterizedDensity}, which
      6  works with L{AbstractParameter} objects rather than simple floats.
      7
      8  Each L{AbstractParameter} holds two properties - L{AbstractParameter.name} and
      9  L{AbstractParameter.value}:
     10
     11      >>> class Sigma(AbstractParameter):
     12      >>>     def _validate(self, value):
     13      >>>         return float(value)
     14      >>>     def _compute(self, base_value):
     15      >>>         return 1.0 / base_value ** 0.5
     16      >>>
     17      >>> sigma = Sigma(3)
     18      >>> sigma.name, sigma.value
     19      sigma, 3
     20
     21  L{AbstractParameter}s holding a single float value are indistinguishable from
     22  the simple float parameters used in L{csb.statistics.pdf.BaseDensity}.
     23  However, each L{AbstractParameter} supports the concept of binding:
     24
     25      >>> sigma.is_virtual
     26      False
     27      >>> precision = Precision(1)
     28      >>> sigma.bind_to(precision)
     29      >>> sigma.is_virtual
     30      True
     31      >>> precision.set(2)  # triggers an implicit, lazy update in sigma
     32      >>> sigma.set(1)
     33      ParameterizationError: Virtual parameters can't be updated explicitly
     34
     35  The instance of Sigma is now a virtual parameter which receives automatic updates
     36  from another base parameter using a predefined rule (L{AbstractParameter._compute}).
     37  This is a lazy, on demand process. As soon as Sigma's computed value is
     38  requested (via C{s.value}), Sigma will query the parameter it depends on
     39  (Precision), which in turn will get recomputed first based on its own base, etc.
     40  Thus, the L{AbstractParameter} model supports a parameter dependency chain with
     41  linear or tree-like topologies::
     42
     43                 sigma -- y
     44                /
     45      precision -- sigma2 -- x
     46
     47  In this graph precision is a base (non-virtual) parameter and sigma, sigma2, x, and y
     48  are all virtual (computed). Binding precision to another parameter will immediately
     49  turn it into a virtual one. However, cycles are not allowed (e.g. precision can't
     50  be bound to sigma2 or x) and each virtual parameter must have exactly one base.
     51
     52  Computed parameters can then be used to implement custom PDFs with dependent
     53  parameters within one PDF or spanning multiple PDFs.
     54  """
     55
     56  import csb.statistics.pdf as pdf
     57
     58  from abc import abstractmethod
     59
     60
     61 -class ParameterizationError(ValueError):
     62      pass
     63
     64 -class ParameterValueError(pdf.ParameterValueError):
     65      pass
     66
     67
     68 -class ParameterizedDensity(pdf.AbstractDensity):
     69      """
     70      Base abstract class for all PDFs, which operate on simple or computed
     71      (chained) parameters. Parameters of type different from L{AbstractParameter}
     72      will trigger TypeError-s.
     73      """
     74
     75 -    def _set(self, param, value):
     76
     77          if not isinstance(value, AbstractParameter):
     78              raise TypeError(value)
     79
     80          super(ParameterizedDensity, self)._set(param, value)
     81
     82
     83 -class AbstractParameter(object):
     84      """
     85      Abstract parameterization, which can exist independently or be coupled
     86      to other parameters upon request. Virtual/coupled/derived parameters cannot
     87      be overwritten explicitly, but their values will get recomputed once their
     88      corresponding base parameters get updated. This is a lazy process - parameter
     89      recalculation happens only when an out of date parameter is requested. This
     90      triggers a real-time cascaded update which affects all parameters from the
     91      nearest consistent base down to the current inconsistent node.
     92
     93      Implementing subclasses must override L{AbstractParameter._validate}
     94      and virtual parameters should additionally override L{AbstractParameter._compute}.
     95
     96      @param value: initial value (defaults to None / AbstractParameter.NULL)
     97      @type value: object
     98      @param name: name of parameter (this is the name of the class by default)
     99      @type name: str
    100      @param base: optional base parameter to compute this instance from
    101      @type base: L{AbstractParameter}
    102      """
    103
    104      NULL = None
    105
    106 -    def __init__(self, value=NULL, name=None, base=None):
    107
    108          self._derivatives = set()
    109          self._base = None
    110          self._consistent = True
    111
    112          if name is None:
    113              name = self.__class__.__name__.lower()
    114
    115          self._name = str(name)
    116          self._value = AbstractParameter.NULL
    117
    118          self._update(value)
    119
    120          if base is not None:
    121              self.bind_to(base)
    122
    123      @property
    124 -    def name(self):
    125          """
    126          Parameter name
    127          """
    128          return self._name
    129
    130      @property
    131 -    def value(self):
    132          """
    133          Parameter value (guaranteed to be up to date)
    134          """
    135          self._ensure_consistency()
    136          return self._value
    137
    138      @property
    139 -    def is_virtual(self):
    140          """
    141          True if this parameter is virtual (computed)
    142          """
    143          return self._base is not None
    144
    145 -    def set(self, value):
    146          """
    147          Update the value of this parameter. This is not possible for
    148          virtual parameters.
    149
    150          @param value: new value
    151          @type value: object
    152
    153          @raise ParameterizationError: if this is a virtual parameter
    154          @raise ParameterValueError: on invalid value
    155          """
    156          if self.is_virtual:
    157              raise ParameterizationError(
    158                              "Virtual parameters can't be updated explicitly")
    159
    160          self._update(value)
    161
    162          self._invalidate()
    163          self._consistent = True
    164
    165 -    def bind_to(self, parameter):
    166          """
    167          Bind the current parameter to a base parameter. This converts
    168          the current parameter to a virtual one, whose value will get
    169          implicitly updated to be consistent with its base.
    170
    171          Note that virtual parameters must have exactly one base; computing a
    172          parameter from multiple bases is not allowed. Cycles are also not
    173          allowed; the topology must always stay a tree with a non-virtual
    174          parameter at the root.
    175
    176          @param parameter: base parameter to compute this instance from
    177          @param parameter: L{AbstractParameter}
    178
    179          @raise ParameterizationError: if this parameter is already virtual
    180          @raise ParameterizationError: on attempt to create a circular dependency
    181
    182          """
    183
    184          if not isinstance(parameter, AbstractParameter):
    185              raise TypeError(parameter)
    186
    187          if parameter.find_base_parameter() is self:
    188              raise ParameterizationError("Circular dependency detected")
    189
    190          if self.is_virtual:
    191              msg = "Parameter {0.name} is already bound to {1.name}"
    192              raise ParameterizationError(msg.format(self, self._base))
    193
    194          self._set_base(parameter)
    195          self._base._add_derived(self)
    196
    197          self._invalidate()
    198
    199 -    def _set_base(self, parameter):
    200          self._base = parameter
    201
    202 -    def _add_derived(self, parameter):
    203          self._derivatives.add(parameter)
    204
    205 -    def _invalidate(self):
    206          """
    207          Mark self and its virtual children as inconsistent
    208          """
    209          for p in self._derivatives:
    210              p._invalidate()
    211
    212          self._consistent = False
    213
    214 -    def _update(self, value):
    215          """
    216          Overwrite the current value of the parameter. This triggers
    217          an abstract (custom) validation hook, but has no side effects
    218          (i.e. it doesn't propagate!)
    219          """
    220          sanitized = self._validate(value)
    221          self._value = sanitized
    222
    223      @abstractmethod
    224 -    def _validate(self, value):
    225          """
    226          Validate and sanitize the specified value before assignment.
    227          @return: sanitized value
    228
    229          @raise ParameterValueError: on invalid value
    230          """
    231          return value
    232
    233 -    def _compute(self, base_value):
    234          """
    235          Compute a new value for the current parameter given the value
    236          of a base parameter (assuming self.is_virtual). By default this returns
    237          the value of the base parameter (i.e. self just inherits the value
    238          of its base untouched).
    239          """
    240          return base_value
    241
    242 -    def _ensure_consistency(self):
    243          """
    244          Make sure that the current value is up to date. If it isn't,
    245          trigger a real-time cascaded update following the path from the
    246          nearest consistent base down to self. Also mark all nodes consistent
    247          in the course of doing this update.
    248          """
    249          if not self._consistent:
    250              path = self._nearest_consistent_base()
    251
    252              for parameter in reversed(path):
    253                  parameter._recompute(consistent=True)
    254
    255 -    def _recompute(self, consistent=True):
    256          """
    257          If self is virtual, force the current parameter to recompute itself from
    258          its immediate base. This operation has no side effects and does not
    259          propagate.
    260          """
    261          if self.is_virtual:
    262              value = self._compute(self._base._value)
    263              self._update(value)
    264
    265          if consistent:
    266              self._consistent = True
    267
    268 -    def _recompute_derivatives(self):
    269          """
    270          Recompute all derived parameters starting from self and mark
    271          them consistent.
    272          """
    273          self._recompute(consistent=True)
    274
    275          for p in self._derivatives:
    276              p._recompute_derivatives()
    277
    278 -    def _nearest_consistent_base(self):
    279          """
    280          Compute and return the path from self to the nearest consistent
    281          base parameter.
    282
    283          @return: path, leaf-to-root
    284          @rtype: list of L{AbstractParameter}
    285          """
    286          root = self
    287          path = [self]
    288
    289          while not root._consistent:
    290              root = root._base
    291              path.append(root)
    292
    293          return path
    294
    295 -    def find_base_parameter(self):
    296          """
    297          Find and return the non-virtual base parameter that is the root
    298          of the current hierarchy. If self is not virtual, return self.
    299
    300          @return: base parameter
    301          @rtype: L{AbstractParameter}
    302          """
    303          root = self
    304
    305          while root.is_virtual:
    306              root = root._base
    307
    308          return root
    309
    310
    311 -class Parameter(AbstractParameter):
    312      """
    313      Default parameter implementation which accepts float values only.
    314      """
    315
    316 -    def __init__(self, value=0.0, name=None, base=None):
    317          super(Parameter, self).__init__(value, name, base)
    318
    319 -    def _validate(self, value):
    320
    321          try:
    322              return float(value)
    323          except (ValueError, TypeError):
    324              raise ParameterValueError(self.name, value)
    325
    326
    327 -class NonVirtualParameter(Parameter):
    328      """
    329      A float L{Parameter} that is explicitly non-computed and cannot be
    330      bound to another L{Parameter}.
    331      """
    332
    333 -    def bind_to(self, parameter):
    334          raise ParameterizationError(
    335                              "This parameter is explicitly non-computed")
    336
    337      @property
    338 -    def is_virtual(self):
    339          return False
    340