diff --git a/CHANGELOG.md b/CHANGELOG.md index 6f500778..ae882cc5 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 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 + +- **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 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 + +--- + ## [Unreleased] โ€” branch `action-options` ### Added diff --git a/Project.toml b/Project.toml index 089530d2..5a25e66b 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.2-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/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) 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..7faa5d1f 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 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..75833f26 100644 --- a/docs/src/manual-solve.md +++ b/docs/src/manual-solve.md @@ -100,8 +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) - `: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..ea2a50d5 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,13 @@ 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, :uno, :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..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 # ============================================================================ @@ -172,6 +238,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 # ============================================================================ @@ -265,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`) @@ -277,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 @@ -409,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)) @@ -428,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 @@ -448,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) 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..1b20bbd8 100644 --- a/test/suite/helpers/test_methods.jl +++ b/test/suite/helpers/test_methods.jl @@ -32,10 +32,12 @@ 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 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 @@ -43,8 +45,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: 10 CPU methods + 2 GPU methods = 12 methods + Test.@test length(methods) == 12 end Test.@testset "Parameter Distribution" begin @@ -54,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) == 8 # All original methods now with :cpu + Test.@test length(cpu_methods) == 10 # All original methods now with :cpu + Uno Test.@test length(gpu_methods) == 2 # Only GPU-capable combinations end @@ -152,7 +154,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,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) == 8 - 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/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 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..1e2b2eac 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,18 @@ 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 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 + ("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)), ] problems = [("Beam", Beam()), ("Goddard", Goddard())] @@ -78,67 +85,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 # ====================================================================