y_petri 2.3.10 → 2.3.11
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +4 -4
- data/lib/y_petri/agent/petri_net_aspect.rb +11 -21
- data/lib/y_petri/agent.rb +13 -0
- data/lib/y_petri/core/timed/runge_kutta.rb +51 -4
- data/lib/y_petri/core.rb +23 -0
- data/lib/y_petri/place.rb +14 -14
- data/lib/y_petri/simulation/dependency.rb +3 -3
- data/lib/y_petri/simulation/nodes.rb +1 -1
- data/lib/y_petri/version.rb +1 -1
- data/lib/y_petri/world.rb +11 -7
- data/lib/y_petri.rb +29 -25
- data/test/core_test.rb +26 -0
- data/test/y_petri_test.rb +1 -0
- metadata +4 -2
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA1:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: 213efc795950a5d200a6b5c10fe3150dddf4940b
|
4
|
+
data.tar.gz: 6c1a54e4526c7f19ea63dc7faad780526e2d3ee2
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: 60800f142748c833d2298583393f3790532d56bf294779d02a6e6b6991409bdc12b584bf64ec286868012d3b4d25e0a18cc8a5ac170359ab4c00ca7c703fdf52
|
7
|
+
data.tar.gz: aacfcbc2bbba5c262c5e2cab747a4fe093d047b02a65781709eec758a30b5bc9222dcd0a6874afdf1041d1a8d3f9d9e143fe0bd80f26361df6f5cd4b64e9182e
|
@@ -3,11 +3,11 @@
|
|
3
3
|
# Petri net aspect of +YPetri::Agent+.
|
4
4
|
#
|
5
5
|
module YPetri::Agent::PetriNetAspect
|
6
|
-
#
|
6
|
+
# A class representing a selection of nets.
|
7
7
|
#
|
8
8
|
NetSelection = Class.new YPetri::Agent::Selection
|
9
9
|
|
10
|
-
# Net point
|
10
|
+
# Net point.
|
11
11
|
#
|
12
12
|
attr_reader :net_point
|
13
13
|
|
@@ -15,6 +15,8 @@ module YPetri::Agent::PetriNetAspect
|
|
15
15
|
#
|
16
16
|
attr_reader :net_selection
|
17
17
|
|
18
|
+
# Standard initialization method. Takes no arguments.
|
19
|
+
#
|
18
20
|
def initialize
|
19
21
|
net_point_reset
|
20
22
|
@net_selection = NetSelection.new
|
@@ -27,37 +29,37 @@ module YPetri::Agent::PetriNetAspect
|
|
27
29
|
:nets, :places, :transitions,
|
28
30
|
to: :world
|
29
31
|
|
30
|
-
#
|
32
|
+
# Returns the name of a place identified by the argument.
|
31
33
|
#
|
32
34
|
def pl( place_id )
|
33
35
|
place( place_id ).name
|
34
36
|
end
|
35
37
|
|
36
|
-
#
|
38
|
+
# Returns the name of a transition identified by the argument.
|
37
39
|
#
|
38
40
|
def tr( transition_id )
|
39
41
|
transition( transition_id ).name
|
40
42
|
end
|
41
43
|
|
42
|
-
#
|
44
|
+
# Names of the places.
|
43
45
|
#
|
44
46
|
def pn
|
45
47
|
places.names
|
46
48
|
end
|
47
49
|
|
48
|
-
#
|
50
|
+
# Names of the transitions.
|
49
51
|
#
|
50
52
|
def tn
|
51
53
|
transitions.names
|
52
54
|
end
|
53
55
|
|
54
|
-
#
|
56
|
+
# Names of the nets.
|
55
57
|
#
|
56
58
|
def nn
|
57
59
|
nets.names
|
58
60
|
end
|
59
61
|
|
60
|
-
#
|
62
|
+
# Constructor of a place: Creates a new place in the current world.
|
61
63
|
#
|
62
64
|
def Place( *ordered_args, **named_args, &block )
|
63
65
|
fail ArgumentError, "If block is given, :guard named argument " +
|
@@ -68,24 +70,12 @@ module YPetri::Agent::PetriNetAspect
|
|
68
70
|
world.Place.send( :new, *ordered_args, **named_args, &block )
|
69
71
|
end
|
70
72
|
|
71
|
-
#
|
73
|
+
# Constructor of a transition: Creates a new transition in the current world.
|
72
74
|
#
|
73
75
|
def Transition( *ordered, **named, &block )
|
74
76
|
world.Transition.send( :new, *ordered, **named, &block )
|
75
77
|
end
|
76
78
|
|
77
|
-
# Place constructor: Creates a new place in the current world.
|
78
|
-
#
|
79
|
-
def Place( *ordered_args, **named_args, &block )
|
80
|
-
fail ArgumentError, "If block is given, :guard named argument " +
|
81
|
-
"must not be given!" if named_args.has? :guard if block
|
82
|
-
named_args.update( guard: block ) if block # use block as a guard
|
83
|
-
named_args.may_have :default_marking, syn!: :m!
|
84
|
-
named_args.may_have :marking, syn!: :m
|
85
|
-
world.Place.send( :new, *ordered_args, **named_args, &block )
|
86
|
-
end
|
87
|
-
|
88
|
-
|
89
79
|
# Assignment transition constructor: Creates a new assignment transition in
|
90
80
|
# the current world. Ordered arguments are collected as codomain. Domain key
|
91
81
|
# (+:domain) is optional. Assignment closure must be supplied in a block.
|
data/lib/y_petri/agent.rb
CHANGED
@@ -6,6 +6,19 @@ require_relative 'agent/petri_net_aspect'
|
|
6
6
|
require_relative 'agent/simulation_aspect'
|
7
7
|
|
8
8
|
# A dumb agent that represents and helps the user.
|
9
|
+
#
|
10
|
+
# An instance of this class (an agent) helps the user to interact
|
11
|
+
# with the world (YPetri::World instance) and the objects in it
|
12
|
+
# (Petri net places, transitions, nets etc.). In particular, this
|
13
|
+
# (YPetri::Agent) class is a convenient place to store various
|
14
|
+
# "shortcuts" meant to reduce the amount of typing the user has to
|
15
|
+
# do in order to construct and manipulate the world and its objects
|
16
|
+
# (such as "pl" instead of "place", "tr" instead of "transition" etc.)
|
17
|
+
# It would not be a good practice to encumber the classes where these
|
18
|
+
# methods are implemented with these semi-idiosyncratic shortcuts. This
|
19
|
+
# way, the implementation of the methods stays the concern of the mother
|
20
|
+
# classes, and Agent class is responsible for improving the ergonomy
|
21
|
+
# of their invocation.
|
9
22
|
#
|
10
23
|
class YPetri::Agent
|
11
24
|
★ PetriNetAspect # ★ means include
|
@@ -5,10 +5,57 @@
|
|
5
5
|
module YPetri::Core::Timed::RungeKutta
|
6
6
|
def delta Δt
|
7
7
|
fail NotImplementedError, "RungeKutta not implemented yet!"
|
8
|
-
|
9
|
-
#
|
10
|
-
#
|
11
|
-
|
8
|
+
|
9
|
+
# f = simulation.method :gradient, parameter: :state
|
10
|
+
f = lambda { |mv| # mv is the marking vector of the free places
|
11
|
+
result = "make hash from free places of the simulation to zeros"
|
12
|
+
nonstoichiometric_transitions.each { |t|
|
13
|
+
places = t.codomain.free
|
14
|
+
inputs = mv.select( t.domain ) # this doesn't work this way
|
15
|
+
f = t.function
|
16
|
+
output = f.call( *inputs )
|
17
|
+
places.each { |p|
|
18
|
+
result[p] += output[p] # again, this doesn't work this way
|
19
|
+
}
|
20
|
+
}
|
21
|
+
stoichiometric_transitions.each { |t|
|
22
|
+
places = t.codomain.free
|
23
|
+
inputs = mv.select( t.domain ) # this doesn't work this way
|
24
|
+
f = t.function
|
25
|
+
output = f.call( *inputs ) * stoichiometry_vector # this doesn't work this way
|
26
|
+
places.each { |p|
|
27
|
+
result[p] += output[p]
|
28
|
+
}
|
29
|
+
}
|
30
|
+
# so you see how many selections one has to do if one doesn't
|
31
|
+
# construct a specific gadget for the operation, that's why
|
32
|
+
# I made those closures, unfortunately I didn't think about
|
33
|
+
# higher-order methods yet when making them, and maybe the core
|
34
|
+
# should own them instead of the simulation object
|
35
|
+
|
36
|
+
return result.to_vector # this doesn't work this way
|
37
|
+
}
|
38
|
+
|
39
|
+
# this is supposed to be Runge-Kutta 4th order
|
40
|
+
# but how do I get those in-between f values...
|
41
|
+
|
42
|
+
y = simulation.state # this must return a vector compatible with the one
|
43
|
+
# returned by f
|
44
|
+
|
45
|
+
k1 = f( y )
|
46
|
+
k2 = f( y + Δt / 2 * k1 )
|
47
|
+
k3 = f( y + Δt / 2 * k2 )
|
48
|
+
k4 = f( y + Δt * k3 )
|
49
|
+
|
50
|
+
rslt = Δt / 6 * ( k1 + 2 * k2 + 2 * k3 + k4 )
|
51
|
+
return rslt.to_the_kind_of_vector_that_delta_method_should_return
|
52
|
+
# which is the vector corresponding to the ordered list of
|
53
|
+
# free places of this simulation (here, every core either has a
|
54
|
+
# simulation assigned, or is parametrized with simulation, which
|
55
|
+
# might be dumb anyway, since core, by its name, should not be that
|
56
|
+
# heavily coupled with a simulation, but actually, atm, each core
|
57
|
+
# belongs to a specific simulation, so it's OK if it's parametrized
|
58
|
+
# with it for now
|
12
59
|
end
|
13
60
|
alias Δ delta
|
14
61
|
end # YPetri::Core::Timed::RungeKutta
|
data/lib/y_petri/core.rb
CHANGED
@@ -3,6 +3,29 @@
|
|
3
3
|
# This class represents a simulator.
|
4
4
|
#
|
5
5
|
class YPetri::Core
|
6
|
+
# TODO: currently, Core and Simulation classes are tightly coupled.
|
7
|
+
# each simulation has just one core, and that core looks directly
|
8
|
+
# into the simulation's state. What needs to be done is a simulation
|
9
|
+
# that at least hints the process of core recruitment for the requested
|
10
|
+
# operation (be it step, step backwards, run forward aso.) and then
|
11
|
+
# imprints the core with its current marking vector, tells the core
|
12
|
+
# what to do, and then reads the result and updates its marking vector
|
13
|
+
# accordingly. There are multiple possibilities, such as constructing
|
14
|
+
# a new core for each operation, or keeping the same core for all the
|
15
|
+
# operations using a given method. A simulation method (like euler,
|
16
|
+
# gillespie, or runge-kutta) should be associated not so much with
|
17
|
+
# the simulation object, as it should be associated with the core
|
18
|
+
# object. A core object should be more or less one-trick pony. While
|
19
|
+
# later, it is possible for a simulation to have broader simulation
|
20
|
+
# strategy, or "method" in the broader sense. But simulation should
|
21
|
+
# also avoid doing too much, because above it, there is Agent class,
|
22
|
+
# and this class can be taught to do the more complicated things
|
23
|
+
# such as parameter optimization or computation of control coefficients
|
24
|
+
# and such. It is also possible to construct more specialized agent-like
|
25
|
+
# classes for these more specialized tasks, since the main purpose
|
26
|
+
# of Agent class, as I saw it, was to represent the user (represent
|
27
|
+
# what the user means), to provide the user interface.
|
28
|
+
|
6
29
|
require_relative 'core/timed'
|
7
30
|
require_relative 'core/timeless'
|
8
31
|
require_relative 'core/guarded'
|
data/lib/y_petri/place.rb
CHANGED
@@ -47,20 +47,20 @@ class YPetri::Place
|
|
47
47
|
# +Marking+ is a standard attribute of a Petri net place, +default_marking+
|
48
48
|
# is marking upon calling the reset method. Default marking may also be used
|
49
49
|
# as the initial value in the simulations involving the place in question.
|
50
|
-
# +Quantum+ attribute is not in use presently. In the future, it
|
51
|
-
#
|
52
|
-
# of
|
53
|
-
# example, the place could only admit non-negative numbers,
|
54
|
-
# than 1, or odd numbers etc.)
|
55
|
-
#
|
56
|
-
#
|
57
|
-
#
|
58
|
-
#
|
59
|
-
#
|
60
|
-
#
|
61
|
-
# data type of the +:marking+ or +:default_marking+ argument. If
|
62
|
-
# that the place has no guards whatsoever, +:guard+
|
63
|
-
# _false_.
|
50
|
+
# +Quantum+ attribute is not in use presently. In the future, it might be used
|
51
|
+
# in deciding when to switch between continuous and discrete stochastic
|
52
|
+
# representation of the marking. +Guard+ is a restriction to the place's
|
53
|
+
# marking. (For example, the place could only admit non-negative numbers,
|
54
|
+
# or numbers smaller than 1, or odd numbers etc.) Any number of guards can
|
55
|
+
# be specified for a constructed place via +Place#guard+ method. For the cases
|
56
|
+
# when a place has only one guard, it is, as a syntactic sugar, possible to
|
57
|
+
# introduce exactly one guard already upon place initialization by supplying
|
58
|
+
# to this constructor, in addition to other parameters, a string expressing
|
59
|
+
# the guard as +:guard+ and a block expressing the same guard in code. If no
|
60
|
+
# guard block is supplied to this constructor, default guards are constructed
|
61
|
+
# based on the data type of the +:marking+ or +:default_marking+ argument. If
|
62
|
+
# it is wished that the place has no guards whatsoever, +:guard+ should be set
|
63
|
+
# to _false_.
|
64
64
|
#
|
65
65
|
def initialize guard: L!, **named_args, &block
|
66
66
|
@upstream_arcs, @downstream_arcs, @guards = [], [], [] # init to empty
|
@@ -38,21 +38,21 @@ class YPetri::Simulation
|
|
38
38
|
end
|
39
39
|
end
|
40
40
|
|
41
|
-
# Necessary to
|
41
|
+
# Necessary to overcome the protected character of the listed methods.
|
42
42
|
#
|
43
43
|
[ :node,
|
44
44
|
:place,
|
45
45
|
:transition
|
46
46
|
].each { |sym| define_method sym do |e| simulation.send sym, e end }
|
47
47
|
|
48
|
-
# Necessary to
|
48
|
+
# Necessary to overcome the protected character of the listed methods.
|
49
49
|
#
|
50
50
|
[ :Nodes,
|
51
51
|
:Places,
|
52
52
|
:Transitions
|
53
53
|
].each { |sym| define_method sym do |array| simulation.send sym, array end }
|
54
54
|
|
55
|
-
# Necessary to
|
55
|
+
# Necessary to overcome the protected character of the listed methods.
|
56
56
|
#
|
57
57
|
[ :nodes,
|
58
58
|
:places,
|
@@ -24,7 +24,7 @@ class YPetri::Simulation
|
|
24
24
|
|
25
25
|
# Creates a subset of this collection (of the same class).
|
26
26
|
#
|
27
|
-
def subset nodes=nil, &block
|
27
|
+
def subset nodes=nil, &block # TODO: Rename to subarray
|
28
28
|
if block_given? then
|
29
29
|
fail ArgumentError, "If block given, arguments not allowed!" unless
|
30
30
|
nodes.nil?
|
data/lib/y_petri/version.rb
CHANGED
data/lib/y_petri/world.rb
CHANGED
@@ -4,10 +4,11 @@ require_relative 'world/dependency'
|
|
4
4
|
require_relative 'world/petri_net_aspect'
|
5
5
|
require_relative 'world/simulation_aspect'
|
6
6
|
|
7
|
-
#
|
8
|
-
# and other assets needed to
|
9
|
-
#
|
10
|
-
#
|
7
|
+
# Represents YPetri workspace, but "world" is shorter. Its instance holds
|
8
|
+
# places, transitions, nets and other assets needed to perform the tasks
|
9
|
+
# of system specification and simulation (simulation settings, place clamps,
|
10
|
+
# initial markings etc.). Provides basic methods to do just what is necessary.
|
11
|
+
# More ergonomic and DSL-like methods may be defined in YPetri::Agent.
|
11
12
|
#
|
12
13
|
class YPetri::World
|
13
14
|
★ NameMagic # ★ means include
|
@@ -15,14 +16,17 @@ class YPetri::World
|
|
15
16
|
★ SimulationAspect
|
16
17
|
|
17
18
|
def initialize
|
18
|
-
#
|
19
|
+
# Set up parametrized subclasses of Place, Transition, Net.
|
19
20
|
param_class!( { Place: YPetri::Place,
|
20
21
|
Transition: YPetri::Transition,
|
21
22
|
Net: YPetri::Net },
|
22
23
|
with: { world: self } )
|
23
|
-
#
|
24
|
+
# Invoke #namespace! method (from YSupport's NameMagic) on each of them.
|
25
|
+
# This causes each of them to do bookkeeping of their instances. This is
|
26
|
+
# because there is little point in keeping the objects from separate
|
27
|
+
# worlds (ie. workspaces) on the same list.
|
24
28
|
[ Place(), Transition(), Net() ].each &:namespace!
|
25
|
-
# And
|
29
|
+
# And proceed with initializations (if any) higher in the lookup chain.
|
26
30
|
super
|
27
31
|
end
|
28
32
|
end
|
data/lib/y_petri.rb
CHANGED
@@ -35,33 +35,37 @@ require_relative 'y_petri/core'
|
|
35
35
|
require_relative 'y_petri/agent'
|
36
36
|
require_relative 'y_petri/dsl'
|
37
37
|
|
38
|
-
# YPetri is a domain
|
39
|
-
#
|
40
|
-
#
|
41
|
-
# modelling: model specification and simulation, excelling in the first one.
|
38
|
+
# YPetri is a domain-specific language (DSL) for modelling dynamical systems. It
|
39
|
+
# caters solely to the two main concerns of modelling: model specification and
|
40
|
+
# simulation.
|
42
41
|
#
|
43
|
-
#
|
44
|
-
#
|
45
|
-
#
|
46
|
-
# (
|
47
|
-
#
|
48
|
-
#
|
49
|
-
#
|
50
|
-
#
|
51
|
-
#
|
52
|
-
#
|
53
|
-
#
|
54
|
-
#
|
42
|
+
# Model specification in YPetri is based on a Petri net. Classical Petri net
|
43
|
+
# (PN), originally described by Carl Adam Petri in 1962, is a bipartite graph
|
44
|
+
# with two kinds of nodes: places (circles) and transitions (rectangles),
|
45
|
+
# connected by arcs (lines). Places act as variables – each place holds exactly
|
46
|
+
# one value ("marking"), a discrete number imagined as consisting of individual
|
47
|
+
# units ("tokens"). The action of transitions ("firing") is also discrete. Each
|
48
|
+
# time a transition fires, a fixed number of tokens is added/subtracted to the
|
49
|
+
# connected places. It turns out that classical PNs are very useful in describing
|
50
|
+
# things like industrial systems, production lines, and also basic chemical
|
51
|
+
# systems with a number of molecules is connected by stoichiometric reactions.
|
52
|
+
#
|
53
|
+
# YPetri allows specification of not just classical PNs, but also of many
|
54
|
+
# extended Petri net (XPN) types, which have been described since Petri's work.
|
55
|
+
# This is achieved by making YPetri transitions functional (mathematical
|
56
|
+
# functions in lambda notation can be attached to them), and allowing the
|
57
|
+
# possibility of transitions being defined as either timed and timeless, and as
|
58
|
+
# eithier nonstoichiometric and explicitly stoichiometric. Together, this makes 4
|
59
|
+
# types of functional transitions available in YPetri, which can be used to
|
60
|
+
# capture almost any type of XPN. In this way, YPetri can serve as a common
|
61
|
+
# platform for data exchange and cooperation between different XPN formalisms,
|
62
|
+
# without sacrificing the special qualities of XPNs described thus far.
|
63
|
+
#
|
64
|
+
# The basic simulation method is simple PN execution. In its course, transitions
|
65
|
+
# fire, and thereby change the places' marking by adding/removing tokens as
|
66
|
+
# dictated by their operating prescription. Other simulation methods become
|
67
|
+
# available for more specific net types, such as timed nets.
|
55
68
|
#
|
56
|
-
# Many extended PN types have been defined. YPetri implements a universal PN
|
57
|
-
# abstraction designed to subsume and integrate the dichotomies of the common
|
58
|
-
# extended PN types. YPetri unifies discrete and stochastic modelling of timed
|
59
|
-
# transitions at the level of model specification in line with the present day
|
60
|
-
# unifying Petri net frameworks. YPetri also integrates other dichotomies: timed
|
61
|
-
# / timeless and stoichiometric / nonstoichiometric. The idea is to save the
|
62
|
-
# Ruby-based modeller the work of researching different PN types and provide a
|
63
|
-
# single abstraction and the DSL to rule them all.
|
64
|
-
#
|
65
69
|
module YPetri
|
66
70
|
class << self
|
67
71
|
def included( receiver )
|
data/test/core_test.rb
ADDED
@@ -0,0 +1,26 @@
|
|
1
|
+
#! /usr/bin/ruby
|
2
|
+
# encoding: utf-8
|
3
|
+
|
4
|
+
gem 'minitest'
|
5
|
+
require 'minitest/autorun'
|
6
|
+
require_relative '../lib/y_petri' # tested component itself
|
7
|
+
# require 'y_petri'
|
8
|
+
# require 'sy'
|
9
|
+
|
10
|
+
describe "use of timed and timeless core" do
|
11
|
+
before do
|
12
|
+
# set up a user of core, which will imitate some of the needs
|
13
|
+
# of the Simulation class, or be an actual instance of that class
|
14
|
+
end
|
15
|
+
|
16
|
+
it "should behave" do
|
17
|
+
# the core will be informed of the task required (bring the system
|
18
|
+
# whose specification is known to the core user mentioned above from
|
19
|
+
# some initial state to some next state by performing a requested
|
20
|
+
# something in a way requested by the user, where something can be
|
21
|
+
# eg. step forward, or run forward by a specified period of time or
|
22
|
+
# number of steps or until some other condition is fulfilled, or
|
23
|
+
# step backward, or even run backward, if the system allows such thing
|
24
|
+
# at all.
|
25
|
+
end
|
26
|
+
end
|
data/test/y_petri_test.rb
CHANGED
@@ -11,6 +11,7 @@ require_relative '../lib/y_petri' # tested component itself
|
|
11
11
|
require_relative 'place_test'
|
12
12
|
require_relative 'transition_test'
|
13
13
|
require_relative 'net_test'
|
14
|
+
require_relative 'core_test'
|
14
15
|
require_relative 'simulation_test'
|
15
16
|
require_relative 'world_test'
|
16
17
|
require_relative 'agent_test'
|
metadata
CHANGED
@@ -1,14 +1,14 @@
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
2
2
|
name: y_petri
|
3
3
|
version: !ruby/object:Gem::Version
|
4
|
-
version: 2.3.
|
4
|
+
version: 2.3.11
|
5
5
|
platform: ruby
|
6
6
|
authors:
|
7
7
|
- boris
|
8
8
|
autorequire:
|
9
9
|
bindir: bin
|
10
10
|
cert_chain: []
|
11
|
-
date:
|
11
|
+
date: 2015-10-05 00:00:00.000000000 Z
|
12
12
|
dependencies:
|
13
13
|
- !ruby/object:Gem::Dependency
|
14
14
|
name: bundler
|
@@ -248,6 +248,7 @@ files:
|
|
248
248
|
- test/acceptance/visualization_test.rb
|
249
249
|
- test/acceptance_tests.rb
|
250
250
|
- test/agent_test.rb
|
251
|
+
- test/core_test.rb
|
251
252
|
- test/examples/demonstrator.rb
|
252
253
|
- test/examples/demonstrator_2.rb
|
253
254
|
- test/examples/demonstrator_4.rb
|
@@ -299,6 +300,7 @@ test_files:
|
|
299
300
|
- test/acceptance/visualization_test.rb
|
300
301
|
- test/acceptance_tests.rb
|
301
302
|
- test/agent_test.rb
|
303
|
+
- test/core_test.rb
|
302
304
|
- test/examples/demonstrator.rb
|
303
305
|
- test/examples/demonstrator_2.rb
|
304
306
|
- test/examples/demonstrator_4.rb
|