1 """Classes for matrices with symbolic indices (i.e., L{Key} objects)
2 """
3 try:
4 from numpy.core.numeric import array
5 try:
6 from numpy.core.numeric import matrixmultiply
7 except ImportError:
8 from numpy.core.numeric import dot as matrixmultiply
9 except ImportError:
10 try:
11 from scipy import array,matrixmultiply
12 except ImportError:
13 from Numeric import array,matrixmultiply
14 from xml.dom.minidom import *
15 import copy
16 from matrices import epsilon
17 from KeyedVector import *
18
20 """A dictionary-based representation of a two-dimensional matrix
21 @cvar defaultValue: the default value to insert into any missing positions
22 @type defaultValue: C{float}
23 @ivar _array: The numeric representation of this matrix
24 @ivar _fresh: flag indicating whether there have been any changes to the array contents since the last update to the numeric array
25 @type _fresh: C{boolean}
26 @ivar _frozen: flag indicating whether the dimensions of this matrix are subject to change
27 @type _frozen: C{boolean}
28 @ivar _rowKeys: an ordered list of the indices of the rows of this matrix
29 @type _rowKeys: L{Key}[]
30 @ivar _rowOrder: a dictionary containing the row position of each key within the matrix
31 @type _rowOrder: L{dict}(L{Key}:C{int})
32 @warning: Use the L{set} method to set individual values. Using built-in assignment calls (e.g., M[row][col] = 0.) will lead to inconsistencies. If you can think of a clever way to enforce this in code, please feel free to do so.
33 """
34 defaultValue = 0.
35 dimension = 2
37 dict.__init__(self)
38 self._fresh = False
39 self._frozen = False
40 self._rowOrder = {}
41 if len(args) > 0:
42
43 self._rowKeys = args.keys()
44 self._rowKeys.sort()
45 for index in range(len(args)):
46
47 key = self._rowKeys[index]
48 self._rowOrder[key] = index
49
50 values = []
51 for rowKey in self._rowKeys:
52 row = args[rowKey]
53 dict.__setitem__(self,rowKey,row)
54 rowValues = []
55 for colKey in row.keys():
56 try:
57 rowValues.append(args[rowKey][colKey])
58 except KeyError:
59 rowValues.append(self.defaultValue)
60 values.append(rowValues)
61 self._array = array(values)
62 else:
63 self._rowKeys = []
64 self._array = None
65
67 """
68 @return: ordered list of the keys to all rows in this matrix
69 @rtype: L{Key}[]
70 """
71 return self._rowKeys
72
74 """
75 @return: ordered list of the keys to all columns in this matrix
76 @rtype: L{Key}[]
77 """
78 if len(self) == 0:
79 return []
80 else:
81 return self.values()[0].keys()
82
84 """
85 @return: the position of the given key within the columns of this matrix
86 @rtype: int
87 """
88 if len(self) == 0:
89 raise ValueError,'No column %s in this matrix' % (str(key))
90 else:
91 try:
92 return self.values()[0]._order[key]
93 except KeyError:
94 raise ValueError,'No column %s in this matrix' % (str(key))
95
96 - def set(self,row,col,value):
97 """Sets an individual value in this matrix
98 @param row,col: the keys for this value's position
99 @type row,col: L{Key}
100 @param value: the value to insert
101 @type value: C{float}
102 """
103 self._fresh = False
104
105 if not self._rowOrder.has_key(row):
106 self.addRows([row])
107 self[row][col] = value
108
110 """Finds the row position where this key should be inserted
111 @param key: the key to insert
112 @type key: L{Key}
113 @return: the index to insert at
114 @rtype: C{int}
115 @warning: does not check whether key is already present
116 """
117
118 for index in range(len(self._rowOrder)):
119 if key < self.rowKeys()[index]:
120 break
121 else:
122 index = len(self._rowOrder)
123 return index
124
125 - def addRows(self,keys,value=None):
126 """Adds a new row slot to this matrix
127 @param keys: the (sorted) keys for the new slots to insert
128 @type keys: L{Key}[]
129 @param value: the row to insert (defaults to a row of L{defaultValue})
130 @type value: C{float}
131 @warning: assumes that this row does not already exist
132 """
133 if self._frozen:
134 raise UserWarning,'You are modifying a frozen matrix'
135 self._fresh = False
136
137 newKeys = []
138 newValues = {}
139 for key in keys:
140 if not self.has_key(key):
141 newKeys.append(key)
142 if value is None:
143 newValues[key] = KeyedVector()
144 elif isinstance(value,KeyedVector):
145 if len(keys) > 1:
146 newValues[key] = copy.copy(value)
147 else:
148 newValues[key] = value
149 elif isinstance(value,dict):
150 try:
151 newValues[key] = value[key]
152 except KeyError:
153 newValues[key] = KeyedVector()
154 else:
155
156 newValues[key] = KeyedVector()
157
158 if len(newKeys) == 0:
159 return
160
161 finalOrder = {}
162 finalKeys = []
163 finalArray = []
164 oldIndex = 0
165 newIndex = 0
166
167 while newIndex < len(newKeys):
168
169 newKey = newKeys[newIndex]
170 while oldIndex < len(self._rowKeys) and \
171 self._rowKeys[oldIndex] < newKey:
172
173 key = self._rowKeys[oldIndex]
174 finalOrder[key] = len(finalKeys)
175 finalKeys.append(key)
176 oldIndex += 1
177
178 finalOrder[newKey] = len(finalKeys)
179 finalKeys.append(newKey)
180 dict.__setitem__(self,newKey,newValues[newKey])
181 newIndex += 1
182 while oldIndex < len(self._rowKeys):
183
184 key = self._rowKeys[oldIndex]
185 finalOrder[key] = len(finalKeys)
186 finalKeys.append(key)
187 oldIndex += 1
188
189 self._rowOrder = finalOrder
190 self._rowKeys = finalKeys
191
193 """
194 @return: C{True} iff this matrix is an identity matrix
195 @rtype: boolean
196 """
197 for key in self.rowKeys():
198 row = self[key]
199 if not isinstance(row,UnchangedRow) or row.sourceKey != key:
200 break
201 else:
202 return True
203 return False
204
206 """Adds new column slots to this matrix
207 @param keys: the (sorted) keys for the new slots to insert
208 @type keys: L{Key}[]
209 @param value: the column to insert (defaults to a row of L{defaultValue})
210 @type value: C{float}
211 """
212 if self._frozen:
213 raise UserWarning,'You are modifying a frozen matrix'
214 self._fresh = False
215 if value is None:
216 value = self.defaultValue
217 for row in self.values():
218 row.addColumns(keys,value)
219
221 """
222 @return: the numeric array representation of this matrix
223 @rtype: C{array}
224 """
225 if not self._fresh:
226 rowValues = map(lambda k:self[k].getArray(),self.rowKeys())
227 self._array = array(rowValues)
228 self._fresh = True
229 return self._array
230
246
248 if self._frozen:
249 raise UserWarning,'You are modifying a frozen matrix'
250 self._fresh = False
251 index = self._rowOrder[key]
252 del self._rowOrder[key]
253 for other in self._rowKeys[index+1:]:
254 self._rowOrder[other] -= 1
255 self._rowKeys.remove(key)
256 dict.__delitem__(self,key)
257
258
260 """Removes the specified column from the matrix
261 @param key: the index for the column to remove
262 @type key: L{Key}
263 """
264 if self._frozen:
265 raise UserWarning,'You are modifying a frozen matrix'
266 self._fresh = False
267 for row in self.values():
268 del row[key]
269
270 - def fill(self,keys,value=None):
271 """Fills in any missing rows and columns with a default value
272 @param keys: the new slots that should be filled
273 @type keys: list of L{Key} instances
274 @param value: the default value (default is L{defaultValue})
275 @note: does not overwrite existing values, but does fill in values for missing existing keys
276 """
277 self.addRows(keys,value)
278 self.addColumns(keys,value)
279
281 """Locks in the dimensions and keys of this matrix"""
282 self._frozen = True
283 for row in self.values():
284 row.freeze()
285
287 """Unlocks the dimensions and keys of this matrix"""
288 self._rowOrder = copy.copy(self._rowOrder)
289 self._rowKeys = copy.copy(self._rowKeys)
290 for row in self.values():
291 row.unfreeze()
292 self._frozen = False
293
295 """Composes the two matrices together using the given operator
296 @param other: the other matrix to compose with
297 @type other: L{KeyedMatrix},float
298 @param op: the operator used to generate the new array values
299 @type op: C{lambda x,y:f(x,y)} where C{x} and C{y} are C{array} instances
300 @rtype: a new L{KeyedMatrix} instance"""
301 if isinstance(other,float):
302 return self.copyFromArray(op(self.getArray(),other))
303 else:
304 return self.copyFromArray(op(self.getArray(),other.getArray()))
305
307 return self.compose(other,lambda x,y:x+y)
308
310 return self + (-other)
311
313 result = self.__class__()
314 result._array = -(self.getArray())
315 result._rowOrder = self._rowOrder
316 result._rowKeys = self._rowKeys
317 for key,row in self.items():
318 dict.__setitem__(result,key,-row)
319 if self._frozen:
320 result.freeze()
321 else:
322 result.unfreeze()
323 return result
324
332
334 """Handles multiplication when multiplicand is two-dimensional
335 @type other: L{KeyedMatrix}"""
336 return self.copyFromArray(matrixmultiply(self.getArray(),
337 other.getArray()))
338
340 """Handles multiplication when multiplicand is one-dimensional
341 @type other: L{KeyedVector}"""
342 result = KeyedVector()
343 try:
344 result._array = matrixmultiply(self.getArray(),other.getArray())
345 except ValueError,e:
346 raise UserWarning,'Your matrices are not aligned; perhaps use fill method to align'
347 result._order = self._rowOrder
348 result._orderedKeys = self._rowKeys
349 result._fresh = None
350 result._updateString()
351 if self._frozen:
352 result.freeze()
353 else:
354 result.unfreeze()
355 return result
356
358 """Handles multiplication when multiplicand is something unknown class (typically, C{float})
359 """
360 return self.copyFromArray(self.getArray() * other)
361
363 """Copies all aspects of this array (keys, etc.) except for the numerical value, which is taken fom the given matrix
364 @param matrix: the numerical value to use
365 @type matrix: array
366 @rtype: L{KeyedMatrix}
367 """
368 result = KeyedMatrix()
369 result._array = matrix
370 result._rowKeys = self._rowKeys
371 result._rowOrder = self._rowOrder
372 for rowKey,oldRow in self.items():
373 row = KeyedVector()
374 row._orderedKeys = oldRow._orderedKeys
375 row._order = oldRow._order
376 row._array = result._array[self._rowOrder[rowKey]]
377 dict.__setitem__(result,rowKey,row)
378 if self._frozen:
379 result.freeze()
380 else:
381 result.unfreeze()
382 return result
383
387
389 """@warning: This is not cached, so repeated hashing may be inefficient"""
390 return hash(str(self))
391
393 """Merges rows from the two matrices (if any row keys are common, then the new row is used)
394 @param other: the matrix to merge
395 @type other: L{KeyedMatrix} instance
396 @return: The new matrix
397 @rtype: L{KeyedMatrix}
398 """
399 result = copy.copy(self)
400 result.addRows(other.rowKeys(),other)
401 return result
402
404 """Substitutes values for any abstract references, using the
405 given substitution table
406 @param table: dictionary of key-value pairs, where the value will be substituted for any appearance of the given key in a field of this L{Key} object
407 @type table: dictionary"""
408
409 newValues = {}
410 for key,value in self.items():
411 row = value.instantiate(table)
412 newKey = key.instantiate(table)
413 if isinstance(newKey,list):
414 keyList = newKey
415 elif newKey == keyDelete:
416 keyList = []
417 else:
418 keyList = [newKey]
419 for newKey in keyList:
420 try:
421 newValues[newKey] += row
422 except KeyError:
423 newValues[newKey] = row
424 return KeyedMatrix(newValues)
425
427 """Substitutes values for any abstract references, using the
428 given substitution table
429 @param table: dictionary of key-value pairs, where the value will be substituted for any appearance of the given key in a field of this L{Key} object
430 @type table: dictionary"""
431
432 newValues = {}
433 for key,value in self.items():
434 value.instantiateKeys(table)
435 newKey = key.instantiate(table)
436 if isinstance(newKey,list):
437 keyList = newKey
438 elif newKey == keyDelete:
439 keyList = []
440 else:
441 keyList = [newKey]
442 for newKey in keyList:
443 try:
444 newValues[newKey] += value
445 except KeyError:
446 newValues[newKey] = value
447 dict.__delitem__(self,key)
448
449 self._rowOrder.clear()
450 self._rowKeys = newValues.keys()
451 self._rowKeys.sort()
452 for index in range(len(self._rowKeys)):
453 key = self._rowKeys[index]
454 dict.__setitem__(self,key,newValues[key])
455 self._rowOrder[key] = index
456
458 """@return: An XML Document object representing this matrix"""
459 doc = Document()
460 root = doc.createElement('matrix')
461 doc.appendChild(root)
462 for key,value in self.items():
463 node = doc.createElement('row')
464 root.appendChild(node)
465 node.appendChild(key.__xml__().documentElement)
466 node.appendChild(value.__xml__().documentElement)
467 return doc
468
469 - def parse(self,element):
470 """Extracts the distribution from the given XML element
471 @param element: The XML object specifying the distribution
472 @type element: Element
473 @warning: modifies object in place; erases any previous contents of this matrix
474 """
475 self.clear()
476 if element.tagName != 'matrix':
477 raise UserWarning,'Expected matrix Element, but got %s instead' %\
478 element.tagName
479 matrixType = str(element.getAttribute('type'))
480 try:
481
482 cls = dynamicsTypes[matrixType]
483 except KeyError:
484
485 for cls in dynamicsTypes.values():
486 if cls.__name__ == '%sMatrix' % (matrixType):
487 break
488 else:
489
490 cls = self.__class__
491 if not isinstance(self,cls):
492 matrix = cls()
493 return matrix.parse(element)
494 else:
495 matrix = self
496 node = element.firstChild
497 while node:
498 if node.nodeType == node.ELEMENT_NODE:
499 for child in node.childNodes:
500 if child.nodeType != child.ELEMENT_NODE:
501 pass
502 elif child.tagName == 'vector':
503 row = KeyedVector()
504 row = row.parse(child)
505 elif child.tagName == 'key':
506 key = Key()
507 key = key.parse(child)
508 else:
509 msg = 'Unable to parse %s element of tag "%s"' % \
510 (matrix.__class__.__name__,child.tagName)
511 raise NotImplementedError,msg
512 matrix[key] = row
513 node = node.nextSibling
514 return matrix
515
516 - def simpleText(self,numbers=True,all=False):
517 """
518 @return: a more user-friendly string representation of this matrix
519 @rtype: C{str}"""
520 content = '{\n'
521 for key in self.rowKeys():
522 row = self[key]
523 for other in row.keys():
524 if all or \
525 (other != key and abs(self[key][other]) > 0.) or \
526 (other == key and self[key][other] != 1.):
527 content += '%s: %s\n' % (key.simpleText(),
528 row.simpleText(numbers=numbers,
529 all=all))
530 break
531 content += '}'
532 return content
533
535 result = self.__class__()
536 result._fresh = False
537 result._rowOrder = copy.copy(self._rowOrder)
538 result._rowKeys = copy.copy(self._rowKeys)
539 for key,row in self.items():
540 dict.__setitem__(result,key,row)
541 return result
542
544 result = self.__class__()
545 memo[id(self)] = result
546 result._fresh = False
547 result._rowOrder = self._rowOrder
548 result._rowKeys = self._rowKeys
549 if self._frozen:
550 result.freeze()
551 else:
552 result.unfreeze()
553 for key,row in self.items():
554 dict.__setitem__(result,key,copy.deepcopy(row,memo))
555 return result
556
558 """Matrix subclass that provides some structure to be exploited for greater user friendliness"""
559 rowClass = DeltaRow
560
561 - def __init__(self,source=None,key=None,value=0.):
573
578
583
585 """@return: An XML Document object representing this matrix"""
586 doc = KeyedMatrix.__xml__(self)
587 doc.documentElement.setAttribute('type',self.__class__.__name__[:-6])
588 return doc
589
591 """A matrix for incrementing/decrementing the value of a given feature by a constant amount
592
593 >>> matrix = IncrementMatrix('economicPower',value=0.1)
594 """
595 rowClass = IncrementRow
596
598 """A matrix for incrementing/decrementing the value of a given feature by some percentage of another feature (possibly the same one)
599
600 >>> matrix = ScaleMatrix('economicPower',key,0.1)
601 """
602 rowClass = ScaleRow
603
605 """A matrix for setting the value of a given feature to a specified constant value
606
607 >>> matrix = SetToConstantMatrix(source='economicPower',value=0.)
608 """
609 rowClass = SetToConstantRow
610
612 """A matrix for setting the value of a given feature to a specified percentage of some other feature
613
614 >>> matrix = SetToFeatureMatrix(source='economicPower',key,value=0.5)
615 """
616 rowClass = SetToFeatureRow
617
619 """A matrix for setting the value of a given feature to a count of a certain type of action
620
621 >>> matrix = ActionCountMatrix(source='economicPower',key,value=0.5)
622 """
623 rowClass = ActionCountRow
624
626 """A matrix for leaving the value of a given feature unchanged
627
628 >>> matrix = IdentityMatrix('economicPower')
629 """
630 rowClass = UnchangedRow
631
633 """Automatic extraction of possible L{DynamicsMatrix} subclasses
634 @return: all available subclasses of L{DynamicsMatrix}
635 @rtype: C{dict:str->class}
636 """
637 import inspect
638 result = {}
639 for key,value in globals().items():
640 if inspect.isclass(value) and issubclass(value,DynamicsMatrix) and \
641 not value is DynamicsMatrix:
642 result[value.rowClass.label] = value
643 return result
644 dynamicsTypes = getDynamicsTypes()
645
647 """Generates an identity matrix, M, over the set of keys. This matrix is filled and frozen.
648 @param keys: the keys of the vector, v, for which we want M*v=v
649 @type keys: L{Key}[]
650 @rtype: L{KeyedMatrix}
651 """
652 matrix = KeyedMatrix()
653 for key in keys:
654 row = KeyedVector()
655 row[key] = 1.
656 matrix[key] = row
657 matrix.fill(keys)
658 matrix.freeze()
659 return matrix
660
661 if __name__ == '__main__':
662
663 row1 = KeyedRow()
664 row1['b'] = .4
665 print 'Row 1:',row1
666
667 row2 = KeyedRow({'a':.3})
668 print 'Row 2:',row2
669
670 print 'Row 2, element "a":',row2['a']
671
672 print 'Row 1 + Row 2 = ',row1 + row2
673
674 print 'Row 1 - Row 2 = ',row1 - row2
675
676 print 'Row 1 * Row 2 = ',row1*row2
677
678 print 'Row 1 * 0.99 = ',row1*.99
679
680
681 matrix1 = KeyedMatrix()
682 matrix1['a'] = row1 + row2
683 matrix1['b'] = row1*2.
684 print 'Matrix 1:',matrix1
685
686 matrix2 = KeyedMatrix({'a':row2})
687 matrix2['a']['b'] = 0.5
688 print 'Matrix 2:',matrix2
689
690 print 'Matrix 1, row "a":',matrix1['a']
691 print 'Matrix 2, row "a", element "b":',matrix2['a']['a']
692
693 print 'Matrix 1, col "b":',matrix1.column('b')
694
695 print 'Matrix 1 + Matrix 2 = ',matrix1+matrix2
696
697 print 'Matrix 1 * 0.99 = ',matrix1*.99
698
699 print 'Matrix 1 * (Row 1+Row 2) = ',matrix1*(row1+row2)
700
701 print '(Row 1+Row 2) * Matrix 1 = ',(row1+row2)*matrix1
702
703
704 print 'Matrix 1 * Matrix 2 = ',matrix1*matrix2
705