From ace0bca32d01a2805c0436432ca63c1a402967e6 Mon Sep 17 00:00:00 2001 From: Olivier Cots Date: Tue, 17 Mar 2026 12:15:06 +0100 Subject: [PATCH 1/7] Integrate Uno solver and add solver requirements documentation - Add Uno solver integration (CPU-only, ADNLP-only) - Add solver requirements documentation with import instructions - Update tests to include Uno with proper constraints - Update CHANGELOG.md for version 1.3.1-beta --- CHANGELOG.md | 33 +++++++ Project.toml | 8 +- docs/Project.toml | 2 + docs/doc.jl | 49 ---------- docs/src/assets/Manifest.toml | 117 +++++++++++------------ docs/src/assets/Project.toml | 2 + docs/src/manual-solve-advanced.md | 1 + docs/src/manual-solve-explicit.md | 16 +++- docs/src/manual-solve-gpu.md | 4 + docs/src/manual-solve.md | 25 +++++ src/OptimalControl.jl | 2 +- src/helpers/describe.jl | 1 + src/helpers/methods.jl | 9 +- src/helpers/print.jl | 27 ++++++ src/helpers/registry.jl | 5 +- src/imports/ctsolvers.jl | 2 +- test/suite/helpers/test_methods.jl | 11 ++- test/suite/helpers/test_registry.jl | 12 ++- test/suite/reexport/test_ctsolvers.jl | 2 + test/suite/solve/test_canonical.jl | 128 +++++++++++++------------- test/suite/solve/test_descriptive.jl | 16 ++++ 21 files changed, 279 insertions(+), 193 deletions(-) delete mode 100644 docs/doc.jl diff --git a/CHANGELOG.md b/CHANGELOG.md index 6f500778..8f7019cf 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -7,6 +7,39 @@ Versions follow [Semantic Versioning](https://semver.org/spec/v2.0.0.html). --- +## [1.3.1-beta] โ€” 2026-03-17 + +### Added + +- **Uno solver integration**: Full support for the Uno nonlinear optimization solver + - Added to solver registry with CPU-only support + - Added method `(:collocation, :adnlp, :uno, :cpu)` to available methods + - Uno only compatible with ADNLP modeler (not ExaModels) + - Comprehensive test coverage with Beam and Goddard problems + - Extension error handling when `UnoSolver` package not loaded + +- **Solver requirements documentation**: Clear documentation of required imports for each solver + - New "Solver requirements" section in `manual-solve.md` + - Updated examples in `manual-solve-explicit.md` with import instructions + - GPU requirements clarification in `manual-solve-gpu.md` + - Based on CTSolvers extension triggers: + - Ipopt: `using NLPModelsIpopt` + - MadNLP: `using MadNLP` (CPU) or `using MadNLPGPU` (GPU) + - Uno: `using UnoSolver` + - MadNCL: `using MadNCL` and `using MadNLP` + - Knitro: `using NLPModelsKnitro` (commercial license) + +- **Solver output detection**: `will_solver_print(::CTSolvers.Uno)` method to check if Uno will produce output based on `logger` option (silent when `logger="SILENT"`) + +### Changed + +- **Solver count**: Updated from 4 to 5 available solvers (Ipopt, MadNLP, Uno, MadNCL, Knitro) +- **Method count**: Updated from 10 to 11 available methods (9 CPU + 2 GPU) +- **Test structure**: Restructured canonical tests to use modeler-solver pairs, respecting Uno/ADNLP-only constraint +- **Documentation**: Updated solver lists and examples throughout documentation to include Uno + +--- + ## [Unreleased] โ€” branch `action-options` ### Added diff --git a/Project.toml b/Project.toml index 089530d2..b7ce44a2 100644 --- a/Project.toml +++ b/Project.toml @@ -1,6 +1,6 @@ name = "OptimalControl" uuid = "5f98b655-cc9a-415a-b60e-744165666948" -version = "1.3.0-beta" +version = "1.3.1-beta" authors = ["Olivier Cots "] [deps] @@ -13,6 +13,7 @@ CTParser = "32681960-a1b1-40db-9bff-a1ca817385d1" CTSolvers = "d3e8d392-8e4b-4d9b-8e92-d7d4e3650ef6" CommonSolve = "38540f10-b2f7-11e9-35d8-d573e4eb0ff2" DocStringExtensions = "ffbed154-4ef7-542d-bbb7-c09d3a79fcae" +Documenter = "e30172f5-a6a5-5a46-863b-614d45cd2de4" ExaModels = "1037b233-b668-4ce9-9b63-f9f681f55dd2" LinearAlgebra = "37e2e46d-f89d-539d-b4ee-838fcccc9c8e" NLPModels = "a4795742-8479-5a88-8948-cc11e1c8c1a6" @@ -33,6 +34,7 @@ CUDA = "5" CommonSolve = "0.2" DifferentiationInterface = "0.7" DocStringExtensions = "0.9" +Documenter = "1.17.0" ExaModels = "0.9" ForwardDiff = "0.10, 1.0" LinearAlgebra = "1" @@ -49,6 +51,7 @@ Reexport = "1" SolverCore = "0.3.9" SplitApplyCombine = "1" Test = "1" +UnoSolver = "0.2" julia = "1.10" [extras] @@ -66,6 +69,7 @@ OrdinaryDiffEq = "1dea7af3-3e70-54e6-95c3-0bf5283fa5ed" Printf = "de0858da-6303-5e67-8744-51eddeeeb8d7" SplitApplyCombine = "03a91e81-4c3e-53e1-a0a4-9c0c8f19dd66" Test = "8dfed614-e22c-5e08-85e1-65c5234f0b40" +UnoSolver = "1baa60ac-02f7-4b39-a7a8-2f4f58486b05" [targets] -test = ["BenchmarkTools", "CUDA", "DifferentiationInterface", "ForwardDiff", "LinearAlgebra", "MadNCL", "MadNLP", "MadNLPGPU", "NLPModelsIpopt", "NonlinearSolve", "OrdinaryDiffEq", "Printf", "SplitApplyCombine", "Test"] +test = ["BenchmarkTools", "CUDA", "DifferentiationInterface", "ForwardDiff", "LinearAlgebra", "MadNCL", "MadNLP", "MadNLPGPU", "NLPModelsIpopt", "NonlinearSolve", "OrdinaryDiffEq", "Printf", "SplitApplyCombine", "Test", "UnoSolver"] diff --git a/docs/Project.toml b/docs/Project.toml index 66265d9c..255eb444 100644 --- a/docs/Project.toml +++ b/docs/Project.toml @@ -24,6 +24,7 @@ NLPModelsKnitro = "bec4dd0d-7755-52d5-9a02-22f0ffc7efcb" NonlinearSolve = "8913a72c-1f9b-4ce2-8d82-65094dcecaec" OrdinaryDiffEq = "1dea7af3-3e70-54e6-95c3-0bf5283fa5ed" Plots = "91a5bcdd-55d7-5caf-9e0b-520d859cae80" +UnoSolver = "1baa60ac-02f7-4b39-a7a8-2f4f58486b05" [compat] ADNLPModels = "0.8" @@ -51,4 +52,5 @@ NLPModelsKnitro = "0.10" NonlinearSolve = "4" OrdinaryDiffEq = "6" Plots = "1" +UnoSolver = "0.2" julia = "1.10" diff --git a/docs/doc.jl b/docs/doc.jl deleted file mode 100644 index c57e8547..00000000 --- a/docs/doc.jl +++ /dev/null @@ -1,49 +0,0 @@ -#!/usr/bin/env julia - -""" - Documentation Generation Script for OptimalControl.jl - -This script generates the documentation for OptimalControl.jl and then removes -OptimalControl from the docs/Project.toml to keep it clean. - -Usage (from any directory): - julia docs/doc.jl - # OR - julia --project=. docs/doc.jl - # OR - julia --project=docs docs/doc.jl - -The script will: -1. Activate the docs environment -2. Add OptimalControl as a development dependency in docs environment -3. Generate the documentation using docs/make.jl -4. Remove OptimalControl from docs/Project.toml -5. Clean up the docs environment -""" - -using Pkg - -println("๐Ÿš€ Starting documentation generation for OptimalControl.jl...") - -# Step 0: Activate docs environment (works from any directory) -docs_dir = joinpath(@__DIR__) -println("๐Ÿ“ Activating docs environment at: $docs_dir") -Pkg.activate(docs_dir) - -# Step 1: Add OptimalControl as development dependency -println("๐Ÿ“ฆ Adding OptimalControl as development dependency...") -# Get the project root (parent of docs directory) -project_root = dirname(docs_dir) -Pkg.develop(; path=project_root) - -# Step 2: Generate documentation -println("๐Ÿ“š Building documentation...") -include(joinpath(docs_dir, "make.jl")) - -# Step 3: Remove OptimalControl from docs environment -println("๐Ÿงน Cleaning up docs environment...") -Pkg.rm("OptimalControl") - -println("โœ… Documentation generated successfully!") -println("๐Ÿ“– Documentation available at: $(joinpath(docs_dir, "build", "index.html"))") -println("๐Ÿ—‚๏ธ OptimalControl removed from docs/Project.toml") diff --git a/docs/src/assets/Manifest.toml b/docs/src/assets/Manifest.toml index 9a04346e..39e3c64c 100644 --- a/docs/src/assets/Manifest.toml +++ b/docs/src/assets/Manifest.toml @@ -2,7 +2,7 @@ julia_version = "1.12.1" manifest_format = "2.0" -project_hash = "7e8b7ef30337725d46f268d49cca508ab1854a3c" +project_hash = "d470f84ad521e691a87a863486162d9840a77916" [[deps.ADNLPModels]] deps = ["ADTypes", "ForwardDiff", "LinearAlgebra", "NLPModels", "Requires", "ReverseDiff", "SparseArrays", "SparseConnectivityTracer", "SparseMatrixColorings"] @@ -157,12 +157,6 @@ version = "1.1.2" OpenCL = "08131aa3-fb12-5dee-8b74-c09406e224a2" oneAPI = "8f75cd03-7ff8-4ecb-9b8f-daf728133b1b" -[[deps.AxisAlgorithms]] -deps = ["LinearAlgebra", "Random", "SparseArrays", "WoodburyMatrices"] -git-tree-sha1 = "01b8ccb13d68535d73d2b0c23e39bd23155fb712" -uuid = "13072b0f-2c55-5437-9ae7-d433b7a33950" -version = "1.1.0" - [[deps.BFloat16s]] deps = ["LinearAlgebra", "Printf", "Random"] git-tree-sha1 = "e386db8b4753b42caac75ac81d0a4fe161a68a97" @@ -238,19 +232,19 @@ version = "1.0.5" [[deps.CTFlows]] deps = ["CTBase", "CTModels", "DocStringExtensions", "ForwardDiff", "LinearAlgebra", "MLStyle", "MacroTools"] -git-tree-sha1 = "b6f5858ef83e0c6bc6bfa8922f6578c77c5fc758" +git-tree-sha1 = "71ec5ebc9464b1d79e64ea2e53675fa0506e20c6" uuid = "1c39547c-7794-42f7-af83-d98194f657c2" -version = "0.8.15" +version = "0.8.16-beta" weakdeps = ["OrdinaryDiffEq"] [deps.CTFlows.extensions] CTFlowsODE = "OrdinaryDiffEq" [[deps.CTModels]] -deps = ["CTBase", "DocStringExtensions", "Interpolations", "LinearAlgebra", "MLStyle", "MacroTools", "OrderedCollections", "Parameters", "RecipesBase"] -git-tree-sha1 = "e355200d0120f802539c0299fa7c5f09de9ff44a" +deps = ["CTBase", "DocStringExtensions", "LinearAlgebra", "MLStyle", "MacroTools", "OrderedCollections", "Parameters", "RecipesBase"] +git-tree-sha1 = "5b8750830b98dc94f93b36ff252b17b33b2a3533" uuid = "34c4fa32-2049-4079-8329-de33c2a22e2d" -version = "0.9.7" +version = "0.9.9-beta" weakdeps = ["JLD2", "JSON3", "Plots"] [deps.CTModels.extensions] @@ -266,9 +260,9 @@ version = "0.8.10-beta" [[deps.CTSolvers]] deps = ["ADNLPModels", "CTBase", "CTModels", "CommonSolve", "DocStringExtensions", "ExaModels", "KernelAbstractions", "NLPModels", "SolverCore"] -git-tree-sha1 = "384ecf74a31d4401e22d422d49270c10fb81bc00" +git-tree-sha1 = "ceab1d109b9711ceadf3cdbf6b1bc6ba69cb26cd" uuid = "d3e8d392-8e4b-4d9b-8e92-d7d4e3650ef6" -version = "0.4.8-beta" +version = "0.4.9-beta" [deps.CTSolvers.extensions] CTSolversCUDA = "CUDA" @@ -278,6 +272,7 @@ version = "0.4.8-beta" CTSolversMadNCL = ["MadNCL", "MadNLP"] CTSolversMadNLP = ["MadNLP"] CTSolversMadNLPGPU = "MadNLPGPU" + CTSolversUno = "UnoSolver" CTSolversZygote = "Zygote" [deps.CTSolvers.weakdeps] @@ -288,6 +283,7 @@ version = "0.4.8-beta" MadNLPGPU = "d72a61cc-809d-412f-99be-fd81f4b8a598" NLPModelsIpopt = "f4238b75-b362-5c4c-b852-0801c9a21d71" NLPModelsKnitro = "bec4dd0d-7755-52d5-9a02-22f0ffc7efcb" + UnoSolver = "1baa60ac-02f7-4b39-a7a8-2f4f58486b05" Zygote = "e88e6eb3-aa80-5325-afca-941959d7151f" [[deps.CUDA]] @@ -1032,6 +1028,12 @@ git-tree-sha1 = "53bb909d1151e57e2484c3d1b53e19552b887fb2" uuid = "42e2da0e-8278-4e71-bc24-59509adca0fe" version = "1.0.2" +[[deps.HSL_jll]] +deps = ["Artifacts", "CompilerSupportLibraries_jll", "JLLWrappers", "Libdl"] +git-tree-sha1 = "4b34f6e368aa509b244847e6b0c9b370791bab09" +uuid = "017b0a0e-03f4-516a-9b91-836bbd1904dd" +version = "4.0.4+0" + [[deps.HTTP]] deps = ["Base64", "CodecZlib", "ConcurrentUtilities", "Dates", "ExceptionUnwrapping", "Logging", "LoggingExtras", "MbedTLS", "NetworkOptions", "OpenSSL", "PrecompileTools", "Random", "SimpleBufferStream", "Sockets", "URIs", "UUIDs"] git-tree-sha1 = "51059d23c8bb67911a2e6fd5130229113735fc7e" @@ -1049,6 +1051,12 @@ git-tree-sha1 = "2eaa69a7cab70a52b9687c8bf950a5a93ec895ae" uuid = "076d061b-32b6-4027-95e0-9a2c6f6d7e74" version = "0.2.0" +[[deps.HiGHS_jll]] +deps = ["Artifacts", "CompilerSupportLibraries_jll", "JLLWrappers", "Libdl", "Zlib_jll", "libblastrampoline_jll"] +git-tree-sha1 = "621d773f277b9eadac7e049eaa6418af65c7b9d7" +uuid = "8fd58aa0-07eb-5a78-9b36-339c94fd15ea" +version = "1.13.1+0" + [[deps.Hwloc_jll]] deps = ["Artifacts", "JLLWrappers", "Libdl", "XML2_jll", "Xorg_libpciaccess_jll"] git-tree-sha1 = "157e2e5838984449e44af851a52fe374d56b9ada" @@ -1090,20 +1098,6 @@ deps = ["Markdown"] uuid = "b77e0a4c-d291-57a0-90e8-8db25a27a240" version = "1.11.0" -[[deps.Interpolations]] -deps = ["Adapt", "AxisAlgorithms", "ChainRulesCore", "LinearAlgebra", "OffsetArrays", "Random", "Ratios", "SharedArrays", "SparseArrays", "StaticArrays", "WoodburyMatrices"] -git-tree-sha1 = "65d505fa4c0d7072990d659ef3fc086eb6da8208" -uuid = "a98d9a8b-a2ab-59e6-89dd-64a1c18fca59" -version = "0.16.2" - - [deps.Interpolations.extensions] - InterpolationsForwardDiffExt = "ForwardDiff" - InterpolationsUnitfulExt = "Unitful" - - [deps.Interpolations.weakdeps] - ForwardDiff = "f6369f11-7733-5829-9624-2563aa707210" - Unitful = "1986cc42-f94f-5a68-af5c-568840ba703d" - [[deps.InverseFunctions]] git-tree-sha1 = "a779299d77cd080bf77b97535acecd73e1c5e5cb" uuid = "3587e190-3f89-42d0-90ee-14403ec27112" @@ -1838,15 +1832,6 @@ weakdeps = ["ForwardDiff"] [deps.NonlinearSolveSpectralMethods.extensions] NonlinearSolveSpectralMethodsForwardDiffExt = "ForwardDiff" -[[deps.OffsetArrays]] -git-tree-sha1 = "117432e406b5c023f665fa73dc26e79ec3630151" -uuid = "6fe1bfb0-de20-5000-8ca7-80f57d26f881" -version = "1.17.0" -weakdeps = ["Adapt"] - - [deps.OffsetArrays.extensions] - OffsetArraysAdaptExt = "Adapt" - [[deps.Ogg_jll]] deps = ["Artifacts", "JLLWrappers", "Libdl"] git-tree-sha1 = "b6aa4566bb7ae78498a5e68943863fa8b5231b59" @@ -1923,9 +1908,9 @@ version = "1.22.0" [[deps.OrdinaryDiffEqCore]] deps = ["ADTypes", "Accessors", "Adapt", "ArrayInterface", "ConcreteStructs", "DataStructures", "DiffEqBase", "DocStringExtensions", "EnumX", "EnzymeCore", "FastBroadcast", "FastClosures", "FastPower", "FillArrays", "FunctionWrappersWrappers", "InteractiveUtils", "LinearAlgebra", "Logging", "MacroTools", "MuladdMacro", "Polyester", "PrecompileTools", "Preferences", "Random", "RecursiveArrayTools", "Reexport", "SciMLBase", "SciMLLogging", "SciMLOperators", "SciMLStructures", "Static", "StaticArrayInterface", "StaticArraysCore", "SymbolicIndexingInterface", "TruncatedStacktraces"] -git-tree-sha1 = "b0b386226690c23ebc771cd68a37d08c55b60924" +git-tree-sha1 = "b4a8d9b96931c2fc69126233bbe6d1a11b053d77" uuid = "bbf590c4-e513-4bbe-9b18-05decba2e5d8" -version = "3.21.0" +version = "3.22.0" [deps.OrdinaryDiffEqCore.extensions] OrdinaryDiffEqCoreMooncakeExt = "Mooncake" @@ -2287,16 +2272,6 @@ git-tree-sha1 = "c6ec94d2aaba1ab2ff983052cf6a606ca5985902" uuid = "e6cf234a-135c-5ec9-84dd-332b85af5143" version = "1.6.0" -[[deps.Ratios]] -deps = ["Requires"] -git-tree-sha1 = "1342a47bf3260ee108163042310d26f2be5ec90b" -uuid = "c84ed2f1-dad5-54f0-aa8e-dbefe2724439" -version = "0.4.5" -weakdeps = ["FixedPointNumbers"] - - [deps.Ratios.extensions] - RatiosFixedPointNumbersExt = "FixedPointNumbers" - [[deps.RecipesBase]] deps = ["PrecompileTools"] git-tree-sha1 = "5c3d09cc4f31f5fc6af001c250bf1278733100ff" @@ -2395,9 +2370,9 @@ version = "2025.9.18+0" [[deps.SciMLBase]] deps = ["ADTypes", "Accessors", "Adapt", "ArrayInterface", "CommonSolve", "ConstructionBase", "Distributed", "DocStringExtensions", "EnumX", "FunctionWrappersWrappers", "IteratorInterfaceExtensions", "LinearAlgebra", "Logging", "Markdown", "Moshi", "PreallocationTools", "PrecompileTools", "Preferences", "Printf", "RecipesBase", "RecursiveArrayTools", "Reexport", "RuntimeGeneratedFunctions", "SciMLLogging", "SciMLOperators", "SciMLPublic", "SciMLStructures", "StaticArraysCore", "Statistics", "SymbolicIndexingInterface"] -git-tree-sha1 = "8787e28326c99b0c9c706b51da525ad09d03c56f" +git-tree-sha1 = "0be0208add9b6836a701e0ac3ad30bda72fee51d" uuid = "0bca4576-84f4-4d90-8ffe-ffa030f20462" -version = "2.149.0" +version = "2.150.0" [deps.SciMLBase.extensions] SciMLBaseChainRulesCoreExt = "ChainRulesCore" @@ -2504,11 +2479,6 @@ git-tree-sha1 = "c5391c6ace3bc430ca630251d02ea9687169ca68" uuid = "efcf1570-3423-57d1-acb7-fd33fddbac46" version = "1.1.2" -[[deps.SharedArrays]] -deps = ["Distributed", "Mmap", "Random", "Serialization"] -uuid = "1a1011a3-84de-559e-8e89-a11a2f7dc383" -version = "1.11.0" - [[deps.Showoff]] deps = ["Dates", "Grisu"] git-tree-sha1 = "91eddf657aca81df9ae6ceb20b959ae5653ad1de" @@ -2583,9 +2553,9 @@ version = "1.2.1" [[deps.SparseMatrixColorings]] deps = ["ADTypes", "DocStringExtensions", "LinearAlgebra", "PrecompileTools", "Random", "SparseArrays"] -git-tree-sha1 = "7b2263c87aa890bf6d18ae05cedbe259754e3f34" +git-tree-sha1 = "fa43a02c01e3e3cb065c89bf9b648b89e3c06f18" uuid = "0a514795-09f3-496d-8182-132a7b665d35" -version = "0.4.24" +version = "0.4.25" [deps.SparseMatrixColorings.extensions] SparseMatrixColoringsCUDAExt = "CUDA" @@ -2627,12 +2597,15 @@ deps = ["ArrayInterface", "Compat", "IfElse", "LinearAlgebra", "PrecompileTools" git-tree-sha1 = "aa1ea41b3d45ac449d10477f65e2b40e3197a0d2" uuid = "0d7ed370-da01-4f52-bd93-41d350b8b718" version = "1.9.0" -weakdeps = ["OffsetArrays", "StaticArrays"] [deps.StaticArrayInterface.extensions] StaticArrayInterfaceOffsetArraysExt = "OffsetArrays" StaticArrayInterfaceStaticArraysExt = "StaticArrays" + [deps.StaticArrayInterface.weakdeps] + OffsetArrays = "6fe1bfb0-de20-5000-8ca7-80f57d26f881" + StaticArrays = "90137ffa-7385-5640-81b9-e52037218182" + [[deps.StaticArrays]] deps = ["LinearAlgebra", "PrecompileTools", "Random", "StaticArraysCore"] git-tree-sha1 = "246a8bb2e6667f832eea063c3a56aef96429a3db" @@ -2825,6 +2798,26 @@ git-tree-sha1 = "53915e50200959667e78a92a418594b428dffddf" uuid = "1cfade01-22cf-5700-b092-accc4b62d6e1" version = "0.4.1" +[[deps.UnoSolver]] +deps = ["LinearAlgebra", "OpenBLAS32_jll", "Uno_jll"] +git-tree-sha1 = "cf39af9c8be02fd5f5027bc3184e8582b8f6d2a0" +uuid = "1baa60ac-02f7-4b39-a7a8-2f4f58486b05" +version = "0.2.7" + + [deps.UnoSolver.extensions] + UnoSolverMathOptInterfaceExt = "MathOptInterface" + UnoSolverNLPModelsExt = "NLPModels" + + [deps.UnoSolver.weakdeps] + MathOptInterface = "b8f27783-ece8-5eb3-8dc8-9495eed66fee" + NLPModels = "a4795742-8479-5a88-8948-cc11e1c8c1a6" + +[[deps.Uno_jll]] +deps = ["ASL_jll", "Artifacts", "CompilerSupportLibraries_jll", "HSL_jll", "HiGHS_jll", "JLLWrappers", "LLVMOpenMP_jll", "Libdl", "METIS_jll", "MUMPS_seq_jll", "SPRAL_jll", "libblastrampoline_jll"] +git-tree-sha1 = "30b1deeaeb5de7c0e0e4a7f9a253195147ed0e9e" +uuid = "396d5378-14f1-5ab1-981d-48acd51740ed" +version = "2.5.0+0" + [[deps.UnsafeAtomics]] git-tree-sha1 = "b13c4edda90890e5b04ba24e20a310fbe6f249ff" uuid = "013be700-e6cd-48c3-b4a1-df204f14c38f" @@ -2851,12 +2844,6 @@ git-tree-sha1 = "96478df35bbc2f3e1e791bc7a3d0eeee559e60e9" uuid = "a2964d1f-97da-50d4-b82a-358c7fce9d89" version = "1.24.0+0" -[[deps.WoodburyMatrices]] -deps = ["LinearAlgebra", "SparseArrays"] -git-tree-sha1 = "248a7031b3da79a127f14e5dc5f417e26f9f6db7" -uuid = "efce3f68-66dc-5838-9240-27a6d6f5f9b6" -version = "1.1.0" - [[deps.XML2_jll]] deps = ["Artifacts", "JLLWrappers", "Libdl", "Libiconv_jll", "Zlib_jll"] git-tree-sha1 = "80d3930c6347cfce7ccf96bd3bafdf079d9c0390" diff --git a/docs/src/assets/Project.toml b/docs/src/assets/Project.toml index 66265d9c..255eb444 100644 --- a/docs/src/assets/Project.toml +++ b/docs/src/assets/Project.toml @@ -24,6 +24,7 @@ NLPModelsKnitro = "bec4dd0d-7755-52d5-9a02-22f0ffc7efcb" NonlinearSolve = "8913a72c-1f9b-4ce2-8d82-65094dcecaec" OrdinaryDiffEq = "1dea7af3-3e70-54e6-95c3-0bf5283fa5ed" Plots = "91a5bcdd-55d7-5caf-9e0b-520d859cae80" +UnoSolver = "1baa60ac-02f7-4b39-a7a8-2f4f58486b05" [compat] ADNLPModels = "0.8" @@ -51,4 +52,5 @@ NLPModelsKnitro = "0.10" NonlinearSolve = "4" OrdinaryDiffEq = "6" Plots = "1" +UnoSolver = "0.2" julia = "1.10" diff --git a/docs/src/manual-solve-advanced.md b/docs/src/manual-solve-advanced.md index 83f96731..eb14e03b 100644 --- a/docs/src/manual-solve-advanced.md +++ b/docs/src/manual-solve-advanced.md @@ -98,6 +98,7 @@ The `route_to` function accepts keyword arguments with **strategy names**: - `route_to(exa=value)` โ€” route to the Exa modeler - `route_to(ipopt=value)` โ€” route to the Ipopt solver - `route_to(madnlp=value)` โ€” route to the MadNLP solver +- `route_to(uno=value)` โ€” route to the Uno solver - `route_to(madncl=value)` โ€” route to the MadNCL solver - `route_to(knitro=value)` โ€” route to the Knitro solver diff --git a/docs/src/manual-solve-explicit.md b/docs/src/manual-solve-explicit.md index 04b9a7ba..1b2e8b5e 100644 --- a/docs/src/manual-solve-explicit.md +++ b/docs/src/manual-solve-explicit.md @@ -42,7 +42,18 @@ The mode is **automatically detected**: if any of `discretizer`, `modeler`, or ` ### Creating strategy instances -Each strategy is constructed with its options as keyword arguments: +Each strategy is constructed with its options as keyword arguments. +First, load the required solver packages: + +```julia +# Load solver packages (only what you need) +using NLPModelsIpopt # for Ipopt +using MadNLP # for MadNLP +using UnoSolver # for Uno +using MadNCL # for MadNCL (also requires MadNLP) +using NLPModelsKnitro # for Knitro (commercial license required) +# GPU solving also requires: using CUDA and using MadNLPGPU +``` ```@example explicit # Discretizer with custom grid and scheme @@ -53,6 +64,9 @@ mod = OptimalControl.ADNLP(backend=:optimized, show_time=false) # Solver with iteration limit and tolerance sol = OptimalControl.Ipopt(max_iter=500, tol=1e-6, print_level=0) + +# Uno solver with custom logging level +uno_sol = OptimalControl.Uno(max_iter=1000, logger="INFO") nothing # hide ``` diff --git a/docs/src/manual-solve-gpu.md b/docs/src/manual-solve-gpu.md index 9f90a6c8..44f8ff93 100644 --- a/docs/src/manual-solve-gpu.md +++ b/docs/src/manual-solve-gpu.md @@ -15,6 +15,10 @@ using CUDA nothing # hide ``` +!!! note "Solver requirements" + + For complete solver requirements including CPU solvers, see [Solver requirements](@ref manual-solve#solver-requirements) in the main solving manual. + !!! warning "CUDA required" GPU solving requires a CUDA-capable GPU and properly configured CUDA drivers. Check `CUDA.functional()` to verify your setup. diff --git a/docs/src/manual-solve.md b/docs/src/manual-solve.md index 8e43f5cb..de8d7110 100644 --- a/docs/src/manual-solve.md +++ b/docs/src/manual-solve.md @@ -102,6 +102,7 @@ Each method is a **quadruplet** `(discretizer, modeler, solver, parameter)`: 3. **Solver** โ€” which NLP solver to use: - `:ipopt`: [Ipopt](https://coin-or.github.io/Ipopt/) interior point solver - `:madnlp`: [MadNLP](https://madnlp.github.io/MadNLP.jl/) pure-Julia solver (GPU-capable) + - `:uno`: [Uno](https://unosolver.readthedocs.io) unified nonlinear optimization solver (CPU-only, ADNLP-only) - `:madncl`: [MadNCL](https://github.com/MadNLP/MadNCL.jl) (GPU-capable) - `:knitro`: [Knitro](https://www.artelys.com/solvers/knitro/) commercial solver (license required) @@ -173,6 +174,18 @@ solve(ocp, :collocation, :ipopt) # specify discretizer + solver solve(ocp, :collocation, :adnlp, :ipopt, :cpu) # complete description ``` +## Solver requirements + +Each solver requires its package to be loaded to provide the solver implementation: + +- **Ipopt**: `using NLPModelsIpopt` +- **MadNLP**: `using MadNLP` (CPU) or `using MadNLPGPU` (GPU) +- **Uno**: `using UnoSolver` +- **MadNCL**: `using MadNCL` and `using MadNLP` (requires both) +- **Knitro**: `using NLPModelsKnitro` (commercial license required) + +For GPU solving with MadNLP or MadNCL, you also need: `using CUDA` + ## Passing options to strategies You can pass options as keyword arguments. They are **automatically routed** to the appropriate strategy: @@ -236,6 +249,7 @@ describe(:exa) ### Solver options ```@example main +using NLPModelsIpopt describe(:ipopt) ``` @@ -244,6 +258,16 @@ using MadNLPGPU describe(:madnlp) ``` +```@example main +using MadNCL +describe(:madncl) +``` + +```@example main +using UnoSolver +describe(:uno) +``` + ### Official documentation For complete option lists, see the official documentation: @@ -252,6 +276,7 @@ For complete option lists, see the official documentation: - **Exa**: [ExaModels documentation](https://exanauts.github.io/ExaModels.jl/stable/) - **Ipopt**: [Ipopt options](https://coin-or.github.io/Ipopt/OPTIONS.html) - **MadNLP**: [MadNLP options](https://madnlp.github.io/MadNLP.jl/stable/options/) +- **Uno**: [Uno documentation](https://unosolver.readthedocs.io) - **MadNCL**: [MadNCL documentation](https://github.com/MadNLP/MadNCL.jl) - **Knitro**: [Knitro options](https://www.artelys.com/docs/knitro/3_referenceManual/userOptions.html) diff --git a/src/OptimalControl.jl b/src/OptimalControl.jl index b60a1e23..ccf0aa7b 100644 --- a/src/OptimalControl.jl +++ b/src/OptimalControl.jl @@ -12,7 +12,7 @@ the complete workflow from problem definition to solution. - **Flexible solve interface**: Descriptive (symbolic) or explicit (typed components) modes - **Multiple discretization methods**: Collocation and other schemes via CTDirect - **Multiple NLP modelers**: ADNLP, ExaModels with CPU/GPU support -- **Multiple solvers**: Ipopt, MadNLP, MadNCL, Knitro with CPU/GPU support +- **Multiple solvers**: Ipopt, MadNLP, Uno, MadNCL, Knitro with CPU/GPU support - **Automatic component completion**: Partial specifications are completed intelligently - **Option routing**: Strategy-specific options are routed to the appropriate components diff --git a/src/helpers/describe.jl b/src/helpers/describe.jl index 79f79543..b1af3718 100644 --- a/src/helpers/describe.jl +++ b/src/helpers/describe.jl @@ -29,6 +29,7 @@ For complete option lists, see the official documentation: - **MadNLP**: [MadNLP options](https://madnlp.github.io/MadNLP.jl/stable/options/) - **MadNCL**: [MadNCL documentation](https://github.com/MadNLP/MadNCL.jl) - **Knitro**: [Knitro options](https://www.artelys.com/docs/knitro/3_referenceManual/userOptions.html) +- **Uno**: [Uno documentation](https://unosolver.readthedocs.io) See also: [`methods`](@ref), [`get_strategy_registry`](@ref), [`solve`](@ref) """ diff --git a/src/helpers/methods.jl b/src/helpers/methods.jl index 30d16f81..dba7da6e 100644 --- a/src/helpers/methods.jl +++ b/src/helpers/methods.jl @@ -18,7 +18,7 @@ julia> m = methods() ((:collocation, :adnlp, :ipopt, :cpu), (:collocation, :adnlp, :madnlp, :cpu), ...) julia> length(m) -10 # 8 CPU methods + 2 GPU methods +11 # 9 CPU methods + 2 GPU methods julia> # CPU methods julia> methods()[1] @@ -32,7 +32,7 @@ julia> methods()[9] # Notes - Returns a precomputed constant tuple (allocation-free, type-stable) - All methods currently use `:collocation` discretization -- CPU methods (8 total): All combinations of `{adnlp, exa}` ร— `{ipopt, madnlp, madncl, knitro}` +- CPU methods (9 total): All combinations of `{adnlp, exa}` ร— `{ipopt, madnlp, uno, madncl, knitro}` - GPU methods (2 total): Only GPU-capable combinations `exa` ร— `{madnlp, madncl}` - GPU-capable strategies use parameterized types with automatic defaults - Used by `CTBase.Descriptions.complete` to complete partial method descriptions @@ -44,11 +44,12 @@ function Base.methods()::Tuple{Vararg{Tuple{Symbol,Symbol,Symbol,Symbol}}} # CPU methods (all existing methods now with :cpu parameter) (:collocation, :adnlp, :ipopt, :cpu), (:collocation, :adnlp, :madnlp, :cpu), + (:collocation, :adnlp, :uno, :cpu), + (:collocation, :adnlp, :madncl, :cpu), + (:collocation, :adnlp, :knitro, :cpu), (:collocation, :exa, :ipopt, :cpu), (:collocation, :exa, :madnlp, :cpu), - (:collocation, :adnlp, :madncl, :cpu), (:collocation, :exa, :madncl, :cpu), - (:collocation, :adnlp, :knitro, :cpu), (:collocation, :exa, :knitro, :cpu), # GPU methods (only combinations that make sense) diff --git a/src/helpers/print.jl b/src/helpers/print.jl index 0de2db04..e8f3d70c 100644 --- a/src/helpers/print.jl +++ b/src/helpers/print.jl @@ -172,6 +172,33 @@ function will_solver_print(solver::CTSolvers.MadNCL) return true end +""" +$(TYPEDSIGNATURES) + +Check if Uno will produce output based on `logger` option. + +Uno is silent when `logger = "SILENT"`, verbose otherwise. +Default is `"INFO"` which prints output. + +# Arguments +- `solver::CTSolvers.Uno`: The Uno solver instance to check + +# Returns +- `Bool`: `true` if Uno will print output, `false` otherwise + +# Notes +- When `logger` is not specified, Uno defaults to verbose output (`"INFO"`) +- Only `"SILENT"` suppresses output, other levels print +- This method allows the display system to conditionally show the `โ–ซ` symbol + +See also: [`will_solver_print(::CTSolvers.AbstractNLPSolver)`](@ref) +""" +function will_solver_print(solver::CTSolvers.Uno) + opts = CTSolvers.options(solver) + logger = get(opts.options, :logger, nothing) + return logger === nothing || logger != "SILENT" +end + # ============================================================================ # Parameter extraction helpers # ============================================================================ diff --git a/src/helpers/registry.jl b/src/helpers/registry.jl index 22f1c622..c57ebb9c 100644 --- a/src/helpers/registry.jl +++ b/src/helpers/registry.jl @@ -25,7 +25,7 @@ julia> CTSolvers.strategy_ids(CTSolvers.AbstractNLPModeler, registry) (:adnlp, :exa) julia> CTSolvers.strategy_ids(CTSolvers.AbstractNLPSolver, registry) -(:ipopt, :madnlp, :madncl, :knitro) +(:ipopt, :madnlp, :uno, :madncl, :knitro) julia> # Check which parameters a strategy supports julia> CTSolvers.available_parameters(:modeler, CTSolvers.Exa, registry) @@ -38,7 +38,7 @@ julia> CTSolvers.available_parameters(:solver, CTSolvers.Ipopt, registry) # Notes - Returns a precomputed registry (allocation-free, type-stable) - GPU-capable strategies (Exa, MadNLP, MadNCL) support both CPU and GPU parameters -- CPU-only strategies (ADNLP, Ipopt, Knitro) support only CPU parameter +- CPU-only strategies (ADNLP, Ipopt, Uno, Knitro) support only CPU parameter - Parameterization is handled at the method level in `methods()` - GPU strategies automatically get appropriate default configurations when parameterized - Used by solve functions for component completion and strategy building @@ -58,6 +58,7 @@ function get_strategy_registry()::CTSolvers.StrategyRegistry CTSolvers.AbstractNLPSolver => ( (CTSolvers.Ipopt, [CTSolvers.CPU]), (CTSolvers.MadNLP, [CTSolvers.CPU, CTSolvers.GPU]), + (CTSolvers.Uno, [CTSolvers.CPU]), (CTSolvers.MadNCL, [CTSolvers.CPU, CTSolvers.GPU]), (CTSolvers.Knitro, [CTSolvers.CPU]), ), diff --git a/src/imports/ctsolvers.jl b/src/imports/ctsolvers.jl index 0cb7468c..8b7272c9 100644 --- a/src/imports/ctsolvers.jl +++ b/src/imports/ctsolvers.jl @@ -12,7 +12,7 @@ import CTSolvers: DiscretizedModel import CTSolvers: AbstractNLPModeler, ADNLP, Exa # Solvers -import CTSolvers: AbstractNLPSolver, Ipopt, MadNLP, MadNCL, Knitro +import CTSolvers: AbstractNLPSolver, Ipopt, MadNLP, MadNCL, Knitro, Uno # Strategies import CTSolvers: diff --git a/test/suite/helpers/test_methods.jl b/test/suite/helpers/test_methods.jl index ac06712d..2c6c9e24 100644 --- a/test/suite/helpers/test_methods.jl +++ b/test/suite/helpers/test_methods.jl @@ -32,6 +32,7 @@ function test_methods() # CPU methods (all existing methods now with :cpu parameter) Test.@test (:collocation, :adnlp, :ipopt, :cpu) in methods Test.@test (:collocation, :adnlp, :madnlp, :cpu) in methods + Test.@test (:collocation, :adnlp, :uno, :cpu) in methods Test.@test (:collocation, :adnlp, :madncl, :cpu) in methods Test.@test (:collocation, :adnlp, :knitro, :cpu) in methods Test.@test (:collocation, :exa, :ipopt, :cpu) in methods @@ -43,8 +44,8 @@ function test_methods() Test.@test (:collocation, :exa, :madnlp, :gpu) in methods Test.@test (:collocation, :exa, :madncl, :gpu) in methods - # Total count: 8 CPU methods + 2 GPU methods = 10 methods - Test.@test length(methods) == 10 + # Total count: 9 CPU methods + 2 GPU methods = 11 methods + Test.@test length(methods) == 11 end Test.@testset "Parameter Distribution" begin @@ -54,7 +55,7 @@ function test_methods() cpu_methods = filter(m -> m[4] == :cpu, methods) gpu_methods = filter(m -> m[4] == :gpu, methods) - Test.@test length(cpu_methods) == 8 # All original methods now with :cpu + Test.@test length(cpu_methods) == 9 # All original methods now with :cpu + Uno Test.@test length(gpu_methods) == 2 # Only GPU-capable combinations end @@ -152,7 +153,7 @@ function test_methods() # Should have all expected solvers solvers = Set(m[3] for m in methods) - expected_solvers = Set([:ipopt, :madnlp, :madncl, :knitro]) + expected_solvers = Set([:ipopt, :madnlp, :uno, :madncl, :knitro]) Test.@test issubset(expected_solvers, solvers) # GPU methods should only use GPU-capable solvers @@ -174,7 +175,7 @@ function test_methods() gpu_methods = filter(m -> m[4] == :gpu, methods) # CPU methods should include all combinations except GPU-only - Test.@test length(cpu_methods) == 8 + Test.@test length(cpu_methods) == 9 Test.@test length(gpu_methods) == 2 # Total should match expected diff --git a/test/suite/helpers/test_registry.jl b/test/suite/helpers/test_registry.jl index 6df55afb..c42d073a 100644 --- a/test/suite/helpers/test_registry.jl +++ b/test/suite/helpers/test_registry.jl @@ -48,9 +48,10 @@ function test_registry() ids = CTSolvers.strategy_ids(CTSolvers.AbstractNLPSolver, registry) Test.@test :ipopt in ids Test.@test :madnlp in ids + Test.@test :uno in ids Test.@test :madncl in ids Test.@test :knitro in ids - Test.@test length(ids) == 4 + Test.@test length(ids) == 5 end Test.@testset "Parameter Support - Modelers" begin @@ -102,11 +103,17 @@ function test_registry() knitro_filtered = CTSolvers.Strategies.available_parameters( :knitro, CTSolvers.AbstractNLPSolver, registry ) + uno_filtered = CTSolvers.Strategies.available_parameters( + :uno, CTSolvers.AbstractNLPSolver, registry + ) # CPU-only solvers Test.@test CTSolvers.CPU in ipopt_filtered Test.@test CTSolvers.GPU โˆ‰ ipopt_filtered + Test.@test CTSolvers.CPU in uno_filtered + Test.@test CTSolvers.GPU โˆ‰ uno_filtered + Test.@test CTSolvers.CPU in knitro_filtered Test.@test CTSolvers.GPU โˆ‰ knitro_filtered @@ -120,6 +127,7 @@ function test_registry() # Test parameter type extraction Test.@test CTSolvers.Strategies.get_parameter_type(CTSolvers.Ipopt) === nothing Test.@test CTSolvers.Strategies.get_parameter_type(CTSolvers.MadNLP) === nothing + Test.@test CTSolvers.Strategies.get_parameter_type(CTSolvers.Uno) === nothing Test.@test CTSolvers.Strategies.get_parameter_type(CTSolvers.MadNCL) === nothing Test.@test CTSolvers.Strategies.get_parameter_type(CTSolvers.Knitro) === nothing end @@ -190,6 +198,7 @@ function test_registry() solver_ids = CTSolvers.strategy_ids(CTSolvers.AbstractNLPSolver, registry) Test.@test :ipopt in solver_ids # CPU-only Test.@test :madnlp in solver_ids # CPU+GPU + Test.@test :uno in solver_ids # CPU-only Test.@test :madncl in solver_ids # CPU+GPU Test.@test :knitro in solver_ids # CPU-only end @@ -357,6 +366,7 @@ function test_registry() Test.@test :exa in modeler_ids Test.@test :ipopt in solver_ids Test.@test :madnlp in solver_ids + Test.@test :uno in solver_ids Test.@test :madncl in solver_ids Test.@test :knitro in solver_ids end diff --git a/test/suite/reexport/test_ctsolvers.jl b/test/suite/reexport/test_ctsolvers.jl index c4acae34..308f957a 100644 --- a/test/suite/reexport/test_ctsolvers.jl +++ b/test/suite/reexport/test_ctsolvers.jl @@ -60,6 +60,7 @@ function test_ctsolvers() OptimalControl.AbstractNLPSolver, OptimalControl.Ipopt, OptimalControl.MadNLP, + OptimalControl.Uno, OptimalControl.MadNCL, OptimalControl.Knitro, ) @@ -165,6 +166,7 @@ function test_ctsolvers() Test.@testset "Solvers" begin Test.@test OptimalControl.Ipopt <: OptimalControl.AbstractNLPSolver Test.@test OptimalControl.MadNLP <: OptimalControl.AbstractNLPSolver + Test.@test OptimalControl.Uno <: OptimalControl.AbstractNLPSolver Test.@test OptimalControl.MadNCL <: OptimalControl.AbstractNLPSolver Test.@test OptimalControl.Knitro <: OptimalControl.AbstractNLPSolver end diff --git a/test/suite/solve/test_canonical.jl b/test/suite/solve/test_canonical.jl index b3ca59f4..d626336f 100644 --- a/test/suite/solve/test_canonical.jl +++ b/test/suite/solve/test_canonical.jl @@ -20,6 +20,7 @@ using NLPModelsIpopt: NLPModelsIpopt using MadNLP: MadNLP using MadNLPGPU: MadNLPGPU using MadNCL: MadNCL +using UnoSolver: UnoSolver using CUDA: CUDA # Include shared test problems via TestProblems module @@ -62,12 +63,17 @@ function test_canonical() ), ] - modelers = [("ADNLP", OptimalControl.ADNLP()), ("Exa", OptimalControl.Exa())] - - solvers = [ - ("Ipopt", OptimalControl.Ipopt(print_level=0)), - ("MadNLP", OptimalControl.MadNLP(print_level=MadNLP.ERROR)), - ("MadNCL", OptimalControl.MadNCL(print_level=MadNLP.ERROR)), + # Define modeler-solver pairs (Uno only works with ADNLP) + modeler_solver_pairs = [ + # ADNLP modeler with all solvers + ("ADNLP", "Ipopt", OptimalControl.ADNLP(), OptimalControl.Ipopt(print_level=0)), + ("ADNLP", "MadNLP", OptimalControl.ADNLP(), OptimalControl.MadNLP(print_level=MadNLP.ERROR)), + ("ADNLP", "Uno", OptimalControl.ADNLP(), OptimalControl.Uno(logger="SILENT")), + ("ADNLP", "MadNCL", OptimalControl.ADNLP(), OptimalControl.MadNCL(print_level=MadNLP.ERROR)), + # Exa modeler with all solvers except Uno + ("Exa", "Ipopt", OptimalControl.Exa(), OptimalControl.Ipopt(print_level=0)), + ("Exa", "MadNLP", OptimalControl.Exa(), OptimalControl.MadNLP(print_level=MadNLP.ERROR)), + ("Exa", "MadNCL", OptimalControl.Exa(), OptimalControl.MadNCL(print_level=MadNLP.ERROR)), ] problems = [("Beam", Beam()), ("Goddard", Goddard())] @@ -78,67 +84,65 @@ function test_canonical() for (pname, pb) in problems Test.@testset "$pname" begin for (dname, disc) in discretizers - for (mname, mod) in modelers - for (sname, sol) in solvers - # Extract short names for display - d_short = String(split(dname, "/")[2]) # Get "midpoint" or "trapeze" - - # Normalize initial guess before calling canonical solve (Layer 3) - normalized_init = OptimalControl.build_initial_guess( - pb.ocp, pb.init + for (mname, sname, mod, sol) in modeler_solver_pairs + # Extract short names for display + d_short = String(split(dname, "/")[2]) # Get "midpoint" or "trapeze" + + # Normalize initial guess before calling canonical solve (Layer 3) + normalized_init = OptimalControl.build_initial_guess( + pb.ocp, pb.init + ) + + # Execute with timing (DRY - single measurement) + timed_result = @timed begin + OptimalControl.solve( + pb.ocp, normalized_init, disc, mod, sol; display=false ) + end - # Execute with timing (DRY - single measurement) - timed_result = @timed begin - OptimalControl.solve( - pb.ocp, normalized_init, disc, mod, sol; display=false - ) - end + # Extract results + solve_result = timed_result.value + solve_time = timed_result.time + memory_bytes = timed_result.bytes - # Extract results - solve_result = timed_result.value - solve_time = timed_result.time - memory_bytes = timed_result.bytes - - success = OptimalControl.successful(solve_result) - obj = success ? OptimalControl.objective(solve_result) : 0.0 - - # Extract iterations using CTModels function - iters = OptimalControl.iterations(solve_result) - - # Display table line (SRP - responsibility delegated) - if VERBOSE - print_test_line( - "CPU", - pname, - d_short, - mname, - sname, - success, - solve_time, - obj, - pb.obj, - iters, - memory_bytes > 0 ? memory_bytes : nothing, - false, # show_memory = false - ) - end + success = OptimalControl.successful(solve_result) + obj = success ? OptimalControl.objective(solve_result) : 0.0 - # Update statistics - total_tests += 1 - if success - passed_tests += 1 - end + # Extract iterations using CTModels function + iters = OptimalControl.iterations(solve_result) - # Run the actual test assertions - Test.@testset "$dname / $mname / $sname" begin - Test.@test success - if success - Test.@test solve_result isa - OptimalControl.AbstractSolution - Test.@test OptimalControl.objective(solve_result) โ‰ˆ - pb.obj rtol = OBJ_RTOL - end + # Display table line (SRP - responsibility delegated) + if VERBOSE + print_test_line( + "CPU", + pname, + d_short, + mname, + sname, + success, + solve_time, + obj, + pb.obj, + iters, + memory_bytes > 0 ? memory_bytes : nothing, + false, # show_memory = false + ) + end + + # Update statistics + total_tests += 1 + if success + passed_tests += 1 + end + + # Run the actual test assertions + Test.@testset "$dname / $mname / $sname" begin + Test.@test success + if success + Test.@test solve_result isa + OptimalControl.AbstractSolution + Test.@test OptimalControl.objective(solve_result) โ‰ˆ + pb.obj rtol = OBJ_RTOL end end end diff --git a/test/suite/solve/test_descriptive.jl b/test/suite/solve/test_descriptive.jl index 815b2cdd..973b6e80 100644 --- a/test/suite/solve/test_descriptive.jl +++ b/test/suite/solve/test_descriptive.jl @@ -20,6 +20,7 @@ using CommonSolve: CommonSolve using NLPModelsIpopt: NLPModelsIpopt using MadNLP: MadNLP using MadNCL: MadNCL +using UnoSolver: UnoSolver using CUDA: CUDA # Include shared test problems via TestProblems module @@ -123,6 +124,21 @@ function test_descriptive() Test.@test result isa CTModels.AbstractSolution Test.@test OptimalControl.successful(result) end + + Test.@testset "Complete description - Goddard with Uno" begin + result = OptimalControl.solve_descriptive( + ocp, + :collocation, + :adnlp, + :uno; + initial_guess=init, + display=false, + registry=registry, + ) + Test.@test result isa CTModels.AbstractSolution + Test.@test OptimalControl.successful(result) + Test.@test OptimalControl.objective(result) โ‰ˆ TestProblems.Goddard().obj rtol=1e-2 + end end # ==================================================================== From 67b9d4a6cc88726240959c6d58297f80e3dd938c Mon Sep 17 00:00:00 2001 From: Olivier Cots Date: Tue, 17 Mar 2026 12:17:24 +0100 Subject: [PATCH 2/7] foo --- docs/src/manual-solve-explicit.md | 3 --- 1 file changed, 3 deletions(-) diff --git a/docs/src/manual-solve-explicit.md b/docs/src/manual-solve-explicit.md index 1b2e8b5e..7faa5d1f 100644 --- a/docs/src/manual-solve-explicit.md +++ b/docs/src/manual-solve-explicit.md @@ -64,9 +64,6 @@ mod = OptimalControl.ADNLP(backend=:optimized, show_time=false) # Solver with iteration limit and tolerance sol = OptimalControl.Ipopt(max_iter=500, tol=1e-6, print_level=0) - -# Uno solver with custom logging level -uno_sol = OptimalControl.Uno(max_iter=1000, logger="INFO") nothing # hide ``` From c5ab961c54d2d74d1da2b67e7866c5cd995a32c3 Mon Sep 17 00:00:00 2001 From: Olivier Cots Date: Tue, 17 Mar 2026 13:51:47 +0100 Subject: [PATCH 3/7] Update Uno to support ExaModels modeler - Add (:collocation, :exa, :uno, :cpu) method to available methods - Update tests to include Uno with Exa modeler (8 combinations total) - Update method counts: 10 CPU methods + 2 GPU methods = 12 total - Update CHANGELOG to reflect Uno works with both ADNLP and Exa - Remove ADNLP-only restriction from documentation --- CHANGELOG.md | 8 ++++---- docs/src/manual-solve.md | 4 ++-- src/helpers/methods.jl | 1 + test/suite/helpers/test_methods.jl | 11 ++++++----- test/suite/solve/test_canonical.jl | 5 +++-- 5 files changed, 16 insertions(+), 13 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 8f7019cf..ae882cc5 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -13,8 +13,8 @@ Versions follow [Semantic Versioning](https://semver.org/spec/v2.0.0.html). - **Uno solver integration**: Full support for the Uno nonlinear optimization solver - Added to solver registry with CPU-only support - - Added method `(:collocation, :adnlp, :uno, :cpu)` to available methods - - Uno only compatible with ADNLP modeler (not ExaModels) + - Added methods `(:collocation, :adnlp, :uno, :cpu)` and `(:collocation, :exa, :uno, :cpu)` to available methods + - Uno compatible with both ADNLP and Exa modelers - Comprehensive test coverage with Beam and Goddard problems - Extension error handling when `UnoSolver` package not loaded @@ -34,8 +34,8 @@ Versions follow [Semantic Versioning](https://semver.org/spec/v2.0.0.html). ### Changed - **Solver count**: Updated from 4 to 5 available solvers (Ipopt, MadNLP, Uno, MadNCL, Knitro) -- **Method count**: Updated from 10 to 11 available methods (9 CPU + 2 GPU) -- **Test structure**: Restructured canonical tests to use modeler-solver pairs, respecting Uno/ADNLP-only constraint +- **Method count**: Updated from 10 to 12 available methods (10 CPU + 2 GPU) +- **Test structure**: Restructured canonical tests to use modeler-solver pairs, Uno now works with both ADNLP and Exa - **Documentation**: Updated solver lists and examples throughout documentation to include Uno --- diff --git a/docs/src/manual-solve.md b/docs/src/manual-solve.md index de8d7110..75833f26 100644 --- a/docs/src/manual-solve.md +++ b/docs/src/manual-solve.md @@ -100,9 +100,9 @@ Each method is a **quadruplet** `(discretizer, modeler, solver, parameter)`: - `:exa`: uses [`ExaModels.ExaModel`](@extref) with SIMD optimization (GPU-capable) 3. **Solver** โ€” which NLP solver to use: - - `:ipopt`: [Ipopt](https://coin-or.github.io/Ipopt/) interior point solver + - `:ipopt`: [Ipopt](https://coin-or.github.io/Ipopt/) interior point solver (CPU-only) - `:madnlp`: [MadNLP](https://madnlp.github.io/MadNLP.jl/) pure-Julia solver (GPU-capable) - - `:uno`: [Uno](https://unosolver.readthedocs.io) unified nonlinear optimization solver (CPU-only, ADNLP-only) + - `:uno`: [Uno](https://unosolver.readthedocs.io) unified nonlinear optimization solver (CPU-only) - `:madncl`: [MadNCL](https://github.com/MadNLP/MadNCL.jl) (GPU-capable) - `:knitro`: [Knitro](https://www.artelys.com/solvers/knitro/) commercial solver (license required) diff --git a/src/helpers/methods.jl b/src/helpers/methods.jl index dba7da6e..ea2a50d5 100644 --- a/src/helpers/methods.jl +++ b/src/helpers/methods.jl @@ -49,6 +49,7 @@ function Base.methods()::Tuple{Vararg{Tuple{Symbol,Symbol,Symbol,Symbol}}} (:collocation, :adnlp, :knitro, :cpu), (:collocation, :exa, :ipopt, :cpu), (:collocation, :exa, :madnlp, :cpu), + (:collocation, :exa, :uno, :cpu), (:collocation, :exa, :madncl, :cpu), (:collocation, :exa, :knitro, :cpu), diff --git a/test/suite/helpers/test_methods.jl b/test/suite/helpers/test_methods.jl index 2c6c9e24..1b20bbd8 100644 --- a/test/suite/helpers/test_methods.jl +++ b/test/suite/helpers/test_methods.jl @@ -37,6 +37,7 @@ function test_methods() Test.@test (:collocation, :adnlp, :knitro, :cpu) in methods Test.@test (:collocation, :exa, :ipopt, :cpu) in methods Test.@test (:collocation, :exa, :madnlp, :cpu) in methods + Test.@test (:collocation, :exa, :uno, :cpu) in methods Test.@test (:collocation, :exa, :madncl, :cpu) in methods Test.@test (:collocation, :exa, :knitro, :cpu) in methods @@ -44,8 +45,8 @@ function test_methods() Test.@test (:collocation, :exa, :madnlp, :gpu) in methods Test.@test (:collocation, :exa, :madncl, :gpu) in methods - # Total count: 9 CPU methods + 2 GPU methods = 11 methods - Test.@test length(methods) == 11 + # Total count: 10 CPU methods + 2 GPU methods = 12 methods + Test.@test length(methods) == 12 end Test.@testset "Parameter Distribution" begin @@ -55,7 +56,7 @@ function test_methods() cpu_methods = filter(m -> m[4] == :cpu, methods) gpu_methods = filter(m -> m[4] == :gpu, methods) - Test.@test length(cpu_methods) == 9 # All original methods now with :cpu + Uno + Test.@test length(cpu_methods) == 10 # All original methods now with :cpu + Uno Test.@test length(gpu_methods) == 2 # Only GPU-capable combinations end @@ -175,8 +176,8 @@ function test_methods() gpu_methods = filter(m -> m[4] == :gpu, methods) # CPU methods should include all combinations except GPU-only - Test.@test length(cpu_methods) == 9 - Test.@test length(gpu_methods) == 2 + Test.@test length(cpu_methods) == 10 # All original methods now with :cpu + Uno + Test.@test length(gpu_methods) == 2 # Only GPU-capable combinations # Total should match expected Test.@test length(methods) == length(cpu_methods) + length(gpu_methods) diff --git a/test/suite/solve/test_canonical.jl b/test/suite/solve/test_canonical.jl index d626336f..1e2b2eac 100644 --- a/test/suite/solve/test_canonical.jl +++ b/test/suite/solve/test_canonical.jl @@ -63,16 +63,17 @@ function test_canonical() ), ] - # Define modeler-solver pairs (Uno only works with ADNLP) + # Define modeler-solver pairs (Uno works with both ADNLP and Exa) modeler_solver_pairs = [ # ADNLP modeler with all solvers ("ADNLP", "Ipopt", OptimalControl.ADNLP(), OptimalControl.Ipopt(print_level=0)), ("ADNLP", "MadNLP", OptimalControl.ADNLP(), OptimalControl.MadNLP(print_level=MadNLP.ERROR)), ("ADNLP", "Uno", OptimalControl.ADNLP(), OptimalControl.Uno(logger="SILENT")), ("ADNLP", "MadNCL", OptimalControl.ADNLP(), OptimalControl.MadNCL(print_level=MadNLP.ERROR)), - # Exa modeler with all solvers except Uno + # Exa modeler with all solvers ("Exa", "Ipopt", OptimalControl.Exa(), OptimalControl.Ipopt(print_level=0)), ("Exa", "MadNLP", OptimalControl.Exa(), OptimalControl.MadNLP(print_level=MadNLP.ERROR)), + ("Exa", "Uno", OptimalControl.Exa(), OptimalControl.Uno(logger="SILENT")), ("Exa", "MadNCL", OptimalControl.Exa(), OptimalControl.MadNCL(print_level=MadNLP.ERROR)), ] From 3c3f7245e0fed90d20525292e95e693c234592c9 Mon Sep 17 00:00:00 2001 From: Olivier Cots Date: Tue, 17 Mar 2026 13:54:12 +0100 Subject: [PATCH 4/7] Add push_preview=true to documentation deployment - Enable preview pushes for documentation PRs - Allows automatic preview builds when changes are pushed --- docs/make.jl | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/make.jl b/docs/make.jl index 4c719d01..5609daab 100644 --- a/docs/make.jl +++ b/docs/make.jl @@ -233,4 +233,4 @@ with_api_reference(src_dir, ext_dir) do api_pages end # โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ• -deploydocs(; repo=repo_url * ".git", devbranch="main") +deploydocs(; repo=repo_url * ".git", devbranch="main", push_preview=true) From 4176281a4709af90454ee8595ce4dc3c5036ed7c Mon Sep 17 00:00:00 2001 From: Olivier Cots Date: Tue, 17 Mar 2026 13:56:12 +0100 Subject: [PATCH 5/7] Bump version to 1.3.2-beta - Update version after Uno integration and ExaModels support - All tests passing: 1309/1309 (100% success rate) - Uno now works with both ADNLP and Exa modelers - Ready for release --- Project.toml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Project.toml b/Project.toml index b7ce44a2..5a25e66b 100644 --- a/Project.toml +++ b/Project.toml @@ -1,6 +1,6 @@ name = "OptimalControl" uuid = "5f98b655-cc9a-415a-b60e-744165666948" -version = "1.3.1-beta" +version = "1.3.2-beta" authors = ["Olivier Cots "] [deps] From 29840742605c65b060756864184c8d1afec6fd68 Mon Sep 17 00:00:00 2001 From: Olivier Cots Date: Tue, 17 Mar 2026 14:45:08 +0100 Subject: [PATCH 6/7] Fix Documenter color display by replacing printstyled with ANSI escape sequences - Add ANSI color helper functions for Documenter compatibility - Replace printstyled calls with _print_ansi_styled in display functions - Support Union{String,Symbol} text input for flexibility - Enable proper color conversion to CSS classes in HTML documentation This fixes the issue where OptimalControl colors were not showing in remote documentation while MadNLP colors (using raw ANSI) were working correctly. --- src/helpers/print.jl | 80 ++++++++++++++++++++++++++++++++++++++++---- 1 file changed, 73 insertions(+), 7 deletions(-) diff --git a/src/helpers/print.jl b/src/helpers/print.jl index e8f3d70c..3adf96e6 100644 --- a/src/helpers/print.jl +++ b/src/helpers/print.jl @@ -1,5 +1,71 @@ # Display helpers for OptimalControl +# ============================================================================ +# ANSI Color helpers for Documenter compatibility +# ============================================================================ + +""" + _ansi_color(color::Symbol, bold::Bool=false) + +Generate ANSI escape sequence for the specified color and formatting. + +# Arguments +- `color::Symbol`: Color name (:cyan, :magenta, etc.) +- `bold::Bool`: Whether to add bold formatting + +# Returns +- `String`: ANSI escape sequence + +# Notes +- Used instead of `printstyled` for Documenter compatibility +- Documenter converts ANSI sequences to CSS classes in HTML output +""" +function _ansi_color(color::Symbol, bold::Bool=false) + color_codes = Dict( + :black => 30, + :red => 31, + :green => 32, + :yellow => 33, + :blue => 34, + :magenta => 35, + :cyan => 36, + :white => 37, + :default => 39 + ) + + code = get(color_codes, color, 39) + if bold + return "\033[1;$(code)m" + else + return "\033[$(code)m" + end +end + +""" + _ansi_reset() + +Generate ANSI reset sequence to clear formatting. + +# Returns +- `String`: ANSI reset sequence +""" +_ansi_reset() = "\033[0m" + +""" + _print_ansi_styled(io, text::Union{String,Symbol}, color::Symbol, bold::Bool=false) + +Print text with ANSI color formatting for Documenter compatibility. + +# Arguments +- `io`: IO stream +- `text::Union{String,Symbol}`: Text to print +- `color::Symbol`: Color name +- `bold::Bool`: Whether to use bold formatting +""" +function _print_ansi_styled(io, text::Union{String,Symbol}, color::Symbol, bold::Bool=false) + print(io, _ansi_color(color, bold), text, _ansi_reset()) +end + # ============================================================================ # Solver output detection # ============================================================================ @@ -292,7 +358,7 @@ parameter displayed inline when appropriate. # Arguments - `io::IO`: Output stream for printing -- `component_id::String`: The component identifier to print +- `component_id::Symbol`: The component identifier to print - `show_inline::Bool`: Whether to show the parameter inline - `param_sym::Union{Symbol, Nothing}`: Parameter symbol to display (can be `nothing`) @@ -304,10 +370,10 @@ parameter displayed inline when appropriate. See also: [`display_ocp_configuration`](@ref) """ function _print_component_with_param(io, component_id, show_inline, param_sym) - printstyled(io, component_id; color=:cyan, bold=true) + _print_ansi_styled(io, component_id, :cyan, true) if show_inline && param_sym !== nothing print(io, " (") - printstyled(io, string(param_sym); color=:magenta, bold=true) + _print_ansi_styled(io, param_sym, :magenta, true) print(io, ")") end end @@ -436,7 +502,7 @@ function display_ocp_configuration( display_strategy = _determine_parameter_display_strategy(param_info.params) # Header with method - print(io, "โ–ซ OptimalControl v", version_str, " solving with: ") + print(io, "โ–ซ This is OptimalControl ", version_str, ", solving with: ") discretizer_id = OptimalControl.id(typeof(discretizer)) modeler_id = OptimalControl.id(typeof(modeler)) @@ -455,7 +521,7 @@ function display_ocp_configuration( # Add common parameter at end if applicable if !display_strategy.show_inline && display_strategy.common !== nothing print(io, " (") - printstyled(io, string(display_strategy.common); color=:magenta, bold=true) + _print_ansi_styled(io, string(display_strategy.common), :magenta, true) print(io, ")") end @@ -475,9 +541,9 @@ function display_ocp_configuration( function print_component(line_prefix, label, pkg, opts) print(io, line_prefix) - printstyled(io, label; bold=true) + _print_ansi_styled(io, label, :default, true) print(io, ": ") - printstyled(io, pkg; color=:cyan, bold=true) + _print_ansi_styled(io, pkg, :cyan, true) if show_options && opts !== nothing # Collect both user and computed options all_items = Tuple{Symbol,Any,Symbol}[] # (key, opt, source) From f19c26b0bceb774879ff2d78406c878143ea7748 Mon Sep 17 00:00:00 2001 From: Olivier Cots Date: Tue, 17 Mar 2026 15:38:15 +0100 Subject: [PATCH 7/7] Fix print tests for ANSI color compatibility MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Update tests to match new header format 'โ–ซ This is OptimalControl' - Make formatting tests robust to ANSI escape sequences - Adjust performance thresholds for ANSI overhead (25000/120000) - All 87 tests now pass with new color system Tests now properly validate the ANSI color implementation that enables colors in Documenter HTML output while maintaining functionality. --- test/suite/helpers/test_print.jl | 49 +++++++++++++++++++++----------- 1 file changed, 33 insertions(+), 16 deletions(-) diff --git a/test/suite/helpers/test_print.jl b/test/suite/helpers/test_print.jl index 5b775753..06716581 100644 --- a/test/suite/helpers/test_print.jl +++ b/test/suite/helpers/test_print.jl @@ -138,8 +138,11 @@ function test_print() io = IOBuffer() OptimalControl._print_component_with_param(io, :exa, true, :gpu) out = String(take!(io)) + # Test with ANSI sequences - check for content regardless of formatting Test.@test occursin("exa", out) - Test.@test occursin("(gpu)", out) + Test.@test occursin("gpu", out) + Test.@test occursin("(", out) + Test.@test occursin(")", out) end Test.@testset "_print_component_with_param - param but not inline" begin @@ -167,9 +170,13 @@ function test_print() ) out = String(take!(io)) - Test.@test occursin("Discretizer: collocation", out) - Test.@test occursin("Modeler: adnlp", out) - Test.@test occursin("Solver: ipopt", out) + # Check content regardless of ANSI formatting + Test.@test occursin("Discretizer", out) + Test.@test occursin("collocation", out) + Test.@test occursin("Modeler", out) + Test.@test occursin("adnlp", out) + Test.@test occursin("Solver", out) + Test.@test occursin("ipopt", out) Test.@test !occursin("[user]", out) # compact mode without sources end @@ -200,9 +207,13 @@ function test_print() out = String(take!(io)) # Just ensure it runs and still prints the ids - Test.@test occursin("Discretizer: collocation", out) - Test.@test occursin("Modeler: adnlp", out) - Test.@test occursin("Solver: ipopt", out) + # Check content regardless of ANSI formatting + Test.@test occursin("Discretizer", out) + Test.@test occursin("collocation", out) + Test.@test occursin("Modeler", out) + Test.@test occursin("adnlp", out) + Test.@test occursin("Solver", out) + Test.@test occursin("ipopt", out) end # ==================================================================== @@ -283,10 +294,13 @@ function test_print() OptimalControl.display_ocp_configuration(io, disc, mod, sol) out = String(take!(io)) - # Check header structure - Test.@test occursin("โ–ซ OptimalControl v", out) + # Check header structure - verify components exist regardless of ANSI formatting + Test.@test occursin("โ–ซ This is OptimalControl", out) Test.@test occursin("solving with:", out) - Test.@test occursin("collocation โ†’ adnlp โ†’ ipopt", out) + Test.@test occursin("collocation", out) + Test.@test occursin("adnlp", out) + Test.@test occursin("ipopt", out) + Test.@test occursin("โ†’", out) end Test.@testset "Configuration section" begin @@ -299,9 +313,12 @@ function test_print() out = String(take!(io)) Test.@test occursin("๐Ÿ“ฆ Configuration:", out) - Test.@test occursin("โ”œโ”€ Discretizer:", out) - Test.@test occursin("โ”œโ”€ Modeler:", out) - Test.@test occursin("โ””โ”€ Solver:", out) + # Check for tree structure elements and content regardless of ANSI formatting + Test.@test occursin("โ”œโ”€", out) + Test.@test occursin("โ””โ”€", out) + Test.@test occursin("Discretizer", out) + Test.@test occursin("Modeler", out) + Test.@test occursin("Solver", out) end Test.@testset "Color and styling" begin @@ -367,7 +384,7 @@ function test_print() allocs = Test.@allocated OptimalControl.display_ocp_configuration( io, disc, mod, sol ) - Test.@test allocs < 20000 # Adjusted from 10000 (14416 observed) + Test.@test allocs < 25000 # Adjusted for ANSI sequences overhead (21648 observed) end Test.@testset "Performance with options" begin @@ -395,7 +412,7 @@ function test_print() io, disc, mod, sol ) end - Test.@test total_allocs < 100000 # Adjusted from 50000 (72080 observed) + Test.@test total_allocs < 120000 # Adjusted for ANSI sequences overhead (108240 observed) end end @@ -471,7 +488,7 @@ function test_print() io, disc, mod, sol ) out = String(take!(io)) - Test.@test occursin("โ–ซ OptimalControl v", out) + Test.@test occursin("โ–ซ This is OptimalControl", out) Test.@test occursin("Configuration:", out) end end