y_petri 1.0.0

Sign up to get free protection for your applications and to get access to all the features.
@@ -0,0 +1,921 @@
1
+ #encoding: utf-8
2
+
3
+ # Now is a good time to talk about transition classification:
4
+ #
5
+ # STOICHIOMETRIC / NON-STOICHIOMETRIC
6
+ # I. For stoichiometric transitions:
7
+ # 1. Rate vector is computed as rate * stoichiometry vector, or
8
+ # 2. Δ vector is computed a action * stoichiometry vector.
9
+ # II. For non-stoichiometric transitions:
10
+ # 1. Rate vector is obtained as the rate closure result, or
11
+ # 2. action vector is obtained as the action closure result.
12
+ #
13
+ # Conclusion: stoichiometricity distinguishes *need to multiply the
14
+ # rate/action closure result by stoichiometry*.
15
+ #
16
+ # HAVING / NOT HAVING RATE
17
+ # I. For transitions with rate, the closure result has to be
18
+ # multiplied by the time step duration (delta_t) to get action.
19
+ # II. For rateless transitions, the closure result is used as is.
20
+ #
21
+ # Conclusion: has_rate? distinguishes *need to multiply the closure
22
+ # result by delta time* - differentiability of action by time.
23
+ #
24
+ # TIMED / TIMELESS
25
+ # I. For timed transitions, action is time-dependent. Transitions with
26
+ # rate are thus always timed. In rateless transitions, timedness means
27
+ # that the action closure expects time step length (delta_t) as its first
28
+ # argument - its arity is thus codomain size + 1.
29
+ # II. For timeless transitions, action is time-independent. Timeless
30
+ # transitions are necessarily also rateless. Arity of the action closure
31
+ # is expected to match the domain size.
32
+ #
33
+ # Conclusion: Transitions with rate are always timed. In rateless
34
+ # transitions, timedness distinguishes the need to supply time step
35
+ # duration as the first argument to the action closure.
36
+ #
37
+ # ASSIGNMENT TRANSITIONS
38
+ # Named argument :assignment_action set to true indicates that the
39
+ # transitions acts by replacing the object stored as place marking by
40
+ # the object supplied by the transition. For numeric types, same
41
+ # effect can be achieved by subtracting the old number from the place
42
+ # and subsequently adding the new value to it.
43
+ #
44
+ module YPetri
45
+
46
+ # Represents a Petri net transition. YPetri transitions come in 6
47
+ # basic types
48
+ #
49
+ # === Basic transition types
50
+ #
51
+ # * <b>ts</b> – timeless nonstoichiometric
52
+ # * <b>tS</b> – timeless stoichiometric
53
+ # * <b>Tsr</b> – timed rateless nonstoichiometric
54
+ # * <b>TSr</b> – timed rateless stoichiometric
55
+ # * <b>sR</b> – nonstoichiometric with rate
56
+ # * <b>SR</b> – stoichiometric with rate
57
+ #
58
+ # These 6 kinds of YPetri transitions correspond to the vertices
59
+ # of a cube with 3 dimensions:
60
+ #
61
+ # - stoichiometric (S) / nonstoichiometric (s)
62
+ # - timed (T) / timeless (t)
63
+ # - having rate (R) / not having rate (r)
64
+ #
65
+ # Since transitions with rate are always timed, and vice-versa, timeless
66
+ # transitions cannot have rate, there are only 6 permissible combinations,
67
+ # mentioned above.
68
+ #
69
+ # === Domain and codomin
70
+ #
71
+ # Each transition has a domain, or 'upstream places': A collection of places
72
+ # whose marking directly affects the transition's operation. Also, each
73
+ # transition has a codomain, or 'downstream places': A collection of places,
74
+ # whose marking is directly affected by the transition's operation.
75
+ #
76
+ # === Action and action vector
77
+ #
78
+ # Regardless of the type, every transition has <em>action</em>:
79
+ # A prescription of how the transition changes the marking of its codomain
80
+ # when it fires. With respect to the transition's codomain, we can also
81
+ # talk about <em>action vector</em>. For non-stoichiometric transitions,
82
+ # the action vector is directly the output of the action closure or rate
83
+ # closure multiplied by Δtime, while for stoichiometric transitions, this
84
+ # needs to be additionaly multiplied by the transitions stoichiometric
85
+ # vector. Now we are finally equipped to talk about the exact meaning of
86
+ # 3 basic transition properties.
87
+ #
88
+ # === Meaning of the 3 basic transition properties
89
+ #
90
+ # ==== Stoichiometric / non-stoichiometric
91
+ # * For stoichiometric transitions:
92
+ # [Rate vector] is computed as rate * stoichiometry vector, or
93
+ # [Δ vector] is computed a action * stoichiometry vector
94
+ # * For non-stoichiometric transitions:
95
+ # [Rate vector] is obtained as the rate closure result, or
96
+ # [action vector] is obtained as the action closure result.
97
+ #
98
+ # Conclusion: stoichiometricity distinguishes <b>need to multiply the
99
+ # rate/action closure result by stoichiometry</b>.
100
+ #
101
+ # ==== Having / not having rate
102
+ # * For transitions with rate, the closure result has to be
103
+ # multiplied by the time step duration (Δt) to get the action.
104
+ # * For rateless transitions, the closure result is used as is.
105
+ #
106
+ # Conclusion: has_rate? distinguishes <b>the need to multiply the closure
107
+ # result by delta time</b> - differentiability of action by time.
108
+ #
109
+ # ==== Timed / Timeless
110
+ # * For timed transitions, action is time-dependent. Transitions with
111
+ # rate are thus always timed. In rateless transitions, timedness means
112
+ # that the action closure expects time step length (delta_t) as its first
113
+ # argument - its arity is thus codomain size + 1.
114
+ # * For timeless transitions, action is time-independent. Timeless
115
+ # transitions are necessarily also rateless. Arity of the action closure
116
+ # is expected to match the domain size.
117
+ #
118
+ # Conclusion: Transitions with rate are always timed. In rateless
119
+ # transitions, timedness distinguishes <b>the need to supply time step
120
+ # duration as the first argument to the action closure</b>.
121
+ #
122
+ # === Other transition types
123
+ #
124
+ # ==== Assignment transitions
125
+ # Named argument :assignment_action set to true indicates that the
126
+ # transitions acts by replacing the object stored as place marking by
127
+ # the object supplied by the transition. (Same as in with spreadsheet
128
+ # functions.) For numeric types, same effect can be achieved by subtracting
129
+ # the old number from the place and subsequently adding the new value to it.
130
+ #
131
+ # ==== Functional / Functionless transitions
132
+ # Original Petri net definition does not speak about transition "functions",
133
+ # but it more or less assumes timeless action according to the stoichiometry.
134
+ # So in YPetri, stoichiometric transitions with no action / rate closure
135
+ # specified become functionless transitions as meant by Carl Adam Petri.
136
+ #
137
+ class Transition
138
+ include NameMagic
139
+
140
+ BASIC_TRANSITION_TYPES = {
141
+ ts: "timeless nonstoichiometric transition",
142
+ tS: "timeless stoichiometric transition",
143
+ Tsr: "timed rateless nonstoichiometric transition",
144
+ TSr: "timed rateless stoichiometric transition",
145
+ sR: "nonstoichiometric transition with rate",
146
+ SR: "stoichiometric transition with rate"
147
+ }
148
+
149
+ # Domain, or 'upstream arcs', is a collection of places, whose marking
150
+ # directly affects the transition's action.
151
+ #
152
+ attr_reader :domain
153
+ alias :domain_arcs :domain
154
+ alias :domain_places :domain
155
+ alias :upstream :domain
156
+ alias :upstream_arcs :domain
157
+ alias :upstream_places :domain
158
+
159
+ # Names of upstream places.
160
+ #
161
+ def domain_pp; domain.map &:name end
162
+ alias :upstream_pp :domain_pp
163
+
164
+ # Names of upstream places as symbols.
165
+ #
166
+ def domain_pp_sym; domain_pp.map &:to_sym end
167
+ alias :upstream_pp_sym :domain_pp_sym
168
+ alias :domain_ppß :domain_pp_sym
169
+ alias :ustream_ppß :domain_pp_sym
170
+
171
+ # Codomain, 'downstream arcs', or 'action arcs' is a collection of places,
172
+ # whose marking is directly changed by firing the trinsition.
173
+ #
174
+ attr_reader :codomain
175
+ alias :codomain_arcs :codomain
176
+ alias :codomain_places :codomain
177
+ alias :downstream :codomain
178
+ alias :downstream_arcs :codomain
179
+ alias :downstream_places :codomain
180
+ alias :action_arcs :codomain
181
+
182
+ # Names of downstream places.
183
+ #
184
+ def codomain_pp; codomain.map &:name end
185
+ alias :downstream_pp :codomain_pp
186
+
187
+ # Names of downstream places as symbols.
188
+ #
189
+ def codomain_pp_sym; codomain_pp.map &:to_sym end
190
+ alias :downstream_pp_sym :codomain_pp_sym
191
+ alias :codomain_ppß :codomain_pp_sym
192
+ alias :downstream_ppß :codomain_pp_sym
193
+
194
+ # Returns the union of action arcs and test arcs.
195
+ #
196
+ def arcs; domain | codomain end
197
+ alias :connectivity :arcs
198
+
199
+ # Returns connectivity as names.
200
+ #
201
+ def cc; connectivity.map &:name end
202
+
203
+ # Returns connectivity as name symbols.
204
+ #
205
+ def cc_sym; cc.map &:to_sym end
206
+ alias :ccß :cc_sym
207
+
208
+ # Is the transition stoichiometric?
209
+ #
210
+ def stoichiometric?; @stoichiometric end
211
+ alias :s? :stoichiometric?
212
+
213
+ # Is the transition nonstoichiometric? (Opposite of #stoichiometric?)
214
+ #
215
+ def nonstoichiometric?; not stoichiometric? end
216
+
217
+ # Stoichiometry (implies that the transition is stoichiometric).
218
+ #
219
+ attr_reader :stoichiometry
220
+
221
+ # Stoichiometry as a hash of pairs:
222
+ # { codomain_place_instance => stoichiometric_coefficient }
223
+ #
224
+ def stoichio; Hash[ codomain.zip( @stoichiometry ) ] end
225
+
226
+ # Stoichiometry as a hash of pairs:
227
+ # { codomain_place_name_symbol => stoichiometric_coefficient }
228
+ #
229
+ def s; stoichio.with_keys { |k| k.name.to_sym } end
230
+
231
+ # Does the transition have rate?
232
+ #
233
+ def has_rate?; @has_rate end
234
+
235
+ # Is the transition rateless?
236
+ #
237
+ def rateless?; not has_rate? end
238
+
239
+ # The term 'flux' (meaning flow) is associated with continuous transitions,
240
+ # while term 'propensity' is used with discrete stochastic transitions.
241
+ # By the design of YPetri, distinguishing between discrete and continuous
242
+ # computation is the responsibility of the simulation method, considering
243
+ # current marking of the transition's connectivity and quanta of its
244
+ # codomain. To emphasize unity of 'flux' and 'propensity', term 'rate' is
245
+ # used to represent both of them. Rate closure input arguments must
246
+ # correspond to the domain places.
247
+ #
248
+ attr_reader :rate_closure
249
+ alias :rate :rate_closure
250
+ alias :flux_closure :rate_closure
251
+ alias :flux :rate_closure
252
+ alias :propensity_closure :rate_closure
253
+ alias :propensity :rate_closure
254
+
255
+ # For rateless transition, action closure must be present. Action closure
256
+ # input arguments must correspond to the domain places, and for timed
257
+ # transitions, the first argument of the action closure must be Δtime.
258
+ #
259
+ attr_reader :action_closure
260
+ alias :action :action_closure
261
+
262
+ # Does the transition's action depend on delta time?
263
+ #
264
+ def timed?; @timed end
265
+
266
+ # Is the transition timeless? (Opposite of #timed?)
267
+ #
268
+ def timeless?; not timed? end
269
+
270
+ # Is the transition functional?
271
+ # Explanation: If rate or action closure is supplied, a transition is always
272
+ # considered 'functional'. Otherwise, it is considered not 'functional'.
273
+ # Note that even transitions that are not functional still have standard
274
+ # action acc. to Petri's definition. Also note that a timed transition is
275
+ # necessarily functional.
276
+ #
277
+ def functional?; @functional end
278
+
279
+ # Opposite of #functional?
280
+ #
281
+ def functionless?; not functional? end
282
+
283
+ # Reports transition membership in one of 6 basic types of YPetri transitions:
284
+ # 1. ts ..... timeless nonstoichiometric
285
+ # 2. tS ..... timeless stoichiometric
286
+ # 3. Tsr .... timed rateless nonstoichiometric
287
+ # 4. TSr .... timed rateless stoichiometric
288
+ # 5. sR ..... nonstoichiometric with rate
289
+ # 6. SR ..... stoichiometric with rate
290
+ #
291
+ def basic_type
292
+ if has_rate? then stoichiometric? ? :SR : :sR
293
+ elsif timed? then stoichiometric? ? :TSr : :Tsr
294
+ else stoichiometric? ? :tS : :ts end
295
+ end
296
+
297
+ # Reports transition's type (basic type + whether it's an assignment
298
+ # transition).
299
+ #
300
+ def type
301
+ assignment_action? ? "A(ts)" : basic_type
302
+ end
303
+
304
+ # Is it an assignment transition?
305
+ #
306
+ # A transition can be specified to have 'assignment action', in which case
307
+ # it completely replaces codomain marking with the objects resulting from
308
+ # the transition's action. Note that for numeric marking, specifying
309
+ # assignment action is a matter of convenience, not necessity, as it can
310
+ # be emulated by fully subtracting the present codomain values and adding
311
+ # the numbers computed by the transition to them. Assignment action flag
312
+ # is a matter of necessity only when codomain marking involves objects
313
+ # not supporting subtraction/addition (which is out of the scope of Petri's
314
+ # original specification anyway.)
315
+ #
316
+ def assignment_action?; @assignment_action end
317
+ alias :assignment? :assignment_action?
318
+
319
+ # Is the transition cocked?
320
+ #
321
+ # The transition has to be cocked before #fire method can be called
322
+ # successfully. (Can be overriden using #fire! method.)
323
+ #
324
+ def cocked?; @cocked end
325
+
326
+ # Opposite of #cocked?
327
+ #
328
+ def uncocked?; not cocked? end
329
+
330
+ # As you could have noted in the introduction, Transition class encompasses
331
+ # all different kinds of Petri net transitions. This is considered a good
332
+ # design pattern for cases like this, but it makes the transition class and
333
+ # its constructor look a bit complicated. Should you feel that way, please
334
+ # remember that you only learn one constructor, but can create many kinds
335
+ # of transition – the computer is doing a lot of work behind the scenes for
336
+ # you. The type of a transition created depends on the qualities of supplied
337
+ # arguments. However, you can also explicitly specify what kind of
338
+ # transition do you want, to exclude any ambiguity.
339
+ #
340
+ # Whatever arguments you supply, the constructor will always need a way to
341
+ # determine domain (upstream arcs) and codomain (downstream arcs) of your
342
+ # transitions, implicitly or explicitly. Secondly, the constructor must
343
+ # have a way to determine the transition's action, although there is more
344
+ # than one way of doing so. So enough talking and onto the examples. We
345
+ # will imagine having 3 places A, B, C, for which we will create various
346
+ # transitions:
347
+ #
348
+ # ==== Timeless nonstoichiometric (ts) transitions
349
+ # Action closure has to be supplied, whose return arity correspons to
350
+ # the codomain size.
351
+ # <tt>
352
+ # Transition.new upstream_arcs: [A, C], downstream_arcs: [A, B],
353
+ # action_closure: proc { |m, x|
354
+ # if x > 0 then [-(m / 2), (m / 2)]
355
+ # else [1, 0] end }
356
+ # </tt>
357
+ # (This represents a transition connected by arcs to places A, B, C, whose
358
+ # operation depends on C in such way, that if C.marking is positive,
359
+ # then half of the marking of A is shifted to B, while if C.marking is
360
+ # nonpositive, 1 is added to A.)
361
+ #
362
+ # ==== Timeless stoichiometric (tS) transitions
363
+ # Stochiometry has to be supplied, with optional action closure.
364
+ # Action closure return arity should be 1 (its result will be multiplied
365
+ # by the stoichiometry vector).
366
+ #
367
+ # If no action closure is given, a <em>functionless</em> transition will
368
+ # be created, whose action closure will be by default 1 * stoichiometry
369
+ # vector.
370
+ #
371
+ # ==== Timed rateless nonstoichiometric (Tsr) transitions
372
+ # Action closure has to be supplied, whose first argument is Δt, and the
373
+ # remaining arguments correspond to the domain size. Return arity of this
374
+ # closure should correspond to the codomain size.
375
+ #
376
+ # ==== Timed rateless stoichiometric (TSr) transitions
377
+ # Action closure has to be supplied, whose first argument is Δt, and the
378
+ # remaining arguments correspond to the domain size. Return arity of this
379
+ # closure should be 1 (to be multiplied by the stoichiometry vector).
380
+ #
381
+ # ==== Nonstoichiometric transitions with rate (sR)
382
+ # Rate closure has to be supplied, whose arity should correspond to the
383
+ # domain size (Δt argument is not needed). Return arity of this closure
384
+ # should correspond to the codomain size and represents rate of change
385
+ # contribution for marking of the codomain places.
386
+ #
387
+ # ==== Stoichiometric transitions with rate (SR)
388
+ #
389
+ # Rate closure and stoichiometry has to be supplied, whose arity should
390
+ # correspond to the domain size. Return arity of this closure should be 1
391
+ # (to be multiplied by the stoichiometry vector, as in all stoichiometric
392
+ # transitions).
393
+ #
394
+ # <tt>Transition( stoichiometry: { A: -1, B: 1 },
395
+ # rate: λ { |a| a * 0.5 } )
396
+ #
397
+ def initialize *args
398
+ # do the big work of checking in the arguments
399
+ check_in_arguments *args
400
+ # Inform the relevant places that they have been connected:
401
+ upstream.each{ |place| place.send :register_downstream_transition, self }
402
+ downstream.each{ |place| place.send :register_upstream_transition, self }
403
+ # transitions initialize uncocked:
404
+ @cocked = false
405
+ end
406
+
407
+ # Marking of the domain places.
408
+ #
409
+ def domain_marking
410
+ domain.map &:marking
411
+ end
412
+
413
+ # Marking of the codomain places.
414
+ #
415
+ def codomain_marking
416
+ codomain.map &:marking
417
+ end
418
+
419
+ # Result of the transition's "function", regardless of the #enabled? status.
420
+ #
421
+ def action Δt=nil
422
+ raise AErr, "Δtime argument required for timed transitions!" if
423
+ timed? and Δt.nil?
424
+ # the code here looks awkward, because I was trying to speed it up
425
+ if has_rate? then
426
+ if stoichiometric? then
427
+ rate = rate_closure.( *domain_marking )
428
+ stoichiometry.map{ |coeff| rate * coeff * Δt }
429
+ else # assuming correct return value arity from the rate closure:
430
+ rate_closure.( *domain_marking ).map{ |e| component * Δt }
431
+ end
432
+ else # rateless
433
+ if timed? then
434
+ if stoichiometric? then
435
+ rslt = action_closure.( Δt, *domain_marking )
436
+ stoichiometry.map{ |coeff| rslt * coeff }
437
+ else
438
+ action_closure.( Δt, *domain_marking ) # caveat result arity!
439
+ end
440
+ else # timeless
441
+ if stoichiometric? then
442
+ rslt = action_closure.( *domain_marking )
443
+ stoichiometry.map{ |coeff| rslt * coeff }
444
+ else
445
+ action_closure.( *domain_marking ) # caveat result arity!
446
+ end
447
+ end
448
+ end
449
+ end # action
450
+
451
+ # Zero action
452
+ #
453
+ def zero_action
454
+ codomain.map { 0 }
455
+ end
456
+
457
+ # Changes to the marking of codomain, as they would happen if #fire! was
458
+ # called right now (ie. honoring #enabled?, but not #cocked? status.
459
+ #
460
+ def action_after_feasibility_check( Δt=nil )
461
+ raise AErr, "Δtime argument required for timed transitions!" if
462
+ timed? and Δt.nil?
463
+ act = Array( action Δt )
464
+ # Assignment actions are always feasible - no need to check:
465
+ return act if assignment?
466
+ # check if the marking after the action would still be positive
467
+ enabled = codomain
468
+ .zip( act )
469
+ .all?{ |place, change| place.marking.to_f >= -change.to_f }
470
+ if enabled then act else
471
+ raise "firing of #{self}#{ Δt ? ' with Δtime %s' % Δt : '' } " +
472
+ "would result in negative marking"
473
+ zero_action
474
+ end
475
+ # LATER: This use of #zip here should be avoided for speed
476
+ end
477
+
478
+ # Allows #fire method to succeed. (#fire! disregards cocking.)
479
+ #
480
+ def cock; @cocked = true end
481
+ alias :cock! :cock
482
+
483
+ # Uncocks a cocked transition without firing it.
484
+ #
485
+ def uncock; @cocked = false end
486
+ alias :uncock! :uncock
487
+
488
+ # If #fire method of a transition applies its action (token adding/taking)
489
+ # on its domain, depending on codomain marking. Time step is expected as
490
+ # argument if the transition is timed. Only works if the transition has
491
+ # been cocked and causes the transition to uncock.
492
+ #
493
+ def fire( Δt=nil )
494
+ raise AErr, "Δtime argument required for timed transitions!" if
495
+ timed? and Δt.nil?
496
+ return false unless cocked?
497
+ uncock
498
+ fire! Δt
499
+ return true
500
+ end
501
+
502
+ # #fire! (with bang) fires the transition regardless of cocked status.
503
+ #
504
+ def fire!( Δt=nil )
505
+ raise AErr, "Δtime required for timed transitions!" if timed? && Δt.nil?
506
+ if assignment_action? then
507
+ act = Array action( Δt )
508
+ codomain.each_with_index do |place, i|
509
+ place.marking = act[i]
510
+ end
511
+ else
512
+ act = action_after_feasibility_check( Δt )
513
+ codomain.each_with_index do |place, i|
514
+ place.add act[i]
515
+ end
516
+ end
517
+ return nil
518
+ end
519
+
520
+ # Sanity of execution is ensured by Petri's notion of transitions being
521
+ # "enabled" if and only if the intended action can immediately take
522
+ # place without getting places into forbidden state (negative marking).
523
+ #
524
+ def enabled?( Δt=nil )
525
+ raise AErr, "Δtime argument required for timed transitions!" if
526
+ timed? and Δt.nil?
527
+ codomain
528
+ .zip( action Δt )
529
+ .all? { |place, change| place.marking.to_f >= -change.to_f }
530
+ end
531
+
532
+ # Recursive firing of the upstream net portion (honors #cocked?).
533
+ #
534
+ def fire_upstream_recursively
535
+ return false unless cocked?
536
+ uncock
537
+ upstream_places.each &:fire_upstream_recursively
538
+ fire!
539
+ return true
540
+ end
541
+
542
+ # Recursive firing of the downstream net portion (honors #cocked?).
543
+ #
544
+ def fire_downstream_recursively
545
+ return false unless cocked?
546
+ uncock
547
+ fire!
548
+ downstream_places.each &:fire_downstream_recursively
549
+ return true
550
+ end
551
+
552
+ # def lock
553
+ # # LATER
554
+ # end
555
+ # alias :disable! :force_disabled
556
+
557
+ # def unlock
558
+ # # LATER
559
+ # end
560
+ # alias :undisable! :remove_force_disabled
561
+
562
+ # def force_enabled!( boolean )
563
+ # # true - the transition is always regarded as enabled
564
+ # # false - the status is removed
565
+ # # LATER
566
+ # end
567
+
568
+ # def clamp
569
+ # # LATER
570
+ # end
571
+
572
+ # def remove_clamp
573
+ # # LATER
574
+ # end
575
+
576
+ # def reset!
577
+ # uncock
578
+ # remove_force_disabled
579
+ # remove_force_enabled
580
+ # remove_clamp
581
+ # return self
582
+ # end
583
+
584
+ # Inspect string for a transition.
585
+ #
586
+ def inspect
587
+ to_s
588
+ end
589
+
590
+ # Conversion to a string.
591
+ #
592
+ def to_s
593
+ "#<Transition: %s >" %
594
+ "#{name.nil? ? '' : '%s ' % name }(#{basic_type}%s)%s" %
595
+ [ "#{assignment_action? ? ' Assign.' : ''}",
596
+ "#{name.nil? ? ' id:%s' % object_id : ''}" ]
597
+ end
598
+
599
+ private
600
+
601
+ # **********************************************************************
602
+ # ARGUMENT CHECK-IN UPON INITIALIZATION
603
+ # **********************************************************************
604
+
605
+ # Checking in the arguments supplied to #initialize looks like a big job.
606
+ # I won't contest to that, but let us not, that it is basically nothing
607
+ # else then defining the duck type of the input argument collection.
608
+ # TypeError is therefore raised if invalid collection has been supplied.
609
+ #
610
+ def check_in_arguments *args
611
+ oo = args.extract_options!
612
+ oo.may_have :stoichiometry, syn!: [ :stoichio,
613
+ :s ]
614
+ oo.may_have :codomain, syn!: [ :codomain_arcs,
615
+ :codomain_places,
616
+ :downstream,
617
+ :downstream_arcs,
618
+ :downstream_places,
619
+ :action_arcs ]
620
+ oo.may_have :domain, syn!: [ :domain_arcs,
621
+ :domain_places,
622
+ :upstream,
623
+ :upstream_arcs,
624
+ :upstream_places ]
625
+ oo.may_have :rate, syn!: [ :flux,
626
+ :propensity,
627
+ :rate_closure,
628
+ :flux_closure,
629
+ :propensity_closure,
630
+ :Φ,
631
+ :φ ]
632
+ oo.may_have :action, syn!: :action_closure
633
+ oo.may_have :timed
634
+
635
+ # was the rate was given?
636
+ @has_rate = oo.has? :rate
637
+
638
+ # is the transition stoichiometric (S) or nonstoichiometric (s)?
639
+ @stoichiometric = oo.has? :stoichiometry
640
+
641
+ # downstream description arguments: codomain, stoichiometry (if S)
642
+ if stoichiometric? then
643
+ @codomain, @stoichiometry = check_in_downstream_description_for_S( oo )
644
+ else # s transitions have no stoichiometry
645
+ @codomain = check_in_downstream_description_for_s( oo )
646
+ end
647
+
648
+ # check in domain first, :missing symbol may appear
649
+ @domain = check_in_domain( oo )
650
+
651
+ # upstream description arguments; also takes care of :missing domain
652
+ if has_rate? then
653
+ @domain, @rate_closure, @timed, @functional =
654
+ check_in_upstream_description_for_R( oo )
655
+ else
656
+ @domain, @action_closure, @timed, @functional =
657
+ check_in_upstream_description_for_r( oo )
658
+ end
659
+
660
+ # optional assignment action:
661
+ @assignment_action = check_in_assignment_action( oo )
662
+ end # def check_in_arguments
663
+
664
+ # Makes sure that supplied collection consists only of appropriate places.
665
+ # Second optional argument customizes the error message.
666
+ #
667
+ def sanitize_place_collection place_collection, what_is_collection=nil
668
+ c = what_is_collection ? what_is_collection.capitalize : "Collection"
669
+ Array( place_collection ).map do |pl_id|
670
+ begin
671
+ place( pl_id )
672
+ rescue NameError
673
+ raise TErr, "#{c} member #{pl_id} does not specify a valid place!"
674
+ end
675
+ end.aT what_is_collection, "not contain duplicate places" do |collection|
676
+ collection == collection.uniq
677
+ end
678
+ end
679
+
680
+ # Private method, part of #initialize argument checking-in.
681
+ #
682
+ def check_in_domain( oo )
683
+ if oo.has? :domain then
684
+ sanitize_place_collection( oo[:domain], "supplied domain" )
685
+ else
686
+ if stoichiometric? then
687
+ # take arcs with non-positive stoichiometry coefficients
688
+ Hash[ [ @codomain, @stoichiometry ].transpose ]
689
+ .delete_if{ |place, coeff| coeff > 0 }.keys
690
+ else
691
+ :missing
692
+ # Barring the caller's error, missing domain can mean:
693
+ # 1. empty domain
694
+ # 2. domain == codomain
695
+ # This will be figured later by rate/action closure arity
696
+ end
697
+ end
698
+ end
699
+
700
+ def check_in_upstream_description_for_R( oo )
701
+ _domain = domain # this method may modify domain
702
+ # check against colliding :action argument
703
+ raise TErr, "Rate & action are mutually exclusive!" if oo.has? :action
704
+ # lets figure the rate closure
705
+ rate_λ = case rate_arg = oo[:rate]
706
+ when Proc then # We received the closure directly,
707
+ # but we've to be concerned about missing domain.
708
+ if domain == :missing then # we've to figure user's intent
709
+ _domain = if rate_arg.arity == 0 then
710
+ [] # user meant empty domain
711
+ else
712
+ codomain # user meant domain same as codomain
713
+ end
714
+ else # domain not missing
715
+ raise TErr, "Rate closure arity (#{rate_arg.arity}) " +
716
+ "greater than domain size (#{domain.size})!" unless
717
+ rate_arg.arity.abs <= domain.size
718
+ end
719
+ rate_arg
720
+ else # We received something else,
721
+ # we must make assumption user's intent.
722
+ if stoichiometric? then # user's intent was mass action
723
+ raise TErr, "When a number is supplied as rate, domain " +
724
+ "must not be given!" if oo.has? :domain
725
+ construct_standard_mass_action( rate_arg )
726
+ else # user's intent was constant closure
727
+ raise TErr, "When rate is a number and no stoichiometry " +
728
+ "is supplied, codomain size must be 1!" unless
729
+ codomain.size == 1
730
+ # Missing domain is OK here,
731
+ _domain = [] if domain == :missing
732
+ # but if it was supplied explicitly, it must be empty.
733
+ raise TErr, "Rate is a number, but non-empty domain was " +
734
+ "supplied!" unless domain.empty? if oo.has?( :domain )
735
+ lambda { rate_arg }
736
+ end
737
+ end
738
+ # R transitions are implicitly timed
739
+ _timed = true
740
+ # check against colliding :timed argument
741
+ oo[:timed].tE :timed, "not be false if rate given" if oo.has? :timed
742
+ # R transitions are implicitly functional
743
+ _functional = true
744
+ return _domain, rate_λ, _timed, _functional
745
+ end
746
+
747
+ def check_in_upstream_description_for_r( oo )
748
+ _domain = domain # this method may modify domain
749
+ _functional = true
750
+ # was action closure was given explicitly?
751
+ if oo.has? :action then
752
+ action_λ = oo[:action].aT_is_a Proc, "supplied action named argument"
753
+ if oo.has? :timed then
754
+ _timed = oo[:timed]
755
+ # Time to worry about the domain_missing
756
+ if domain == :missing then
757
+ # figure user's intent from closure arity
758
+ _domain = if action_λ.arity == ( _timed ? 1 : 0 ) then
759
+ [] # user meant empty domain
760
+ else
761
+ codomain # user meant domain same as codomain
762
+ end
763
+ else # domain not missing
764
+ raise TErr, "Rate closure arity (#{rate_arg.arity}) > domain " +
765
+ "size (#{domain.size})!" if action_λ.arity.abs > domain.size
766
+ end
767
+ else # :timed argument not supplied
768
+ if domain == :missing then
769
+ # If no domain was supplied, there is no way to reasonably figure
770
+ # out the user's intent, except when arity is 0:
771
+ _domain = case action_λ.arity
772
+ when 0 then
773
+ _timed = false
774
+ [] # empty domain is implied
775
+ else # no deduction of user intent possible
776
+ raise AErr, "Too much ambiguity: Neither domain nor " +
777
+ "timedness of the rateless transition was specified."
778
+ end
779
+ else # domain not missing
780
+ # Even if the user did not bother to inform us explicitly about
781
+ # timedness, we can use closure arity as a clue. If it equals the
782
+ # domain size, leaving no room for Δtime argument, the user intent
783
+ # was to create timeless transition. If it equals domain size + 1,
784
+ # theu user intended to create a timed transition.
785
+ _timed = case action_λ.arity
786
+ when domain.size then false
787
+ when domain.size + 1 then true
788
+ else # no deduction of user intent possible
789
+ raise AErr, "Timedness was not specified, and the " +
790
+ "arity of the action supplied action closure " +
791
+ "(#{action_λ.arity}) does not give clear hint on it."
792
+ end
793
+ end
794
+ end
795
+ else # rateless cases with no action closure specified
796
+ # Assumption must be made on transition's action. In particular,
797
+ # lambda { 1 } action closure will be assumed,
798
+ action_λ = lambda { 1 }
799
+ # and it will be required that the transition be stoichiometric and
800
+ # timeless. Domain will thus be required empty.
801
+ raise AErr, "Stoichiometry is compulsory, if rate/action was " +
802
+ "not supplied." unless stoichiometric?
803
+ # With this, we can drop worries about missing domain.
804
+ raise AErr, "When no rate/action is supplied, the transition can't " +
805
+ "be declared timed." if oo[:timed] if oo.has? :timed
806
+ _timed = false
807
+ _domain = []
808
+ _functional = false # the transition is considered functionless
809
+ end
810
+ return _domain, action_λ, _timed, _functional
811
+ end
812
+
813
+ def construct_standard_mass_action( num )
814
+ # assume standard mass-action law
815
+ nonpositive_coeffs = stoichiometry.select { |coeff| coeff <= 0 }
816
+ # the closure takes markings of the domain as its arguments
817
+ lambda { |*markings|
818
+ nonpositive_coeffs.size.times.reduce num do |acc, i|
819
+ marking, coeff = markings[ i ], nonpositive_coeffs[ i ]
820
+ # Stoichiometry coefficients equal to zero are taken to indicate
821
+ # plain factors, assuming that if these places were not involved
822
+ # in the transition at all, the user would not be mentioning them.
823
+ case coeff
824
+ when 0, -1 then marking * acc
825
+ else marking ** -coeff end
826
+ end
827
+ }
828
+ end
829
+
830
+ # Private method, checking in downstream specification from the argument
831
+ # field for stoichiometric transition.
832
+ #
833
+ def check_in_downstream_description_for_S( oo )
834
+ codomain, stoichio =
835
+ case oo[:stoichiometry]
836
+ when Hash then
837
+ # contains pairs { codomain place => stoichiometry coefficient }
838
+ raise AErr, "With hash-type stoichiometry, :codomain named " +
839
+ "argument must not be supplied." if oo.has? :codomain
840
+ oo[:stoichiometry].each_with_object [[], []] do |pair, memo|
841
+ codomain_place, stoichio_coeff = pair
842
+ memo[0] << codomain_place
843
+ memo[1] << stoichio_coeff
844
+ end
845
+ else
846
+ # array of stoichiometry coefficients
847
+ raise AErr, "With array-type stoichiometry, :codomain named " +
848
+ "argument must be supplied." unless oo.has? :codomain
849
+ [ oo[:codomain], Array( oo[:stoichiometry] ) ]
850
+ end
851
+ # enforce that stoichiometry is a collection of numbers
852
+ return sanitize_place_collection( codomain, "supplied codomain" ),
853
+ stoichio.aT_all_numeric( "supplied stoichiometry" )
854
+ end
855
+
856
+ # Private method, checking in downstream specification from the argument
857
+ # field for nonstoichiometric transition.
858
+ #
859
+ def check_in_downstream_description_for_s( oo )
860
+ # codomain must be explicitly given - no way around it:
861
+ raise AErr, "For non-stoichiometric transitions, :codomain named " +
862
+ "argument is compulsory." unless oo.has? :codomain
863
+ return sanitize_place_collection( oo[:codomain], "supplied codomain" )
864
+ end
865
+
866
+ # Private method, part of #initialize argument checking-in.
867
+ #
868
+ def check_in_assignment_action( oo )
869
+ if oo.has? :assignment_action, syn!: [ :assignment, :assign, :A ] then
870
+ if timed? then
871
+ msg = "Timed transitions may not have assignment action!"
872
+ raise TypeError, msg if oo[:assignment_action]
873
+ false
874
+ else # timeless transitions are eligible for assignment action
875
+ oo[:assignment_action]
876
+ end
877
+ else # if assignment action is not specified, false is
878
+ false
879
+ end
880
+ end
881
+
882
+ # Place class pertinent herein. Provided for the purpose of parametrized
883
+ # subclassing; expected to be overriden in the subclasses.
884
+ #
885
+ def Place
886
+ ::YPetri::Place
887
+ end
888
+
889
+ # Transition class pertinent herein. Provided for the purpose of
890
+ # parametrized subclassing; expected to be overriden in the subclasses.
891
+ #
892
+ def Transition
893
+ ::YPetri::Transition
894
+ end
895
+
896
+ # Net class pertinent herein. Provided for the purpose of parametrized
897
+ # subclassing; expected to be overriden in the subclasses.
898
+ #
899
+ def Net
900
+ ::YPetri::Net
901
+ end
902
+
903
+ # Presents Place instance specified by the argument.
904
+ #
905
+ def place instance_identifier
906
+ Place().instance( instance_identifier )
907
+ end
908
+
909
+ # Presents Transition instance specified by the argument.
910
+ #
911
+ def transition instance_identifier
912
+ Transition().instance( instance_identifier )
913
+ end
914
+
915
+ # Presents Net instance specified by the argument.
916
+ #
917
+ def net instance_identifier
918
+ Net().instance( instance_identifier )
919
+ end
920
+ end # class Transition
921
+ end # module YPetri