1
"""Additional auxiliary data types"""
4
"""Simple matrix data type.
6
Of course there are much more advanced matrix data types for Python (for
7
instance, the C{ndarray} data type of Numeric Python) and this implementation
8
does not want to compete with them. The only role of this data type is to
9
provide a convenient interface for the matrices returned by the C{Graph}
10
object (for instance, allow indexing with tuples in the case of adjacency
14
def __init__(self, data=None):
15
"""Initializes a matrix.
17
@param data: the elements of the matrix as a list of lists, or C{None} to
23
def Fill(klass, value, *args):
24
"""Creates a matrix filled with the given value
26
@param value: the value to be used
27
@keyword shape: the shape of the matrix. Can be a single integer,
28
two integers or a tuple. If a single integer is
29
given here, the matrix is assumed to be square-shaped.
31
if len(args) < 1: raise TypeError, "expected an integer or a tuple"
33
if hasattr(args[0], "__len__"):
34
h, w = map(int, args[0][0:2])
36
h, w = int(args[0]), int(args[0])
37
else: h, w = map(int, args[0:2])
38
mtrx = [[value]*w for _ in xrange(h)]
40
Fill=classmethod(Fill)
42
def Zero(klass, *args):
43
"""Creates a matrix filled with zeros.
45
@keyword shape: the shape of the matrix. Can be a single integer,
46
two integers or a tuple. If a single integer is
47
given here, the matrix is assumed to be square-shaped.
49
result = klass.Fill(0, *args)
51
Zero=classmethod(Zero)
53
def Identity(klass, *args):
54
"""Creates an identity matrix.
56
@keyword shape: the shape of the matrix. Can be a single integer,
57
two integers or a tuple. If a single integer is
58
given here, the matrix is assumed to be square-shaped.
60
result=klass.Fill(0, *args)
61
for i in xrange(min(*result.shape)): result._data[i][i] = 1
63
Identity=classmethod(Identity)
65
def _set_data(self, data=None):
66
"""Sets the data stored in the matrix"""
68
self._data = map(list, data)
69
self._nrow = len(data)
70
self._ncol = max(map(len, data))
71
for row in self._data:
72
if len(row) < self._ncol: row.extend([0]*(self._ncol-len(row)))
75
"""Returns the data stored in the matrix as a list of lists"""
76
return [list(row) for row in self._data]
77
data = property(_get_data, _set_data, doc="Elements of the matrix as a list of lists")
80
"""Returns the shape of the matrix"""
81
return self._nrow, self._ncol
82
shape = property(_get_shape, doc="Shape of the matrix as a tuple")
84
def __getitem__(self, i):
85
"""Returns a single item, a row or a column of the matrix
87
@param i: if a single integer, returns the M{i}th row as a list. If a
88
slice, returns the corresponding rows as another L{Matrix} object. If
89
a 2-tuple, the first element of the tuple is used to select a row and
90
the second is used to select a column.
92
if isinstance(i, int):
93
return list(self._data[i])
94
elif isinstance(i, slice):
95
return self.__class__(self._data[i])
96
elif isinstance(i, tuple):
105
if type(first) == slice and type(second) == slice:
106
return self.__class__([row[second] for row in self._data[first]])
107
elif type(first) == slice:
108
return [row[second] for row in self._data[first]]
110
return self._data[first][second]
112
raise IndexError, "invalid matrix index"
115
def __setitem__(self, i, value):
116
"""Sets a single item, a row or a column of the matrix
118
@param i: if a single integer, sets the M{i}th row as a list. If a
119
slice, sets the corresponding rows from another L{Matrix} object.
120
If a 2-tuple, the first element of the tuple is used to select a row
121
and the second is used to select a column.
122
@param value: the new value
124
if isinstance(i, int):
126
if len(value) != len(self._data[i]):
127
raise ValueError, "new value must have %d items" % len(self._data[i])
128
self._data[i] = list(value)
129
elif isinstance(i, slice):
130
# Setting multiple rows
131
if len(value) != len(self._data[i]):
132
raise ValueError, "new value must have %d items" % len(self._data[i])
133
for j in xrange(len(value)):
134
if len(value[j]) != len(self._data[0]):
135
raise ValueError, "rows of new value must have %d items" % len(self._data[0])
136
self._data[i] = list(map(list, value))
137
elif isinstance(i, tuple):
146
if type(first) == slice and type(second) == slice:
147
# Setting a submatrix
149
raise NotImplementedError
150
elif type(first) == slice:
151
# Setting a submatrix
152
raise NotImplementedError
154
# Setting a single element
155
self._data[first][second] = value
157
raise IndexError, "invalid matrix index"
161
return "Matrix([\n %s])" % ",\n ".join(["[%s]" % (", ".join(map(repr, row))) for row in self])
164
return "[%s]" % "\n ".join(["[%s]" % (", ".join(map(str, row))) for row in self])
167
"""Support for iteration.
169
This is actually implemented as a generator, so there is no need for a
170
separate iterator class. The generator returns I{copies} of the rows in
171
the matrix as lists to avoid messing around with the internals. Feel
172
free to do anything with the copies, the changes won't be reflected in
173
the original matrix."""
174
for row in self._data: yield list(row)
176
def __plot__(self, context, bbox, palette, *args, **kwds):
177
"""Plots the matrix to the given Cairo context in the given box
179
Besides the usual self-explanatory plotting parameters (C{context},
180
C{bbox}, C{palette}), it accepts the following keyword arguments:
182
- C{style}: the style of the plot. C{boolean} is useful for plotting
183
matrices with boolean (C{True}/C{False} or 0/1) values: C{False}
184
will be shown with a white box and C{True} with a black box.
185
C{palette} uses the given palette to represent numbers by colors,
186
the minimum will be assigned to palette color index 0 and the maximum
187
will be assigned to the length of the palette. The default style is
188
C{boolean} (but it may change in the future). C{None} values in
189
the matrix are treated specially in both cases: nothing is drawn in
190
the cell corresponding to C{None}.
192
- C{square}: whether the cells of the matrix should be square or not.
195
- C{grid_width}: line width of the grid shown on the matrix. If zero or
196
negative, the grid is turned off. The grid is also turned off if the size
197
of a cell is less than three times the given line width. Default is C{1}.
198
Fractional widths are also allowed.
200
- C{border_width}: line width of the border shown around the matrix.
201
If zero or negative, the border is turned off. Default is C{1}.
203
- C{row_names}: the names of the rows
205
- C{col_names}: the names of the columns.
207
- C{values}: values to be displayed in the cells. If C{None} or
208
C{False}, no values are displayed. If C{True}, the values come
209
from the matrix being plotted. If it is another matrix, the
210
values of that matrix are shown in the cells. In this case,
211
the shape of the value matrix must match the shape of the
212
matrix being plotted.
214
- C{value_format}: a format string or a callable that specifies how
215
the values should be plotted. If it is a callable, it must be a
216
function that expects a single value and returns a string.
217
Example: C{"%#.2f"} for floating-point numbers with always exactly
218
two digits after the decimal point. See the Python documentation of
219
the C{%} operator for details on the format string. If the format
220
string is not given, it defaults to the C{str} function.
222
If only the row names or the column names are given and the matrix
223
is square-shaped, the same names are used for both column and row
227
gw = float(kwds.get("grid_width", 1.))
228
border_width = float(kwds.get("border_width", 1.))
229
style = kwds.get("style", "boolean")
230
row_names = kwds.get("row_names", None)
231
col_names = kwds.get("col_names", row_names)
232
values = kwds.get("values", None)
233
value_format = kwds.get("value_format", str)
236
if style not in ("boolean", "palette"): raise ValueError, "invalid style"
237
if row_names is None and col_names is not None: row_names=col_names
238
if row_names is not None:
239
row_names=list(row_names)[0:self._nrow]
240
if len(row_names) < self._nrow:
241
row_names.extend([""]*(self._nrow-len(row_names)))
242
if col_names is not None:
243
col_names=list(col_names)[0:self._ncol]
244
if len(col_names) < self._ncol:
245
col_names.extend([""]*(self._ncol-len(col_names)))
246
if values == False: values=None
247
if values == True: values=self
248
if isinstance(values, list): values=Matrix(list)
249
if values is not None and not isinstance(values, Matrix):
250
raise TypeError, "values must be None, False, True or a matrix"
251
if values is not None and values.shape != self.shape:
252
raise ValueError, "values must be a matrix of size %s" % str(self.shape)
254
_, _, font_height, _, _ = context.font_extents()
256
# Calculate text extents if needed
257
if row_names is not None or col_names is not None:
258
te = context.text_extents
259
space_width = te(" ")[4]
260
max_row_name_width = max([te(s)[4] for s in row_names])+space_width
261
max_col_name_width = max([te(s)[4] for s in col_names])+space_width
263
max_row_name_width, max_col_name_width = 0, 0
266
total_width = float(bbox.width)-max_row_name_width
267
total_height = float(bbox.height)-max_col_name_width
268
dx = total_width / self.shape[1]
269
dy = total_height / self.shape[0]
270
if kwds.get("square", True):
271
dx, dy = min(dx, dy), min(dx, dy)
272
total_width, total_height = dx*self.shape[1], dy*self.shape[0]
273
ox = bbox.left + (bbox.width - total_width - max_row_name_width) / 2.0
274
oy = bbox.top + (bbox.height - total_height - max_col_name_width) / 2.0
275
ox += max_row_name_width
276
oy += max_col_name_width
278
# Determine rescaling factors for the palette if needed
279
if style == "palette":
280
mi, ma = self.min(), self.max()
282
color_ratio = (len(palette)-1) / float(ma-mi)
284
# Validate grid width
285
if dx < 3*gw or dy < 3*gw: gw = 0.
286
if gw>0: context.set_line_width(gw)
288
# Draw row names (if any)
289
context.set_source_rgb(0., 0., 0.)
290
if row_names is not None:
292
for heading in row_names:
293
_, _, _, h, xa, _ = context.text_extents(heading)
294
context.move_to(x-xa-space_width, y + (dy+h)/2.)
295
context.show_text(heading)
298
# Draw column names (if any)
299
if col_names is not None:
301
context.translate(ox, oy)
302
context.rotate(-1.5707963285) # pi/2
304
for heading in col_names:
305
_, _, _, h, _, _ = context.text_extents(heading)
306
context.move_to(x+space_width, y + (dx+h)/2.)
307
context.show_text(heading)
318
if style == "boolean":
320
context.set_source_rgb(0., 0., 0.)
322
context.set_source_rgb(1., 1., 1.)
323
elif style == "palette":
324
cidx = int((item-color_offset)*color_ratio)
325
if cidx < 0: cidx = 0
326
context.set_source_rgb(*palette.get(cidx))
327
context.rectangle(x, y, dx, dy)
329
context.fill_preserve()
330
context.set_source_rgb(0.5, 0.5, 0.5)
338
if values is not None:
340
context.set_source_rgb(0., 0., 0.)
341
for row in values.data:
342
if hasattr(value_format, "__call__"):
343
values = [value_format(item) for item in row]
345
values = [value_format % item for item in row]
347
th, tw = context.text_extents(item)[3:5]
348
context.move_to(x+(dx-tw)/2., y+(dy+th)/2.)
349
context.show_text(item)
355
context.set_line_width(border_width)
356
context.set_source_rgb(0., 0., 0.)
357
context.rectangle(ox, oy, dx*self.shape[1], dy*self.shape[0])
361
def min(self, dim=None):
362
"""Returns the minimum of the matrix along the given dimension
364
@param dim: the dimension. 0 means determining the column minimums, 1 means
365
determining the row minimums. If C{None}, the global minimum is
368
if dim == 1: return map(min, self._data)
369
if dim == 0: return map(min, [[row[idx] for row in self._data] for idx in xrange(self._ncol)])
370
return min(map(min, self._data))
372
def max(self, dim=None):
373
"""Returns the maximum of the matrix along the given dimension
375
@param dim: the dimension. 0 means determining the column maximums, 1 means
376
determining the row maximums. If C{None}, the global maximum is
379
if dim == 1: return map(max, self._data)
380
if dim == 0: return map(max, [[row[idx] for row in self._data] for idx in xrange(self._ncol)])
381
return max(map(max, self._data))
384
class DyadCensus(tuple):
385
"""Dyad census of a graph.
387
This is a pretty simple class - basically it is a tuple, but it allows
388
the user to refer to its individual items by the names C{mutual} (or
389
C{mut}), C{asymmetric} (or C{asy} or C{asym} or C{asymm}) and C{null}.
393
>>> g=Graph.Erdos_Renyi(100, 0.2, directed=True)
394
>>> dc=g.dyad_census()
399
>>> print tuple(dc), list(dc)
400
(179, 1609, 3162) [179, 1609, 3162]
401
>>> print dc.as_dict()
402
{"mutual": 179, "asymmetric": 1609, "null": 3162}
404
_remap = {"mutual": 0, "mut": 0, "sym": 0, "symm": 0,
405
"asy": 1, "asym": 1, "asymm": 1, "asymmetric": 1, "null": 2}
407
def __getitem__(self, idx):
408
return tuple.__getitem__(self, self._remap.get(idx, idx))
410
def __getattr__(self, attr):
411
if attr in self._remap.keys():
412
return tuple.__getitem__(self, self._remap[attr])
413
raise AttributeError, "no such attribute: %s" % attr
416
return "DyadCensus((%d, %d, %d))" % self
419
return "%d mutual, %d asymmetric, %d null dyads" % self
422
"""Converts the dyad census to a dict using the known dyad names."""
423
return {"mutual": self[0], "asymmetric": self[1], "null": self[2]}
426
class TriadCensus(tuple):
427
"""Triad census of a graph.
429
This is a pretty simple class - basically it is a tuple, but it allows
430
the user to refer to its individual items by the following triad names:
432
- C{003} -- the empty graph
433
- C{012} -- a graph with a single directed edge (C{A --> B, C})
434
- C{102} -- a graph with a single mutual edge (C{A <-> B, C})
435
- C{021D} -- the binary out-tree (C{A <-- B --> C})
436
- C{021U} -- the binary in-tree (C{A --> B <-- C})
437
- C{021C} -- the directed line (C{A --> B --> C})
438
- C{111D} -- C{A <-> B <-- C}
439
- C{111U} -- C{A <-> B --> C}
440
- C{030T} -- C{A --> B <-- C, A --> C}
441
- C{030C} -- C{A <-- B <-- C, A --> C}
442
- C{201} -- C{A <-> B <-> C}
443
- C{120D} -- C{A <-- B --> C, A <-> C}
444
- C{120U} -- C{A --> B <-- C, A <-> C}
445
- C{120C} -- C{A --> B --> C, A <-> C}
446
- C{210C} -- C{A --> B <-> C, A <-> C}
447
- C{300} -- the complete graph (C{A <-> B <-> C, A <-> C})
449
Attribute and item accessors are provided. Due to the syntax of Python,
450
attribute names are not allowed to start with a number, therefore the
451
triad names must be prepended with a lowercase C{t} when accessing
452
them as attributes. This is not necessary with the item accessor syntax.
456
>>> g=Graph.Erdos_Renyi(100, 0.2, directed=True)
457
>>> tc=g.triad_census()
463
_remap = {"003": 0, "012": 1, "102": 2, "021D": 3, "021U": 4, "021C": 5, \
464
"111D": 6, "111U": 7, "030T": 8, "030C": 9, "201": 10, "120D": 11, \
465
"120U": 12, "120C": 13, "210": 14, "300": 15}
467
def __getitem__(self, idx):
468
if isinstance(idx, basestring): idx=idx.upper()
469
return tuple.__getitem__(self, self._remap.get(idx, idx))
471
def __getattr__(self, attr):
472
if isinstance(attr, basestring) and attr[0] == 't' \
473
and attr[1:].upper() in self._remap.keys():
474
return tuple.__getitem__(self, self._remap[attr[1:].upper()])
475
raise AttributeError, "no such attribute: %s" % attr
478
return "TriadCensus((%s))" % ", ".join(map(str, self))
483
numwidth = len(str(maxcount))
484
captionwidth = max(map(len, self._remap.keys()))
487
rowcount = maxidx / colcount
488
if rowcount * colcount < maxidx: rowcount += 1
490
invmap = dict((v,k) for k,v in self._remap.iteritems())
491
result, row, idx = [], [], 0
492
for rowidx in xrange(rowcount):
493
for colidx in xrange(colcount):
494
if idx >= maxidx: break
495
row.append("%-*s: %*d" % (captionwidth, invmap.get(idx, ""),
496
numwidth, self[idx]))
498
result.append(" | ".join(row))
501
return "\n".join(result)
505
"""A cut of a given graph.
507
This is a simple class used to represent cuts returned by
508
L{Graph.mincut}. It has the following attributes:
510
- C{graph} - the graph on which this cut is defined
512
- C{value} - the value (capacity) of the cut
514
- C{partition} - vertex IDs in the parts created
515
after removing edges in the cut
517
- C{cut} - edge IDs in the cut
519
- C{es} - an edge selector restricted to the edges
522
Attributes are not read only, but you should consider them
523
as being read only. Unexpected behaviour may occur if you
526
You can use indexing on this object to obtain L{VertexSeq}
527
objects of the partitions.
529
This class is usually not instantiated directly, everything
530
is taken care of by L{Graph.mincut}.
534
>>> g = Graph.Ring(20)
538
>>> print min(map(len, mc))
540
>>> mc.es["color"] = ["red"] * len(mc.es)
543
__slots__ = ["_graph", "_value", "_partition", "_cut"]
545
def __init__(self, graph, value, cut, partition, partition2):
546
"""Initializes the cut.
548
This should not be called directly, everything is
549
taken care of by L{Graph.mincut}.
552
self._value = float(value)
553
self._partition = (partition, partition2)
557
return "%s(%s,%s,%s,%s,%s)" % \
558
(self.__class__.__name__, repr(self._graph), \
559
self._value, repr(self._cut), \
560
repr(self._partition[0]), repr(self._partition[1]))
563
return "Graph cut (%d edges, %d vs %d vertices, value=%.4f)" % \
564
(len(self._cut), len(self._partition[0]), len(self._partition[1]), \
567
def __len__(self): return 2
569
def __getitem__(self, idx):
570
if idx >= 2: raise IndexError, "a cut has only two partitions"
571
return self.graph.vs.select(self._partition[idx])
573
def _get_es(self): return self._graph.es.select(self.cut)
574
def _get_graph(self): return self._graph
575
def _get_partition(self): return self._partition
576
def _get_cut(self): return self._cut
577
def _get_value(self): return self._value
579
es = property(_get_es, doc="Returns an edge selector restricted to the cut")
580
graph = property(_get_graph, doc="The graph over which this cut is defined")
581
partition = property(_get_partition,
582
doc="Vertex IDs partitioned according to the cut")
583
cut = property(_get_cut, doc="Edge IDs in the cut")
584
value = property(_get_value, doc="Sum of edge capacities in the cut")
587
class UniqueIdGenerator(object):
588
"""A dictionary-like class that can be used to assign unique IDs to
589
names (say, vertex names).
593
>>> gen = UniqueIdGenerator()
600
>>> gen["A"] # Retrieving already existing ID
602
>>> gen.add("D") # Synonym of gen["D"]
604
>>> len(gen) # Number of already used IDs
608
def __init__(self, id_generator=None, initial=None):
609
"""Creates a new unique ID generator. `id_generator` specifies how do we
610
assign new IDs to elements that do not have an ID yet. If it is `None`,
611
elements will be assigned integer identifiers starting from 0. If it is
612
an integer, elements will be assigned identifiers starting from the given
613
integer. If it is an iterator or generator, its `next` method will be
614
called every time a new ID is needed."""
615
if id_generator is None: id_generator = 0
616
if isinstance(id_generator, int):
618
self._generator = itertools.count(id_generator)
620
self._generator = id_generator
623
for value in initial:
626
def __getitem__(self, item):
627
"""Retrieves the ID corresponding to `item`. Generates a new ID for `item`
628
if it is the first time we request an ID for it."""
630
return self._ids[item]
632
self._ids[item] = self._generator.next()
633
return self._ids[item]
636
""""Returns the number of items"""
637
return len(self._ids)
639
def reverse_dict(self):
640
"""Returns the reverse mapping, i.e., the one that maps from generated
641
IDs to their corresponding objects"""
642
return dict((v, k) for k, v in self._ids.iteritems())
645
"""Returns the values stored so far. If the generator generates items
646
according to the standard sorting order, the values returned will be
647
exactly in the order they were added. This holds for integer IDs for
648
instance (but for many other ID generators as well)."""
649
return sorted(self._ids.keys(), key = lambda x: self._ids[x])