Linear Solver Algorithm Interface
Every algorithm that LinearSolve.jl can be handed as solve(prob, alg) is a subtype of LinearSolve.SciMLLinearSolveAlgorithm. This page is the contract that such a type has to satisfy. It applies equally to algorithms defined inside LinearSolve.jl, in one of its package extensions, and in downstream packages.
The interface is checked in LinearSolve.jl's test suite for every algorithm the package knows about, using LinearSolve.algorithm_interface_issues; downstream packages can run the same check against their own algorithms.
Choosing a supertype
SciMLLinearSolveAlgorithm
├── AbstractFactorization
│ ├── AbstractDenseFactorization
│ └── AbstractSparseFactorization
├── AbstractKrylovSubspaceMethod
└── AbstractSolveFunctionSubtype the most specific abstract type that fits. Doing so supplies several of the methods below; subtyping SciMLLinearSolveAlgorithm directly supplies none of them, so such an algorithm must define needs_concrete_A itself. Every trait default that comes from the categorized types is listed in the tables below.
Required methods
| Method | Meaning |
|---|---|
SciMLBase.solve!(cache::LinearCache, alg::MyAlg; kwargs...) | Solve the system held in cache and return SciMLBase.build_linear_solution(alg, u, resid, cache). |
LinearSolve.needs_concrete_A(alg::MyAlg)::Bool | Whether the algorithm needs the entries of A, or only matrix-vector products with it. |
solve! is the algorithm proper. It reads cache.A, cache.b, cache.u, cache.Pl/cache.Pr, cache.abstol/cache.reltol/cache.maxiters, and writes the solution into cache.u. cache.isfresh is true when A has changed since the previous solve; algorithms that build a factorization or a solver object should do so only when it is set, store the result in cache.cacheval, and then set cache.isfresh = false.
needs_concrete_A is a pure function of the algorithm type. Downstream solvers — OrdinaryDiffEq.jl and NonlinearSolve.jl in particular — call it on a user-supplied linsolve to decide whether to assemble a concrete Jacobian.
needs_concrete_A, needs_square_A, default_alias_A and default_alias_b must be defined in the same package (and ideally the same file) as the algorithm struct, not in the package extension that implements the algorithm. They are queried before the backend package is necessarily loaded, so a trait that lives in an extension is either missing (a MethodError deep inside an unrelated solver) or silently wrong (the inherited default is used instead of the intended value). Only solve!, init_cacheval, update_tolerances_internal!, and other methods that genuinely need the backend belong in the extension.
algorithm_interface_issues reports a trait defined in an extension as a violation, so this is enforced rather than merely recommended.
Optional methods
Each of these has a default, so an algorithm only defines the ones whose default does not fit.
| Method | Default | Purpose |
|---|---|---|
LinearSolve.init_cacheval(alg, A, b, u, Pl, Pr, maxiters, abstol, reltol, verbose, assumptions) | nothing | Build the algorithm's private cache, stored as cache.cacheval. Should return the same type that solve! later stores, so the cache stays type-stable. |
LinearSolve.default_alias_A(alg, A, b)::Bool | false; true for AbstractKrylovSubspaceMethod and AbstractSparseFactorization | Whether init may alias the user's A instead of copying it. true is only correct for algorithms that never mutate A. |
LinearSolve.default_alias_b(alg, A, b)::Bool | false; true for AbstractKrylovSubspaceMethod and AbstractSparseFactorization | The same for b. |
LinearSolve.needs_square_A(alg)::Bool | true | Whether the algorithm requires a square A. Least-squares capable algorithms return false. |
LinearSolve.update_tolerances_internal!(cache, alg, abstol, reltol) | throws: the algorithm has no tolerances to update | Hook for LinearSolve.update_tolerances!. LinearCache.abstol/reltol have already been set when it runs. |
update_tolerances_internal! needs no definition when the algorithm has no tolerances at all. Define it as nothing when the algorithm reads cache.abstol/cache.reltol at solve time (Krylov methods do), and define it to write into cache.cacheval when the algorithm snapshots the tolerances into its own solver object at init time.
The four Bool-valued traits are called while init builds the cache, so they must be inferable as Bool — write them as literal returns (needs_concrete_A(::MyAlg) = true) rather than as runtime computations.
A complete example
using LinearSolve, LinearAlgebra, SciMLBase
struct MyLUFactorization <: LinearSolve.AbstractDenseFactorization end
# Optional: give the cache its final type up front so repeated solves stay
# type-stable. `ArrayInterface.lu_instance(A)` is the cheap way to do this.
function LinearSolve.init_cacheval(
alg::MyLUFactorization, A, b, u, Pl, Pr, maxiters::Int, abstol, reltol,
verbose, assump::LinearSolve.OperatorAssumptions
)
return lu(convert(AbstractMatrix, A))
end
function SciMLBase.solve!(cache::LinearSolve.LinearCache, alg::MyLUFactorization; kwargs...)
if cache.isfresh
cache.cacheval = lu!(convert(AbstractMatrix, cache.A))
cache.isfresh = false
end
y = ldiv!(cache.u, cache.cacheval, cache.b)
return SciMLBase.build_linear_solution(alg, y, nothing, cache)
endSubtyping AbstractDenseFactorization supplies needs_concrete_A(alg) = true and the false aliasing defaults, which is what a destructive dense factorization wants. An algorithm that subtypes SciMLLinearSolveAlgorithm directly would additionally need
LinearSolve.needs_concrete_A(::MyLUFactorization) = trueMatrix-free implementation
An iterative algorithm can promise that it only needs the generic mul! operation. The operator does not need to expose a factorization or a dense matrix representation; the algorithm still receives the standard LinearCache and returns the standard LinearSolution.
struct DocIdentityOperator{T} <: AbstractMatrix{T}
n::Int
end
Base.size(A::DocIdentityOperator) = (A.n, A.n)
Base.getindex(A::DocIdentityOperator{T}, i::Int, j::Int) where {T} =
i == j ? one(T) : zero(T)
function LinearAlgebra.mul!(y::AbstractVector, ::DocIdentityOperator, x::AbstractVector)
copyto!(y, x)
return y
end
struct DocMatVecAlg <: LinearSolve.AbstractKrylovSubspaceMethod end
function SciMLBase.solve!(cache::LinearSolve.LinearCache, alg::DocMatVecAlg; kwargs...)
mul!(cache.u, cache.A, cache.b)
return SciMLBase.build_linear_solution(
alg, cache.u, nothing, cache; retcode = SciMLBase.ReturnCode.Success
)
end
A = DocIdentityOperator{Float64}(3)
b = [1.0, 2.0, 3.0]
cache = init(LinearProblem(A, b), DocMatVecAlg())
@assert solve!(cache).u == bThe example is intentionally limited to the generic contract: a real iterative algorithm must also define its convergence, stopping, and preconditioning rules and should exercise those rules with dedicated tests.
Checking compliance
using Test
@test isempty(LinearSolve.algorithm_interface_issues(MyLUFactorization()))algorithm_interface_issues accepts either an instance or the type, and returns one message per violation. Pass check_solve = false to check only the traits, which is what LinearSolve.jl's own test suite does for algorithms whose solve! lives in an extension whose backend is not loaded in that test group.
To sweep every algorithm a session knows about:
for T in LinearSolve.concrete_algorithm_types()
@test isempty(LinearSolve.algorithm_interface_issues(T))
endLinearSolve.algorithm_interface_issues — Function
LinearSolve.algorithm_interface_issues(alg; check_solve = true) -> Vector{String}Check alg – an algorithm instance or an algorithm type – against the SciMLLinearSolveAlgorithm interface and return one message per violation. An empty result means alg is interface compliant.
Arguments
alg: An algorithm instance or a subtype ofSciMLLinearSolveAlgorithm.
Keywords
check_solve::Bool: Require a concreteSciMLBase.solve!method whentrue(default). Set this tofalseonly when the method is supplied by an unloaded package extension; trait checks still run.
Returns
A Vector{String} containing one diagnostic per violated interface rule. An empty vector means that the requested checks passed.
Required methods
SciMLBase.solve!(cache::LinearCache, alg::MyAlg; kwargs...)performs the solve and returnsSciMLBase.build_linear_solution(alg, u, resid, cache).LinearSolve.needs_concrete_A(alg::MyAlg)::Boolstates whether the algorithm needs the entries ofAor only matrix-vector products. Downstream solvers (OrdinaryDiffEq.jl, NonlinearSolve.jl) query it to decide whether to build a concrete Jacobian, so it must be defined next to the algorithm struct and never in a package extension: it is called before the backend package is necessarily loaded.
Subtyping AbstractFactorization, AbstractSparseFactorization, AbstractKrylovSubspaceMethod or AbstractSolveFunction supplies needs_concrete_A; subtyping SciMLLinearSolveAlgorithm directly does not.
Trait placement
needs_concrete_A, needs_square_A, default_alias_A and default_alias_b must be defined in the module that defines the algorithm type, never in a package extension. A trait defined in an extension is invisible until the backend loads, so callers silently get the inherited default instead — that is reported as a violation too.
Optional methods
These have defaults, so they are never reported as issues; they are listed here because they complete the interface:
| Method | Default |
|---|---|
init_cacheval(alg, A, b, u, Pl, Pr, maxiters, abstol, reltol, verbose, assumptions) | nothing |
default_alias_A(alg, A, b) | false; true for Krylov and sparse factorizations |
default_alias_b(alg, A, b) | false; true for Krylov and sparse factorizations |
needs_square_A(alg) | true |
update_tolerances_internal!(cache, alg, abstol, reltol) | throws: the algorithm has no tolerances to update |
The four Bool-valued traits are additionally checked to be inferred as Bool, since init calls them while building the cache.
Examples
struct MyLUFactorization <: LinearSolve.SciMLLinearSolveAlgorithm end
LinearSolve.needs_concrete_A(::MyLUFactorization) = true
function SciMLBase.solve!(cache::LinearSolve.LinearCache, alg::MyLUFactorization; kwargs...)
# ...
end
@test isempty(LinearSolve.algorithm_interface_issues(MyLUFactorization()))LinearSolve.concrete_algorithm_types — Function
LinearSolve.concrete_algorithm_types(T = SciMLLinearSolveAlgorithm)Arguments
T: Abstract algorithm type to inspect. Defaults toSciMLLinearSolveAlgorithm.
Returns
A newly allocated Vector{Any} containing every loaded concrete subtype of T, including types reachable through intermediate abstract types. The result depends on which package extensions have been loaded in the current session.
Examples
algorithms = LinearSolve.concrete_algorithm_types()
@assert all(T -> T <: LinearSolve.SciMLLinearSolveAlgorithm, algorithms)LinearSolve.needs_square_A — Function
needs_square_A(alg)Returns true if the algorithm requires a square matrix.
init enforces this: an algorithm that returns true and is handed a non-square A throws an ArgumentError naming the algorithms that do solve least-squares/minimum-norm systems, rather than letting a DimensionMismatch (or worse) escape from inside the factorization. Anything not listed below falls back to the conservative true, so a new algorithm is rejected on non-square input until it is declared otherwise.
LinearSolve.update_tolerances! — Function
LinearSolve.update_tolerances!(cache; abstol = nothing, reltol = nothing)Change the convergence tolerances of an existing LinearCache in place. The abstol/reltol fields are updated and then update_tolerances_internal! gives the algorithm a chance to propagate the new values into cache.cacheval.
Not every algorithm has tolerances to update: factorizations, and algorithms that do not define update_tolerances_internal!, throw instead of silently ignoring the request.
LinearSolve.update_tolerances_internal! — Function
LinearSolve.update_tolerances_internal!(cache, alg, abstol, reltol)Arguments
cache: ExistingLinearCachewhose tolerance fields have already been updated.alg: Algorithm associated withcache.abstol: New absolute tolerance, ornothingwhen it is unchanged.reltol: New relative tolerance, ornothingwhen it is unchanged.
Interface rules
Define this hook as nothing for an algorithm that reads cache.abstol and cache.reltol at solve time. Define it to update cache.cacheval when the algorithm snapshots tolerances into its own solver object during init.
Returns
nothing. The default method throws because an algorithm that has no hook is treated as not supporting tolerance updates.
Throws
ArgumentError if the algorithm does not support changing tolerances after init.