Custom Operators
This guide explains how to define custom operators in AbstractOperators.jl by extending the necessary interface functions.
Linear Operators
To create a custom linear operator, define a struct that inherits from LinearOperator and implement the mandatory functions.
Minimal Example
using AbstractOperators
# Define the operator struct
struct MyCustomLinOp{N,M,D,C} <: LinearOperator
dim_in::NTuple{M,Int}
dim_out::NTuple{N,Int}
# Add any additional fields needed for your operator
end
# Mandatory functions
# 1. size: Return (codomain_size, domain_size)
Base.size(L::MyCustomLinOp) = (L.dim_out, L.dim_in)
# 2. domain_type: Return the element type of the input
AbstractOperators.domain_type(::MyCustomLinOp{N,M,D,C}) where {N,M,D,C} = D
# 3. codomain_type: Return the element type of the output
AbstractOperators.codomain_type(::MyCustomLinOp{N,M,D,C}) where {N,M,D,C} = C
# 4. fun_name: Return a string/symbol for display purposes
AbstractOperators.fun_name(::MyCustomLinOp) = "MyOp"
# 5. mul!: Forward operator (output, operator, input)
function LinearAlgebra.mul!(y::AbstractArray, L::MyCustomLinOp, x::AbstractArray)
# Utility function to check if
# - eltype(x) == domain_type(L)
# - eltype(y) == codomain_type(L)
# - size(x) == size(L, 2)
# - size(y) == size(L, 1)
# - x isa domain_storage_type(L)
# - y isa codomain_storage_type(L)
AbstractOperators.check(y, L, x)
# Implement your forward operation here
# Example: y .= some_function(x)
return y
end
# 6. mul! for adjoint: Adjoint operator (output, adjoint_operator, input)
function LinearAlgebra.mul!(y::AbstractArray, L::AdjointOperator{<:MyCustomLinOp}, x::AbstractArray)
AbstractOperators.check(y, L, x) # Utility function to check types and sizes
# Implement your adjoint operation here
# Example: y .= adjoint_function(x)
return y
endComplete Example: Diagonal Scaling Operator
Here's a complete example of a custom diagonal operator:
using AbstractOperators, LinearAlgebra
struct DiagonalScaling{N,D,C} <: LinearOperator
dim::NTuple{N,Int}
diagonal::Vector{D}
end
# Constructor
function DiagonalScaling(d::Vector{T}) where {T}
N = 1
D = T
C = T
return DiagonalScaling{N,D,C}((length(d),), d)
end
# Mandatory interface
Base.size(L::DiagonalScaling) = (L.dim, L.dim)
AbstractOperators.domain_type(::DiagonalScaling{N,D,C}) where {N,D,C} = D
AbstractOperators.codomain_type(::DiagonalScaling{N,D,C}) where {N,D,C} = C
AbstractOperators.fun_name(::DiagonalScaling) = "D"
function LinearAlgebra.mul!(
y::AbstractArray{C,N},
L::DiagonalScaling{N,D,C},
x::AbstractArray{D,N}
) where {N,D,C}
y .= L.diagonal .* x
return y
end
function LinearAlgebra.mul!(
y::AbstractArray{D,N},
L::AdjointOperator{DiagonalScaling{N,D,C}},
x::AbstractArray{C,N}
) where {N,D,C}
y .= conj.(L.A.diagonal) .* x
return y
end
# Usage
d = [1.0, 2.0, 3.0]
D = DiagonalScaling(d)
x = ones(3)
y = D * x # Returns [1.0, 2.0, 3.0]Nonlinear Operators
For nonlinear operators, inherit from AbstractOperator instead. The interface is similar but you typically don't need to implement the adjoint.
Minimal Example
using AbstractOperators
struct MyNonlinearOp{T,N} <: AbstractOperator
dim::NTuple{N,Int}
# Add any parameters needed
end
# Mandatory functions
Base.size(L::MyNonlinearOp) = (L.dim, L.dim)
AbstractOperators.domain_type(::MyNonlinearOp{T,N}) where {T,N} = T
AbstractOperators.codomain_type(::MyNonlinearOp{T,N}) where {T,N} = T
AbstractOperators.fun_name(::MyNonlinearOp) = "NonlinOp"
function LinearAlgebra.mul!(
y::AbstractArray{T,N},
L::MyNonlinearOp{T,N},
x::AbstractArray{T,N}
) where {T,N}
# Implement your nonlinear function
y .= sin.(x) # Example
return y
endJacobian for Nonlinear Operators
If you want to support automatic differentiation with your nonlinear operator, you need to implement the adjoint of the Jacobian:
function LinearAlgebra.mul!(
y::AbstractArray,
J::AdjointOperator{Jacobian{A,TT}},
b::AbstractArray
) where {T,N,A<:MyNonlinearOp{T,N},TT<:AbstractArray{T,N}}
L = J.A
# Implement the Jacobian transpose multiplication
# L.x contains the point at which the Jacobian is evaluated
y .= cos.(L.x) .* b # Example for sin operator
return y
endUsing MyLinOp for Quick Prototyping
For quick prototyping without defining a new struct, you can use the built-in MyLinOp constructor:
n, m = 5, 4
A = randn(n, m)
# Define operator with just the forward and adjoint functions
op = MyLinOp(
Float64, # domain type
(m,), # input dimensions
(n,), # output dimensions
(y, x) -> mul!(y, A, x), # forward function
(y, x) -> mul!(y, A', x) # adjoint function
)Mandatory Functions Summary
All custom operators must implement:
size(L::YourOperator): Returns a tuple(codomain_dims, domain_dims)where each element is itself a tuple of dimensions.domain_type(L::YourOperator): Returns the element type (Float64,Complex{Float64}, etc.) of the operator's input.codomain_type(L::YourOperator): Returns the element type of the operator's output.fun_name(L::YourOperator): Returns a string used for displaying the operator. Can be a simple name or a Unicode symbol.mul!(y, L, x): In-place multiplication implementing the forward operation. Must modifyyand return it.mul!(y, L', x)(forLinearOperatoronly): In-place multiplication for the adjoint operator, whereL'is of typeAdjointOperator{YourOperator}.
Optional Properties and Traits
Beyond the mandatory functions, you can optionally define various properties and traits to enable optimizations and provide additional information about your operator. These include:
- Thread safety:
is_thread_safe(L::YourOperator) = true - Storage types:
domain_storage_type,codomain_storage_type - Algebraic properties:
is_diagonal,is_symmetric,is_invertible, etc. - Operator norm:
opnorm(L::YourOperator) - And many more...
StructuredOptimization.jl uses the following properties for optimization, so implementing them is recommended if they apply to your operator:
is_linear-> to identify linear operators (it is already defined forLinearOperatortypes)is_diagonal-> queried when the operator is used in least squares termis_AAc_diagonal-> queried when the term involving the operator is checked if it is proximable
For a complete list of available properties and traits, see the Properties and Traits page.
Tips for Implementation
Type parameters: Use type parameters in your struct to store compile-time information (dimensions, element types) for better performance.
In-place operations: Always use in-place operations (
.=,mul!) when possible to minimize memory allocations.Thread safety: If your operator's
mul!implementation doesn't use mutable state (e.g. buffers for intermediate results in mul! stored in the struct), mark it as thread-safe withis_thread_safe(::YourOperator) = true.Consistent types: Ensure that the types returned by
domain_typeandcodomain_typematch the actual element types used in yourmul!implementations.Testing: Test your operator with various input sizes and types, and verify that the adjoint is correct (for linear operators) by checking that
⟨Ax, y⟩ = ⟨x, A'y⟩.
Example: Testing Your Custom Operator
using Test, LinearAlgebra
# Create your operator
L = MyCustomLinOp(...)
# Test basic functionality
x = randn(size(L, 2)...)
y = L * x
@test size(y) == size(L, 1)
@test eltype(y) == codomain_type(L)
# Test adjoint (for linear operators)
x = randn(domain_type(L), size(L, 2)...)
y = randn(codomain_type(L), size(L, 1)...)
@test dot(L * x, y) ≈ dot(x, L' * y)
# Test in-place operations
y_out = similar(y)
mul!(y_out, L, x)
@test y_out ≈ L * x