y_petri 2.0.14 → 2.0.15

Sign up to get free protection for your applications and to get access to all the features.
@@ -1,15 +1,19 @@
1
1
  #encoding: utf-8
2
2
 
3
- # The Petri net model is agnostic of simulation settings. Only for the purpose
4
- # of simulation, model is combined together with specific simulation settings.
5
- # Simulation settings consist of global settings (time step, sampling rate...)
6
- # and object specific settings (clamps, constraints...). Again, clamps and
7
- # constraints *do not belong* to the model. The Petri net model is also agnostic
8
- # of the simulation methods. Simulation is not achieved by calling instance
9
- # methods of the model. Instead, Simulation class makes a 'mental image' of the
10
- # model, and uses that one in the actual simulation.
3
+ require_relative 'simulation/collections'
4
+ require_relative 'simulation/timed'
5
+
6
+ # Represents a simulation of a Petri net, using certain method and settings.
7
+ # Simulation concerns (simulation method and settings, initial values, marking
8
+ # clamps, guards...) are separated from those Petri net domain model (existence,
9
+ # naming, connectivity and function specification of the net). Again, clamps,
10
+ # guards, initial values etc. <b>do not belong</b> to the model, although for
11
+ # convenience, places may carry default initial marking, default guards, and
12
+ # default clamps for use in simulations if none other are specified.
11
13
  #
12
14
  class YPetri::Simulation
15
+ include Collections
16
+
13
17
  SAMPLING_DECIMAL_PLACES = 5
14
18
  SIMULATION_METHODS =
15
19
  [
@@ -24,626 +28,30 @@ class YPetri::Simulation
24
28
  self.class.const_get :DEFAULT_SIMULATION_METHOD
25
29
  end
26
30
 
27
- # Exposing @recording
28
- #
31
+ attr_reader :method, :guarded, :timed, :net
32
+ alias guarded? guarded
33
+ alias timed? timed
34
+ attr_reader :marking_vector
35
+ attr_reader :zero_ᴍ, :zero_gradient
29
36
  attr_reader :recording
30
37
  alias :r :recording
31
38
 
32
- # Zero marking vector.
33
- #
34
- attr_reader :zero_ᴍ
35
-
36
- # Zero gradient.
37
- #
38
- attr_reader :zero_gradient
39
-
40
- # Simulation settings.
41
- #
42
- def settings; {} end
43
- alias :simulation_settings :settings
44
-
45
- def recording_csv_string
46
- CSV.generate do |csv|
47
- @recording.keys.zip( @recording.values ).map{ |a, b| [ a ] + b.to_a }
48
- .each{ |line| csv << line }
49
- end
50
- end
51
-
52
- # Currently, simulation is largely immutable. Net, initial marking, clamps
53
- # and simulation settings are set upon initialization, whereupon the instance
54
- # forms their "mental image", which remains immune to any subsequent changes
55
- # to the original objects. Required parameters are :net, :marking_clamps, and
56
- # :initial_marking. Optional is :method (simulation method), and :guarded
57
- # (true/false, whether the simulation guards the transition function results).
58
- # Guard conditions can be either implicit (guarding against negative values
59
- # and against type changes in by transition action), or explicitly associated
60
- # with either places, or transition function results.
61
- #
62
- def initialize( method: default_simulation_method,
63
- guarded: false,
64
- net: raise( ArgumentError, "Net argument absent!" ),
65
- marking_clamps: {},
66
- initial_marking: {} )
67
- puts "starting to set up Simulation" if YPetri::DEBUG
68
- @method, @guarded, @net = method, guarded, net
69
- @places, @transitions = @net.places.dup, @net.transitions.dup
70
- self.singleton_class.class_exec {
71
- define_method :Place do net.send :Place end
72
- define_method :Transition do net.send :Transition end
73
- define_method :Net do net.send :Net end
74
- private :Place, :Transition, :Net
75
- }; puts "setup of :net mental image complete" if YPetri::DEBUG
76
-
77
- # A simulation distinguishes between free and clamped places. For free
78
- # places, initial marking has to be specified. For clamped places, marking
79
- # clamps have to be specified. Both come as hashes:
80
- @marking_clamps = marking_clamps.with_keys { |k| place k }
81
- @initial_marking = initial_marking.with_keys { |k| place k }
82
- # Enforce that keys in the hashes must be unique:
83
- @marking_clamps.keys.aT_equal @marking_clamps.keys.uniq
84
- @initial_marking.keys.aT_equal @initial_marking.keys.uniq
85
- puts "setup of clamps and initial marking done" if YPetri::DEBUG
86
-
87
- # Each place must have either clamp, or initial marking:
88
- places.each { |pl|
89
- pl.aT "place #{pl}", "have either clamp or initial marking" do |pl|
90
- ( @marking_clamps.keys + @initial_marking.keys ).include? pl
91
- end
92
- }; puts "clamp || initial marking test passed" if YPetri::DEBUG
93
-
94
- # @F2A * ᴍ (marking vector of free places) maps ᴍ to all places.
95
- @F2A = Matrix.correspondence_matrix( free_places, places )
96
- # @C2A * marking_vector_of_clamped_places maps it to all places.
97
- @C2A = Matrix.correspondence_matrix( clamped_places, places )
98
- puts "correspondence matrices set up" if YPetri::DEBUG
99
-
100
- # Stoichiometry matrices:
101
- @S_for_tS = S_for tS_transitions()
102
- @S_for_SR = S_for SR_transitions()
103
- @S_for_TSr = S_for TSr_transitions()
104
- puts "stoichiometry matrices set up" if YPetri::DEBUG
105
-
106
- # Other assets:
107
- @Δ_closures_for_tsa = create_Δ_closures_for_tsa
108
- @Δ_closures_for_Tsr = create_Δ_closures_for_Tsr
109
- @action_closures_for_tS = create_action_closures_for_tS
110
- @action_closures_for_TSr = create_action_closures_for_TSr
111
- @rate_closures_for_sR = create_rate_closures_for_sR
112
- @rate_closures_for_SR = create_rate_closures_for_SR
113
- @assignment_closures_for_A = create_assignment_closures_for_A
114
- @zero_ᴍ = compute_initial_marking_vector_of_free_places.map { |e| e * 0 }
115
- @zero_gradient = @zero_ᴍ.dup
116
- puts "other assets set up, about to reset" if YPetri::DEBUG
117
-
118
- reset!; puts "reset complete" if YPetri::DEBUG
119
- end
120
-
121
- # Returns a new instance of the system simulation at a specified state, with
122
- # same simulation settings. This state (:marking argument) can be specified
123
- # either as marking vector for free or all places, marking array for free or
124
- # all places, or marking hash. If vector or array is given, its size must
125
- # correspond to the number of either free, or all places. If hash is given,
126
- # it is not necessary to specify marking of every place – marking of those
127
- # left out will be left same as in the current state.
128
- #
129
- def at( marking: marking, **oo )
130
- err_msg = "Size of supplied marking must match either the number of " +
131
- "free places, or the number of all places!"
132
- update_method = case marking
133
- when Hash then :update_marking_from_a_hash
134
- when Matrix then
135
- case marking.column_to_a.size
136
- when places.size then :set_marking_vector
137
- when free_places.size then :set_ᴍ
138
- else raise TypeError, err_msg end
139
- else # marking assumed to be an array
140
- case marking.size
141
- when places.size then :set_marking
142
- when free_places.size then :set_m
143
- else raise TypeError, err_msg end
144
- end
145
- return dup( **oo ).send( update_method, marking )
146
- end
147
-
148
- # Exposing @net.
149
- #
150
- attr_reader :net
151
-
152
- # Is the simulation guarded?
153
- #
154
- def guarded?; @guarded end
155
-
156
- # Without arguments or block, it returns simply a list of places. Otherwise,
157
- # it returns a has whose keys are the places, and whose values are governed
158
- # by the supplied parameters (either another collection, or message to #send
159
- # to self to obtain a second collection).
39
+ # Stoichiometry matrix for *tS* transitions.
160
40
  #
161
- def places *aa, &b
162
- return @places.dup if aa.empty? && b.nil?
163
- zip_to_hash places, *aa, &b
164
- end
165
-
166
- # Without arguments or block, it returns simply a list of transitions.
167
- # Otherwise, it returns a has whose keys are the places, and whose values are
168
- # governed by the supplied parameters (either another collection, or message
169
- # to #send to self to obtain a second collection).
170
- #
171
- def transitions *aa, &b
172
- return @transitions.dup if aa.empty? && b.nil?
173
- zip_to_hash transitions, *aa, &b
174
- end
175
-
176
- # Without arguments or block, it returns simply a list of place names.
177
- # Otherwise, it returns a hash whose keys are place names, and whose values
178
- # are determined by the supplied argument(s) and/or block (either another
179
- # collection, or a message to #send to self to obtain such collection). Unary
180
- # block can be supplied to modify these values.
181
- #
182
- def pp *aa, &b
183
- return places.map &:name if aa.empty? && b.nil?
184
- zip_to_hash( places.map { |p| p.name || p }, *aa, &b )
185
- end
186
-
187
- # Without arguments or block, it returns simply a list of transition names.
188
- # Otherwise, it returns a hash whose keys are transition names, and whose
189
- # values are determined by the supplied argument(s) and/or block (either
190
- # another collection, or a message to #send to self to obtain such collection).
191
- # Unary block can be supplied to modify these values.
192
- #
193
- def tt *aa, &b
194
- return transitions.map &:name if aa.empty? && b.nil?
195
- zip_to_hash( transitions.map { |t| t.name || t }, *aa, &b )
196
- end
197
-
198
- # Without arguments or block, it returns simply a list of free places.
199
- # Otherwise, it returns a hash, whose keys are the free places, and whose
200
- # values are governed by the supplied parameters (either another collection,
201
- # or message to #send to self to obtain a second collection).
202
- #
203
- def free_places *aa, &b
204
- return zip_to_hash free_places, *aa, &b unless aa.empty? && b.nil?
205
- kk = @initial_marking.keys
206
- places.select { |p| kk.include? p }
207
- end
41
+ attr_reader :S_tS
208
42
 
209
- # Behaves like #free_places, except that it uses place names instead of
210
- # instances whenever possible.
43
+ # Stoichiometry matrix for *TSr* transitions.
211
44
  #
212
- def free_pp *aa, &b
213
- return free_places.map { |p| p.name || p } if aa.empty? && b.nil?
214
- zip_to_hash free_pp, *aa, &b
215
- end
216
-
217
- # Initial marking definitions for free places (array).
218
- #
219
- def im
220
- free_places.map { |p| @initial_marking[p] }
221
- end
222
-
223
- # Marking array of all places as it appears at the beginning of a simulation.
224
- #
225
- def initial_marking
226
- raise # FIXME: "Initial marking" for all places (ie. incl. clamped ones).
227
- end
228
-
229
- # Initial marking of free places as a column vector.
230
- #
231
- def im_vector
232
- Matrix.column_vector im
233
- end
234
- alias iᴍ im_vector
235
-
236
- # Marking of all places at the beginning of a simulation, as a column vector.
237
- #
238
- def initial_marking_vector
239
- Matrix.column_vector initial_marking
240
- end
45
+ attr_reader :S_TSr
241
46
 
242
- # Without arguments or block, it returns simply a list of clamped places.
243
- # Otherwise, it returns a hash, whose keys are the places, and whose values
244
- # are governed by the supplied parameters (either another collection, or
245
- # message to #send to self to obtain a second collection).
47
+ # Stoichiometry matrix for *SR* transitions.
246
48
  #
247
- def clamped_places *aa, &b
248
- return zip_to_hash clamped_places, *aa, &b unless aa.empty? && b.nil?
249
- kk = @marking_clamps.keys
250
- places.select { |p| kk.include? p }
251
- end
252
-
253
- # Behaves like #clamped_places, except that it uses place names instead of
254
- # instances whenever possible.
255
- #
256
- def clamped_pp *aa, &b
257
- return clamped_places.map { |p| p.name || p } if aa.empty? && b.nil?
258
- zip_to_hash clamped_pp, *aa, &b
259
- end
260
-
261
- # Place clamp definitions for clamped places (array)
262
- #
263
- def marking_clamps
264
- clamped_places.map { |p| @marking_clamps[p] }
265
- end
266
- alias place_clamps marking_clamps
267
-
268
- # Marking array of free places.
269
- #
270
- def m
271
- m_vector.column_to_a
272
- end
273
-
274
- # Marking hash of free places { name: marking }.
275
- #
276
- def pm
277
- free_pp :m
278
- end
279
- alias p_m pm
280
-
281
- # Marking hash of free places { place: marking }.
282
- #
283
- def place_m
284
- free_places :m
285
- end
286
-
287
- # Marking array of all places.
288
- #
289
- def marking
290
- marking_vector ? marking_vector.column_to_a : nil
291
- end
292
-
293
- # Marking hash of all places { name: marking }.
294
- #
295
- def pmarking
296
- pp :marking
297
- end
298
- alias p_marking pmarking
299
-
300
- # Marking hash of all places { place: marking }.
301
- #
302
- def place_marking
303
- places :marking
304
- end
305
-
306
- # Marking of a specified place(s)
307
- #
308
- def marking_of place_or_collection_of_places
309
- if place_or_collection_of_places.respond_to? :each then
310
- place_or_collection_of_places.map { |pl| place_marking[ place( pl ) ] }
311
- else
312
- place_marking[ place( place_or_collection_of_places ) ]
313
- end
314
- end
315
- alias m_of marking_of
316
-
317
- # Marking of free places as a column vector.
318
- #
319
- def m_vector
320
- F2A().t * @marking_vector
321
- end
322
- alias ᴍ m_vector
323
-
324
- # Marking of clamped places as a column vector.
325
- #
326
- def marking_vector_of_clamped_places
327
- C2A().t * @marking_vector
328
- end
329
- alias ᴍ_clamped marking_vector_of_clamped_places
330
-
331
- # Marking of clamped places as an array.
332
- #
333
- def marking_of_clamped_places
334
- ᴍ_clamped.column( 0 ).to_a
335
- end
336
- alias m_clamped marking_of_clamped_places
337
-
338
- # Marking of all places as a column vector.
339
- #
340
- attr_reader :marking_vector
341
-
342
- # Creation of stoichiometry matrix for an arbitrary array of stoichio.
343
- # transitions, that maps (has the number of rows equal to) the free places.
344
- #
345
- def S_for( array_of_S_transitions )
346
- array_of_S_transitions.map { |t| sparse_σ t }
347
- .reduce( Matrix.empty( free_places.size, 0 ), :join_right )
348
- end
349
-
350
- # Creation of stoichiometry matrix for an arbitrary array of stoichio.
351
- # transitions, that maps (has the number of rows equal to) all the places.
352
- #
353
- def stoichiometry_matrix_for( array_of_S_transitions )
354
- array_of_S_transitions.map { |t| sparse_stoichiometry_vector t }
355
- .reduce( Matrix.empty( places.size, 0 ), :join_right )
356
- end
357
-
358
- # 3. Stoichiometry matrix for timeless stoichiometric transitions.
359
- #
360
- attr_reader :S_for_tS
361
-
362
- # 4. Stoichiometry matrix for timed rateless stoichiometric transitions.
363
- #
364
- attr_reader :S_for_TSr
365
-
366
- # 6. Stoichiometry matrix for stoichiometric transitions with rate.
367
- #
368
- attr_reader :S_for_SR
369
-
370
- # Stoichiometry matrix, with the distinction, that the caller asserts,
371
- # that all transitions in this simulation are stoichiometric transitions
372
- # with rate (or error).
373
- #
374
- def S
375
- return S_for_SR() if s_transitions.empty? && r_transitions.empty?
376
- raise "The simulation contains also non-stoichiometric transitions! " +
377
- "Consider using #S_for_SR."
378
- end
379
-
380
- # ==== 1. Exposing ts transitions
381
-
382
- # Without arguments or block, it returns simply a list of timeless
383
- # nonstoichiometric transitions. Otherwise, it returns a hash, whose keys
384
- # are the ts transitions, and values are governed by the supplied parameters
385
- # (either another collection, or a message to #send to self to obtain the
386
- # collection of values).
387
- #
388
- def ts_transitions *aa, &b
389
- return zip_to_hash ts_transitions, *aa, &b unless aa.empty? && b.nil?
390
- sift_from_net :ts_transitions
391
- end
392
-
393
- # Like #ts_transitions, except that transition names are used instead of
394
- # instances, whenever possible.
395
- #
396
- def ts_tt *aa, &b
397
- return zip_to_hash ts_tt, *aa, &b unless aa.empty? && b.nil?
398
- ts_transitions.map { |t| t.name || t }
399
- end
400
-
401
- # Assignment transitions (A transition) can be regarded as a special kind
402
- # of ts transition (subtracting away the current marking of their domain
403
- # and replacing it with the result of their function). But it may often
404
- # be useful to exclude A transitions from among the ts transitions, and
405
- # such set is called tsa transitions (timeless nonstoichiometric
406
- # nonassignment transitions).
407
- #
408
- def tsa_transitions *aa, &b
409
- return zip_to_hash tsa_transitions, *aa, &b unless aa.empty? && b.nil?
410
- sift_from_net :tsa_transitions
411
- end
412
-
413
- # Like #tsa_transitions, except that transition names are used instead of
414
- # instance, whenever possible.
415
- #
416
- def tsa_tt *aa, &b
417
- return zip_to_hash tsa_tt, *aa, &b unless aa.empty? && b.nil?
418
- tsa_transitions.map { |t| t.name || t }
419
- end
420
-
421
- # ==== 2. Exposing tS transitions
422
-
423
- # Without arguments or block, it returns simply a list of timeless
424
- # stoichiometric transitions. Otherwise, it returns a hash, whose keys are
425
- # the tS transitions, and values are governed by the supplied parameters
426
- # (either another collection, or a message to #send to self to obtain the
427
- # collection of values).
428
- #
429
- def tS_transitions *aa, &b
430
- return zip_to_hash tS_transitions, *aa, &b unless aa.empty? && b.nil?
431
- sift_from_net :tS_transitions
432
- end
433
-
434
- # Like #tS_transitions, except that transition names are used instead of
435
- # instances, whenever possible.
436
- #
437
- def tS_tt *aa, &b
438
- return zip_to_hash tS_tt, *aa, &b unless aa.empty? && b.nil?
439
- tS_transitions.map { |t| t.name || t }
440
- end
441
-
442
- # ==== 3. Exposing Tsr transitions
443
-
444
- # Without arguments or block, it returns simply a list of timed rateless
445
- # nonstoichiometric transitions. Otherwise, it returns a hash, whose keys
446
- # are the Tsr transitions, and whose values are governed by the supplied
447
- # arguments (either an explicit collection of values, or a message to #send
448
- # to self to obtain such collection).
449
- #
450
- def Tsr_transitions *aa, &b
451
- return zip_to_hash Tsr_transitions(), *aa, &b unless aa.empty? && b.nil?
452
- sift_from_net :Tsr_transitions
453
- end
454
-
455
- # Like #Tsr_transitions, except that transition names are used instead of
456
- # instances, whenever possible.
457
- #
458
- def Tsr_tt *aa, &b
459
- return zip_to_hash Tsr_tt(), *aa, &b unless aa.empty? && b.nil?
460
- Tsr_transitions().map { |t| t.name || t }
461
- end
462
-
463
- # ==== 4. Exposing TSr transitions
464
-
465
- # Without arguments or block, it returns simply a list of timed rateless
466
- # stoichiometric transitions. Otherwise, it returns a hash, whose keys are
467
- # are the TSr transitions, and whose values are governed by the supplied
468
- # arguments (either an explicit collection of values, or a message to #send
469
- # to self to obtain such collection).
470
- #
471
- def TSr_transitions *aa, &b
472
- return zip_to_hash TSr_transitions(), *aa, &b unless aa.empty? && b.nil?
473
- sift_from_net :TSr_transitions
474
- end
475
-
476
- # Like #TSr_transitions, except that transition names are used instead of
477
- # instances, whenever possible.
478
- #
479
- def TSr_tt *aa, &b
480
- return zip_to_hash TSr_tt(), *aa, &b unless aa.empty? && b.nil?
481
- TSr_transitions().map { |t| t.name || t }
482
- end
483
-
484
- # ==== 5. Exposing sR transitions
485
-
486
- # Without arguments or block, it returns simply a list of nonstoichiometric
487
- # transitions with rate. Otherwise, it returns a hash, whose keys are
488
- # are the sR transitions, and whose values are governed by the supplied
489
- # arguments (either an explicit collection of values, or a message to #send
490
- # to self to obtain such collection).
491
- #
492
- def sR_transitions *aa, &b
493
- return zip_to_hash sR_transitions(), *aa, &b unless aa.empty? && b.nil?
494
- sift_from_net :sR_transitions
495
- end
496
-
497
- # Like #sR_transitions, except that transition names are used instead of
498
- # instances, whenever possible.
499
- #
500
- def sR_tt *aa, &b
501
- return zip_to_hash sR_tt(), *aa, &b unless aa.empty? && b.nil?
502
- sR_transitions.map { |t| t.name || t }
503
- end
504
-
505
- # ==== 6. Exposing SR transitions
506
-
507
- # Without arguments or block, it returns simply a list of stoichiometric
508
- # transitions with rate. Otherwise, it returns a hash, whose keys are
509
- # are the SR transitions, and whose values are governed by the supplied
510
- # arguments (either an explicit collection of values, or a message to #send
511
- # to self to obtain such collection).
512
- #
513
- def SR_transitions *aa, &b
514
- return zip_to_hash SR_transitions(), *aa, &b unless aa.empty? && b.nil?
515
- sift_from_net :SR_transitions
516
- end
517
-
518
- # Like #SR_transitions, except that transition names are used instead of
519
- # instances, whenever possible.
520
- #
521
- def SR_tt *aa, &b
522
- return zip_to_hash SR_tt(), *aa, &b unless aa.empty? && b.nil?
523
- SR_transitions().map { |t| t.name || t }
524
- end
525
-
526
- # ==== Assignment (A) transitions
527
-
528
- # Without arguments or block, it returns simply a list of assignment
529
- # transitions. Otherwise, it returns a hash, whose keys are the A
530
- # transitions, and whose values are governed by the supplied arguments
531
- # (either an explicit collection of values, or a message to #send
532
- # to self to obtain such collection).
533
- #
534
- def A_transitions *aa, &b
535
- return zip_to_hash A_transitions(), *aa, &b unless aa.empty? && b.nil?
536
- sift_from_net :A_transitions
537
- end
538
- alias assignment_transitions A_transitions
539
-
540
- # Like #A_transitions, except that transition names are used instead of
541
- # instances, whenever possible.
542
- #
543
- def A_tt *aa, &b
544
- return zip_to_hash A_tt(), *aa, &b unless aa.empty? && b.nil?
545
- A_transitions().map { |t| t.name || t }
546
- end
547
- alias assignment_tt A_tt
548
-
549
- # ==== Stoichiometric transitions of any kind (S transitions)
550
-
551
- # Without arguments or block, it returns simply a list of stoichiometric
552
- # transitions. Otherwise, it returns a hash, whose keys are the S
553
- # transitions, and whose values are governed by the supplied arguments
554
- # (either an explicit collection of values, or a message to #send to
555
- # self to obtain such collection).
556
- #
557
- def S_transitions *aa, &b
558
- return zip_to_hash S_transitions(), *aa, &b unless aa.empty? && b.nil?
559
- sift_from_net :S_transitions
560
- end
561
-
562
- # Like #S_transitions, except that transition names are used instead of
563
- # instances, whenever possible.
564
- #
565
- def S_tt *aa, &b
566
- return zip_to_hash S_tt(), *aa, &b unless aa.empty? && b.nil?
567
- S_transitions().map { |t| t.name || t }
568
- end
569
-
570
- # ==== Nonstoichiometric transitions of any kind (s transitions)
571
-
572
- # Without arguments or block, it returns simply a list of
573
- # nonstoichiometric transitions. Otherwise, it returns a hash, whose
574
- # keys are the s transitions, and whose values are governed by the
575
- # supplied arguments (either an explicit collection of values, or a
576
- # message to #send to self to obtain such collection).
577
- #
578
- def s_transitions *aa, &b
579
- return zip_to_hash s_transitions, *aa, &b unless aa.empty? && b.nil?
580
- sift_from_net :s_transitions
581
- end
582
-
583
- # Like #s_transitions, except that transition names are used instead of
584
- # instances, whenever possible.
585
- #
586
- def s_tt *aa, &b
587
- return zip_to_hash s_tt, *aa, &b unless aa.empty? && b.nil?
588
- s_transitions.map { |t| t.name || t }
589
- end
590
-
591
- # ==== Transitions with rate (R transitions), otherwise of any kind
592
-
593
- # Without arguments or block, it returns simply a list of transitions
594
- # with rate. Otherwise, it returns a hash, whose keys are the R
595
- # transitions, and whose values are governed by the supplied arguments
596
- # (either an explicit collection of values, or a message to #send to
597
- # self to obtain such collection).
598
- #
599
- def R_transitions *aa, &b
600
- return zip_to_hash R_transitions(), *aa, &b unless aa.empty? && b.nil?
601
- sift_from_net :R_transitions
602
- end
603
-
604
- # Like #s_transitions, except that transition names are used instead of
605
- # instances, whenever possible.
606
- #
607
- def R_tt *aa, &b
608
- return zip_to_hash R_tt(), *aa, &b unless aa.empty? && b.nil?
609
- R_transitions().map { |t| t.name || t }
610
- end
611
-
612
- # ==== Rateless transitions (r transitions), otherwise of any kind
613
-
614
- # Without arguments or block, it returns simply a list of rateless
615
- # transitions. Otherwise, it returns a hash, whose keys are the r
616
- # transitions, and whose values are governed by the supplied arguments
617
- # (either an explicit collection of values, or a message to #send to
618
- # self to obtain such collection).
619
- #
620
- def r_transitions *aa, &b
621
- return zip_to_hash r_transitions, *aa, &b unless aa.empty? && b.nil?
622
- sift_from_net :r_transitions
623
- end
624
-
625
- # Like #r_transitions, except that transition names are used instead of
626
- # instances, whenever possible.
627
- #
628
- def r_tt *aa, &b
629
- return zip_to_hash r_tt, *aa, &b unless aa.empty? && b.nil?
630
- r_transitions.map { |t| t.name || t }
631
- end
632
-
633
- # === Methods presenting other simulation assets
634
-
635
- # ==== Regarding ts transitions
636
- #
637
- # (Their closures supply directly Δ codomain.)
49
+ attr_reader :S_SR
638
50
 
639
51
  # Exposing Δ state closures for ts transitions.
640
52
  #
641
53
  attr_reader :Δ_closures_for_tsa
642
54
 
643
- # Delta state contribution if timed nonstoichiometric non-assignment (tsa)
644
- # transitions fire once. The closures are called in their order, but the state
645
- # update is not performed between the calls (they fire simultaneously).
646
- #
647
55
  # Note: 'a' in 'tsa' is needed because A (assignment) transitions can also be
648
56
  # regarded as a special kind of ts transitions, while they obviously do not
649
57
  # act through Δ state, but rather directly enforce marking of their codomain.
@@ -652,29 +60,16 @@ class YPetri::Simulation
652
60
  Δ_closures_for_tsa.map( &:call ).reduce( @zero_ᴍ, :+ )
653
61
  end
654
62
 
655
- # ==== Regarding Tsr transitions
656
- #
657
- # (Their closures do take Δt as argument, but do not expose their ∂,
658
- # and they might not even have one.)
659
-
660
63
  # Exposing Δ state closures for Tsr transitions.
661
64
  #
662
65
  attr_reader :Δ_closures_for_Tsr
663
66
 
664
67
  # Delta state contribution for Tsr transitions given Δt.
665
68
  #
666
- def Δ_for_Tsr( Δt )
69
+ def Δ_Tsr( Δt )
667
70
  Δ_closures_for_Tsr.map { |cl| cl.( Δt ) }.reduce( @zero_ᴍ, :+ )
668
71
  end
669
-
670
- # ==== Regarding tS transitions
671
- #
672
- # (These transitions are timeless, but stoichiometric. It means that their
673
- # closures do not output Δ state contribution directly, but instead they
674
- # output a single number, which is a transition action, and Δ state is then
675
- # computed from it by multiplying the the action vector with the
676
- # stoichiometry matrix.
677
-
72
+
678
73
  # Exposing action closures for tS transitions.
679
74
  #
680
75
  attr_reader :action_closures_for_tS
@@ -686,7 +81,7 @@ class YPetri::Simulation
686
81
  def action_vector_for_tS
687
82
  Matrix.column_vector action_closures_for_tS.map( &:call )
688
83
  end
689
- alias α_for_tS action_vector_for_tS
84
+ alias ᴀ_tS action_vector_for_tS
690
85
 
691
86
  # Action vector if tS transitions fire once, like the previous method.
692
87
  # But by calling this method, the caller asserts that all timeless
@@ -698,20 +93,14 @@ class YPetri::Simulation
698
93
  "transitions! Consider using #action_vector_for_tS."
699
94
  end
700
95
  alias action_vector_for_t action_vector_for_timeless_transitions
701
- alias α_for_t action_vector_for_timeless_transitions
96
+ alias ᴀ_t action_vector_for_timeless_transitions
702
97
 
703
98
  # Δ state contribution for tS transitions.
704
99
  #
705
100
  def Δ_if_tS_fire_once
706
- S_for_tS() * action_vector_for_tS
101
+ S_tS() * action_vector_for_tS
707
102
  end
708
103
 
709
- # ==== Regarding TSr transitions
710
- #
711
- # (Same as Tsr, but stoichiometric. That is, their closures do not return
712
- # Δ contribution, but transition's action, which is to be multiplied by
713
- # the its stoichiometry to obtain Δ contribution.)
714
-
715
104
  # Exposing action closures for TSr transitions.
716
105
  #
717
106
  attr_reader :action_closures_for_TSr
@@ -730,7 +119,7 @@ class YPetri::Simulation
730
119
  def action_vector_for_TSr( Δt )
731
120
  Matrix.column_vector action_closures_for_TSr.map { |c| c.( Δt ) }
732
121
  end
733
- alias α_for_TSr action_vector_for_TSr
122
+ alias ᴀ_TSr action_vector_for_TSr
734
123
 
735
124
  # Action vector for timed rateless stoichiometric transitions
736
125
  # By calling this method, the caller asserts that all timeless transitions
@@ -741,20 +130,14 @@ class YPetri::Simulation
741
130
  raise "The simulation also contains nonstoichiometric timed rateless " +
742
131
  "transitions! Consider using #action_vector_for_TSr."
743
132
  end
744
- alias α_for_Tr action_vector_for_Tr
133
+ alias ᴀ_Tr action_vector_for_Tr
745
134
 
746
- # Computes delta state for TSr transitions, given a Δt.
135
+ # State contribution of TSr transitions for the period Δt.
747
136
  #
748
- def Δ_for_TSr( Δt )
749
- S_for_TSr() * action_vector_for_TSr( Δt )
137
+ def Δ_TSr( Δt )
138
+ S_TSr() * action_vector_for_TSr( Δt )
750
139
  end
751
140
 
752
- # ==== Regarding sR transitions
753
- #
754
- # (Whether nonstoichiometric or stoichiometric, transitions with rate
755
- # explicitly provide their contribution to the the state differential,
756
- # rather than just contribution to the Δ state.)
757
-
758
141
  # Exposing rate closures for sR transitions.
759
142
  #
760
143
  attr_reader :rate_closures_for_sR
@@ -781,20 +164,11 @@ class YPetri::Simulation
781
164
  free_pp :gradient_for_sR
782
165
  end
783
166
 
784
- # While for sR transitions, state differential is what matters the most,
785
- # as a conveniece, this method for multiplying the differential by provided
786
- # Δt is added.
167
+ # First-order state contribution of sR transitions during Δt.
787
168
  #
788
- def Δ_Euler_for_sR( Δt )
169
+ def Δ_sR( Δt )
789
170
  gradient_for_sR * Δt
790
171
  end
791
- alias Δ_euler_for_sR Δ_Euler_for_sR
792
-
793
- # ==== Regarding SR_transitions
794
- #
795
- # (Whether nonstoichiometric or stoichiometric, transitions with rate
796
- # explicitly provide their contribution to the the state differential,
797
- # rather than just contribution to the Δ state.)
798
172
 
799
173
  # Exposing rate closures for SR transitions.
800
174
  #
@@ -891,7 +265,7 @@ class YPetri::Simulation
891
265
  # State differential for SR transitions.
892
266
  #
893
267
  def gradient_for_SR
894
- S_for_SR() * flux_vector_for_SR
268
+ S_SR() * flux_vector_for_SR
895
269
  end
896
270
 
897
271
  # State differential for SR transitions as a hash { place_name: ∂ / ∂ᴛ }.
@@ -900,44 +274,30 @@ class YPetri::Simulation
900
274
  free_pp :gradient_for_SR
901
275
  end
902
276
 
903
- # Action vector for SR transitions under an assumption of making an Euler
904
- # step, whose size is given by the Δt argument.
277
+ # First-order action vector for SR transitions for the time period Δt.
905
278
  #
906
- def Euler_action_vector_for_SR( Δt )
279
+ def first_order_action_vector_for_SR( Δt )
907
280
  flux_vector_for_SR * Δt
908
281
  end
909
- alias euler_action_vector_for_SR Euler_action_vector_for_SR
910
- alias Euler_α_for_SR Euler_action_vector_for_SR
911
- alias euler_α_for_SR Euler_action_vector_for_SR
282
+ alias ᴀ_SR first_order_action_vector_for_SR
912
283
 
913
- # Euler action fro SR transitions as an array.
284
+ # First-order action (as array) for SR for the time period Δt.
914
285
  #
915
- def Euler_action_for_SR( Δt )
916
- Euler_action_vector_for_SR( Δt ).column( 0 ).to_a
286
+ def first_order_action_for_SR( Δt )
287
+ first_order_action_vector_for_SR( Δt ).column( 0 ).to_a
917
288
  end
918
- alias euler_action_for_SR Euler_action_for_SR
919
289
 
920
- # Convenience calculator of Δ state for SR transitions, assuming a single
921
- # Euler step with Δt given as argument.
290
+ # First-order state contribution of SR transitions during Δt.
922
291
  #
923
- def Δ_Euler_for_SR( Δt )
292
+ def Δ_SR( Δt )
924
293
  gradient_for_SR * Δt
925
294
  end
926
- alias Δ_euler_for_SR Δ_Euler_for_SR
927
295
 
928
- # Δ state for SR transitions, assuming Euler step with Δt as the argument,
929
- # returned as an array.
296
+ # First-order state contribution for SR transitions during Δt (as array).
930
297
  #
931
- def Δ_Euler_array_for_SR( Δt )
298
+ def Δ_array_for_SR( Δt )
932
299
  Δ_Euler_for_SR( Δt ).column( 0 ).to_a
933
300
  end
934
- alias Δ_euler_array_for_SR Δ_Euler_array_for_SR
935
-
936
-
937
- # ==== Regarding A transitions
938
- #
939
- # (Assignment transitions directly replace the values in their codomain
940
- # places with their results.)
941
301
 
942
302
  # Exposing assignment closures for A transitions.
943
303
  #
@@ -977,6 +337,150 @@ class YPetri::Simulation
977
337
  # TODO: Assignment action to a clamped place should result in a warning.
978
338
  end
979
339
 
340
+ # Correspondence matrix free places => all places.
341
+ #
342
+ attr_reader :F2A
343
+
344
+ # Correspondence matrix clamped places => all places.
345
+ #
346
+ attr_reader :C2A
347
+
348
+ # Simulation settings.
349
+ #
350
+ def settings; {} end
351
+ alias :simulation_settings :settings
352
+
353
+ def recording_csv_string
354
+ CSV.generate do |csv|
355
+ @recording.keys.zip( @recording.values ).map{ |a, b| [ a ] + b.to_a }
356
+ .each{ |line| csv << line }
357
+ end
358
+ end
359
+
360
+ # Currently, simulation is largely immutable. Net, initial marking, clamps
361
+ # and simulation settings are set upon initialization, whereupon the instance
362
+ # forms their "mental image", which remains immune to any subsequent changes
363
+ # to the original objects. Required parameters are :net, :marking_clamps, and
364
+ # :initial_marking. Optional is :method (simulation method), and :guarded
365
+ # (true/false, whether the simulation guards the transition function results).
366
+ # Guard conditions can be either implicit (guarding against negative values
367
+ # and against type changes in by transition action), or explicitly associated
368
+ # with either places, or transition function results.
369
+ #
370
+ # A simulation distinguishes between free and clamped places. For free
371
+ # places, initial marking has to be specified. For clamped places, marking
372
+ # clamps have to be specified. Both come as hashes:
373
+ #
374
+ # In addition to the arguments required by the regular simulation
375
+ # constructor, timed simulation constructor also expects :step_size
376
+ # (alias :step), :sampling_period (alias :sampling), and :target_time
377
+ # named arguments.
378
+ #
379
+ def initialize( method: default_simulation_method,
380
+ guarded: false,
381
+ marking_clamps: {},
382
+ initial_marking: {},
383
+ **nn )
384
+ puts "constructing a simulation" if YPetri::DEBUG
385
+
386
+ @method, @guarded = method, guarded
387
+ @net = nn.fetch( :net )
388
+ @places, @transitions = @net.places.dup, @net.transitions.dup
389
+ self.singleton_class.class_exec {
390
+ define_method :Place do net.send :Place end
391
+ define_method :Transition do net.send :Transition end
392
+ define_method :Net do net.send :Net end
393
+ private :Place, :Transition, :Net
394
+ }; puts "setup of :net mental image complete" if YPetri::DEBUG
395
+
396
+ @marking_clamps = marking_clamps.with_keys { |k| place k }
397
+ @initial_marking = initial_marking.with_keys { |k| place k }
398
+ # Enforce that keys in the hashes must be unique:
399
+ @marking_clamps.keys.aT_equal @marking_clamps.keys.uniq
400
+ @initial_marking.keys.aT_equal @initial_marking.keys.uniq
401
+ puts "setup of clamps and initial marking done" if YPetri::DEBUG
402
+
403
+ places.each { |pl| # each place must have either clamp, or initial marking
404
+ pl.aT "place #{pl}", "have either clamp or initial marking" do |pl|
405
+ ( @marking_clamps.keys + @initial_marking.keys ).include? pl
406
+ end
407
+ }; puts "clamp || initial marking test passed" if YPetri::DEBUG
408
+
409
+ # @F2A * ᴍ (marking vector of free places) maps ᴍ to all places.
410
+ @F2A = Matrix.correspondence_matrix( free_places, places )
411
+ # @C2A * marking_vector_of_clamped_places maps it to all places.
412
+ @C2A = Matrix.correspondence_matrix( clamped_places, places )
413
+ puts "correspondence matrices set up" if YPetri::DEBUG
414
+
415
+ # Stoichiometry matrices:
416
+ @S_tS = S_for tS_transitions()
417
+ @S_SR = S_for SR_transitions()
418
+ @S_TSr = S_for TSr_transitions()
419
+ puts "stoichiometry matrices set up" if YPetri::DEBUG
420
+
421
+ # Other assets:
422
+ @Δ_closures_for_tsa = create_Δ_closures_for_tsa
423
+ @Δ_closures_for_Tsr = create_Δ_closures_for_Tsr
424
+ @action_closures_for_tS = create_action_closures_for_tS
425
+ @action_closures_for_TSr = create_action_closures_for_TSr
426
+ @rate_closures_for_sR = create_rate_closures_for_sR
427
+ @rate_closures_for_SR = create_rate_closures_for_SR
428
+ @assignment_closures_for_A = create_assignment_closures_for_A
429
+ @zero_ᴍ = compute_initial_marking_vector_of_free_places.map { |e| e * 0 }
430
+ @zero_gradient = @zero_ᴍ.dup
431
+ puts "other assets set up" if YPetri::DEBUG
432
+
433
+ @timed = if nn.has?( :time ) || nn.has?( :step ) || nn.has?( :sampling )
434
+ extend Timed
435
+ true
436
+ else false end
437
+
438
+ if timed? then # we have to set up all the expected variables
439
+ if nn[:time] then # time range given
440
+ time_range = nn[:time]
441
+ @initial_time, @target_time = time_range.begin, time_range.end
442
+ @step_size = nn[:step] || target_time / target_time.to_f
443
+ @sampling_period = nn[:sampling] || step_size
444
+ else
445
+ anything = nn[:step] || nn[:sampling]
446
+ @initial_time, @target_time = anything * 0, anything * Float::INFINITY
447
+ @step_size = nn[:step] || anything / anything.to_f
448
+ @sampling_period = nn[:sampling] || step_size
449
+ end
450
+ end
451
+
452
+ puts "timedness of the simulation decided" if YPetri::DEBUG
453
+
454
+ reset!
455
+ end
456
+
457
+ # Returns a new instance of the system simulation at a specified state, with
458
+ # same simulation settings. This state (:marking argument) can be specified
459
+ # either as marking vector for free or all places, marking array for free or
460
+ # all places, or marking hash. If vector or array is given, its size must
461
+ # correspond to the number of either free, or all places. If hash is given,
462
+ # it is not necessary to specify marking of every place – marking of those
463
+ # left out will be left same as in the current state.
464
+ #
465
+ def at( marking: marking, **nn )
466
+ err_msg = "Size of supplied marking must match either the number of " +
467
+ "free places, or the number of all places!"
468
+ update_method = case marking
469
+ when Hash then :update_marking_from_a_hash
470
+ when Matrix then
471
+ case marking.column_to_a.size
472
+ when places.size then :set_marking_vector
473
+ when free_places.size then :set_ᴍ
474
+ else fail TypeError, err_msg end
475
+ else # marking assumed to be an array
476
+ case marking.size
477
+ when places.size then :set_marking
478
+ when free_places.size then :set_m
479
+ else fail TypeError, err_msg end
480
+ end
481
+ return dup( **nn ).send( update_method, marking )
482
+ end
483
+
980
484
  # ==== Sparse stoichiometry vectors for transitions
981
485
 
982
486
  # For the transition specified by the argument, this method returns the
@@ -1001,14 +505,6 @@ class YPetri::Simulation
1001
505
  Matrix.column_vector( instance.stoichiometry )
1002
506
  end
1003
507
 
1004
- # Correspondence matrix free places => all places.
1005
- #
1006
- attr_reader :F2A
1007
-
1008
- # Correspondence matrix clamped places => all places.
1009
- #
1010
- attr_reader :C2A
1011
-
1012
508
  # Produces the inspect string of the transition.
1013
509
  #
1014
510
  def inspect
@@ -1023,34 +519,6 @@ class YPetri::Simulation
1023
519
 
1024
520
  private
1025
521
 
1026
- # This helper method takes a collection, a variable number of other arguments
1027
- # and an optional block, and returns a hash whose keys are the collection
1028
- # members, and whose values are given by the supplied othe arguments and/or
1029
- # block in the following way: If there is no additional argument, but a block
1030
- # is supplied, this is applied to the collection. If there is exactly one
1031
- # other argument, and it is also a collection, it is used as values.
1032
- # Otherwise, these other arguments are treated as a message to be sent to
1033
- # self (via #send), expecting it to return a collection to be used as hash
1034
- # values. Optional block (which is always assumed to be unary) can be used
1035
- # to additionally modify the second collection.
1036
- #
1037
- def zip_to_hash collection, *args, &block
1038
- sz = args.size
1039
- values = if sz == 0 then collection
1040
- elsif sz == 1 && args[0].respond_to?( :each ) then args[0]
1041
- else send *args end
1042
- Hash[ collection.zip( block ? values.map( &block ) : values ) ]
1043
- end
1044
-
1045
- # Chicken approach towards ensuring that transitions in question come in
1046
- # the same order as in @transitions local variable. Takes a symbol as the
1047
- # argument (:SR, :TSr, :sr etc.)
1048
- #
1049
- def sift_from_net type_of_transitions
1050
- from_net = net.send type_of_transitions
1051
- @transitions.select { |t| from_net.include? t }
1052
- end
1053
-
1054
522
  # Resets the simulation
1055
523
  #
1056
524
  def reset!
@@ -1334,8 +802,8 @@ end
1334
802
 
1335
803
  # Duplicate creation.
1336
804
  #
1337
- def dup( **oo )
1338
- self.class.new( oo.reverse_merge!( { method: @method,
805
+ def dup( **nn )
806
+ self.class.new( nn.reverse_merge!( { method: @method,
1339
807
  guarded: @guarded,
1340
808
  net: @net,
1341
809
  marking_clamps: @marking_clamps,