Skip to content

Commit dd821bc

Browse files
committed
more docs
1 parent a03f8ee commit dd821bc

File tree

3 files changed

+238
-46
lines changed

3 files changed

+238
-46
lines changed

README.md

Lines changed: 149 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -11,11 +11,11 @@
1111
[![ColPrac: Contributor's Guide on Collaborative Practices for Community Packages](https://img.shields.io/badge/ColPrac-Contributor's%20Guide-blueviolet)](https://github.com/SciML/ColPrac)
1212
[![SciML Code Style](https://img.shields.io/static/v1?label=code%20style&message=SciML&color=9558b2&labelColor=389826)](https://github.com/SciML/SciMLStyle)
1313

14-
`SciMLOperators` is a package for managing linear, nonlinear, and
15-
time-dependent operators acting on vectors, (or column-vectors of matrices).
16-
We provide wrappers for matrix-free operators, fast tensor-product
17-
evaluations, pre-cached mutating evaluations, as well as `Zygote`-compatible
18-
non-mutating evaluations.
14+
`SciMLOperators` is a package for managing linear, nonlinear,
15+
time-dependent, and parameter dependent operators acting on vectors,
16+
(or column-vectors of matrices). We provide wrappers for matrix-free
17+
operators, fast tensor-product evaluations, pre-cached mutating
18+
evaluations, as well as `Zygote`-compatible non-mutating evaluations.
1919

2020
The lazily implemented operator algebra allows the user to update the
2121
operator state by passing in an update function that accepts arbirary
@@ -37,8 +37,8 @@ julia> Pkg.add("SciMLOperators")
3737

3838
## Examples
3939

40-
Let `M`, `D`, `F` be matrix, diagonal, and function-based `SciMLOperators`
41-
respectively.
40+
Let `M`, `D`, `F` be matrix, diagonal matrix, and function-based
41+
`SciMLOperators` respectively.
4242

4343
```julia
4444
N = 4
@@ -52,37 +52,56 @@ F = FunctionOperator(f, zeros(N), zeros(N))
5252
Then, the following codes just work.
5353

5454
```julia
55-
L1 = 2M + 3F + LinearAlgebra.I
55+
L1 = 2M + 3F + LinearAlgebra.I + rand(N, N)
5656
L2 = D * F * M'
5757
L3 = kron(M, D, F)
5858
L4 = M \ D
5959
L5 = [M; D]' * [M F; F D] * [F; D]
6060
```
6161

62-
Each `L#` can be applied to vectors of appropriate sizes:
62+
Each `L#` can be applied to `AbstractVector`s of appropriate sizes:
6363

6464
```julia
65+
p = nothing # parameter struct
66+
t = 0.0 # time
67+
6568
u = rand(N)
66-
v = zeros(N)
67-
u_kron = rand(N ^ 3)
69+
v = L1(u, p, t) # == L1 * u
6870

69-
v = L1 * u
70-
v_kron = L3(u_kron, p, t)
71+
u_kron = rand(N ^ 3)
72+
v_kron = L3(u_kron, p, t) # == L3 * u_kron
7173
```
7274

7375
For mutating operator evaluations, call `cache_operator` to generate
7476
in-place cache so the operation is nonallocating.
7577

7678
```julia
79+
α, β = rand(2)
80+
7781
# allocate cache
7882
L2 = cache_operator(L2, u)
7983
L4 = cache_operator(L4, u)
8084

8185
# allocation-free evaluation
82-
mul!(v, L2, u)
83-
L4(v, u, p, t)
86+
L2(v, u, p, t) # == mul!(v, L2, u)
87+
L4(v, u, p, t, α, β) # == mul!(v, L4, u, α, β)
8488
```
8589

90+
The calling signature `L(u, p, t)`, for out-of-place evaluations is
91+
equivalent to `L * u`, and the in-place evaluation `L(v, u, p, t, args...)`
92+
is equivalent to `LinearAlgebra.mul!(v, L, u, args...)`, where the arguments
93+
`p, t` are passed to `L` to update its state. More details are provided
94+
in the operator update section below. While overloads to `Base.*`
95+
and `LinearAlgebra.mul!` are available, where a `SciMLOperator` behaves
96+
like an `AbstractMatrix`, we recommend sticking with the
97+
`L(u, p, t)`, `L(v, u, p, t)`, `L(v, u, p, t, α, β)` calling signatures
98+
as the latter internally update the operator state.
99+
100+
The `(u, p, t)` calling signature is standardized over the `SciML`
101+
ecosystem and is flexible enough to support use cases such as time-evolution
102+
in ODEs, as well as sensitivity computation with respect to the parameter
103+
object `p`.
104+
86105
Thanks to overloads defined for evaluation methods and traits in
87106
`Base`, `LinearAlgebra`, the behaviour of a `SciMLOperator` is
88107
indistinguishable from an `AbstractMatrix`. These operators can be
@@ -94,8 +113,120 @@ interface include, but are not limited, the following:
94113
- `LinearAlgebra: mul!, ldiv!, lmul!, rmul!, factorize, issymmetric, ishermitian, isposdef`
95114
- `SparseArrays: sparse, issparse`
96115

116+
## Multidimension arrays and batching
117+
118+
SciMLOperator can also be applied to `AbstractMatrix` subtypes where
119+
operator-evaluation is done column-wise.
120+
121+
```julia
122+
K = 10
123+
u_mat = rand(N, K)
124+
125+
v_mat = F(u_mat, p, t) # == mul!(v_mat, F, u_mat)
126+
size(v_mat) == (N, K) # true
127+
```
128+
129+
`L#` can also be applied to `AbstractArray`s that are not
130+
`AbstractVecOrMat`s so long as their size in the first dimension is appropriate
131+
for matrix-multiplication. Internally, `SciMLOperator`s reshapes an
132+
`N`-dimensional array to an `AbstractMatrix`, and applies the operator via
133+
matrix-multiplication.
134+
97135
## Operator update
98136

137+
This package can also be used to write time-dependent, and
138+
parameter-dependent operators, whose state can be updated per
139+
a user-defined function.
140+
The updates can be done in-place, i.e. by mutating the object,
141+
or out-of-place, i.e. in a non-mutating, `Zygote`-compatible way.
142+
143+
For example,
144+
145+
```julia
146+
u = rand(N)
147+
p = rand(N)
148+
t = rand()
149+
150+
# out-of-place update
151+
mat_update_func = (A, u, p, t) -> t * (p * p')
152+
sca_update_func = (a, u, p, t) -> t * sum(p)
153+
154+
M = MatrixOperator(zero(N, N); update_func = mat_update_func)
155+
α = ScalarOperator(zero(Float64); update_func = sca_update_func)
156+
157+
L = α * M
158+
L = cache_operator(L, u)
159+
160+
# L is initialized with zero state
161+
L * u == zeros(N) # true
162+
163+
# update operator state with `(u, p, t)`
164+
L = update_coefficients(L, u, p, t)
165+
# and multiply
166+
L * u != zeros(N) # true
167+
168+
# updates state and evaluates L at (u, p, t)
169+
L(u, p, t) != zeros(N) # true
170+
```
171+
172+
The out-of-place evaluation function `L(u, p, t)` calls
173+
`update_coefficients` under the hood, which recursively calls
174+
the `update_func` for each component `SciMLOperator`.
175+
Therefore the out-of-place evaluation function is equivalent to
176+
calling `update_coefficients` followed by `Base.*`. Notice that
177+
the out-of-place evaluation does not return the updated operator.
178+
179+
On the other hand,, the in-place evaluation function, `L(v, u, p, t)`,
180+
mutates `L`, and is equivalent to calling `update_coefficients!`
181+
followed by `mul!`. The in-place update behaviour works the same way
182+
with a few `<!>`s appended here and there. For example,
183+
184+
```julia
185+
v = rand(N)
186+
u = rand(N)
187+
p = rand(N)
188+
t = rand()
189+
190+
# in-place update
191+
_A = rand(N, N)
192+
_d = rand(N)
193+
mat_update_func! = (A, u, p, t) -> (copy!(A, _A); lmul!(t, A); nothing)
194+
diag_update_func! = (diag, u, p, t) -> copy!(diag, N)
195+
196+
M = MatrixOperator(zero(N, N); update_func! = mat_update_func!)
197+
D = DiagonalOperator(zero(N); update_func! = diag_update_func!)
198+
199+
L = D * M
200+
L = cache_operator(L, u)
201+
202+
# L is initialized with zero state
203+
L * u == zeros(N) # true
204+
205+
# update L in-place
206+
update_coefficients!(L, u, p, t)
207+
# and multiply
208+
mul!(v, u, p, t) != zero(N) # true
209+
210+
# updates L in-place, and evaluates at (u, p, t)
211+
L(v, u, p, t) != zero(N) # true
212+
```
213+
214+
The update behaviour makes this package flexible enough to be used
215+
in `OrdianryDiffEq`. As the parameter object `p` is often reserved
216+
for sensitivy computation via automatic-differentiation, a user may
217+
prefer to pass in state information via other arguments. For that
218+
reason, we allow for update functions with arbitrary keyword arguments.
219+
220+
```julia
221+
mat_update_func = (A, u, p, t; scale = 0.0) -> scale * (p * p')
222+
223+
M = MatrixOperator(zero(N, N); update_func = mat_update_func,
224+
accepted_kwargs = (:state,))
225+
226+
M(u, p, t) == zeros(N) # true
227+
M(u, p, t; scale = 1.0) != zero(N)
228+
```
229+
99230
## Features
100231

101232
* Matrix-free operators with `FunctionOperator`
@@ -143,9 +274,9 @@ Some packages providing similar functionality are
143274
## Interoperability and extended Julia ecosystem
144275

145276
`SciMLOperator.jl` overloads the `AbstractMatrix` interface for
146-
`AbstractSciMLOperator`s, here allowing seamless compatibility with
277+
`AbstractSciMLOperator`s, allowing seamless compatibility with
147278
linear solves, and nonlinear solvers. Further, due to the update functionality,
148-
`AbstractSciMLOperato`s can represent an `ODEFunction` in `OrdinaryDiffEq.jl`,
279+
`AbstractSciMLOperator`s can represent an `ODEFunction` in `OrdinaryDiffEq.jl`,
149280
and downstream packages. See tutorials for example of usage with
150281
`OrdinaryDiffEq.jl`, `LinearSolve.jl`, `NonlinearSolve.jl`.
151282

@@ -157,7 +288,7 @@ An example of `Zygote.jl` usage with
157288
[`Lux.jl`](https://github.com/LuxDL/Lux.jl) is also provided in the tutorials.
158289

159290
Please make an issue [here](https://github.com/SciML/SciMLOperators.jl/issues)
160-
if you come across an unexpected issue using `SciMLOperator`
291+
if you come across an unexpected issue while using `SciMLOperators`.
161292

162293
We provide below a list of packages that make use of `SciMLOperators`.
163294
If you are using `SciMLOperators` in your work, feel free to create a PR
@@ -190,4 +321,3 @@ and add your package to this list.
190321
- [JuliaDiffEq](https://gitter.im/JuliaDiffEq/Lobby) on Gitter
191322
- On the Julia Discourse forums (look for the [modelingtoolkit tag](https://discourse.julialang.org/tag/modelingtoolkit)
192323
- See also [SciML Community page](https://sciml.ai/community/)
193-

src/matrix.jl

Lines changed: 78 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -1,12 +1,60 @@
11
#
22
"""
3-
MatrixOperator(A; [update_func, update_func!, accepted_kwargs])
3+
$SIGNATURES
44
5-
Represents a time-dependent linear operator given by an AbstractMatrix. The
6-
update function is called by `update_coefficients!` and is assumed to have
7-
the following signature:
5+
Represents a linear operator given by an `AbstractMatrix` that may be
6+
applied to an `AbstractVecOrMat`. Its state is updated by the user-provided
7+
`update_func` during operator evaluation (`L([v,], u, p, t)`), or by calls
8+
to `update_coefficients[!](L, u, p, t)`. Both recursively call the
9+
`update_function`, `update_func` which is assumed to have the signature
810
9-
update_func(A::AbstractMatrix,u,p,t; <accepted kwargs>) -> [modifies A]
11+
update_func(A::AbstractMatrix, u, p, t; <accepted kwargs>) -> newA
12+
or
13+
update_func!(A::AbstractMatrix, u ,p , t; <accepted kwargs>) -> [modifies A]
14+
15+
The set of keyword-arguments accepted by `update_func[!]` must be provided
16+
to `MatrixOperator` via the kwarg `accepted_kwargs` as a tuple of `Symbol`s.
17+
`kwargs` cannot be passed down to `update_func[!]` if `accepted_kwargs`
18+
are not provided.
19+
20+
$(UPDATE_COEFFS_WARNING)
21+
22+
# Interface
23+
24+
Lazy matrix algebra is defined for `AbstractSciMLOperator`s. The Interface
25+
supports lazy addition, subtraction, multiplication, inversion,
26+
adjoints, transposes.
27+
28+
# Example
29+
30+
```
31+
u = rand(4)
32+
p = rand(N, N)
33+
t = rand()
34+
35+
mat_update = (A, u, p, t; scale = 0.0) -> t * p
36+
M = MatrixOperator(0.0; update_func = mat_update; accepted_kwargs = (:scale,))
37+
38+
L = M * M + 3I
39+
L = cache_operator(M, u)
40+
41+
# update L and evaluate
42+
v = L(u, p, t; scale = 1.0)
43+
```
44+
45+
```
46+
v = zero(4)
47+
u = rand(4)
48+
p = nothing
49+
t = rand()
50+
51+
mat_update! = (A, u, p, t; scale = 0.0) -> (copy!(A, p); lmul!(t, A))
52+
M = MatrixOperator(0.0; update_func! = val_update!; accepted_kwargs = (:scale,))
53+
L = M * M + 3I
54+
55+
# update L in-place and evaluate
56+
L(v, u, p, t; scale = 1.0)
57+
```
1058
"""
1159
struct MatrixOperator{T,AT<:AbstractMatrix{T},F,F!} <: AbstractSciMLOperator{T}
1260
A::AT
@@ -135,22 +183,37 @@ LinearAlgebra.ldiv!(v::AbstractVecOrMat, L::MatrixOperator, u::AbstractVecOrMat)
135183
LinearAlgebra.ldiv!(L::MatrixOperator, u::AbstractVecOrMat) = ldiv!(L.A, u)
136184

137185
"""
138-
DiagonalOperator(diag; [update_func, update_func!, accepted_kwargs])
139-
140-
Represents a time-dependent elementwise scaling (diagonal-scaling) operation.
141-
The update function is called by `update_coefficients!` and is assumed to have
142-
the following signature:
186+
$SIGNATURES
143187
144-
update_func(diag::AbstractVector,u,p,t; <accepted kwargs>) -> [modifies diag]
145-
146-
When `diag` is an `AbstractVector` of length N, `L=DiagonalOpeator(diag, ...)`
147-
can be applied to `AbstractArray`s with `size(u, 1) == N`. Each column of the `u`
148-
will be scaled by `diag`, as in `LinearAlgebra.Diagonal(diag) * u`.
188+
Represents a elementwise scaling (diagonal-scaling) operation that may
189+
be applied to an `AbstractVecOrMat`. When `diag` is an `AbstractVector`
190+
of length N, `L = DiagonalOpeator(diag, ...)` can be applied to
191+
`AbstractArray`s with `size(u, 1) == N`. Each column of the `u` will be
192+
scaled by `diag`, as in `LinearAlgebra.Diagonal(diag) * u`.
149193
150194
When `diag` is a multidimensional array, `L = DiagonalOperator(diag, ...)` forms
151195
an operator of size `(N, N)` where `N = size(diag, 1)` is the leading length of `diag`.
152196
`L` then is the elementwise-scaling operation on arrays of `length(u) = length(diag)`
153197
with leading length `size(u, 1) = N`.
198+
199+
Its state is updated by the user-provided `update_func` during operator
200+
evaluation (`L([v,], u, p, t)`), or by calls to
201+
`update_coefficients[!](L, u, p, t)`. Both recursively call the
202+
`update_function`, `update_func` which is assumed to have the signature
203+
204+
update_func(diag::AbstractVecOrMat, u, p, t; <accepted kwargs>) -> new_diag
205+
or
206+
update_func!(diag::AbstractVecOrMat, u, p, t; <accepted kwargs>) -> [modifies diag]
207+
208+
The set of keyword-arguments accepted by `update_func[!]` must be provided
209+
to `MatrixOperator` via the kwarg `accepted_kwargs` as a tuple of `Symbol`s.
210+
`kwargs` cannot be passed down to `update_func[!]` if `accepted_kwargs`
211+
are not provided.
212+
213+
$(UPDATE_COEFFS_WARNING)
214+
215+
# Example
216+
154217
"""
155218
function DiagonalOperator(diag::AbstractVector;
156219
update_func = DEFAULT_UPDATE_FUNC,

0 commit comments

Comments
 (0)