The AbstractSciMLOperator Interface
SciMLOperators.AbstractSciMLOperator — Type
abstract type AbstractSciMLOperator{T}AbstractSciMLOperator is the extension point for matrix-like and matrix-free operators. A subtype represents an operator $L(u,p,t)$ whose action on an array $v$ is written $L(u,p,t)v$. The subtype may be constant, state-dependent, or time-dependent, and may be composed with other SciML operators through the lazy algebra.
This is an interface type, not a constructor. The concrete type should be public only when users are expected to construct or extend it; otherwise use the qualified developer-facing API documented in this section.
Mathematical Notation
An AbstractSciMLOperator $L$ is an operator which is used to represent the following type of equation:
\[w = L(u,p,t)[v]\]
where L[v] is the operator application of $L$ on the vector $v$.
Construction and Extension Rules
AbstractSciMLOperator is an interface, not a concrete constructor. New operator types should subtype it with the scalar element type T and must preserve their mathematical action when they participate in lazy algebra.
Required Interface
A concrete subtype must implement size(L) -> (m, n), *(L, v), and mul!(w, L, v). The returned action has leading size m for an input whose leading size is n, and the in-place method must return w after writing the result. The scaling form mul!(w, L, v, α, β) is required when has_mul!(L) == true; it must compute $w \leftarrow α(Lv) + βw$.
has_mul(L), has_mul!(L), has_ldiv(L), and has_ldiv!(L) are promises, not capability probes: return true only when the corresponding operation is valid for all compatible inputs. convert(AbstractMatrix, L) is optional and should be defined only when isconvertible(L) == true; its result must have the same size and action as L in its current state.
An AbstractSciMLOperator can be called like a function in the following ways:
L(v, u, p, t)- Out-of-place application wherevis the action vector anduis the update vectorL(w, v, u, p, t)- In-place application wherewis the destination,vis the action vector, anduis the update vectorL(w, v, u, p, t, α, β)- In-place application with scaling:w = α*(L*v) + β*w
Operator state can be updated separately from application:
update_coefficients!(L, u, p, t)for in-place operator updateL = update_coefficients(L, u, p, t)for out-of-place operator update
SciMLOperators also overloads Base.*, LinearAlgebra.mul!, LinearAlgebra.ldiv! for operator evaluation without updating operator state. An AbstractSciMLOperator behaves like a matrix in these methods. Allocation-free methods, suffixed with a ! often need cache arrays. To precache an AbstractSciMLOperator, call the function L = cache_operator(L, input_vector).
Required Interface For Subtypes
A concrete subtype must define Base.size(L) and one of the following application paths:
Base.:*(L, v)for out-of-place matrix-like application.LinearAlgebra.mul!(w, L, v)and, whenhas_mul!(L)istrue,LinearAlgebra.mul!(w, L, v, α, β)for in-place application.Base.convert(AbstractMatrix, L)whenisconvertible(L)istrue.
If the operator state depends on (u, p, t) or accepted keyword arguments, the subtype must implement update_coefficients for out-of-place state updates or update_coefficients! for in-place state updates. The out-of-place form returns a new operator and leaves L unchanged; the in-place form returns nothing. Composite operators assume these update methods may be called recursively on every operator returned by getops(L).
The positional arguments are forwarded unchanged through a composite operator. u is the state supplied by the caller and is not necessarily the same shape as the action vector v; an operator whose action is nonlinear in v should generally use FunctionOperator and report islinear(L) == false. For a constant leaf, the default update is a no-op. A stateful leaf must override isconstant rather than inheriting the empty-child default.
Subtypes that need preallocated work arrays for allocation-free application must implement cache_self(L, v) for their own caches, cache_internals(L, v) for child-operator caches, or both. cache_operator(L, v) calls these hooks and downstream solvers may call it before repeated mul! evaluations.
Caching Rules
cache_operator(L, v) may return either L or a cached replacement. A subtype that advertises has_mul!(L) == true must ensure the cached result is ready for repeated mul! calls with compatible vectors. Cache hooks may not change the mathematical action, dimensions, or trait values of the operator. A cached operator's scratch is mutable and is not safe to use concurrently unless the subtype explicitly provides that guarantee.
Composite types expose their children through the developer-facing getops method. A new composite must forward state updates, caching, and traits to every child that contributes to its action. The public action must remain unchanged by flattening, caching, or updating the composition.
Trait Rules
Trait functions such as isconstant, islinear, isconvertible, has_concretization, has_mul, has_mul!, has_ldiv, and has_ldiv! are part of the public operator interface. A trait returning true is a promise that the corresponding operation is valid for inputs with compatible sizes. For example, has_mul!(L) means mul!(w, L, v) is available, and has_concretization(L) means either convert(AbstractMatrix, L) or convert(Number, L) can materialize the operator state without changing its mathematical action.
isconstant(L) means repeated calls to update_coefficients[!] are not required to keep L current. islinear(L) means the action is linear in the vector being multiplied; state dependence on (u, p, t) is still allowed for a linear operator.
Keyword Arguments
When an operator accepts keywords during updates, its constructor must record the accepted names with accepted_kwargs, normally as Val((:name1, :name2)). Composite operators forward only those accepted keywords to each component. Extension authors must therefore accept (u, p, t; kwargs...) consistently in every update and application method they advertise. An unlisted keyword must not be silently passed to a leaf update function.
Standard Actions
The behavior of a SciMLOperator is indistinguishable from an AbstractMatrix. These operators can be passed to linear solver packages, and even to ordinary differential equation solvers. The list of overloads to the AbstractMatrix interface includes, but is not limited to, the following:
Base: size, zero, one, +, -, *, /, \, ∘, inv, adjoint, transpose, convertLinearAlgebra: mul!, ldiv!, lmul!, rmul!, factorize, issymmetric, ishermitian, isposdefSparseArrays: sparse, issparse
Multidimensional arrays and batching
SciMLOperator can also be applied to AbstractMatrix subtypes where operator-evaluation is done column-wise.
using LinearAlgebra, SciMLOperators
N = 4
K = 10
L = MatrixOperator(Matrix(I, N, N))
u_mat = rand(N, K)
v_mat = L(u_mat, nothing, nothing, 0.0)
size(v_mat) == (N, K) # trueL can also be applied to AbstractArrays that are not AbstractVecOrMats so long as their size in the first dimension is appropriate for matrix-multiplication. Internally, SciMLOperators reshapes an N-dimensional array to an AbstractMatrix, and applies the operator via matrix-multiplication.
Operator update
This package can also be used to write state-dependent, time-dependent, and parameter-dependent operators, whose state can be updated per a user-defined function. The updates can be done in-place, i.e. by mutating the object, or out-of-place, i.e. in a non-mutating, Zygote-compatible way.
For example,
using LinearAlgebra, SciMLOperators
n = 4
v = rand(n)
u = rand(n)
p = rand(n)
t = rand()
# out-of-place update
mat_update_func = (A, u, p, t) -> t * (p * u')
sca_update_func = (a, u, p, t) -> t * sum(p)
M = MatrixOperator(zeros(n, n); update_func = mat_update_func)
α = ScalarOperator(0.0; update_func = sca_update_func)
L = α * M
L = cache_operator(L, v)
# L is initialized with zero state
L * v == zeros(n) # true
# update operator state with `(u, p, t)`
L = update_coefficients(L, u, p, t)
# and multiply
L * v != zeros(n) # true
# updates state and evaluates L*v at (u, p, t)
L(v, u, p, t) != zeros(n) # trueThe out-of-place evaluation function L(v, u, p, t) calls update_coefficients under the hood, which recursively calls the update_func for each component SciMLOperator. Therefore, the out-of-place evaluation function is equivalent to calling update_coefficients followed by Base.*. Notice that the out-of-place evaluation does not return the updated operator.
On the other hand, the in-place evaluation function, L(w, v, u, p, t), mutates L, and is equivalent to calling update_coefficients! followed by mul!. The in-place update behavior works the same way, with a few <!>s appended here and there. For example,
using LinearAlgebra, SciMLOperators
n = 4
w = rand(n)
v = rand(n)
u = rand(n)
p = rand(n)
t = rand()
# in-place update
_A = rand(n, n)
mat_update_func! = (A, u, p, t) -> (copy!(A, _A); lmul!(t, A); nothing)
M = MatrixOperator(zeros(n, n); update_func! = mat_update_func!)
L = M
L = cache_operator(L, v)
# L is initialized with zero state
L * v == zeros(n) # true
# update L in-place
update_coefficients!(L, v, p, t)
# and multiply
mul!(w, L, v) != zeros(n) # true
# updates L in-place, and evaluates w=L*v at (u, p, t)
L(w, v, u, p, t) != zeros(n) # trueThe update behavior makes this package flexible enough to be used in OrdinaryDiffEq. As the parameter object p is often reserved for sensitivity computation via automatic-differentiation, a user may prefer to pass in state information via other arguments. For that reason, we allow update functions with arbitrary keyword arguments.
using SciMLOperators
n = 4
v = rand(n)
u = rand(n)
p = rand(n)
t = 0.0
mat_update_func = (A, u, p, t; scale = 0.0) -> scale * (p * u')
M = MatrixOperator(zeros(n, n); update_func = mat_update_func,
accepted_kwargs = Val((:scale,)))
M(v, u, p, t) == zeros(n) # true
M(v, u, p, t; scale = 1.0) != zeros(n)SciMLOperators.AbstractSciMLScalarOperator — Type
AbstractSciMLScalarOperator{T} <: AbstractSciMLOperator{T}Abstract interface for a scalar-valued linear scaling operator.
Interface Rules
Subtypes must provide convert(Number, operator) for their current scalar value and eltype through the type parameter T. Scalar application to an array must preserve the array shape. If the subtype is stateful, its update methods use the same (u, p, t; kwargs...) contract as AbstractSciMLOperator: the out-of-place form returns a new scalar operator, while the in-place form mutates the operator and returns nothing.
islinear describes linearity in the array being scaled. has_ldiv and has_ldiv! may be true only when the current scalar value is invertible. Scalar addition, multiplication, division, and inversion remain lazy so that later updates affect the composed expression. Use ScalarOperator when a premade implementation is sufficient.
Examples
using SciMLOperators
struct MutableScale <: AbstractSciMLScalarOperator{Float64}
value::Float64
end
Base.convert(::Type{Number}, L::MutableScale) = L.value
Base.:*(L::MutableScale, v::AbstractArray) = L.value .* v
SciMLOperators.islinear(::MutableScale) = true
L = MutableScale(2.0)
L * [3.0, 4.0]
concretize(L) == 2.0Use ScalarOperator to construct a concrete scalar operator. Custom scalar operator types should only claim traits such as has_ldiv when the converted scalar has the corresponding operation for the current state.
Interface API Reference
SciMLOperators.update_coefficients — Function
update_coefficients(L, u, p, t; kwargs...)
Update the state of L based on u, input vector, p parameter object, t, and keyword arguments. Internally, update_coefficients calls the user-provided update_func method for every component operator in L with the positional arguments (u, p, t) and keyword arguments corresponding to the symbols provided to the operator via kwarg accepted_kwargs.
This method is out-of-place, i.e. fully non-mutating and Zygote-compatible.
The user-provided update_func[!] must not use u in its computation. Positional argument (u, p, t) to update_func[!] are passed down by update_coefficients[!](L, u, p, t), where u is the input-vector to the composite AbstractSciMLOperator. For that reason, the values of u, or even shape, may not correspond to the input expected by update_func[!]. If an operator's state depends on its input vector, then it is, by definition, a nonlinear operator. We recommend sticking such nonlinearities in FunctionOperator. This topic is further discussed in this issue.
Arguments
L: Operator whose state should be updated.u: State or update vector supplied to the operator.p: Parameter object supplied to the operator.t: Time or other scalar update value.
Keyword Arguments
kwargs...: Keywords accepted by the operator's update function and recorded by itsaccepted_kwargsconstructor option.
Returns
A new operator representing the updated state. L is not mutated.
Examples
using SciMLOperators
mat_update_func = (A, u, p, t; scale = 1.0) -> p * p' * scale * t
M = MatrixOperator(zeros(4,4); update_func = mat_update_func,
accepted_kwargs = Val((:scale,)))
L = M + IdentityOperator(4)
u = rand(4)
p = rand(4)
t = 1.0
v = rand(4)
# Update the operator to `(u,p,t)` and apply it to `v`
L = update_coefficients(L, u, p, t; scale = 2.0)
result = L * v
# Or use the interface which separates the update from the application
result = L(v, u, p, t; scale = 2.0)SciMLOperators.update_coefficients! — Function
update_coefficients!(L, u, p, t; kwargs...)
Update in-place the state of L based on u, input vector, p parameter object, t, and keyword arguments. Internally, update_coefficients! calls the user-provided mutating update_func! method for every component operator in L with the positional arguments (u, p, t) and keyword arguments corresponding to the symbols provided to the operator via kwarg accepted_kwargs.
The user-provided update_func[!] must not use u in its computation. Positional argument (u, p, t) to update_func[!] are passed down by update_coefficients[!](L, u, p, t), where u is the input-vector to the composite AbstractSciMLOperator. For that reason, the values of u, or even shape, may not correspond to the input expected by update_func[!]. If an operator's state depends on its input vector, then it is, by definition, a nonlinear operator. We recommend sticking such nonlinearities in FunctionOperator. This topic is further discussed in this issue.
Arguments
L: Operator whose state should be updated.u: State or update vector supplied to the operator.p: Parameter object supplied to the operator.t: Time or other scalar update value.
Keyword Arguments
kwargs...: Keywords accepted by the operator's mutating update function and recorded by itsaccepted_kwargsconstructor option.
Returns
nothing. The operator L is mutated in place.
Examples
using SciMLOperators
_A = rand(4, 4)
mat_update_func! = (A, u, p, t; scale = 1.0) -> copy!(A, _A)
M = MatrixOperator(zeros(4,4); update_func! = mat_update_func!)
L = M + IdentityOperator(4)
u = rand(4)
p = rand(4)
t = 1.0
v = rand(4)
update_coefficients!(L, u, p, t)
L * vSciMLOperators.cache_operator — Function
cache_operator(L, u)
Allocate caches for L for in-place evaluation with u-like input vectors.
Arguments
L: Operator to prepare.u: Prototype vector or matrix whose shape determines compatible scratch.
Returns
L or a cached replacement. The returned operator must have the same action, size, and trait values as the input operator.
Interface Rules
Call this before repeated mul! or callable in-place evaluations when the operator needs scratch storage. A custom type that needs storage should implement cache_self for its own buffers and cache_internals for child operators.
SciMLOperators.concretize — Function
concretize(L) -> AbstractMatrix
concretize(L) -> NumberConvert SciMLOperator to a concrete type via eager fusion. This method is a no-op for types that are already concrete.
Arguments
L: Matrix-like or scalar operator to materialize.
Returns
An AbstractMatrix for matrix-like operators or a Number for scalar operators, with the same current action as L.
Errors
Throws the conversion error from L when the operator does not support the requested concrete representation. Check isconvertible(L) before relying on eager matrix materialization.
SciMLOperators.jacobian_stale — Function
jacobian_stale(W) -> BoolReport whether the Jacobian-dependent state of W may have changed since a consumer last declared its cached factorization or reduction current.
Arguments
W: The object whose Jacobian-dependent state is being queried. For aWOperator, this is itsjac_staleflag. Other objects use the conservative fallback.
Returns
true means that a consumer must assume its Jacobian-dependent cache is stale. false means that the consumer may reuse that cache with respect to the Jacobian; it must still handle changes to gamma according to its own algorithm. The fallback returns true.
Interface Rules
A solver that owns a factorization or reduction of W.J should query this predicate before reusing it and call mark_jacobian_current! only after refreshing its cache. The flag is shared by consumers, so a single WOperator must not be used as the cache source for independent consumers; give each consumer its own operator or track its own generation.
Examples
using LinearAlgebra, SciMLOperators
W = WOperator{true}(I, 0.5, [1.0 0.0; 0.0 2.0], zeros(2))
jacobian_stale(W) # true until the consumer has initialized its cache
mark_jacobian_current!(W)
!jacobian_stale(W)SciMLOperators.mark_jacobian_updated! — Function
mark_jacobian_updated!(W) -> WAnnounce that the contents of W's Jacobian have changed, invalidating any Jacobian-dependent factorization or reduction a solver may be holding. A no-op for anything that does not track a Jacobian, so it is safe to call unconditionally after an in-place Jacobian update.
Arguments
W: AWOperator, or any object for which the unconditional no-op fallback is desired.
Returns
The same object W. For a WOperator, its jac_stale flag is set to true.
Interface Rules
Call this after the contents of W.J have been changed in place. Changing gamma is a different event and needs no announcement because it is visible as a field. This function does not refactorize or otherwise update a cache.
Examples
using LinearAlgebra, SciMLOperators
J = [1.0 0.0; 0.0 2.0]
W = WOperator{true}(I, 0.5, J, zeros(2))
mark_jacobian_current!(W)
J .= 3I
mark_jacobian_updated!(W)
jacobian_stale(W) # trueSciMLOperators.mark_jacobian_current! — Function
mark_jacobian_current!(W) -> WDeclare that the caller's Jacobian-dependent cache is up to date, clearing the flag mark_jacobian_updated! set. A no-op off the type.
Arguments
W: AWOperator, or any object for which the unconditional no-op fallback is desired.
Returns
The same object W. For a WOperator, its jac_stale flag is set to false.
Interface Rules
Call this only after the consumer has actually refreshed its factorization or reduction. Calling it does not inspect J and does not refresh any cache.
Examples
using LinearAlgebra, SciMLOperators
W = WOperator{true}(I, 0.5, [1.0 0.0; 0.0 2.0], zeros(2))
mark_jacobian_current!(W)
@assert !jacobian_stale(W)This is a single shared flag, not a per-consumer generation count. Two solvers caching factorizations of the same W will race: whichever clears first hides the event from the other, which then reuses a stale factorization. Give each consumer its own WOperator if you need more than one.
SciMLOperators.DEFAULT_UPDATE_FUNC — Function
DEFAULT_UPDATE_FUNC(A, u, p, t)
The default update function for AbstractSciMLOperators, a no-op that leaves the operator state unchanged.
Arguments
A: Current operator state.u: State or update vector supplied by the caller.p: Parameter object supplied by the caller.t: Time or other scalar update value.
Returns
The unchanged value A. The positional update arguments are accepted so this function can be used wherever an operator update function is expected.
Traits
SciMLOperators.isconstant — Function
isconstant(_)
Checks if an L's state is constant or needs to be updated by calling update_coefficients.
Arguments
L: Operator or operator-like object to inspect.
Returns
true when updating with supported (u, p, t; kwargs...) values cannot change the operator action. A stateful leaf must override this trait explicitly.
SciMLOperators.iscached — Function
iscached(L)
Checks whether L has preallocated caches for inplace evaluations.
Arguments
L: Operator to inspect. For a composite, all child caches are checked.
Returns
true when the operator and every child needed for in-place evaluation has a usable cache, otherwise false.
Check if SciMLOperator L has preallocated cache-arrays for in-place computation.
SciMLOperators.issquare — Function
Checks if size(L, 1) == size(L, 2).
Arguments
L: Matrix-like object or operator to inspect.
Returns
true for a square operator and false for a rectangular operator or vector. For multiple arguments, the result is the elementwise conjunction of their individual square predicates.
SciMLOperators.islinear — Function
islinear(_)
Checks if L is a linear operator.
Arguments
L: Operator to inspect.
Returns
true when the action is linear in the action vector, even if the operator state depends on (u, p, t).
SciMLOperators.isconvertible — Function
isconvertible(L) -> BoolChecks if L can be cheaply converted to an AbstractMatrix via eager fusion.
Arguments
L: Operator to inspect.
Returns
true only when convert(AbstractMatrix, L) is a supported operation for the current operator state. This trait does not require eager conversion to be the preferred execution path.
SciMLOperators.has_adjoint — Function
has_adjoint(L)
Check if adjoint(L) is lazily defined.
Arguments
L: Operator to inspect.
Returns
true only when adjoint(L) is supported without relying on an unadvertised conversion fallback.
SciMLOperators.has_expmv — Function
has_expmv(L)
Check if expmv(L, v, t), equivalent to exp(t * A) * v, is defined for Number t, and AbstractArray u of appropriate size.
Arguments
L: Operator to inspect.
Returns
true only when the out-of-place exponential action is supported.
SciMLOperators.has_expmv! — Function
has_expmv!(L)
Check if expmv!(w, L, v, t), equivalent to mul!(w, exp(t * A), v), is defined for Number t, and AbstractArrays w, v of appropriate sizes.
Arguments
L: Operator to inspect.
Returns
true only when the in-place exponential action is part of the operator's supported contract.
SciMLOperators.has_exp — Function
has_exp(L)
Check if exp(L) is defined lazily.
Arguments
L: Operator to inspect.
Returns
true only when exp(L) is a supported lazy operation.
SciMLOperators.has_mul — Function
has_mul(L)
Check if L * v is defined for AbstractArray u of appropriate size.
Arguments
L: Operator to inspect.
Returns
true only when L * v is supported for compatible action arrays.
SciMLOperators.has_mul! — Function
has_mul!(L)
Check if mul!(w, L, v) is defined for AbstractArrays w, v of appropriate sizes.
Arguments
L: Operator to inspect.
Returns
true only when the in-place multiplication and its return-value contract are supported for compatible arrays.
SciMLOperators.has_ldiv — Function
has_ldiv(L)
Check if L \ v is defined for AbstractArray v of appropriate size.
Arguments
L: Operator to inspect.
Returns
true only when the out-of-place solve is supported for compatible right-hand sides.
SciMLOperators.has_ldiv! — Function
has_ldiv!(L)
Check if ldiv!(w, L, v) is defined for AbstractArrays w, v of appropriate sizes.
Arguments
L: Operator to inspect.
Returns
true only when the in-place solve is supported for compatible arrays.
SciMLOperators.has_concretization — Function
has_concretization(_)
Return whether L can be materialized into a concrete scalar or matrix representation for fallback operations.
Arguments
L: An operator-like object.
Returns
true when concretize(L) is expected to succeed by calling convert(AbstractMatrix, L) or convert(Number, L), and false otherwise.
Interface Rules
Subtypes of AbstractSciMLOperator should define has_concretization(L) as true only when their current state can be materialized without changing the operator action. Composite operators should return true only when every component needed for the materialization also has concretization.
This trait is intentionally separate from isconvertible(L): an operator can have a correct concrete representation but avoid cheap eager fusion in generic algebra paths.
Examples
using SciMLOperators
A = MatrixOperator([1.0 2.0; 3.0 4.0])
has_concretization(A) # true
F = FunctionOperator((y, x, u, p, t) -> copyto!(y, x), zeros(2), zeros(2);
isinplace = true, T = Float64, islinear = true)
has_concretization(F) # falseSciMLOperators.NoKwargFilter — Type
This type indicates to preprocess_update_func to not to filter keyword arguments. Required in implementation of lazy Base.adjoint, Base.conj, Base.transpose.
Developer Extension Hooks
The names in this section are qualified, developer-facing extension points. They are documented so solver packages can depend on a stable contract, but they are not general user construction APIs. Downstream solver developers should extend the documented methods rather than relying on private fields or unexported implementation helpers.
SciMLOperators.AbstractWOperator — Type
AbstractWOperator{T} <: AbstractSciMLOperator{T}Developer-facing interface for operators representing the implicit-solver matrix $W = J - MM / gamma$. This type is intentionally not exported and should not be used as a user-facing construction target.
Interface Rules
A concrete subtype must provide size, matrix-like *, and mul!(out, W, x) with the same shape and return-value rules as AbstractSciMLOperator. If has_mul!(W) is true, it must also provide mul!(out, W, x, alpha, beta) implementing out = alpha * (W * x) + beta * out, and both mutating methods must return out. It must also expose enough state for the implicit solver that owns it to update the Jacobian and mass-matrix actions. Define isconvertible and convert(AbstractMatrix, W) only when materialization is supported.
If a consumer caches a Jacobian-dependent factorization, it should implement the jacobian_stale, mark_jacobian_updated!, and mark_jacobian_current! protocol or keep an equivalent private generation for its own subtype.
SciMLOperators.has_tensor_outer_mul_fast — Function
has_tensor_outer_mul_fast(outer) -> BoolReturn whether outer provides the specialized tensor_outer_mul_fast! contract used by batched TensorProductOperator multiplication.
Developer API
This hook is for extension authors implementing an allocation-free fast path for an outer operator type. Return true only when the corresponding tensor_outer_mul_fast! methods are defined for both the unscaled and scaled call signatures. End users should rely on mul! or TensorProductOperator instead of calling or extending this hook directly.
SciMLOperators.tensor_outer_mul_fast! — Function
tensor_outer_mul_fast!(w, outer, C, mi, mo, no, k[, α, β]) -> wWrite the batched outer multiplication used by TensorProductOperator into w without allocating intermediate arrays.
Arguments
w: destination withmi * morows andkcolumns.outer: operator of size(mo, no).C: cached intermediate data withmi * norows andkcolumns.mi,mo,no,k: dimensions derived from the tensor-product factors and the batch size.α,β: optional scaling coefficients; the scaled method must computew = α * outer_product + β * w.
Developer API
Only implement this hook together with has_tensor_outer_mul_fast returning true for the same outer type. Implement both call signatures, preserve the stated destination shape, and return w. This contract exists for package extensions; ordinary callers should use mul! on the enclosing TensorProductOperator.
Sharing scratch space between the summands of an AddedOperator
mul! applies the summands of an AddedOperator one after another, so no two of their caches are ever live at the same time and a summand can reuse the scratch of an earlier one. An operator type takes part by defining getcache, and can additionally avoid allocating a cache it would only discard by defining adopt_cache. Both default to declining, so a type that defines neither behaves exactly as it did before.
A wrapper holding no scratch of its own, such as ScaledOperator, forwards all three hooks to the operator it wraps, so 2A + 3B and A - B share as A + B does. A new wrapper of that kind should do the same.
Applying a cached operator from several tasks at once was never safe, because they would write to the same scratch. Once its summands share scratch they are not independently safe either, so code that hand-parallelizes over the ops of an AddedOperator needs its own uncached copies.
SciMLOperators.getcache — Function
getcache(_)
Return the cache held by op, or nothing when it holds none.
Defining this method opts op's type into the scratch sharing AddedOperator does between its summands, which is sound because mul! applies them strictly serially and no two of their caches are ever live at once. A summand may then be given the cache of an earlier one whose cache has the identical type and identically sized slots, so only define this for operators where two such caches really are interchangeable — including which slots deliberately alias each other. The nothing default keeps a type out of it entirely.
Two further conditions come with defining it. The first is on the operator's own cache_self: it must treat the buffers it is handed as a template to similar, zero or reshape and never retain a reference to them, since the whole point is that those buffers may afterwards be replaced by an earlier summand's. Every cache_self in this package does. The second is on the caller: applying the summands of an AddedOperator concurrently, by hand, is no longer safe once they share scratch — a cached operator was never safe to apply concurrently, but now its summands are not independently safe either.
A wrapper that holds no scratch of its own — ScaledOperator, AdjointOperator, TransposedOperator — is transparent to all of this and forwards getcache, update_cache and adopt_cache to the operator it wraps. A new wrapper of that kind should do the same; without it the operator underneath drops out of sharing even though its cache would have qualified.
See also adopt_cache, which additionally spares the operator from allocating a cache it would only discard.
SciMLOperators.update_cache — Function
update_cache(op, new_cache)
Replace op's cache with new_cache. Only ever called with a new_cache of exactly the same type as the one it replaces, so op's type is unchanged.
Arguments
op: Cached operator whose scratch should be replaced.new_cache: Cache with the same type, shape, and aliasing layout as the existing cache.
Returns
An operator with new_cache installed. Implementations must preserve the operator action and type parameters.
SciMLOperators.adopt_cache — Function
adopt_cache(_, cache, v)
Return op set up for in-place use with v using cache — the cache of an operator already established as interchangeable with it — instead of allocating buffers of its own. Return nothing to decline, in which case op is cached the ordinary way and then offered the same cache after the fact, which costs an allocation that is immediately discarded. What comes back must report cache as its own — getcache on it is checked against what was handed over, and anything else is treated as having declined, since an implementation that quietly allocated instead would otherwise skip the ordinary path too.
Declining is the default, because there is no implementation that is correct for every operator: one that follows the cache_self/cache_internals split opts in with
adopt_cache(op::MyOperator, cache, v) = cache_internals(update_cache(op, cache), v)whereas one that defines its own cache_operator must instead do whatever that does, minus the allocation — the line above would silently skip its internal caching entirely.
Defining this asks more of an operator than getcache alone. getcache only claims that two caches of the same type with the same slot sizes are interchangeable, and that is checked against buffers that already exist. This additionally claims that what cache_self builds is a function of the operator's type, its operands' sizes and v, since that is what the caller compares before any buffer exists. Leave it undefined when the cache depends on anything else — FunctionOperator does, because it sizes buffers from traits.sizes, which size does not expose.
Arguments
op: Operator that may adopt the supplied cache.cache: An interchangeable cache from another operator.v: Prototype action vector or matrix used to build child caches.
Returns
An operator using cache, or nothing to decline. Returning an operator that silently allocates a different cache is a contract violation.
Note About Affine Operators
Affine operators are operators that have the action Q*x = A*x + b. These operators have no matrix representation, since if there was, it would be a linear operator instead of an affine operator. You can only represent an affine operator as a linear operator in a dimension of one larger via the operation: [A b] * [u;1], so it would require something modified to the input as well. As such, affine operators are a distinct generalization of linear operators.
While it seems like it might doom the idea of using matrix-free affine operators, it turns out that affine operators can be used in all cases where matrix-free linear solvers are used due to an easy generalization of the standard convergence proofs. If Q is the affine operator $Q(x) = Ax + b$, then solving $Qx = c$ is equivalent to solving $Ax + b = c$ or $Ax = c-b$. If you now do this same “plug-and-chug” handling of the affine operator into the GMRES/CG/etc. convergence proofs, move the affine part to the rhs residual, and show it converges to solving $Ax = c-b$, and thus GMRES/CG/etc. solves $Q(x) = c$ for an affine operator properly.
That same trick can be used mostly anywhere you would've had a linear operator to extend the proof to affine operators, so then $exp(A*t)*v$ operations via Krylov methods work for A being affine as well, and all sorts of things. Thus, affine operators have no matrix representation, but they are still compatible with essentially any Krylov method, which would otherwise be compatible with matrix-free representations, hence their support in the SciMLOperators interface.
Note about keyword arguments to update_coefficients!
In rare cases, an operator may be used in a context where additional state is expected to be provided to update_coefficients! beyond u, p, and t. In this case, the operator may accept this additional state through arbitrary keyword arguments to update_coefficients!. When the caller provides these, they will be recursively propagated downwards through composed operators just like u, p, and t, and provided to the operator. For the premade SciMLOperators, one can specify the keyword arguments used by an operator with an accepted_kwargs argument (by default, none are passed).
In the below example, we create an operator that gleefully ignores u, p, and t and uses its own special scaling.
using SciMLOperators
γ = ScalarOperator(0.0;
update_func = (a, u, p, t; my_special_scaling) -> my_special_scaling,
accepted_kwargs = Val((:my_special_scaling,)))
# Update coefficients, then apply operator
update_coefficients!(γ, nothing, nothing, nothing; my_special_scaling = 7.0)
@show γ * [2.0]
# Use operator application form
@show γ([2.0], nothing, nothing, nothing; my_special_scaling = 5.0)γ * [2.0] = [14.0]
γ([2.0], nothing, nothing, nothing; my_special_scaling = 5.0) = [10.0]