Showing
9 changed files
with
330 additions
and
61 deletions
... | @@ -895,6 +895,25 @@ class Substitution (object) : | ... | @@ -895,6 +895,25 @@ class Substitution (object) : |
895 | for var in other : | 895 | for var in other : |
896 | res._dict[var] = self(other(var)) | 896 | res._dict[var] = self(other(var)) |
897 | return res | 897 | return res |
898 | + def restrict (self, domain) : | ||
899 | + """Restrict the substitution to `domain`, ie remove all | ||
900 | + elements that are not in `domain`. Note that `domain` may | ||
901 | + include names that are not in the substitution, they are | ||
902 | + simply ignored. | ||
903 | + | ||
904 | + >>> s = Substitution(a=1, b=2, c=3, d=4).restrict(['a', 'b', 'z']) | ||
905 | + >>> list(sorted(s.domain())) | ||
906 | + ['a', 'b'] | ||
907 | + | ||
908 | + @param domain: the new domain as a set/list/... of names | ||
909 | + @type domain: `iterable` | ||
910 | + @return: the restricted substitution | ||
911 | + @rtype: `Substitution` | ||
912 | + """ | ||
913 | + result = self.copy() | ||
914 | + for name in result.domain() - set(domain) : | ||
915 | + result._dict.pop(name, None) | ||
916 | + return result | ||
898 | 917 | ||
899 | class Symbol (object) : | 918 | class Symbol (object) : |
900 | """A symbol that may be used as a constant | 919 | """A symbol that may be used as a constant | ... | ... |
This diff is collapsed. Click to expand it.
1 | -"""Draw Petri nets using PyGraphViz | 1 | +"""Adds methods to draw `PetriNet` and `StateGraph` instances using |
2 | +GraphViz. | ||
2 | 3 | ||
3 | - * adds a method `draw` to `PetriNet` and `StateGraph` that creates | 4 | +For example, let's first define a Petri net: |
4 | - a drawing of the object in a file. | ||
5 | 5 | ||
6 | >>> import snakes.plugins | 6 | >>> import snakes.plugins |
7 | >>> snakes.plugins.load('gv', 'snakes.nets', 'nets') | 7 | >>> snakes.plugins.load('gv', 'snakes.nets', 'nets') |
... | @@ -16,18 +16,35 @@ | ... | @@ -16,18 +16,35 @@ |
16 | >>> n.add_output('p11', 't10', Expression('x+1')) | 16 | >>> n.add_output('p11', 't10', Expression('x+1')) |
17 | >>> n.add_input('p11', 't01', Variable('y')) | 17 | >>> n.add_input('p11', 't01', Variable('y')) |
18 | >>> n.add_output('p00', 't01', Expression('y-1')) | 18 | >>> n.add_output('p00', 't01', Expression('y-1')) |
19 | + | ||
20 | +Thanks to plugin `gv`, we can draw it using the various engines of | ||
21 | +GraphViz; we can also draw the state graph: | ||
22 | + | ||
19 | >>> for engine in ('neato', 'dot', 'circo', 'twopi', 'fdp') : | 23 | >>> for engine in ('neato', 'dot', 'circo', 'twopi', 'fdp') : |
20 | ... n.draw(',test-gv-%s.png' % engine, engine=engine) | 24 | ... n.draw(',test-gv-%s.png' % engine, engine=engine) |
21 | >>> s = StateGraph(n) | 25 | >>> s = StateGraph(n) |
22 | >>> s.build() | 26 | >>> s.build() |
23 | >>> s.draw(',test-gv-graph.png') | 27 | >>> s.draw(',test-gv-graph.png') |
24 | ->>> for node in sorted(n.node(), key=str) : | 28 | + |
29 | +The plugin also allows to layout the nodes without drawing the net | ||
30 | +(this is only available for `PetriNet`, not for `StateGraph`). We | ||
31 | +first move every node to position `(-100, -100)`; then, we layout the | ||
32 | +net; finally, we check that every node has indeed been moved away from | ||
33 | +where we had put it: | ||
34 | + | ||
35 | +>>> for node in n.node() : | ||
25 | ... node.pos.moveto(-100, -100) | 36 | ... node.pos.moveto(-100, -100) |
37 | +>>> all(node.pos.x == node.pos.y == -100 for node in n.node()) | ||
38 | +True | ||
26 | >>> n.layout() | 39 | >>> n.layout() |
27 | ->>> any(node.pos == (-100, -100) for node in sorted(n.node(), key=str)) | 40 | +>>> any(node.pos.x == node.pos.y == -100 for node in n.node()) |
28 | False | 41 | False |
29 | 42 | ||
30 | -@todo: revise documentation | 43 | +@note: setting nodes position has no influence on how a net is drawn: |
44 | + GraphViz will redo the layout in any case. Method `layout` is here | ||
45 | + just in the case you would need a layout of your nets. | ||
46 | +@note: this plugin depens on plugins `pos` and `clusters` (that are | ||
47 | + automatically loaded) | ||
31 | """ | 48 | """ |
32 | 49 | ||
33 | import os, os.path, subprocess, collections | 50 | import os, os.path, subprocess, collections |
... | @@ -35,6 +52,7 @@ import snakes.plugins | ... | @@ -35,6 +52,7 @@ import snakes.plugins |
35 | from snakes.plugins.clusters import Cluster | 52 | from snakes.plugins.clusters import Cluster |
36 | from snakes.compat import * | 53 | from snakes.compat import * |
37 | 54 | ||
55 | +# apidoc skip | ||
38 | class Graph (Cluster) : | 56 | class Graph (Cluster) : |
39 | def __init__ (self, attr) : | 57 | def __init__ (self, attr) : |
40 | Cluster.__init__(self) | 58 | Cluster.__init__(self) |
... | @@ -150,37 +168,56 @@ class Graph (Cluster) : | ... | @@ -150,37 +168,56 @@ class Graph (Cluster) : |
150 | "snakes.plugins.pos"]) | 168 | "snakes.plugins.pos"]) |
151 | def extend (module) : | 169 | def extend (module) : |
152 | class PetriNet (module.PetriNet) : | 170 | class PetriNet (module.PetriNet) : |
153 | - "An extension with a method `draw`" | 171 | + "An extension with methods `draw` and `layout`" |
154 | - def draw (self, filename=None, engine="dot", debug=False, | 172 | + def draw (self, filename, engine="dot", debug=False, |
155 | graph_attr=None, cluster_attr=None, | 173 | graph_attr=None, cluster_attr=None, |
156 | place_attr=None, trans_attr=None, arc_attr=None) : | 174 | place_attr=None, trans_attr=None, arc_attr=None) : |
157 | - """ | 175 | + """Draw the Petri net to a picture file. How the net is |
158 | - @param filename: the name of the image file to create or | 176 | + rendered can be controlled using the arguments `..._attr`. |
159 | - `None` if only the computed graph is needed | 177 | + For instance, to draw place in red with names in |
160 | - @type filename: `None` or `str` | 178 | + uppercase, and hide True guards, we can proceed as |
161 | - @param engine: the layout engine to use: 'dot' (default), | 179 | + follows: |
162 | - 'neato', 'circo', 'twopi' or 'fdp' | 180 | + |
181 | + >>> import snakes.plugins | ||
182 | + >>> snakes.plugins.load('gv', 'snakes.nets', 'nets') | ||
183 | + <module ...> | ||
184 | + >>> from nets import * | ||
185 | + >>> n = PetriNet('N') | ||
186 | + >>> n.add_place(Place('p')) | ||
187 | + >>> n.add_transition(Transition('t')) | ||
188 | + >>> n.add_input('p', 't', Value(dot)) | ||
189 | + >>> def draw_place (place, attr) : | ||
190 | + ... attr['label'] = place.name.upper() | ||
191 | + ... attr['color'] = '#FF0000' | ||
192 | + >>> def draw_transition (trans, attr) : | ||
193 | + ... if str(trans.guard) == 'True' : | ||
194 | + ... attr['label'] = trans.name | ||
195 | + ... else : | ||
196 | + ... attr['label'] = '%s\\n%s' % (trans.name, trans.guard) | ||
197 | + >>> n.draw(',net-with-colors.png', | ||
198 | + ... place_attr=draw_place, trans_attr=draw_transition) | ||
199 | + | ||
200 | + @param filename: the name of the image file to create | ||
201 | + @type filename: `str` | ||
202 | + @param engine: the layout engine to use: `'dot'` (default), | ||
203 | + `'neato'`, `'circo'`, `'twopi'` or `'fdp'` | ||
163 | @type engine: `str` | 204 | @type engine: `str` |
164 | @param place_attr: a function to format places, it will be | 205 | @param place_attr: a function to format places, it will be |
165 | called with the place and its attributes dict as | 206 | called with the place and its attributes dict as |
166 | parameters | 207 | parameters |
167 | - @type place_attr: `function(Place,dict)->None` | 208 | + @type place_attr: `callable` |
168 | @param trans_attr: a function to format transitions, it | 209 | @param trans_attr: a function to format transitions, it |
169 | will be called with the transition and its attributes | 210 | will be called with the transition and its attributes |
170 | dict as parameters | 211 | dict as parameters |
171 | - @type trans_attr: `function(Transition,dict)->None` | 212 | + @type trans_attr: `callable` |
172 | @param arc_attr: a function to format arcs, it will be | 213 | @param arc_attr: a function to format arcs, it will be |
173 | called with the label and its attributes dict as | 214 | called with the label and its attributes dict as |
174 | parameters | 215 | parameters |
175 | - @type arc_attr: `function(ArcAnnotation,dict)->None` | 216 | + @type arc_attr: `callable` |
176 | @param cluster_attr: a function to format clusters of | 217 | @param cluster_attr: a function to format clusters of |
177 | nodes, it will be called with the cluster and its | 218 | nodes, it will be called with the cluster and its |
178 | attributes dict as parameters | 219 | attributes dict as parameters |
179 | - @type cluster_attr: | 220 | + @type cluster_attr: `callable` |
180 | - `function(snakes.plugins.clusters.Cluster,dict)->None` | ||
181 | - @return: `None` if `filename` is not `None`, the computed | ||
182 | - graph otherwise | ||
183 | - @rtype: `None` or `pygraphviz.AGraph` | ||
184 | """ | 221 | """ |
185 | nodemap = dict((node.name, "node_%s" % num) | 222 | nodemap = dict((node.name, "node_%s" % num) |
186 | for num, node in enumerate(self.node())) | 223 | for num, node in enumerate(self.node())) |
... | @@ -236,17 +273,51 @@ def extend (module) : | ... | @@ -236,17 +273,51 @@ def extend (module) : |
236 | def layout (self, xscale=1.0, yscale=1.0, engine="dot", | 273 | def layout (self, xscale=1.0, yscale=1.0, engine="dot", |
237 | debug=False, graph_attr=None, cluster_attr=None, | 274 | debug=False, graph_attr=None, cluster_attr=None, |
238 | place_attr=None, trans_attr=None, arc_attr=None) : | 275 | place_attr=None, trans_attr=None, arc_attr=None) : |
276 | + """Layout the nodes of the Petri net by calling GraphViz | ||
277 | + and reading back the picture it creates. The effect is to | ||
278 | + change attributes `pos` (see plugin `pos`) for every node | ||
279 | + according to the positions calculated by GraphViz. | ||
280 | + | ||
281 | + @param xscale: how much the image is scaled in the | ||
282 | + horizontal axis after GraphViz has done the layout | ||
283 | + @type xscale: `float` | ||
284 | + @param yscale: how much the image is scaled in the | ||
285 | + vertical axis after GraphViz has done the layout | ||
286 | + @type yscale: `float` | ||
287 | + @param filename: the name of the image file to create | ||
288 | + @type filename: `str` | ||
289 | + @param engine: the layout engine to use: `'dot'` (default), | ||
290 | + `'neato'`, `'circo'`, `'twopi'` or `'fdp'` | ||
291 | + @type engine: `str` | ||
292 | + @param place_attr: a function to format places, it will be | ||
293 | + called with the place and its attributes dict as | ||
294 | + parameters | ||
295 | + @type place_attr: `callable` | ||
296 | + @param trans_attr: a function to format transitions, it | ||
297 | + will be called with the transition and its attributes | ||
298 | + dict as parameters | ||
299 | + @type trans_attr: `callable` | ||
300 | + @param arc_attr: a function to format arcs, it will be | ||
301 | + called with the label and its attributes dict as | ||
302 | + parameters | ||
303 | + @type arc_attr: `callable` | ||
304 | + @param cluster_attr: a function to format clusters of | ||
305 | + nodes, it will be called with the cluster and its | ||
306 | + attributes dict as parameters | ||
307 | + @type cluster_attr: `callable` | ||
308 | + """ | ||
239 | g = self.draw(None, engine, debug, graph_attr, cluster_attr, | 309 | g = self.draw(None, engine, debug, graph_attr, cluster_attr, |
240 | place_attr, trans_attr, arc_attr) | 310 | place_attr, trans_attr, arc_attr) |
241 | node = dict((v, k) for k, v in g.nodemap.items()) | 311 | node = dict((v, k) for k, v in g.nodemap.items()) |
242 | for n, x, y in g.layout(engine, debug) : | 312 | for n, x, y in g.layout(engine, debug) : |
243 | self.node(node[n]).pos.moveto(x*xscale, y*yscale) | 313 | self.node(node[n]).pos.moveto(x*xscale, y*yscale) |
244 | - | ||
245 | class StateGraph (module.StateGraph) : | 314 | class StateGraph (module.StateGraph) : |
246 | "An extension with a method `draw`" | 315 | "An extension with a method `draw`" |
247 | - def draw (self, filename=None, engine="dot", debug=False, | 316 | + def draw (self, filename, engine="dot", debug=False, |
248 | node_attr=None, edge_attr=None, graph_attr=None) : | 317 | node_attr=None, edge_attr=None, graph_attr=None) : |
249 | - """@param filename: the name of the image file to create or | 318 | + """Draw the state graph to a picture file. |
319 | + | ||
320 | + @param filename: the name of the image file to create or | ||
250 | `None` if only the computed graph is needed | 321 | `None` if only the computed graph is needed |
251 | @type filename: `None` or `str` | 322 | @type filename: `None` or `str` |
252 | @param engine: the layout engine to use: 'dot' (default), | 323 | @param engine: the layout engine to use: 'dot' (default), |
... | @@ -255,19 +326,16 @@ def extend (module) : | ... | @@ -255,19 +326,16 @@ def extend (module) : |
255 | @param node_attr: a function to format nodes, it will be | 326 | @param node_attr: a function to format nodes, it will be |
256 | called with the state number, the `StateGraph` object | 327 | called with the state number, the `StateGraph` object |
257 | and attributes dict as parameters | 328 | and attributes dict as parameters |
258 | - @type node_attr: `function(int,StateGraph,dict)->None` | 329 | + @type node_attr: `callable` |
259 | @param edge_attr: a function to format edges, it will be | 330 | @param edge_attr: a function to format edges, it will be |
260 | called with the transition, its mode and attributes | 331 | called with the transition, its mode and attributes |
261 | dict as parameters | 332 | dict as parameters |
262 | @type trans_attr: | 333 | @type trans_attr: |
263 | - `function(Transition,Substitution,dict)->None` | 334 | + `callable` |
264 | @param graph_attr: a function to format grapg, it will be | 335 | @param graph_attr: a function to format grapg, it will be |
265 | called with the state graphe and attributes dict as | 336 | called with the state graphe and attributes dict as |
266 | parameters | 337 | parameters |
267 | - @type graph_attr: `function(StateGraph,dict)->None` | 338 | + @type graph_attr: `callable` |
268 | - @return: `None` if `filename` is not `None`, the computed | ||
269 | - graph otherwise | ||
270 | - @rtype: `None` or `pygraphviz.AGraph` | ||
271 | """ | 339 | """ |
272 | attr = dict(style="invis", | 340 | attr = dict(style="invis", |
273 | splines="true") | 341 | splines="true") | ... | ... |
1 | -"""A plugin to add labels to nodes and nets. | 1 | +"""A plugin to add labels to nodes and nets. Labels are names (valid |
2 | +Python identifiers) associated to arbitrary objects. | ||
2 | 3 | ||
3 | -@todo: revise (actually make) documentation | 4 | +>>> import snakes.plugins |
5 | +>>> snakes.plugins.load('labels', 'snakes.nets', 'nets') | ||
6 | +<module ...> | ||
7 | +>>> from nets import * | ||
8 | +>>> t = Transition('t') | ||
9 | +>>> t.label(foo='bar', spam=42) | ||
10 | +>>> t.label('foo') | ||
11 | +'bar' | ||
12 | +>>> t.label('spam') | ||
13 | +42 | ||
14 | + | ||
15 | +Note that when nodes in a Petri net are merged, their labels are | ||
16 | +merged too, but in an arbitrary order. So, for example: | ||
17 | + | ||
18 | +>>> n = PetriNet('N') | ||
19 | +>>> n.add_place(Place('p1')) | ||
20 | +>>> n.place('p1').label(foo='bar', spam='ham') | ||
21 | +>>> n.add_place(Place('p2')) | ||
22 | +>>> n.place('p2').label(hello='world', spam='egg') | ||
23 | +>>> n.merge_places('p', ['p1', 'p2']) | ||
24 | +>>> n.place('p').label('hello') | ||
25 | +'world' | ||
26 | +>>> n.place('p').label('foo') | ||
27 | +'bar' | ||
28 | +>>> n.place('p').label('spam') in ['ham', 'egg'] | ||
29 | +True | ||
30 | + | ||
31 | +In the latter statement, we cannot know whether the label will be one | ||
32 | +or the other value because merging has been done in an arbitrary | ||
33 | +order. | ||
4 | """ | 34 | """ |
5 | 35 | ||
6 | from snakes.plugins import plugin, new_instance | 36 | from snakes.plugins import plugin, new_instance |
... | @@ -10,6 +40,22 @@ from snakes.pnml import Tree | ... | @@ -10,6 +40,22 @@ from snakes.pnml import Tree |
10 | def extend (module) : | 40 | def extend (module) : |
11 | class Transition (module.Transition) : | 41 | class Transition (module.Transition) : |
12 | def label (self, *get, **set) : | 42 | def label (self, *get, **set) : |
43 | + """Get and set labels for the transition. The labels given | ||
44 | + in `get` will be returned as a `tuple` and the labels | ||
45 | + assigned in `set` will be changed accordingly. If a label | ||
46 | + is given both in `get`and `set`, the returned value is | ||
47 | + that it had at the beginning of the call, ie, before it is | ||
48 | + set by the call. | ||
49 | + | ||
50 | + @param get: labels which values have to be returned | ||
51 | + @type get: `str` | ||
52 | + @param set: labels which values have to be changed | ||
53 | + @type set: `object` | ||
54 | + @return: the tuples of values corresponding to `get` | ||
55 | + @rtype: `tuple` | ||
56 | + @raise KeyError: when a label given in `get` has not been | ||
57 | + assigned previouly | ||
58 | + """ | ||
13 | if not hasattr(self, "_labels") : | 59 | if not hasattr(self, "_labels") : |
14 | self._labels = {} | 60 | self._labels = {} |
15 | result = tuple(self._labels[g] for g in get) | 61 | result = tuple(self._labels[g] for g in get) |
... | @@ -21,10 +67,22 @@ def extend (module) : | ... | @@ -21,10 +67,22 @@ def extend (module) : |
21 | elif len(set) == 0 : | 67 | elif len(set) == 0 : |
22 | return self._labels.copy() | 68 | return self._labels.copy() |
23 | def has_label (self, name, *names) : | 69 | def has_label (self, name, *names) : |
70 | + """Check is a label has been assigned to the transition. | ||
71 | + | ||
72 | + @param name: the label to check | ||
73 | + @type name: `str` | ||
74 | + @param names: additional labels to check, if used, the | ||
75 | + return value is a `tuple` of `bool` instead of a | ||
76 | + single `bool` | ||
77 | + @return: a Boolean indicating of the checked labels are | ||
78 | + present or not in the transitions | ||
79 | + @rtype: `bool` | ||
80 | + """ | ||
24 | if len(names) == 0 : | 81 | if len(names) == 0 : |
25 | return name in self._labels | 82 | return name in self._labels |
26 | else : | 83 | else : |
27 | return tuple(n in self._labels for n in (name,) + names) | 84 | return tuple(n in self._labels for n in (name,) + names) |
85 | + # apidoc stop | ||
28 | def copy (self, name=None, **options) : | 86 | def copy (self, name=None, **options) : |
29 | if not hasattr(self, "_labels") : | 87 | if not hasattr(self, "_labels") : |
30 | self._labels = {} | 88 | self._labels = {} |
... | @@ -79,6 +137,7 @@ def extend (module) : | ... | @@ -79,6 +137,7 @@ def extend (module) : |
79 | return t | 137 | return t |
80 | class Place (module.Place) : | 138 | class Place (module.Place) : |
81 | def label (self, *get, **set) : | 139 | def label (self, *get, **set) : |
140 | + "See documentation for `Transition.label` above" | ||
82 | if not hasattr(self, "_labels") : | 141 | if not hasattr(self, "_labels") : |
83 | self._labels = {} | 142 | self._labels = {} |
84 | result = tuple(self._labels[g] for g in get) | 143 | result = tuple(self._labels[g] for g in get) |
... | @@ -90,10 +149,12 @@ def extend (module) : | ... | @@ -90,10 +149,12 @@ def extend (module) : |
90 | elif len(set) == 0 : | 149 | elif len(set) == 0 : |
91 | return self._labels.copy() | 150 | return self._labels.copy() |
92 | def has_label (self, name, *names) : | 151 | def has_label (self, name, *names) : |
152 | + "See documentation for `Transition.has_label` above" | ||
93 | if len(names) == 0 : | 153 | if len(names) == 0 : |
94 | return name in self._labels | 154 | return name in self._labels |
95 | else : | 155 | else : |
96 | return tuple(n in self._labels for n in (name,) + names) | 156 | return tuple(n in self._labels for n in (name,) + names) |
157 | + # apidoc stop | ||
97 | def copy (self, name=None, **options) : | 158 | def copy (self, name=None, **options) : |
98 | if not hasattr(self, "_labels") : | 159 | if not hasattr(self, "_labels") : |
99 | self._labels = {} | 160 | self._labels = {} |
... | @@ -152,6 +213,7 @@ def extend (module) : | ... | @@ -152,6 +213,7 @@ def extend (module) : |
152 | return p | 213 | return p |
153 | class PetriNet (module.PetriNet) : | 214 | class PetriNet (module.PetriNet) : |
154 | def label (self, *get, **set) : | 215 | def label (self, *get, **set) : |
216 | + "See documentation for `Transition.label` above" | ||
155 | if not hasattr(self, "_labels") : | 217 | if not hasattr(self, "_labels") : |
156 | self._labels = {} | 218 | self._labels = {} |
157 | result = tuple(self._labels[g] for g in get) | 219 | result = tuple(self._labels[g] for g in get) |
... | @@ -163,10 +225,12 @@ def extend (module) : | ... | @@ -163,10 +225,12 @@ def extend (module) : |
163 | elif len(set) == 0 : | 225 | elif len(set) == 0 : |
164 | return self._labels.copy() | 226 | return self._labels.copy() |
165 | def has_label (self, name, *names) : | 227 | def has_label (self, name, *names) : |
228 | + "See documentation for `Transition.has_label` above" | ||
166 | if len(names) == 0 : | 229 | if len(names) == 0 : |
167 | return name in self._labels | 230 | return name in self._labels |
168 | else : | 231 | else : |
169 | return tuple(n in self._labels for n in (name,) + names) | 232 | return tuple(n in self._labels for n in (name,) + names) |
233 | + # apidoc stop | ||
170 | def copy (self, name=None, **options) : | 234 | def copy (self, name=None, **options) : |
171 | if not hasattr(self, "_labels") : | 235 | if not hasattr(self, "_labels") : |
172 | self._labels = {} | 236 | self._labels = {} | ... | ... |
1 | -"""A plugin to compose nets. | 1 | +# -*- encoding: latin-1 |
2 | +"""A plugin to compose nets _à la_ Petri Box Calculus. | ||
2 | 3 | ||
3 | -The compositions are based on place status and automatically merge | 4 | +@note: this plugin depends on plugins `clusters` and `status` that are |
4 | -some nodes (buffers and variables, tick transitions). | 5 | + automatically loaded |
6 | +""" | ||
7 | + | ||
8 | +""" | ||
9 | +## Control-flow and buffers places ## | ||
10 | + | ||
11 | +When this module is used, places are equipped with statuses (see also | ||
12 | +plugin `status` that provides this service). We distinguish in | ||
13 | +particular: | ||
14 | + | ||
15 | + * _entry_ places marked at the initial state of the net | ||
16 | + * _exit_ places marked at a final state of the net | ||
17 | + * _internal_ places marked during the execution | ||
18 | + * all together, these places form the _control-flow places_ and can | ||
19 | + be marked only by black tokens (ie, they are typed `tBlackToken` | ||
20 | + and thus can hold only `dot` values) | ||
21 | + * _buffer_ places that may hold data of any type, each buffer place | ||
22 | + is given a name that is the name of the buffer modelled by the | ||
23 | + place | ||
24 | + | ||
25 | +Plugin `ops` exports these statuses as Python objects:[^1] `entry`, | ||
26 | +`internal` and `exit` are instances of class `Status` while `buffer` | ||
27 | +is a function that returns an instance of `Buffer` (a subclass of | ||
28 | +`Status`) when called with a name as its parameter. | ||
29 | + | ||
30 | +[^1]: All these objects are actually defined in plugin `status` | ||
31 | + | ||
32 | +Let's define a net with one entry place, one exit place and two buffer | ||
33 | +places. Note how we use keyword argument `status` to the place | ||
34 | +constructor to define the status of the created place: | ||
5 | 35 | ||
6 | >>> import snakes.plugins | 36 | >>> import snakes.plugins |
7 | >>> snakes.plugins.load('ops', 'snakes.nets', 'nets') | 37 | >>> snakes.plugins.load('ops', 'snakes.nets', 'nets') |
8 | <module ...> | 38 | <module ...> |
9 | >>> from nets import * | 39 | >>> from nets import * |
10 | ->>> from snakes.plugins.status import entry, internal, exit, buffer | ||
11 | >>> basic = PetriNet('basic') | 40 | >>> basic = PetriNet('basic') |
12 | >>> basic.add_place(Place('e', status=entry)) | 41 | >>> basic.add_place(Place('e', status=entry)) |
13 | >>> basic.add_place(Place('x', status=exit)) | 42 | >>> basic.add_place(Place('x', status=exit)) |
14 | >>> basic.add_transition(Transition('t')) | 43 | >>> basic.add_transition(Transition('t')) |
15 | ->>> basic.add_input('e', 't', Value(1)) | 44 | +>>> basic.add_input('e', 't', Value(dot)) |
16 | ->>> basic.add_output('x', 't', Value(2)) | 45 | +>>> basic.add_output('x', 't', Value(dot)) |
17 | ->>> basic.add_place(Place('b', [1], status=buffer('buf'))) | 46 | +>>> basic.add_place(Place('b1', [1], status=buffer('egg'))) |
47 | +>>> basic.add_place(Place('b2', [2], status=buffer('spam'))) | ||
48 | +>>> basic.add_input('b1', 't', Variable('x')) | ||
49 | +>>> basic.add_output('b2', 't', Expression('x+1')) | ||
50 | + | ||
51 | +The simplest operation is to change buffer names, let's do it on a | ||
52 | +copy of our net. This operation is called `hide` because it is | ||
53 | +basically used to hide a buffer: | ||
18 | 54 | ||
19 | >>> n = basic.copy() | 55 | >>> n = basic.copy() |
20 | ->>> n.hide(entry) | 56 | +>>> n.node('b1').status |
21 | ->>> n.node('e').status | 57 | +Buffer('buffer','egg') |
58 | +>>> n.hide(buffer('egg')) | ||
59 | +>>> n.node('b1').status | ||
22 | Status(None) | 60 | Status(None) |
23 | ->>> n.hide(buffer('buf'), buffer(None)) | ||
24 | ->>> n.node('b').status | ||
25 | -Buffer('buffer') | ||
26 | 61 | ||
27 | ->>> n = basic / 'buf' | 62 | +As we can see, place `b1` now has a dummy status. But `hide` can |
28 | ->>> n.node('[b/buf]').status | 63 | +accept a second argument and allows to rename a buffer: |
64 | + | ||
65 | +>>> n.node('b2').status | ||
66 | +Buffer('buffer','spam') | ||
67 | +>>> n.hide(buffer('spam'), buffer('ham')) | ||
68 | +>>> n.node('b2').status | ||
69 | +Buffer('buffer','ham') | ||
70 | + | ||
71 | +A slighlty different way for hiding buffers is to use operator `/`, | ||
72 | +which actually constructs a new net, changing the names of every node | ||
73 | +in the original net to `'[.../egg]'`: | ||
74 | + | ||
75 | +>>> n = basic / 'egg' | ||
76 | +>>> n.node('[b1/egg]').status | ||
29 | Buffer('buffer') | 77 | Buffer('buffer') |
30 | 78 | ||
79 | +As we can see, a buffer hidden using `/` still has a buffer status but | ||
80 | +with no name associated. Such an anonymous buffer is treated in a | ||
81 | +special way as we'll see later on. | ||
82 | + | ||
83 | +The systematic renaming of nodes is something we should get used to | ||
84 | +before to continue. When one or two nets are constructed through an | ||
85 | +operation, the nodes of the operand nets are combined in one way or | ||
86 | +another. For an arbitrary operation `A % B`, the resulting net will be | ||
87 | +called `'[A.name%B.name]'` (if `A` and `B` are nets). Whenever a node | ||
88 | +`a` from `A` is combined with a node `b` from `B`, the resulting node | ||
89 | +will be called `'[a%b]'`. If only one node is copied in the resulting | ||
90 | +net, it will be called `'[a%]'` or `'[%b]'` depending on wher it comes | ||
91 | +from. This systematic renaming allows to ensure that even if `A` and | ||
92 | +`B` have nodes with the same names, there will be no name clash in the | ||
93 | +result, while, at the same time allowing users to predict the new name | ||
94 | +of a node. | ||
95 | + | ||
96 | +## Control-flow compositions ## | ||
97 | + | ||
98 | +Using control-flow places, it becomes possible to build nets by | ||
99 | +composing smaller nets through control flow operations. Let's start | ||
100 | +with the _sequential composition_ `A & B`, basically, its combines the | ||
101 | +exit places of net `A` with the entry places of net `B`, ensuring thus | ||
102 | +that the resulting net behaves like if we execute `A` followed by `B`. | ||
103 | +In the example below, we use method `status` of a net to get the | ||
104 | +places with a given status: | ||
105 | + | ||
31 | >>> n = basic & basic | 106 | >>> n = basic & basic |
32 | >>> n.status(internal) | 107 | >>> n.status(internal) |
33 | ('[x&e]',) | 108 | ('[x&e]',) |
34 | >>> n.place('[x&e]').pre | 109 | >>> n.place('[x&e]').pre |
35 | -{'[t&]': Value(2)} | 110 | +{'[t&]': Value(dot)} |
36 | >>> n.place('[x&e]').post | 111 | >>> n.place('[x&e]').post |
37 | -{'[&t]': Value(1)} | 112 | +{'[&t]': Value(dot)} |
38 | ->>> n.status(buffer('buf')) | 113 | +>>> n.status(buffer('egg')) |
39 | -('[b&b]',) | 114 | +('[b1&b1]',) |
115 | +>>> n.status(buffer('spam')) | ||
116 | +('[b2&b2]',) | ||
117 | + | ||
118 | +We can see that `n` now has one internal place that is the combination | ||
119 | +of the exit place of the left copy of `basic` with the entry place of | ||
120 | +the right copy of `basic`. (Hence its name: `'[x&e]'`.) We can see | ||
121 | +also how it is connected to the left/right copy of `t` as its | ||
122 | +input/output. Last, we can see that buffer places from the two copies | ||
123 | +of `basic` have been merged when they have had the same name. This | ||
124 | +merging won't occur for anonymous buffers. For example, hiding | ||
125 | +`'spam'` in the right net of the sequential composition will result in | ||
126 | +two buffer places, one still named `'spam'` that is the copy of `'b2'` | ||
127 | +from the left operand of `&`, another that is anonymous and is the | ||
128 | +copy of `'[b2/spam]'` (ie, `'b2'`after its status was hidden) from the | ||
129 | +right operand of `&`. | ||
130 | + | ||
131 | +>>> n = basic & (basic / 'spam') | ||
132 | +>>> n.status(buffer('spam')) | ||
133 | +('[b2&]',) | ||
134 | +>>> n.status(buffer(None)) | ||
135 | +('[&[b2/spam]]',) | ||
136 | + | ||
137 | +The next operation is the _choice `A + B` that behave either as `A` or | ||
138 | +as `B`. This is obtained by comining the entry places of both nets one | ||
139 | +the one hand, and by combining their exit places on the other hand. | ||
40 | 140 | ||
41 | >>> n = basic + basic | 141 | >>> n = basic + basic |
42 | >>> n.status(entry) | 142 | >>> n.status(entry) |
43 | ('[e+e]',) | 143 | ('[e+e]',) |
44 | >>> list(sorted(n.place('[e+e]').post.items())) | 144 | >>> list(sorted(n.place('[e+e]').post.items())) |
45 | -[('[+t]', Value(1)), ('[t+]', Value(1))] | 145 | +[('[+t]', Value(dot)), ('[t+]', Value(dot))] |
46 | >>> n.status(exit) | 146 | >>> n.status(exit) |
47 | ('[x+x]',) | 147 | ('[x+x]',) |
48 | >>> list(sorted(n.place('[x+x]').pre.items())) | 148 | >>> list(sorted(n.place('[x+x]').pre.items())) |
49 | -[('[+t]', Value(2)), ('[t+]', Value(2))] | 149 | +[('[+t]', Value(dot)), ('[t+]', Value(dot))] |
150 | + | ||
151 | +Another operation is the _iteration_ `A * B` that behaves by executing | ||
152 | +`A` repeatedly (including no repetition) followed by one execution of | ||
153 | +`B`. This is obtained by combining the entry and exit places of `A` | ||
154 | +with the entry place of `B`. | ||
50 | 155 | ||
51 | >>> n = basic * basic | 156 | >>> n = basic * basic |
52 | >>> n.status(entry) | 157 | >>> n.status(entry) |
53 | ('[e,x*e]',) | 158 | ('[e,x*e]',) |
54 | >>> n.place('[e,x*e]').post | 159 | >>> n.place('[e,x*e]').post |
55 | -{'[t*]': Value(1), '[*t]': Value(1)} | 160 | +{'[t*]': Value(dot), '[*t]': Value(dot)} |
56 | >>> n.place('[e,x*e]').pre | 161 | >>> n.place('[e,x*e]').pre |
57 | -{'[t*]': Value(2)} | 162 | +{'[t*]': Value(dot)} |
58 | 163 | ||
164 | +Finally, there is the _parallel composition_ `A | B` that just | ||
165 | +executes both nets in parallel. But because of the merging of buffer | ||
166 | +places, they are able to communicate. | ||
167 | + | ||
168 | +>>> n = basic | basic | ||
169 | +>>> n.status(buffer('egg')) | ||
170 | +('[b1|b1]',) | ||
171 | +>>> n.status(buffer('spam')) | ||
172 | +('[b2|b2]',) | ||
173 | + | ||
174 | +>>> pass | ||
59 | >>> n1 = basic.copy() | 175 | >>> n1 = basic.copy() |
60 | >>> n1.declare('global x; x=1') | 176 | >>> n1.declare('global x; x=1') |
61 | >>> n2 = basic.copy() | 177 | >>> n2 = basic.copy() |
... | @@ -65,8 +181,6 @@ Buffer('buffer') | ... | @@ -65,8 +181,6 @@ Buffer('buffer') |
65 | (1, 2) | 181 | (1, 2) |
66 | >>> n._declare | 182 | >>> n._declare |
67 | ['global x; x=1'] | 183 | ['global x; x=1'] |
68 | - | ||
69 | -@todo: revise documentation | ||
70 | """ | 184 | """ |
71 | 185 | ||
72 | import snakes.plugins | 186 | import snakes.plugins |
... | @@ -113,13 +227,14 @@ def _glue (op, one, two) : | ... | @@ -113,13 +227,14 @@ def _glue (op, one, two) : |
113 | depends=["snakes.plugins.clusters", | 227 | depends=["snakes.plugins.clusters", |
114 | "snakes.plugins.status"]) | 228 | "snakes.plugins.status"]) |
115 | def extend (module) : | 229 | def extend (module) : |
116 | - "Build the extended module" | 230 | + """Essentially, class `PetriNet` is extended to support the binary |
231 | + operations discussed above.""" | ||
117 | class PetriNet (module.PetriNet) : | 232 | class PetriNet (module.PetriNet) : |
118 | def __or__ (self, other) : | 233 | def __or__ (self, other) : |
119 | - "Parallel" | 234 | + "Parallel composition" |
120 | return _glue("|", self, other) | 235 | return _glue("|", self, other) |
121 | def __and__ (self, other) : | 236 | def __and__ (self, other) : |
122 | - "Sequence" | 237 | + "Sequential composition" |
123 | result = _glue("&", self, other) | 238 | result = _glue("&", self, other) |
124 | remove = set() | 239 | remove = set() |
125 | for x, e in cross((self.status(exit), other.status(entry))) : | 240 | for x, e in cross((self.status(exit), other.status(entry))) : |
... | @@ -161,11 +276,13 @@ def extend (module) : | ... | @@ -161,11 +276,13 @@ def extend (module) : |
161 | result.remove_place(p) | 276 | result.remove_place(p) |
162 | return result | 277 | return result |
163 | def hide (self, old, new=None) : | 278 | def hide (self, old, new=None) : |
279 | + "Status hiding and renaming" | ||
164 | if new is None : | 280 | if new is None : |
165 | new = Status(None) | 281 | new = Status(None) |
166 | for node in self.status(old) : | 282 | for node in self.status(old) : |
167 | self.set_status(node, new) | 283 | self.set_status(node, new) |
168 | def __div__ (self, name) : | 284 | def __div__ (self, name) : |
285 | + "Buffer hiding" | ||
169 | result = self.copy() | 286 | result = self.copy() |
170 | for node in result.node() : | 287 | for node in result.node() : |
171 | result.rename_node(node.name, "[%s/%s]" % (node, name)) | 288 | result.rename_node(node.name, "[%s/%s]" % (node, name)) |
... | @@ -173,6 +290,7 @@ def extend (module) : | ... | @@ -173,6 +290,7 @@ def extend (module) : |
173 | if status._value == name : | 290 | if status._value == name : |
174 | result.hide(status, status.__class__(status._name, None)) | 291 | result.hide(status, status.__class__(status._name, None)) |
175 | return result | 292 | return result |
293 | + # apidoc skip | ||
176 | def __truediv__ (self, other) : | 294 | def __truediv__ (self, other) : |
177 | return self.__div__(other) | 295 | return self.__div__(other) |
178 | return PetriNet | 296 | return PetriNet | ... | ... |
This diff is collapsed. Click to expand it.
This diff is collapsed. Click to expand it.
This diff is collapsed. Click to expand it.
... | @@ -382,7 +382,7 @@ class DocExtract (object) : | ... | @@ -382,7 +382,7 @@ class DocExtract (object) : |
382 | % (info["type"].get(arg, "object").strip("`"), | 382 | % (info["type"].get(arg, "object").strip("`"), |
383 | arg)) | 383 | arg)) |
384 | for kw, text in sorted(info["keyword"].items()) : | 384 | for kw, text in sorted(info["keyword"].items()) : |
385 | - self.writelist("`%s`: %s" % (kw, text)) | 385 | + self.writelist("keyword `%s`: %s" % (kw, text)) |
386 | if any(k in info for k in ("return", "rtype")) : | 386 | if any(k in info for k in ("return", "rtype")) : |
387 | if "return" in info : | 387 | if "return" in info : |
388 | self.writelist("`return %s`: %s" | 388 | self.writelist("`return %s`: %s" | ... | ... |
-
Please register or login to post a comment