-
Notifications
You must be signed in to change notification settings - Fork 1
/
simulation.go
288 lines (267 loc) · 7.97 KB
/
simulation.go
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
// Package godesim can be described as a simple interface
// to solve a first-order system of non-linear differential equations
// which can be defined as Go code.
package godesim
import (
"os"
"time"
"github.com/soypat/godesim/state"
"gonum.org/v1/gonum/floats"
)
// Simulation contains dynamics of system and stores
// simulation results.
//
// Defines an object that can solve
// a non-autonomous, non-linear system
// of differential equations
type Simulation struct {
Timespan
Logger Logger
State state.State
currentStep int
results []state.State
Solver func(sim *Simulation) []state.State
change map[state.Symbol]state.Diff
Diffs state.Diffs
inputs map[state.Symbol]state.Input
eventers []Eventer
events []struct {
Label string
State state.State
}
Config
}
// Config modifies Simulation behaviour/output.
// Set with simulation.SetConfig method
type Config struct {
// Domain is symbol name for step variable. Default is `time`
Domain state.Symbol `yaml:"domain"`
Log LoggerOptions
Behaviour struct {
StepDelay time.Duration `yaml:"delay"`
} `yaml:"behaviour"`
Algorithm struct {
// Number of algorithm steps. Different to simulation Timespan.Len()
Steps int `yaml:"steps"`
// Step limits for adaptive algorithms
Step struct {
Max float64 `yaml:"max"`
Min float64 `yaml:"min"`
} `yaml:"step"`
Error struct {
// Sets max error before proceeding with adaptive iteration
// Step.Min should override this
Max float64 `yaml:"max"`
} `yaml:"error"`
// Below are numerical factors
// Newton-Raphson method's convergence can benefit from a relaxationFactor between 0 and 0.5
RelaxationFactor float64 `yaml:"relaxation"`
// Newton-Raphson Method requires multiple sub-iterations to converge. On each
// iteration the Jacobian is calculated, which is an expensive operation.
// A good number may be between 10 and 100.
IterationMax int `yaml:"iterations"`
} `yaml:"algorithm"`
Symbols struct {
// Sorts symbols for consistent logging and testing
NoOrdering bool `yaml:"no_ordering"`
} `yaml:"symbols"`
}
// New creates blank simulation. To run a simulation one
// must also set the domain with NewTimespan()
// and create the differential equation system with SetX0FromMap()
// and SetDiffFromMap(). Examples can be found at https://pkg.go.dev/github.com/soypat/godesim.
//
// The Default solver is RK4Solver.
// Other default values are set by DefaultConfig()
func New() *Simulation {
sim := Simulation{
change: make(map[state.Symbol]state.Diff),
Solver: RK4Solver,
Logger: newLogger(os.Stdout),
}
sim.Config = DefaultConfig()
return &sim
}
// SetConfig Set configuration to modify default Simulation values
func (sim *Simulation) SetConfig(cfg Config) *Simulation {
sim.Config = cfg
return sim
}
// DefaultConfig returns configuration set for all new
// simulations by New()
//
// Domain is the integration variable. "time" is default value
// simulation.Domain
// Solver used is fourth order Runge-Kutta multivariable integration.
// simulation.Solver
// How many solver steps are run between Timespan steps. Set to 1
// simulation.Algorithm.Steps
func DefaultConfig() Config {
cfg := Config{Domain: "time"}
cfg.Log.Results.Precision = -1 // to prevent logging
cfg.Algorithm.Steps = 1 // 1 step needed as minimum
return cfg
}
// Begin starts simulation
//
// Unrecoverable errors will panic. Warnings may be printed.
func (sim *Simulation) Begin() {
// This is step 0 of simulation
for sym := range sim.inputs { // create state symbols and set them to zero in case some inputs depend on other inputs
sim.State.UEqual(sym, 0)
}
sim.setInputs()
sim.verifyPreBegin()
sim.results = make([]state.State, 0, sim.Algorithm.Steps*sim.Len())
sim.results = append(sim.results, sim.State)
sim.events = make([]struct {
Label string
State state.State
}, 0, len(sim.eventers))
eventsOn := sim.eventers != nil && len(sim.eventers) > 0
logging := sim.Log.Results.FormatLen > 0
if logging {
sim.logStates(sim.results[:1])
}
var states []state.State
for sim.IsRunning() {
sim.currentStep++
states = sim.Solver(sim)
sim.results = append(sim.results, states[1:]...)
sim.State = states[len(states)-1]
sim.setInputs()
if logging {
sim.logStates(states[1:])
}
time.Sleep(sim.Behaviour.StepDelay)
if eventsOn {
sim.handleEvents()
}
}
if logging {
sim.Logger.flush()
}
}
// SetX0FromMap sets simulation's initial X values from a Symbol map
func (sim *Simulation) SetX0FromMap(m map[state.Symbol]float64) {
sim.State = state.New()
for sym, v := range m {
sim.State.XEqual(sym, v)
}
}
// SetDiffFromMap Sets the ODE equations (change in X) with a pre-built map
//
// i.e. theta(t) = 0.5 * t^2
//
// sim.SetDiffFromMap(map[state.Symbol]state.Change{
// "theta": func(s state.State) float64 {
// return s.X("Dtheta")
// },
// "Dtheta": func(s state.State) float64 {
// return 1
// },
// })
func (sim *Simulation) SetDiffFromMap(m map[state.Symbol]state.Diff) {
sim.change = m
}
// SetInputFromMap Sets Input (U) functions with pre-built map
func (sim *Simulation) SetInputFromMap(m map[state.Symbol]state.Input) {
sim.inputs = m
}
// CurrentTime obtain simulation step variable
func (sim *Simulation) CurrentTime() float64 {
return sim.results[len(sim.results)-1].Time()
}
// Results get vector of simulation results for given symbol (X or U)
//
// Special case is the Simulation.Domain (default "time") symbol.
func (sim *Simulation) Results(sym state.Symbol) []float64 {
vec := make([]float64, len(sim.results))
// TODO verify simulation has run!
if sym == sim.Domain {
for i, r := range sim.results {
vec[i] = r.Time()
}
return vec
}
symV := []state.Symbol{sym}
consU, consX := !floats.HasNaN(sim.State.ConsistencyU(symV)), !floats.HasNaN(sim.State.ConsistencyX(symV))
if consU {
for i, r := range sim.results {
vec[i] = r.U(sym)
}
return vec
}
if consX {
for i, r := range sim.results {
vec[i] = r.X(sym)
}
return vec
}
throwf("Simulation.Results: %s not found in X or U symbols", sym)
return nil
}
// StatesCopy returns a copy of all result states. Simulation must have been run beforehand.
func (sim *Simulation) States() (states []state.State) {
if sim.IsRunning() {
throwf("states requested during simulation execution")
}
n := len(sim.results)
if n == 0 {
throwf("requested results of length 0. Did you remember to call Begin() ?")
}
states = make([]state.State, n)
for i := 0; i < n; i++ {
states[i] = sim.results[i].Clone()
}
return states
}
// ForEachState calls f on all result states. Simulation must have been run beforehand.
func (sim *Simulation) ForEachState(f func(i int, s state.State)) {
if sim.IsRunning() {
throwf("states requested during simulation execution")
}
n := len(sim.results)
if n == 0 {
throwf("requested results of length 0. Did you remember to call Begin() ?")
}
for i := range sim.results {
f(i, sim.results[i])
}
}
// StateDiff obtain Diffs results without modifying State
// Returns state evolution (result of applying Diffs functions to S)
func StateDiff(F state.Diffs, S state.State) state.State {
diff := S.Clone()
syms := S.XSymbols()
if len(F) != len(syms) {
throwf("length of func slice not equal to float slice (%v vs. %v)", len(F), len(syms))
}
for i := 0; i < len(F); i++ {
diff.XEqual(syms[i], F[i](S))
}
return diff
}
// AddEventHandlers add event handlers to simulation.
//
// Events which return errors will create a special event with
// an error message for Label(). If one wishes to stop simulation
// execution one can call panic() in an event.
func (sim *Simulation) AddEventHandlers(evhand ...Eventer) {
if len(evhand) == 0 {
throwf("AddEventHandlers: can't add 0 event handlers")
}
if sim.eventers == nil {
sim.eventers = make([]Eventer, 0, len(evhand))
}
for i := range evhand {
sim.eventers = append(sim.eventers, evhand[i])
}
}
// Events Returns a copy of all simulation events
func (sim *Simulation) Events() []struct {
Label string
State state.State
} {
return sim.events
}