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
63
66
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):
81
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
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
125 """
126 Parameter name
127 """
128 return self._name
129
130 @property
132 """
133 Parameter value (guaranteed to be up to date)
134 """
135 self._ensure_consistency()
136 return self._value
137
138 @property
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
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
200 self._base = parameter
201
203 self._derivatives.add(parameter)
204
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
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
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
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
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
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
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
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
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
312 """
313 Default parameter implementation which accepts float values only.
314 """
315
316 - def __init__(self, value=0.0, name=None, base=None):
318
325
328 """
329 A float L{Parameter} that is explicitly non-computed and cannot be
330 bound to another L{Parameter}.
331 """
332
336
337 @property
340