diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 76f776a..c483152 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -13,7 +13,7 @@ jobs: matrix: version: - '1' - - '1.6' + - '1.10' os: - ubuntu-latest - macOS-latest diff --git a/Project.toml b/Project.toml index 73a52e2..9bf0b22 100644 --- a/Project.toml +++ b/Project.toml @@ -1,10 +1,11 @@ name = "StructuredOptimization" uuid = "46cd3e9d-64ff-517d-a929-236bc1a1fc9d" -version = "0.4.0" +version = "0.5.0" [deps] AbstractOperators = "d9c5613a-d543-52d8-9afd-8f241a8c3f1c" DSP = "717857b8-e6f2-59f4-9121-6e50c889abd2" +DifferentiationInterface = "a0c0ee7d-e4b9-4e03-894e-1c5f64a51d63" FFTW = "7a1cc6ca-52ef-59f5-83cd-3a7055c09341" LinearAlgebra = "37e2e46d-f89d-539d-b4ee-838fcccc9c8e" ProximalAlgorithms = "140ffc9f-1907-541a-a177-7475e0a401e9" @@ -12,18 +13,24 @@ ProximalOperators = "a725b495-10eb-56fe-b38b-717eba820537" RecursiveArrayTools = "731186ca-8d62-57ce-b412-fbd966d074cd" [compat] -AbstractOperators = "0.3" -DSP = "0.5.1 - 0.7" +AbstractOperators = "0.4" +Aqua = "0.8" +DSP = "0.5.1 - 0.8" +DifferentiationInterface = "0.6" FFTW = "1" -ProximalAlgorithms = "0.5" -ProximalOperators = "0.15" -RecursiveArrayTools = "1 - 2" -julia = "1.4" +LinearAlgebra = "1" +ProximalAlgorithms = "0.7" +ProximalOperators = "0.16" +Random = "1" +RecursiveArrayTools = "1 - 3" +Test = "1" +julia = "1.10" [extras] +Aqua = "4c88cf16-eb10-579e-8560-4a9242c79595" LinearAlgebra = "37e2e46d-f89d-539d-b4ee-838fcccc9c8e" Random = "9a3f8284-a2c9-5f02-9a11-845980a1fd5c" Test = "8dfed614-e22c-5e08-85e1-65c5234f0b40" [targets] -test = ["LinearAlgebra", "Test", "Random"] +test = ["LinearAlgebra", "Test", "Random", "Aqua"] diff --git a/README.md b/README.md index 6ec97e5..f69ea04 100644 --- a/README.md +++ b/README.md @@ -2,6 +2,7 @@ [![Build status](https://github.com/JuliaFirstOrder/StructuredOptimization.jl/workflows/CI/badge.svg)](https://github.com/JuliaFirstOrder/StructuredOptimization.jl/actions?query=workflow%3ACI) [![codecov](https://codecov.io/gh/JuliaFirstOrder/StructuredOptimization.jl/branch/master/graph/badge.svg)](https://codecov.io/gh/JuliaFirstOrder/StructuredOptimization.jl) +[![Aqua QA](https://raw.githubusercontent.com/JuliaTesting/Aqua.jl/master/badge.svg)](https://github.com/JuliaTesting/Aqua.jl) [![](https://img.shields.io/badge/docs-stable-blue.svg)](https://juliafirstorder.github.io/StructuredOptimization.jl/stable) [![](https://img.shields.io/badge/docs-latest-blue.svg)](https://juliafirstorder.github.io/StructuredOptimization.jl/latest) diff --git a/src/StructuredOptimization.jl b/src/StructuredOptimization.jl index e989dad..2b24d17 100644 --- a/src/StructuredOptimization.jl +++ b/src/StructuredOptimization.jl @@ -9,6 +9,11 @@ using ProximalAlgorithms import ProximalAlgorithms: ZeroFPR, PANOC, PANOCplus export ZeroFPR, PANOC, PANOCplus +ProximalAlgorithms.value_and_gradient(f, x) = begin + y, fy = gradient(f, x) + return fy, y +end + include("syntax/syntax.jl") include("calculus/precomposeNonlinear.jl") # TODO move to ProximalOperators? include("arraypartition.jl") # TODO move to ProximalOperators? diff --git a/src/solvers/build_solve.jl b/src/solvers/build_solve.jl index b360902..99aa8b0 100644 --- a/src/solvers/build_solve.jl +++ b/src/solvers/build_solve.jl @@ -1,7 +1,5 @@ -export build - """ - parse_problem(terms::Tuple, solver::ForwardBackwardSolver) + parse_problem(terms::Tuple, solver::ForwardBackwardSolver) Takes as input a tuple containing the terms defining the problem and the solver. @@ -22,26 +20,26 @@ julia> StructuredOptimization.parse_problem(p, PANOCplus()); ``` """ function parse_problem(terms::Tuple, solver::T) where T <: ForwardBackwardSolver - x = extract_variables(terms) - # Separate smooth and nonsmooth - smooth, nonsmooth = split_smooth(terms) - if is_proximable(nonsmooth) - g = extract_proximable(x, nonsmooth) + x = extract_variables(terms) + # Separate smooth and nonsmooth + smooth, nonsmooth = split_smooth(terms) + if is_proximable(nonsmooth) + g = extract_proximable(x, nonsmooth) kwargs = Dict{Symbol, Any}(:g => g) - if !isempty(smooth) - if is_linear(smooth) - f = extract_functions(smooth) - A = extract_operators(x, smooth) - kwargs[:A] = A - else # ?? - f = extract_functions_nodisp(smooth) - A = extract_affines(x, smooth) - f = PrecomposeNonlinear(f, A) - end - kwargs[:f] = f - end - return (x, kwargs) - end + if !isempty(smooth) + if is_linear(smooth) + f = extract_functions(smooth) + A = extract_operators(x, smooth) + kwargs[:A] = A + else # ?? + f = extract_functions_nodisp(smooth) + A = extract_affines(x, smooth) + f = PrecomposeNonlinear(f, A) + end + kwargs[:f] = f + end + return (x, kwargs) + end error("Sorry, I cannot parse this problem for solver of type $(T)") end @@ -49,7 +47,7 @@ end export solve """ - solve(terms::Tuple, solver::ForwardBackwardSolver) + solve(terms::Tuple, solver::ForwardBackwardSolver) Takes as input a tuple containing the terms defining the problem and the solver options. @@ -71,8 +69,8 @@ julia> ~x ``` """ function solve(terms::Tuple, solver::ForwardBackwardSolver) - x, kwargs = parse_problem(terms, solver) + x, kwargs = parse_problem(terms, solver) x_star, it = solver(; x0 = ~x, kwargs...) - ~x .= x_star - return x, it + ~x .= x_star + return x, it end diff --git a/src/syntax/expressions/addition.jl b/src/syntax/expressions/addition.jl index bb700a0..aee3125 100644 --- a/src/syntax/expressions/addition.jl +++ b/src/syntax/expressions/addition.jl @@ -1,7 +1,7 @@ import Base: +, - """ - +(ex1::AbstractExpression, ex2::AbstractExpression) + +(ex1::AbstractExpression, ex2::AbstractExpression) Add two expressions. @@ -47,112 +47,97 @@ julia> ex3.+z function (+)(a::AbstractExpression, b::AbstractExpression) A = convert(Expression,a) B = convert(Expression,b) - if variables(A) == variables(B) + if variables(A) == variables(B) return Expression{length(A.x)}(A.x,affine(A)+affine(B)) - else - opA = affine(A) - xA = variables(A) - opB = affine(B) - xB = variables(B) + else + opA = affine(A) + xA = variables(A) + opB = affine(B) + xB = variables(B) xNew, opNew = Usum_op(xA,xB,opA,opB,true) return Expression{length(xNew)}(xNew,opNew) - end + end end # sum expressions function (-)(a::AbstractExpression, b::AbstractExpression) A = convert(Expression,a) B = convert(Expression,b) - if variables(A) == variables(B) + if variables(A) == variables(B) return Expression{length(A.x)}(A.x,affine(A)-affine(B)) - else - opA = affine(A) - xA = variables(A) - opB = affine(B) - xB = variables(B) + else + opA = affine(A) + xA = variables(A) + opB = affine(B) + xB = variables(B) xNew, opNew = Usum_op(xA,xB,opA,opB,false) return Expression{length(xNew)}(xNew,opNew) - end + end end #unsigned sum affines with single variables -function Usum_op(xA::Tuple{Variable}, - xB::Tuple{Variable}, - A::AbstractOperator, - B::AbstractOperator,sign::Bool) +function Usum_op(xA::Tuple{Variable}, xB::Tuple{Variable}, A::AbstractOperator, B::AbstractOperator, sign::Bool) xNew = (xA...,xB...) opNew = sign ? hcat(A,B) : hcat(A,-B) - return xNew, opNew + return xNew, opNew end #unsigned sum: HCAT + AbstractOperator -function Usum_op(xA::NTuple{N,Variable}, - xB::Tuple{Variable}, - A::L1, - B::AbstractOperator,sign::Bool) where {N, M, L1<:HCAT{N}} - if xB[1] in xA +function Usum_op(xA::NTuple{N,Variable}, xB::Tuple{Variable}, A::HCAT{N}, B::AbstractOperator, sign::Bool) where {N} + if xB[1] in xA idx = findfirst(xA.==Ref(xB[1])) S = sign ? A[idx]+B : A[idx]-B - xNew = xA + xNew = xA opNew = hcat(A[1:idx-1],S,A[idx+1:N] ) - else + else xNew = (xA...,xB...) opNew = sign ? hcat(A,B) : hcat(A,-B) - end - return xNew, opNew + end + return xNew, opNew end #unsigned sum: AbstractOperator+HCAT -function Usum_op(xA::Tuple{Variable}, - xB::NTuple{N,Variable}, - A::AbstractOperator, - B::L2,sign::Bool) where {N, M, L2<:HCAT{N}} - if xA[1] in xB +function Usum_op(xA::Tuple{Variable}, xB::NTuple{N,Variable}, A::AbstractOperator, B::HCAT{N}, sign::Bool) where {N} + if xA[1] in xB idx = findfirst(xA.==Ref(xB[1])) S = sign ? A+B[idx] : B[idx]-A - xNew = xB + xNew = xB opNew = sign ? hcat(B[1:idx-1],S,B[idx+1:N] ) : -hcat(B[1:idx-1],S,B[idx+1:N] ) - else + else xNew = (xA...,xB...) opNew = sign ? hcat(A,B) : hcat(A,-B) - end + end - return xNew, opNew + return xNew, opNew end #unsigned sum: HCAT+HCAT -function Usum_op(xA::NTuple{NA,Variable}, - xB::NTuple{NB,Variable}, - A::L1, - B::L2,sign::Bool) where {NA,NB,M, - L1<:HCAT{NB}, - L2<:HCAT{NB} } - xNew = xA - opNew = A - for i in eachindex(xB) - xNew, opNew = Usum_op(xNew, (xB[i],), opNew, B[i], sign) - end +function Usum_op(xA::NTuple{NA,Variable}, xB::NTuple{NB,Variable}, A::HCAT{NB}, B::HCAT{NB}, sign::Bool) where {NA,NB} + xNew = xA + opNew = A + for i in eachindex(xB) + xNew, opNew = Usum_op(xNew, (xB[i],), opNew, B[i], sign) + end return xNew,opNew end #unsigned sum: multivar AbstractOperator + AbstractOperator -function Usum_op(xA::NTuple{N,Variable}, - xB::Tuple{Variable}, - A::AbstractOperator, - B::AbstractOperator,sign::Bool) where {N} - if xB[1] in xA - Z = Zeros(A) #this will be an HCAT +function Usum_op( + xA::NTuple{N,Variable}, xB::Tuple{Variable}, A::AbstractOperator, B::AbstractOperator, sign::Bool +) where {N} + if xB[1] in xA + Z = Zeros(A) #this will be an HCAT xNew, opNew = Usum_op(xA,xB,Z,B,sign) - opNew += A - else + opNew += A + else xNew = (xA...,xB...) opNew = sign ? hcat(A,B) : hcat(A,-B) - end - return xNew, opNew + end + return xNew, opNew end """ - +(ex::AbstractExpression, b::Union{AbstractArray,Number}) + +(ex::AbstractExpression, b::Union{AbstractArray,Number}) Add a scalar or an `Array` to an expression: @@ -213,9 +198,9 @@ function Broadcast.broadcasted(::typeof(+),a::AbstractExpression, b::AbstractExp elseif prod(size(affine(B),1)) > prod(size(affine(A),1)) A = Expression{length(A.x)}(variables(A), BroadCast(affine(A),size(affine(B),1))) - end + end return A+B - end + end return A+B end @@ -229,8 +214,8 @@ function Broadcast.broadcasted(::typeof(-),a::AbstractExpression, b::AbstractExp elseif prod(size(affine(B),1)) > prod(size(affine(A),1)) A = Expression{length(A.x)}(variables(A), BroadCast(affine(A),size(affine(B),1))) - end + end return A-B - end + end return A-B end diff --git a/src/syntax/variable.jl b/src/syntax/variable.jl index d5ede3f..c3416c7 100644 --- a/src/syntax/variable.jl +++ b/src/syntax/variable.jl @@ -16,11 +16,12 @@ Returns a `Variable` of dimension `dims` initialized with an array of all zeros. Returns a `Variable` of dimension `size(x)` initialized with `x` """ -function Variable(T::Type, args::Vararg{I,N}) where {I <: Integer,N} - Variable{T,N,Array{T,N}}(zeros(T, args...)) +function Variable(T::Type, args::Int...) + N = length(args) + Variable{T,N,Array{T,N}}(zeros(T, args...)) end -function Variable(args::Vararg{I}) where {I <: Integer} +function Variable(args::Int...) Variable(zeros(args...)) end @@ -30,7 +31,6 @@ function Base.show(io::IO, x::Variable) print(io, "Variable($(eltype(x.x)), $(size(x.x)))") end - """ ~(x::Variable) @@ -46,7 +46,7 @@ size(x::Variable, [dim...]) Like `size(A::AbstractArray, [dims...])` returns the tuple containing the dimensions of the variable `x`. """ size(x::Variable) = size(x.x) -size(x::Variable, dim::I) where { I <: Integer} = size(x.x, dim) +size(x::Variable, dim::Integer) = size(x.x, dim) """ eltype(x::Variable) diff --git a/test/runtests.jl b/test/runtests.jl index b6731bd..a256eba 100644 --- a/test/runtests.jl +++ b/test/runtests.jl @@ -6,30 +6,46 @@ using RecursiveArrayTools using LinearAlgebra, Random using DSP, FFTW using Test +using Aqua Random.seed!(0) @testset "StructuredOptimization" begin + @testset "Calculus" begin + include("test_proxstuff.jl") + end -@testset "Calculus" begin - include("test_proxstuff.jl") -end + @testset "Syntax" begin + include("test_variables.jl") + include("test_expressions.jl") + include("test_AbstractOp_binding.jl") + include("test_terms.jl") + end -@testset "Syntax" begin - include("test_variables.jl") - include("test_expressions.jl") - include("test_AbstractOp_binding.jl") - include("test_terms.jl") -end + @testset "Problem construction" begin + include("test_problem.jl") + include("test_build_minimize.jl") + end -@testset "Problem construction" begin - include("test_problem.jl") - include("test_build_minimize.jl") -end - -@testset "End-to-end tests" begin - include("test_usage_small.jl") - include("test_usage.jl") -end + @testset "End-to-end tests" begin + include("test_usage_small.jl") + include("test_usage.jl") + end + @testset "Aqua" begin + Aqua.test_all(StructuredOptimization; ambiguities=false, piracies=false) + Aqua.test_ambiguities( + StructuredOptimization; exclude=[Base.:(+), Base.:<=, Base.:>=], broken=true + ) + Aqua.test_piracies( + StructuredOptimization; + treat_as_own=[ + ProximalAlgorithms.value_and_gradient, + ProximalOperators.prox, + ProximalOperators.prox!, + ProximalOperators.gradient, + ProximalOperators.gradient!, + ], + ) + end end diff --git a/test/test_build_minimize.jl b/test/test_build_minimize.jl index 828e221..a4c2c18 100644 --- a/test/test_build_minimize.jl +++ b/test/test_build_minimize.jl @@ -42,15 +42,18 @@ xp = copy(~x) @test norm(xp-xpg) <= 1e-4 # test nonconvex Rosenbrock function with known minimum -solvers = [ZeroFPR(tol = 1e-6), PANOC(tol = 1e-6)] -for solver in solvers - x = Variable(1) - y = Variable(1) - a,b = 2.0, 100.0 +function test_solver(solver) + x = Variable(1) + y = Variable(1) + a, b = 2.0, 100.0 - cf = norm(x-a)^2+b*norm(pow(x,2)-y)^2 - @minimize cf+1e-10*norm(x,1)+1e-10*norm(y,1) with solver + cf = norm(x - a)^2 + b * norm(pow(x, 2) - y)^2 + @minimize cf + 1e-10 * norm(x, 1) + 1e-10 * norm(y, 1) with solver - @test norm(~x-[a]) < 1e-4 - @test norm(~y-[a^2]) < 1e-4 + @test norm(~x - [a]) < 1e-4 + @test norm(~y - [a^2]) < 1e-4 +end +solvers = [ZeroFPR(; tol=1e-6), PANOC(; tol=1e-6)] +for solver in solvers + test_solver(solver) end