diff options
| author | Juan Marin Noguera <juan@mnpi.eu> | 2023-07-25 18:22:04 +0200 |
|---|---|---|
| committer | Juan Marin Noguera <juan@mnpi.eu> | 2023-07-25 18:22:04 +0200 |
| commit | 46afb6bf501f4001c4bdb7bd2f2db7c466f95554 (patch) | |
| tree | 38700d5545a2cf172434aa656825371ff9563825 /step.go | |
Solvned project from 2020
Note that Go didn't have generics back then.
Diffstat (limited to 'step.go')
| -rw-r--r-- | step.go | 324 |
1 files changed, 324 insertions, 0 deletions
@@ -0,0 +1,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) +} |
