y_petri 2.0.3 → 2.0.7

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -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