y_petri 2.0.14 → 2.0.15

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