y_petri 2.0.15 → 2.1.3

Sign up to get free protection for your applications and to get access to all the features.
Files changed (117) hide show
  1. checksums.yaml +4 -4
  2. data/lib/y_petri/{manipulator → agent}/hash_key_pointer.rb +2 -2
  3. data/lib/y_petri/agent/petri_net_related.rb +115 -0
  4. data/lib/y_petri/{manipulator → agent}/selection.rb +2 -1
  5. data/lib/y_petri/{manipulator/simulation_related_methods.rb → agent/simulation_related.rb} +93 -110
  6. data/lib/y_petri/agent.rb +22 -0
  7. data/lib/y_petri/core/timed/euler.rb +20 -0
  8. data/lib/y_petri/core/timed/pseudo_euler.rb +31 -0
  9. data/lib/y_petri/core/timed/quasi_euler.rb +23 -0
  10. data/lib/y_petri/core/timed.rb +70 -0
  11. data/lib/y_petri/core/timeless/pseudo_euler.rb +20 -0
  12. data/lib/y_petri/core/timeless.rb +12 -0
  13. data/lib/y_petri/core.rb +100 -0
  14. data/lib/y_petri/dsl.rb +66 -0
  15. data/lib/y_petri/fixed_assets.rb +7 -0
  16. data/lib/y_petri/net/element_access.rb +239 -0
  17. data/lib/y_petri/net/state/feature/delta.rb +88 -0
  18. data/lib/y_petri/net/state/feature/firing.rb +57 -0
  19. data/lib/y_petri/net/state/feature/flux.rb +58 -0
  20. data/lib/y_petri/net/state/feature/gradient.rb +75 -0
  21. data/lib/y_petri/net/state/feature/marking.rb +62 -0
  22. data/lib/y_petri/net/state/feature.rb +79 -0
  23. data/lib/y_petri/net/state/features/dataset.rb +135 -0
  24. data/lib/y_petri/net/state/features/record.rb +50 -0
  25. data/lib/y_petri/net/state/features.rb +126 -0
  26. data/lib/y_petri/net/state.rb +121 -0
  27. data/lib/y_petri/net/timed.rb +8 -0
  28. data/lib/y_petri/net/visualization.rb +3 -3
  29. data/lib/y_petri/net.rb +73 -77
  30. data/lib/y_petri/place.rb +8 -3
  31. data/lib/y_petri/simulation/dependency.rb +107 -0
  32. data/lib/y_petri/simulation/element_representation.rb +20 -0
  33. data/lib/y_petri/simulation/elements/access.rb +57 -0
  34. data/lib/y_petri/simulation/elements.rb +45 -0
  35. data/lib/y_petri/simulation/feature_set.rb +21 -0
  36. data/lib/y_petri/simulation/initial_marking/access.rb +55 -0
  37. data/lib/y_petri/simulation/initial_marking.rb +15 -0
  38. data/lib/y_petri/simulation/marking_clamps/access.rb +34 -0
  39. data/lib/y_petri/simulation/marking_clamps.rb +18 -0
  40. data/lib/y_petri/simulation/marking_vector/access.rb +106 -0
  41. data/lib/y_petri/simulation/marking_vector.rb +156 -0
  42. data/lib/y_petri/simulation/matrix.rb +64 -0
  43. data/lib/y_petri/simulation/place_mapping.rb +62 -0
  44. data/lib/y_petri/simulation/place_representation.rb +74 -0
  45. data/lib/y_petri/simulation/places/access.rb +121 -0
  46. data/lib/y_petri/simulation/places/clamped.rb +8 -0
  47. data/lib/y_petri/simulation/places/free.rb +8 -0
  48. data/lib/y_petri/simulation/places/types.rb +25 -0
  49. data/lib/y_petri/simulation/places.rb +41 -0
  50. data/lib/y_petri/simulation/recorder.rb +54 -0
  51. data/lib/y_petri/simulation/timed/recorder.rb +53 -0
  52. data/lib/y_petri/simulation/timed.rb +161 -261
  53. data/lib/y_petri/simulation/timeless/recorder.rb +25 -0
  54. data/lib/y_petri/simulation/timeless.rb +35 -0
  55. data/lib/y_petri/simulation/transition_representation/A.rb +58 -0
  56. data/lib/y_petri/simulation/transition_representation/S.rb +45 -0
  57. data/lib/y_petri/simulation/transition_representation/T.rb +80 -0
  58. data/lib/y_petri/simulation/transition_representation/TS.rb +46 -0
  59. data/lib/y_petri/simulation/transition_representation/Ts.rb +32 -0
  60. data/lib/y_petri/simulation/transition_representation/a.rb +30 -0
  61. data/lib/y_petri/simulation/transition_representation/s.rb +29 -0
  62. data/lib/y_petri/simulation/transition_representation/t.rb +37 -0
  63. data/lib/y_petri/simulation/transition_representation/tS.rb +38 -0
  64. data/lib/y_petri/simulation/transition_representation/ts.rb +32 -0
  65. data/lib/y_petri/simulation/transition_representation/types.rb +62 -0
  66. data/lib/y_petri/simulation/transition_representation.rb +79 -0
  67. data/lib/y_petri/simulation/transitions/A.rb +40 -0
  68. data/lib/y_petri/simulation/transitions/S.rb +24 -0
  69. data/lib/y_petri/simulation/transitions/T.rb +34 -0
  70. data/lib/y_petri/simulation/transitions/TS.rb +57 -0
  71. data/lib/y_petri/simulation/transitions/Ts.rb +60 -0
  72. data/lib/y_petri/simulation/transitions/a.rb +8 -0
  73. data/lib/y_petri/simulation/transitions/access.rb +186 -0
  74. data/lib/y_petri/simulation/transitions/s.rb +9 -0
  75. data/lib/y_petri/simulation/transitions/t.rb +22 -0
  76. data/lib/y_petri/simulation/transitions/tS.rb +55 -0
  77. data/lib/y_petri/simulation/transitions/ts.rb +58 -0
  78. data/lib/y_petri/simulation/transitions/types.rb +98 -0
  79. data/lib/y_petri/simulation/transitions.rb +21 -0
  80. data/lib/y_petri/simulation.rb +176 -781
  81. data/lib/y_petri/transition/assignment.rb +7 -5
  82. data/lib/y_petri/transition/construction.rb +119 -187
  83. data/lib/y_petri/transition/init.rb +311 -0
  84. data/lib/y_petri/transition/ordinary_timeless.rb +8 -6
  85. data/lib/y_petri/transition/timed.rb +11 -18
  86. data/lib/y_petri/transition.rb +104 -132
  87. data/lib/y_petri/version.rb +1 -1
  88. data/lib/y_petri/world/dependency.rb +40 -0
  89. data/lib/y_petri/world/petri_net_related.rb +61 -0
  90. data/lib/y_petri/{workspace/simulation_related_methods.rb → world/simulation_related.rb} +42 -49
  91. data/lib/y_petri/world.rb +27 -0
  92. data/lib/y_petri.rb +47 -99
  93. data/test/{manipulator_test.rb → agent_test.rb} +19 -17
  94. data/{lib/y_petri → test/examples}/demonstrator.rb +0 -0
  95. data/{lib/y_petri → test/examples}/demonstrator_2.rb +1 -0
  96. data/{lib/y_petri → test/examples}/demonstrator_3.rb +0 -0
  97. data/{lib/y_petri → test/examples}/demonstrator_4.rb +0 -0
  98. data/test/examples/example_2.rb +16 -0
  99. data/test/{manual_examples.rb → examples/manual_examples.rb} +0 -0
  100. data/test/net_test.rb +126 -121
  101. data/test/place_test.rb +1 -1
  102. data/test/sim_test +565 -0
  103. data/test/simulation_test.rb +338 -264
  104. data/test/transition_test.rb +77 -174
  105. data/test/world_mock.rb +12 -0
  106. data/test/{workspace_test.rb → world_test.rb} +19 -20
  107. data/test/y_petri_test.rb +4 -5
  108. metadata +101 -26
  109. data/lib/y_petri/dependency_injection.rb +0 -45
  110. data/lib/y_petri/manipulator/petri_net_related_methods.rb +0 -74
  111. data/lib/y_petri/manipulator.rb +0 -20
  112. data/lib/y_petri/net/selections.rb +0 -209
  113. data/lib/y_petri/simulation/collections.rb +0 -460
  114. data/lib/y_petri/workspace/parametrized_subclassing.rb +0 -22
  115. data/lib/y_petri/workspace/petri_net_related_methods.rb +0 -88
  116. data/lib/y_petri/workspace.rb +0 -16
  117. data/test/timed_simulation_test.rb +0 -153
@@ -0,0 +1,311 @@
1
+ # encoding: utf-8
2
+
3
+ # Constructor syntax aspect of a transition. Large part of the functionality
4
+ # of the Transition class is the convenient constructor syntax.
5
+ #
6
+ class YPetri::Transition
7
+ # Transition class represents many different kinds of Petri net transitions.
8
+ # It makes the constructor syntax a bit more polymorphic. The type of the
9
+ # transition to construct is mostly inferred from the constructor arguments.
10
+ #
11
+ # Mandatorily, the constructor will always need a way to determine the domain
12
+ # (upstream arcs) and codomain (downstream arcs) of the transition. Also, the
13
+ # constructor must have a way to determine the transition's action. This is
14
+ # best explained by examples -- let us have 3 places A, B, C, for whe we will
15
+ # create different kinds of transitions:
16
+ #
17
+ #
18
+ # ==== TS (timed stoichiometric)
19
+ #
20
+ # Rate closure and stoichiometry has to be supplied. Rate closure arity should
21
+ # correspond to the domain size. Return arity should be 1 (to be multiplied by
22
+ # the stoichiometry vector, as in all other stoichiometric transitions).
23
+ #
24
+ # Transition.new stoichiometry: { A: -1, B: 1 },
25
+ # rate: -> a { a * 0.5 }
26
+ #
27
+ #
28
+ # ==== Ts (timed nonstoichiometric)
29
+ #
30
+ # Rate closure has to be supplied, whose arity should match the domain, and
31
+ # output arity codomain.
32
+ #
33
+ # ==== tS (timeless stoichiometric)
34
+ #
35
+ # Stoichiometry has to be supplied, action closure is optional. If supplied,
36
+ # its return arity should be 1 (to be multiplied by the stoichiometry vector).
37
+ #
38
+ # ==== ts transitions (timeless nonstoichiometric)
39
+ #
40
+ # Action closure is expected with return arity equal to the codomain size:
41
+ #
42
+ # Transition.new upstream_arcs: [A, C], downstream_arcs: [A, B],
43
+ # action_closure: proc { |m, x|
44
+ # if x > 0 then [-(m / 2), (m / 2)]
45
+ # else [1, 0] end
46
+ # }
47
+ #
48
+ def initialize *args, &block
49
+ check_in_arguments *args, &block # the big job
50
+ extend timed? ? Timed : ( assignment? ? Assignment : OrdinaryTimeless )
51
+ inform_upstream_places # that they have been connected
52
+ inform_downstream_places # that they have been connected
53
+ uncock # transitions initialize uncocked
54
+ end
55
+
56
+ private
57
+
58
+ # Checking in the arguments supplied to #initialize looks like a big job.
59
+ # I won't contest to that, but let us not, that it is basically nothing
60
+ # else then defining the duck type of the input argument collection.
61
+ # TypeError is therefore raised if invalid collection has been supplied.
62
+ #
63
+ def check_in_arguments **nn, &block
64
+ nn.update( action: block ) if block_given?
65
+ nn.may_have :domain, syn!: [ :domain_arcs, :domain_places,
66
+ :upstream, :upstream_arcs, :upstream_places ]
67
+ nn.may_have :codomain, syn!: [ :codomain_arcs, :codomain_places,
68
+ :downstream,
69
+ :downstream_arcs, :downstream_places,
70
+ :action_arcs ]
71
+ nn.may_have :rate, syn!: [ :rate_closure, :propensity,
72
+ :propensity_closure ]
73
+ nn.may_have :action, syn!: :action_closure
74
+ nn.may_have :stoichiometry, syn!: [ :stoichio, :s ]
75
+ nn.may_have :domain_guard
76
+ nn.may_have :codomain_guard
77
+
78
+ # If the rate was given, the transition is timed:
79
+ @timed = nn.has? :rate
80
+
81
+ # If stoichiometry was given, the transition is stoichiometric:
82
+ @stoichiometric = nn.has? :stoichiometry
83
+
84
+ # Downstream description involves the codomain, and the stochiometry
85
+ # (for stoichiometric transitions only):
86
+ if stoichiometric? then
87
+ @codomain, @stoichiometry = __downstream_for_S__( **nn )
88
+ else
89
+ @codomain = __downstream_for_s__( **nn )
90
+ end
91
+
92
+ # Check in the domain first, :missing symbol may be returned if the user
93
+ # has not supplied the domaing (the constructor will attempt to guessf it
94
+ # automatically).
95
+ @domain = __domain__( **nn )
96
+
97
+ # Upstream description involves the domain and the rate/action closure.
98
+ # Also, :missing domain is taken care of here.
99
+ if timed? then
100
+ @domain, @rate_closure, @functional = __upstream_for_T__( **nn )
101
+ else
102
+ @domain, @action_closure, @functional = __upstream_for_t__( **nn )
103
+ end
104
+
105
+ # Optional assignment action:
106
+ @assignment_action = __assignment_action__( **nn )
107
+
108
+ # Optional type guards for domain / codomain:
109
+ @domain_guard, @codomain_guard = __guards__( **nn )
110
+ end
111
+
112
+ # Validates that the supplied collection consists only of places of
113
+ # correct type. Second optional argument customizes the error message.
114
+ #
115
+ def sanitize_place_collection place_collection, what_is_collection=nil
116
+ c = what_is_collection ? what_is_collection.capitalize : "Collection"
117
+ Array( place_collection ).map do |pl_id|
118
+ begin
119
+ place( pl_id )
120
+ rescue NameError
121
+ raise TypeError, "#{c} member #{pl_id} does not specify a valid place!"
122
+ end
123
+ end.aT what_is_collection, "not contain duplicate places" do |coll|
124
+ coll == coll.uniq
125
+ end
126
+ end
127
+
128
+ # Private method, part of #initialize argument checking-in.
129
+ #
130
+ def __domain__( **nn )
131
+ if nn.has? :domain then
132
+ sanitize_place_collection( nn[:domain], "supplied domain" )
133
+ else
134
+ if stoichiometric? then
135
+ # take arcs with non-positive coefficients
136
+ Hash[ @codomain.zip @stoichiometry ].delete_if { |_, c| c > 0 }.keys
137
+ else
138
+ :missing # may mean empty domain, or domain == codomain
139
+ end
140
+ end
141
+ end
142
+
143
+ # Private method, part of the init process for timed transitions. Also takes
144
+ # care for :missing domain, if :missing.
145
+ #
146
+ def __upstream_for_T__( **nn )
147
+ dom = domain # this method may modify domain
148
+ fail ArgumentError, "Rate and action collision!" if nn.has? :action
149
+ # Let's figure the rate closure now.
150
+ λ = nn[:rate]
151
+ if λ.is_a? Proc then
152
+ if dom == :missing then
153
+ dom = λ.arity == 0 ? [] : codomain
154
+ else
155
+ msg = "Rate closure arity (#{λ.arity}) > domain (#{dom.size})!"
156
+ fail TypeError, msg if λ.arity.abs > dom.size
157
+ end
158
+ else # not a Proc, must guess user's intent
159
+ λ = if stoichiometric? then # standard mass action
160
+ msg = "With numeric rate, domain must not be given!"
161
+ fail TypeError, msg if nn.has? :domain
162
+ __standard_mass_action__( λ )
163
+ else # constant closure
164
+ msg = "With numeric rate and no stoichio., codomain size must be 1!"
165
+ fail TypeError, msg unless codomain.size == 1
166
+ lambda { λ }.tap do
167
+ if dom == :missing then
168
+ dom = [] # Missing domain is natural here
169
+ else # but should it was supplied explicitly, it must be empty.
170
+ msg = "Rate is a number, but domain is non-empty!"
171
+ fail TypeError, msg unless domain.empty? if nn.has? :domain
172
+ end
173
+ end
174
+ end
175
+ end
176
+ dom.aT_is_a Array
177
+ λ.aT_is_a Proc
178
+ return dom, λ, true # true here means "functional?", always true for T
179
+ end
180
+
181
+ # Private method, part of the init process when :rate is not given. Also
182
+ # takes care for missing domain (@domain == :missing).
183
+ #
184
+ def __upstream_for_t__( **nn )
185
+ dom = domain # this method may modify domain
186
+ funct = true # "functional?"
187
+ # Was action given explicitly?
188
+ if nn.has? :action then
189
+ λ = nn[:action].aT_is_a Proc, "supplied action named argument"
190
+ # Time to worry about the domain_missing, guess the user's intention:
191
+ if dom == :missing then
192
+ dom = λ.arity == 0 ? [] : codomain
193
+ else
194
+ msg = "Action closure arity (#{λ.arity}) > domain (#{dom.size})!"
195
+ fail TypeError, msg if λ.arity.abs > dom.size
196
+ end
197
+ else # "functionless"
198
+ funct = false
199
+ λ = proc { 1 }
200
+ msg = "Stoichiometry is compulsory, if no rate/action was supplied!"
201
+ fail ArgumentError, msg unless stoichiometric?
202
+ dom = [] # in any case, the domain is empty
203
+ end
204
+ return dom, λ, funct
205
+ end
206
+
207
+ # Default rate closure for SR transitions whose rate is hinted as a number.
208
+ #
209
+ def __standard_mass_action__( num )
210
+ # assume standard mass-action law
211
+ nonpositive_coeffs = stoichiometry.select { |coeff| coeff <= 0 }
212
+ # the closure takes markings of the domain as its arguments
213
+ -> *markings do
214
+ nonpositive_coeffs.size.times.reduce num do |acc, i|
215
+ marking, coeff = markings[ i ], nonpositive_coeffs[ i ]
216
+ # Stoichiometry coefficients equal to zero are taken to indicate
217
+ # plain factors, assuming that if these places were not involved
218
+ # in the transition at all, the user would not be mentioning them.
219
+ case coeff
220
+ when 0, -1 then marking * acc
221
+ else marking ** -coeff end
222
+ end
223
+ end
224
+ end
225
+
226
+ # Private method, checking in downstream specification from the argument
227
+ # field for stoichiometric transition.
228
+ #
229
+ def __downstream_for_S__( **oo )
230
+ codomain, stoichio =
231
+ case oo[:stoichiometry]
232
+ when Hash then
233
+ # contains pairs { codomain place => stoichiometry coefficient }
234
+ msg = "With hash-type stoichiometry, :codomain must not be given!"
235
+ fail ArgumentError, msg if oo.has? :codomain
236
+ oo[:stoichiometry].each_with_object [[], []] do |(cd_pl, coeff), memo|
237
+ memo[0] << cd_pl
238
+ memo[1] << coeff
239
+ end
240
+ else
241
+ # array of stoichiometry coefficients
242
+ msg = "With array-type stoichiometry, :codomain must be given!"
243
+ fail ArgumentError unless oo.has? :codomain
244
+ [ oo[:codomain], Array( oo[:stoichiometry] ) ]
245
+ end
246
+ # enforce that stoichiometry is a collection of numbers
247
+ return sanitize_place_collection( codomain, "supplied codomain" ),
248
+ stoichio.aT_all_numeric( "supplied stoichiometry" )
249
+ end
250
+
251
+ # Private method, checking in downstream specification from the argument
252
+ # field for nonstoichiometric transition.
253
+ #
254
+ def __downstream_for_s__( **oo )
255
+ # codomain must be explicitly given - no way around it:
256
+ fail ArgumentError, "For non-stoichiometric transitions, :codomain " +
257
+ "argument is compulsory." unless oo.has? :codomain
258
+ return sanitize_place_collection( oo[:codomain], "supplied codomain" )
259
+ end
260
+
261
+ # Private method, part of #initialize argument checking-in.
262
+ #
263
+ def __assignment_action__( **oo )
264
+ if oo.has? :assignment_action, syn!: [ :assignment, :assign, :A ] then
265
+ if timed? then
266
+ false.tap do
267
+ msg = "Timed transitions may not have assignment action!"
268
+ raise TypeError, msg if oo[:assignment_action]
269
+ end
270
+ else oo[:assignment_action] end # only timeless transitions are eligible
271
+ else false end # the default value
272
+ end
273
+
274
+ # Private method, part of #initialize argument checking-in
275
+ #
276
+ def __guards__( **oo )
277
+ if oo.has? :domain_guard then
278
+ oo[:domain_guard].aT_is_a Proc, "supplied domain guard"
279
+ else
280
+ place_guards = domain_places.map &:guard
281
+ -> dm do # constructing the default domain guard
282
+ fails = [domain, dm, place_guards].transpose.map { |pl, m, guard|
283
+ [ pl, m, begin; guard.( m ); true; rescue YPetri::GuardError; false end ]
284
+ }.reduce [] do |memo, triple| memo << triple unless triple[2] end
285
+ # TODO: Watch "Exceptional Ruby" video by Avdi Grimm.
286
+ unless fails.size == 0
287
+ fail YPetri::GuardError, "Domain guard of #{self} rejects marking " +
288
+ if fails.size == 1 then
289
+ p, m, _ = fails[0]
290
+ "#{m} of place #{p.name || p.object_id}!"
291
+ else
292
+ "of the following places: %s!" %
293
+ Hash[ fails.map { |pl, m, _| [pl.name || pl.object_id, m] } ]
294
+ end
295
+ end
296
+ end
297
+ end
298
+ end
299
+
300
+ # Informs upstream places that they have been connected to this transition.
301
+ #
302
+ def inform_upstream_places
303
+ upstream_places.each { |p| p.send :register_downstream_transition, self }
304
+ end
305
+
306
+ # Informs downstream places that they are connected to this transition.
307
+ #
308
+ def inform_downstream_places
309
+ downstream_places.each { |p| p.send :register_upstream_transition, self }
310
+ end
311
+ end # class YPetri::Transition
@@ -1,4 +1,4 @@
1
- # -*- coding: utf-8 -*-
1
+ # encoding: utf-8
2
2
 
3
3
  # Mixin for timed non-assignment timeless Petri net transitions.
4
4
  #
@@ -24,11 +24,13 @@ module YPetri::Transition::OrdinaryTimeless
24
24
  # Fires the transition regardless of cocking.
25
25
  #
26
26
  def fire!
27
- try "to call #fire method" do
28
- act = note "action", is: Array( action )
29
- codomain.each_with_index do |codomain_place, i|
30
- note "adding action element no. #{i} to place #{codomain_place}"
31
- codomain_place.add( note "marking change", is: act.fetch( i ) )
27
+ consciously "call #fire method" do
28
+ act = Array( action )
29
+ msg = "Wrong output arity of the action closure of #{self}!"
30
+ fail TypeError, msg if act.size != codomain.size
31
+ codomain.each_with_index do |p, i|
32
+ note "adding action element no. #{i} to #{p}"
33
+ p.add note( "marking change", is: act.fetch( i ) )
32
34
  end
33
35
  end
34
36
  return nil
@@ -6,20 +6,11 @@ module YPetri::Transition::Timed
6
6
  # Transition's action (before validation). Requires Δt as an argument.
7
7
  #
8
8
  def action Δt
9
- if has_rate? then
10
- if stoichiometric? then
11
- rate = rate_closure.( *domain_marking )
12
- stoichiometry.map { |coeff| rate * coeff * Δt }
13
- else # assuming that rate closure return value has correct arity
14
- rate_closure.( *domain_marking ).map { |e| component * Δt }
15
- end
16
- else # timed rateless
17
- if stoichiometric? then
18
- rslt = action_closure.( Δt, *domain_marking )
19
- stoichiometry.map { |coeff| rslt * coeff }
20
- else
21
- action_closure.( Δt, *domain_marking ) # caveat result arity!
22
- end
9
+ if stoichiometric? then
10
+ rate = rate_closure.( *domain_marking )
11
+ stoichiometry.map { |coeff| rate * coeff * Δt }
12
+ else
13
+ Array( rate_closure.( *domain_marking ) ).map { |e| e * Δt }
23
14
  end
24
15
  end
25
16
 
@@ -34,11 +25,13 @@ module YPetri::Transition::Timed
34
25
  # Δt as an argument.
35
26
  #
36
27
  def fire! Δt
37
- try "to call #fire method" do
28
+ consciously "call #fire method" do
38
29
  act = note "action", is: Array( action Δt )
39
- codomain.each_with_index do |codomain_place, i|
40
- note "adding action element no. #{i} to place #{codomain_place}"
41
- codomain_place.add( note "marking change", is: act.fetch( i ) )
30
+ msg = "Wrong output arity of the action closure of #{self}!"
31
+ fail TypeError, msg if act.size != codomain.size
32
+ codomain.each_with_index do |p, i|
33
+ note "adding action element no. #{i} to #{p}"
34
+ p.add note( "marking change", is: act.fetch( i ) )
42
35
  end
43
36
  end
44
37
  return nil