LCOV - code coverage report
Current view: top level - src - output.jl (source / functions) Hit Total Coverage
Test: on branch nothing Lines: 112 123 91.1 %
Date: 2025-02-26 15:13:49 Functions: 0 0 -

          Line data    Source code
       1             : """
       2             :     trypush!(spline, t, y, dy)
       3             : 
       4             : Push new step `(t, y, dy)` onto `spline`, but check if time `t` is
       5             : consistent: Do **not** push if either the same `t` is currently the
       6             : last knot of `spline` (ignore silently) or if `t < last(spline)`. The
       7             : latter may yield a warning, because there may be a flaw in the
       8             : 
       9             : output (cascade).
      10             : 
      11             : """
      12      142830 : function trypush!(s::HermiteSpline.AbstractHermiteSpline, t, y, dy)
      13      283657 :     if HermiteSpline.ismonotone(s, t)
      14      142826 :         push!(s, t, y, dy)
      15           4 :     elseif t != last(s)
      16           0 :         @warn "Cannot push t=$t after at $(last(s))"
      17             :     end
      18      142830 :     nothing
      19             : end
      20             : 
      21             : #-----------------------------------------------------------------------------
      22             : 
      23             : """
      24             :     sink(t, y, dy) # collect data from solver
      25             : 
      26             : Data structure that collects results from [`Solver`](@ref)/
      27             : 
      28             : A *sink* can be used as "end-point" of an [`RK43.AbstractOutput`](@ref).
      29             : 
      30             : See also [`RK43.AbstractOutput`](@ref), [`RK43.TryPush`](@ref)
      31             : """
      32             : abstract type AbstractSink end
      33             : 
      34           0 : (::AbstractSink)(t, y, dy) = @show((t=t, y=y, dy=dy))
      35             : 
      36             : """
      37             : [`RK43.AbstractSink`](@ref) that calls [`RK43.trypush!`](@ref) on provided data
      38             : structure (e.g., a `HermiteSpline`).
      39             : 
      40             : See also [`RK43.AbstractSink`](@ref), [`pushinto`](@ref)
      41             : """
      42             : struct TryPush{S} <: AbstractSink
      43             :     sink::S
      44        2005 :     TryPush{S}(sink::S) where {S} = new(sink)
      45             : end
      46             : 
      47      142830 : (s::TryPush{S})(t, y, dy) where {S} = RK43.trypush!(s.sink, t, y, dy)
      48             : 
      49             : """
      50             :     sink = indexedfill(array, Val(2))
      51             : 
      52             : [`RK43.AbstractSink`](@ref) that calls fills an array or array view for
      53             : indexed times/knots with `index` starting from `1` for every event.
      54             : 
      55             : The type parameter `P::Int` defines a projection of the event tuple
      56             : `(t, y, dy)`, e.g., `P=2` stores positions `y`.
      57             : 
      58             : !!! note
      59             :     Use with dense [`sample`](@ref)s to fill (part of) an array, e.g.,
      60             :     for sampling a *flow map*.
      61             : 
      62             : See also [`RK43.AbstractSink`](@ref), [`RK43.indexedfill`](@ref), [`sample`](@ref)
      63             : """
      64             : mutable struct IndexedFill{A, P} <: AbstractSink
      65             :     const ary::A
      66             :     idx::Int
      67        2862 :     IndexedFill{A, P}(ary::A) where {A<:AbstractArray, P} = new(ary, 1)
      68             : end
      69             : 
      70       10000 : _projection(::Val{1}, t, _, _) = t
      71       99444 : _projection(::Val{2}, _, y, _) = y
      72       10000 : _projection(::Val{3}, _, _, dy) = dy
      73             : 
      74      119444 : function (s::IndexedFill{A, P})(t, y, dy) where {A, P}
      75      119444 :     s.ary[s.idx] = _projection(Val(P), t, y, dy)
      76      119444 :     s.idx += 1
      77             : end
      78             : 
      79             : """
      80             :     sink = indexedfill(array, Val(2))
      81             : 
      82             : Create [`IndexedFill`](@ref) sink.
      83             : 
      84             : See also [`RK43.AbstractSink`](@ref), [`RK43.IndexedFill`](@ref), [`sample`](@ref)
      85             : """
      86        2863 : indexedfill(a::A, proj::Val{N} = Val(2)) where {A<:AbstractArray, N} =
      87             :     IndexedFill{A, N}(a)
      88             : 
      89             : #-----------------------------------------------------------------------------
      90             : 
      91             : """
      92             :     out(solver) # process current state of solver
      93             : 
      94             : Process events/results from [`Solver`](@ref).
      95             : 
      96             : 1. Steer `solver`: `out(solver)` may return
      97             :    - [`RK43.State`](@ref): `Ok` "commits" step, `RK43.Stopped` stops integration,
      98             :     `OutOfDomain` repeats step with adaptation or bisection.
      99             :   - `Bool`, where `true` and `false are mapped to `Ok` and `Stopped`,
     100             :      respectively.
     101             :    - `nothing`, which is equivalent to `Ok`
     102             : 2. Store results if `out` is associated with an [`RK43.AbstractSink`](@ref).
     103             : 
     104             : !!! note
     105             :     Multiple outputs may be cascaded to a [`Pipeline`](@ref). A
     106             :     [`RK43.Pipeline`](@ref) is an [`RK43.AbstractOutput`](@ref).
     107             : 
     108             : See also [`RK43.Pipeline`](@ref), [`RK43.AbstractSink`](@ref), [`RK43.Output`](@ref),
     109             : [`Callback`](@ref), [`NoOutput`](@ref)
     110             : """
     111             : abstract type AbstractOutput end
     112             : 
     113           0 : (::AbstractOutput)(::Solver{N, T, S}) where {N, T, S} = Ok
     114             : 
     115             : """
     116             : No output: discard all events.
     117             : 
     118             : See also [`RK43.nooutput`](@ref), [`RK43.AbstractOutput`](@ref)
     119             : """
     120         920 : struct NoOutput <: AbstractOutput
     121             : end
     122             : 
     123             : """
     124             : No output: discard all events
     125             : 
     126             : See also [`NoOutput`](@ref)
     127             : """
     128          26 : nooutput() = NoOutput()
     129             : 
     130             : """
     131             : Output that is associated with `Snk`, an [`RK43.AbstractSink`](@ref).
     132             : 
     133             : See also [`RK43.AbstractOutput`](@ref), [`RK43.AbstractSink`](@ref),
     134             : [`RK43.output`](@ref), [`RK43.Callback`](@ref)
     135             : """
     136             : struct Output{Snk} <:AbstractOutput
     137             :     sink::Snk
     138        1066 :     Output{Snk}(sink::Snk) where {Snk<:AbstractSink} = new(sink)
     139             : end
     140             : 
     141       48930 : function (out::Output{Snk})(solver::RK43.Solver{T, N, S}) where {T, N, S, Snk}
     142       48930 :     out.sink(solver.tnew, solver.ynew, solver.dynew)
     143       48930 :     RK43.Ok
     144             : end
     145             : 
     146             : """
     147             :     out = output(sink)
     148             : 
     149             : Create an [`RK43.Output`](@ref) associated with `sink`.
     150             : 
     151             : See also [`RK43.Output`](@ref), [`pushinto`](@ref)
     152             : """
     153        1066 : output(sink::S) where {S<:AbstractSink} = Output{S}(sink)
     154             : 
     155             : """
     156             :     out = pushinto(spline)
     157             : 
     158             : Create an [`RK43.Output`](@ref) and a [`RK43.TryPush`](@Ref) sink associated with
     159             : `spline`.
     160             : 
     161             : See also [`RK43.Output`](@ref), [`RK43.TryPush`](@ref)
     162             : """
     163        1066 : pushinto(s::S) where {S} = output(TryPush{S}(s))
     164             : 
     165             : # TODO: [alg:upstream] filteredpushinto -- push or replace last if Δh < thresh (don't record excessive bisection)
     166             : 
     167             : """
     168             :     cb = callback() do solver
     169             :              # ...
     170             :              Ok   # return state
     171             :          end
     172             : 
     173             : In contrast to [`RK43.Output`](@ref) `Callback` has no [`RK43.AbstractSink`](@ref).
     174             : The called function must return [`RK43.State`](@ref), `Bool` or `nothing` (see
     175             : [`RK43.AbstractOutput`](@ref)).
     176             : 
     177             : See also See also [`RK43.AbstractOutput`](@ref), [`RK43.AbstractSink`](@ref),
     178             : [`callback`](@ref)
     179             : """
     180             : struct Callback{F} <: AbstractOutput
     181             :     callback::F
     182          37 :     Callback{F}(f::F) where {F} = new(f)
     183             : end
     184             : 
     185         473 : function (out::Callback{F})(solver::RK43.Solver{T, N, S}) where {T, N, S, F}
     186         726 :     out.callback(solver)
     187             : end
     188             : 
     189             : """
     190             : Create [`Callback`](@ref)
     191             : """
     192          37 : callback(f::F) where {F} = Callback{F}(f)
     193             : 
     194         229 : _characteristic(s::Bool) = s
     195         330 : _characteristic(s::T) where {T<:Real} = (s >= 0)::Bool
     196             : 
     197           6 : function isstepinside(f::F) where {F}
     198           6 :     callback() do solver
     199         133 :         (f(solver.ynew) |> _characteristic) ? AcceptStep : OutOfDomain
     200             :     end
     201             : end
     202             : 
     203           5 : function isstepinside(f::F, n::Int) where {F}
     204           5 :     callback() do solver
     205         194 :         for t in range(solver.t, solver.tnew, length=n)
     206         157 :             y, _ = _interpolate(solver, t)
     207         157 :             (f(y) |> _characteristic) || return OutOfDomain
     208          75 :         end
     209           0 :         AcceptStep
     210             :     end
     211             : end
     212             : 
     213          10 : function isstepinside(f::F, ::Val{N}) where {F, N}
     214          10 :     callback() do solver
     215         388 :         for t in range(solver.t, solver.tnew, length=N+1)[2:end]
     216         269 :             y, _ = _interpolate(solver, t)
     217         269 :             (f(y) |> _characteristic) || return OutOfDomain
     218         105 :         end
     219           0 :         AcceptStep
     220             :     end
     221             : end
     222             : 
     223             : # NOTE:
     224             : # - Chose not to implement isstepinside via samplestep: avoid passing
     225             : #   generator even though it's probably zero cost.
     226             : # - Special case ::Val{2} makes no sense (need only testing ynew)
     227             : 
     228             : """
     229             :     chi(y) = norm(y) <= 1  # characteristic function of unit circle
     230             :     chi(y) = 1 - norm(y)   # alternative: >= 0 inside unit circle
     231             : 
     232             :     cb = isstepinside(chi)
     233             :     cb = isstepinside(chi, n)
     234             :     cb = isstepinside(chi, Val(N))
     235             : 
     236             : Cretae [`callback`](@ref) that checks if the current, not yet
     237             : [`commit!`](@ref)ed step is inside domain by *sampling* the
     238             : characteristic function of the domain `chi`.
     239             : 
     240             : The characteristic function either returns `Bool` (`true` ⇔ inside) or
     241             : a `sign::Real` (`s >= 0` ⇔ inside).
     242             : 
     243             : !!! warning "precondition"
     244             :     Assumes that the current starting position for the step is inside.
     245             :     (This is *not* tested.)
     246             : 
     247             : !!! warning
     248             :     Testing only the end point of the current, tentative step is
     249             :     sufficient (but *without guarantees*) for **convex** domains
     250             :     **only**!
     251             : 
     252             : !!! warning
     253             :     Testing `n` points by sampling the new, tentative trajectory piece
     254             :     gives **no guarantee** that a boundary intersection is detected!
     255             : 
     256             : !!! note
     257             :     In order to have a guarantee for non-convex domains, one must
     258             :     *intersect* the new, tentative trajectory piece with the boundary
     259             :     curve! This is **not** done by `isstepinside`!
     260             : 
     261             : !!! note
     262             :     Use [`samplestep`](@ref) or [`cpstep`](@ref) if all samples or
     263             :     the control polygon are required, e.g., for intersection tests.
     264             : 
     265             : 
     266             : See also [`callback`](@ref), [`samplestep`](@ref)
     267             : """ isstepinside
     268             : 
     269           1 : function samplestep(f::F, ::Val{N}) where {F, N}
     270           1 :     callback() do solver
     271           5 :         ts = range(solver.t, solver.tnew, length=N)
     272           5 :         ys = (first(_interpolate(solver, t)) for t in ts)
     273           5 :         f(ys)
     274             :     end
     275             : end
     276             : 
     277           2 : function samplestep(f::F, ::Val{2}) where {F}
     278           2 :     callback() do solver
     279          10 :         f((solver.y, solver.ynew))
     280             :     end
     281             : end
     282             : 
     283           1 : samplestep(f::F) where {F} = samplestep(f, Val(2))
     284             : 
     285             : """
     286             :     cb = samplestep(Val(N)) do ys
     287             :         # check positions ys sampled from tentative piece
     288             :         return AccepStep # or OutOfDomain
     289             :     end
     290             : 
     291             :     cb = samplestep(f)  # default: Val(2)
     292             : 
     293             : Sample trajectory piece from current, tentative step
     294             : (with pending [`commit!`](@ref)) at `N` regularly spaced samples
     295             : passed as tuple `ys`.
     296             : 
     297             : If `Val(N)` is omitted, `samplestep` passes the two endpoints `(y,
     298             : ynew)` of the polynomial piece as a tuple.
     299             : 
     300             : !!! note
     301             :     Samples are passed as generator or tuple.
     302             : 
     303             : See also [`callback`](@ref), [`isstepinside`](@ref), [`cpstep`](@ref)
     304             : """ samplestep
     305             : 
     306             : """
     307             :     cb = cpstep() do bs
     308             :         bs :: Tuple
     309             :         # check control polygon of tentative step
     310             :         return AccepStep # or OutOfDomain
     311             :     end
     312             : 
     313             : Provide control polygon defined by Bernstein-Bezier form of tentative
     314             : step ("Bezier points") parametrized over `[0, 1]` for testing
     315             : `OutOfDomain`: The *convex hull property* can provide a guarantee that
     316             : the step is inside (but cannot decide for any step whether it leaves
     317             : the domain).
     318             : 
     319             : See also [`callback`](@ref), [`isstepinside`](@ref), [`samplestep`](@ref)
     320             : """
     321           1 : function cpstep(f::F) where {F}
     322           1 :     callback() do solver
     323           5 :         @massert order0(solver.stepper) == 4 "assume cubic piece"
     324           5 :         (solver.t == solver.tnew) && return RK43.Ok
     325             : 
     326           4 :         h = solver.tnew - solver.t
     327             :         # @show h
     328             : 
     329           4 :         b0 = solver.y
     330           4 :         b1 = b0 + solver.dy*(h/3)
     331           4 :         b3 = solver.ynew
     332           4 :         b2 = b3 - solver.dynew*(h/3)
     333             : 
     334           4 :         f((b0, b1, b2, b3))
     335             :     end
     336             : end
     337             : 
     338             : """
     339             :     out = sample(sink, range(0, 1, length=4))
     340             :     out = sample(sink, [0, 0.25, 0.5, 0.75, 1])
     341             :     out = sample(array, times[, Val(P)])
     342             : 
     343             : Sample solution at given times during integration. Samples can be specified,
     344             : e.g., as range or `Vector`.
     345             : 
     346             : !!! warning "precondition"
     347             :     The sequence of samples must be *increasing* for forward integration
     348             :     **or** *decreasing* for backward integration.
     349             : 
     350             : See also [`RK43.AbstractOutput`](@ref), [`RK43.AbstractSink`](@ref),
     351             : [`sample`](@ref)
     352             : """
     353             : mutable struct Sample{Snk, Ts} <: AbstractOutput
     354             :     const sink::Snk
     355             :     const times::Ts
     356             :     idx::Int
     357        3800 :     Sample{Snk, Ts}(sink::Snk, times::Ts) where {Snk, Ts} =
     358             :         new(sink, times, 1)
     359             : end
     360             : 
     361      147653 : function (out::Sample{Snk, Ts})(solver::RK43.Solver{T, N, S}) where {T, N, S, Snk, Ts}
     362      147653 :     n = length(out.times)
     363      147653 :     i = out.idx
     364             : 
     365      147653 :     (i <= n) || return Ok
     366             : 
     367      147653 :     if isforward(solver)
     368      141313 :         (solver.tnew < out.times[i]) && return Ok
     369             : 
     370       94275 :         while (i <= n) && (out.times[i] < solver.t)
     371           0 :             i+= 1
     372           0 :         end
     373             :     else
     374        6340 :         (solver.tnew > out.times[i]) && return Ok
     375             : 
     376        3700 :          while (i <= n) && (out.times[i] > solver.t)
     377           0 :             i+= 1
     378           0 :          end
     379             :     end
     380             : 
     381      335843 :     while (i <= n) && isinside(solver, out.times[i])
     382      213344 :         out.sink(out.times[i], _interpolate(solver, out.times[i])...)
     383      213344 :         i += 1
     384      213344 :     end
     385             : 
     386       97975 :     out.idx = i
     387           0 :     Ok
     388             : end
     389             : 
     390             : # TODO: [alg] make sample(...) emit/repeat last point at boundaries
     391             : 
     392        2861 : sample(sink::Snk, times::Ts) where {Snk<:AbstractSink, Ts} =
     393             :     Sample{Snk, Ts}(sink, times)
     394             : 
     395         939 : sample(s::S, times::Ts) where {S, Ts} =
     396             :     Sample{TryPush{S}, Ts}(TryPush{S}(s), times)
     397             : 
     398        5522 : function sample(ary::AbstractArray, times::Ts, ::Val{N} = Val(2)) where {Ts, N}
     399        5522 :     sample(indexedfill(ary, Val(N)), times)
     400             : end
     401             : 
     402         100 : sample(ary::AbstractArray, times::Ts, n::Int) where {Ts} =
     403             :     sample(ary, times, Val(n))
     404             : 
     405             : """
     406             :     out = sample(sink, range(0, 1, length=4))
     407             :     out = sample(sink, [0, 0.25, 0.5, 0.75, 1])
     408             : 
     409             : Sample solution at given times during integration. Samples can be specified,
     410             : e.g., as range or `Vector`.
     411             : 
     412             :     out = sample(array, times[, Val(P)])
     413             : 
     414             : Same as above but create an [`indexedfill`](@ref) sink, i.e., store
     415             : events in `array` (`P` defines a projection, i.e., `(t, y, dy)[P]`
     416             : refers to the posotion `y` for `P=2`.
     417             : 
     418             : !!! warning "precondition"
     419             :     The sequence of samples must be *increasing* for forward integration
     420             :     **or** *decreasing* for backward integration.
     421             : 
     422             : See also [`RK43.Sample`](@ref), [`RK43.AbstractOutput`](@ref), [`RK43.AbstractSink`](@ref),
     423             : [`indexedfill`](@ref)
     424             : """ sample
     425             : 
     426             : #-----------------------------------------------------------------------------
     427             : 
     428             : """
     429             :     p = pipeline(out1, out2, ...)
     430             :     p(solver)
     431             :     # calls and checks out1(solver), out2(solver), ...
     432             : 
     433             : Cascade outputs to a new "pipeline output".
     434             : 
     435             : `Pipeline` stages are processed in order. An action onto a `Pipeline`
     436             : is successful only if all stages yield `Ok`:
     437             : 
     438             : - The next stage is evaluated only if the current stage returned `Ok`.
     439             : - If the current stage does not return `OK`, its return value is
     440             :   returned by the pipeline.
     441             : 
     442             : See also [`RK43.AbstractOutput`](@ref), [`pipeline`](@ref)
     443             : """
     444             : struct Pipeline{Args} <: AbstractOutput
     445             :     args::Args
     446        4898 :     function Pipeline{Args}(args) where {Args}
     447           0 :         @massert all(a -> isa(a, AbstractOutput), args)
     448        4898 :         new(args)
     449             :     end
     450             : end
     451             : 
     452          13 : _check_output(state::Bool) = state ? RK43.Ok : RK43.Stopped
     453     1275827 : _check_output(state::RK43.State) = state
     454          21 : _check_output(_) = RK43.Ok
     455             : 
     456      196820 : function (ch::Pipeline{Args})(solver::RK43.Solver{T, N, S}) where {Args, T, N, S}
     457      196820 :     for a in ch.args
     458      590468 :         state = _check_output(a(solver))
     459      393983 :         RK43.issuccess(state) || return state
     460      589783 :     end
     461             : 
     462      196480 :     RK43.Ok
     463             : end
     464             : 
     465        4898 : Base.pipeline(out::Out, args...) where {Out<:AbstractOutput} =
     466             :     Pipeline{typeof(tuple(out, args...))}(tuple(out, args...))
     467             : 
     468           1 : Base.pipeline(ch::Pipeline{Args}, out::Out) where {Args, Out<:AbstractOutput} =
     469             :     pipeline(ch.args..., out)
     470             : 
     471           3 : Base.pipeline(out::Out, ch::Pipeline{Args}) where {Args, Out<:AbstractOutput} =
     472             :     pipeline(out, ch.args...)
     473             : 
     474           1 : Base.pipeline(ch1::Pipeline{Args1}, ch2::Pipeline{Args2}) where {Args1, Args2} =
     475             :     pipeline(ch1.args..., ch2.args...)
     476             : 
     477             : #-----------------------------------------------------------------------------
     478             : 
     479           1 : Base.show(io::IO, ::Snk) where {Snk<:AbstractSink} = print(io, "sink")
     480             : 
     481           1 : Base.show(io::IO, ::TryPush) = print(io, "pushinto")
     482             : 
     483             : 
     484           1 : Base.show(io::IO, ::IndexedFill) = print(io, "indexedfill")
     485             : 
     486           1 : Base.show(io::IO, ::Out) where {Out<:AbstractOutput} =
     487             :     print(io, Out)
     488             : 
     489           3 : Base.show(io::IO, ::NoOutput) = print(io, "nooutput")
     490             : 
     491           1 : Base.show(io::IO, out::Output{Snk}) where {Snk<:AbstractSink} =
     492             :     show(io, out.sink)
     493             : 
     494           1 : function Base.show(io::IO, ch::Pipeline{Args}) where {Args}
     495           1 :     print(io, "Pipeline{")
     496             : 
     497           1 :     out= map(ch.args) do a
     498           2 :         buf = IOBuffer()
     499           2 :         show(buf, a)
     500           2 :         String(take!(buf))
     501             :     end
     502           1 :     print(io, join(out, ", "))
     503             : 
     504           1 :     print(io, "}")
     505             : end
     506             : 
     507             : #-----------------------------------------------------------------------------
     508             : 
     509             : # TODO: [test] check performance (JET & BenchmarkTools, mind @assert and string interpolation)
     510             : 
     511             : # TODO: [test] test dense sampling (forward, backward, edge cases)

Generated by: LCOV version 1.16