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
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
|
// Stepping routines and method interface.
//
// Copyright (C) 2020 Juan Marín Noguera
//
// This file is part of Solvned.
//
// Solvned is free software: you can redistribute it and/or modify it under the
// terms of the GNU Lesser General Public License as published by the Free
// Software Foundation, either version 3 of the License, or (at your option) any
// later version.
//
// Solvned is distributed in the hope that it will be useful, but WITHOUT ANY
// WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR
// A PARTICULAR PURPOSE. See the GNU Lesser General Public License for more
// details.
//
// You should have received a copy of the GNU Lesser General Public License
// along with Solvned. If not, see <https://www.gnu.org/licenses/>.
package mned
import (
"math"
"sort"
)
// A Stepper is an iterator over points of the solution of an IVP. ODE solver
// methods generally produce steppers that go from the initial values of the
// problems to lower or greater values.
type Stepper interface {
// Go to the next point and return a pointer to it. If the second value
// is false, there are no more points in the direction the Stepper is
// following and the value of Point should not be trusted.
//
// The point returned might change after the next call to next() on the
// same Stepper.
Next() (*Point, bool)
}
// Starting in the point init, step until the domain of the function ends or
// one of the events says that the stepping should stop, calculating the
// intermediante points with the given interpolator and, if `callback` is not
// nil, calling the given callback with each new point. If ok is false, the
// iteration ended because the stepper returned false; otherwise index contains
// the index of the event that made the iteration stop.
func StepUntil(
s Stepper,
init Point,
interp Interpolator,
callback func(*Point),
evs ...Event,
) (index int, ok bool) {
var prev Point = init
vs := make([]float64, len(evs))
for i := 0; i < len(evs); i++ {
vs[i] = evs[i].Cross(&prev)
}
for {
point, ok := s.Next()
if !ok {
return 0, false
}
actions := make(requiredActions, 0)
for i := 0; i < len(evs); i++ {
newVal := evs[i].Cross(point)
if differentSign(vs[i], newVal) {
actions = append(actions, requiredAction{
index: i,
point: evs[i].FindPoint(
interp, &prev, point,
)})
}
vs[i] = newVal
}
sort.Sort(actions)
size := len(actions)
if point.Time < prev.Time {
for i := 0; i < size/2; i++ {
actions[i], actions[size-i] =
actions[size-i], actions[i]
}
}
for _, a := range actions {
if !evs[a.index].Action(&a.point) {
return a.index, true
}
}
callback(point)
prev = point.Clone()
}
}
// A ConfigurableStepper is a Stepper which allows specifying the delta of the
// next step; that is, the difference between the time of a point and that of
// the previous point.
type ConfigurableStepper interface {
Stepper
// Like Stepper.Next but the time of the next point is specified to be
// the time of the latest point plus the value given.
NextStep(float64) (*Point, bool)
}
// A Method is a way of approximating points of the solution to an IVP. It works
// as a factory of Steppers that step forward and backward from the initial
// values of the problem.
type Method interface {
// Make a stepper over the solution of the given IVP that starts at the
// initial values of the problem and goes to ever increasing values for
// the independent variable.
Forward(*IVP) Stepper
// Make a stepper over the solution of the given IVP that starts at the
// initial values of the problem and goes to ever decreasing values for
// the independent variable.
Backward(*IVP) Stepper
}
type fixedStepper struct {
state IVP
step float64
method func(*IVP, float64) bool
}
func (s *fixedStepper) NextStep(h float64) (*Point, bool) {
ok := s.method(&s.state, h)
if !ok {
return nil, false
}
s.state.Start.Time += h
return &s.state.Start, true
}
func (s *fixedStepper) Next() (*Point, bool) {
return s.NextStep(s.step)
}
// Create a fixed step method, one where all steps increment or decrement the
// time by the same amount, with the added condition that it shouldn't depend
// on extra state. The Step is the fixed increment/decrement of time, which
// must be positive. The steppers returned by this method are
// `ConfigurableStepper`s.
//
// The Method is a function that takes an IVP and a step and stores in
// IVP.Start.Value the value for IVP.Start.Time+step, while leaving
// IVP.Start.Time untouched. The return value of `method` is true if the
// operation completed successfully or false if there was a problem, such as
// going out of the domain of the IVP.Derivative, in which case the resulting
// state of the IVP is unspecified.
type FixedStepMethod struct {
Step float64
Method func(*IVP, float64) bool
}
func (m FixedStepMethod) Forward(ivp *IVP) Stepper {
return &fixedStepper{
state: ivp.Clone(),
step: m.Step,
method: m.Method}
}
func (m FixedStepMethod) Backward(ivp *IVP) Stepper {
return &fixedStepper{
state: ivp.Clone(),
step: -m.Step,
method: m.Method}
}
type adaptiveStepper struct {
state IVP
step float64
hmin float64
hmax float64
tolerance float64
method func(*IVP, float64) bool
order uint8
}
// Return the new step that should be taken by an AdaptiveStepMethod of the
// given order and with a given tolerance took a step of a given size yielding
// the given amount of error, with max as the maximum size of a step.
func AdaptiveUpdateStep(
step float64, tol float64, err float64, max float64, order uint8,
) float64 {
var q float64
adjust := tol * math.Abs(step) / (2 * err)
if order == 1 { // Fast path
q = adjust
} else {
q = math.Pow(adjust, 1/float64(order))
}
switch {
case q < 0.1:
q = 0.1
case q > 4:
q = 4
}
newStep := q * step
if math.Abs(newStep) > max {
if newStep > 0 {
newStep = max
} else {
newStep = -max
}
}
return newStep
}
func (s *adaptiveStepper) adaptStep(err float64) {
s.step = AdaptiveUpdateStep(s.step, s.tolerance, err, s.hmax, s.order)
}
func (s *adaptiveStepper) getSteps(full []float64, half []float64) bool {
orig := s.state.Start.Value
copy(full, orig)
s.state.Start.Value = full
if !s.method(&s.state, s.step) {
return false
}
copy(half, orig)
s.state.Start.Value = half
if !s.method(&s.state, s.step/2) {
return false
}
if !s.method(&s.state, s.step/2) {
return false
}
s.state.Start.Value = orig
return true
}
func (s *adaptiveStepper) Next() (*Point, bool) {
size := len(s.state.Start.Value)
full := make([]float64, size)
half := make([]float64, size)
for s.step >= s.hmin {
for !s.getSteps(full, half) {
if s.step *= 0.5; s.step < s.hmin {
return nil, false
}
}
orderm1 := 1 / (math.Ldexp(1, int(s.order)) - 1)
multiplier := 1 + orderm1
sqerr := float64(0)
for i, v := range full {
diff := v - half[i]
sqerr += diff * diff
}
err := math.Sqrt(sqerr) * multiplier
tol := s.tolerance * math.Abs(s.step)
if err > tol {
s.adaptStep(err)
continue
}
for i, v := range full {
s.state.Start.Value[i] =
multiplier*half[i] - orderm1*v
}
s.state.Start.Time += s.step
s.adaptStep(err)
return &s.state.Start, true
}
return nil, false // s.step < s.hmin
}
// An AdaptiveStepMethod is a kind of adaptive method (that is, one that changes
// the step over time for greater precision and performance) that can be adapted
// from a fixed-step method.
//
// Step, Hmin, and Hmax are the initial, minimum, and maximum step. If the
// step must go lower than Hmin to satisfy the tolerance, the stepper ends;
// if it could get greater than Hmax, it saturates to Hmax. The Tolerance is
// such that the Euclidean norm of the estimated error of any step of size `h`
// must be lower than `h*Tolerance`.
//
// Method works the same way as in `FixedStepMethod` except that its error for a
// given interval of time must be on the order of `O(h^Order)`, with the
// variable `h` being the step. Order must be positive.
//
// The algorithm used is the following, where `(t,x(t))` are the initial values,
// `y(h,t,x(t))` is the result of applying the inner method with step `h` from
// `(t,x(t))`, `k` is the order of the method and `h` is the step.
// 1. First, `F:=y(h,t,x(t))` and `H:=y(h/2,t+h/2,y(h/2,t,x(t)))` are obtained,
// and the error is estimated as `e:=|(1+1/(2^k-1))*(H-F)|`.
// 2. If that error is lower than `|h|*Tolerance`, the step is accepted.
// Otherwise we set `q:=(|h|*Tolerance)/(2*e)`, saturate so that `q` is in
// `[0.1, 4]`, set `h:=q*h`, check bounds on `h` and return to the start.
// 3. The next value is taken to be `(t+h, (2^k*H - H)/(2^k - 1))`, and the
// next step size is calculated by changing `h` as in step 2.
//
// The steppers are *not* `ConfigurableStepper`s.
type AdaptiveStepMethod struct {
Step float64
Hmin float64
Hmax float64
Tolerance float64
Method func(*IVP, float64) bool
Order uint8
}
func (m *AdaptiveStepMethod) toStepper(ivp *IVP, step float64) Stepper {
return &adaptiveStepper{
state: ivp.Clone(),
step: step,
hmin: m.Hmin,
hmax: m.Hmax,
tolerance: m.Tolerance,
method: m.Method,
order: m.Order,
}
}
func (m *AdaptiveStepMethod) Forward(ivp *IVP) Stepper {
return m.toStepper(ivp, m.Step)
}
func (m *AdaptiveStepMethod) Backward(ivp *IVP) Stepper {
return m.toStepper(ivp, -m.Step)
}
|