diff --git a/.github/workflows/Test.yml b/.github/workflows/Test.yml index ffbb080..5c34e82 100644 --- a/.github/workflows/Test.yml +++ b/.github/workflows/Test.yml @@ -6,31 +6,37 @@ on: - master pull_request: workflow_dispatch: + jobs: build: - name: Julia ${{ matrix.version }} - ${{ matrix.os }} - ${{ matrix.arch }} + name: Julia ${{ matrix.julia-version }} - ${{ matrix.os }} - ${{ matrix.julia-arch }} runs-on: ${{ matrix.os }} + strategy: fail-fast: false matrix: - version: - - '1' - - '1.2' - - '1.6' - os: - - ubuntu-latest - - macOS-latest - - windows-latest - arch: - - x64 + julia-version: ['1.2', 'lts', '1'] + julia-arch: [x64] + os: [ubuntu-latest, windows-latest, macOS-latest] + + # needed to allow julia-actions/cache to delete old caches that it has created + permissions: + actions: write + contents: read + steps: - - uses: actions/checkout@v2 + - uses: actions/checkout@v5 - uses: julia-actions/setup-julia@latest with: - version: ${{ matrix.version }} - arch: ${{ matrix.arch }} + version: ${{ matrix.julia-version }} + arch: ${{ matrix.julia-arch }} + - uses: julia-actions/cache@v2 - uses: julia-actions/julia-buildpkg@latest - uses: julia-actions/julia-runtest@latest - - uses: julia-actions/julia-uploadcodecov@latest + - uses: codecov/codecov-action@v5 + # Upload coverage only from one job (Linux, Julia latest version) + if: matrix.os == 'ubuntu-latest' && matrix.julia-version == '1' + with: + files: lcov.info env: CODECOV_TOKEN: ${{ secrets.CODECOV_TOKEN }} diff --git a/.gitignore b/.gitignore index ba39cc5..1209b4f 100644 --- a/.gitignore +++ b/.gitignore @@ -1 +1,2 @@ Manifest.toml +docs/build diff --git a/Project.toml b/Project.toml index 6c7f1d1..a6c625f 100644 --- a/Project.toml +++ b/Project.toml @@ -1,7 +1,7 @@ name = "ProximalCore" uuid = "dc4f5ac2-75d1-4f31-931e-60435d74994b" authors = ["Lorenzo Stella "] -version = "0.1.2" +version = "0.2.0" [deps] LinearAlgebra = "37e2e46d-f89d-539d-b4ee-838fcccc9c8e" diff --git a/docs/Project.toml b/docs/Project.toml new file mode 100644 index 0000000..827172f --- /dev/null +++ b/docs/Project.toml @@ -0,0 +1,3 @@ +[deps] +Documenter = "e30172f5-a6a5-5a46-863b-614d45cd2de4" +ProximalCore = "dc4f5ac2-75d1-4f31-931e-60435d74994b" diff --git a/docs/make.jl b/docs/make.jl new file mode 100644 index 0000000..cbf9b0b --- /dev/null +++ b/docs/make.jl @@ -0,0 +1,17 @@ +using Documenter +using ProximalCore + +makedocs( + sitename = "ProximalCore.jl", + format = Documenter.HTML(), + modules = [ProximalCore], + pages = [ + "Home" => "index.md", + "API Reference" => "api.md" + ] +) + +#=deploydocs( + repo = "github.com/JuliaFirstOrder/ProximalCore.jl", + devbranch = "main" +)=# diff --git a/docs/src/api.md b/docs/src/api.md new file mode 100644 index 0000000..a50b7a1 --- /dev/null +++ b/docs/src/api.md @@ -0,0 +1,25 @@ +# API Reference + +```@meta +CurrentModule = ProximalCore +``` + +## Interface Meta-Functions +The main goal of this function to provide a universal interface to smooth functions +and proximable functions to be used by proximal gradient algorithms. +```@autodocs +Modules = [ProximalCore] +Pages = ["gradient_and_prox.jl"] +``` + +## Properties +```@autodocs +Modules = [ProximalCore] +Pages = ["properties.jl"] +``` + +## Basic Functions +```@autodocs +Modules = [ProximalCore] +Pages = ["base_functions.jl"] +``` diff --git a/docs/src/index.md b/docs/src/index.md new file mode 100644 index 0000000..5e007ca --- /dev/null +++ b/docs/src/index.md @@ -0,0 +1,20 @@ +# ProximalCore.jl + +Core definitions for the ProximalOperators and ProximalAlgorithms ecosystem. + +## Installation + +```julia +using Pkg +Pkg.add("ProximalCore") +``` + +## Overview + +ProximalCore.jl provides the fundamental types and definitions used by the broader proximal optimization ecosystem in Julia, specifically [ProximalOperators.jl](https://github.com/JuliaFirstOrder/ProximalOperators.jl) and [ProximalAlgorithms.jl](https://github.com/JuliaFirstOrder/ProximalAlgorithms.jl). + +## Contents + +```@contents +Pages = ["api.md"] +``` diff --git a/src/ProximalCore.jl b/src/ProximalCore.jl index 0bc6855..ad20bb3 100644 --- a/src/ProximalCore.jl +++ b/src/ProximalCore.jl @@ -2,140 +2,8 @@ module ProximalCore using LinearAlgebra -is_convex(::Type) = false -is_convex(::T) where T = is_convex(T) - -is_generalized_quadratic(::Type) = false -is_generalized_quadratic(::T) where T = is_generalized_quadratic(T) - -""" - gradient!(y, f, x) - -In-place gradient (and value) of `f` at `x`. - -The gradient is written to the (pre-allocated) array `y`, which should have the same shape/size as `x`. - -Returns the value `f` at `x`. - -See also: [`gradient`](@ref). -""" -gradient! - -""" - gradient(f, x) - -Gradient (and value) of `f` at `x`. - -Return a tuple `(y, fx)` consisting of -- `y`: the gradient of `f` at `x` -- `fx`: the value of `f` at `x` - -See also: [`gradient!`](@ref). -""" -function gradient(f, x) - y = similar(x) - fx = gradient!(y, f, x) - return y, fx -end - -""" - prox!(y, f, x, gamma=1) - -In-place proximal mapping for `f`, evaluated at `x`, with stepsize `gamma`. - -The proximal mapping is defined as -```math -\\mathrm{prox}_{\\gamma f}(x) = \\arg\\min_z \\left\\{ f(z) + \\tfrac{1}{2\\gamma}\\|z-x\\|^2 \\right\\}. -``` -The result is written to the (pre-allocated) array `y`, which should have the same shape/size as `x`. - -Returns the value of `f` at `y`. - -See also: [`prox`](@ref). -""" -prox! - -prox!(y, f, x) = prox!(y, f, x, 1) - -""" - prox(f, x, gamma=1) - -Proximal mapping for `f`, evaluated at `x`, with stepsize `gamma`. - -The proximal mapping is defined as -```math -\\mathrm{prox}_{\\gamma f}(x) = \\arg\\min_z \\left\\{ f(z) + \\tfrac{1}{2\\gamma}\\|z-x\\|^2 \\right\\}. -``` - -Returns a tuple `(y, fy)` consisting of -- `y`: the output of the proximal mapping of `f` at `x` with stepsize `gamma` -- `fy`: the value of `f` at `y` - -See also: [`prox!`](@ref). -""" -function prox(f, x, gamma=1) - y = similar(x) - fy = prox!(y, f, x, gamma) - return y, fy -end - -struct Zero end - -(::Zero)(x) = real(eltype(x))(0) - -function gradient!(y, f::Zero, x) - y .= eltype(x)(0) - return f(x) -end - -function prox!(y, ::Zero, x, gamma) - y .= x - return real(eltype(y))(0) -end - -is_convex(::Type{Zero}) = true -is_generalized_quadratic(::Type{Zero}) = true - -struct IndZero end - -function (::IndZero)(x) - R = real(eltype(x)) - if iszero(x) - return R(0) - end - return R(Inf) -end - -is_convex(::Type{IndZero}) = true -is_generalized_quadratic(::Type{IndZero}) = true - -function prox!(y, ::IndZero, x, gamma) - R = real(eltype(x)) - y .= R(0) - return R(0) -end - -struct ConvexConjugate{T} - f::T -end - -is_convex(::Type{<:ConvexConjugate}) = true -is_generalized_quadratic(::Type{ConvexConjugate{T}}) where T = is_generalized_quadratic(T) - -function prox_conjugate!(y, u, f, x, gamma) - u .= x ./ gamma - v = prox!(y, f, u, 1 / gamma) - v = real(dot(x, y)) - gamma * real(dot(y, y)) - v - y .= x .- gamma .* y - return v -end - -prox_conjugate!(y, f, x, gamma) = prox_conjugate!(y, similar(x), f, x, gamma) - -prox!(y, g::ConvexConjugate, x, gamma) = prox_conjugate!(y, g.f, x, gamma) - -convex_conjugate(f) = ConvexConjugate(f) -convex_conjugate(::Zero) = IndZero() -convex_conjugate(::IndZero) = Zero() +include("properties.jl") +include("gradient_and_prox.jl") +include("base_functions.jl") end # module diff --git a/src/base_functions.jl b/src/base_functions.jl new file mode 100644 index 0000000..498e546 --- /dev/null +++ b/src/base_functions.jl @@ -0,0 +1,95 @@ +""" + Zero() + +Constructs the zero function, i.e., the function that is zero everywhere. + +# Example +```jldoctest +julia> f = Zero() +Zero() + +julia> f(rand(3)) +0.0 +``` +""" +struct Zero end + +(::Zero)(x) = real(eltype(x))(0) + +function gradient!(y, f::Zero, x) + y .= eltype(x)(0) + return f(x) +end + +function prox!(y, ::Zero, x, gamma) + y .= x + return real(eltype(y))(0) +end + +is_convex(::Type{Zero}) = true +is_generalized_quadratic(::Type{Zero}) = true + +""" + IndZero() + +Constructs the indicator function of the zero set, i.e., the function that is zero if the input is zero and infinity otherwise. + +# Example +```jldoctest +julia> f = IndZero() +IndZero() + +julia> f([1, 2, 3]) +Inf + +julia> f([0, 0, 0]) +0.0 +``` +""" +struct IndZero end + +function (::IndZero)(x) + R = real(eltype(x)) + if iszero(x) + return R(0) + end + return R(Inf) +end + +is_convex(::Type{IndZero}) = true +is_generalized_quadratic(::Type{IndZero}) = true + +function prox!(y, ::IndZero, x, gamma) + R = real(eltype(x)) + y .= R(0) + return R(0) +end + +""" + ConvexConjugate(f) + +Constructs the convex conjugate of the function `f`. The convex conjugate of a function `f` is defined as + `f*(y) = sup_x { - f(x)}`. +""" +struct ConvexConjugate{T} + f::T +end + +is_convex(::Type{<:ConvexConjugate}) = true +is_generalized_quadratic(::Type{ConvexConjugate{T}}) where T = is_generalized_quadratic(T) + +function prox_conjugate!(y, u, f, x, gamma) + u .= x ./ gamma + v = prox!(y, f, u, 1 / gamma) + v = real(dot(x, y)) - gamma * real(dot(y, y)) - v + y .= x .- gamma .* y + return v +end + +prox_conjugate!(y, f, x, gamma) = prox_conjugate!(y, similar(x), f, x, gamma) + +prox!(y, g::ConvexConjugate, x, gamma) = prox_conjugate!(y, g.f, x, gamma) + +convex_conjugate(f) = ConvexConjugate(f) +convex_conjugate(::Zero) = IndZero() +convex_conjugate(::IndZero) = Zero() diff --git a/src/gradient_and_prox.jl b/src/gradient_and_prox.jl new file mode 100644 index 0000000..ea06076 --- /dev/null +++ b/src/gradient_and_prox.jl @@ -0,0 +1,70 @@ +""" + gradient!(y, f, x) + +In-place gradient (and value) of `f` at `x`. + +The gradient is written to the (pre-allocated) array `y`, which should have the same shape/size as `x`. + +Returns the value `f` at `x`. + +See also: [`gradient`](@ref). +""" +gradient! + +""" + gradient(f, x) + +Gradient (and value) of `f` at `x`. + +Return a tuple `(y, fx)` consisting of +- `y`: the gradient of `f` at `x` +- `fx`: the value of `f` at `x` + +See also: [`gradient!`](@ref). +""" +function gradient(f, x) + y = similar(x) + fx = gradient!(y, f, x) + return y, fx +end + +""" + prox!(y, f, x, gamma=1) + +In-place proximal mapping for `f`, evaluated at `x`, with stepsize `gamma`. + +The proximal mapping is defined as +```math +\\mathrm{prox}_{\\gamma f}(x) = \\arg\\min_z \\left\\{ f(z) + \\tfrac{1}{2\\gamma}\\|z-x\\|^2 \\right\\}. +``` +The result is written to the (pre-allocated) array `y`, which should have the same shape/size as `x`. + +Returns the value of `f` at `y`. + +See also: [`prox`](@ref). +""" +prox! + +prox!(y, f, x) = prox!(y, f, x, 1) + +""" + prox(f, x, gamma=1) + +Proximal mapping for `f`, evaluated at `x`, with stepsize `gamma`. + +The proximal mapping is defined as +```math +\\mathrm{prox}_{\\gamma f}(x) = \\arg\\min_z \\left\\{ f(z) + \\tfrac{1}{2\\gamma}\\|z-x\\|^2 \\right\\}. +``` + +Returns a tuple `(y, fy)` consisting of +- `y`: the output of the proximal mapping of `f` at `x` with stepsize `gamma` +- `fy`: the value of `f` at `y` + +See also: [`prox!`](@ref). +""" +function prox(f, x, gamma=1) + y = similar(x) + fy = prox!(y, f, x, gamma) + return y, fy +end diff --git a/src/properties.jl b/src/properties.jl new file mode 100644 index 0000000..0ce598a --- /dev/null +++ b/src/properties.jl @@ -0,0 +1,129 @@ +""" + is_convex(T::Type) + +Returns `true` if the type `T` represents a convex function. +A function f(x) is convex if its domain is a convex set and for all x, y in the domain and for all λ in [0, 1], we have f(λx + (1-λ)y) ≤ λf(x) + (1-λ)f(y). +""" +is_convex(::Type) = false +is_convex(::T) where T = is_convex(T) + +""" + is_generalized_quadratic(T::Type) + +Returns `true` if the type `T` represents a generalized quadratic function, i.e. a quadratic function over a subspace and +∞ outside of it. +A quadratic function has the form f(x)=(1/2)* + + c where A is a symmetric matrix, b is a vector, and c a real number. +""" +is_generalized_quadratic(::Type) = false +is_generalized_quadratic(::T) where T = is_generalized_quadratic(T) + +""" + is_proximable(T::Type) + +Returns `true` if the type `T` has a proximal operator that can be expressed in a closed formula. + (i.e. `prox!` function is defined for the type `T`). +""" +is_proximable(::Type) = true +is_proximable(::T) where T = is_proximable(T) + +""" + is_separable(T::Type) + +Returns `true` if the type `T` represents a separable function. +A function f(x) is separable if it can applied to each element of x independently. +""" +is_separable(::Type) = false +is_separable(::T) where T = is_separable(T) + +""" + is_singleton_indicator(T::Type) + +Returns `true` if the type `T` represents a singleton indicator function. +An indicator function f(x) is a singleton if it is 0 for a single value and ∞ otherwise. +""" +is_singleton_indicator(::Type) = false +is_singleton_indicator(::T) where T = is_singleton_indicator(T) + +""" + is_cone_indicator(T::Type) + +Returns `true` if the type `T` represents the indicator function of a cone. +A cone is a set C such that if x ∈ C, then λx ∈ C for any λ ≥ 0. In other words, is a set that is closed under non-negative scaling. +`` +""" +is_cone_indicator(::Type) = false +is_cone_indicator(::T) where T = is_cone_indicator(T) + +""" + is_affine_indicator(T::Type) + +Returns `true` if the type `T` represents the indicator of an affine set. +An affine set is a set that can be represented as the solution set of a system of linear equations. +In other words, f(x) = 0 if Ax = b, for a given matrix A and vector b, and ∞ otherwise. +""" +is_affine_indicator(T::Type) = is_singleton_indicator(T) +is_affine_indicator(::T) where T = is_affine_indicator(T) + +""" + is_set_indicator(T::Type) + +Returns `true` if the type `T` represents an indicator function of a set. +The indicator of a set S is a function associating 0 to points in S, and ∞ otherwise. +""" +is_set_indicator(T::Type) = is_cone_indicator(T) || is_affine_indicator(T) +is_set_indicator(::T) where T = is_set_indicator(T) + +""" + is_positively_homogeneous(T::Type) + +Returns `true` if the type `T` represents a positively homogeneous function. +A function f(x) is positively homogeneous if f(λx) = λf(x) for all λ ≥ 0. +""" +is_positively_homogeneous(T::Type) = is_cone_indicator(T) +is_positively_homogeneous(::T) where T = is_positively_homogeneous(T) + +""" + is_support(T::Type) + +Returns `true` if the type `T` represents a support function of a set. +A function f(x) is a support function of a set C if f(x) = sup{⟨x, c⟩ : c ∈ C}. +""" +is_support(T::Type) = is_convex(T) && is_positively_homogeneous(T) +is_support(::T) where T = is_support(T) + +""" + is_locally_smooth(T::Type) + +Returns `true` if the type `T` represents a locally smooth function. +A function f(x) is locally smooth if it is smooth on every open set within its domain. +If f is locally smooth, then `gradient!(y, f, x)` is expected to be defined, and it should return the value of f at x and store the gradient in y. +""" +is_locally_smooth(T::Type) = is_smooth(T) +is_locally_smooth(::T) where T = is_locally_smooth(T) + +""" + is_smooth(T::Type) + +Returns `true` if the type `T` represents a smooth function. +A function f(x) is smooth if it is continuously differentiable and its gradient is Lipschitz. +If f is smooth, then `gradient!(y, f, x)` is expected to be defined, and it should return the value of f at x and store the gradient in y. +""" +is_smooth(::Type) = false +is_smooth(::T) where T = is_smooth(T) + +""" + is_quadratic(T::Type) + +Returns `true` if the type `T` represents a quadratic function. +A function f(x) is quadratic if it is smooth and its Hessian is constant. +""" +is_quadratic(T::Type) = is_generalized_quadratic(T) && is_smooth(T) +is_quadratic(::T) where T = is_quadratic(T) + +""" + is_strongly_convex(T::Type) + +Returns `true` if the type `T` represents a strongly convex function. +A function f(x) is strongly convex if it is convex and there exists a positive constant μ such that f(x) - μ/2 * ||x||^2 is convex. +""" +is_strongly_convex(::Type) = false +is_strongly_convex(::T) where T = is_strongly_convex(T)