1 """For manipulation of (random) variables
2
3 @var _version: Version of this module
4 @type _version: String
5 """
6
7 _version = '$Id: Variables.py,v 1.3 2008/10/07 09:14:46 jc Exp $'
8
10 """Base class for errors in the Variables module"""
11 pass
12
13 import operator
14
16 """Return x/y where 0/0 = 0
17
18 @param x: LHS of division
19 @type x: Numeric
20 @param y: LHS of division
21 @type y: Numeric
22 @return: C{x/y} where 0/0 = 0
23 @rtype: Numeric
24 @raise ZeroDivisionError: If C{y==0} and C{x!=0}.
25 """
26 try:
27 return x / y
28 except ZeroDivisionError:
29 if x == 0: return 0
30 else: raise ZeroDivisionError
31
32 -class Domain(object):
33 """A domain is a set of random variables, implicitly representing all functions
34 from joint instantiations of the variables
35
36 @ivar _domain: A dictionary mapping each variable to its set of possible values.
37 Each key is a variable name. Each value is a frozenset.
38 @type _domain: Dictionary
39 @ivar _numvals: A dictionary mapping each variable to the number of values it can have.
40 Each key is a variable name. Each value is a positive integer
41 @type _numvals: Dictionary
42 @ivar _instd: Set of instantiated variables
43 @type _instd: Set
44 """
45
46 - def __init__(self,domain=None,new_domain_variables=None,must_be_new=False):
47 """Construct a domain
48
49 @param domain: An existing domain to use. If None the object will be
50 independent of any existing domains. Otherwise C{self} and C{domain} will have identical
51 attributes.
52 @type domain: L{Domain} or None
53 @param new_domain_variables: A dictionary containing a mapping from any new
54 variables to their values.
55 @type new_domain_variables: Dict or None
56 @param must_be_new: Whether domain variables in C{new_domain_variables} have
57 to be new
58 @type must_be_new: Boolean
59 @raise VariableError: If a variable in C{new_domain_variables}
60 already exists with values different from
61 its values in C{new_domain_variables};
62 Or if C{must_be_new} is set and the variable already exists.
63 """
64 if domain is None:
65 self._domain = {}
66 self._numvals = {}
67 self._instd = set()
68 else:
69 self._domain = domain._domain
70 self._numvals = domain._numvals
71 self._instd = domain._instd
72 if new_domain_variables:
73 self.add_domain_variables(new_domain_variables,must_be_new)
74
76 return 'Domain(None,%s)' % self._domain
77
79 return str(self._domain)
80
81 - def add_domain_variable(self,variable,values,must_be_new=False):
82 """Add a variable and its associated values
83
84 If C{variable} already exists then a check is done to ensure that C{values}
85 is correct.
86 @param variable: The new variable
87 @type variable: Immutable
88 @param values: The values of the new variable
89 @type values: Iterable
90 @param must_be_new: If the variable should be a new variable
91 @type must_be_new: Boolean
92 @raise VariableError: If C{variable} already exists with values different from
93 C{values}; Or if C{must_be_new} is set and the variable already exists.
94 """
95 values = frozenset(values)
96 if variable in self._domain:
97 if must_be_new:
98 raise VariableError("%s already exists" % variable)
99 if values != self._domain[variable]:
100 raise VariableError("Conflicting values for %s\n new: %s\n old: %s" %
101 (variable, values, self._domain[variable]))
102 else:
103 self._domain[variable] = values
104 lvals = len(values)
105 self._numvals[variable] = lvals
106 if lvals == 1:
107 self._instd.add(variable)
108
109 - def add_domain_variables(self,new_domain_variables,must_be_new=False):
110 """Add variables from C{new_domain_variables}
111
112 @param new_domain_variables: A dictionary mapping variables to values
113 @type new_domain_variables: Dictionary
114 @param must_be_new: Whether domain variables in C{new_domain_variables} have
115 to be new
116 @type must_be_new: Boolean
117 @raise VariableError: If a variable in C{new_domain_variables}
118 already exists with values different from
119 its values in C{new_domain_variables};
120 Or if C{must_be_new} is set and the variable already exists.
121 """
122 for variable, values in new_domain_variables.items():
123 self.add_domain_variable(variable,values,must_be_new)
124
125 - def add_domain_variables_from_rawdata(self,rawdata,must_be_new=False):
126 """Add variables from C{rawdata}
127
128 @param rawdata: A tuple like that returned by L{IO.read_csv}.
129 @type rawdata: Tuple
130 @param must_be_new: Whether domain variables from C{rawdata} have
131 to be new
132 @type must_be_new: Boolean
133 @raise VariableError: If a variable in C{rawdata}
134 already exists with values different from
135 its values in C{rawdata};
136 Or if C{must_be_new} is set and the variable already exists.
137 """
138 self.add_domain_variables(rawdata[1],must_be_new)
139
140 - def change_domain_variable(self,variable,values):
141 """Change the values associated with a domain variable
142
143 @param variable: The variable
144 @type variable: Immutable
145 @param values: The new values of the C{variable}
146 @type values: Iterable
147 @raise KeyError: If C{variable} is not in the domain
148 """
149 values = frozenset(values)
150 self._domain[variable] = values
151 lvals = len(values)
152 self._numvals[variable] = lvals
153 if lvals == 1:
154 self._instd.add(variable)
155 else:
156 self._instd.discard(variable)
157
158 - def change_domain_variables(self,new_values):
159 """Change the values associated with a domain variables
160
161 @param new_values: The new values of the C{variable}
162 @type new_values: Iterable
163 @raise KeyError: If C{new_values} contains a variable not in the domain
164 """
165 for variable, values in new_values.items():
166 self.change_domain_variable(variable,values)
167
168 - def common_domain(self,other):
169 """Make the domain for C{other} identical to that of C{self}
170
171 The domain for self is updated to include any extra variables in C{other}
172 @param other: Domain
173 @type other: L{Domain} or subclass
174 @raise VariableError: If C{self} and C{other} use a variable with different values
175 in each one's domain.
176 """
177 if self._domain is not other._domain:
178 self.add_domain_variables(other._domain)
179 other._domain = self._domain
180 other._numvals = self._numvals
181 other._instd = self._instd
182
184 """Return a (deep) copy of a domain
185
186 @return: The copy
187 @rtype: L{Domain}
188 """
189 return Domain(new_domain_variables=self._domain)
190
191 - def known_variable(self,variable):
192 """Whether C{self} knows C{variable} (and thus its vales)
193
194 If, as is common, C{self} uses the internal default domain to
195 keep track of variables, this amounts to checking whether the
196 variable is in the internal default domain.
197
198 @param variable: A variable
199 @type variable: Immutable (usually string)
200 """
201 return variable in self._domain
202
203 - def numvals(self,variable):
204 """Return the number of values associated with a variable
205
206 @param variable: Variable whose number of values is sought.
207 @type variable: String
208 @return: The number of values associated with a variable
209 @rtype: Int
210 @raise KeyError: If C{variable} is not in the domain
211 """
212 return self._numvals[variable]
213
214 - def values(self,variable):
215 """Return the values associated with a variable
216
217 @param variable: Variable whose number of values is sought.
218 @type variable: String
219 @return: The values associated with a variable
220 @rtype: Frozenset
221 @raise KeyError: If C{variable} is not in the domain
222 """
223 return self._domain[variable]
224
225 - def variables(self):
226 """Return the object's variables
227
228 @return: The object's variables
229 @rtype: Frozenset
230 """
231 return frozenset(self._domain)
232
233 - def varvalues(self):
234 """Return the dictionary mapping the object's variables
235 to their set of possible values
236
237 @return: Map from variables to values
238 @rtype: Dictionary
239 """
240 return self._domain.copy()
241
242 _default_domain = Domain()
243 """Default domain
244
245 Initially empty. IGNORE any value given in epydoc documentation. If there
246 is one this is just an unwanted by product of the way the documentation is produced.
247 """
248
250 """Reset the internal default domain to be empty"""
251 global _default_domain
252 _default_domain = Domain()
253
255 """Print out the internal default domain
256
257 """
258 print _default_domain
259
261 """Set the internal default domain
262
263 Any previous values will be deleted!
264
265 @param dict: Mapping from each variable to the values it can have
266 @type dict: Dictionary
267 """
268 global _default_domain
269 _default_domain = Domain(new_domain_variables=dict)
270
272 """Add a variable and its associated values to the default domain
273
274 If C{variable} already exists then a check is done to ensure that C{values}
275 is correct.
276 @param variable: The new variable
277 @type variable: Immutable
278 @param values: The values of the new variable
279 @type values: Iterable
280 @param must_be_new: If the variable should be a new variable
281 @type must_be_new: Boolean
282 @raise VariableError: If C{variable} already exists with values different from
283 C{values}; Or if C{must_be_new} is set and the variable already exists.
284 """
285 _default_domain.add_domain_variable(variable,values,must_be_new=False)
286
288 """Change the values associated with a domain variable
289
290 @param variable: The variable
291 @type variable: Immutable
292 @param values: The new values of the C{variable}
293 @type values: Iterable
294 @raise KeyError: If C{variable} is not in the domain
295 """
296 _default_domain.change_domain_variable(variable,values)
297
298 -class SubDomain(Domain):
299 """A subdomain is a domain together with a specified subset of the domain variables.
300
301 A subdomain implicitly represents all functions from joint
302 instantiations of the domain variables whose values depend only on
303 the specified subset of the domain variables. As such they can represent data-less
304 L{Parameters.Factor} object.
305
306 @ivar _variables: The specified subset of domain variables.
307 @type _variables: frozenset
308 """
309
310 - def __init__(self,variables=(),domain=None,new_domain_variables=None,
311 must_be_new=False,check=False):
312 """Construct a L{SubDomain} object
313
314 @param variables: The subset of domain variables for the object.
315 @type variables: Iterable
316 @param domain: A domain for the model.
317 If None the internal default domain is used.
318 If the string 'new', a new empty domain is used.
319 @type domain: L{Domain} or None
320 @param new_domain_variables: A dictionary containing a mapping from any new
321 variables to their values. C{domain} is updated with these values
322 @type new_domain_variables: Dict or None
323 @param must_be_new: Whether domain variables in C{new_domain_variables} have
324 to be new
325 @type must_be_new: Boolean
326 @param check: Whether to check that all variables exist in C{domain}
327 @type check: Boolean
328 @raise VariableError: If a variable in C{new_domain_variables}
329 already exists with values different from
330 its values in C{new_domain_variables};
331 Or if C{must_be_new} is set and the variable already exists.
332 Or if C{check} is set and a variable in C{variables} is not in the domain
333 """
334 if domain is None:
335 domain = _default_domain
336 elif domain == 'new':
337 domain = Domain()
338 Domain.__init__(self,domain,new_domain_variables,must_be_new)
339 variables = frozenset(variables)
340 if check and not variables.issubset(self._domain):
341 raise VariableError("Variables %s not a subset of %s " %
342 (variables,frozenset(self._domain)))
343 self._variables = variables
344
345 - def __add__(self, other):
346 """Factor addition
347
348 ('Factors' can be L{SubDomain} objects.)
349 @param other: Factor or scalar on the RHS of the addition
350 @type other: Typically L{Parameters.Factor} or float object
351 @return: Result of factor addition
352 @rtype: Same as C{self}
353 @raise VariableError: If C{self} and C{other} use a variable with different values
354 in each one's domain.
355 """
356 return self.copy()._pointwise_op(other,operator.add)
357
358 - def __div__(self,other):
359 """Division, typically of factors
360
361 0/0 is defined to equal 1
362 ('Factors' can be L{SubDomain} objects.)
363 @param other: Factor or scalar on the RHS of the division
364 @type other: Typically L{Parameters.Factor} object or float object
365 @return: Result of factor division
366 @rtype: Same as C{self}
367 @raise VariableError: If C{self} and C{other} use a variable with different values
368 in each one's domain.
369 """
370 return self.copy()._pointwise_op(other,extdiv)
371
372 - def __iadd__(self,other):
373 """Does in-place addition
374
375 ('Factors' can be L{SubDomain} objects.)
376 @param other: Factor or scalar on the RHS of the addition
377 @type other: Typically L{Parameters.Factor} or float object
378 @return: C{self} after being added
379 @rtype: Same as C{self}
380 @raise VariableError: If C{self} and C{other} use a variable with different values
381 in each one's domain.
382 """
383 return self._pointwise_op(other, operator.add)
384
385 - def __idiv__(self,other):
386 """Does in-place division
387
388 ('Factors' can be L{SubDomain} objects.)
389 @param other: Factor or scalar on the RHS of the division
390 @type other: Typically L{Parameters.Factor} or float object
391 @return: C{self} after being divided
392 @rtype: Same as C{self}
393 @raise VariableError: If C{self} and C{other} use a variable with different values
394 in each one's domain.
395 """
396 return self._pointwise_op(other,extdiv)
397
398 - def __imul__(self,other):
399 """Does in-place multiplication
400
401 ('Factors' can be L{SubDomain} objects.)
402 @param other: Factor or scalar on the RHS of the multiplication
403 @type other: Typically L{Parameters.Factor} or float object
404 @return: C{self} after being multiplied
405 @rtype: Same as C{self}
406 @raise VariableError: If C{self} and C{other} use a variable with different values
407 in each one's domain.
408 """
409 return self._pointwise_op(other,operator.mul)
410
411 - def __isub__(self,other):
412 """Does in-place subtraction
413
414 ('Factors' can be L{SubDomain} objects.)
415 @param other: Factor or scalar on the RHS of the subtraction
416 @type other: Typically L{Parameters.Factor} or float object
417 @return: C{self} after being subtracted
418 @rtype: Same as C{self}
419 @raise VariableError: If C{self} and C{other} use a variable with different values
420 in each one's domain.
421 """
422 return self._pointwise_op(other, operator.sub)
423
424 - def __mul__(self,other):
425 """Factor multiplication
426
427 ('Factors' can be L{SubDomain} objects.)
428 @param other: Factor or scalar on the RHS of the multiplication
429 @type other: Typically L{Parameters.Factor} or float object
430 @return: Result of factor multiplication
431 @rtype: Same as C{self}
432 @raise VariableError: If C{self} and C{other} use a variable with different values
433 in each one's domain.
434 """
435 return self.copy()._pointwise_op(other,operator.mul)
436
437 - def __rdiv__(self,other):
438 """Only called when evaluating other / self, where
439 other is not a Factor, but self is
440
441 ('Factors' can be L{SubDomain} objects.)
442 @param other: Number
443 @type other: Numeric
444 @return: C{other} / C{self}
445 @rtype: Same as C{self}
446 @raise TypeError: If other is not a number
447 """
448 return self.copy()._pointwise_op(other,extdiv,swapped=True)
449
450 - def __sub__(self, other):
451 """Factor subtraction
452
453 ('Factors' can be L{SubDomain} objects.)
454 @param other: Factor or scalar on the RHS of the subtraction
455 @type other: Typically L{Parameters.Factor} or float object
456 @return: Result of factor subtraction
457 @rtype: Same as C{self}
458 @raise VariableError: If C{self} and C{other} use a variable with different values
459 in each one's domain.
460 """
461 return self.copy()._pointwise_op(other,operator.sub)
462
463 - def __repr__(self):
464 return 'SubDomain(%s,%s)' % (self._variables,Domain.__repr__(self))
465
467 return str(dict((v,self._domain[v]) for v in self._variables))
468
469 - def __rmul__(self,other):
470 """Only called when evaluating other*self, where
471 other is not a Factor, but self is
472
473 ('Factors' can be L{SubDomain} objects.)
474 @param other: Number
475 @type other: Numeric
476 @return: C{other} * C{self}
477 @rtype: Same as C{self}
478 @raise TypeError: If other is not a number
479
480 """
481 return self.copy()._pointwise_op(other,operator.mul,swapped=True)
482
483 - def copy(self,copy_domain=False):
484 """Return a 'copy' of a subdomain
485
486 @param copy_domain: If true C{self}'s domain is copied, otherwise the copy
487 shares C{self}'s domain
488 @type copy_domain: Boolean
489 @return: The copy
490 @rtype: L{SubDomain}
491 """
492 if copy_domain:
493 domain = Domain.copy(self)
494 else:
495 domain = self
496 return SubDomain(self._variables,domain)
497
498
499 - def insts(self,variables=None):
500 """Return an iterator over joint instantiations of C{variables}
501
502 Each instantiation is a tuple of values, the order of the values
503 corresponding to the ordering of variables in C{variables}. The instantiations
504 themselves follow the standard ordering.
505 @param variables: Which variables to include. If None, all the table/factor's
506 variables are included in order.
507 @type variables: Sequence or None
508 @return: An iterator over joint instantiations
509 @rtype: Iterator
510 @raise KeyError: If a variable in C{variables} is not defined.
511 """
512 if variables is None:
513 variables = sorted(self._variables)
514 return self._insts(variables)
515
516 - def insts_indices(self,variables=None):
517 """Return an iterator over the indices of joint instantiations of C{variables}
518
519 Each instantiation is a tuple of integers, the order of the values
520 corresponding to the ordering of variables in C{variables}. The instantiations
521 themselves follow the standard ordering.
522 @param variables: Which variables to include. If None, all the table/factor's
523 variables are included in order.
524 @type variables: Sequence or None
525 @return: An iterator over joint instantiations
526 @rtype: Iterator
527 @raise KeyError: If a variable in C{variables} is not defined.
528 """
529 if variables is None:
530 variables = sorted(self._variables)
531 return self._insts_indices(variables)
532
533 - def inst2index(self,inst):
534 """Return the index associated with the instantiation C{inst}.
535
536 The first time this is called a lookup table (dictionary) is created
537 to enable subsequent calls to be returned by lookup.
538 @param inst: A tuple of ordered values, one for each variable
539 @type inst: Tuple
540 @return: The index associated with the instantiation C{inst}
541 @rtype: Int
542 @raise KeyError: If there is no joint instantiation C{inst}.
543 """
544 try:
545 return self._inst2index[inst]
546 except AttributeError:
547 self._inst2index = {}
548 for i, tmp_inst in enumerate(self.insts()):
549 self._inst2index[tmp_inst] = i
550 return self._inst2index[inst]
551
552 - def drop_variable(self,variable):
553 """Alter self by dropping C{variable}
554
555 @param variable: Variable to drop
556 @type variable: Immutable
557 @return: The altered C{self}
558 @rtype: Class of C{self}
559 """
560 return self.drop_variables((variable,))
561
562
563 - def drop_variables(self,variables):
564 """Alter self by dropping C{variables}
565
566 C{variables} is not altered. Variables in C{variables}
567 which are not in C{self}'s variables are ignored.
568 @param variables: Variables to drop
569 @type variables: Sequence
570 @return: The altered C{self}
571 @rtype: Class of C{self}
572 """
573 self._variables = self._variables.difference(variables)
574 return self
575
576
577 - def marginalise_onto(self,variables):
578 """Alter self by marginalising onto the intersection of
579 C{variables} and C{self}'s variables
580
581 @param variables: Variables to keep
582 @type variables: Sequence
583 """
584 return self.marginalise_away(self._variables.difference(variables))
585
586
587 - def table_size(self,variables=None):
588 """Return the number of joint instantiations of C{variables} in C{self}
589
590 @param variables: Variables for which we want number of joint instantiations.
591 If None, all variables are considered.
592 @type variables: Iterable or None
593 @return: The number of joint instantiations of variables in C{self}
594 @rtype: Integer
595 """
596 if variables is None:
597 variables = self._variables
598 return reduce(operator.mul,[self._numvals[v] for v in variables],1)
599
600 - def sumout(self,vars_togo):
601 """Summing out (marginalising) the variables vars_togo from the factor
602
603 Does not alter C{self}
604 @param vars_togo: Variables to sum out
605 @type vars_togo: Iterable, e.g. list, tuple, set
606 @return: New factor with vars_togo summed out
607 @rtype: L{Parameters.Factor} object
608 """
609 return self.copy().marginalise_away(vars_togo)
610
612 """Whether C{self} uses the internal default domain
613
614 @return: Whether C{self} uses the internal default domain
615 @rtype: Boolean
616 """
617 return self._domain is _default_domain._domain
618
619 - def variables(self):
620 return self._variables
621
622 - def varvalues(self):
623 return dict([[k,self._domain[k]] for k in self._variables])
624
625
626 - def _decode_inst(self,inst):
627 if isinstance(inst,(tuple,list)):
628 return self.inst2index(tuple(inst))
629 elif isinstance(inst,dict):
630 vals = []
631 for name in sorted(self._variables):
632 val = inst[name]
633 if val not in self._domain[name]:
634 raise KeyError("%s has no value called %s" % (name,val))
635 else:
636 vals.append(val)
637 return self.inst2index(tuple(vals))
638 else:
639 return inst
640
641
642 - def _insts(self,variables):
643 if variables:
644 for value in sorted(self._domain[variables[0]]):
645 for inst in self._insts(variables[1:]):
646 yield ((value),) + inst
647 else:
648 yield ()
649
650 - def _insts_indices(self,variables):
651 if variables:
652 for i in range(self._numvals[variables[0]]):
653 for inst in self._insts_indices(variables[1:]):
654 yield ((i),) + inst
655 else:
656 yield ()
657
658
659 - def _pointwise_op(self,other,op,swapped=False):
660 """
661 @raise VariableError: If C{self} and C{other} use a variable with different values
662 in each one's values dictionary.
663 """
664 if not isinstance(other,(float,int)):
665 self._variables = self._get_result_variables(other)
666 return self
667
668 - def _get_result_variables(self,other):
669 """
670 @raise VariableError: If C{self} and C{other} use a variable with different values
671 in each one's domain.
672 """
673 self.common_domain(other)
674 return self._variables | other._variables
675