Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 3 additions & 1 deletion Project.toml
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@ OrdinaryDiffEqLowOrderRK = "1344f307-1e59-4825-a18e-ace9aa3fa4c6"
PrecompileTools = "aea7be01-6a6a-4083-8856-8a6e6704d82a"
RecursiveArrayTools = "731186ca-8d62-57ce-b412-fbd966d074cd"
SciMLBase = "0bca4576-84f4-4d90-8ffe-ffa030f20462"
SciMLLogging = "a6db7da4-7206-11f0-1eab-35f2a5dbe1d1"
SymbolicIndexingInterface = "2efcf032-c050-4f8e-a9bb-153293bab1f5"
TimerOutputs = "a759f4b9-e2f1-59dc-863e-4aeb61b1ea8f"
Unrolled = "9602ed7d-8fef-5bc8-8597-8f21381861e8"
Expand All @@ -29,7 +30,8 @@ RecursiveArrayTools = "3.39.0, 4"
SafeTestsets = "0.1.0"
SciMLBase = "2.77.0, 3.1"
SciMLIterators = "1"
SciMLTesting = "2.1"
SciMLLogging = "2"
SciMLTesting = "2.4"
SymbolicIndexingInterface = "0.3.36"
Test = "1"
TimerOutputs = "0.5.28, 1.0"
Expand Down
4 changes: 2 additions & 2 deletions docs/make.jl
Original file line number Diff line number Diff line change
Expand Up @@ -24,8 +24,8 @@ makedocs(
collapselevel = 1
),
sitename = "OrdinaryDiffEqOperatorSplitting.jl",
doctest = false,
warnonly = true,
doctest = true,
checkdocs = :exports,
draft = false,
pages = Any[
"Home" => "index.md",
Expand Down
155 changes: 66 additions & 89 deletions docs/src/devdocs/index.md
Original file line number Diff line number Diff line change
@@ -1,110 +1,87 @@
# Developer documentation
# Developer Documentation

## Synchronizers API
!!! warning "Developer extension interface v1"
These interfaces are for packages implementing operator-splitting solvers
or synchronizers. They are not end-user APIs. Version 1 may change only in
a breaking release of OrdinaryDiffEqOperatorSplitting.jl.

A key part of operator splitting algorithms is the synchronization logic. Parameters of one subproblem might need to be kept in sync with the solution of other subproblems and vice versa. To handle this efficiently OrdinaryDiffEqOperatorSplitting.jl provides a small set of utils.
## Solver Extensions

```@docs
OrdinaryDiffEqOperatorSplitting.NoExternalSynchronization
OrdinaryDiffEqOperatorSplitting.forward_sync_subintegrator!
OrdinaryDiffEqOperatorSplitting.backward_sync_subintegrator!
OrdinaryDiffEqOperatorSplitting.need_sync
OrdinaryDiffEqOperatorSplitting.sync_vectors!
```

## Adding Synchronizers

!!! warning

The API is not stable yet and subject to breaking changes.
A solver extension defines an algorithm type with an `inner_algs` tuple, a
concrete cache, `init_cache`, and `_perform_step!`. The tuple shape must mirror
the corresponding `GenericSplitFunction`. A step implementation synchronizes
each child before and after advancing it, and sets `parent.force_stepfail` if a
child reports failure.

You need to provide dispatches for

```@docs; canonical=false
OrdinaryDiffEqOperatorSplitting.forward_sync_subintegrator!
OrdinaryDiffEqOperatorSplitting.backward_sync_subintegrator!
```@docs
OrdinaryDiffEqOperatorSplitting.AbstractOperatorSplittingAlgorithm
OrdinaryDiffEqOperatorSplitting.AbstractOperatorSplittingCache
OrdinaryDiffEqOperatorSplitting.init_cache
OrdinaryDiffEqOperatorSplitting._perform_step!
```

with your custom synchronizer object and add it to the split function construction as follows:
The following sweeps the operators in reverse order, which is a complete
first-order splitting in its own right (it coincides with `LieTrotterGodunov`
exactly when the operators commute):

```julia
f1, f2 = generate_individual_functions() # assuming 3 unknowns each
i1, i2 = generate_solution_indices() # e.g. ([1,2,3], Int[])
synchronizer_tree = generate_my_synchronizer_tree() # e.g. (MySynchronizer([1,2,3]), NoExternalSynchronization())
f = GenericSplitFunction((f1, f2), (i1, i2), synchronizer_tree)
u0 = [-1.0, 1.0, 0.0]
tspan = (0.0, 1.0)
prob = OperatorSplittingProblem(f, u0, tspan)
```

## Adding Solvers

!!! warning

The API is not stable yet and subject to breaking changes.

To add a new solver just define two new structs, one for the algorithm description and one for the algorithm cache and dispatch internal functions, as follows:
import OrdinaryDiffEqOperatorSplitting as OS

```julia
using SciMLBase, OrdinaryDiffEqOperatorSplitting
struct MySimpleFirstOrderAlgorithm{InnerAlgorithmTypes} <:
OrdinaryDiffEqOperatorSplitting.AbstractOperatorSplittingAlgorithm
inner_algs::InnerAlgorithmTypes # Tuple of solver for the problem sequence
struct ReverseLieTrotterGodunov{AlgTupleType} <: OS.AbstractOperatorSplittingAlgorithm
inner_algs::AlgTupleType
end

struct MySimpleFirstOrderCache{uType, uprevType, iiType} <:
OrdinaryDiffEqOperatorSplitting.AbstractOperatorSplittingCache
struct ReverseLieTrotterGodunovCache{uType, uprevType} <: OS.AbstractOperatorSplittingCache
u::uType
uprev::uprevType
inner_caches::iiType
end

function OrdinaryDiffEqOperatorSplitting.init_cache(
f::GenericSplitFunction, alg::MySimpleFirstOrderAlgorithm;
function OS.init_cache(
f::GenericSplitFunction, alg::ReverseLieTrotterGodunov;
uprev::AbstractArray, u::AbstractVector,
inner_caches,
alias_uprev = true,
alias_u = false
)
@assert length(inner_caches) == 2
_uprev = alias_uprev ? uprev : SciMLBase.recursivecopy(uprev)
_u = alias_u ? u : SciMLBase.recursivecopy(u)
return MySimpleFirstOrderAlgorithmCache(_u, _uprev, inner_caches)
)
return ReverseLieTrotterGodunovCache(u, uprev)
end

@inline function OrdinaryDiffEqOperatorSplitting.advance_solution_to!(
outer_integrator::OperatorSplittingIntegrator, subintegrators::Tuple,
solution_indices::Tuple, synchronizers::Tuple,
cache::MySimpleFirstOrderAlgorithmCache, tnext)
# We assume that the integrators are already synced
(;inner_caches) = cache

# Advance first subproblem
OrdinaryDiffEqOperatorSplitting.forward_sync_subintegrator!(
outer_integrator, subintegrators[1], solution_indices[1], synchronizers[1])
OrdinaryDiffEqOperatorSplitting.advance_solution_to!(
outer_integrator, subintegrators[1], solution_indices[1],
synchronizers[1], inner_caches[1], tnext)
if subintegrators[1].sol.retcode ∉
(SciMLBase.ReturnCode.Default, SciMLBase.ReturnCode.Success)
return
end
OrdinaryDiffEqOperatorSplitting.backward_sync_subintegrator!(
outer_integrator, subintegrators[1], solution_indices[1], synchronizers[1])

# Advance second subproblem
OrdinaryDiffEqOperatorSplitting.forward_sync_subintegrator!(
outer_integrator, subintegrators[2], solution_indices[2], synchronizers[2])
OrdinaryDiffEqOperatorSplitting.advance_solution_to!(
outer_integrator, subintegrators[2], solution_indices[2],
synchronizers[2], inner_caches[2], tnext)
if subintegrators[2].sol.retcode ∉
(SciMLBase.ReturnCode.Default, SciMLBase.ReturnCode.Success)
return
function OS._perform_step!(
parent, children::Tuple, cache::ReverseLieTrotterGodunovCache, dt
)
for i in reverse(eachindex(children))
child = children[i]
idxs = parent.child_solution_indices[i]
sync = parent.child_synchronizers[i]

OS.forward_sync_subintegrator!(parent, child, idxs, sync)
OS.advance_solution_by!(parent, child, dt)
if OS._child_failed(child)
parent.force_stepfail = true
return nothing
end
OS.backward_sync_subintegrator!(parent, child, idxs, sync)
end
OrdinaryDiffEqOperatorSplitting.backward_sync_subintegrator!(
outer_integrator, subintegrators[2], solution_indices[2], synchronizers[2])

# Done :)
return nothing
end
```

The algorithm is then used like any built-in one, with one inner algorithm per
leaf operator:

```julia
alg = ReverseLieTrotterGodunov((Euler(), Euler()))
sol = solve(OperatorSplittingProblem(f, u0, tspan), alg; dt = 0.01)
```

## Synchronizer Extensions

Synchronizers communicate parameters and solution buffers between parent and
child integrators. Define the external synchronization methods for the custom
synchronizer type, then supply one synchronizer for every operator in a
`GenericSplitFunction`.

```@docs
OrdinaryDiffEqOperatorSplitting.NoExternalSynchronization
OrdinaryDiffEqOperatorSplitting.forward_sync_subintegrator!
OrdinaryDiffEqOperatorSplitting.backward_sync_subintegrator!
OrdinaryDiffEqOperatorSplitting.need_sync
OrdinaryDiffEqOperatorSplitting.sync_vectors!
```
64 changes: 61 additions & 3 deletions src/OrdinaryDiffEqOperatorSplitting.jl
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@ import Unrolled: @unroll

import BinaryHeaps

import SciMLBase, DiffEqBase
import SciMLBase, DiffEqBase, SciMLLogging
import SciMLBase: ReturnCode
import SciMLBase: DEIntegrator, NullParameters, isadaptive
import SymbolicIndexingInterface: variable_symbols
Expand All @@ -21,8 +21,12 @@ import OrdinaryDiffEqCore: OrdinaryDiffEqCore, isdtchangeable,
# In OrdinaryDiffEq v7 / DiffEqBase v7, passing verbose::Bool to inner ODE
# integrators is no longer supported. Convert Bool → DEVerbosity when available.
@static if isdefined(DiffEqBase, :DEVerbosity)
_inner_verbose(verbose::Bool) = verbose ? DiffEqBase.DEFAULT_VERBOSE : DiffEqBase.DEVerbosity(DiffEqBase.None())
const DEFAULT_VERBOSITY = DiffEqBase.DEFAULT_VERBOSE
# `DEVerbosity()` is what DiffEqBase itself defaults `verbose` to; spelling it
# out keeps this off the non-public `DEFAULT_VERBOSE` binding.
_inner_verbose(verbose::Bool) = verbose ?
DiffEqBase.DEVerbosity() :
DiffEqBase.DEVerbosity(SciMLLogging.None())
const DEFAULT_VERBOSITY = DiffEqBase.DEVerbosity()
else
const DEFAULT_VERBOSITY = false
end
Expand All @@ -38,9 +42,63 @@ _is_verbose(verbose) = true
end

abstract type AbstractOperatorSplitFunction <: SciMLBase.AbstractODEFunction{true} end

"""
AbstractOperatorSplittingAlgorithm

Abstract supertype for operator-splitting algorithms.

## Extension interface v1

Solver extensions must provide an `inner_algs` tuple matching the associated
[`GenericSplitFunction`](@ref), an [`init_cache`](@ref) method, and an
[`_perform_step!`](@ref) method. This is a developer extension interface, not
an end-user API. It may change in a breaking release of this package.
"""
abstract type AbstractOperatorSplittingAlgorithm end

"""
AbstractOperatorSplittingCache

Abstract supertype for caches used by operator-splitting algorithms.

## Extension interface v1

Define a concrete cache subtype and return it from [`init_cache`](@ref) when
implementing an [`AbstractOperatorSplittingAlgorithm`](@ref). This is a
developer extension interface, not an end-user API. It may change in a
breaking release of this package.
"""
abstract type AbstractOperatorSplittingCache end

"""
init_cache(f::GenericSplitFunction, alg::AbstractOperatorSplittingAlgorithm; uprev, u)

Create the cache for an operator-splitting algorithm.

## Extension interface v1

Extensions must dispatch on their concrete algorithm type and return a concrete
[`AbstractOperatorSplittingCache`](@ref). `u` and `uprev` are the mutable local
solution buffers for the current node. This is a developer extension interface,
not an end-user API. It may change in a breaking release of this package.
"""
function init_cache end

"""
_perform_step!(parent, children::Tuple, cache::AbstractOperatorSplittingCache, dt)

Advance one operator-splitting step for a developer-defined algorithm.

## Extension interface v1

Extensions must synchronize every child before and after advancing it and set
`parent.force_stepfail` when a child fails. This is a developer extension
interface, not an end-user API. It may change in a breaking release of this
package.
"""
function _perform_step! end

@inline SciMLBase.isadaptive(::AbstractOperatorSplittingAlgorithm) = false
@inline isdtchangeable(alg::AbstractOperatorSplittingAlgorithm) = all(isdtchangeable.(alg.inner_algs))

Expand Down
14 changes: 7 additions & 7 deletions src/config_tree.jl
Original file line number Diff line number Diff line change
Expand Up @@ -210,32 +210,32 @@ Base.setindex!(opt::TreeOption, v, node::SplitNode) =
TreeOptionSubtree

The target of a broadcast assignment into a [`TreeOption`](@ref), produced by
`Base.dotview`. Assigning into it writes one value to a node and all of its
`Base.Broadcast.dotview`. Assigning into it writes one value to a node and all of its
descendants.
"""
struct TreeOptionSubtree{T}
node::TreeOption{T}
end

Base.dotview(opt::TreeOption) = TreeOptionSubtree(opt)
Base.dotview(opt::TreeOption, i::Integer, is::Integer...) =
Base.Broadcast.dotview(opt::TreeOption) = TreeOptionSubtree(opt)
Base.Broadcast.dotview(opt::TreeOption, i::Integer, is::Integer...) =
TreeOptionSubtree(_option_node(opt, _path(i, is...)))
Base.dotview(opt::TreeOption, node::SplitNode) =
Base.Broadcast.dotview(opt::TreeOption, node::SplitNode) =
TreeOptionSubtree(_option_node(opt, node.path))

const _Broadcasted = Base.Broadcast.Broadcasted

# A TreeOption is not a collection as far as broadcasting is concerned. Without this
# the default `broadcastable` tries to `collect` it, and `opt .op= x` fails with a
# MethodError about iteration instead of the explanation in `_broadcast_value`.
Base.broadcastable(opt::TreeOption) = Ref(opt)
Base.Broadcast.broadcastable(opt::TreeOption) = Ref(opt)

# Broadcast assignment into a TreeOption only ever means "give every node of this
# subtree the same value". Anything else that lowers to the same call is rejected
# rather than silently given a made up meaning -- see `_broadcast_value`.
Base.materialize!(dest::TreeOptionSubtree, bc::_Broadcasted) =
Base.Broadcast.materialize!(dest::TreeOptionSubtree, bc::_Broadcasted) =
_fill_subtree!(dest.node, _broadcast_value(bc))
Base.materialize!(dest::TreeOption, bc::_Broadcasted) =
Base.Broadcast.materialize!(dest::TreeOption, bc::_Broadcasted) =
_fill_subtree!(dest, _broadcast_value(bc))

function _broadcast_value(bc::_Broadcasted)
Expand Down
21 changes: 19 additions & 2 deletions src/function.jl
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,18 @@
GenericSplitFunction(functions::Tuple, solution_indices::Tuple)
GenericSplitFunction(functions::Tuple, solution_indices::Tuple, synchronizers::Tuple)

This type of function describes a set of connected inner functions in mass-matrix form, as usually found in operator splitting procedures.
Construct an operator tree for an [`OperatorSplittingProblem`](@ref).

`functions` contains ODE operators or nested `GenericSplitFunction`s.
`solution_indices[i]` gives the entries of the parent solution used by
`functions[i]`. The three-argument form accepts one synchronizer per operator;
the two-argument form uses [`NoExternalSynchronization`](@ref) for every
operator.

# Arguments
- `functions`: Tuple of ODE operators or nested split functions.
- `solution_indices`: Tuple of parent-solution index collections.
- `synchronizers`: Tuple of synchronization objects, one for each operator.
"""
struct GenericSplitFunction{fSetType <: Tuple, idxSetType <: Tuple, sSetType <: Tuple} <:
AbstractOperatorSplitFunction
Expand Down Expand Up @@ -36,7 +47,13 @@ num_operators(f::GenericSplitFunction) = length(f.functions)
"""
NoExternalSynchronization()

Indicator that no synchronization between parameters and solution vectors is necessary.
Marker indicating that a split operator requires no external parameter
synchronization.

## Extension interface v1

This marker is for synchronizer implementations. It is not an end-user API and
may change in a breaking release of this package.
"""
struct NoExternalSynchronization end

Expand Down
6 changes: 3 additions & 3 deletions src/integrator.jl
Original file line number Diff line number Diff line change
Expand Up @@ -789,7 +789,7 @@ function step_footer!(integrator::AnySplitIntegrator)
end

function abort_below_dtmin!(integrator::AnySplitIntegrator)
abs(integrator.dt) > abs(OrdinaryDiffEqCore.timedepentdtmin(integrator)) && return nothing
abs(integrator.dt) > abs(DiffEqBase.timedepentdtmin(integrator)) && return nothing
_is_verbose(integrator.opts.verbose) &&
@warn("dt <= dtmin. Aborting. There is either an error in your model specification or the true solution is unstable.")
_set_retcode!(integrator, ReturnCode.DtLessThanMin)
Expand Down Expand Up @@ -901,7 +901,7 @@ function SciMLBase.check_error(integrator::OperatorSplittingIntegrator)
integrator.sol.retcode != ReturnCode.Default
return integrator.sol.retcode
end
if DiffEqBase.NAN_CHECK(integrator.dtcache) || DiffEqBase.NAN_CHECK(integrator.dt)
if isnan(integrator.dtcache) || isnan(integrator.dt)
_is_verbose(integrator.opts.verbose) &&
@warn("NaN dt detected. Likely a NaN value in the state, parameters, or derivative value caused this outcome.")
return ReturnCode.DtNaN
Expand All @@ -914,7 +914,7 @@ function SciMLBase.check_error(integrator::SplitSubIntegrator)
integrator.status.retcode != ReturnCode.Default
return integrator.status.retcode
end
if DiffEqBase.NAN_CHECK(integrator.dtcache) || DiffEqBase.NAN_CHECK(integrator.dt)
if isnan(integrator.dtcache) || isnan(integrator.dt)
_is_verbose(integrator.opts.verbose) &&
@warn("NaN dt detected. Likely a NaN value in the state, parameters, or derivative value caused this outcome.")
return ReturnCode.DtNaN
Expand Down
Loading
Loading