y_petri 2.0.3 → 2.0.7

Sign up to get free protection for your applications and to get access to all the features.
@@ -0,0 +1,51 @@
1
+ # -*- coding: utf-8 -*-
2
+
3
+ # Connectivity aspect of a transition.
4
+ #
5
+ class YPetri::Transition
6
+ # Names of upstream places.
7
+ #
8
+ def domain_pp; domain.map { |p| p.name || p.object_id } end
9
+ alias :upstream_pp :domain_pp
10
+
11
+ # Names of downstream places.
12
+ #
13
+ def codomain_pp; codomain.map { |p| p.name || p.object_id } end
14
+ alias :downstream_pp :codomain_pp
15
+
16
+ # Union of action arcs and test arcs.
17
+ #
18
+ def arcs; domain | codomain end
19
+
20
+ # Returns names of the (places connected to) the transition's arcs.
21
+ #
22
+ def aa; arcs.map { |p| p.name || p.object_id } end
23
+
24
+ # Marking of the domain places.
25
+ #
26
+ def domain_marking; domain.map &:marking end
27
+
28
+ # Marking of the codomain places.
29
+ #
30
+ def codomain_marking; codomain.map &:marking end
31
+
32
+ # Recursive firing of the upstream net portion (honors #cocked?).
33
+ #
34
+ def fire_upstream_recursively
35
+ return false unless cocked?
36
+ uncock
37
+ upstream_places.each &:fire_upstream_recursively
38
+ fire!
39
+ return true
40
+ end
41
+
42
+ # Recursive firing of the downstream net portion (honors #cocked?).
43
+ #
44
+ def fire_downstream_recursively
45
+ return false unless cocked?
46
+ uncock
47
+ fire!
48
+ downstream_places.each &:fire_downstream_recursively
49
+ return true
50
+ end
51
+ end # class YPetri::Transition
@@ -0,0 +1,32 @@
1
+ # -*- coding: utf-8 -*-
2
+
3
+ # Cocking mechanics of a transition. A transition has to be cocked, before
4
+ # it can succesfuly +#fire+. (+#fire!+ method disregards cocking.)
5
+ #
6
+ class YPetri::Transition
7
+ # Is the transition cocked?
8
+ #
9
+ def cocked?
10
+ @cocked
11
+ end
12
+
13
+ # Negation of +#cocked?+ method.
14
+ #
15
+ def uncocked?
16
+ not cocked?
17
+ end
18
+
19
+ # Cocks teh transition -- allows +#fire+ to succeed.
20
+ #
21
+ def cock
22
+ @cocked = true
23
+ end
24
+ alias :cock! :cock
25
+
26
+ # Sets the transition state to uncocked.
27
+ #
28
+ def uncock
29
+ @cocked = false
30
+ end
31
+ alias :uncock! :uncock
32
+ end # class YPetri::Transition
@@ -0,0 +1,378 @@
1
+ # -*- coding: utf-8 -*-
2
+
3
+ # Constructor syntax aspect of a transition. Large part of the functionality
4
+ # of the Transition class is the convenient constructor syntax.
5
+ #
6
+ class YPetri::Transition
7
+ # Transition class represents many different kinds of Petri net transitions.
8
+ # It makes the constructor syntax a bit more polymorphic. The type of the
9
+ # transition to construct is mostly inferred from the constructor arguments.
10
+ #
11
+ # Mandatorily, the constructor will always need a way to determine the domain
12
+ # (upstream arcs) and codomain (downstream arcs) of the transition. Also, the
13
+ # constructor must have a way to determine the transition's action. This is
14
+ # best explained by examples -- let us have 3 places A, B, C, for whe we will
15
+ # create different kinds of transitions:
16
+ #
17
+ # ==== ts transitions (timeless nonstoichiometric)
18
+ # Action closure is expected with return arity equal to the codomain size:
19
+ #
20
+ # Transition.new upstream_arcs: [A, C], downstream_arcs: [A, B],
21
+ # action_closure: proc { |m, x|
22
+ # if x > 0 then [-(m / 2), (m / 2)]
23
+ # else [1, 0] end
24
+ # }
25
+ #
26
+ # (If C is positive, half of A's marking is moved to B, otherwise A is
27
+ # incremented by 1.)
28
+ #
29
+ # ==== tS transitions (timeless stoichiometric)
30
+ # Stochiometry has to be supplied, action closure is optional. If supplied,
31
+ # its return arity should be 1 (to be multiplied by the stochiometry vector).
32
+ #
33
+ # If no action closure is given, a _functionless_ transition will be
34
+ # constructed, with action closure == 1 * stoichiometry vector.
35
+ #
36
+ # ==== Tsr transitions (timed rateless nonstoichiometric)
37
+ # Action closure has to be supplied, whose first argument is Δt, and the
38
+ # remaining ones correspond to the domain size. Return arity of this closure
39
+ # should, in turn, correspond to the codomain size.
40
+ #
41
+ # ==== TSr transitions (timed rateless stoichiometric)
42
+ # Action closure has to be supplied, whose first argument is Δt, and the
43
+ # remaining ones correspond to the domain size. Return arity of this closure
44
+ # should be 1 (to be multiplied by the stoichiometry vector).
45
+ #
46
+ # ==== sR transitions (nonstoichiometric transitions with rate)
47
+ # Rate closure has to be supplied, whose arity should correspond to the domain
48
+ # size (Δt argument is not needed). Return arity of this should, in turn,
49
+ # correspond to the codomain size -- it represents this transition's
50
+ # contribution to the rate of change of marking of the codomain places.
51
+ #
52
+ # ==== SR transitions (stoichiometric transitions with rate)
53
+ #
54
+ # Rate closure and stoichiometry has to be supplied. Rate closure arity should
55
+ # correspond to the domain size. Return arity should be 1 (to be multiplied by
56
+ # the stoichiometry vector, as in all other stoichiometric transitions).
57
+ #
58
+ # Transition.new stoichiometry: { A: -1, B: 1 },
59
+ # rate: λ { |a| a * 0.5 }
60
+ #
61
+ def initialize *args
62
+ check_in_arguments *args # the big work of checking in args
63
+ inform_upstream_places # that they have been connected
64
+ inform_downstream_places # that they have been connected
65
+ uncock # transitions initialize uncocked
66
+ end
67
+
68
+ private
69
+
70
+ # Checking in the arguments supplied to #initialize looks like a big job.
71
+ # I won't contest to that, but let us not, that it is basically nothing
72
+ # else then defining the duck type of the input argument collection.
73
+ # TypeError is therefore raised if invalid collection has been supplied.
74
+ #
75
+ def check_in_arguments *aa, **oo, &block
76
+ oo.may_have :stoichiometry, syn!: [ :stoichio, :s ]
77
+ oo.may_have :codomain, syn!: [ :codomain_arcs, :codomain_places,
78
+ :downstream,
79
+ :downstream_arcs, :downstream_places,
80
+ :action_arcs ]
81
+ oo.may_have :domain, syn!: [ :domain_arcs, :domain_places,
82
+ :upstream, :upstream_arcs, :upstream_places ]
83
+ oo.may_have :rate, syn!: [ :rate_closure, :propensity,
84
+ :propensity_closure ]
85
+ oo.may_have :action, syn!: :action_closure
86
+ oo.may_have :timed
87
+ oo.may_have :domain_guard
88
+ oo.may_have :codomain_guard
89
+
90
+ @has_rate = oo.has? :rate # was the rate was given?
91
+
92
+ # is the transition stoichiometric (S) or nonstoichiometric (s)?
93
+ @stoichiometric = oo.has? :stoichiometry
94
+
95
+ # downstream description arguments: codomain, stoichiometry (if S)
96
+ if stoichiometric? then
97
+ @codomain, @stoichiometry = check_in_downstream_description_for_S( oo )
98
+ else # s transitions have no stoichiometry
99
+ @codomain = check_in_downstream_description_for_s( oo )
100
+ end
101
+
102
+ # check in domain first, :missing symbol may appear
103
+ @domain = check_in_domain( oo )
104
+
105
+ # upstream description arguments; also takes care of :missing domain
106
+ if has_rate? then
107
+ @domain, @rate_closure, @timed, @functional =
108
+ check_in_upstream_description_for_R( oo, &block )
109
+ else
110
+ @domain, @action_closure, @timed, @functional =
111
+ check_in_upstream_description_for_r( oo, &block )
112
+ end
113
+
114
+ # optional assignment action:
115
+ @assignment_action = check_in_assignment_action( oo )
116
+
117
+ # optional type guards for domain / codomain:
118
+ @domain_guard, @codomain_guard = check_in_guards( oo )
119
+ end # def check_in_arguments
120
+
121
+ # Validates that the supplied collection consists only of places of
122
+ # correct type. Second optional argument customizes the error message.
123
+ #
124
+ def sanitize_place_collection place_collection, what_is_collection=nil
125
+ c = what_is_collection ? what_is_collection.capitalize : "Collection"
126
+ Array( place_collection ).map do |pl_id|
127
+ begin
128
+ place( pl_id )
129
+ rescue NameError
130
+ raise TypeError, "#{c} member #{pl_id} does not specify a valid place!"
131
+ end
132
+ end.aT what_is_collection, "not contain duplicate places" do |coll|
133
+ coll == coll.uniq
134
+ end
135
+ end
136
+
137
+ # Private method, part of #initialize argument checking-in.
138
+ #
139
+ def check_in_domain( oo )
140
+ if oo.has? :domain then
141
+ sanitize_place_collection( oo[:domain], "supplied domain" )
142
+ else
143
+ if stoichiometric? then
144
+ # take arcs with non-positive stoichiometry coefficients
145
+ Hash[ [ @codomain, @stoichiometry ].transpose ]
146
+ .delete_if{ |_place, coeff| coeff > 0 }.keys
147
+ else
148
+ :missing
149
+ # Barring the caller's error, missing domain can mean:
150
+ # 1. empty domain
151
+ # 2. domain == codomain
152
+ # This will be figured later by rate/action closure arity
153
+ end
154
+ end
155
+ end
156
+
157
+ # Private method, part of the init process when :rate is given. Also takes
158
+ # care for missing domain (@domain == :missing).
159
+ #
160
+ def check_in_upstream_description_for_R( oo, &block )
161
+ _domain = domain # this method may modify domain
162
+ fail ArgumentError, "Rate/propensity and action may not be both given!" if
163
+ oo.has? :action # check against colliding :action named argument
164
+ fail ArgumentError, "If block is given, rate must not be given!" if block
165
+ # Let's figure the rate closure now. (Block is never used.)
166
+ rate_λ = case ra = oo[:rate]
167
+ when Proc then # We received the closure directly,
168
+ ra.tap do |λ| # but we've to be concerned about missing domain.
169
+ if domain == :missing then # we've to figure user's intent
170
+ _domain = if λ.arity == 0 then [] # user meant empty domain
171
+ else codomain end # user meant domain == codomain
172
+ else # domain not missing
173
+ fail TypeError, "Rate closure arity (#{λ.arity}) > " +
174
+ "domain (#{domain.size})!" if λ.arity.abs > domain.size
175
+ end
176
+ end
177
+ else # We received something else, must guess user's intent.
178
+ if stoichiometric? then # user's intent was mass action
179
+ fail TypeError, "When a number is supplied as rate, " +
180
+ "domain must not be given!" if oo.has? :domain
181
+ construct_standard_mass_action( ra )
182
+ else # user's intent was constant closure
183
+ fail TypeError, "When rate is a number and stoichiometry " +
184
+ "is not given, codomain size must be 1!" unless
185
+ codomain.size == 1
186
+ # Missing domain is OK here,
187
+ _domain = [] if domain == :missing
188
+ # but if it was supplied explicitly, it must be empty.
189
+ fail TypeError, "Rate is a number, but non-empty domain " +
190
+ "was supplied!" unless domain.empty? if oo.has?( :domain )
191
+ -> { ra }
192
+ end
193
+ end
194
+ # R transitions are implicitly timed
195
+ _timed = true
196
+ # check against colliding :timed argument
197
+ oo[:timed].tE :timed, "not be false if rate given" if oo.has? :timed
198
+ # R transitions are implicitly functional
199
+ _functional = true
200
+ return _domain, rate_λ, _timed, _functional
201
+ end
202
+
203
+ # Private method, part of the init process when :rate is not given. Also
204
+ # takes care for missing domain (@domain == :missing).
205
+ #
206
+ def check_in_upstream_description_for_r( oo, &block )
207
+ _domain = domain # this method may modify domain
208
+ _functional = true
209
+ # Was action closure was given explicitly?
210
+ if oo.has? :action then
211
+ fail ArgumentError, "If block is given, rate must not be given!" if block
212
+ action_λ = oo[:action].aT_is_a Proc, "supplied action named argument"
213
+ if oo.has? :timed then
214
+ _timed = oo[:timed]
215
+ # Time to worry about the domain_missing
216
+ if domain == :missing then # figure user's intent from closure arity
217
+ _domain = if action_λ.arity == ( _timed ? 1 : 0 ) then
218
+ [] # user meant empty domain
219
+ else
220
+ codomain # user meant domain same as codomain
221
+ end
222
+ else # domain not missing
223
+ fail TypeError, "Rate closure arity (#{rate_arg.arity}) > domain " +
224
+ "size (#{domain.size})!" if action_λ.arity.abs > domain.size
225
+ end
226
+ else # :timed argument not supplied
227
+ if domain == :missing then
228
+ # If no domain was supplied, there is no way to reasonably figure
229
+ # out the user's intent, except when arity is 0:
230
+ _domain = case action_λ.arity
231
+ when 0 then
232
+ _timed = false
233
+ [] # empty domain is implied
234
+ else # no deduction of user intent possible
235
+ fail ArgumentError, "Too much ambiguity: Rateless " +
236
+ "transition with neither domain nor timedness given."
237
+ end
238
+ else # domain not missing
239
+ # Even if the user did not bother to inform us explicitly about
240
+ # timedness, we can use closure arity as a clue. If it equals the
241
+ # domain size, leaving no room for Δtime argument, the user intent
242
+ # was to create timeless transition. If it equals domain size + 1,
243
+ # theu user intended to create a timed transition.
244
+ _timed = case action_λ.arity
245
+ when domain.size then false
246
+ when domain.size + 1 then true
247
+ else # no deduction of user intent possible
248
+ fail ArgumentError, "Timedness was not specified, and " +
249
+ "action closure arity (#{action_λ.arity}) does not " +
250
+ "give a clear hint on it!"
251
+ end
252
+ end
253
+ end
254
+ else # rateless cases with no action closure specified
255
+ # Consume block, if given:
256
+ check_in_upstream_for_r oo.update( action: block ) if block
257
+ # If there is really really no closure, an assumption must be made taken
258
+ # as for the transition's action, in particular, -> { 1 } closure:
259
+ action_λ = -> { 1 }
260
+ # The transition is then required to be stoichiometric and timeless.
261
+ # Domain will be required empty.
262
+ fail ArgumentError, "Stoichiometry is compulsory, if no rate/action " +
263
+ "was supplied." unless stoichiometric?
264
+ # With this, we can drop worries about missing domain.
265
+ fail ArgumentError, "When no rate/propensity or action is supplied, " +
266
+ "the transition cannot be timed." if oo[:timed] if oo.has? :timed
267
+ _timed = false
268
+ _domain = []
269
+ _functional = false # the transition is considered functionless
270
+ end
271
+ return _domain, action_λ, _timed, _functional
272
+ end
273
+
274
+ # Default rate closure for SR transitions whose rate is hinted as a number.
275
+ #
276
+ def construct_standard_mass_action( num )
277
+ # assume standard mass-action law
278
+ nonpositive_coeffs = stoichiometry.select { |coeff| coeff <= 0 }
279
+ # the closure takes markings of the domain as its arguments
280
+ -> *markings do
281
+ nonpositive_coeffs.size.times.reduce num do |acc, i|
282
+ marking, coeff = markings[ i ], nonpositive_coeffs[ i ]
283
+ # Stoichiometry coefficients equal to zero are taken to indicate
284
+ # plain factors, assuming that if these places were not involved
285
+ # in the transition at all, the user would not be mentioning them.
286
+ case coeff
287
+ when 0, -1 then marking * acc
288
+ else marking ** -coeff end
289
+ end
290
+ end
291
+ end
292
+
293
+ # Private method, checking in downstream specification from the argument
294
+ # field for stoichiometric transition.
295
+ #
296
+ def check_in_downstream_description_for_S( oo )
297
+ codomain, stoichio =
298
+ case oo[:stoichiometry]
299
+ when Hash then
300
+ # contains pairs { codomain place => stoichiometry coefficient }
301
+ fail ArgumentError, "With hash-type stoichiometry, :codomain " +
302
+ "argument must not be given!" if oo.has? :codomain
303
+ oo[:stoichiometry].each_with_object [[], []] do |(cd_pl, coeff), memo|
304
+ memo[0] << cd_pl
305
+ memo[1] << coeff
306
+ end
307
+ else
308
+ # array of stoichiometry coefficients
309
+ fail ArgumentError, "With array-type stoichiometry, :codomain " +
310
+ "argument must be given!" unless oo.has? :codomain
311
+ [ oo[:codomain], Array( oo[:stoichiometry] ) ]
312
+ end
313
+ # enforce that stoichiometry is a collection of numbers
314
+ return sanitize_place_collection( codomain, "supplied codomain" ),
315
+ stoichio.aT_all_numeric( "supplied stoichiometry" )
316
+ end
317
+
318
+ # Private method, checking in downstream specification from the argument
319
+ # field for nonstoichiometric transition.
320
+ #
321
+ def check_in_downstream_description_for_s( oo )
322
+ # codomain must be explicitly given - no way around it:
323
+ fail ArgumentError, "For non-stoichiometric transitions, :codomain " +
324
+ "argument is compulsory." unless oo.has? :codomain
325
+ return sanitize_place_collection( oo[:codomain], "supplied codomain" )
326
+ end
327
+
328
+ # Private method, part of #initialize argument checking-in.
329
+ #
330
+ def check_in_assignment_action( oo )
331
+ if oo.has? :assignment_action, syn!: [ :assignment, :assign, :A ] then
332
+ if timed? then
333
+ false.tap do
334
+ msg = "Timed transitions may not have assignment action!"
335
+ raise TypeError, msg if oo[:assignment_action]
336
+ end
337
+ else oo[:assignment_action] end # only timeless transitions are eligible
338
+ else false end # the default value
339
+ end
340
+
341
+ # Private method, part of #initialize argument checking-in
342
+ #
343
+ def check_in_guards( oo )
344
+ if oo.has? :domain_guard then
345
+ oo[:domain_guard].aT_is_a Proc, "supplied domain guard"
346
+ else
347
+ place_guards = domain_places.map &:guard
348
+ -> dm do # constructing the default domain guard
349
+ fails = [domain, dm, place_guards].transpose.map { |pl, m, guard|
350
+ [ pl, m, begin; guard.( m ); true; rescue YPetri::GuardError; false end ]
351
+ }.reduce [] do |memo, triple| memo << triple unless triple[2] end
352
+ # TODO: Watch "Exceptional Ruby" video by Avdi Grimm.
353
+ unless fails.size == 0
354
+ fail YPetri::GuardError, "Domain guard of #{self} rejects marking " +
355
+ if fails.size == 1 then
356
+ p, m, _ = fails[0]
357
+ "#{m} of place #{p.name || p.object_id}!"
358
+ else
359
+ "of the following places: %s!" %
360
+ Hash[ fails.map { |pl, m, _| [pl.name || pl.object_id, m] } ]
361
+ end
362
+ end
363
+ end
364
+ end
365
+ end
366
+
367
+ # Informs upstream places that they have been connected to this transition.
368
+ #
369
+ def inform_upstream_places
370
+ upstream_places.each { |p| p.send :register_downstream_transition, self }
371
+ end
372
+
373
+ # Informs downstream places that they are connected to this transition.
374
+ #
375
+ def inform_downstream_places
376
+ downstream_places.each { |p| p.send :register_upstream_transition, self }
377
+ end
378
+ end # class YPetri::Transition