y_petri 1.0.0

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,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