Simple and flexible simulation framework for Go.
This framework provides concurrent execution of simulations according to scenarios, output of results, monitor of progress and management of random number seeds.
All We need is implement the scenario and the actors who play it.
The algorithm of this framework is shown in the pseudo code below.
for i := 0; i < iter; i++ {
sem <- struct{}{}
go func() {
scenario := NewScenarioFn(rnd.Int63())
actor := NewActorFn(rnd.Int63())
for scenario.Scan() {
action, _ := actor.Act(scenario.Line())
w.Write(action.String())
}
<-sem
}()
}
- Implement our scenario.
- Implement an actor who plays the scenario.
- Implement an action which represents performance of the actor.
- Implement a callback function which is called at each iteration. (Optional)
- And stage.New().Run()
dir := "log" // Directory for output
concurrency := runtime.NumCPU() // Number of concurrency for scenario
seed := 1 // Seed for random
iter := 3 // Number of iteration
s := stage.New(dir, concurrency, seed)
s.Run(iter, NewActorFn, NewScenarioFn, stage.NoOpeCallbackFn)
The result will be output to log file.
log
└── 20200530184234-1 # Timestamp-Seed
├── iter_00-a_7947919477105006377-s_5355116748216652230.log # Iteration log files
├── iter_01-a_4846631296614585111-s_2007235010091403794.log # with seed for actor(a)
└── iter_02-a_0610076349056253918-s_3540139325796113853.log # and seed for scenario(s)
See also examples.
Scenario interface represents our simulation scenario. This framework runs the scenario number of iterations times in parallel.
Our scenario has Scan() method. This method reads the scenario one line. So, we can implements it as a counter or file reader.
type Scenario struct {
t int
limit int
rnd *rand.Rand
}
func (s *Scenario) Scan() bool {
s.t++
return s.t < s.limit
}
And the scenario has Line() method too.
This method returns the current line for the scenario as stage.Line
.
Actors will perform according to it by each line.
We can use stage.Line
flexibly because the struct is type of map[string]interface{}
.
func (s *Scenario) Line() stage.Line {
return stage.Line{"x": s.rnd.NormFloat64()}
}
Finally, we define a method to generate the scenario for each iteration.
func NewScenarioFn(seed int64) stage.Scenario {
return &Scenario{
t: -1,
limit: 3000,
rnd: rand.New(rand.NewSource(seed)),
}
}
Actor interface represents our simulation actor. This framework runs the actor number of lines times from the scenario.
Our actor has Act() method.
This method is called with a line of scenario one by one and returns a result of simulation as stage.Action
.
type Actor struct {
n int
sum int
}
func (s *Actor) Act(line stage.Line) (stage.Action, error) {
s.n++
s.sum += line["x"].(float64)
return Action{avg: float64(s.sum/s.n)}
}
We can implement stage.Action
like a Stringer
.
type Action struct {
avg float64
}
func (a Action) String() string {
return fmt.Sprintf("%f\n", a.avg)
}
Finally, we define a method to generate the actor for each iteration.
func NewActorFn(seed int64) stage.Actor {
return &Actor{}
}
If our scenario is going to run long, we can monitor it's progress by using stage.CallbackFn
.
The callback function is called when each iteration finished.
So, we can use our favorite progress bar library.
callbackFn := func(i int) { bar.Increment() } }
The framework has also an empty operation callback function named stage.NoOpeCallbackFn
.
We usually use the function if we don't need monitor of progress.
This example shows that an adaptive window scaling algorithm called ADWIN detects and changes window size using two scenario. Each scenario provides blue point according with a probability ditribution.
$ go run _examples/adwin/main.go -s abrupt
$ go run _examples/adwin/main.go -s gradual
Abrupt changes | Gradual changes |
---|---|
This example shows that printing progress using a progress bar library.
$ go run _examples/progress/main.go
180 / 200 [---------------------------->___] 90.00% 40 p/s
$ go get github.com/monochromegane/stage