diff --git a/Project.toml b/Project.toml index cb55cd22..64596034 100644 --- a/Project.toml +++ b/Project.toml @@ -1,7 +1,7 @@ name = "OptimalControlProblems" uuid = "59046045-fb9c-4c23-964f-ff0a25704f96" authors = ["Olivier Cots "] -version = "0.2.2" +version = "0.3.0" [deps] ADNLPModels = "54578032-b7ea-4c30-94aa-7cbd1cce6c9a" @@ -24,7 +24,7 @@ OptimalControlModels = "OptimalControl" [compat] ADNLPModels = "0.8" CTBase = "0.16" -CTDirect = "0.16" +CTDirect = "0.17" CTModels = "0.6" DocStringExtensions = "0.9" ExaModels = "0.9" diff --git a/docs/Project.toml b/docs/Project.toml index 1871f85f..c9409a0d 100644 --- a/docs/Project.toml +++ b/docs/Project.toml @@ -13,6 +13,7 @@ Markdown = "d6f4376e-aef5-505a-96c1-9c027394607a" NLPModels = "a4795742-8479-5a88-8948-cc11e1c8c1a6" NLPModelsIpopt = "f4238b75-b362-5c4c-b852-0801c9a21d71" OptimalControl = "5f98b655-cc9a-415a-b60e-744165666948" +OptimalControlProblems = "59046045-fb9c-4c23-964f-ff0a25704f96" Plots = "91a5bcdd-55d7-5caf-9e0b-520d859cae80" Printf = "de0858da-6303-5e67-8744-51eddeeeb8d7" TOML = "fa267f1f-6049-4f14-aa54-33bafae1ed76" @@ -25,7 +26,9 @@ Documenter = "1" DocumenterInterLinks = "1" DocumenterMermaid = "0.2" ExaModels = "0.9" +FilePathsBase = "0.9" Ipopt = "1" +JSON = "0.21" JuMP = "1" Markdown = "1" NLPModels = "0.21" @@ -34,3 +37,4 @@ OptimalControl = "1" Plots = "1" Printf = "1" TOML = "1" +Tables = "1" diff --git a/docs/browser.jl b/docs/browser.jl index 567f8378..bfcd86b8 100644 --- a/docs/browser.jl +++ b/docs/browser.jl @@ -67,12 +67,12 @@ const TABLE_STYLE = """ /* ============================== Base Palette ============================== */ - --color-dark-blue: #003d4d; /* very dark blue */ - --color-deep-blue: #005f73; /* deep blue */ - --color-bright-blue:#0096a0; /* bright blue */ - --color-orange: #f18f01; /* orange / ochre */ - --color-soft-red: #d72638; /* soft red */ - --color-deep-violet:#6a0572; /* deep violet */ + --color-dark-blue: #003d4d; + --color-deep-blue: #005f73; + --color-bright-blue:#0096a0; + --color-orange: #f18f01; + --color-soft-red: #d72638; + --color-deep-violet:#6a0572; --color-bootstrap-blue: #007BFF; --color-light-gray: #ddd; --color-gray-text: #666; @@ -92,24 +92,13 @@ const TABLE_STYLE = """ --color-final: var(--color-dark-blue); --color-constraints: var(--color-dark-blue); - /* Constraint buttons */ - --btn-x: var(--color-light-gray); - --btn-u: var(--color-light-gray); - --btn-v: var(--color-light-gray); - --btn-c: var(--color-light-gray); - --btn-b: var(--color-light-gray); - - /* DataTables Filter buttons */ + /* Buttons */ --btn-filters-active: var(--color-soft-green); --btn-filters-disabled: var(--color-light-gray); --btn-filters-enabled: var(--color-bootstrap-blue); --btn-filters-hover: var(--color-soft-green-lighter); - /* DataTables Constraints buttons */ - --btn-constraints-active: var(--color-soft-green); --btn-constraints-disabled: var(--color-light-gray); - --btn-constraints-enabled: var(--color-bootstrap-blue); - --btn-constraints-hover: var(--color-soft-green-lighter); } /* ============================== @@ -118,7 +107,10 @@ const TABLE_STYLE = """ #problems-table { width: 100%; border-collapse: collapse; + opacity: 0; /* fade-in on load */ + transition: opacity 0.5s ease; } +#problems-table.visible { opacity: 1; } #problems-table thead th { background: linear-gradient(to bottom, var(--color-table-bg), var(--color-table-bg-alt)); @@ -126,7 +118,6 @@ const TABLE_STYLE = """ padding: 6px 8px; border-bottom: 2px solid var(--color-light-gray); } - #problems-table thead th:nth-child(1) { color: var(--color-problem); } #problems-table thead th:nth-child(2) { color: var(--color-state); } #problems-table thead th:nth-child(3) { color: var(--color-control); } @@ -139,11 +130,9 @@ const TABLE_STYLE = """ padding: 6px 8px; text-align: left; } - #problems-table tbody tr:nth-child(even) { background-color: var(--color-table-bg-alt); } - #problems-table tbody tr:hover { background-color: var(--color-table-bg-alt); cursor: pointer; @@ -152,47 +141,36 @@ const TABLE_STYLE = """ /* ============================== DataTables Controls ============================== */ -div.dataTables_wrapper div.dataTables_length { - margin-bottom: 8px; -} - .dt-top-buttons { margin-bottom: 6px; } - .dt-top-buttons .dt-buttons { display: flex; gap: 8px; align-items: center; margin-bottom: 20px; } - .dt-top-buttons .dt-buttons button { - margin: 0; - padding: 6px 10px; -} - -.dt-buttons button i { - font-size: 1.2em; - vertical-align: middle; -} - -.dt-buttons button { padding: 4px 8px!important; } - -/* Export buttons colors */ -.dt-buttons .buttons-copy { color: #0096a0!important;} -.dt-buttons .buttons-csv { color: #f18f01!important;} -.dt-buttons .buttons-excel{ color: #2F8F3F!important;} -.dt-buttons .buttons-pdf { color: #d72638!important;} -.dt-buttons .buttons-print{ color: #6a0572!important;} - +.dt-buttons button i { font-size: 1.2em; vertical-align: middle; } .dt-buttons button:hover { opacity: 0.85; transform: scale(1.03); } +/* Export buttons (semantic classes) */ +.dt-buttons .buttons-copy { color: var(--color-bright-blue) !important; } +.dt-buttons .buttons-csv { color: var(--color-orange) !important; } +.dt-buttons .buttons-excel { color: var(--color-soft-green) !important; } +.dt-buttons .buttons-pdf { color: var(--color-soft-red) !important; } +.dt-buttons .buttons-print { color: var(--color-deep-violet) !important; } + +/* Also target icons inside buttons, in case inherits default color */ +.dt-buttons button i { + color: inherit !important; +} + .dt-top-controls { display: flex; justify-content: space-between; @@ -201,79 +179,59 @@ div.dataTables_wrapper div.dataTables_length { margin-bottom: 8px; width: 100%; } - -.dt-top-controls .dataTables_length { - margin: 0; -} - -.dt-top-controls .dataTables_length label { - margin: 0; - display: flex; - align-items: center; - gap: 6px; -} - -.dt-top-controls .dataTables_length select { - min-width: 70px; -} - -.dt-top-controls .dataTables_filter { - margin: 0; -} - +.dt-top-controls .dataTables_length, +.dt-top-controls .dataTables_filter { margin: 0; } +.dt-top-controls .dataTables_length label, .dt-top-controls .dataTables_filter label { - margin: 0; display: flex; align-items: center; gap: 6px; + margin: 0; } - +.dt-top-controls .dataTables_length select { min-width: 70px; } .dt-top-controls .dataTables_filter input { width: 220px; max-width: 40vw; padding: 6px 8px; box-sizing: border-box; } - -/* responsive: stack controls vertically on narrow screens */ @media (max-width: 680px) { - .dt-top-controls { - flex-direction: column; - align-items: stretch; - } - .dt-top-controls .dataTables_filter input { - width: 100%; - } + .dt-top-controls { flex-direction: column; align-items: stretch; } + .dt-top-controls .dataTables_filter input { width: 100%; } } /* ============================== - Constraint Buttons (Table Rows) + Constraint Buttons ============================== */ .constraint-btn { + border: none; border-radius: 12px; padding: 4px 8px; - margin: 0px; - font-size: 0.85em; font-weight: bold; - border: none; cursor: pointer; transition: 0.2s; } +.constraint-btn.small { font-size: 0.75em; } +.constraint-btn.normal { font-size: 0.85em; } -.constraint-btn[data-dim="0"] { background-color: var(--btn-constraints-disabled); color: var(--color-gray-text); } -.constraint-btn[data-dim]:not([data-dim="0"]) { background-color: var(--btn-constraints-enabled); color: white; } +/* State (zero vs nonzero) */ +.constraint-btn.dim-zero { + background-color: var(--btn-constraints-disabled); + color: var(--color-gray-text); +} +.constraint-btn.dim-nonzero { color: white; } -.constraint-btn[data-type="x"] { background-color: var(--btn-x); } -.constraint-btn[data-type="u"] { background-color: var(--btn-u); } -.constraint-btn[data-type="v"] { background-color: var(--btn-v); } -.constraint-btn[data-type="c"] { background-color: var(--btn-c); } -.constraint-btn[data-type="b"] { background-color: var(--btn-b); color: white; } +/* Type-specific (nonzero only) */ +.constraint-btn.constraint-x.dim-nonzero { background-color: var(--color-dark-blue); } +.constraint-btn.constraint-u.dim-nonzero { background-color: var(--color-deep-blue); } +.constraint-btn.constraint-v.dim-nonzero { background-color: var(--color-bright-blue); } +.constraint-btn.constraint-c.dim-nonzero { background-color: var(--color-orange); } +.constraint-btn.constraint-b.dim-nonzero { background-color: var(--color-soft-red); } .constraints-wrapper strong { - padding: 0px 0px; - border-radius: 4px; font-size: 0.95em; font-weight: bold; + border-radius: 4px; transition: background-color 0.3s, transform 0.2s; } @@ -287,25 +245,27 @@ div.dataTables_wrapper div.dataTables_length { font-size: 0.85em; font-weight: bold; border: none; - transition: background-color 0.2s, transform 0.1s; cursor: pointer; background-color: var(--btn-filters-disabled); color: #333; + transition: background-color 0.2s, transform 0.1s; } - .constraint-filter-btn:hover { background-color: var(--btn-filters-hover); color: white; transform: scale(1.05); } - .constraint-filter-btn.active { background-color: var(--btn-filters-active); color: white; } +.constraint-filter-btn { background: #ccc; color: #333; } /* default gray */ +.constraint-filter-btn.positive { background: #4caf50; color: white; } /* green */ +.constraint-filter-btn.negative { background: #f44336; color: white; } /* red */ + /* ============================== - Filters (Numeric Inputs / Selects) + Filters (Inputs / Selects) ============================== */ #problems-table thead input[type="text"] { width: 90%!important; @@ -316,7 +276,6 @@ div.dataTables_wrapper div.dataTables_length { border-radius: 4px; text-align: center; } - #problems-table thead select { width: 95%; max-width: 80px; @@ -327,20 +286,15 @@ div.dataTables_wrapper div.dataTables_length { background: #fff; text-align: center; } - -/* Constraint Filter Container */ #constraints-filter { text-align: center; } - #constraints-filter > div:first-child { display: flex!important; flex-direction: column; align-items: center; - justify-content: center!important; padding: 2px; } - #constraints-filter select { margin-left: 4px; padding: 2px 5px; @@ -348,20 +302,19 @@ div.dataTables_wrapper div.dataTables_length { border-radius: 4px; margin-bottom: 5px; } - #constraints-filter > div > div { display: flex!important; justify-content: left!important; } - #constraints-filter .btn-label { font-size: 0.75em; - line-height: 1.1; margin-top: 2px; color: #333; } -/* Reduce spacing of sort arrows (before and after) */ +/* ============================== + Sorting arrows spacing + ============================== */ table.dataTable thead .sorting::before, table.dataTable thead .sorting::after, table.dataTable thead .sorting_asc::before, @@ -372,19 +325,49 @@ table.dataTable thead .sorting_asc_disabled::before, table.dataTable thead .sorting_asc_disabled::after, table.dataTable thead .sorting_desc_disabled::before, table.dataTable thead .sorting_desc_disabled::after { - right: 2px !important; /* adjust distance from right edge */ + right: 2px !important; +} + +/* ============================== + Loading Overlay + ============================== */ +#loading-overlay { + position: fixed; + top: 0; left: 0; + width: 100%; height: 100%; + background: rgba(255,255,255,0.9); + display: flex; + align-items: center; + justify-content: center; + z-index: 9999; + transition: opacity 0.5s ease; +} +#loading-overlay.hidden { + opacity: 0; + pointer-events: none; +} +.spinner { + border: 6px solid #f3f3f3; + border-top: 6px solid var(--color-deep-blue); + border-radius: 50%; + width: 50px; height: 50px; + animation: spin 1s linear infinite; +} +@keyframes spin { + 0% { transform: rotate(0deg); } + 100% { transform: rotate(360deg); } } """ const TABLE_LOGIC = """ +
""" diff --git a/docs/make.jl b/docs/make.jl index 1a58aff1..3a03401d 100644 --- a/docs/make.jl +++ b/docs/make.jl @@ -135,7 +135,10 @@ with_problems_browser() do browser_file # generates the problems browser and rem "Get a problem" => "tutorial-get.md", "Solve a problem" => "tutorial-solve.md", ], - "Developers" => ["Add a problem" => "dev-add.md", "API" => "dev-api.md"], + "Developers" => [ + "Add a problem" => "dev-add.md", + "API" => "dev-api.md", + ], ], plugins=[links], ) diff --git a/docs/problems.jl b/docs/problems.jl index 119229a1..baa2ccd8 100644 --- a/docs/problems.jl +++ b/docs/problems.jl @@ -5,9 +5,9 @@ function draft_meta(draft::Union{Bool,Nothing}) if isnothing(draft) return "" elseif draft - return """```@meta\nDraft = true\n```""" + return """```@meta\nDraft = true\n```\n""" else - return """```@meta\nDraft = false\n```""" + return """```@meta\nDraft = false\n```\n""" end end @@ -24,7 +24,7 @@ end # ----------------------------------- function generate_documentation(PROBLEM::String, DESCRIPTION::String; draft::Union{Bool,Nothing}) - TITLE = uppercasefirst(replace(PROBLEM, "_" => " ")) + TITLE = "[" * uppercasefirst(replace(PROBLEM, "_" => " ")) * "](@id description-$PROBLEM)" DRAFT = draft_meta(draft) LEFT_MARGIN = get_left_margin(Symbol(PROBLEM)) @@ -67,6 +67,27 @@ function generate_documentation(PROBLEM::String, DESCRIPTION::String; draft::Uni nothing # hide ``` + ## Metadata + + The default number of time steps is: + + ```@example main + metadata(:$PROBLEM)[:grid_size] + ``` + + The default values of the parameters are: + + ```@example main + metadata(:$PROBLEM)[:parameters] + using Printf # hide + println("Parameter = Value") # hide + println("------------------") # hide + for e ∈ pairs(metadata(:$PROBLEM)[:parameters]) # hide + @printf("%6s = ", string(e.first)) # hide + @printf("%11.4e\\n", e.second) # hide + end # hide + ``` + ## Initial guess Before solving the problem, it is often useful to inspect the initial guess (sometimes called the first iterate). This guess is obtained by running the NLP solver with `max_iter = 0`, which evaluates the problem formulation without performing any optimisation steps. @@ -82,25 +103,22 @@ function generate_documentation(PROBLEM::String, DESCRIPTION::String; draft::Uni ```@example main function plot_initial_guess(problem) - # ----------------------------- - # Extract dimensions from metadata - # ----------------------------- - x_vars = metadata[problem][:state_name] - u_vars = metadata[problem][:control_name] - n_states = length(x_vars) - n_controls = length(u_vars) - # ----------------------------- # Build OptimalControl problem # ----------------------------- - ocp_model = eval(problem)(OptimalControlBackend()) - nlp_oc = nlp_model(ocp_model) + docp = eval(problem)(OptimalControlBackend()) + nlp_oc = nlp_model(docp) + ocp_oc = ocp_model(docp) # Solve NLP with zero iterations (initial guess) nlp_oc_sol = NLPModelsIpopt.ipopt(nlp_oc; max_iter=0) # Build OptimalControl solution - ocp_sol = build_ocp_solution(ocp_model, nlp_oc_sol) + ocp_sol = build_ocp_solution(docp, nlp_oc_sol) + + # get dimensions + n = state_dimension(ocp_oc) + m = control_dimension(ocp_oc) # ----------------------------- # Plot OptimalControl solution @@ -112,13 +130,13 @@ function generate_documentation(PROBLEM::String, DESCRIPTION::String; draft::Uni control_style=(color=1, legend=:none), path_style=(color=1, legend=:none), dual_style=(color=1, legend=:none), - size=(816, 220*(n_states+n_controls)), + size=(816, 220*(n+m)), label="OptimalControl", leftmargin=$LEFT_MARGIN, ) # Hide legend for additional state plots - for i in 2:n_states + for i in 2:n plot!(plt[i]; legend=:none) end @@ -133,28 +151,28 @@ function generate_documentation(PROBLEM::String, DESCRIPTION::String; draft::Uni optimize!(nlp_jp) # Extract trajectories - t_grid = time_grid(problem, nlp_jp) - x_fun = state(problem, nlp_jp) - u_fun = control(problem, nlp_jp) - p_fun = costate(problem, nlp_jp) + t_grid = time_grid(nlp_jp) + x_fun = state(nlp_jp) + u_fun = control(nlp_jp) + p_fun = costate(nlp_jp) # ----------------------------- # Plot JuMP solution on top # ----------------------------- # States - for i in 1:n_states + for i in 1:n label = i == 1 ? "JuMP" : :none plot!(plt[i], t_grid, t -> x_fun(t)[i]; color=2, linestyle=:dash, label=label) end # Costates - for i in 1:n_states - plot!(plt[n_states+i], t_grid, t -> -p_fun(t)[i]; color=2, linestyle=:dash, label=:none) + for i in 1:n + plot!(plt[n+i], t_grid, t -> -p_fun(t)[i]; color=2, linestyle=:dash, label=:none) end # Controls - for i in 1:n_controls - plot!(plt[2*n_states+i], t_grid, t -> u_fun(t)[i]; color=2, linestyle=:dash, label=:none) + for i in 1:m + plot!(plt[2*n+i], t_grid, t -> u_fun(t)[i]; color=2, linestyle=:dash, label=:none) end return plt @@ -179,14 +197,12 @@ function generate_documentation(PROBLEM::String, DESCRIPTION::String; draft::Uni Before solving, we can inspect the discretisation details of the problem. The table below reports the number of grid points, decision variables, and constraints associated with the chosen formulation. ```@example main - push!(data_pb, - ( - Problem=:$PROBLEM, - Grid_Size=metadata[:$PROBLEM][:N], - Variables=get_nvar(nlp_model($PROBLEM(OptimalControlBackend()))), - Constraints=get_ncon(nlp_model($PROBLEM(OptimalControlBackend()))), - ) - ) + push!(data_pb,( + Problem=:$PROBLEM, + Grid_Size=metadata(:$PROBLEM)[:grid_size], + Variables=get_nvar(nlp_model($PROBLEM(OptimalControlBackend()))), + Constraints=get_ncon(nlp_model($PROBLEM(OptimalControlBackend()))), + )) data_pb # hide ``` @@ -236,24 +252,20 @@ function generate_documentation(PROBLEM::String, DESCRIPTION::String; draft::Uni ```@example main # from OptimalControl model - push!(data_re, - ( - Model=:OptimalControl, - Flag=nlp_oc_sol.status, - Iterations=nlp_oc_sol.iter, - Objective=nlp_oc_sol.objective, - ) - ) + push!(data_re,( + Model=:OptimalControl, + Flag=nlp_oc_sol.status, + Iterations=nlp_oc_sol.iter, + Objective=nlp_oc_sol.objective, + )) # from JuMP model - push!(data_re, - ( - Model=:JuMP, - Flag=termination_status(nlp_jp), - Iterations=barrier_iterations(nlp_jp), - Objective=objective_value(nlp_jp), - ) - ) + push!(data_re,( + Model=:JuMP, + Flag=termination_status(nlp_jp), + Iterations=barrier_iterations(nlp_jp), + Objective=objective_value(nlp_jp), + )) data_re # hide ``` @@ -287,16 +299,16 @@ function generate_documentation(PROBLEM::String, DESCRIPTION::String; draft::Uni i_oc = iterations(ocp_sol) # get relevant data from JuMP model - t_jp = time_grid(problem, nlp_jp) - x_jp = state(problem, nlp_jp).(t_jp) - u_jp = control(problem, nlp_jp).(t_jp) - o_jp = objective(problem, nlp_jp) - v_jp = variable(problem, nlp_jp) - i_jp = iterations(problem, nlp_jp) + t_jp = time_grid(nlp_jp) + x_jp = state(nlp_jp).(t_jp) + u_jp = control(nlp_jp).(t_jp) + o_jp = objective(nlp_jp) + v_jp = variable(nlp_jp) + i_jp = iterations(nlp_jp) - x_vars = metadata[problem][:state_name] - u_vars = metadata[problem][:control_name] - v_vars = metadata[problem][:variable_name] + x_vars = state_components(nlp_jp) + u_vars = control_components(nlp_jp) + v_vars = variable_components(nlp_jp) println("┌─ ", string(problem)) println("│") @@ -365,8 +377,8 @@ function generate_documentation(PROBLEM::String, DESCRIPTION::String; draft::Uni ocp_sol = build_ocp_solution(docp, nlp_oc_sol) # dimensions - n = state_dimension(ocp_sol) # or length(metadata[:$PROBLEM][:state_name]) - m = control_dimension(ocp_sol) # or length(metadata[:$PROBLEM][:control_name]) + n = state_dimension(ocp_sol) + m = control_dimension(ocp_sol) # from OptimalControl solution plt = plot( @@ -381,10 +393,10 @@ function generate_documentation(PROBLEM::String, DESCRIPTION::String; draft::Uni end # from JuMP solution - t = time_grid(:$PROBLEM, nlp_jp) # t0, ..., tN = tf - x = state(:$PROBLEM, nlp_jp) # function of time - u = control(:$PROBLEM, nlp_jp) # function of time - p = costate(:$PROBLEM, nlp_jp) # function of time + t = time_grid(nlp_jp) # t0, ..., tN = tf + x = state(nlp_jp) # function of time + u = control(nlp_jp) # function of time + p = costate(nlp_jp) # function of time for i in 1:n # state label = i == 1 ? "JuMP" : :none diff --git a/docs/src/assets/Manifest.toml b/docs/src/assets/Manifest.toml index 8d1d9468..eca499af 100644 --- a/docs/src/assets/Manifest.toml +++ b/docs/src/assets/Manifest.toml @@ -1,8 +1,8 @@ # This file is machine-generated - editing it directly is not advised -julia_version = "1.11.6" +julia_version = "1.11.7" manifest_format = "2.0" -project_hash = "05847f69ae3d937e6a44ff3a4a0761aed56ed09e" +project_hash = "0333523248cf50d887072c74218ec6daa141e6a3" [[deps.ADNLPModels]] deps = ["ADTypes", "ForwardDiff", "LinearAlgebra", "NLPModels", "Requires", "ReverseDiff", "SparseArrays", "SparseConnectivityTracer", "SparseMatrixColorings"] @@ -11,9 +11,9 @@ uuid = "54578032-b7ea-4c30-94aa-7cbd1cce6c9a" version = "0.8.13" [[deps.ADTypes]] -git-tree-sha1 = "60665b326b75db6517939d0e1875850bc4a54368" +git-tree-sha1 = "27cecae79e5cc9935255f90c53bb831cc3c870d7" uuid = "47edcb42-4c32-4615-8424-f2b9edc5f35b" -version = "1.17.0" +version = "1.18.0" [deps.ADTypes.extensions] ADTypesChainRulesCoreExt = "ChainRulesCore" @@ -43,9 +43,9 @@ version = "0.4.5" [[deps.Adapt]] deps = ["LinearAlgebra", "Requires"] -git-tree-sha1 = "f7817e2e585aa6d924fd714df1e2a84be7896c60" +git-tree-sha1 = "7e35fca2bdfba44d797c53dfe63a51fabf39bfc0" uuid = "79e6a3ab-5dfb-504d-930d-738a2a938a0e" -version = "4.3.0" +version = "4.4.0" weakdeps = ["SparseArrays", "StaticArrays"] [deps.Adapt.extensions] @@ -105,21 +105,21 @@ weakdeps = ["HTTP", "JSON"] [[deps.CTDirect]] deps = ["CTBase", "CTModels", "DocStringExtensions", "HSL", "MKL", "SparseArrays"] -git-tree-sha1 = "1b3aa9b9b9bbb32b90bed66d16998fcb89848c21" +git-tree-sha1 = "5f7bd0cc0a2b4ff09a6621071bdc07aa121349a2" uuid = "790bbbee-bee9-49ee-8912-a9de031322d5" -version = "0.16.3" +version = "0.17.2" [deps.CTDirect.extensions] CTDirectExtADNLP = ["ADNLPModels"] CTDirectExtExa = ["ExaModels"] CTDirectExtIpopt = ["NLPModelsIpopt"] CTDirectExtKnitro = ["NLPModelsKnitro"] - CTDirectExtMadNLP = ["MadNLP"] + CTDirectExtMadNLP = ["MadNLPMumps"] [deps.CTDirect.weakdeps] ADNLPModels = "54578032-b7ea-4c30-94aa-7cbd1cce6c9a" ExaModels = "1037b233-b668-4ce9-9b63-f9f681f55dd2" - MadNLP = "2621e9c9-9eb4-46b1-8089-e8c72242dfb6" + MadNLPMumps = "3b83494e-c0a4-4895-918b-9157a7a085a1" NLPModelsIpopt = "f4238b75-b362-5c4c-b852-0801c9a21d71" NLPModelsKnitro = "bec4dd0d-7755-52d5-9a02-22f0ffc7efcb" @@ -137,9 +137,9 @@ version = "0.8.8" [[deps.CTModels]] deps = ["CTBase", "DocStringExtensions", "Interpolations", "LinearAlgebra", "MLStyle", "MacroTools", "OrderedCollections", "Parameters", "RecipesBase"] -git-tree-sha1 = "20872a1b453a9b7a94822cc00a22c6741f71cf68" +git-tree-sha1 = "5dd30f45b055084374b2613cb4c79e73420962f0" uuid = "34c4fa32-2049-4079-8329-de33c2a22e2d" -version = "0.6.5" +version = "0.6.7" [deps.CTModels.extensions] CTModelsJLD = "JLD2" @@ -153,9 +153,9 @@ version = "0.6.5" [[deps.CTParser]] deps = ["CTBase", "DocStringExtensions", "MLStyle", "OrderedCollections", "Parameters", "Unicode"] -git-tree-sha1 = "507a19db4be22ac5aa6967d78c97e68fd95a670a" +git-tree-sha1 = "7e95684a3b47015e3d2feac81292a89d60a9dae6" uuid = "32681960-a1b1-40db-9bff-a1ca817385d1" -version = "0.6.4" +version = "0.7.0" [[deps.Cairo_jll]] deps = ["Artifacts", "Bzip2_jll", "CompilerSupportLibraries_jll", "Fontconfig_jll", "FreeType2_jll", "Glib_jll", "JLLWrappers", "LZO_jll", "Libdl", "Pixman_jll", "Xorg_libXext_jll", "Xorg_libXrender_jll", "Zlib_jll", "libpng_jll"] @@ -187,9 +187,9 @@ version = "0.7.8" [[deps.ColorSchemes]] deps = ["ColorTypes", "ColorVectorSpace", "Colors", "FixedPointNumbers", "PrecompileTools", "Random"] -git-tree-sha1 = "a656525c8b46aa6a1c76891552ed5381bb32ae7b" +git-tree-sha1 = "b0fd3f56fa442f81e0a47815c92245acfaaa4e34" uuid = "35d6a980-a343-548e-a6ea-1d62b119f2f4" -version = "3.30.0" +version = "3.31.0" [[deps.ColorTypes]] deps = ["FixedPointNumbers", "Random"] @@ -266,9 +266,9 @@ version = "1.16.0" [[deps.DataFrames]] deps = ["Compat", "DataAPI", "DataStructures", "Future", "InlineStrings", "InvertedIndices", "IteratorInterfaceExtensions", "LinearAlgebra", "Markdown", "Missings", "PooledArrays", "PrecompileTools", "PrettyTables", "Printf", "Random", "Reexport", "SentinelArrays", "SortingAlgorithms", "Statistics", "TableTraits", "Tables", "Unicode"] -git-tree-sha1 = "a37ac0840a1196cd00317b57e39d6586bf0fd6f6" +git-tree-sha1 = "c967271c27a95160e30432e011b58f42cd7501b5" uuid = "a93c6f00-e57d-5684-b7b6-d8193f3e46c0" -version = "1.7.1" +version = "1.8.0" [[deps.DataStructures]] deps = ["OrderedCollections"] @@ -437,9 +437,9 @@ version = "1.11.0" [[deps.FillArrays]] deps = ["LinearAlgebra"] -git-tree-sha1 = "6a70198746448456524cb442b8af316927ff3e1a" +git-tree-sha1 = "173e4d8f14230a7523ae11b9a3fa9edb3e0efd78" uuid = "1a297f60-69ca-5386-bcde-b61e274b549b" -version = "1.13.0" +version = "1.14.0" [deps.FillArrays.extensions] FillArraysPDMatsExt = "PDMats" @@ -470,9 +470,9 @@ version = "1.3.7" [[deps.ForwardDiff]] deps = ["CommonSubexpressions", "DiffResults", "DiffRules", "LinearAlgebra", "LogExpFunctions", "NaNMath", "Preferences", "Printf", "Random", "SpecialFunctions"] -git-tree-sha1 = "ce15956960057e9ff7f1f535400ffa14c92429a4" +git-tree-sha1 = "dc41303865a16274ecb8450c220021ce1e0cf05f" uuid = "f6369f11-7733-5829-9624-2563aa707210" -version = "1.1.0" +version = "1.2.1" weakdeps = ["StaticArrays"] [deps.ForwardDiff.extensions] @@ -550,9 +550,9 @@ version = "2.51.1+0" [[deps.Glib_jll]] deps = ["Artifacts", "GettextRuntime_jll", "JLLWrappers", "Libdl", "Libffi_jll", "Libiconv_jll", "Libmount_jll", "PCRE2_jll", "Zlib_jll"] -git-tree-sha1 = "35fbd0cefb04a516104b8e183ce0df11b70a3f1a" +git-tree-sha1 = "50c11ffab2a3d50192a228c313f05b5b5dc5acb2" uuid = "7746bdde-850d-59dc-9ae8-88ece973131d" -version = "2.84.3+0" +version = "2.86.0+0" [[deps.Graphite2_jll]] deps = ["Artifacts", "JLLWrappers", "Libdl"] @@ -630,12 +630,15 @@ deps = ["Adapt", "AxisAlgorithms", "ChainRulesCore", "LinearAlgebra", "OffsetArr git-tree-sha1 = "65d505fa4c0d7072990d659ef3fc086eb6da8208" uuid = "a98d9a8b-a2ab-59e6-89dd-64a1c18fca59" version = "0.16.2" -weakdeps = ["ForwardDiff", "Unitful"] [deps.Interpolations.extensions] InterpolationsForwardDiffExt = "ForwardDiff" InterpolationsUnitfulExt = "Unitful" + [deps.Interpolations.weakdeps] + ForwardDiff = "f6369f11-7733-5829-9624-2563aa707210" + Unitful = "1986cc42-f94f-5a68-af5c-568840ba703d" + [[deps.InvertedIndices]] git-tree-sha1 = "6da3c4316095de0f5ee2ebd875df8721e7e0bdbe" uuid = "41ab1584-1d38-5bbf-9106-f11c6c58b48f" @@ -705,9 +708,9 @@ version = "3.1.3+0" [[deps.JuMP]] deps = ["LinearAlgebra", "MacroTools", "MathOptInterface", "MutableArithmetics", "OrderedCollections", "PrecompileTools", "Printf", "SparseArrays"] -git-tree-sha1 = "d9c29fadef257492791c83b34ceede0d92a51470" +git-tree-sha1 = "b201ac010ecdcc3617649175fa59c3dbd9bf96a0" uuid = "4076af6c-e467-56ae-b986-b466b2749572" -version = "1.29.0" +version = "1.29.1" [deps.JuMP.extensions] JuMPDimensionalDataExt = "DimensionalData" @@ -827,9 +830,9 @@ version = "2.41.1+0" [[deps.Libtiff_jll]] deps = ["Artifacts", "JLLWrappers", "JpegTurbo_jll", "LERC_jll", "Libdl", "XZ_jll", "Zlib_jll", "Zstd_jll"] -git-tree-sha1 = "4ab7581296671007fc33f07a721631b8855f4b1d" +git-tree-sha1 = "f04133fe05eff1667d2054c53d59f9122383fe05" uuid = "89763e89-9b03-5906-acba-b20f662cd828" -version = "4.7.1+0" +version = "4.7.2+0" [[deps.Libuuid_jll]] deps = ["Artifacts", "JLLWrappers", "Libdl"] @@ -937,9 +940,9 @@ version = "0.1.2" [[deps.MathOptInterface]] deps = ["BenchmarkTools", "CodecBzip2", "CodecZlib", "DataStructures", "ForwardDiff", "JSON3", "LinearAlgebra", "MutableArithmetics", "NaNMath", "OrderedCollections", "PrecompileTools", "Printf", "SparseArrays", "SpecialFunctions", "Test"] -git-tree-sha1 = "9603279ae328cb943a5f36ecd40de2774b5646d3" +git-tree-sha1 = "700acfa97a2b23569c0a6dcfcd85f183d7258e31" uuid = "b8f27783-ece8-5eb3-8dc8-9495eed66fee" -version = "1.44.0" +version = "1.45.0" [[deps.MbedTLS]] deps = ["Dates", "MbedTLS_jll", "MozillaCACerts_jll", "NetworkOptions", "Random", "Sockets"] @@ -973,9 +976,9 @@ version = "2023.12.12" [[deps.MutableArithmetics]] deps = ["LinearAlgebra", "SparseArrays", "Test"] -git-tree-sha1 = "491bdcdc943fcbc4c005900d7463c9f216aabf4c" +git-tree-sha1 = "a03ae6a640a92191615fb53baae6a74b74bce56d" uuid = "d8a4904e-b15c-11e9-3269-09a3773c0cb0" -version = "1.6.4" +version = "1.6.5" [[deps.NLPModels]] deps = ["FastClosures", "LinearAlgebra", "LinearOperators", "Printf", "SparseArrays"] @@ -1032,9 +1035,9 @@ version = "0.8.5+0" [[deps.OpenSSH_jll]] deps = ["Artifacts", "JLLWrappers", "Libdl", "OpenSSL_jll", "Zlib_jll"] -git-tree-sha1 = "cb7acd5d10aff809b4d0191dfe1956c2edf35800" +git-tree-sha1 = "1f2f0911e1c02f28a390bb720f97f3349c4dcefb" uuid = "9bd350c2-7e96-507f-8002-3f2e150b4e1b" -version = "10.0.1+0" +version = "10.0.2+0" [[deps.OpenSSL]] deps = ["BitFlags", "Dates", "MozillaCACerts_jll", "OpenSSL_jll", "Sockets"] @@ -1056,15 +1059,15 @@ version = "0.5.6+0" [[deps.OptimalControl]] deps = ["ADNLPModels", "CTBase", "CTDirect", "CTFlows", "CTModels", "CTParser", "CommonSolve", "DocStringExtensions", "ExaModels"] -git-tree-sha1 = "811aefbb89810f672a92f34a9601d6d7d1a9de38" +git-tree-sha1 = "5aa867a566d505a9ad0802a1a26384ad95427b71" uuid = "5f98b655-cc9a-415a-b60e-744165666948" -version = "1.1.1" +version = "1.1.2" [[deps.OptimalControlProblems]] deps = ["ADNLPModels", "CTBase", "CTDirect", "CTModels", "DocStringExtensions", "ExaModels", "OrderedCollections", "SolverCore"] path = "/Users/ocots/Research/logiciels/dev/control-toolbox/OptimalControlProblems" uuid = "59046045-fb9c-4c23-964f-ff0a25704f96" -version = "0.2.2" +version = "1.0.0" weakdeps = ["JuMP", "OptimalControl"] [deps.OptimalControlProblems.extensions] @@ -1089,9 +1092,9 @@ version = "10.42.0+1" [[deps.Pango_jll]] deps = ["Artifacts", "Cairo_jll", "Fontconfig_jll", "FreeType2_jll", "FriBidi_jll", "Glib_jll", "HarfBuzz_jll", "JLLWrappers", "Libdl"] -git-tree-sha1 = "275a9a6d85dc86c24d03d1837a0010226a96f540" +git-tree-sha1 = "1f7f9bbd5f7a2e5a9f7d96e51c9754454ea7f60b" uuid = "36c8627f-9965-5494-a995-c6b170f724f3" -version = "1.56.3+0" +version = "1.56.4+0" [[deps.Parameters]] deps = ["OrderedCollections", "UnPack"] @@ -1133,10 +1136,10 @@ uuid = "995b91a9-d308-5afd-9ec6-746e21dbc043" version = "1.4.3" [[deps.Plots]] -deps = ["Base64", "Contour", "Dates", "Downloads", "FFMPEG", "FixedPointNumbers", "GR", "JLFzf", "JSON", "LaTeXStrings", "Latexify", "LinearAlgebra", "Measures", "NaNMath", "Pkg", "PlotThemes", "PlotUtils", "PrecompileTools", "Printf", "REPL", "Random", "RecipesBase", "RecipesPipeline", "Reexport", "RelocatableFolders", "Requires", "Scratch", "Showoff", "SparseArrays", "Statistics", "StatsBase", "TOML", "UUIDs", "UnicodeFun", "UnitfulLatexify", "Unzip"] -git-tree-sha1 = "0c5a5b7e440c008fe31416a3ac9e0d2057c81106" +deps = ["Base64", "Contour", "Dates", "Downloads", "FFMPEG", "FixedPointNumbers", "GR", "JLFzf", "JSON", "LaTeXStrings", "Latexify", "LinearAlgebra", "Measures", "NaNMath", "Pkg", "PlotThemes", "PlotUtils", "PrecompileTools", "Printf", "REPL", "Random", "RecipesBase", "RecipesPipeline", "Reexport", "RelocatableFolders", "Requires", "Scratch", "Showoff", "SparseArrays", "Statistics", "StatsBase", "TOML", "UUIDs", "UnicodeFun", "Unzip"] +git-tree-sha1 = "12ce661880f8e309569074a61d3767e5756a199f" uuid = "91a5bcdd-55d7-5caf-9e0b-520d859cae80" -version = "1.40.19" +version = "1.41.1" [deps.Plots.extensions] FileIOExt = "FileIO" @@ -1171,10 +1174,10 @@ uuid = "21216c6a-2e73-6563-6e65-726566657250" version = "1.5.0" [[deps.PrettyTables]] -deps = ["Crayons", "LaTeXStrings", "Markdown", "PrecompileTools", "Printf", "Reexport", "StringManipulation", "Tables"] -git-tree-sha1 = "1101cd475833706e4d0e7b122218257178f48f34" +deps = ["Crayons", "LaTeXStrings", "Markdown", "PrecompileTools", "Printf", "REPL", "Reexport", "StringManipulation", "Tables"] +git-tree-sha1 = "60f23271568238b236899d00fc290e43d1f2658f" uuid = "08abe8d2-0d0c-5749-adfa-8a2ac140af0d" -version = "2.4.0" +version = "3.0.10" [[deps.Printf]] deps = ["Unicode"] @@ -1346,17 +1349,19 @@ version = "1.11.0" [[deps.SparseConnectivityTracer]] deps = ["ADTypes", "DocStringExtensions", "FillArrays", "LinearAlgebra", "Random", "SparseArrays"] -git-tree-sha1 = "339efef69fda0cccf14c06a483561527e9169b8f" +git-tree-sha1 = "3c3a42a29f696f16273741ffe589b4003f539088" uuid = "9f842d2f-2579-4b1d-911e-f412cf18a3f5" -version = "1.0.1" +version = "1.1.0" [deps.SparseConnectivityTracer.extensions] + SparseConnectivityTracerChainRulesCoreExt = "ChainRulesCore" SparseConnectivityTracerLogExpFunctionsExt = "LogExpFunctions" SparseConnectivityTracerNNlibExt = "NNlib" SparseConnectivityTracerNaNMathExt = "NaNMath" SparseConnectivityTracerSpecialFunctionsExt = "SpecialFunctions" [deps.SparseConnectivityTracer.weakdeps] + ChainRulesCore = "d360d2e6-b24c-11e9-a2a3-2a2ae2dbcce4" LogExpFunctions = "2ab3a3ac-af41-5b50-aa03-7779005ae688" NNlib = "872c559c-99b0-510c-b3b7-b6c96a88d5cd" NaNMath = "77ba4419-2d1f-58cd-9bb1-8ffee604a2e3" @@ -1528,30 +1533,6 @@ git-tree-sha1 = "53915e50200959667e78a92a418594b428dffddf" uuid = "1cfade01-22cf-5700-b092-accc4b62d6e1" version = "0.4.1" -[[deps.Unitful]] -deps = ["Dates", "LinearAlgebra", "Random"] -git-tree-sha1 = "6258d453843c466d84c17a58732dda5deeb8d3af" -uuid = "1986cc42-f94f-5a68-af5c-568840ba703d" -version = "1.24.0" - - [deps.Unitful.extensions] - ConstructionBaseUnitfulExt = "ConstructionBase" - ForwardDiffExt = "ForwardDiff" - InverseFunctionsUnitfulExt = "InverseFunctions" - PrintfExt = "Printf" - - [deps.Unitful.weakdeps] - ConstructionBase = "187b0558-2788-49d3-abe0-74a17ed4e7c9" - ForwardDiff = "f6369f11-7733-5829-9624-2563aa707210" - InverseFunctions = "3587e190-3f89-42d0-90ee-14403ec27112" - Printf = "de0858da-6303-5e67-8744-51eddeeeb8d7" - -[[deps.UnitfulLatexify]] -deps = ["LaTeXStrings", "Latexify", "Unitful"] -git-tree-sha1 = "af305cc62419f9bd61b6644d19170a4d258c7967" -uuid = "45397f5d-5981-4c77-b2b3-fc36d6e9b728" -version = "1.7.0" - [[deps.Unzip]] git-tree-sha1 = "ca0969166a028236229f63514992fc073799bb78" uuid = "41fe7b60-77ed-43a1-b4f0-825fd5a5650d" diff --git a/docs/src/assets/Project.toml b/docs/src/assets/Project.toml index 3ca81632..c9409a0d 100644 --- a/docs/src/assets/Project.toml +++ b/docs/src/assets/Project.toml @@ -26,7 +26,9 @@ Documenter = "1" DocumenterInterLinks = "1" DocumenterMermaid = "0.2" ExaModels = "0.9" +FilePathsBase = "0.9" Ipopt = "1" +JSON = "0.21" JuMP = "1" Markdown = "1" NLPModels = "0.21" @@ -35,3 +37,4 @@ OptimalControl = "1" Plots = "1" Printf = "1" TOML = "1" +Tables = "1" diff --git a/docs/src/dev-add.md b/docs/src/dev-add.md index 841967b8..2988675c 100644 --- a/docs/src/dev-add.md +++ b/docs/src/dev-add.md @@ -6,14 +6,11 @@ To add a new problem to **OptimalControlProblems**, you must follow these steps: ```julia new_problem_meta = OrderedDict( - :name => "new_problem", # Problem name - :N => 100, # Number of steps - :minimise => true, # Whether we minimise (true) or maximise (false) - :state_name => ["x1", "x2"], # Names of the state components - :costate_name => ["∂x1", "∂x2"], # Names of the dynamics constraints (for the costate) - :control_name => ["u"], # Names of the control components - :variable_name => ["v"], # Names of the optimisation variables - :final_time => (:fixed, 1), # Final time information + :grid_size => 100, # Number of steps + :parameters => ( + t0 = 0, # Value of the initial time + tf = 1, # Value of the final time + ), ) ``` @@ -23,6 +20,10 @@ new_problem_meta = OrderedDict( **2.** Define the **OptimalControl** model of the problem in a file named `new_problem.jl` in the `ext/OptimalControlModels` directory, following the template: +!!! tip + + Get inspiration from the already existing problems in [OptimalControlProblems.jl/ext/OptimalControlModels](https://github.com/control-toolbox/OptimalControlProblems.jl/tree/main/ext/OptimalControlModels). Especially if the final time is free. + ```julia """ Documentation of the method @@ -30,31 +31,33 @@ new_problem_meta = OrderedDict( function OptimalControlProblems.new_problem( ::OptimalControlBackend, description::Symbol...; - N::Int=steps_number_data(:new_problem), + grid_size::Int=grid_size_data(:new_problem), kwargs..., ) - # if tf is fixed - tf = final_time_data(:new_problem) + # parameters + params = parameters_data(:beam, parameters) + t0 = params[:t0] + tf = params[:tf] # model @def ocp begin - # Define the problem here - # ... + t ∈ [t0, tf], time + ... end # initial guess for the problem init = () # discretise the optimal control problem - docp = direct_transcription( - ocp, - description...; + docp = direct_transcription( + ocp, + description...; lagrange_to_mayer=false, - init=init, - grid_size=N, - disc_method=:trapeze, - kwargs... + init=init, + grid_size=grid_size, + disc_method=:trapeze, + kwargs..., ) return docp @@ -62,28 +65,79 @@ function OptimalControlProblems.new_problem( end ``` -**3.** Define the **JuMP** model of the problem in a file named `new_problem.jl` in the `ext/JuMPModels` directory, following the template: +**3.** Define the scalarised **OptimalControl** model of the problem in a file named `new_problem_s.jl` in the `ext/OptimalControlModels_s` directory. This version is similar to the previous case but, as an example, replace the dynamics: + +```julia +ẋ(t) == [x₂(t), u(t)] +``` + +by + +```julia +∂(x₁)(t) == x₂(t) +∂(x₂)(t) == u(t) +``` + +!!! warning + + The dynamics and the nonlinear constraints must be in scalar form. See the section [Dynamics (coordinatewise)](@ref OptimalControl manual-abstract-dynamics-coord) and the following section for more details. + +**4.** Define the **JuMP** model of the problem in a file named `new_problem.jl` in the `ext/JuMPModels` directory, following the template: ```julia """ Documentation of the method """ function OptimalControlProblems.new_problem( - ::JuMPBackend, args...; N::Int=steps_number_data(:new_problem), kwargs... + ::JuMPBackend, args...; grid_size::Int=grid_size_data(:new_problem), kwargs... ) - # if tf is fixed - tf = final_time_data(:new_problem) + # parameters + params = parameters_data(:beam, parameters) + t0 = params[:t0] + tf = params[:tf] # model model = JuMP.Model(args...; kwargs...) - # define the problem - # @variables, @constraints, @objective... + # metadata: required + model[:time_grid] = () -> range(t0, tf, grid_size+1) # tf is a fixed + model[:state_components] = ["x₁", "x₂"] + model[:costate_components] = ["∂x₁", "∂x₂"] + model[:control_components] = ["u"] + model[:variable_components] = String[] # no variable + + # N = grid_size + @expression(model, N, grid_size) + + # variables and initial guess + @variables( + model, + begin + x₁[0:N], (start = 0.5) # consistent with model[:state_components] + x₂[0:N], (start = 0.1) # consistent with model[:state_components] + u[0:N], (start = 0.1) # consistent with model[:control_components] + end + ) + + # @constraints, @objective... return model end ``` -**4.** Describe the problem in a file named `new_problem.jl` in the `ext/Descriptions` directory. Please get inspiration from the already existing descriptions in [OptimalControlProblems.jl/ext/Descriptions](https://github.com/control-toolbox/OptimalControlProblems.jl/tree/main/ext/Descriptions). +!!! warning + + - The metadata in JuMP are required and must be consistent with the other models. + - If `tf` is free, then define: + + ```julia + model[:time_grid] = () -> range(t0, value(model[:tf]), grid_size+1) # tf is a free + ``` + +!!! tip + + Get inspiration from the already existing problems in [OptimalControlProblems.jl/ext/JuMPModels](https://github.com/control-toolbox/OptimalControlProblems.jl/tree/main/ext/JuMPModels). Especially if the final time is free. + +**5.** Describe the problem in a file named `new_problem.jl` in the `ext/Descriptions` directory. Get inspiration from the already existing descriptions in [OptimalControlProblems.jl/ext/Descriptions](https://github.com/control-toolbox/OptimalControlProblems.jl/tree/main/ext/Descriptions). diff --git a/docs/src/dev-api.md b/docs/src/dev-api.md index 40c387c4..78fcc095 100644 --- a/docs/src/dev-api.md +++ b/docs/src/dev-api.md @@ -1,41 +1,12 @@ # API -## OptimalControlProblems - -```@index -Pages = ["dev-api.md"] -Modules = [OptimalControlProblems, CTModels, ExaModels] -Order = [:module, :constant, :type, :function, :macro] -``` - -```@autodocs -Modules = [OptimalControlProblems] -Order = [:module, :constant, :type, :function, :macro] -Pages = ["OptimalControlProblems.jl"] -``` - -## OptimalControlModels - -```@index -Pages = ["dev-api.md"] -Modules = [OptimalControlModels] -Order = [:module, :constant, :type, :function, :macro] -``` - -```@autodocs -Modules = [OptimalControlModels] -Order = [:module, :constant, :type, :function, :macro] -``` - -## JuMPModels - ```@index Pages = ["dev-api.md"] -Modules = [JuMPModels, CTModels, ExaModels] +Modules = [OptimalControlProblems, CTModels, ExaModels, OptimalControlModels, JuMPModels] Order = [:module, :constant, :type, :function, :macro] ``` ```@autodocs -Modules = [JuMPModels] +Modules = [OptimalControlProblems, OptimalControlModels, JuMPModels] Order = [:module, :constant, :type, :function, :macro] ``` \ No newline at end of file diff --git a/docs/src/index.md b/docs/src/index.md index 977e26ed..6d2dd2a2 100644 --- a/docs/src/index.md +++ b/docs/src/index.md @@ -47,12 +47,12 @@ If you want to ask a question, feel free to start a discussion [here](https://gi ## Credits (not exhaustive!) -- [Nico77310](https://github.com/Nico77310) +- [BaptisteCbl](https://github.com/BaptisteCbl) - [0Yassine0](https://github.com/0Yassine0) +- [Nico77310](https://github.com/Nico77310) - [frapac](https://github.com/frapac) -- [BaptisteCbl](https://github.com/BaptisteCbl) -- [COPS: Large-Scale Optimization Problems](https://www.mcs.anl.gov/~more/cops) and [COPSBenchmark.jl](https://github.com/MadNLP/COPSBenchmark.jl) - [BOCOP - A collection of examples](https://project.inria.fr/bocop/files/2017/05/Examples-BOCOP.pdf) +- [COPS: Large-Scale Optimization Problems](https://www.mcs.anl.gov/~more/cops) and [COPSBenchmark.jl](https://github.com/MadNLP/COPSBenchmark.jl) ## Reproducibility diff --git a/docs/src/problems-introduction.md b/docs/src/problems-introduction.md index e7c69347..36efbf86 100644 --- a/docs/src/problems-introduction.md +++ b/docs/src/problems-introduction.md @@ -97,11 +97,11 @@ For each problem, additional data is provided in the [MetaData](https://github.c OptimalControlProblems.metadata ``` -To list all metadata, use `metadata`. To access the metadata of a specific problem, for example `chain`, run: +To list all metadata, use `metadata()`. To access the metadata of a specific problem, for example `chain`, run: ```@example main using OptimalControlProblems -metadata[:chain] +metadata(:chain) ``` ## Problems characteristics @@ -179,29 +179,25 @@ We detail below the characteristics of the optimal control problems and their as end # - push!(data_ocp, - ( - Problem=problem, - State=state_dimension(ocp), - Control=control_dimension(ocp), - Variable=variable_dimension(ocp), - Cost=cost, - FinalTime=final_time, - Constraints=constraints, - ) - ) + push!(data_ocp,( + Problem=problem, + State=state_dimension(ocp), + Control=control_dimension(ocp), + Variable=variable_dimension(ocp), + Cost=cost, + FinalTime=final_time, + Constraints=constraints, + )) # - N = metadata[problem][:N] # get default number of steps - - push!(data_nlp, - ( - Problem=problem, - Steps=N, - Variables=get_nvar(nlp), - Constraints=get_ncon(nlp), - ) - ) + N = metadata(problem)[:grid_size] # get default number of steps + + push!(data_nlp,( + Problem=problem, + Steps=N, + Variables=get_nvar(nlp), + Constraints=get_ncon(nlp), + )) end ``` diff --git a/docs/src/tutorial-get.md b/docs/src/tutorial-get.md index 3b93e9b4..86b97c86 100644 --- a/docs/src/tutorial-get.md +++ b/docs/src/tutorial-get.md @@ -31,17 +31,33 @@ The `nlp` model represents the nonlinear programming problem (NLP) obtained afte You also have access to the DOCP model, which corresponds to the discretised optimal control problem. Roughly speaking, the DOCP model is the union of the NLP and OCP models. For more details, see this [tutorial](@extref Tutorials Discretization-and-NLP-problem) or the documentation of [`CTDirect.DOCP`](@extref). To get the OCP model: - ```julia + ```@example main_oc ocp = ocp_model(docp) + nothing # hide ``` -!!! note +You can pass any `description` and `kwargs` of [`CTDirect.direct_transcription`](@extref) to the `beam` problem or any other. - You can pass any `description` and `kwargs` of [`CTDirect.direct_transcription`](@extref) to the `beam` problem or any other. +```@example main_oc +docp = beam(OptimalControlBackend(), :adnlp; grid_size=100, disc_method=:euler) +nothing # hide +``` - ```julia - docp = beam(OptimalControlBackend(), :madnlp; grid_size=100, disc_method=:euler) - ``` +You can also replace any default parameter value: + +```@example main_oc +docp = beam(OptimalControlBackend(); parameters=(tf=2, )) +nothing # hide +``` + +To get the list of `:beam` parameters and the default values, make: + +```@example main_oc +metadata(:beam)[:parameters] +nothing # hide +``` + +To have the description of the Beam problem parameters, either check the Beam [page](@ref description-beam) or the [code](https://github.com/control-toolbox/OptimalControlProblems.jl/blob/main/ext/OptimalControlModels/beam.jl). ### Number of variables, constraints, and nonzeros @@ -85,7 +101,7 @@ println("nnzh = ", nnzh) The (default) number of steps $N$ is stored in the metadata: ```@example main_oc -metadata[:beam][:N] +N = metadata(:beam)[:grid_size] ``` !!! note @@ -94,12 +110,12 @@ metadata[:beam][:N] Each problem can be parameterised by the number of steps: ```@example main_oc -docp = beam(OptimalControlBackend(); N=100) +docp = beam(OptimalControlBackend(); grid_size=100) get_nvar(nlp_model(docp)) ``` ```@example main_oc -docp = beam(OptimalControlBackend(); N=200) +docp = beam(OptimalControlBackend(); grid_size=200) get_nvar(nlp_model(docp)) ``` @@ -117,13 +133,14 @@ Then, to obtain the JuMP model of the beam problem, run: nlp = beam(JuMPBackend()) ``` -!!! note - For details on how to interact with the JuMP model, see the [JuMP documentation](https://jump.dev/JuMP.jl). In particular, you can pass any arguments and keyword arguments of [`JuMP.Model`](@extref) to the `beam` problem or any other. +For details on how to interact with the JuMP model, see the [JuMP documentation](https://jump.dev/JuMP.jl). In particular, you can pass any arguments and keyword arguments of [`JuMP.Model`](@extref) to the `beam` problem or any other. - ```julia - using Ipopt - nlp = beam(JuMPBackend(), Ipopt.Optimizer; add_bridges=true) - ``` +```julia +using Ipopt +nlp = beam(JuMPBackend(), Ipopt.Optimizer; add_bridges=true) +``` !!! note - You can also transform the JuMP model into a [`NLPModelsJuMP.MathOptNLPModel`](@extref) and then use all the API of [NLPModels.jl](https://github.com/JuliaSmoothOptimizers/NLPModels.jl). See this [tutorial](https://jso.dev/NLPModelsJuMP.jl/dev/tutorial) for more details. + + - As with `OptimalControlBackend`, you can replace any default parameter value. + - You can transform the JuMP model into a [`NLPModelsJuMP.MathOptNLPModel`](@extref) and then use all the API of [NLPModels.jl](https://github.com/JuliaSmoothOptimizers/NLPModels.jl). See this [tutorial](https://jso.dev/NLPModelsJuMP.jl/dev/tutorial) for more details. \ No newline at end of file diff --git a/docs/src/tutorial-solve.md b/docs/src/tutorial-solve.md index 63f711fe..d516aa3f 100644 --- a/docs/src/tutorial-solve.md +++ b/docs/src/tutorial-solve.md @@ -78,7 +78,7 @@ From `ocp_sol` you can also plot the state, control, and costate trajectories. F ```@example main using Plots -plt = plot(ocp_sol; color=1, size=(800, 700), control_style=(label="OptimalControl", )) +plt = plot(ocp_sol; color=1, size=(700, 600), control_style=(label="OptimalControl", )) ``` ## Solving from a JuMP model @@ -122,12 +122,12 @@ objective_value(nlp) To get the time grid, state, control, and costate, but also to retrieve the number of iterations and the objective value, OptimalControlProblems provides the following getters: ```@example main -t = time_grid(:beam, nlp) # t0, ..., tN = tf -x = state(:beam, nlp) # function of time -u = control(:beam, nlp) # function of time -p = costate(:beam, nlp) # function of time -o = objective(:beam, nlp) # scalar objective value -i = iterations(:beam, nlp) # number of iteration +t = time_grid(nlp) # t0, ..., tN = tf +x = state(nlp) # function of time +u = control(nlp) # function of time +p = costate(nlp) # function of time +o = objective(nlp) # scalar objective value +i = iterations(nlp) # number of iteration tf = t[end] println("tf = ", tf) @@ -142,14 +142,14 @@ println("iterations: ", i) If the `problem` includes additional optimisation variables, such as the final time, you can retrieve them with: ```julia - v = variable(problem, nlp) + v = variable(nlp) ``` Now, we can add the state, costate, and control to the plot: ```@example main -n = length(metadata[:beam][:state_name]) # dimension of the state -m = length(metadata[:beam][:control_name]) # dimension of the control +n = state_dimension(nlp) +m = control_dimension(nlp) for i in 1:n # state plot!(plt[i], t, t -> x(t)[i]; color=2, linestyle=:dash, label=:none) diff --git a/ext/JuMPModels.jl b/ext/JuMPModels.jl index 74e532d3..39e7c04d 100644 --- a/ext/JuMPModels.jl +++ b/ext/JuMPModels.jl @@ -2,7 +2,19 @@ module JuMPModels using OptimalControlProblems using JuMP -import CTModels: CTModels, time_grid, state, control, costate, iterations +import CTModels: + CTModels, + time_grid, + state, + control, + costate, + iterations, + control_components, + control_dimension, + state_components, + state_dimension, + variable_components, + variable_dimension import ExaModels: ExaModels, variable, objective using DocStringExtensions using OrderedCollections: OrderedDict @@ -10,11 +22,175 @@ using OrderedCollections: OrderedDict # include problems files rel_path_problems = "JuMPModels" path = joinpath(dirname(@__FILE__), rel_path_problems) -files = filter(x -> x[(end - 2):end] == ".jl", readdir(path)) -for file in files - if file ≠ "JuMPModels.jl" - include(joinpath(rel_path_problems, file)) - end +list_of_problems = OptimalControlProblems.problems() +for problem in list_of_problems + include(joinpath(rel_path_problems, "$(problem).jl")) +end + +""" +$(TYPEDSIGNATURES) + +Return the list of costate component names stored in a JuMP model. + +# Arguments + +- `model::JuMP.GenericModel`: A JuMP model that contains the key `:costate_components`. + +# Returns + +- `Vector{String}`: The names of the costate components. + +# Example + +```julia-repl +julia> costate_components(model) +["∂x", "∂v", "∂θ", "∂ω"] +``` +""" +costate_components(model::JuMP.GenericModel) = model[:costate_components] + + +""" +$(TYPEDSIGNATURES) + +Return the list of state component names stored in a JuMP model. + +# Arguments + +- `model::JuMP.GenericModel`: A JuMP model that contains the key `:state_components`. + +# Returns + +- `Vector{String}`: The names of the state components. + +# Example + +```julia-repl +julia> OptimalControlProblems.state_components(model) +["x", "v", "θ", "ω"] +``` +""" +OptimalControlProblems.state_components(model::JuMP.GenericModel) = model[:state_components] + + +""" +$(TYPEDSIGNATURES) + +Return the list of control component names stored in a JuMP model. + +# Arguments + +- `model::JuMP.GenericModel`: A JuMP model that contains the key `:control_components`. + +# Returns + +- `Vector{String}`: The names of the control components. + +# Example + +```julia-repl +julia> OptimalControlProblems.control_components(model) +["Fex"] +``` +""" +OptimalControlProblems.control_components(model::JuMP.GenericModel) = model[:control_components] + + +""" +$(TYPEDSIGNATURES) + +Return the list of additional variable component names stored in a JuMP model. + +# Arguments + +- `model::JuMP.GenericModel`: A JuMP model that contains the key `:variable_components`. + +# Returns + +- `Vector{String}`: The names of the additional variable components. + +# Example + +```julia-repl +julia> OptimalControlProblems.variable_components(model) +["tf", "ddx"] +``` +""" +OptimalControlProblems.variable_components(model::JuMP.GenericModel) = model[:variable_components] + + +""" +$(TYPEDSIGNATURES) + +Return the number of state components in a JuMP model. + +# Arguments + +- `model::JuMP.GenericModel`: A JuMP model that contains the key `:state_components`. + +# Returns + +- `Int`: The number of state components. + +# Example + +```julia-repl +julia> OptimalControlProblems.state_dimension(model) +4 +``` +""" +function OptimalControlProblems.state_dimension(model::JuMP.GenericModel) + return length(model[:state_components]) +end + + +""" +$(TYPEDSIGNATURES) + +Return the number of control components in a JuMP model. + +# Arguments + +- `model::JuMP.GenericModel`: A JuMP model that contains the key `:control_components`. + +# Returns + +- `Int`: The number of control components. + +# Example + +```julia-repl +julia> OptimalControlProblems.control_dimension(model) +1 +``` +""" +function OptimalControlProblems.control_dimension(model::JuMP.GenericModel) + return length(model[:control_components]) +end + + +""" +$(TYPEDSIGNATURES) + +Return the number of additional variable components in a JuMP model. + +# Arguments + +- `model::JuMP.GenericModel`: A JuMP model that contains the key `:variable_components`. + +# Returns + +- `Int`: The number of additional variable components. + +# Example + +```julia-repl +julia> OptimalControlProblems.variable_dimension(model) +2 +``` +""" +function OptimalControlProblems.variable_dimension(model::JuMP.GenericModel) + return length(model[:variable_components]) end """ @@ -24,7 +200,6 @@ Compute the discretised time grid for a given optimal control problem solved wit # Arguments -- `problem::Symbol`: The name of the problem as defined in `metadata`. - `model::JuMP.GenericModel`: The JuMP model containing the problem solution. # Returns @@ -34,33 +209,11 @@ Compute the discretised time grid for a given optimal control problem solved wit # Example ```julia-repl -julia> tgrid = OptimalControlProblems.time_grid(:my_problem, model) +julia> tgrid = OptimalControlProblems.time_grid(model) 0.0:0.1:1.0 ``` """ -function OptimalControlProblems.time_grid(problem::Symbol, model::JuMP.GenericModel) - - # get N - x_vars = metadata[problem][:state_name] - x_jp_var = JuMP.value.(model[Symbol(x_vars[1])]) - N = length(x_jp_var) - 1 - - ## time grid: we assume that t0 = 0 - time_data, time_value_or_index = metadata[problem][:final_time] - - t0 = 0 - tf = if time_data == :fixed - time_value_or_index - elseif time_data == :free - v_vars = metadata[problem][:variable_name] - value.(model[Symbol(v_vars[time_value_or_index])]) - else - error("the final time must be :fixed or :free, not: ", time_data) - end - t_jp = range(t0, tf, N+1) - - return t_jp -end +OptimalControlProblems.time_grid(model::JuMP.GenericModel) = model[:time_grid]() """ $(TYPEDSIGNATURES) @@ -69,7 +222,6 @@ Extract and interpolate the state trajectory from a JuMP model of an optimal con # Arguments -- `problem::Symbol`: The name of the problem as defined in `metadata`. - `model::JuMP.GenericModel`: The JuMP model containing the problem solution. # Returns @@ -80,26 +232,25 @@ Extract and interpolate the state trajectory from a JuMP model of an optimal con # Example ```julia-repl -julia> x = OptimalControlProblems.state(:my_problem, model) +julia> x = OptimalControlProblems.state(model) julia> x(0.5) [0.23, 0.71] ``` """ -function OptimalControlProblems.state(problem::Symbol, model::JuMP.GenericModel) +function OptimalControlProblems.state(model::JuMP.GenericModel) # time grid - T = CTModels.time_grid(problem, model) + T = CTModels.time_grid(model) N = length(T) - 1 - # get dimension - state_names = metadata[problem][:state_name] - dim_x = length(state_names) + # get components and dimension + x_vars = state_components(model) + dim_x = state_dimension(model) # get state from the model X = zeros(N + 1, dim_x) for i in 1:dim_x - x_name = state_names[i] - X[:, i] = JuMP.value.(model[Symbol(x_name)]) + X[:, i] = JuMP.value.(model[Symbol(x_vars[i])]) end # interpolate @@ -120,7 +271,6 @@ Extract and interpolate the control trajectory from a JuMP model of an optimal c # Arguments -- `problem::Symbol`: The name of the problem as defined in `metadata`. - `model::JuMP.GenericModel`: The JuMP model containing the problem solution. # Returns @@ -131,26 +281,25 @@ Extract and interpolate the control trajectory from a JuMP model of an optimal c # Example ```julia-repl -julia> u = OptimalControlProblems.control(:my_problem, model) +julia> u = OptimalControlProblems.control(model) julia> u(0.25) 0.42 ``` """ -function OptimalControlProblems.control(problem::Symbol, model::JuMP.GenericModel) +function OptimalControlProblems.control(model::JuMP.GenericModel) # time grid - T = CTModels.time_grid(problem, model) + T = CTModels.time_grid(model) N = length(T) - 1 - # get dimension - control_names = metadata[problem][:control_name] - dim_u = length(control_names) + # get components and dimension + u_vars = control_components(model) + dim_u = control_dimension(model) # get control from the model U = zeros(N + 1, dim_u) for i in 1:dim_u - u_name = control_names[i] - U[:, i] = JuMP.value.(model[Symbol(u_name)]) + U[:, i] = JuMP.value.(model[Symbol(u_vars[i])]) end # interpolate @@ -171,7 +320,6 @@ Extract and interpolate the costate trajectory (dual variables associated with s # Arguments -- `problem::Symbol`: The name of the problem as defined in `metadata`. - `model::JuMP.GenericModel`: The JuMP model containing the problem solution. # Returns @@ -182,26 +330,25 @@ Extract and interpolate the costate trajectory (dual variables associated with s # Example ```julia-repl -julia> p = OptimalControlProblems.costate(:my_problem, model) +julia> p = OptimalControlProblems.costate(model) julia> p(0.75) [-0.12, 0.05] ``` """ -function OptimalControlProblems.costate(problem::Symbol, model::JuMP.GenericModel) +function OptimalControlProblems.costate(model::JuMP.GenericModel) # time grid - T = CTModels.time_grid(problem, model) + T = CTModels.time_grid( model) N = length(T) - 1 # get dimension - costate_names = metadata[problem][:costate_name] - dim_x = length(costate_names) + p_vars = costate_components(model) + dim_x = state_dimension(model) # get costate from the model P = zeros(N, dim_x) for i in 1:dim_x - p_name = costate_names[i] - P[:, i] = JuMP.dual.(model[Symbol(p_name)]) + P[:, i] = JuMP.dual.(model[Symbol(p_vars[i])]) end # interpolate @@ -226,37 +373,35 @@ Extract scalar or vector decision variables (such as final time when free) from # Arguments -- `problem::Symbol`: The name of the problem as defined in `metadata`. - `model::JuMP.GenericModel`: The JuMP model containing the problem solution. # Returns -- `var::Union{Nothing,Float64,Vector{Float64}}`: - - `nothing` if the problem defines no additional variables. +- `var::Union{Float64,Vector{Float64}}`: + - `Float64[]` if the problem defines no additional variables. - A scalar if there is one variable. - A vector if multiple variables exist. # Example ```julia-repl -julia> v = OptimalControlProblems.variable(:my_problem, model) +julia> v = OptimalControlProblems.variable(model) 1.5 ``` """ -function OptimalControlProblems.variable(problem::Symbol, model::JuMP.GenericModel) - variable_names = metadata[problem][:variable_name] +function OptimalControlProblems.variable(model::JuMP.GenericModel) - if isnothing(variable_names) - return nothing + # get components and dimension + v_vars = variable_components(model) + dim_v = variable_dimension(model) + if dim_v == 0 + return Float64[] end - dim_v = length(variable_names) - # get variable from the model v = zeros(dim_v) for i in 1:dim_v - v_name = variable_names[i] - v[i] = JuMP.value.(model[Symbol(v_name)]) + v[i] = JuMP.value.(model[Symbol(v_vars[i])]) end # force scalar output when dimension is 1 @@ -272,17 +417,16 @@ Get the objective value from a JuMP model. # Arguments -- `problem::Symbol`: The name of the problem as defined in `metadata`. - `model::JuMP.GenericModel`: The JuMP model containing the problem solution. # Example ```julia-repl -julia> OptimalControlProblems.objective(:my_problem, model) +julia> OptimalControlProblems.objective(model) 1.5 ``` """ -function OptimalControlProblems.objective(::Symbol, model::JuMP.GenericModel) +function OptimalControlProblems.objective(model::JuMP.GenericModel) return objective_value(model) end @@ -293,17 +437,16 @@ Get the number of iterations from a JuMP model. # Arguments -- `problem::Symbol`: The name of the problem as defined in `metadata`. - `model::JuMP.GenericModel`: The JuMP model containing the problem solution. # Example ```julia-repl -julia> OptimalControlProblems.iterations(:my_problem, model) +julia> OptimalControlProblems.iterations(model) 20 ``` """ -function OptimalControlProblems.iterations(::Symbol, model::JuMP.GenericModel) +function OptimalControlProblems.iterations(model::JuMP.GenericModel) return barrier_iterations(model) end diff --git a/ext/JuMPModels/beam.jl b/ext/JuMPModels/beam.jl index cddc2e21..a2efc021 100644 --- a/ext/JuMPModels/beam.jl +++ b/ext/JuMPModels/beam.jl @@ -8,7 +8,7 @@ The problem is formulated as in the BOCOP repository. # Arguments - `::JuMPBackend`: Placeholder type to specify the JuMP backend or solver interface. -- `N::Int=500`: (Keyword) Number of discretisation steps for the time grid. +- `grid_size::Int=500`: (Keyword) Number of discretisation steps for the time grid. # Returns @@ -28,23 +28,43 @@ julia> model = OptimalControlProblems.beam(JuMPBackend(); N=100) - Problem formulation available at: https://github.com/control-toolbox/bocop/tree/main/bocop """ function OptimalControlProblems.beam( - ::JuMPBackend, args...; N::Int=steps_number_data(:beam), kwargs... + ::JuMPBackend, args...; + grid_size::Int=grid_size_data(:beam), + parameters::Union{Nothing, NamedTuple}=nothing, + kwargs... ) # parameters - tf = final_time_data(:beam) - step = tf / N # t0 = 0 + params = parameters_data(:beam, parameters) + t0 = params[:t0] + tf = params[:tf] + x₁_l = params[:x₁_l] + x₁_u = params[:x₁_u] + x₁_t0 = params[:x₁_t0] + x₂_t0 = params[:x₂_t0] + x₁_tf = params[:x₁_tf] + x₂_tf = params[:x₂_tf] # model model = JuMP.Model(args...; kwargs...) + # metadata: required + model[:time_grid] = () -> range(t0, tf, grid_size+1) # tf is a fixed + model[:state_components] = ["x₁", "x₂"] + model[:costate_components] = ["∂x₁", "∂x₂"] + model[:control_components] = ["u"] + model[:variable_components] = String[] + + # N = grid_size + @expression(model, N, grid_size) + # variables and initial guess @variables( model, begin - 0.0 <= x1[0:N] <= 0.1, (start = 0.05) - x2[0:N], (start = 0.1) - u[0:N], (start = 0.1) + x₁_l ≤ x₁[0:N] ≤ x₁_u, (start = 0.05) + x₂[0:N], (start = 0.1) + u[0:N], (start = 0.1) end ) @@ -52,24 +72,38 @@ function OptimalControlProblems.beam( @constraints( model, begin - x1[0] == 0 - x2[0] == 1 - x1[N] == 0 - x2[N] == -1 + x₁[0] == x₁_t0 + x₂[0] == x₂_t0 + x₁[N] == x₁_tf + x₂[N] == x₂_tf end ) # dynamics + @expressions( + model, + begin + # + Δt, (tf - t0) / N + + # dynamics + dx₁[i = 0:N], x₂[i] + dx₂[i = 0:N], u[i] + + # objective + dc[i = 0:N], u[i]^2 + end + ) @constraints( model, begin - ∂x1[i = 1:N], x1[i] == x1[i - 1] + 0.5 * step * (x2[i] + x2[i - 1]) - ∂x2[i = 1:N], x2[i] == x2[i - 1] + 0.5 * step * (u[i] + u[i - 1]) + ∂x₁[i = 1:N], x₁[i] == x₁[i - 1] + 0.5 * Δt * (dx₁[i] + dx₁[i - 1]) + ∂x₂[i = 1:N], x₂[i] == x₂[i - 1] + 0.5 * Δt * (dx₂[i] + dx₂[i - 1]) end ) # objective - @objective(model, Min, 0.5 * step * sum(u[i]^2 + u[i - 1]^2 for i in 1:N)) + @objective(model, Min, 0.5 * Δt * sum(dc[i] + dc[i - 1] for i in 1:N)) return model end diff --git a/ext/JuMPModels/bioreactor.jl b/ext/JuMPModels/bioreactor.jl index 68613873..8efa0c4f 100644 --- a/ext/JuMPModels/bioreactor.jl +++ b/ext/JuMPModels/bioreactor.jl @@ -11,7 +11,7 @@ The objective is to minimise a cost function derived from the system dynamics. # Arguments - `::JuMPBackend`: Specifies the backend for building the JuMP model. -- `N::Int=500`: (Keyword) Number of discretisation steps in the time grid. +- `grid_size::Int=500`: (Keyword) Number of discretisation steps in the time grid. # Returns @@ -31,31 +31,57 @@ julia> model = OptimalControlProblems.bioreactor(JuMPBackend(); N=100) - [control-toolbox/bocop](https://github.com/control-toolbox/bocop/tree/main/bocop) """ function OptimalControlProblems.bioreactor( - ::JuMPBackend, args...; N::Int=steps_number_data(:bioreactor), kwargs... + ::JuMPBackend, args...; + grid_size::Int=grid_size_data(:bioreactor), + parameters::Union{Nothing, NamedTuple}=nothing, + kwargs... ) # parameters - β = 1 - c = 2 - γ = 1 - halfperiod = 5 - Ks = 0.05 - μ2m = 0.1 - μbar = 1 - r = 0.005 - T = final_time_data(:bioreactor) + params = parameters_data(:bioreactor, parameters) + t0 = params[:t0] + tf = params[:tf] + β = params[:β] + c = params[:c] + γ = params[:γ] + halfperiod = params[:halfperiod] + Ks = params[:Ks] + μ2m = params[:μ2m] + μbar = params[:μbar] + r = params[:r] + y_l = params[:y_l] + s_l = params[:s_l] + b_l = params[:b_l] + u_l = params[:u_l] + u_u = params[:u_u] + y_t0_l = params[:y_t0_l] + y_t0_u = params[:y_t0_u] + s_t0_l = params[:s_t0_l] + s_t0_u = params[:s_t0_u] + b_t0_l = params[:b_t0_l] + b_t0_u = params[:b_t0_u] # model model = JuMP.Model(args...; kwargs...) + # metadata: required + model[:time_grid] = () -> range(t0, tf, grid_size+1) # tf is a fixed + model[:state_components] = ["y", "s", "b"] + model[:costate_components] = ["∂y", "∂s", "∂b"] + model[:control_components] = ["u"] + model[:variable_components] = String[] + + # N = grid_size + @expression(model, N, grid_size) + # variables and initial guess @variables( model, begin - y[0:N] >= 0, (start = 50) - s[0:N] >= 0, (start = 50) - b[0:N] >= 0.001, (start = 50) - 0 <= u[0:N] <= 1, (start = 0.5) + y[0:N] ≥ y_l, (start = 50) + s[0:N] ≥ s_l, (start = 50) + b[0:N] ≥ b_l, (start = 50) + u_l ≤ u[0:N] ≤ u_u, (start = 0.5) end ) @@ -63,9 +89,9 @@ function OptimalControlProblems.bioreactor( @constraints( model, begin - 0.05 <= y[0] <= 0.25 - 0.5 <= s[0] <= 5 - 0.5 <= b[0] <= 3 + y_t0_l ≤ y[0] ≤ y_t0_u + s_t0_l ≤ s[0] ≤ s_t0_u + b_t0_l ≤ b[0] ≤ b_t0_u end ) @@ -73,15 +99,14 @@ function OptimalControlProblems.bioreactor( @expressions( model, begin - # - step, T / N + Δt, (tf-t0) / N # intermediate variables growth[k = 0:N], μ2m * s[k] / (s[k] + Ks) μ2[k = 0:N], growth[k] - days[k = 0:N], (k * step) / (halfperiod * 2) + days[k = 0:N], (k * Δt) / (halfperiod * 2) tau[k = 0:N], (days[k] - floor(days[k])) * 2π light[k = 0:N], max(0, sin(tau[k]))^2 μ[k = 0:N], light[k] * μbar @@ -92,21 +117,21 @@ function OptimalControlProblems.bioreactor( db[k = 0:N], (μ2[k] - u[k] * β) * b[k] # objective - dc[k = 0:N], -μ2[k] * b[k] / (β + c) + dc[k = 0:N], μ2[k] * b[k] / (β + c) end ) @constraints( model, begin - ∂y[k = 1:N], y[k] == y[k - 1] + 0.5 * step * (dy[k] + dy[k - 1]) - ∂s[k = 1:N], s[k] == s[k - 1] + 0.5 * step * (ds[k] + ds[k - 1]) - ∂b[k = 1:N], b[k] == b[k - 1] + 0.5 * step * (db[k] + db[k - 1]) + ∂y[k = 1:N], y[k] == y[k - 1] + 0.5 * Δt * (dy[k] + dy[k - 1]) + ∂s[k = 1:N], s[k] == s[k - 1] + 0.5 * Δt * (ds[k] + ds[k - 1]) + ∂b[k = 1:N], b[k] == b[k - 1] + 0.5 * Δt * (db[k] + db[k - 1]) end ) # objective - @objective(model, Min, 0.5 * step * sum(dc[k] + dc[k - 1] for k in 1:N)) + @objective(model, Max, 0.5 * Δt * sum(dc[k] + dc[k - 1] for k in 1:N)) return model end diff --git a/ext/JuMPModels/cart_pendulum.jl b/ext/JuMPModels/cart_pendulum.jl index 517b42df..bbe158a5 100644 --- a/ext/JuMPModels/cart_pendulum.jl +++ b/ext/JuMPModels/cart_pendulum.jl @@ -8,7 +8,7 @@ The system dynamics, constraints, and objective are discretised over `N` steps. # Arguments - `::JuMPBackend`: Specifies the backend for building the JuMP model. -- `N::Int=500`: (Keyword) Number of discretisation steps in the time grid. +- `grid_size::Int=500`: (Keyword) Number of discretisation steps in the time grid. # Returns @@ -28,33 +28,56 @@ julia> model = OptimalControlProblems.cart_pendulum(JuMPBackend(); N=200) - [Cart–Pendulum Optimal Control Problem](https://arxiv.org/pdf/2303.16746) """ function OptimalControlProblems.cart_pendulum( - ::JuMPBackend, args...; N::Int=steps_number_data(:cart_pendulum), kwargs... + ::JuMPBackend, args...; grid_size::Int=grid_size_data(:cart_pendulum), + parameters::Union{Nothing, NamedTuple}=nothing, + kwargs... ) # parameters - g = 9.81 # gravitation [m/s^2] - L = 1 # pendulum length [m] - m = 1 # pendulum mass [kg] + params = parameters_data(:cart_pendulum, parameters) + t0 = params[:t0] + g = params[:g] + L = params[:L] + m = params[:m] I = m * L^2 / 12 # pendulum moment of inertia - mcart = 0.5 # cart mass [kg] - max_f = 5 - max_x = 1 - max_v = 2 - + mcart = params[:mcart] + Fex_l = params[:Fex_l] + Fex_u = params[:Fex_u] + x_l = params[:x_l] + x_u = params[:x_u] + v_l = params[:v_l] + v_u = params[:v_u] + tf_l = params[:tf_l] + x_t0 = params[:x_t0] + θ_t0 = params[:θ_t0] + ω_t0 = params[:ω_t0] + θ_tf = params[:θ_tf] + ω_tf = params[:ω_tf] + # model model = JuMP.Model(args...; kwargs...) + # metadata: required + model[:time_grid] = () -> range(t0, value(model[:tf]), grid_size+1) # tf is a free + model[:state_components] = ["x", "v", "θ", "ω"] + model[:costate_components] = ["∂x", "∂v", "∂θ", "∂ω"] + model[:control_components] = ["Fex"] + model[:variable_components] = ["tf", "ddx"] + + # N = grid_size + @expression(model, N, grid_size) + # variables and initial guess @variables( model, begin - 0.1 <= tf, (start = 1.0) - ddx, (start = 0.1) - -max_x <= x[0:N] <= max_x, (start = 0.1) - -max_v <= v[0:N] <= max_v, (start = 0.1) - θ[0:N], (start = 0.1) - ω[0:N], (start = 0.1) - -max_f <= Fex[0:N] <= max_f, (start = 0.1) + tf ≥ tf_l, (start = 1.0) + ddx, (start = 0.1) + x_l ≤ x[0:N] ≤ x_u, (start = 0.1) + v_l ≤ v[0:N] ≤ v_u, (start = 0.1) + θ[0:N], (start = 0.1) + ω[0:N], (start = 0.1) + Fex_l ≤ Fex[0:N] ≤ Fex_u, (start = 0.1) end ) @@ -62,11 +85,11 @@ function OptimalControlProblems.cart_pendulum( @constraints( model, begin - x[0] == 0 - θ[0] == 0 - ω[0] == 0 - θ[N] == π - ω[N] == 0 + x[0] == x_t0 + θ[0] == θ_t0 + ω[0] == ω_t0 + θ[N] == θ_tf + ω[N] == ω_tf end ) @@ -74,7 +97,7 @@ function OptimalControlProblems.cart_pendulum( @expressions( model, begin - step, tf / N + Δt, (tf - t0) / N α_ddx[i = 0:N], 1 / (I + 0.25 * m * L^2) * 0.5 * L * m * (-ddx * cos(θ[i]) - g * sin(θ[i])) @@ -90,19 +113,20 @@ function OptimalControlProblems.cart_pendulum( J, mcart c[i = 0:N], eq[i] - J * ddx + dx[i = 0:N], v[i] dv[i = 0:N], -1 / J * c[i] - dω[i = 0:N], - 1 / (I + 0.25 * m * L^2) * 0.5 * L * m * (-dv[i] * cos(θ[i]) - g * sin(θ[i])) + dθ[i = 0:N], ω[i] + dω[i = 0:N], 1 / (I + 0.25 * m * L^2) * 0.5 * L * m * (-dv[i] * cos(θ[i]) - g * sin(θ[i])) end ) @constraints( model, begin - ∂x[k = 1:N], x[k] == x[k - 1] + 0.5 * step * (v[k] + v[k - 1]) - ∂v[k = 1:N], v[k] == v[k - 1] + 0.5 * step * (dv[k] + dv[k - 1]) - ∂θ[k = 1:N], θ[k] == θ[k - 1] + 0.5 * step * (ω[k] + ω[k - 1]) - ∂ω[k = 1:N], ω[k] == ω[k - 1] + 0.5 * step * (dω[k] + dω[k - 1]) + ∂x[k = 1:N], x[k] == x[k - 1] + 0.5 * Δt * (dx[k] + dx[k - 1]) + ∂v[k = 1:N], v[k] == v[k - 1] + 0.5 * Δt * (dv[k] + dv[k - 1]) + ∂θ[k = 1:N], θ[k] == θ[k - 1] + 0.5 * Δt * (dθ[k] + dθ[k - 1]) + ∂ω[k = 1:N], ω[k] == ω[k - 1] + 0.5 * Δt * (dω[k] + dω[k - 1]) end ) diff --git a/ext/JuMPModels/chain.jl b/ext/JuMPModels/chain.jl index fefec281..690f261e 100644 --- a/ext/JuMPModels/chain.jl +++ b/ext/JuMPModels/chain.jl @@ -8,7 +8,7 @@ The formulation follows a standard optimal control approach with discretised dyn # Arguments - `::JuMPBackend`: Specifies the backend for building the JuMP model. -- `N::Int=500`: (Keyword) Number of discretisation steps in the time grid. +- `grid_size::Int=500`: (Keyword) Number of discretisation steps in the time grid. # Returns @@ -28,14 +28,23 @@ julia> model = OptimalControlProblems.chain(JuMPBackend(); N=300) - [COPS Benchmark Problems – Hanging Chain](https://www.mcs.anl.gov/~more/cops/) """ function OptimalControlProblems.chain( - ::JuMPBackend, args...; N::Int=steps_number_data(:chain), kwargs... + ::JuMPBackend, args...; grid_size::Int=grid_size_data(:chain), + parameters::Union{Nothing, NamedTuple}=nothing, + kwargs... ) # parameters - L = 4 - a = 1 - b = 3 - tf = final_time_data(:chain) + params = parameters_data(:chain, parameters) + t0 = params[:t0] + tf = params[:tf] + L = params[:L] + a = params[:a] + b = params[:b] + x₁_t0 = a + x₂_t0 = params[:x₂_t0] + x₃_t0 = params[:x₃_t0] + x₁_tf = b + x₃_tf = L # tmin = b > a ? 1 / 4 : 3 / 4 @@ -43,11 +52,21 @@ function OptimalControlProblems.chain( # model model = JuMP.Model(args...; kwargs...) + # metadata: required + model[:time_grid] = () -> range(t0, tf, grid_size+1) # tf is a fixed + model[:state_components] = ["x₁", "x₂", "x₃"] + model[:costate_components] = ["∂x₁", "∂x₂", "∂x₃"] + model[:control_components] = ["u"] + model[:variable_components] = String[] + + # N = grid_size + @expression(model, N, grid_size) + # time @expressions( model, begin - t[k = 0:N], k * tf / N + t[k = 0:N], t0 + k * (tf-t0) / N end ) @@ -55,27 +74,26 @@ function OptimalControlProblems.chain( @variables( model, begin - u[k = 0:N], (start = 4 * abs(b - a) * (t[k] / tf - tmin)) - x1[k = 0:N], - (start = 4 * abs(b - a) * t[k] / tf * (0.5 * t[k] / tf - tmin) + a) - x2[k = 0:N], + u[k = 0:N], (start = 4 * abs(b - a) * ((t[k] - t0) / (tf - t0) - tmin)) + x₁[k = 0:N], (start = 4 * abs(b - a) * (t[k] - t0) / (tf - t0) * (0.5 * (t[k] - t0) / (tf - t0) - tmin) + a) + x₂[k = 0:N], ( start = - (4 * abs(b - a) * t[k] / tf * (0.5 * t[k] / tf - tmin) + a) * - (4 * abs(b - a) * (t[k] / tf - tmin)) + (4 * abs(b - a) * (t[k] - t0) / (tf - t0) * (0.5 * (t[k] - t0) / (tf - t0) - tmin) + a) * + (4 * abs(b - a) * ((t[k] - t0) / (tf - t0) - tmin)) ) - x3[k = 0:N], (start = 4 * abs(b - a) * (t[k] / tf - tmin)) + x₃[k = 0:N], (start = 4 * abs(b - a) * ((t[k] - t0) / (tf - t0) - tmin)) end ) @constraints( model, begin - x1[0] == a - x2[0] == 0 - x3[0] == 0 - x1[N] == b - x3[N] == L + x₁[0] == x₁_t0 + x₂[0] == x₂_t0 + x₃[0] == x₃_t0 + x₁[N] == x₁_tf + x₃[N] == x₃_tf end ) @@ -83,23 +101,23 @@ function OptimalControlProblems.chain( @expressions( model, begin - step, tf / N - dx1[k = 0:N], u[k] - dx2[k = 0:N], x1[k] * √(1 + u[k]^2) - dx3[k = 0:N], √(1 + u[k]^2) + Δt, (tf - t0) / N + dx₁[k = 0:N], u[k] + dx₂[k = 0:N], x₁[k] * √(1 + u[k]^2) + dx₃[k = 0:N], √(1 + u[k]^2) end ) @constraints( model, begin - ∂x1[k = 1:N], x1[k] == x1[k - 1] + 0.5 * step * (dx1[k] + dx1[k - 1]) - ∂x2[k = 1:N], x2[k] == x2[k - 1] + 0.5 * step * (dx2[k] + dx2[k - 1]) - ∂x3[k = 1:N], x3[k] == x3[k - 1] + 0.5 * step * (dx3[k] + dx3[k - 1]) + ∂x₁[k = 1:N], x₁[k] == x₁[k - 1] + 0.5 * Δt * (dx₁[k] + dx₁[k - 1]) + ∂x₂[k = 1:N], x₂[k] == x₂[k - 1] + 0.5 * Δt * (dx₂[k] + dx₂[k - 1]) + ∂x₃[k = 1:N], x₃[k] == x₃[k - 1] + 0.5 * Δt * (dx₃[k] + dx₃[k - 1]) end ) - @objective(model, Min, x2[N]) + @objective(model, Min, x₂[N]) return model end diff --git a/ext/JuMPModels/dielectrophoretic_particle.jl b/ext/JuMPModels/dielectrophoretic_particle.jl index cf98a71d..4a4eacf1 100644 --- a/ext/JuMPModels/dielectrophoretic_particle.jl +++ b/ext/JuMPModels/dielectrophoretic_particle.jl @@ -8,7 +8,7 @@ The system dynamics are discretised over `N` steps, with the final time optimise # Arguments - `::JuMPBackend`: Specifies the backend for building the JuMP model. -- `N::Int=500`: (Keyword) Number of discretisation steps in the time grid. +- `grid_size::Int=500`: (Keyword) Number of discretisation steps in the time grid. # Returns @@ -29,53 +29,76 @@ julia> model = OptimalControlProblems.dielectrophoretic_particle(JuMPBackend(); IEEE Transactions on Automatic Control, 51(7), 1100–1114. """ function OptimalControlProblems.dielectrophoretic_particle( - ::JuMPBackend, args...; N::Int=steps_number_data(:dielectrophoretic_particle), kwargs... + ::JuMPBackend, args...; grid_size::Int=grid_size_data(:dielectrophoretic_particle), + parameters::Union{Nothing, NamedTuple}=nothing, + kwargs... ) # parameters - x0 = 1 - xf = 2 - α = -0.75 - c = 1 - + params = parameters_data(:dielectrophoretic_particle, parameters) + t0 = params[:t0] + x_t0 = params[:x_t0] + y_t0 = params[:y_t0] + x_tf = params[:x_tf] + α = params[:α] + c = params[:c] + u_l = params[:u_l] + u_u = params[:u_u] + tf_l = params[:tf_l] + # model model = JuMP.Model(args...; kwargs...) - # state, control and variable (final time) - @variable(model, x[0:N], start = 1) - @variable(model, y[0:N], start = 1) - @variable(model, -1 <= u[0:N] <= 1, start = 0.1) - @variable(model, 0 <= tf, start = 5) + # metadata: required + model[:time_grid] = () -> range(t0, value(model[:tf]), grid_size+1) # tf is a free + model[:state_components] = ["x", "y"] + model[:costate_components] = ["∂x", "∂y"] + model[:control_components] = ["u"] + model[:variable_components] = ["tf"] - # Objective - @objective(model, Min, tf) + # N = grid_size + @expression(model, N, grid_size) - # Dynamics - @expressions( + # state, control and variable (final time) + @variables( model, begin - step, tf / N - dx[k = 0:N], y[k] * u[k] + α * u[k]^2 - dy[k = 0:N], -c * y[k] + u[k] + x[0:N], (start = 1) + y[0:N], (start = 1) + u_l ≤ u[0:N] ≤ u_u, (start = 0.1) + tf ≥ tf_l, (start = 5) end ) - # Collocation + + # Boundary constraints @constraints( model, begin - ∂x[k = 1:N], x[k] == x[k - 1] + 0.5 * step * (dx[k] + dx[k - 1]) - ∂y[k = 1:N], y[k] == y[k - 1] + 0.5 * step * (dy[k] + dy[k - 1]) + x[0] == x_t0 + y[0] == y_t0 + x[N] == x_tf + end + ) + + # Dynamics + @expressions( + model, + begin + Δt, (tf - t0) / N + dx[k = 0:N], y[k] * u[k] + α * u[k]^2 + dy[k = 0:N], -c * y[k] + u[k] end ) - # Boundary constraints @constraints( model, begin - x[0] == x0 - x[N] == xf - y[0] == 0 + ∂x[k = 1:N], x[k] == x[k - 1] + 0.5 * Δt * (dx[k] + dx[k - 1]) + ∂y[k = 1:N], y[k] == y[k - 1] + 0.5 * Δt * (dy[k] + dy[k - 1]) end ) + # Objective + @objective(model, Min, tf) + return model end diff --git a/ext/JuMPModels/double_oscillator.jl b/ext/JuMPModels/double_oscillator.jl index c7c60c1c..676db24f 100644 --- a/ext/JuMPModels/double_oscillator.jl +++ b/ext/JuMPModels/double_oscillator.jl @@ -8,7 +8,7 @@ The system dynamics are discretised over `N` steps, with collocation constraints # Arguments - `::JuMPBackend`: Specifies the backend for building the JuMP model. -- `N::Int=500`: (Keyword) Number of discretisation steps in the time grid. +- `grid_size::Int=500`: (Keyword) Number of discretisation steps in the time grid. # Returns @@ -29,29 +29,47 @@ julia> model = OptimalControlProblems.double_oscillator(JuMPBackend(); N=200) IFAC-PapersOnLine, 51(2), 49–54. """ function OptimalControlProblems.double_oscillator( - ::JuMPBackend, args...; N::Int=steps_number_data(:double_oscillator), kwargs... + ::JuMPBackend, args...; grid_size::Int=grid_size_data(:double_oscillator), + parameters::Union{Nothing, NamedTuple}=nothing, + kwargs... ) # parameters - m1 = 100 # [kg] - m2 = 2 # [kg] - c = 0.5 # [Ns/m] - k1 = 100 # [N/m] - k2 = 3 # [N/m] - tf = final_time_data(:double_oscillator) + params = parameters_data(:double_oscillator, parameters) + t0 = params[:t0] + tf = params[:tf] + m1 = params[:m1] + m2 = params[:m2] + c = params[:c] + k1 = params[:k1] + k2 = params[:k2] + u_l = params[:u_l] + u_u = params[:u_u] + x₁_t0 = params[:x₁_t0] + x₂_t0 = params[:x₂_t0] # model model = JuMP.Model(args...; kwargs...) + # metadata: required + model[:time_grid] = () -> range(t0, tf, grid_size+1) # tf is a fixed + model[:state_components] = ["x₁", "x₂", "x₃", "x₄"] + model[:costate_components] = ["∂x₁", "∂x₂", "∂x₃", "∂x₄"] + model[:control_components] = ["u"] + model[:variable_components] = String[] + + # N = grid_size + @expression(model, N, grid_size) + # state, control and initial guess @variables( model, begin - x1[0:N], (start = 0.1) - x2[0:N], (start = 0.1) - x3[0:N], (start = 0.1) - x4[0:N], (start = 0.1) - -1 <= u[0:N] <= 1, (start = 0.1) + x₁[0:N], (start = 0.1) + x₂[0:N], (start = 0.1) + x₃[0:N], (start = 0.1) + x₄[0:N], (start = 0.1) + u_l ≤ u[0:N] ≤ u_u, (start = 0.1) end ) @@ -59,8 +77,8 @@ function OptimalControlProblems.double_oscillator( @constraints( model, begin - x1[0] == 0 - x2[0] == 0 + x₁[0] == x₁_t0 + x₂[0] == x₂_t0 end ) @@ -68,35 +86,34 @@ function OptimalControlProblems.double_oscillator( @expressions( model, begin - # - step, tf / N - t[k = 0:N], k * tf / N - F[k = 0:N], sin(t[k] * 2π / tf) + Δt, (tf - t0) / N + t[k = 0:N], t0 + k * (tf-t0) / N + F[k = 0:N], sin((t[k] - t0) * 2π / (tf - t0)) # dynamics - dx1[k = 0:N], x3[k] - dx2[k = 0:N], x4[k] - dx3[k = 0:N], -(k1 + k2) / m1 * x1[k] + k2 / m1 * x2[k] + 1 / m1 * F[k] - dx4[k = 0:N], k2 / m2 * x1[k] - k2 / m2 * x2[k] - c * (1 - u[k]) / m2 * x4[k] + dx₁[k = 0:N], x₃[k] + dx₂[k = 0:N], x₄[k] + dx₃[k = 0:N], -(k1 + k2) / m1 * x₁[k] + k2 / m1 * x₂[k] + 1 / m1 * F[k] + dx₄[k = 0:N], k2 / m2 * x₁[k] - k2 / m2 * x₂[k] - c * (1 - u[k]) / m2 * x₄[k] # objective - dc[k = 0:N], 0.5 * (x1[k]^2 + x2[k]^2 + u[k]^2) + dc[k = 0:N], 0.5 * (x₁[k]^2 + x₂[k]^2 + u[k]^2) end ) @constraints( model, begin - ∂x1[k = 1:N], x1[k] == x1[k - 1] + 0.5 * step * (dx1[k] + dx1[k - 1]) - ∂x2[k = 1:N], x2[k] == x2[k - 1] + 0.5 * step * (dx2[k] + dx2[k - 1]) - ∂x3[k = 1:N], x3[k] == x3[k - 1] + 0.5 * step * (dx3[k] + dx3[k - 1]) - ∂x4[k = 1:N], x4[k] == x4[k - 1] + 0.5 * step * (dx4[k] + dx4[k - 1]) + ∂x₁[k = 1:N], x₁[k] == x₁[k - 1] + 0.5 * Δt * (dx₁[k] + dx₁[k - 1]) + ∂x₂[k = 1:N], x₂[k] == x₂[k - 1] + 0.5 * Δt * (dx₂[k] + dx₂[k - 1]) + ∂x₃[k = 1:N], x₃[k] == x₃[k - 1] + 0.5 * Δt * (dx₃[k] + dx₃[k - 1]) + ∂x₄[k = 1:N], x₄[k] == x₄[k - 1] + 0.5 * Δt * (dx₄[k] + dx₄[k - 1]) end ) # objective: trapeze rule - @objective(model, Min, 0.5 * step * sum(dc[k] + dc[k - 1] for k in 1:N)) + @objective(model, Min, 0.5 * Δt * sum(dc[k] + dc[k - 1] for k in 1:N)) return model end diff --git a/ext/JuMPModels/ducted_fan.jl b/ext/JuMPModels/ducted_fan.jl index 3208486d..c1b9dc67 100644 --- a/ext/JuMPModels/ducted_fan.jl +++ b/ext/JuMPModels/ducted_fan.jl @@ -8,7 +8,7 @@ The system is discretised over `N` steps, with collocation constraints enforcing # Arguments - `::JuMPBackend`: Specifies the backend for building the JuMP model. -- `N::Int=250`: (Keyword) Number of discretisation steps in the time grid. +- `grid_size::Int=250`: (Keyword) Number of discretisation steps in the time grid. # Returns @@ -29,50 +29,87 @@ julia> model = OptimalControlProblems.ducted_fan(JuMPBackend(); N=100) Optimal Control Applications and Methods, 30(6), 537–561. [GP2009] """ function OptimalControlProblems.ducted_fan( - ::JuMPBackend, args...; N::Int=steps_number_data(:ducted_fan), kwargs... + ::JuMPBackend, args...; grid_size::Int=grid_size_data(:ducted_fan), + parameters::Union{Nothing, NamedTuple}=nothing, + kwargs... ) # parameters - r = 0.2 # [m] - J = 0.05 # [kg.m2] - m = 2.2 # [kg] - mg = 4 # [N] - μ = 1000 + params = parameters_data(:ducted_fan, parameters) + t0 = params[:t0] + r = params[:r] + J = params[:J] + m = params[:m] + mg = params[:mg] + μ = params[:μ] + α_l = params[:α_l] + α_u = params[:α_u] + u₁_l = params[:u₁_l] + u₁_u = params[:u₁_u] + u₂_l = params[:u₂_l] + u₂_u = params[:u₂_u] + tf_l = params[:tf_l] + x₁_t0 = params[:x₁_t0] + v₁_t0 = params[:v₁_t0] + x₂_t0 = params[:x₂_t0] + v₂_t0 = params[:v₂_t0] + α_t0 = params[:α_t0] + vα_t0 = params[:vα_t0] + x₁_tf = params[:x₁_tf] + v₁_tf = params[:v₁_tf] + x₂_tf = params[:x₂_tf] + v₂_tf = params[:v₂_tf] + α_tf = params[:α_tf] + vα_tf = params[:vα_tf] # model model = JuMP.Model(args...; kwargs...) + # metadata: required + model[:time_grid] = () -> range(t0, value(model[:tf]), grid_size+1) # tf is a free + model[:state_components] = ["x₁", "v₁", "x₂", "v₂", "α", "vα"] + model[:costate_components] = ["∂x₁", "∂v₁", "∂x₂", "∂v₂", "∂α", "∂vα"] + model[:control_components] = ["u₁", "u₂"] + model[:variable_components] = ["tf"] + + # N = grid_size + @expression(model, N, grid_size) + # state, control, variable (final time) and initial guess - @variable(model, x₁[0:N], start = 0.1) - @variable(model, v₁[0:N], start = 0.1) - @variable(model, x₂[0:N], start = -0.1) - @variable(model, v₂[0:N], start = 0.1) - @variable(model, -deg2rad(30) <= α[0:N] <= deg2rad(30), start = 0.1) # radian - @variable(model, vα[0:N], start = 0.1) - @variable(model, -5 <= u₁[0:N] <= 5, start = 0.1) # [N] - @variable(model, 0 <= u₂[0:N] <= 17, start = 1) # [N] - @variable(model, 0.1 <= tf, start = 1.5) + @variables( + model, + begin + x₁[0:N], (start = 0.1) + v₁[0:N], (start = 0.1) + x₂[0:N], (start = -0.1) + v₂[0:N], (start = 0.1) + α_l ≤ α[0:N] ≤ α_u, (start = 0.1) # radian + vα[0:N], (start = 0.1) + u₁_l ≤ u₁[0:N] ≤ u₁_u, (start = 0.1) # [N] + u₂_l ≤ u₂[0:N] ≤ u₂_u, (start = 1) # [N] + tf ≥ tf_l, (start = 1.5) + end + ) # Boundary constraints @constraints( model, begin - # initial - x₁[0] == 0 - v₁[0] == 0 - x₂[0] == 0 - v₂[0] == 0 - α[0] == 0 - vα[0] == 0 + x₁[0] == x₁_t0 + v₁[0] == v₁_t0 + x₂[0] == x₂_t0 + v₂[0] == v₂_t0 + α[0] == α_t0 + vα[0] == vα_t0 # final - x₁[N] == 1 - v₁[N] == 0 - x₂[N] == 0 - v₂[N] == 0 - α[N] == 0 - vα[N] == 0 + x₁[N] == x₁_tf + v₁[N] == v₁_tf + x₂[N] == x₂_tf + v₂[N] == v₂_tf + α[N] == α_tf + vα[N] == vα_tf end ) @@ -80,9 +117,8 @@ function OptimalControlProblems.ducted_fan( @expressions( model, begin - # - step, tf / N + Δt, (tf - t0) / N # dynamics dx₁[k = 0:N], v₁[k] @@ -100,18 +136,18 @@ function OptimalControlProblems.ducted_fan( @constraints( model, begin - ∂x₁[k = 1:N], x₁[k] == x₁[k - 1] + 0.5 * step * (dx₁[k] + dx₁[k - 1]) - ∂v₁[k = 1:N], v₁[k] == v₁[k - 1] + 0.5 * step * (dv₁[k] + dv₁[k - 1]) - ∂x₂[k = 1:N], x₂[k] == x₂[k - 1] + 0.5 * step * (dx₂[k] + dx₂[k - 1]) - ∂v₂[k = 1:N], v₂[k] == v₂[k - 1] + 0.5 * step * (dv₂[k] + dv₂[k - 1]) - ∂α[k = 1:N], α[k] == α[k - 1] + 0.5 * step * (dα[k] + dα[k - 1]) - ∂vα[k = 1:N], vα[k] == vα[k - 1] + 0.5 * step * (dvα[k] + dvα[k - 1]) + ∂x₁[k = 1:N], x₁[k] == x₁[k - 1] + 0.5 * Δt * (dx₁[k] + dx₁[k - 1]) + ∂v₁[k = 1:N], v₁[k] == v₁[k - 1] + 0.5 * Δt * (dv₁[k] + dv₁[k - 1]) + ∂x₂[k = 1:N], x₂[k] == x₂[k - 1] + 0.5 * Δt * (dx₂[k] + dx₂[k - 1]) + ∂v₂[k = 1:N], v₂[k] == v₂[k - 1] + 0.5 * Δt * (dv₂[k] + dv₂[k - 1]) + ∂α[k = 1:N], α[k] == α[k - 1] + 0.5 * Δt * (dα[k] + dα[k - 1]) + ∂vα[k = 1:N], vα[k] == vα[k - 1] + 0.5 * Δt * (dvα[k] + dvα[k - 1]) end ) # objective @objective( - model, Min, (1 / tf) * 0.5 * step * sum(dc[k] + dc[k - 1] for k in 1:N) + (μ * tf) + model, Min, (1 / tf) * 0.5 * Δt * sum(dc[k] + dc[k - 1] for k in 1:N) + (μ * tf) ) return model diff --git a/ext/JuMPModels/electric_vehicle.jl b/ext/JuMPModels/electric_vehicle.jl index 18716527..ac707134 100644 --- a/ext/JuMPModels/electric_vehicle.jl +++ b/ext/JuMPModels/electric_vehicle.jl @@ -8,7 +8,7 @@ The system dynamics are discretised over `N` steps, and collocation constraints # Arguments - `::JuMPBackend`: Specifies the backend for building the JuMP model. -- `N::Int=500`: (Keyword) Number of discretisation steps in the time grid. +- `grid_size::Int=500`: (Keyword) Number of discretisation steps in the time grid. # Returns @@ -28,35 +28,60 @@ julia> model = OptimalControlProblems.electric_vehicle(JuMPBackend(); N=100) - Petit, N., & Sciarretta, A. (2011). *Optimal drive of electric vehicles using an inversion-based trajectory generation approach.* IFAC Proceedings Volumes, 44(1), 14519–14526. [PS2011] """ function OptimalControlProblems.electric_vehicle( - ::JuMPBackend, args...; N::Int=steps_number_data(:electric_vehicle), kwargs... + ::JuMPBackend, args...; grid_size::Int=grid_size_data(:electric_vehicle), + parameters::Union{Nothing, NamedTuple}=nothing, + kwargs... ) # parameters - tf = final_time_data(:electric_vehicle) - D = 10 - b1 = 1e0 - b2 = 1e0 - h0 = 0.1 - h1 = 1 - h2 = 1e-3 - α0, α1, α2, α3 = (3, 0.4, -1, 0.1) + params = parameters_data(:electric_vehicle, parameters) + t0 = params[:t0] + tf = params[:tf] + b1 = params[:b1] + b2 = params[:b2] + h0 = params[:h0] + h1 = params[:h1] + h2 = params[:h2] + α0 = params[:α0] + α1 = params[:α1] + α2 = params[:α2] + α3 = params[:α3] + x_t0 = params[:x_t0] + v_t0 = params[:v_t0] + x_tf = params[:x_tf] + v_tf = params[:v_tf] # model model = JuMP.Model(args...; kwargs...) + # metadata: required + model[:time_grid] = () -> range(t0, tf, grid_size+1) # tf is a fixed + model[:state_components] = ["x", "v"] + model[:costate_components] = ["∂x", "∂v"] + model[:control_components] = ["u"] + model[:variable_components] = String[] + + # N = grid_size + @expression(model, N, grid_size) + # state, control and initial guess - @variable(model, x[0:N], start = 0.1) - @variable(model, v[0:N], start = 0.1) - @variable(model, u[0:N], start = 0.1) + @variables( + model, + begin + x[0:N], (start = 0.1) + v[0:N], (start = 0.1) + u[0:N], (start = 0.1) + end + ) # boundary constraints @constraints( model, begin - x[0] == 0 - v[0] == 0 - x[N] == D - v[N] == 0 + x[0] == x_t0 + v[0] == v_t0 + x[N] == x_tf + v[N] == v_tf end ) @@ -64,9 +89,8 @@ function OptimalControlProblems.electric_vehicle( @expressions( model, begin - # - step, tf / N + Δt, (tf - t0) / N road[k = 0:N], α0 + α1 * x[k] + α2 * x[k]^2 + α3 * x[k]^3 # dynamics @@ -81,13 +105,13 @@ function OptimalControlProblems.electric_vehicle( @constraints( model, begin - ∂x[k = 1:N], x[k] == x[k - 1] + 0.5 * step * (dx[k - 1] + dx[k]) - ∂v[k = 1:N], v[k] == v[k - 1] + 0.5 * step * (dv[k - 1] + dv[k]) + ∂x[k = 1:N], x[k] == x[k - 1] + 0.5 * Δt * (dx[k - 1] + dx[k]) + ∂v[k = 1:N], v[k] == v[k - 1] + 0.5 * Δt * (dv[k - 1] + dv[k]) end ) # objective - @objective(model, Min, 0.5 * step * sum(dc[k] + dc[k - 1] for k in 1:N)) + @objective(model, Min, 0.5 * Δt * sum(dc[k] + dc[k - 1] for k in 1:N)) return model end diff --git a/ext/JuMPModels/glider.jl b/ext/JuMPModels/glider.jl index ab6f607f..d2fb88c1 100644 --- a/ext/JuMPModels/glider.jl +++ b/ext/JuMPModels/glider.jl @@ -8,7 +8,7 @@ The system dynamics are discretised over `N` steps, and collocation constraints # Arguments - `::JuMPBackend`: Specifies the backend for building the JuMP model. -- `N::Int=500`: (Keyword) Number of discretisation steps in the time grid. +- `grid_size::Int=500`: (Keyword) Number of discretisation steps in the time grid. # Returns @@ -28,41 +28,58 @@ julia> model = OptimalControlProblems.glider(JuMPBackend(); N=100) - Hang Glider Problem formulation as in: https://www.mcs.anl.gov/~more/cops/ """ function OptimalControlProblems.glider( - ::JuMPBackend, args...; N::Int=steps_number_data(:glider), kwargs... + ::JuMPBackend, args...; grid_size::Int=grid_size_data(:glider), + parameters::Union{Nothing, NamedTuple}=nothing, + kwargs... ) # parameters - x_0 = 0 - y_0 = 1000 - y_f = 900 - vx_0 = 13.23 - vx_f = 13.23 - vy_0 = -1.288 - vy_f = -1.288 - u_c = 2.5 - r_0 = 100 - m = 100 - g = 9.81 - c0 = 0.034 - c1 = 0.069662 - S = 14 - ρ = 1.13 - cL_min = 0 - cL_max = 1.4 + params = parameters_data(:glider, parameters) + t0 = params[:t0] + x_t0 = params[:x_t0] + y_t0 = params[:y_t0] + y_tf = params[:y_tf] + vx_t0 = params[:vx_t0] + vx_tf = params[:vx_tf] + vy_t0 = params[:vy_t0] + vy_tf = params[:vy_tf] + u_c = params[:u_c] + r_t0 = params[:r_t0] + m = params[:m] + g = params[:g] + c0 = params[:c0] + c1 = params[:c1] + S = params[:S] + ρ = params[:ρ] + cL_min = params[:cL_min] + cL_max = params[:cL_max] + tf_l = params[:tf_l] + x_l = params[:x_l] + vx_l = params[:vx_l] # model model = JuMP.Model(args...; kwargs...) + # metadata: required + model[:time_grid] = () -> range(t0, value(model[:tf]), grid_size+1) # tf is a free + model[:state_components] = ["x", "y", "vx", "vy"] + model[:costate_components] = ["∂x", "∂y", "∂vx", "∂vy"] + model[:control_components] = ["cL"] + model[:variable_components] = ["tf"] + + # N = grid_size + @expression(model, N, grid_size) + # state, control, variable (final time) and initial guess @variables( model, begin - 0 <= tf, (start = 1) - 0 <= x[k = 0:N], (start = x_0 + vx_0 * k / N) - y[k = 0:N], (start = y_0 + (k / N) * (y_f - y_0)) - 0 <= vx[k = 0:N], (start = vx_0) - vy[k = 0:N], (start = vy_0) - cL_min <= cL[k = 0:N] <= cL_max, (start = cL_max / 2) + tf ≥ tf_l, (start = 1) + x[k = 0:N] ≥ x_l, (start = x_t0 + vx_t0 * k / N) + y[k = 0:N], (start = y_t0 + (k / N) * (y_tf - y_t0)) + vx[k = 0:N] ≥ vx_l, (start = vx_t0) + vy[k = 0:N], (start = vy_t0) + cL_min ≤ cL[k = 0:N] ≤ cL_max, (start = cL_max / 2) end ) @@ -70,13 +87,13 @@ function OptimalControlProblems.glider( @constraints( model, begin - x[0] == x_0 - y[0] == y_0 - vx[0] == vx_0 - vy[0] == vy_0 - y[N] == y_f - vx[N] == vx_f - vy[N] == vy_f + x[0] == x_t0 + y[0] == y_t0 + vx[0] == vx_t0 + vy[0] == vy_t0 + y[N] == y_tf + vx[N] == vx_tf + vy[N] == vy_tf end ) @@ -84,12 +101,11 @@ function OptimalControlProblems.glider( @expressions( model, begin - # - step, tf / N + Δt, (tf - t0) / N # - r[k = 0:N], (x[k] / r_0 - 2.5)^2 + r[k = 0:N], (x[k] / r_t0 - 2.5)^2 u[k = 0:N], u_c * (1 - r[k]) * exp(-r[k]) w[k = 0:N], vy[k] - u[k] v[k = 0:N], √(vx[k]^2 + w[k]^2) @@ -105,15 +121,15 @@ function OptimalControlProblems.glider( @constraints( model, begin - ∂x[k = 1:N], x[k] == x[k - 1] + 0.5 * step * (vx[k] + vx[k - 1]) - ∂y[k = 1:N], y[k] == y[k - 1] + 0.5 * step * (vy[k] + vy[k - 1]) - ∂vx[k = 1:N], vx[k] == vx[k - 1] + 0.5 * step * (dvx[k] + dvx[k - 1]) - ∂vy[k = 1:N], vy[k] == vy[k - 1] + 0.5 * step * (dvy[k] + dvy[k - 1]) + ∂x[k = 1:N], x[k] == x[k - 1] + 0.5 * Δt * (vx[k] + vx[k - 1]) + ∂y[k = 1:N], y[k] == y[k - 1] + 0.5 * Δt * (vy[k] + vy[k - 1]) + ∂vx[k = 1:N], vx[k] == vx[k - 1] + 0.5 * Δt * (dvx[k] + dvx[k - 1]) + ∂vy[k = 1:N], vy[k] == vy[k - 1] + 0.5 * Δt * (dvy[k] + dvy[k - 1]) end ) # objective - @objective(model, Min, -x[N]) + @objective(model, Max, x[N]) return model end diff --git a/ext/JuMPModels/insurance.jl b/ext/JuMPModels/insurance.jl index b24b0daf..71b3ebda 100644 --- a/ext/JuMPModels/insurance.jl +++ b/ext/JuMPModels/insurance.jl @@ -8,7 +8,7 @@ The system is discretised using `N` steps, and collocation constraints enforce t # Arguments - `::JuMPBackend`: Specifies the backend for building the JuMP model. -- `N::Int=500`: (Keyword) Number of discretisation steps in the time grid. +- `grid_size::Int=500`: (Keyword) Number of discretisation steps in the time grid. # Returns @@ -28,36 +28,64 @@ julia> model = OptimalControlProblems.insurance(JuMPBackend(); N=100) - Problem formulation available at: https://github.com/control-toolbox/bocop/tree/main/bocop """ function OptimalControlProblems.insurance( - ::JuMPBackend, args...; N::Int=steps_number_data(:insurance), kwargs... + ::JuMPBackend, args...; grid_size::Int=grid_size_data(:insurance), + parameters::Union{Nothing, NamedTuple}=nothing, + kwargs... ) # parameters - tf = final_time_data(:insurance) - γ = 0.2 - λ = 0.25 - h0 = 1.5 - w = 1 - s = 10 - k = 0 - σ = 0 - α = 4 - + params = parameters_data(:insurance, parameters) + t0 = params[:t0] + tf = params[:tf] + γ = params[:γ] + λ = params[:λ] + h0 = params[:h0] + w = params[:w] + s = params[:s] + k = params[:k] + σ = params[:σ] + α = params[:α] + I_l = params[:I_l] + I_u = params[:I_u] + m_l = params[:m_l] + m_u = params[:m_u] + h_l = params[:h_l] + h_u = params[:h_u] + R_l = params[:R_l] + H_l = params[:H_l] + U_l = params[:U_l] + dUdR_l = params[:dUdR_l] + P_l = params[:P_l] + I_t0 = params[:I_t0] + m_t0 = params[:m_t0] + x₃_t0 = params[:x₃_t0] + # model model = JuMP.Model(args...; kwargs...) + # metadata: required + model[:time_grid] = () -> range(t0, tf, grid_size+1) # tf is a fixed + model[:state_components] =["I", "m", "x₃"] + model[:costate_components] = ["∂I", "∂m", "∂x₃"] + model[:control_components] = ["h", "R", "H", "U", "dUdR"] + model[:variable_components] = ["P"] + + # N = grid_size + @expression(model, N, grid_size) + # state, control and initial guess @variables( model, begin - 0 <= I[0:N] <= 1.5, (start = 0.1) - 0 <= m[0:N] <= 1.5, (start = 0.1) - x₃[0:N], (start = 0.1) - 0 <= h[0:N] <= 25, (start = 0.1) - 0 <= R[0:N], (start = 0.1) - 0 <= H[0:N], (start = 0.1) - 0 <= U[0:N], (start = 0.1) - 0.001 <= dUdR[0:N], (start = 0.1) - P >= 0, (start = 0.1) + I_l ≤ I[0:N] ≤ I_u, (start = 0.1) + m_l ≤ m[0:N] ≤ m_u, (start = 0.1) + x₃[0:N], (start = 0.1) + h_l ≤ h[0:N] ≤ h_u, (start = 0.1) + R[0:N] ≥ R_l, (start = 0.1) + H[0:N] ≥ H_l, (start = 0.1) + U[0:N] ≥ U_l, (start = 0.1) + dUdR[0:N] ≥ dUdR_l, (start = 0.1) + P ≥ P_l, (start = 0.1) end ) @@ -65,9 +93,9 @@ function OptimalControlProblems.insurance( @constraints( model, begin - I[0] == 0 - m[0] == 0.001 - x₃[0] == 0 + I[0] == I_t0 + m[0] == m_t0 + x₃[0] == x₃_t0 P - x₃[N] == 0 end ) @@ -75,10 +103,10 @@ function OptimalControlProblems.insurance( @expressions( model, begin - step, tf / N - t[i = 0:N], i*step - ε[i = 0:N], k * t[i] / (tf - t[i] + 1) - fx[i = 0:N], λ * exp(-λ * t[i]) + exp(-λ * tf) / tf + Δt, (tf - t0) / N + t[i = 0:N], t0 + i * Δt + ε[i = 0:N], k * (t[i] - t0) / (tf - t[i] + 1) + fx[i = 0:N], λ * exp(-λ * (t[i] - t0)) + exp(-λ * (tf - t0)) / (tf - t0) v[i = 0:N], m[i]^(α / 2) / (1 + m[i]^(α / 2)) vprime[i = 0:N], α / 2 * m[i]^(α / 2 - 1) / (1 + m[i]^(α / 2))^2 end @@ -88,7 +116,7 @@ function OptimalControlProblems.insurance( model, begin cond1[i = 0:N], R[i] - (w - P + I[i] - m[i] - ε[i]) == 0 - cond2[i = 0:N], H[i] - (h0 - γ * step * i * (1 - v[i])) == 0 + cond2[i = 0:N], H[i] - (h0 - γ * i * Δt * (1 - v[i])) == 0 cond3[i = 0:N], U[i] - (1 - exp(-s * R[i]) + H[i]) == 0 cond4[i = 0:N], dUdR[i] - (s * exp(-s * R[i])) == 0 end @@ -98,28 +126,27 @@ function OptimalControlProblems.insurance( @expressions( model, begin - # dynamics - dI[i = 0:N], (1 - γ * t[i] * vprime[i] / dUdR[i]) * h[i] + dI[i = 0:N], (1 - γ * (t[i] - t0) * vprime[i] / dUdR[i]) * h[i] dm[i = 0:N], h[i] dx₃[i = 0:N], (1 + σ) * I[i] * fx[i] # objective - dc[i = 0:N], -U[i] * fx[i] + dc[i = 0:N], U[i] * fx[i] end ) @constraints( model, begin - ∂I[i = 1:N], I[i] == I[i - 1] + 0.5 * step * (dI[i] + dI[i - 1]) - ∂m[i = 1:N], m[i] == m[i - 1] + 0.5 * step * (dm[i] + dm[i - 1]) - ∂x₃[i = 1:N], x₃[i] == x₃[i - 1] + 0.5 * step * (dx₃[i] + dx₃[i - 1]) + ∂I[i = 1:N], I[i] == I[i - 1] + 0.5 * Δt * (dI[i] + dI[i - 1]) + ∂m[i = 1:N], m[i] == m[i - 1] + 0.5 * Δt * (dm[i] + dm[i - 1]) + ∂x₃[i = 1:N], x₃[i] == x₃[i - 1] + 0.5 * Δt * (dx₃[i] + dx₃[i - 1]) end ) # objective - @objective(model, Min, 0.5 * step * sum(dc[i] + dc[i - 1] for i in 1:N)) + @objective(model, Max, 0.5 * Δt * sum(dc[i] + dc[i - 1] for i in 1:N)) return model end diff --git a/ext/JuMPModels/jackson.jl b/ext/JuMPModels/jackson.jl index 64b54b94..a6b3c2eb 100644 --- a/ext/JuMPModels/jackson.jl +++ b/ext/JuMPModels/jackson.jl @@ -2,14 +2,14 @@ $(TYPEDSIGNATURES) Constructs and returns a JuMP model for the **Jackson Optimal Control Problem**. -The model represents a dynamic system with three state variables `a`, `b`, and `x3` and a control variable `u`. -The objective is to maximise the final value of `x3` by optimising the control `u` over the time horizon `[0, tf]`. +The model represents a dynamic system with three state variables `a`, `b`, and `x₃` and a control variable `u`. +The objective is to maximise the final value of `x₃` by optimising the control `u` over the time horizon `[0, tf]`. The dynamics are discretised using `N` steps with trapezoidal collocation. # Arguments - `::JuMPBackend`: Specifies the backend for building the JuMP model. -- `N::Int=500`: (Keyword) Number of discretisation steps in the time grid. +- `grid_size::Int=500`: (Keyword) Number of discretisation steps in the time grid. # Returns @@ -29,25 +29,50 @@ julia> model = OptimalControlProblems.jackson(JuMPBackend(); N=100) - Problem formulation available at: https://github.com/control-toolbox/bocop/tree/main/bocop """ function OptimalControlProblems.jackson( - ::JuMPBackend, args...; N::Int=steps_number_data(:jackson), kwargs... + ::JuMPBackend, args...; grid_size::Int=grid_size_data(:jackson), + parameters::Union{Nothing, NamedTuple}=nothing, + kwargs... ) # parameters - tf = final_time_data(:jackson) - k1 = 1 - k2 = 10 - k3 = 1 + params = parameters_data(:jackson, parameters) + t0 = params[:t0] + tf = params[:tf] + k1 = params[:k1] + k2 = params[:k2] + k3 = params[:k3] + a_l = params[:a_l] + a_u = params[:a_u] + b_l = params[:b_l] + b_u = params[:b_u] + x₃_l = params[:x₃_l] + x₃_u = params[:x₃_u] + u_l = params[:u_l] + u_u = params[:u_u] + a_t0 = params[:a_t0] + b_t0 = params[:b_t0] + x₃_t0 = params[:x₃_t0] # model model = JuMP.Model(args...; kwargs...) + # metadata: required + model[:time_grid] = () -> range(t0, tf, grid_size+1) # tf is a fixed + model[:state_components] = ["a", "b", "x₃"] + model[:costate_components] = ["∂a", "∂b", "∂x₃"] + model[:control_components] = ["u"] + model[:variable_components] = String[] + + # N = grid_size + @expression(model, N, grid_size) + @variables( model, begin - 0 <= a[0:N] <= 1.1, (start = 0.1) - 0 <= b[0:N] <= 1.1, (start = 0.1) - 0 <= x3[0:N] <= 1.1, (start = 0.1) - 0 <= u[0:N] <= 1, (start = 0.1) + a_l ≤ a[0:N] ≤ a_u, (start = 0.1) + b_l ≤ b[0:N] ≤ b_u, (start = 0.1) + x₃_l ≤ x₃[0:N] ≤ x₃_u, (start = 0.1) + u_l ≤ u[0:N] ≤ u_u, (start = 0.1) end ) @@ -55,9 +80,9 @@ function OptimalControlProblems.jackson( @constraints( model, begin - a[0] == 1 - b[0] == 0 - x3[0] == 0 + a[0] == a_t0 + b[0] == b_t0 + x₃[0] == x₃_t0 end ) @@ -65,24 +90,24 @@ function OptimalControlProblems.jackson( @expressions( model, begin - step, tf / N + Δt, (tf - t0) / N da[i = 0:N], -u[i] * (k1 * a[i] - k2 * b[i]) db[i = 0:N], u[i] * (k1 * a[i] - k2 * b[i]) - (1 - u[i]) * k3 * b[i] - dx3[i = 0:N], (1 - u[i]) * k3 * b[i] + dx₃[i = 0:N], (1 - u[i]) * k3 * b[i] end ) @constraints( model, begin - ∂a[i = 1:N], a[i] == a[i - 1] + 0.5 * step * (da[i] + da[i - 1]) - ∂b[i = 1:N], b[i] == b[i - 1] + 0.5 * step * (db[i] + db[i - 1]) - ∂x3[i = 1:N], x3[i] == x3[i - 1] + 0.5 * step * (dx3[i] + dx3[i - 1]) + ∂a[i = 1:N], a[i] == a[i - 1] + 0.5 * Δt * (da[i] + da[i - 1]) + ∂b[i = 1:N], b[i] == b[i - 1] + 0.5 * Δt * (db[i] + db[i - 1]) + ∂x₃[i = 1:N], x₃[i] == x₃[i - 1] + 0.5 * Δt * (dx₃[i] + dx₃[i - 1]) end ) # objective - @objective(model, Min, -x3[N]) + @objective(model, Max, x₃[N]) return model end diff --git a/ext/JuMPModels/moonlander.jl b/ext/JuMPModels/moonlander.jl index be89759a..ec8aa277 100644 --- a/ext/JuMPModels/moonlander.jl +++ b/ext/JuMPModels/moonlander.jl @@ -9,7 +9,7 @@ The dynamics include translational acceleration, rotation, and thrust allocation # Arguments - `::JuMPBackend`: Specifies the backend for building the JuMP model. -- `N::Int=500`: (Keyword) Number of discretisation steps in the time grid. +- `grid_size::Int=500`: (Keyword) Number of discretisation steps in the time grid. # Returns @@ -29,38 +29,71 @@ julia> model = OptimalControlProblems.moonlander(JuMPBackend(); N=100) - Problem formulation available at: https://arxiv.org/pdf/2303.16746 """ function OptimalControlProblems.moonlander( - ::JuMPBackend, args...; N::Int=steps_number_data(:moonlander), kwargs... + ::JuMPBackend, args...; grid_size::Int=grid_size_data(:moonlander), + parameters::Union{Nothing, NamedTuple}=nothing, + kwargs... ) # parameters - target=[5.0, 5.0] - m = 1 - g = 9.81 - I = 0.1 - D = 1 - max_thrust = 2g - + params = parameters_data(:moonlander, parameters) + t0 = params[:t0] + m = params[:m] + g = params[:g] + I = params[:I] + D = params[:D] + max_thrust = params[:max_thrust] + tf_l = params[:tf_l] + tf_u = params[:tf_u] + F₁_l = params[:F₁_l] + F₁_u = max_thrust + F₂_l = params[:F₂_l] + F₂_u = max_thrust + tf_l = params[:tf_l] + tf_u = params[:tf_u] + F₁_l = params[:F₁_l] + F₂_l = params[:F₂_l] + p₁_t0 = params[:p₁_t0] + p₂_t0 = params[:p₂_t0] + dp₁_t0 = params[:dp₁_t0] + dp₂_t0 = params[:dp₂_t0] + θ_t0 = params[:θ_t0] + dθ_t0 = params[:dθ_t0] + p₁_tf = params[:p₁_tf] + p₂_tf = params[:p₂_tf] + dp₁_tf = params[:dp₁_tf] + dp₂_tf = params[:dp₂_tf] + # define the problem model = JuMP.Model(args...; kwargs...) + # metadata: required + model[:time_grid] = () -> range(t0, value(model[:tf]), grid_size+1) # tf is a free + model[:state_components] = ["p₁", "p₂", "dp₁", "dp₂", "θ", "dθ"] + model[:costate_components] = ["∂p₁", "∂p₂", "∂dp₁", "∂dp₂", "∂θ", "∂dθ"] + model[:control_components] = ["F₁", "F₂"] + model[:variable_components] = ["tf"] + + # N = grid_size + @expression(model, N, grid_size) + # state, control and final time variables @variables( model, begin # final time - 0.1 <= tf <= 1.0, (start = 0.5) + tf_l ≤ tf ≤ tf_u, (start = 0.5) # state variables - p1[k = 0:N], (start = 0.1) - p2[k = 0:N], (start = 0.1) - dp1[k = 0:N], (start = 0.1) - dp2[k = 0:N], (start = 0.1) + p₁[k = 0:N], (start = 0.1) + p₂[k = 0:N], (start = 0.1) + dp₁[k = 0:N], (start = 0.1) + dp₂[k = 0:N], (start = 0.1) θ[k = 0:N], (start = 0.1) dθ[k = 0:N], (start = 0.1) # control variables - 0 <= F1[k = 0:N] <= max_thrust, (start = 5.0) - 0 <= F2[k = 0:N] <= max_thrust, (start = 5.0) + F₁_l ≤ F₁[k = 0:N] ≤ F₁_u, (start = 5.0) + F₂_l ≤ F₂[k = 0:N] ≤ F₂_u, (start = 5.0) end ) @@ -68,16 +101,17 @@ function OptimalControlProblems.moonlander( @constraints( model, begin - p1[0] == 0 - p2[0] == 0 - dp1[0] == 0 - dp2[0] == 0 - θ[0] == 0 - dθ[0] == 0 - p1[N] == target[1] - p2[N] == target[2] - dp1[N] == 0 - dp2[N] == 0 + p₁[0] == p₁_t0 + p₂[0] == p₂_t0 + dp₁[0] == dp₁_t0 + dp₂[0] == dp₂_t0 + θ[0] == θ_t0 + dθ[0] == dθ_t0 + + p₁[N] == p₁_tf + p₂[N] == p₂_tf + dp₁[N] == dp₁_tf + dp₂[N] == dp₂_tf end ) @@ -86,8 +120,8 @@ function OptimalControlProblems.moonlander( model, begin F_r[k = 0:N], [ - cos(θ[k]) -sin(θ[k]) p1[k] - sin(θ[k]) cos(θ[k]) p2[k] + cos(θ[k]) -sin(θ[k]) p₁[k] + sin(θ[k]) cos(θ[k]) p₂[k] 0 0 1 ] end @@ -95,32 +129,31 @@ function OptimalControlProblems.moonlander( @expressions( model, begin - F_tot[k = 0:N], (F_r[k] * [0; F1[k] + F2[k]; 0])[1:2] + F_tot[k = 0:N], (F_r[k] * [0; F₁[k] + F₂[k]; 0])[1:2] end ) @expressions( model, begin - # - step, tf / N + Δt, (tf - t0) / N # - ddp1[k = 0:N], (1 / m) * F_tot[k][1] - ddp2[k = 0:N], (1 / m) * F_tot[k][2] - g - ddθ[k = 0:N], (1 / I) * (D / 2) * (F2[k] - F1[k]) + ddp₁[k = 0:N], (1 / m) * F_tot[k][1] + ddp₂[k = 0:N], (1 / m) * F_tot[k][2] - g + ddθ[k = 0:N], (1 / I) * (D / 2) * (F₂[k] - F₁[k]) end ) @constraints( model, begin - ∂p1[k = 1:N], p1[k] == p1[k - 1] + 0.5 * step * (dp1[k] + dp1[k - 1]) - ∂p2[k = 1:N], p2[k] == p2[k - 1] + 0.5 * step * (dp2[k] + dp2[k - 1]) - ∂dp1[k = 1:N], dp1[k] == dp1[k - 1] + 0.5 * step * (ddp1[k] + ddp1[k - 1]) - ∂dp2[k = 1:N], dp2[k] == dp2[k - 1] + 0.5 * step * (ddp2[k] + ddp2[k - 1]) - ∂θ[k = 1:N], θ[k] == θ[k - 1] + 0.5 * step * (dθ[k] + dθ[k - 1]) - ∂dθ[k = 1:N], dθ[k] == dθ[k - 1] + 0.5 * step * (ddθ[k] + ddθ[k - 1]) + ∂p₁[k = 1:N], p₁[k] == p₁[k - 1] + 0.5 * Δt * (dp₁[k] + dp₁[k - 1]) + ∂p₂[k = 1:N], p₂[k] == p₂[k - 1] + 0.5 * Δt * (dp₂[k] + dp₂[k - 1]) + ∂dp₁[k = 1:N], dp₁[k] == dp₁[k - 1] + 0.5 * Δt * (ddp₁[k] + ddp₁[k - 1]) + ∂dp₂[k = 1:N], dp₂[k] == dp₂[k - 1] + 0.5 * Δt * (ddp₂[k] + ddp₂[k - 1]) + ∂θ[k = 1:N], θ[k] == θ[k - 1] + 0.5 * Δt * (dθ[k] + dθ[k - 1]) + ∂dθ[k = 1:N], dθ[k] == dθ[k - 1] + 0.5 * Δt * (ddθ[k] + ddθ[k - 1]) end ) diff --git a/ext/JuMPModels/robbins.jl b/ext/JuMPModels/robbins.jl index 5beaa746..938c1752 100644 --- a/ext/JuMPModels/robbins.jl +++ b/ext/JuMPModels/robbins.jl @@ -8,7 +8,7 @@ The objective is to minimise a cost functional that combines linear and quadrati # Arguments - `::JuMPBackend`: Specifies the backend for building the JuMP model. -- `N::Int=500`: (Keyword) Number of discretisation steps for the time grid. +- `grid_size::Int=500`: (Keyword) Number of discretisation steps for the time grid. # Returns @@ -28,28 +28,46 @@ julia> model = OptimalControlProblems.robbins(JuMPBackend(); N=100) - Problem formulation available at: https://github.com/control-toolbox/bocop/tree/main/bocop """ function OptimalControlProblems.robbins( - ::JuMPBackend, args...; N::Int=steps_number_data(:robbins), kwargs... + ::JuMPBackend, args...; grid_size::Int=grid_size_data(:robbins), + parameters::Union{Nothing, NamedTuple}=nothing, + kwargs... ) # parameters - tf = final_time_data(:robbins) - α = 3 - β = 0 - γ = 0.5 - - # - step = tf / N + params = parameters_data(:robbins, parameters) + t0 = params[:t0] + tf = params[:tf] + α = params[:α] + β = params[:β] + γ = params[:γ] + x₁_l = params[:x₁_l] + x₁_t0 = params[:x₁_t0] + x₂_t0 = params[:x₂_t0] + x₃_t0 = params[:x₃_t0] + x₁_tf = params[:x₁_tf] + x₂_tf = params[:x₂_tf] + x₃_tf = params[:x₃_tf] # model model = JuMP.Model(args...; kwargs...) + # metadata: required + model[:time_grid] = () -> range(t0, tf, grid_size+1) # tf is a fixed + model[:state_components] = ["x₁", "x₂", "x₃"] + model[:costate_components] = ["∂x₁", "∂x₂", "∂x₃"] + model[:control_components] = ["u"] + model[:variable_components] = String[] + + # N = grid_size + @expression(model, N, grid_size) + # state, control and initial guess @variables( model, begin - 0 <= x1[0:N], (start = 0.1) - x2[0:N], (start = 0.1) - x3[0:N], (start = 0.1) + x₁[0:N] ≥ x₁_l, (start = 0.1) + x₂[0:N], (start = 0.1) + x₃[0:N], (start = 0.1) u[0:N], (start = 0.1) end ) @@ -58,22 +76,28 @@ function OptimalControlProblems.robbins( @constraints( model, begin - x1[0] == 1 - x2[0] == -2 - x3[0] == 0 - x1[N] == 0 - x2[N] == 0 - x3[N] == 0 + x₁[0] == x₁_t0 + x₂[0] == x₂_t0 + x₃[0] == x₃_t0 + x₁[N] == x₁_tf + x₂[N] == x₂_tf + x₃[N] == x₃_tf end ) # dynamics + @expressions( + model, + begin + Δt, (tf - t0) / N + end + ) @constraints( model, begin - ∂x1[i = 1:N], x1[i] == x1[i - 1] + 0.5 * step * (x2[i] + x2[i - 1]) - ∂x2[i = 1:N], x2[i] == x2[i - 1] + 0.5 * step * (x3[i] + x3[i - 1]) - ∂x3[i = 1:N], x3[i] == x3[i - 1] + 0.5 * step * (u[i] + u[i - 1]) + ∂x₁[i = 1:N], x₁[i] == x₁[i - 1] + 0.5 * Δt * (x₂[i] + x₂[i - 1]) + ∂x₂[i = 1:N], x₂[i] == x₂[i - 1] + 0.5 * Δt * (x₃[i] + x₃[i - 1]) + ∂x₃[i = 1:N], x₃[i] == x₃[i - 1] + 0.5 * Δt * (u[i] + u[i - 1]) end ) @@ -81,11 +105,11 @@ function OptimalControlProblems.robbins( @expressions( model, begin - dc[i = 0:N], (α * x1[i] + β * x1[i]^2 + γ * u[i]^2) + dc[i = 0:N], (α * x₁[i] + β * x₁[i]^2 + γ * u[i]^2) end ) - @objective(model, Min, 0.5 * step * sum(dc[i] + dc[i - 1] for i in 1:N)) + @objective(model, Min, 0.5 * Δt * sum(dc[i] + dc[i - 1] for i in 1:N)) return model end diff --git a/ext/JuMPModels/robot.jl b/ext/JuMPModels/robot.jl index 68e94655..aad9c4d5 100644 --- a/ext/JuMPModels/robot.jl +++ b/ext/JuMPModels/robot.jl @@ -8,7 +8,7 @@ The objective is to minimise the final time required for the robot arm to move b # Arguments - `::JuMPBackend`: Specifies the backend for building the JuMP model. -- `N::Int=250`: (Keyword) Number of discretisation steps for the time horizon. +- `grid_size::Int=250`: (Keyword) Number of discretisation steps for the time horizon. # Returns @@ -28,44 +28,81 @@ julia> model = OptimalControlProblems.robot(JuMPBackend(); N=100) - Problem formulation available at: https://github.com/MadNLP/COPSBenchmark.jl/blob/main/src/robot.jl """ function OptimalControlProblems.robot( - ::JuMPBackend, args...; N::Int=steps_number_data(:robot), kwargs... + ::JuMPBackend, args...; grid_size::Int=grid_size_data(:robot), + parameters::Union{Nothing, NamedTuple}=nothing, + kwargs... ) # parameters + params = parameters_data(:robot, parameters) + t0 = params[:t0] # total length of arm - L = 5 + L = params[:L] # Upper bounds on the controls - max_uρ = 1 - max_uθ = 1 - max_uϕ = 1 + uρ_l = params[:uρ_l] + uρ_u = params[:uρ_u] + uθ_l = params[:uθ_l] + uθ_u = params[:uθ_u] + uϕ_l = params[:uϕ_l] + uϕ_u = params[:uϕ_u] # Initial positions of the length and the angles for the robot arm - ρ0 = 4.5 - ϕ0 = π/4 - θf = 2π/3 + ρ_t0 = params[:ρ_t0] + θ_t0 = params[:θ_t0] + ϕ_t0 = params[:ϕ_t0] + dρ_t0 = params[:dρ_t0] + dθ_t0 = params[:dθ_t0] + dϕ_t0 = params[:dϕ_t0] + + # Final positions + ρ_tf = params[:ρ_tf] + θ_tf = params[:θ_tf] + ϕ_tf = params[:ϕ_tf] + dρ_tf = params[:dρ_tf] + dθ_tf = params[:dθ_tf] + dϕ_tf = params[:dϕ_tf] + + # + ρ_l = params[:ρ_l] + ρ_u = L + θ_l = params[:θ_l] + θ_u = params[:θ_u] + ϕ_l = params[:ϕ_l] + ϕ_u = params[:ϕ_u] + tf_l = params[:tf_l] # model model = JuMP.Model(args...; kwargs...) + # metadata: required + model[:time_grid] = () -> range(t0, value(model[:tf]), grid_size+1) # tf is a free + model[:state_components] = ["ρ", "dρ", "θ", "dθ", "ϕ", "dϕ"] + model[:costate_components] = ["∂ρ", "∂dρ", "∂θ", "∂dθ", "∂ϕ", "∂dϕ"] + model[:control_components] = ["uρ", "uθ", "uϕ"] + model[:variable_components] = ["tf"] + + # N = grid_size + @expression(model, N, grid_size) + # state, control, variable (final time) and initial guess @variables( model, begin - 0 <= ρ[k = 0:N] <= L, (start = ρ0) - -π <= θ[k = 0:N] <= π, (start = 2π/3 * (k / N)^2) - 0 <= ϕ[k = 0:N] <= π, (start = ϕ0) + ρ_l ≤ ρ[k = 0:N] ≤ ρ_u, (start = ρ_t0) + θ_l ≤ θ[k = 0:N] ≤ θ_u, (start = 2π/3 * (k / N)^2) + ϕ_l ≤ ϕ[k = 0:N] ≤ ϕ_u, (start = ϕ_t0) - dρ[k = 0:N], (start = 0) - dθ[k = 0:N], (start = 4π/3 * (k / N)) - dϕ[k = 0:N], (start = 0) + dρ[k = 0:N], (start = 0) + dθ[k = 0:N], (start = 4π/3 * (k / N)) + dϕ[k = 0:N], (start = 0) - -max_uρ <= uρ[0:N] <= max_uρ, (start = 0) - -max_uθ <= uθ[0:N] <= max_uθ, (start = 0) - -max_uϕ <= uϕ[0:N] <= max_uϕ, (start = 0) + uρ_l ≤ uρ[0:N] ≤ uρ_u, (start = 0) + uθ_l ≤ uθ[0:N] ≤ uθ_u, (start = 0) + uϕ_l ≤ uϕ[0:N] ≤ uϕ_u, (start = 0) - tf >= 0.1, (start = 1) + tf ≥ tf_l, (start = 1) end ) @@ -73,22 +110,21 @@ function OptimalControlProblems.robot( @constraints( model, begin - # initial - ρ[0] == ρ0 - ϕ[0] == ϕ0 - θ[0] == 0 - dρ[0] == 0 - dθ[0] == 0 - dϕ[0] == 0 + ρ[0] == ρ_t0 + θ[0] == θ_t0 + ϕ[0] == ϕ_t0 + dρ[0] == dρ_t0 + dθ[0] == dθ_t0 + dϕ[0] == dϕ_t0 # final - ρ[N] == ρ0 - θ[N] == θf - ϕ[N] == ϕ0 - dρ[N] == 0 - dθ[N] == 0 - dϕ[N] == 0 + ρ[N] == ρ_tf + θ[N] == θ_tf + ϕ[N] == ϕ_tf + dρ[N] == dρ_tf + dθ[N] == dθ_tf + dϕ[N] == dϕ_tf end ) @@ -96,9 +132,8 @@ function OptimalControlProblems.robot( @expressions( model, begin - # - step, tf / N + Δt, (tf - t0) / N # I_θ[i = 0:N], ((L - ρ[i])^3 + ρ[i]^3) * (sin(ϕ[i]))^2 @@ -114,12 +149,12 @@ function OptimalControlProblems.robot( @constraints( model, begin - ∂ρ[i = 1:N], ρ[i] == ρ[i - 1] + 0.5 * step * (dρ[i] + dρ[i - 1]) - ∂ϕ[i = 1:N], ϕ[i] == ϕ[i - 1] + 0.5 * step * (dϕ[i] + dϕ[i - 1]) - ∂θ[i = 1:N], θ[i] == θ[i - 1] + 0.5 * step * (dθ[i] + dθ[i - 1]) - ∂dρ[i = 1:N], dρ[i] == dρ[i - 1] + 0.5 * step * (ddρ[i] + ddρ[i - 1]) - ∂dθ[i = 1:N], dθ[i] == dθ[i - 1] + 0.5 * step * (ddθ[i] + ddθ[i - 1]) - ∂dϕ[i = 1:N], dϕ[i] == dϕ[i - 1] + 0.5 * step * (ddϕ[i] + ddϕ[i - 1]) + ∂ρ[i = 1:N], ρ[i] == ρ[i - 1] + 0.5 * Δt * (dρ[i] + dρ[i - 1]) + ∂ϕ[i = 1:N], ϕ[i] == ϕ[i - 1] + 0.5 * Δt * (dϕ[i] + dϕ[i - 1]) + ∂θ[i = 1:N], θ[i] == θ[i - 1] + 0.5 * Δt * (dθ[i] + dθ[i - 1]) + ∂dρ[i = 1:N], dρ[i] == dρ[i - 1] + 0.5 * Δt * (ddρ[i] + ddρ[i - 1]) + ∂dθ[i = 1:N], dθ[i] == dθ[i - 1] + 0.5 * Δt * (ddθ[i] + ddθ[i - 1]) + ∂dϕ[i = 1:N], dϕ[i] == dϕ[i - 1] + 0.5 * Δt * (ddϕ[i] + ddϕ[i - 1]) end ) diff --git a/ext/JuMPModels/rocket.jl b/ext/JuMPModels/rocket.jl index 330366d6..7a15b675 100644 --- a/ext/JuMPModels/rocket.jl +++ b/ext/JuMPModels/rocket.jl @@ -8,7 +8,7 @@ The objective is to maximise the final altitude of the rocket while satisfying b # Arguments - `::JuMPBackend`: Specifies the backend for building the JuMP model. -- `N::Int=500`: (Keyword) Number of discretisation steps for the time horizon. +- `grid_size::Int=500`: (Keyword) Number of discretisation steps for the time horizon. # Returns @@ -28,35 +28,53 @@ julia> model = OptimalControlProblems.rocket(JuMPBackend(); N=200) - Problem formulation available at: https://github.com/MadNLP/COPSBenchmark.jl/blob/main/src/rocket.jl """ function OptimalControlProblems.rocket( - ::JuMPBackend, args...; N::Int=steps_number_data(:rocket), kwargs... + ::JuMPBackend, args...; grid_size::Int=grid_size_data(:rocket), + parameters::Union{Nothing, NamedTuple}=nothing, + kwargs... ) # parameters - h0 = 1 - v0 = 0 - m0 = 1 - g0 = 1 - Tc = 3.5 - hc = 500 - vc = 620 - mc = 0.6 - c = 0.5 * sqrt(g0 * h0) - mf = mc * m0 - Dc = 0.5 * vc * (m0 / g0) - Tmax = Tc * m0 * g0 + params = parameters_data(:rocket, parameters) + t0 = params[:t0] + h_t0 = params[:h_t0] + v_t0 = params[:v_t0] + m_t0 = params[:m_t0] + g0 = params[:g0] + Tc = params[:Tc] + hc = params[:hc] + vc = params[:vc] + mc = params[:mc] + T_l = params[:T_l] + tf_l = params[:tf_l] + + # + c = 0.5 * sqrt(g0 * h_t0) + m_tf = mc * m_t0 + Dc = 0.5 * vc * (m_t0 / g0) + Tmax = Tc * m_t0 * g0 # model model = JuMP.Model(args...; kwargs...) + # metadata + model[:time_grid] = () -> range(t0, value(model[:tf]), grid_size+1) # tf is a free + model[:state_components] = ["h", "v", "m"] + model[:costate_components] = ["∂h", "∂v", "∂m"] + model[:control_components] = ["T"] + model[:variable_components] = ["tf"] + + # N = grid_size + @expression(model, N, grid_size) + # state, control, variable (final time) and initial guess @variables( model, begin - h[i = 0:N] >= h0, (start = 1) - v[i = 0:N] >= v0, (start = i / N * (1 - i / N)) - mf <= m[i = 0:N] <= m0, (start = (mf - m0) * (i / N) + m0) - 0 <= T[i = 0:N] <= Tmax, (start = Tmax / 2) - 0 <= tf, (start = 1) + h[i = 0:N] ≥ h_t0, (start = 1) + v[i = 0:N] ≥ v_t0, (start = i / N * (1 - i / N)) + m_tf ≤ m[i = 0:N] ≤ m_t0, (start = (m_tf - m_t0) * (i / N) + m_t0) + T_l ≤ T[i = 0:N] ≤ Tmax, (start = Tmax / 2) + tf ≥ tf_l, (start = 1) end ) @@ -64,10 +82,10 @@ function OptimalControlProblems.rocket( @constraints( model, begin - h_ic, h[0] == h0 - v_ic, v[0] == v0 - m_ic, m[0] == m0 - mfc, m[N] == mf + h[0] == h_t0 + v[0] == v_t0 + m[0] == m_t0 + m[N] == m_tf end ) @@ -75,13 +93,12 @@ function OptimalControlProblems.rocket( @expressions( model, begin - # - step, tf / N + Δt, (tf - t0) / N # - D[i = 0:N], Dc * v[i]^2 * exp(-hc * (h[i] - h0)) / h0 - g[i = 0:N], g0 * (h0 / h[i])^2 + D[i = 0:N], Dc * v[i]^2 * exp(-hc * (h[i] - h_t0)) / h_t0 + g[i = 0:N], g0 * (h_t0 / h[i])^2 # dh[i = 0:N], v[i] @@ -93,14 +110,14 @@ function OptimalControlProblems.rocket( @constraints( model, begin - ∂h[i = 1:N], h[i] == h[i - 1] + 0.5 * step * (dh[i] + dh[i - 1]) - ∂v[i = 1:N], v[i] == v[i - 1] + 0.5 * step * (dv[i] + dv[i - 1]) - ∂m[i = 1:N], m[i] == m[i - 1] + 0.5 * step * (dm[i] + dm[i - 1]) + ∂h[i = 1:N], h[i] == h[i - 1] + 0.5 * Δt * (dh[i] + dh[i - 1]) + ∂v[i = 1:N], v[i] == v[i - 1] + 0.5 * Δt * (dv[i] + dv[i - 1]) + ∂m[i = 1:N], m[i] == m[i - 1] + 0.5 * Δt * (dm[i] + dm[i - 1]) end ) # objective - @objective(model, Min, -h[N]) + @objective(model, Max, h[N]) return model end diff --git a/ext/JuMPModels/space_shuttle.jl b/ext/JuMPModels/space_shuttle.jl index 697f072c..e0ccef26 100644 --- a/ext/JuMPModels/space_shuttle.jl +++ b/ext/JuMPModels/space_shuttle.jl @@ -9,7 +9,7 @@ Note: no heating limit path constraint is included in this formulation. # Arguments - `::JuMPBackend`: Specifies the backend for building the JuMP model. -- `N::Int=500`: (Keyword) Number of discretisation steps for the time horizon. +- `grid_size::Int=500`: (Keyword) Number of discretisation steps for the time horizon. # Returns @@ -29,115 +29,146 @@ julia> model = OptimalControlProblems.space_shuttle(JuMPBackend(); N=200) - Problem formulation and tutorial available at: https://jump.dev/JuMP.jl/stable/tutorials/nonlinear/space_shuttle_reentry_trajectory/ """ function OptimalControlProblems.space_shuttle( - ::JuMPBackend, args...; N::Int=steps_number_data(:space_shuttle), kwargs... + ::JuMPBackend, args...; grid_size::Int=grid_size_data(:space_shuttle), + parameters::Union{Nothing, NamedTuple}=nothing, + kwargs... ) - ## Global variables - w = 203000.0 # weight (lb) - g₀ = 32.174 # acceleration (ft/sec^2) + # Parameters + params = parameters_data(:space_shuttle, parameters) + t0 = params[:t0] + + ## + w = params[:w] + g₀ = params[:g₀] m = w / g₀ # mass (slug) ## Aerodynamic and atmospheric forces on the vehicle - ρ₀ = 0.002378 - hᵣ = 23800 - Rₑ = 20902900 - μ = 0.14076539e17 - S = 2690 - a₀ = -0.20704 - a₁ = 0.029244 - b₀ = 0.07854 - b₁ = -0.61592e-2 - b₂ = 0.621408e-3 - # c₀ = 1.0672181 - # c₁ = -0.19213774e-1 - # c₂ = 0.21286289e-3 - # c₃ = -0.10117249e-5 + ρ₀ = params[:ρ₀] + hᵣ = params[:hᵣ] + Rₑ = params[:Rₑ] + μ = params[:μ] + S = params[:S] + a₀ = params[:a₀] + a₁ = params[:a₁] + b₀ = params[:b₀] + b₁ = params[:b₁] + b₂ = params[:b₂] # - Δt_min = 3.5 - Δt_max = 4.5 - tf_min = 500*Δt_min - tf_max = 500*Δt_max + Δt_min = params[:Δt_min] + Δt_max = params[:Δt_max] + tf_l = grid_size*Δt_min + tf_u = grid_size*Δt_max ## Initial conditions - h_s = 2.6 # altitude (ft) / 1e5 - ϕ_s = deg2rad(0) # longitude (rad) - θ_s = deg2rad(0) # latitude (rad) - v_s = 2.56 # velocity (ft/sec) / 1e4 - γ_s = deg2rad(-1) # flight path angle (rad) - ψ_s = deg2rad(90) # azimuth (rad) - α_s = deg2rad(0) # angle of attack (rad) - β_s = deg2rad(0) # bank angle (rad) - t_s = 1.00 # time step (sec) + h_t0 = params[:h_t0] + ϕ_t0 = params[:ϕ_t0] + θ_t0 = params[:θ_t0] + v_t0 = params[:v_t0] + γ_t0 = params[:γ_t0] + ψ_t0 = params[:ψ_t0] + + # for initial guess + α_s = params[:α_s] + β_s = params[:β_s] ## Final conditions, the so-called Terminal Area Energy Management (TAEM) - h_t = 0.8 # altitude (ft) / 1e5 - v_t = 0.25 # velocity (ft/sec) / 1e4 - γ_t = deg2rad(-5) # flight path angle (rad) + h_tf = params[:h_tf] + v_tf = params[:v_tf] + γ_tf = params[:γ_tf] + + ## + h_l = params[:h_l] + ϕ_l = params[:ϕ_l] + ϕ_u = params[:ϕ_u] + θ_l = params[:θ_l] + θ_u = params[:θ_u] + v_l = params[:v_l] + γ_l = params[:γ_l] + γ_u = params[:γ_u] + ψ_l = params[:ψ_l] + ψ_u = params[:ψ_u] + α_l = params[:α_l] + α_u = params[:α_u] + β_l = params[:β_l] + β_u = params[:β_u] + + ## Scalings + scaling_h = 1e5 + scaling_v = 1e4 # model model = JuMP.Model(args...; kwargs...) + # metadata: required + model[:time_grid] = () -> range(t0, value(model[:tf]), grid_size+1) # tf is a free + model[:state_components] = ["scaled_h", "ϕ", "θ", "scaled_v", "γ", "ψ"] + model[:costate_components] = ["∂h", "∂ϕ", "∂θ", "∂v", "∂γ", "∂ψ"] + model[:control_components] = ["α", "β"] + model[:variable_components] = ["tf"] + + # N = grid_size + @expression(model, N, grid_size) + # state, control and variable (final time) @variables( model, begin - # state - 0 ≤ scaled_h[0:N] # altitude (ft) / 1e5 - -2π ≤ ϕ[0:N] ≤ 2π # longitude (rad) - deg2rad(-89) ≤ θ[0:N] ≤ deg2rad(89) # latitude (rad) - 1e-4 ≤ scaled_v[0:N] # velocity (ft/sec) / 1e4 - deg2rad(-89) ≤ γ[0:N] ≤ deg2rad(89) # flight path angle (rad) - -2π ≤ ψ[0:N] ≤ 2π # azimuth (rad) + scaled_h[0:N] ≥ h_l # altitude (ft) / scaling_h + ϕ_l ≤ ϕ[0:N] ≤ ϕ_u # longitude (rad) + θ_l ≤ θ[0:N] ≤ θ_u # latitude (rad) + scaled_v[0:N] ≥ v_l # velocity (ft/sec) / scaling_v + γ_l ≤ γ[0:N] ≤ γ_u # flight path angle (rad) + ψ_l ≤ ψ[0:N] ≤ ψ_u # azimuth (rad) # control - deg2rad(-90) ≤ α[0:N] ≤ deg2rad(90) # angle of attack (rad) - deg2rad(-89) ≤ β[0:N] ≤ deg2rad(1) # bank angle (rad) + α_l ≤ α[0:N] ≤ α_u # angle of attack (rad) + β_l ≤ β[0:N] ≤ β_u # bank angle (rad) # - tf_min ≤ tf ≤ tf_max # final time (sec) + tf_l ≤ tf ≤ tf_u # final time (sec) end ) - ## Fix initial conditions - # initial and final conditions + # Fix initial conditions + ## initial and final conditions @constraints( model, begin - con_h0, scaled_h[0] == h_s - con_ϕ0, ϕ[0] == ϕ_s - con_θ0, θ[0] == θ_s - con_v0, scaled_v[0] == v_s - con_γ0, γ[0] == γ_s - con_ψ0, ψ[0] == ψ_s - con_hf, scaled_h[N] == h_t - con_vf, scaled_v[N] == v_t - con_γf, γ[N] == γ_t + scaled_h[0] == h_t0 + ϕ[0] == ϕ_t0 + θ[0] == θ_t0 + scaled_v[0] == v_t0 + γ[0] == γ_t0 + ψ[0] == ψ_t0 + + scaled_h[N] == h_tf + scaled_v[N] == v_tf + γ[N] == γ_tf end ) - ## initial guess: linear interpolation between boundary conditions - - # Helper function for linear interpolation + # initial guess: linear interpolation between boundary conditions + ## Helper function for linear interpolation function linear_interpolate(x_s, x_t, n) return [x_s + (i - 1) / (n - 1) * (x_t - x_s) for i in 1:n] end - # Interpolate each parameter separately - h_interp = linear_interpolate(h_s, h_t, N+1) - ϕ_interp = linear_interpolate(ϕ_s, ϕ_s, N+1) # no change in longitude - θ_interp = linear_interpolate(θ_s, θ_s, N+1) # no change in latitude - v_interp = linear_interpolate(v_s, v_t, N+1) - γ_interp = linear_interpolate(γ_s, γ_t, N+1) - ψ_interp = linear_interpolate(ψ_s, ψ_s, N+1) # no change in azimuth + ## Interpolate each parameter separately + h_interp = linear_interpolate(h_t0, h_tf, N+1) + ϕ_interp = linear_interpolate(ϕ_t0, ϕ_t0, N+1) # no change in longitude + θ_interp = linear_interpolate(θ_t0, θ_t0, N+1) # no change in latitude + v_interp = linear_interpolate(v_t0, v_tf, N+1) + γ_interp = linear_interpolate(γ_t0, γ_tf, N+1) + ψ_interp = linear_interpolate(ψ_t0, ψ_t0, N+1) # no change in azimuth α_interp = linear_interpolate(α_s, α_s, N+1) # no change in angle of attack β_interp = linear_interpolate(β_s, β_s, N+1) # no change in bank angle - t_interp = linear_interpolate(t_s, t_s, N+1) # no change in time step - # Combine all interpolated parameters into an array of arrays + ## Combine all interpolated parameters into an array of arrays interpolated_values = [ - transpose([h, ϕ, θ, v, γ, ψ, α, β, t]) for (h, ϕ, θ, v, γ, ψ, α, β, t) in zip( + transpose([h, ϕ, θ, v, γ, ψ, α, β]) for (h, ϕ, θ, v, γ, ψ, α, β) in zip( h_interp, ϕ_interp, θ_interp, @@ -146,11 +177,10 @@ function OptimalControlProblems.space_shuttle( ψ_interp, α_interp, β_interp, - t_interp, ) ] - # Create the initial guess by summing the interpolated values + ## Create the initial guess by summing the interpolated values initial_guess = reduce(vcat, interpolated_values) set_start_value.(model[:scaled_h], vec(initial_guess[:, 1])) set_start_value.(model[:ϕ], vec(initial_guess[:, 2])) @@ -160,14 +190,13 @@ function OptimalControlProblems.space_shuttle( set_start_value.(model[:ψ], vec(initial_guess[:, 6])) set_start_value.(model[:α], vec(initial_guess[:, 7])) set_start_value.(model[:β], vec(initial_guess[:, 8])) - set_start_value.(model[:tf], (tf_min+tf_max)/2) - #set_start_value.(model[:Δt], vec(initial_guess[1:(end - 1), 9])) + set_start_value.(model[:tf], (tf_l+tf_u)/2) - ## Functions to restore `h` and `v` to their true scale - @expression(model, h[j = 0:N], scaled_h[j] * 1e5) - @expression(model, v[j = 0:N], scaled_v[j] * 1e4) + # Functions to restore `h` and `v` to their true scale + @expression(model, h[j = 0:N], scaled_h[j] * scaling_h) + @expression(model, v[j = 0:N], scaled_v[j] * scaling_v) - # Helper functions + ## Helper functions @expression(model, c_L[j = 0:N], a₀ + a₁ * rad2deg(α[j])) @expression(model, c_D[j = 0:N], b₀ + b₁ * rad2deg(α[j]) + b₂ * rad2deg(α[j])^2) @expression(model, ρ[j = 0:N], ρ₀ * exp(-h[j] / hᵣ)) @@ -176,7 +205,7 @@ function OptimalControlProblems.space_shuttle( @expression(model, r[j = 0:N], Rₑ + h[j]) @expression(model, g[j = 0:N], μ / r[j]^2) - ## Motion of the vehicle as a differential-algebraic system of equations (DAEs) + # Motion of the vehicle as a differential-algebraic system of equations (DAEs) @expression(model, δh[j = 0:N], v[j] * sin(γ[j])) @expression(model, δϕ[j = 0:N], (v[j] / r[j]) * cos(γ[j]) * sin(ψ[j]) / cos(θ[j])) @expression(model, δθ[j = 0:N], (v[j] / r[j]) * cos(γ[j]) * cos(ψ[j])) @@ -193,21 +222,21 @@ function OptimalControlProblems.space_shuttle( (v[j] / (r[j] * cos(θ[j]))) * cos(γ[j]) * sin(ψ[j]) * sin(θ[j]) ) - @expression(model, Δt[i = 1:N], tf / N) + @expression(model, Δt, (tf - t0) / N) @constraints( model, begin - ∂h[i = 1:N], h[i] == h[i - 1] + 0.5 * Δt[i] * (δh[i - 1] + δh[i]) - ∂ϕ[i = 1:N], ϕ[i] == ϕ[i - 1] + 0.5 * Δt[i] * (δϕ[i - 1] + δϕ[i]) - ∂θ[i = 1:N], θ[i] == θ[i - 1] + 0.5 * Δt[i] * (δθ[i - 1] + δθ[i]) - ∂v[i = 1:N], v[i] == v[i - 1] + 0.5 * Δt[i] * (δv[i - 1] + δv[i]) - ∂γ[i = 1:N], γ[i] == γ[i - 1] + 0.5 * Δt[i] * (δγ[i - 1] + δγ[i]) - ∂ψ[i = 1:N], ψ[i] == ψ[i - 1] + 0.5 * Δt[i] * (δψ[i - 1] + δψ[i]) + ∂h[i = 1:N], h[i] == h[i - 1] + 0.5 * Δt * (δh[i - 1] + δh[i]) + ∂ϕ[i = 1:N], ϕ[i] == ϕ[i - 1] + 0.5 * Δt * (δϕ[i - 1] + δϕ[i]) + ∂θ[i = 1:N], θ[i] == θ[i - 1] + 0.5 * Δt * (δθ[i - 1] + δθ[i]) + ∂v[i = 1:N], v[i] == v[i - 1] + 0.5 * Δt * (δv[i - 1] + δv[i]) + ∂γ[i = 1:N], γ[i] == γ[i - 1] + 0.5 * Δt * (δγ[i - 1] + δγ[i]) + ∂ψ[i = 1:N], ψ[i] == ψ[i - 1] + 0.5 * Δt * (δψ[i - 1] + δψ[i]) end ) - @objective(model, Min, -θ[N]) + @objective(model, Max, θ[N]) return model end diff --git a/ext/JuMPModels/steering.jl b/ext/JuMPModels/steering.jl index b7418551..7ba164f1 100644 --- a/ext/JuMPModels/steering.jl +++ b/ext/JuMPModels/steering.jl @@ -2,13 +2,13 @@ $(TYPEDSIGNATURES) Constructs and returns a JuMP model for the **Particle Steering Problem**. -The model represents the dynamics of a particle with four state variables (`x1`, `x2`, `x3`, `x4`) and a control input `u`. +The model represents the dynamics of a particle with four state variables (`x₁`, `x₂`, `x₃`, `x₄`) and a control input `u`. The objective is to minimise the final time required for the particle to reach a specified altitude and terminal velocity while satisfying the system dynamics and boundary conditions. # Arguments - `::JuMPBackend`: Specifies the backend for building the JuMP model. -- `N::Int=500`: (Keyword) Number of discretisation steps for the time horizon. +- `grid_size::Int=500`: (Keyword) Number of discretisation steps for the time horizon. # Returns @@ -28,62 +28,87 @@ julia> model = OptimalControlProblems.steering(JuMPBackend(); N=200) - Problem formulation available at: https://github.com/MadNLP/COPSBenchmark.jl/blob/main/src/steering.jl """ function OptimalControlProblems.steering( - ::JuMPBackend, args...; N::Int=steps_number_data(:steering), kwargs... + ::JuMPBackend, args...; grid_size::Int=grid_size_data(:steering), + parameters::Union{Nothing, NamedTuple}=nothing, + kwargs... ) # parameters - a = 100 - u_min = -π/2 - u_max = π/2 - xs = zeros(4) - xf = [NaN, 5, 45, 0] - + params = parameters_data(:steering, parameters) + t0 = params[:t0] + a = params[:a] + u_min = params[:u_min] + u_max = params[:u_max] + tf_l = params[:tf_l] + x₁_t0 = params[:x₁_t0] + x₂_t0 = params[:x₂_t0] + x₃_t0 = params[:x₃_t0] + x₄_t0 = params[:x₄_t0] + x₂_tf = params[:x₂_tf] + x₃_tf = params[:x₃_tf] + x₄_tf = params[:x₄_tf] + + # tf_start = 1 - function gen_x0(k, i) if i == 1 || i == 4 return 0.0 elseif i == 2 - return 5.0 * k * tf_start / N + return 5.0 * k * (tf_start-t0) / N elseif i == 3 - return 45.0 * k * tf_start / N + return 45.0 * k * (tf_start-t0) / N end end # model model = JuMP.Model(args...; kwargs...) - @variable(model, u_min <= u[i = 1:(N + 1)] <= u_max, start = 0) # control - @variable(model, x1[i = 1:(N + 1)], start = gen_x0(i, 1)) # state x1 - @variable(model, x2[i = 1:(N + 1)], start = gen_x0(i, 2)) # state x2 - @variable(model, x3[i = 1:(N + 1)], start = gen_x0(i, 3)) # state x3 - @variable(model, x4[i = 1:(N + 1)], start = gen_x0(i, 4)) # state x4 - @variable(model, tf, start = tf_start) # final time + # metadata: required + model[:time_grid] = () -> range(t0, value(model[:tf]), grid_size+1) # tf is a free + model[:state_components] = ["x₁", "x₂", "x₃", "x₄"] + model[:costate_components] = ["∂x₁", "∂x₂", "∂x₃", "∂x₄"] + model[:control_components] = ["u"] + model[:variable_components] = ["tf"] - @expression(model, Δt, tf / N) # step size + # N = grid_size + @expression(model, N, grid_size) - # boundary conditions - @constraint(model, x1[1] == xs[1]) - @constraint(model, x2[1] == xs[2]) - @constraint(model, x3[1] == xs[3]) - @constraint(model, x4[1] == xs[4]) - @constraint(model, x2[N + 1] == xf[2]) - @constraint(model, x3[N + 1] == xf[3]) - @constraint(model, x4[N + 1] == xf[4]) + # state, control and variable (final time) + @variables( + model, + begin + u_min ≤ u[i = 0:N] ≤ u_max, (start = 0) + x₁[i = 0:N], (start = gen_x0(i, 1)) + x₂[i = 0:N], (start = gen_x0(i, 2)) + x₃[i = 0:N], (start = gen_x0(i, 3)) + x₄[i = 0:N], (start = gen_x0(i, 4)) + tf ≥ tf_l, (start = tf_start) + end + ) - # constraint on final time - @constraint(model, tf >= 0) + # boundary conditions + @constraints( + model, + begin + x₁[0] == x₁_t0 + x₂[0] == x₂_t0 + x₃[0] == x₃_t0 + x₄[0] == x₄_t0 + x₂[N] == x₂_tf + x₃[N] == x₃_tf + x₄[N] == x₄_tf + end + ) # dynamics + @expression(model, Δt, (tf - t0) / N) # Δt size @constraints( model, begin - ∂x1[i = 1:N], x1[i + 1] == x1[i] + 0.5 * Δt * (x3[i] + x3[i + 1]) - ∂x2[i = 1:N], x2[i + 1] == x2[i] + 0.5 * Δt * (x4[i] + x4[i + 1]) - ∂x3[i = 1:N], - x3[i + 1] == x3[i] + 0.5 * Δt * (a * cos(u[i]) + a * cos(u[i + 1])) - ∂x4[i = 1:N], - x4[i + 1] == x4[i] + 0.5 * Δt * (a * sin(u[i]) + a * sin(u[i + 1])) + ∂x₁[i = 1:N], x₁[i] == x₁[i - 1] + 0.5 * Δt * (x₃[i - 1] + x₃[i]) + ∂x₂[i = 1:N], x₂[i] == x₂[i - 1] + 0.5 * Δt * (x₄[i - 1] + x₄[i]) + ∂x₃[i = 1:N], x₃[i] == x₃[i - 1] + 0.5 * Δt * (a * cos(u[i - 1]) + a * cos(u[i])) + ∂x₄[i = 1:N], x₄[i] == x₄[i - 1] + 0.5 * Δt * (a * sin(u[i - 1]) + a * sin(u[i])) end ) diff --git a/ext/JuMPModels/vanderpol.jl b/ext/JuMPModels/vanderpol.jl index 8e4eeda6..8e55634b 100644 --- a/ext/JuMPModels/vanderpol.jl +++ b/ext/JuMPModels/vanderpol.jl @@ -7,7 +7,7 @@ The model represents the dynamics of the Van der Pol oscillator with control inp # Arguments - `::JuMPBackend`: Specifies the backend for building the JuMP model. -- `N::Int=500`: (Keyword) Number of discretisation steps for the time horizon. +- `grid_size::Int=500`: (Keyword) Number of discretisation steps for the time horizon. # Returns @@ -27,23 +27,39 @@ julia> model = OptimalControlProblems.vanderpol(JuMPBackend(); N=100) - Problem formulation available at: https://github.com/control-toolbox/bocop/tree/main/bocop """ function OptimalControlProblems.vanderpol( - ::JuMPBackend, args...; N::Int=steps_number_data(:vanderpol), kwargs... + ::JuMPBackend, args...; grid_size::Int=grid_size_data(:vanderpol), + parameters::Union{Nothing, NamedTuple}=nothing, + kwargs... ) # parameters - tf = final_time_data(:vanderpol) - ω = 1 - ε = 1 - + params = parameters_data(:vanderpol, parameters) + t0 = params[:t0] + tf = params[:tf] + ω = params[:ω] + ε = params[:ε] + x₁_t0 = params[:x₁_t0] + x₂_t0 = params[:x₂_t0] + # model model = JuMP.Model(args...; kwargs...) + # metadata: required + model[:time_grid] = () -> range(t0, tf, grid_size+1) # tf is a fixed + model[:state_components] = ["x₁", "x₂"] + model[:costate_components] = ["∂x₁", "∂x₂"] + model[:control_components] = ["u"] + model[:variable_components] = String[] + + # N = grid_size + @expression(model, N, grid_size) + # state, control and initial guess @variables( model, begin - x1[0:N], (start = 0.1) - x2[0:N], (start = 0.1) + x₁[0:N], (start = 0.1) + x₂[0:N], (start = 0.1) u[0:N], (start = 0.1) end ) @@ -52,8 +68,8 @@ function OptimalControlProblems.vanderpol( @constraints( model, begin - x1[0] == 1 - x2[0] == 0 + x₁[0] == x₁_t0 + x₂[0] == x₂_t0 end ) @@ -61,29 +77,28 @@ function OptimalControlProblems.vanderpol( @expressions( model, begin - # - step, tf / N + Δt, (tf - t0) / N # dynamics - dx1[i = 0:N], x2[i] - dx2[i = 0:N], ε * ω * (1 - x1[i]^2) * x2[i] - ω^2 * x1[i] + u[i] + dx₁[i = 0:N], x₂[i] + dx₂[i = 0:N], ε * ω * (1 - x₁[i]^2) * x₂[i] - ω^2 * x₁[i] + u[i] # objective - dc[i = 0:N], 0.5 * (x1[i]^2 + x2[i]^2 + u[i]^2) + dc[i = 0:N], 0.5 * (x₁[i]^2 + x₂[i]^2 + u[i]^2) end ) @constraints( model, begin - ∂x1[i = 1:N], x1[i] == x1[i - 1] + 0.5 * step * (dx1[i] + dx1[i - 1]) - ∂x2[i = 1:N], x2[i] == x2[i - 1] + 0.5 * step * (dx2[i] + dx2[i - 1]) + ∂x₁[i = 1:N], x₁[i] == x₁[i - 1] + 0.5 * Δt * (dx₁[i] + dx₁[i - 1]) + ∂x₂[i = 1:N], x₂[i] == x₂[i - 1] + 0.5 * Δt * (dx₂[i] + dx₂[i - 1]) end ) # objective - @objective(model, Min, 0.5 * step * sum(dc[i] + dc[i - 1] for i in 1:N)) + @objective(model, Min, 0.5 * Δt * sum(dc[i] + dc[i - 1] for i in 1:N)) return model -end +end \ No newline at end of file diff --git a/ext/MetaData/beam.jl b/ext/MetaData/beam.jl index 27804ebe..769fdf42 100644 --- a/ext/MetaData/beam.jl +++ b/ext/MetaData/beam.jl @@ -1,10 +1,13 @@ beam_meta = OrderedDict( - :name => "beam", - :N => 500, - :minimise => true, - :state_name => ["x1", "x2"], - :costate_name => ["∂x1", "∂x2"], - :control_name => ["u"], - :variable_name => nothing, - :final_time => (:fixed, 1), + :grid_size => 500, + :parameters => ( + t0 = 0, + tf = 1, + x₁_l = 0, + x₁_u = 0.1, + x₁_t0 = 0, + x₂_t0 = 1, + x₁_tf = 0, + x₂_tf = -1, + ), ) diff --git a/ext/MetaData/bioreactor.jl b/ext/MetaData/bioreactor.jl index ca363bc0..04cd1385 100644 --- a/ext/MetaData/bioreactor.jl +++ b/ext/MetaData/bioreactor.jl @@ -1,10 +1,26 @@ bioreactor_meta = OrderedDict( - :name => "bioreactor", - :N => 600, - :minimise => false, - :state_name => ["y", "s", "b"], - :costate_name => ["∂y", "∂s", "∂b"], - :control_name => ["u"], - :variable_name => nothing, - :final_time => (:fixed, 200), # the final time is 10N where N = 20 by default + :grid_size => 600, + :parameters => ( + t0 = 0, + tf = 200, # the final time is 10N where N = 20 by default + β = 1, + c = 2, + γ = 1, + halfperiod = 5, + Ks = 0.05, + μ2m = 0.1, + μbar = 1, + r = 0.005, + y_l = 0, + s_l = 0, + b_l = 0.001, + u_l = 0, + u_u = 1, + y_t0_l = 0.05, + y_t0_u = 0.25, + s_t0_l = 0.5, + s_t0_u = 5, + b_t0_l = 0.5, + b_t0_u = 3, + ), ) diff --git a/ext/MetaData/cart_pendulum.jl b/ext/MetaData/cart_pendulum.jl index 7a154d6c..155c018f 100644 --- a/ext/MetaData/cart_pendulum.jl +++ b/ext/MetaData/cart_pendulum.jl @@ -1,10 +1,22 @@ cart_pendulum_meta = OrderedDict( - :name => "cart_pendulum", - :N => 500, - :minimise => true, - :state_name => ["x", "v", "θ", "ω"], - :costate_name => ["∂x", "∂v", "∂θ", "∂ω"], - :control_name => ["Fex"], - :variable_name => ["tf", "ddx"], - :final_time => (:free, 1), # first component of the variable + :grid_size => 500, + :parameters => ( + t0 = 0, + g = 9.81, # gravitation [m/s^2] + L = 1, # pendulum length [m] + m = 1, # pendulum mass [kg] + mcart = 0.5, # cart mass [kg] + Fex_l = -5, + Fex_u = 5, + x_l = -1, + x_u = 1, + v_l = -2, + v_u = 2, + tf_l = 0.1, + x_t0 = 0, + θ_t0 = 0, + ω_t0 = 0, + θ_tf = π, + ω_tf = 0, + ), ) diff --git a/ext/MetaData/chain.jl b/ext/MetaData/chain.jl index 99183aa4..8276d6a7 100644 --- a/ext/MetaData/chain.jl +++ b/ext/MetaData/chain.jl @@ -1,10 +1,12 @@ chain_meta = OrderedDict( - :name => "chain", - :N => 500, - :minimise => true, - :state_name => ["x1", "x2", "x3"], - :costate_name => ["∂x1", "∂x2", "∂x3"], - :control_name => ["u"], - :variable_name => nothing, - :final_time => (:fixed, 1), + :grid_size => 500, + :parameters => ( + t0 = 0, + tf = 1, + L = 4, + a = 1, + b = 3, + x₂_t0 = 0, + x₃_t0 = 0, + ), ) diff --git a/ext/MetaData/dielectrophoretic_particle.jl b/ext/MetaData/dielectrophoretic_particle.jl index a7156f59..251fc569 100644 --- a/ext/MetaData/dielectrophoretic_particle.jl +++ b/ext/MetaData/dielectrophoretic_particle.jl @@ -1,10 +1,14 @@ dielectrophoretic_particle_meta = OrderedDict( - :name => "dielectrophoretic_particle", - :N => 500, - :minimise => true, - :state_name => ["x", "y"], - :costate_name => ["∂x", "∂y"], - :control_name => ["u"], - :variable_name => ["tf"], - :final_time => (:free, 1), # first component of the variable + :grid_size => 500, + :parameters => ( + t0 = 0, + x_t0 = 1, + y_t0 = 0, + x_tf = 2, + α = -0.75, + c = 1, + u_l = -1, + u_u = 1, + tf_l = 0, + ), ) diff --git a/ext/MetaData/double_oscillator.jl b/ext/MetaData/double_oscillator.jl index 41416d8c..eb0e9619 100644 --- a/ext/MetaData/double_oscillator.jl +++ b/ext/MetaData/double_oscillator.jl @@ -1,10 +1,16 @@ double_oscillator_meta = OrderedDict( - :name => "double_oscillator", - :N => 500, - :minimise => true, - :state_name => ["x1", "x2", "x3", "x4"], - :costate_name => ["∂x1", "∂x2", "∂x3", "∂x4"], - :control_name => ["u"], - :variable_name => nothing, - :final_time => (:fixed, 2π), + :grid_size => 500, + :parameters => ( + t0 = 0, + tf = 2π, + m1 = 100, # [kg] + m2 = 2, # [kg] + c = 0.5, # [Ns/m] + k1 = 100, # [N/m] + k2 = 3, # [N/m] + u_l = -1, + u_u = 1, + x₁_t0 = 0, + x₂_t0 = 0, + ), ) diff --git a/ext/MetaData/ducted_fan.jl b/ext/MetaData/ducted_fan.jl index c5d36a1d..4750c7dc 100644 --- a/ext/MetaData/ducted_fan.jl +++ b/ext/MetaData/ducted_fan.jl @@ -1,10 +1,30 @@ ducted_fan_meta = OrderedDict( - :name => "ducted_fan", - :N => 250, - :minimise => true, - :state_name => ["x₁", "v₁", "x₂", "v₂", "α", "vα"], - :costate_name => ["∂x₁", "∂v₁", "∂x₂", "∂v₂", "∂α", "∂vα"], - :control_name => ["u₁", "u₂"], - :variable_name => ["tf"], - :final_time => (:free, 1), # first component of the variable + :grid_size => 250, + :parameters => ( + t0 = 0, + r = 0.2, # [m] + J = 0.05, # [kg.m2] + m = 2.2, # [kg] + mg = 4, # [N] + μ = 1000, + α_l = -deg2rad(30), + α_u = deg2rad(30), + u₁_l = -5, + u₁_u = 5, + u₂_l = 0, + u₂_u = 17, + tf_l = 0.1, + x₁_t0 = 0, + v₁_t0 = 0, + x₂_t0 = 0, + v₂_t0 = 0, + α_t0 = 0, + vα_t0 = 0, + x₁_tf = 1, + v₁_tf = 0, + x₂_tf = 0, + v₂_tf = 0, + α_tf = 0, + vα_tf = 0, + ), ) diff --git a/ext/MetaData/electric_vehicle.jl b/ext/MetaData/electric_vehicle.jl index 28ecd810..968ea52c 100644 --- a/ext/MetaData/electric_vehicle.jl +++ b/ext/MetaData/electric_vehicle.jl @@ -1,10 +1,20 @@ electric_vehicle_meta = OrderedDict( - :name => "electric_vehicle", - :N => 500, - :minimise => true, - :state_name => ["x", "v"], - :costate_name => ["∂x", "∂v"], - :control_name => ["u"], - :variable_name => nothing, - :final_time => (:fixed, 1), + :grid_size => 500, + :parameters => ( + t0 = 0, + tf = 1, + b1 = 1e0, + b2 = 1e0, + h0 = 0.1, + h1 = 1, + h2 = 1e-3, + α0 = 3, + α1 = 0.4, + α2 = -1, + α3 = 0.1, + x_t0 = 0, + v_t0 = 0, + x_tf = 10, + v_tf = 0, + ), ) diff --git a/ext/MetaData/glider.jl b/ext/MetaData/glider.jl index 0522917b..654e7b1d 100644 --- a/ext/MetaData/glider.jl +++ b/ext/MetaData/glider.jl @@ -1,10 +1,26 @@ glider_meta = OrderedDict( - :name => "glider", - :N => 500, - :minimise => false, - :state_name => ["x", "y", "vx", "vy"], - :costate_name => ["∂x", "∂y", "∂vx", "∂vy"], - :control_name => ["cL"], - :variable_name => ["tf"], - :final_time => (:free, 1), # first component of the variable + :grid_size => 500, + :parameters => ( + t0 = 0, + x_t0 = 0, + y_t0 = 1000, + y_tf = 900, + vx_t0 = 13.23, + vx_tf = 13.23, + vy_t0 = -1.288, + vy_tf = -1.288, + u_c = 2.5, + r_t0 = 100, + m = 100, + g = 9.81, + c0 = 0.034, + c1 = 0.069662, + S = 14, + ρ = 1.13, + cL_min = 0, + cL_max = 1.4, + tf_l = 0, + x_l = 0, + vx_l = 0, + ), ) diff --git a/ext/MetaData/insurance.jl b/ext/MetaData/insurance.jl index c7e17882..e440e873 100644 --- a/ext/MetaData/insurance.jl +++ b/ext/MetaData/insurance.jl @@ -1,10 +1,29 @@ insurance_meta = OrderedDict( - :name => "insurance", - :N => 500, - :minimise => false, - :state_name => ["I", "m", "x₃"], - :costate_name => ["∂I", "∂m", "∂x₃"], - :control_name => ["h", "R", "H", "U", "dUdR"], - :variable_name => nothing, - :final_time => (:fixed, 10), + :grid_size => 500, + :parameters => ( + t0 = 0, + tf = 10, + γ = 0.2, + λ = 0.25, + h0 = 1.5, + w = 1, + s = 10, + k = 0, + σ = 0, + α = 4, + I_l = 0, + I_u = 1.5, + m_l = 0, + m_u = 1.5, + h_l = 0, + h_u = 25, + R_l = 0, + H_l = 0, + U_l = 0, + dUdR_l = 0.001, + P_l = 0, + I_t0 = 0, + m_t0 = 0.001, + x₃_t0 = 0, + ), ) diff --git a/ext/MetaData/jackson.jl b/ext/MetaData/jackson.jl index ddf75f85..4b48eca1 100644 --- a/ext/MetaData/jackson.jl +++ b/ext/MetaData/jackson.jl @@ -1,10 +1,21 @@ jackson_meta = OrderedDict( - :name => "jackson", - :N => 500, - :minimise => false, - :state_name => ["a", "b", "x3"], - :costate_name => ["∂a", "∂b", "∂x3"], - :control_name => ["u"], - :variable_name => nothing, - :final_time => (:fixed, 4), + :grid_size => 500, + :parameters => ( + t0 = 0, + tf = 4, + k1 = 1, + k2 = 10, + k3 = 1, + a_l = 0, + a_u = 1.1, + b_l = 0, + b_u = 1.1, + x₃_l = 0, + x₃_u = 1.1, + u_l = 0, + u_u = 1, + a_t0 = 1, + b_t0 = 0, + x₃_t0 = 0, + ), ) diff --git a/ext/MetaData/moonlander.jl b/ext/MetaData/moonlander.jl index 1cb1e371..7b3b36a3 100644 --- a/ext/MetaData/moonlander.jl +++ b/ext/MetaData/moonlander.jl @@ -1,10 +1,25 @@ moonlander_meta = OrderedDict( - :name => "moonlander", - :N => 500, - :minimise => true, - :state_name => ["p1", "p2", "dp1", "dp2", "θ", "dθ"], - :costate_name => ["∂p1", "∂p2", "∂dp1", "∂dp2", "∂θ", "∂dθ"], - :control_name => ["F1", "F2"], - :variable_name => ["tf"], - :final_time => (:free, 1), # first component of the variable + :grid_size => 500, + :parameters => ( + t0 = 0, + m = 1, + g = 9.81, + I = 0.1, + D = 1, + max_thrust = 2*9.81, + tf_l = 0.1, + tf_u = 1.0, + F₁_l = 0, + F₂_l = 0, + p₁_t0 = 0, + p₂_t0 = 0, + dp₁_t0 = 0, + dp₂_t0 = 0, + θ_t0 = 0, + dθ_t0 = 0, + p₁_tf = 5, + p₂_tf = 5, + dp₁_tf = 0, + dp₂_tf = 0, + ), ) diff --git a/ext/MetaData/robbins.jl b/ext/MetaData/robbins.jl index 19932b55..95fbf9f0 100644 --- a/ext/MetaData/robbins.jl +++ b/ext/MetaData/robbins.jl @@ -1,10 +1,17 @@ robbins_meta = OrderedDict( - :name => "robbins", - :N => 500, - :minimise => true, - :state_name => ["x1", "x2", "x3"], - :costate_name => ["∂x1", "∂x2", "∂x3"], - :control_name => ["u"], - :variable_name => nothing, - :final_time => (:fixed, 10), + :grid_size => 500, + :parameters => ( + t0 = 0, + tf = 10, + α = 3, + β = 0, + γ = 0.5, + x₁_l = 0, + x₁_t0 = 1, + x₂_t0 = -2, + x₃_t0 = 0, + x₁_tf = 0, + x₂_tf = 0, + x₃_tf = 0, + ), ) diff --git a/ext/MetaData/robot.jl b/ext/MetaData/robot.jl index e572b558..c69128c8 100644 --- a/ext/MetaData/robot.jl +++ b/ext/MetaData/robot.jl @@ -1,10 +1,35 @@ robot_meta = OrderedDict( - :name => "robot", - :N => 250, - :minimise => true, - :state_name => ["ρ", "dρ", "θ", "dθ", "ϕ", "dϕ"], - :costate_name => ["∂ρ", "∂dρ", "∂θ", "∂dθ", "∂ϕ", "∂dϕ"], - :control_name => ["uρ", "uθ", "uϕ"], - :variable_name => ["tf"], - :final_time => (:free, 1), # first component of the variable + :grid_size => 250, + :parameters => ( + t0 = 0, + L = 5, + # Lower and upper bounds on the controls, + uρ_l = -1, + uρ_u = 1, + uθ_l = -1, + uθ_u = 1, + uϕ_l = -1, + uϕ_u = 1, + # Initial positions of the length and the angles for the robot arm, + ρ_t0 = 4.5, + θ_t0 = 0, + ϕ_t0 = π/4, + dρ_t0 = 0, + dθ_t0 = 0, + dϕ_t0 = 0, + # Final positions + ρ_tf = 4.5, + θ_tf = 2π/3, + ϕ_tf = π/4, + dρ_tf = 0, + dθ_tf = 0, + dϕ_tf = 0, + # + ρ_l = 0, + θ_l = -π, + θ_u = π, + ϕ_l = 0, + ϕ_u = π, + tf_l = 0.1, + ), ) diff --git a/ext/MetaData/rocket.jl b/ext/MetaData/rocket.jl index b55d126b..dba1f1f6 100644 --- a/ext/MetaData/rocket.jl +++ b/ext/MetaData/rocket.jl @@ -1,10 +1,25 @@ rocket_meta = OrderedDict( - :name => "rocket", - :N => 500, - :minimise => false, - :state_name => ["h", "v", "m"], - :costate_name => ["∂h", "∂v", "∂m"], - :control_name => ["T"], - :variable_name => ["tf"], - :final_time => (:free, 1), # first component of the variable + :grid_size => 500, + :state_components => ["h", "v", "m"], + :costate_components => ["∂h", "∂v", "∂m"], + :control_components => ["T"], + :variable_components => ["tf"], + :time_grid_names => Dict( + :initial_time => "t0", + :final_time => "tf", + :grid_size => "N", + ), + :parameters => ( + t0 = 0, + h_t0 = 1, + v_t0 = 0, + m_t0 = 1, + g0 = 1, + Tc = 3.5, + hc = 500, + vc = 620, + mc = 0.6, + T_l = 0, + tf_l = 0, + ), ) diff --git a/ext/MetaData/space_shuttle.jl b/ext/MetaData/space_shuttle.jl index 7c43dca3..c96c05ae 100644 --- a/ext/MetaData/space_shuttle.jl +++ b/ext/MetaData/space_shuttle.jl @@ -1,10 +1,52 @@ space_shuttle_meta = OrderedDict( - :name => "space_shuttle", - :N => 500, - :minimise => false, - :state_name => ["scaled_h", "ϕ", "θ", "scaled_v", "γ", "ψ"], - :costate_name => ["∂h", "∂ϕ", "∂θ", "∂v", "∂γ", "∂ψ"], - :control_name => ["α", "β"], - :variable_name => ["tf"], - :final_time => (:free, 1), # first component of the variable + :grid_size => 500, + :parameters => ( + t0 = 0, + w = 203000.0, # weight (lb), + g₀ = 32.174, # acceleration (ft/sec^2), + ## Aerodynamic and atmospheric forces on the vehicle, + ρ₀ = 0.002378, + hᵣ = 23800, + Rₑ = 20902900, + μ = 0.14076539e17, + S = 2690, + a₀ = -0.20704, + a₁ = 0.029244, + b₀ = 0.07854, + b₁ = -0.61592e-2, + b₂ = 0.621408e-3, + # , + Δt_min = 3.5, + Δt_max = 4.5, + ## Initial conditions, + h_t0 = 2.6, # altitude (ft) / 1e5, + ϕ_t0 = deg2rad(0), # longitude (rad), + θ_t0 = deg2rad(0), # latitude (rad), + v_t0 = 2.56, # velocity (ft/sec) / 1e4, + γ_t0 = deg2rad(-1), # flight path angle (rad), + ψ_t0 = deg2rad(90), # azimuth (rad), + α_t0 = deg2rad(0), # angle of attack (rad), + ## for initial guess + α_s = deg2rad(0), # angle of attack (rad) + β_s = deg2rad(0), # bank angle (rad), + ## Final conditions, the so-called Terminal Area Energy Management (TAEM), + h_tf = 0.8, # altitude (ft) / 1e5, + v_tf = 0.25, # velocity (ft/sec) / 1e4, + γ_tf = deg2rad(-5), # flight path angle (rad), + # + h_l = 0, + ϕ_l = -2π, + ϕ_u = 2π, + θ_l = deg2rad(-89), + θ_u = deg2rad(89), + v_l = 1e-4, + γ_l = deg2rad(-89), + γ_u = deg2rad(89), + ψ_l = -2π, + ψ_u = 2π, + α_l = deg2rad(-90), + α_u = deg2rad(90), + β_l = deg2rad(-89), + β_u = deg2rad(1), + ), ) diff --git a/ext/MetaData/steering.jl b/ext/MetaData/steering.jl index 19138cb8..67a7fdd6 100644 --- a/ext/MetaData/steering.jl +++ b/ext/MetaData/steering.jl @@ -1,10 +1,17 @@ steering_meta = OrderedDict( - :name => "steering", - :N => 500, - :minimise => true, - :state_name => ["x1", "x2", "x3", "x4"], - :costate_name => ["∂x1", "∂x2", "∂x3", "∂x4"], - :control_name => ["u"], - :variable_name => ["tf"], - :final_time => (:free, 1), # first component of the variable + :grid_size => 500, + :parameters => ( + t0 = 0, + a = 100, + u_min = -π/2, + u_max = π/2, + tf_l = 0, + x₁_t0 = 0, + x₂_t0 = 0, + x₃_t0 = 0, + x₄_t0 = 0, + x₂_tf = 5, + x₃_tf = 45, + x₄_tf = 0, + ), ) diff --git a/ext/MetaData/vanderpol.jl b/ext/MetaData/vanderpol.jl index 45623ad8..43d0c14d 100644 --- a/ext/MetaData/vanderpol.jl +++ b/ext/MetaData/vanderpol.jl @@ -1,10 +1,11 @@ vanderpol_meta = OrderedDict( - :name => "vanderpol", - :N => 500, - :minimise => true, - :state_name => ["x1", "x2"], - :costate_name => ["∂x1", "∂x2"], - :control_name => ["u"], - :variable_name => nothing, - :final_time => (:fixed, 2), + :grid_size => 500, + :parameters => ( + t0 = 0, + tf = 2, + ω = 1, + ε = 1, + x₁_t0 = 1, + x₂_t0 = 0, + ), ) diff --git a/ext/OptimalControlModels.jl b/ext/OptimalControlModels.jl index 59e86fcb..22ce8397 100644 --- a/ext/OptimalControlModels.jl +++ b/ext/OptimalControlModels.jl @@ -5,14 +5,21 @@ using OptimalControl using DocStringExtensions using OrderedCollections: OrderedDict +# list of problems +list_of_problems = OptimalControlProblems.problems() + # include problems files rel_path_problems = "OptimalControlModels" path = joinpath(dirname(@__FILE__), rel_path_problems) -files = filter(x -> x[(end - 2):end] == ".jl", readdir(path)) -for file in files - if file ≠ "OptimalControlModels.jl" - include(joinpath(rel_path_problems, file)) - end +for problem in list_of_problems + include(joinpath(rel_path_problems, "$(problem).jl")) +end + +# include problems files (_s versions) +rel_path_problems = "OptimalControlModels_s" +path = joinpath(dirname(@__FILE__), rel_path_problems) +for problem in list_of_problems + include(joinpath(rel_path_problems, "$(problem)_s.jl")) end end diff --git a/ext/OptimalControlModels/beam.jl b/ext/OptimalControlModels/beam.jl index 766c8328..77f0d132 100644 --- a/ext/OptimalControlModels/beam.jl +++ b/ext/OptimalControlModels/beam.jl @@ -8,7 +8,7 @@ It then performs direct transcription to generate a discrete optimal control pro # Arguments - `::OptimalControlBackend`: Placeholder type to specify the OptimalControl backend or solver interface. -- `N::Int=500`: (Keyword) Number of discretisation points for the direct transcription grid. +- `grid_size::Int=500`: (Keyword) Number of discretisation points for the direct transcription grid. # Returns @@ -30,22 +30,34 @@ julia> docp = OptimalControlProblems.beam(OptimalControlBackend(); N=100); function OptimalControlProblems.beam( ::OptimalControlBackend, description::Symbol...; - N::Int=steps_number_data(:beam), + grid_size::Int=grid_size_data(:beam), + parameters::Union{Nothing, NamedTuple}=nothing, kwargs..., ) - # - tf = final_time_data(:beam) + # parameters + params = parameters_data(:beam, parameters) + t0 = params[:t0] + tf = params[:tf] + x₁_l = params[:x₁_l] + x₁_u = params[:x₁_u] + x₁_t0 = params[:x₁_t0] + x₂_t0 = params[:x₂_t0] + x₁_tf = params[:x₁_tf] + x₂_tf = params[:x₂_tf] # model ocp = @def begin - t ∈ [0, tf], time + t ∈ [t0, tf], time x ∈ R², state u ∈ R, control - x(0) == [0, 1] - x(tf) == [0, -1] + + x(t0) == [x₁_t0, x₂_t0] + x(tf) == [x₁_tf, x₂_tf] + x₁_l ≤ x₁(t) ≤ x₁_u + ẋ(t) == [x₂(t), u(t)] - 0 ≤ x₁(t) ≤ 0.1 + ∫(u(t)^2) → min end @@ -58,7 +70,7 @@ function OptimalControlProblems.beam( description...; lagrange_to_mayer=false, init=init, - grid_size=N, + grid_size=grid_size, disc_method=:trapeze, kwargs..., ) diff --git a/ext/OptimalControlModels/bioreactor.jl b/ext/OptimalControlModels/bioreactor.jl index 99e5bd28..3721228b 100644 --- a/ext/OptimalControlModels/bioreactor.jl +++ b/ext/OptimalControlModels/bioreactor.jl @@ -8,7 +8,7 @@ It performs direct transcription to create a discretised optimal control problem # Arguments - `::OptimalControlBackend`: Placeholder type specifying the OptimalControl backend or solver interface. -- `N::Int=500`: (Keyword) Number of discretisation points for the direct transcription grid. +- `grid_size::Int=500`: (Keyword) Number of discretisation points for the direct transcription grid. # Returns @@ -30,51 +30,44 @@ julia> docp = OptimalControlProblems.bioreactor(OptimalControlBackend(); N=100); function OptimalControlProblems.bioreactor( ::OptimalControlBackend, description::Symbol...; - N::Int=steps_number_data(:bioreactor), + grid_size::Int=grid_size_data(:bioreactor), + parameters::Union{Nothing, NamedTuple}=nothing, kwargs..., ) - # METHANE PROBLEM - # μ2 according to growth model - # μ according to light model - # time scale is [0,10] for 24h (day then night) - - # growth model MONOD - function growth(s, μ2m, Ks) - return μ2m * s / (s + Ks) - end - - # light model: max^2 (0,sin) * μbar - # DAY/NIGHT CYCLE: [0,2 halfperiod] rescaled to [0,2pi] - function light(time, halfperiod) - days = time / (halfperiod * 2) - tau = (days - floor(days)) * 2π - return max(0, sin(tau))^2 - end - # parameters - β = 1 - c = 2 - γ = 1 - halfperiod = 5 - Ks = 0.05 - μ2m = 0.1 - μbar = 1 - r = 0.005 - T = final_time_data(:bioreactor) + params = parameters_data(:bioreactor, parameters) + t0 = params[:t0] + tf = params[:tf] + β = params[:β] + c = params[:c] + γ = params[:γ] + halfperiod = params[:halfperiod] + Ks = params[:Ks] + μ2m = params[:μ2m] + μbar = params[:μbar] + r = params[:r] + y_l = params[:y_l] + s_l = params[:s_l] + b_l = params[:b_l] + u_l = params[:u_l] + u_u = params[:u_u] + y_t0_l = params[:y_t0_l] + y_t0_u = params[:y_t0_u] + s_t0_l = params[:s_t0_l] + s_t0_u = params[:s_t0_u] + b_t0_l = params[:b_t0_l] + b_t0_u = params[:b_t0_u] # Model ocp = @def begin - t ∈ [0, T], time + t ∈ [t0, tf], time x = (y, s, b) ∈ R³, state u ∈ R, control - x(t) ≥ [0, 0, 0.001] - 0 ≤ u(t) ≤ 1 - - 0.05 ≤ y(0) ≤ 0.25 - 0.5 ≤ s(0) ≤ 5 - 0.5 ≤ b(0) ≤ 3 + x(t) ≥ [y_l, s_l, b_l] + u_l ≤ u(t) ≤ u_u + [y_t0_l, s_t0_l, b_t0_l] ≤ x(t0) ≤ [y_t0_u, s_t0_u, b_t0_u] μ = light(t, halfperiod) * μbar μ2 = growth(s(t), μ2m, Ks) @@ -85,7 +78,25 @@ function OptimalControlProblems.bioreactor( (μ2 - u(t) * β) * b(t), ] - -∫(μ2 * b(t) / (β + c)) → min + ∫(μ2 * b(t) / (β + c)) → max + end + + # METHANE PROBLEM + # μ2 according to growth model + # μ according to light model + # time scale is [0,10] for 24h (day then night) + + # growth model MONOD + function growth(s, μ2m, Ks) + return μ2m * s / (s + Ks) + end + + # light model: max^2 (0,sin) * μbar + # DAY/NIGHT CYCLE: [0,2 halfperiod] rescaled to [0,2pi] + function light(time, halfperiod) + days = time / (halfperiod * 2) + tau = (days - floor(days)) * 2π + return max(0, sin(tau))^2 end # initial guess @@ -97,7 +108,7 @@ function OptimalControlProblems.bioreactor( description...; lagrange_to_mayer=false, init=init, - grid_size=N, + grid_size=grid_size, disc_method=:trapeze, kwargs..., ) diff --git a/ext/OptimalControlModels/cart_pendulum.jl b/ext/OptimalControlModels/cart_pendulum.jl index a551f98f..651d6337 100644 --- a/ext/OptimalControlModels/cart_pendulum.jl +++ b/ext/OptimalControlModels/cart_pendulum.jl @@ -8,7 +8,7 @@ It performs direct transcription to produce a discretised optimal control proble # Arguments - `::OptimalControlBackend`: Placeholder type specifying the OptimalControl backend or solver interface. -- `N::Int=500`: (Keyword) Number of discretisation points for the direct transcription grid. +- `grid_size::Int=500`: (Keyword) Number of discretisation points for the direct transcription grid. # Returns @@ -30,46 +30,57 @@ julia> docp = OptimalControlProblems.cart_pendulum(OptimalControlBackend(); N=10 function OptimalControlProblems.cart_pendulum( ::OptimalControlBackend, description::Symbol...; - N::Int=steps_number_data(:cart_pendulum), + grid_size::Int=grid_size_data(:cart_pendulum), + parameters::Union{Nothing, NamedTuple}=nothing, kwargs..., ) # parameters - g = 9.81 # gravitation [m/s^2] - L = 1 # pendulum length [m] - m = 1 # pendulum mass [kg] + params = parameters_data(:cart_pendulum, parameters) + t0 = params[:t0] + g = params[:g] + L = params[:L] + m = params[:m] I = m * L^2 / 12 # pendulum moment of inertia - mcart = 0.5 # cart mass [kg] - max_f = 5 - max_x = 1 - max_v = 2 + mcart = params[:mcart] + Fex_l = params[:Fex_l] + Fex_u = params[:Fex_u] + x_l = params[:x_l] + x_u = params[:x_u] + v_l = params[:v_l] + v_u = params[:v_u] + tf_l = params[:tf_l] + x_t0 = params[:x_t0] + θ_t0 = params[:θ_t0] + ω_t0 = params[:ω_t0] + θ_tf = params[:θ_tf] + ω_tf = params[:ω_tf] ocp = @def begin - # time, variable, state and control w = (tf, ddx) ∈ R², variable - t ∈ [0, tf], time + t ∈ [t0, tf], time y = (x, v, θ, ω) ∈ R⁴, state Fex ∈ R, control # state constraints - -max_x ≤ x(t) ≤ max_x, (x_con) - -max_v ≤ v(t) ≤ max_v, (v_con) + x_l ≤ x(t) ≤ x_u, (x_c) + v_l ≤ v(t) ≤ v_u, (v_c) # control constraints - -max_f ≤ Fex(t) ≤ max_f, (Fex_con) + Fex_l ≤ Fex(t) ≤ Fex_u, (Fex_c) # variables constraints - tf ≥ 0.1, (tf_con) + tf ≥ tf_l, (tf_c) # initial conditions - x(0) == 0, (x_ic) - θ(0) == 0, (θ_ic) - ω(0) == 0, (ω_ic) + x(t0) == x_t0, (x_t0) + θ(t0) == θ_t0, (θ_t0) + ω(t0) == ω_t0, (ω_t0) # final conditions - θ(tf) == π, (θ_fc) - ω(tf) == 0, (ω_fc) + θ(tf) == θ_tf, (θ_tf) + ω(tf) == ω_tf, (ω_tf) # dynamics ẏ(t) == dynamics(v(t), θ(t), ω(t), Fex(t), ddx) @@ -110,7 +121,7 @@ function OptimalControlProblems.cart_pendulum( description...; lagrange_to_mayer=false, init=init, - grid_size=N, + grid_size=grid_size, disc_method=:trapeze, kwargs..., ) diff --git a/ext/OptimalControlModels/chain.jl b/ext/OptimalControlModels/chain.jl index e4111be6..cf19f21e 100644 --- a/ext/OptimalControlModels/chain.jl +++ b/ext/OptimalControlModels/chain.jl @@ -8,7 +8,7 @@ It performs direct transcription to produce a discretised optimal control proble # Arguments - `::OptimalControlBackend`: Placeholder type specifying the OptimalControl backend or solver interface. -- `N::Int=500`: (Keyword) Number of discretisation points for the direct transcription grid. +- `grid_size::Int=500`: (Keyword) Number of discretisation points for the direct transcription grid. # Returns @@ -31,32 +31,38 @@ julia> docp = OptimalControlProblems.chain(OptimalControlBackend(); N=100); function OptimalControlProblems.chain( ::OptimalControlBackend, description::Symbol...; - N::Int=steps_number_data(:chain), + grid_size::Int=grid_size_data(:chain), + parameters::Union{Nothing, NamedTuple}=nothing, kwargs..., ) # parameters - L = 4 - a = 1 - b = 3 - tf = final_time_data(:chain) + params = parameters_data(:chain, parameters) + t0 = params[:t0] + tf = params[:tf] + L = params[:L] + a = params[:a] + b = params[:b] + x₁_t0 = a + x₂_t0 = params[:x₂_t0] + x₃_t0 = params[:x₃_t0] + x₁_tf = b + x₃_tf = L # model ocp = @def begin - - # - t ∈ [0, tf], time + t ∈ [t0, tf], time x ∈ R³, state u ∈ R, control # initial conditions - x₁(0) == a, (x1_ic) - x₂(0) == 0, (x2_ic) - x₃(0) == 0, (x3_ic) + x₁(t0) == x₁_t0, (x₁_t0) + x₂(t0) == x₂_t0, (x₂_t0) + x₃(t0) == x₃_t0, (x₃_t0) # final conditions - x₁(tf) == b, (x1_con) - x₃(tf) == L, (x3_con) + x₁(tf) == x₁_tf, (x₁_tf) + x₃(tf) == x₃_tf, (x₃_tf) # dynamics ẋ(t) == dynamics(x(t), u(t)) @@ -74,12 +80,12 @@ function OptimalControlProblems.chain( tmin = b > a ? 1 / 4 : 3 / 4 xinit = t -> [ - 4 * abs(b - a) * t / tf * (0.5 * t / tf - tmin) + a, - (4 * abs(b - a) * t / tf * (0.5 * t / tf - tmin) + a) * - (4 * abs(b - a) * (t / tf - tmin)), - 4 * abs(b - a) * (t / tf - tmin), + 4 * abs(b - a) * (t - t0) / (tf - t0) * (0.5 * (t - t0) / (tf - t0) - tmin) + a, + (4 * abs(b - a) * (t - t0) / (tf - t0) * (0.5 * (t - t0) / (tf - t0) - tmin) + a) * + (4 * abs(b - a) * ((t - t0) / (tf - t0) - tmin)), + 4 * abs(b - a) * ((t - t0) / (tf - t0) - tmin), ] - uinit = t -> 4 * abs(b - a) * (t / tf - tmin) + uinit = t -> 4 * abs(b - a) * ((t - t0) / (tf - t0) - tmin) init = (state=xinit, control=uinit) # discretise the optimal control problem @@ -88,7 +94,7 @@ function OptimalControlProblems.chain( description...; lagrange_to_mayer=false, init=init, - grid_size=N, + grid_size=grid_size, disc_method=:trapeze, kwargs..., ) diff --git a/ext/OptimalControlModels/dielectrophoretic_particle.jl b/ext/OptimalControlModels/dielectrophoretic_particle.jl index e3dbfa75..324b1092 100644 --- a/ext/OptimalControlModels/dielectrophoretic_particle.jl +++ b/ext/OptimalControlModels/dielectrophoretic_particle.jl @@ -8,7 +8,7 @@ It performs direct transcription to produce a discretised optimal control proble # Arguments - `::OptimalControlBackend`: Placeholder type specifying the OptimalControl backend or solver interface. -- `N::Int=500`: (Keyword) Number of discretisation points for the direct transcription grid. +- `grid_size::Int=500`: (Keyword) Number of discretisation points for the direct transcription grid. # Returns @@ -31,28 +31,34 @@ julia> docp = OptimalControlProblems.dielectrophoretic_particle(OptimalControlBa function OptimalControlProblems.dielectrophoretic_particle( ::OptimalControlBackend, description::Symbol...; - N::Int=steps_number_data(:dielectrophoretic_particle), + grid_size::Int=grid_size_data(:dielectrophoretic_particle), + parameters::Union{Nothing, NamedTuple}=nothing, kwargs..., ) # parameters - x0 = 1 - xf = 2 - α = -0.75 - c = 1 + params = parameters_data(:dielectrophoretic_particle, parameters) + t0 = params[:t0] + x_t0 = params[:x_t0] + y_t0 = params[:y_t0] + x_tf = params[:x_tf] + α = params[:α] + c = params[:c] + u_l = params[:u_l] + u_u = params[:u_u] + tf_l = params[:tf_l] ocp = @def begin tf ∈ R, variable - t ∈ [0, tf], time + t ∈ [t0, tf], time q = (x, y) ∈ R², state u ∈ R, control - x(0) == x0, (x0_con) - y(0) == 0, (y0_con) - x(tf) == xf, (xf_con) - - tf ≥ 0, (tf_con) - -1 ≤ u(t) ≤ 1, (u_con) + x(t0) == x_t0, (x_t0) + y(t0) == y_t0, (y_t0) + x(tf) == x_tf, (x_tf) + tf ≥ tf_l, (tf_c) + u_l ≤ u(t) ≤ u_u, (u_c) q̇(t) == dynamics(y(t), u(t)) @@ -72,7 +78,7 @@ function OptimalControlProblems.dielectrophoretic_particle( description...; lagrange_to_mayer=false, init=init, - grid_size=N, + grid_size=grid_size, disc_method=:trapeze, kwargs..., ) diff --git a/ext/OptimalControlModels/double_oscillator.jl b/ext/OptimalControlModels/double_oscillator.jl index 36139e70..82b87bb7 100644 --- a/ext/OptimalControlModels/double_oscillator.jl +++ b/ext/OptimalControlModels/double_oscillator.jl @@ -8,7 +8,7 @@ It uses direct transcription to produce a discretised optimal control problem (D # Arguments - `::OptimalControlBackend`: Placeholder type specifying the OptimalControl backend or solver interface. -- `N::Int=500`: (Keyword) Number of discretisation points for the direct transcription grid. +- `grid_size::Int=500`: (Keyword) Number of discretisation points for the direct transcription grid. # Returns @@ -31,28 +31,34 @@ julia> docp = OptimalControlProblems.double_oscillator(OptimalControlBackend(); function OptimalControlProblems.double_oscillator( ::OptimalControlBackend, description::Symbol...; - N::Int=steps_number_data(:double_oscillator), + grid_size::Int=grid_size_data(:double_oscillator), + parameters::Union{Nothing, NamedTuple}=nothing, kwargs..., ) # parameters - m1 = 100 # [kg] - m2 = 2 # [kg] - c = 0.5 # [Ns/m] - k1 = 100 # [N/m] - k2 = 3 # [N/m] - tf = final_time_data(:double_oscillator) + params = parameters_data(:double_oscillator, parameters) + t0 = params[:t0] + tf = params[:tf] + m1 = params[:m1] + m2 = params[:m2] + c = params[:c] + k1 = params[:k1] + k2 = params[:k2] + u_l = params[:u_l] + u_u = params[:u_u] + x₁_t0 = params[:x₁_t0] + x₂_t0 = params[:x₂_t0] # model ocp = @def begin - t ∈ [0, tf], time + t ∈ [t0, tf], time x ∈ R⁴, state u ∈ R, control - - -1 ≤ u(t) ≤ 1, (u_con) - - x₁(0) == 0, (x1_con) - x₂(0) == 0, (x2_con) + + u_l ≤ u(t) ≤ u_u, (u_c) + x₁(t0) == x₁_t0, (x₁_t0) + x₂(t0) == x₂_t0, (x₂_t0) ẋ(t) == dynamics(x(t), u(t), F(t)) @@ -60,20 +66,20 @@ function OptimalControlProblems.double_oscillator( end function F(t) - return sin(t * 2π / tf) + return sin((t - t0) * 2π / (tf - t0)) end function dynamics(x, u, F) - x1, x2, x3, x4 = x - dx1 = x3 - dx2 = x4 - dx3 = -(k1 + k2) / m1 * x1 + k2 / m1 * x2 + 1 / m1 * F - dx4 = k2 / m2 * x1 - k2 / m2 * x2 - c * (1 - u) / m2 * x4 - return [dx1, dx2, dx3, dx4] + x₁, x₂, x₃, x₄ = x + dx₁ = x₃ + dx₂ = x₄ + dx₃ = -(k1 + k2) / m1 * x₁ + k2 / m1 * x₂ + 1 / m1 * F + dx₄ = k2 / m2 * x₁ - k2 / m2 * x₂ - c * (1 - u) / m2 * x₄ + return [dx₁, dx₂, dx₃, dx₄] end # initial guess - xinit = [0.1, 0.1, 0.1, 0.1] # [x1, x2, x3, x4] + xinit = [0.1, 0.1, 0.1, 0.1] # [x₁, x₂, x₃, x₄] uinit = [0.1] # [u] init = (state=xinit, control=uinit) @@ -83,7 +89,7 @@ function OptimalControlProblems.double_oscillator( description...; lagrange_to_mayer=false, init=init, - grid_size=N, + grid_size=grid_size, disc_method=:trapeze, kwargs..., ) diff --git a/ext/OptimalControlModels/ducted_fan.jl b/ext/OptimalControlModels/ducted_fan.jl index ba077e2e..f6101bd6 100644 --- a/ext/OptimalControlModels/ducted_fan.jl +++ b/ext/OptimalControlModels/ducted_fan.jl @@ -8,7 +8,7 @@ It returns both a discretised direct optimal control problem (DOCP) and the corr # Arguments - `::OptimalControlBackend`: Placeholder type specifying the OptimalControl backend or solver interface. -- `N::Int=250`: (Keyword) Number of discretisation points for the direct transcription grid. +- `grid_size::Int=250`: (Keyword) Number of discretisation points for the direct transcription grid. # Returns @@ -31,48 +31,70 @@ julia> docp = OptimalControlProblems.ducted_fan(OptimalControlBackend(); N=250); function OptimalControlProblems.ducted_fan( ::OptimalControlBackend, description::Symbol...; - N::Int=steps_number_data(:ducted_fan), + grid_size::Int=grid_size_data(:ducted_fan), + parameters::Union{Nothing, NamedTuple}=nothing, kwargs..., ) # parameters - r = 0.2 # [m] - J = 0.05 # [kg.m2] - m = 2.2 # [kg] - mg = 4 # [N] - μ = 1000 + params = parameters_data(:ducted_fan, parameters) + t0 = params[:t0] + r = params[:r] + J = params[:J] + m = params[:m] + mg = params[:mg] + μ = params[:μ] + α_l = params[:α_l] + α_u = params[:α_u] + u₁_l = params[:u₁_l] + u₁_u = params[:u₁_u] + u₂_l = params[:u₂_l] + u₂_u = params[:u₂_u] + tf_l = params[:tf_l] + x₁_t0 = params[:x₁_t0] + v₁_t0 = params[:v₁_t0] + x₂_t0 = params[:x₂_t0] + v₂_t0 = params[:v₂_t0] + α_t0 = params[:α_t0] + vα_t0 = params[:vα_t0] + x₁_tf = params[:x₁_tf] + v₁_tf = params[:v₁_tf] + x₂_tf = params[:x₂_tf] + v₂_tf = params[:v₂_tf] + α_tf = params[:α_tf] + vα_tf = params[:vα_tf] ocp = @def begin tf ∈ R, variable - t ∈ [0, tf], time + t ∈ [t0, tf], time x = (x₁, v₁, x₂, v₂, α, vα) ∈ R⁶, state u ∈ R², control # tf constraints - tf ≥ 0.1, (tf_con) + tf ≥ tf_l, (tf_c) # state constraints - -deg2rad(30) ≤ α(t) ≤ deg2rad(30), (α_con) + α_l ≤ α(t) ≤ α_u, (α_c) # control constraints - -5 ≤ u₁(t) ≤ 5, (u₁_con) - 0 ≤ u₂(t) ≤ 17, (u₂_con) + u₁_l ≤ u₁(t) ≤ u₁_u, (u₁_c) + u₂_l ≤ u₂(t) ≤ u₂_u, (u₂_c) # initial constraints - x₁(0) == 0, (x₁_i) - v₁(0) == 0, (v₁_i) - x₂(0) == 0, (x₂_i) - v₂(0) == 0, (v₂_i) - α(0) == 0, (α_i) - vα(0) == 0, (vα_i) + x₁(t0) == x₁_t0, (x₁_t0) + v₁(t0) == v₁_t0, (v₁_t0) + x₂(t0) == x₂_t0, (x₂_t0) + v₂(t0) == v₂_t0, (v₂_t0) + α(t0) == α_t0, (α_t0) + vα(t0) == vα_t0, (vα_t0) # final constraints - x₁(tf) == 1, (x₁_f) - v₁(tf) == 0, (v₁_f) - x₂(tf) == 0, (x₂_f) - v₂(tf) == 0, (v₂_f) - α(tf) == 0, (α_f) - vα(tf) == 0, (vα_f) + x₁(tf) == x₁_tf, (x₁_tf) + v₁(tf) == v₁_tf, (v₁_tf) + x₂(tf) == x₂_tf, (x₂_tf) + v₂(tf) == v₂_tf, (v₂_tf) + α(tf) == α_tf , (α_tf ) + vα(tf) == vα_tf, (vα_tf) # dynamics ẋ(t) == dynamics(v₁(t), v₂(t), α(t), vα(t), u₁(t), u₂(t)) @@ -104,7 +126,7 @@ function OptimalControlProblems.ducted_fan( description...; lagrange_to_mayer=false, init=init, - grid_size=N, + grid_size=grid_size, disc_method=:trapeze, kwargs..., ) diff --git a/ext/OptimalControlModels/electric_vehicle.jl b/ext/OptimalControlModels/electric_vehicle.jl index 6c0f42c0..e294293b 100644 --- a/ext/OptimalControlModels/electric_vehicle.jl +++ b/ext/OptimalControlModels/electric_vehicle.jl @@ -8,7 +8,7 @@ It returns both a discretised direct optimal control problem (DOCP) and the corr # Arguments - `::OptimalControlBackend`: Placeholder type specifying the OptimalControl backend or solver interface. -- `N::Int=500`: (Keyword) Number of discretisation points for the direct transcription grid. +- `grid_size::Int=500`: (Keyword) Number of discretisation points for the direct transcription grid. # Returns @@ -31,30 +31,39 @@ julia> docp = OptimalControlProblems.electric_vehicle(OptimalControlBackend(); N function OptimalControlProblems.electric_vehicle( ::OptimalControlBackend, description::Symbol...; - N::Int=steps_number_data(:electric_vehicle), + grid_size::Int=grid_size_data(:electric_vehicle), + parameters::Union{Nothing, NamedTuple}=nothing, kwargs..., ) # parameters - tf = final_time_data(:electric_vehicle) - D = 10 - b1 = 1e0 - b2 = 1e0 - h0 = 0.1 - h1 = 1 - h2 = 1e-3 - α0, α1, α2, α3 = (3, 0.4, -1, 0.1) + params = parameters_data(:electric_vehicle, parameters) + t0 = params[:t0] + tf = params[:tf] + b1 = params[:b1] + b2 = params[:b2] + h0 = params[:h0] + h1 = params[:h1] + h2 = params[:h2] + α0 = params[:α0] + α1 = params[:α1] + α2 = params[:α2] + α3 = params[:α3] + x_t0 = params[:x_t0] + v_t0 = params[:v_t0] + x_tf = params[:x_tf] + v_tf = params[:v_tf] # model ocp = @def begin - t ∈ [0, tf], time + t ∈ [t0, tf], time y = (x, v) ∈ R², state u ∈ R, control - x(0) == 0, (x_i) - v(0) == 0, (v_i) - x(tf) == D, (x_f) - v(tf) == 0, (v_f) + x(t0) == x_t0, (x_t0) + v(t0) == v_t0, (v_t0) + x(tf) == x_tf, (x_tf) + v(tf) == v_tf, (v_tf) ẏ(t) == dynamics(x(t), v(t), u(t)) @@ -75,7 +84,7 @@ function OptimalControlProblems.electric_vehicle( description...; lagrange_to_mayer=false, init=init, - grid_size=N, + grid_size=grid_size, disc_method=:trapeze, kwargs..., ) diff --git a/ext/OptimalControlModels/glider.jl b/ext/OptimalControlModels/glider.jl index 208d41fc..5919573d 100644 --- a/ext/OptimalControlModels/glider.jl +++ b/ext/OptimalControlModels/glider.jl @@ -8,7 +8,7 @@ It returns both a discretised direct optimal control problem (DOCP) and the corr # Arguments - `::OptimalControlBackend`: Placeholder type specifying the OptimalControl backend or solver interface. -- `N::Int=500`: (Keyword) Number of discretisation points for the direct transcription grid. +- `grid_size::Int=500`: (Keyword) Number of discretisation points for the direct transcription grid. # Returns @@ -31,64 +31,70 @@ julia> docp = OptimalControlProblems.glider(OptimalControlBackend(); N=500); function OptimalControlProblems.glider( ::OptimalControlBackend, description::Symbol...; - N::Int=steps_number_data(:glider), + grid_size::Int=grid_size_data(:glider), + parameters::Union{Nothing, NamedTuple}=nothing, kwargs..., ) # parameters - x_0 = 0 - y_0 = 1000 - y_f = 900 - vx_0 = 13.23 - vx_f = 13.23 - vy_0 = -1.288 - vy_f = -1.288 - u_c = 2.5 - r_0 = 100 - m = 100 - g = 9.81 - c0 = 0.034 - c1 = 0.069662 - S = 14 - ρ = 1.13 - cL_min = 0 - cL_max = 1.4 + params = parameters_data(:glider, parameters) + t0 = params[:t0] + x_t0 = params[:x_t0] + y_t0 = params[:y_t0] + y_tf = params[:y_tf] + vx_t0 = params[:vx_t0] + vx_tf = params[:vx_tf] + vy_t0 = params[:vy_t0] + vy_tf = params[:vy_tf] + u_c = params[:u_c] + r_t0 = params[:r_t0] + m = params[:m] + g = params[:g] + c0 = params[:c0] + c1 = params[:c1] + S = params[:S] + ρ = params[:ρ] + cL_min = params[:cL_min] + cL_max = params[:cL_max] + tf_l = params[:tf_l] + x_l = params[:x_l] + vx_l = params[:vx_l] # model ocp = @def begin tf ∈ R, variable - t ∈ [0, tf], time + t ∈ [t0, tf], time z = (x, y, vx, vy) ∈ R⁴, state cL ∈ R, control # state constraints - x(t) ≥ 0, (x_con) - vx(t) ≥ 0, (vx_con) + x(t) ≥ x_l, (x_c) + vx(t) ≥ vx_l, (vx_c) # control constraints - cL_min ≤ cL(t) ≤ cL_max, (cL_con) + cL_min ≤ cL(t) ≤ cL_max, (cL_c) # initial conditions - x(0) == x_0, (x0_con) - y(0) == y_0, (y0_con) - vx(0) == vx_0, (vx0_con) - vy(0) == vy_0, (vy0_con) + x(t0) == x_t0, (x0_t0) + y(t0) == y_t0, (y0_t0) + vx(t0) == vx_t0, (vx0_t0) + vy(t0) == vy_t0, (vy0_t0) # final conditions - tf ≥ 0 - y(tf) == y_f, (yf_con) - vx(tf) == vx_f, (vxf_con) - vy(tf) == vy_f, (vyf_con) + tf ≥ tf_l + y(tf) == y_tf, (yf_tf) + vx(tf) == vx_tf, (vxf_tf) + vy(tf) == vy_tf, (vyf_tf) # dynamics ż(t) == dynamics(x(t), vx(t), vy(t), cL(t)) # objective - -x(tf) → min + x(tf) → max end function dynamics(x, vx, vy, cL) - r = (x / r_0 - 2.5)^2 + r = (x / r_t0 - 2.5)^2 UpD = u_c * (1 - r) * exp(-r) w = vy - UpD v = √(vx^2 + w^2) @@ -105,7 +111,7 @@ function OptimalControlProblems.glider( # initial guess tfinit = 1 - xinit = t -> [x_0 + vx_0 * t / tfinit, y_0 + t / tfinit * (y_f - y_0), vx_0, vy_0] + xinit = t -> [x_t0 + vx_t0 * t / tfinit, y_t0 + t / tfinit * (y_tf - y_t0), vx_t0, vy_t0] uinit = cL_max / 2 init = (state=xinit, control=uinit, variable=tfinit) @@ -115,7 +121,7 @@ function OptimalControlProblems.glider( description...; lagrange_to_mayer=false, init=init, - grid_size=N, + grid_size=grid_size, disc_method=:trapeze, kwargs..., ) diff --git a/ext/OptimalControlModels/insurance.jl b/ext/OptimalControlModels/insurance.jl index 92e0dc0e..36fcb604 100644 --- a/ext/OptimalControlModels/insurance.jl +++ b/ext/OptimalControlModels/insurance.jl @@ -8,7 +8,7 @@ It returns both a discretised direct optimal control problem (DOCP) and the corr # Arguments - `::OptimalControlBackend`: Placeholder type specifying the OptimalControl backend or solver interface. -- `N::Int=500`: (Keyword) Number of discretisation points for the direct transcription grid. +- `grid_size::Int=500`: (Keyword) Number of discretisation points for the direct transcription grid. # Returns @@ -30,20 +30,37 @@ julia> docp = OptimalControlProblems.insurance(OptimalControlBackend(); N=500); function OptimalControlProblems.insurance( ::OptimalControlBackend, description::Symbol...; - N::Int=steps_number_data(:insurance), + grid_size::Int=grid_size_data(:insurance), + parameters::Union{Nothing, NamedTuple}=nothing, kwargs..., ) # parameters - tf = final_time_data(:insurance) - γ = 0.2 - λ = 0.25 - h0 = 1.5 - w = 1 - s = 10 - k = 0 - σ = 0 - α = 4 + params = parameters_data(:insurance, parameters) + t0 = params[:t0] + tf = params[:tf] + γ = params[:γ] + λ = params[:λ] + h0 = params[:h0] + w = params[:w] + s = params[:s] + k = params[:k] + σ = params[:σ] + α = params[:α] + I_l = params[:I_l] + I_u = params[:I_u] + m_l = params[:m_l] + m_u = params[:m_u] + h_l = params[:h_l] + h_u = params[:h_u] + R_l = params[:R_l] + H_l = params[:H_l] + U_l = params[:U_l] + dUdR_l = params[:dUdR_l] + P_l = params[:P_l] + I_t0 = params[:I_t0] + m_t0 = params[:m_t0] + x₃_t0 = params[:x₃_t0] # I: Insurance # m: Expense @@ -54,27 +71,28 @@ function OptimalControlProblems.insurance( # Model ocp = @def begin P ∈ R, variable - t ∈ [0, tf], time + t ∈ [t0, tf], time x = (I, m, x₃) ∈ R³, state u = (h, R, H, U, dUdR) ∈ R⁵, control # constraints - 0 ≤ I(t) ≤ 1.5 - 0 ≤ m(t) ≤ 1.5 - 0 ≤ h(t) ≤ 25 - 0 ≤ R(t) ≤ Inf - 0 ≤ H(t) ≤ Inf - 0 ≤ U(t) ≤ Inf - 0.001 ≤ dUdR(t) ≤ Inf - 0 ≤ P ≤ Inf - - x(0) == [0, 0.001, 0] + I_l ≤ I(t) ≤ I_u + m_l ≤ m(t) ≤ m_u + h_l ≤ h(t) ≤ h_u + R(t) ≥ R_l + H(t) ≥ H_l + U(t) ≥ U_l + dUdR(t) ≥ dUdR_l + P ≥ P_l + + x(t0) == [I_t0, m_t0, x₃_t0] P - x₃(tf) == 0 - ε = k * t / (tf - t + 1) + # + ε = k * (t - t0) / (tf - t + 1) # illness distribution - fx = λ * exp(-λ * t) + exp(-λ * tf) / tf + fx = λ * exp(-λ * (t - t0)) + exp(-λ * (tf - t0)) / (tf - t0) # expense effect v = m(t)^(α / 2) / (1 + m(t)^(α / 2)) @@ -82,15 +100,15 @@ function OptimalControlProblems.insurance( # constraints R(t) - (w - P + I(t) - m(t) - ε) == 0 - H(t) - (h0 - γ * t * (1 - v)) == 0 + H(t) - (h0 - γ * (t - t0) * (1 - v)) == 0 U(t) - (1 - exp(-s * R(t)) + H(t)) == 0 dUdR(t) - (s * exp(-s * R(t))) == 0 # dynamics - ẋ(t) == [(1 - γ * t * vprime / dUdR(t)) * h(t), h(t), (1 + σ) * I(t) * fx] + ẋ(t) == [(1 - γ * (t - t0) * vprime / dUdR(t)) * h(t), h(t), (1 + σ) * I(t) * fx] # objective - -∫(U(t) * fx) → min + ∫(U(t) * fx) → max end # initial guess @@ -105,7 +123,7 @@ function OptimalControlProblems.insurance( description...; lagrange_to_mayer=false, init=init, - grid_size=N, + grid_size=grid_size, disc_method=:trapeze, kwargs..., ) diff --git a/ext/OptimalControlModels/jackson.jl b/ext/OptimalControlModels/jackson.jl index 9af363c2..c1f72194 100644 --- a/ext/OptimalControlModels/jackson.jl +++ b/ext/OptimalControlModels/jackson.jl @@ -8,7 +8,7 @@ It returns both a discretised direct optimal control problem (DOCP) and the corr # Arguments - `::OptimalControlBackend`: Placeholder type specifying the OptimalControl backend or solver interface. -- `N::Int=500`: (Keyword) Number of discretisation points for the direct transcription grid. +- `grid_size::Int=500`: (Keyword) Number of discretisation points for the direct transcription grid. # Returns @@ -30,29 +30,40 @@ julia> docp = OptimalControlProblems.jackson(OptimalControlBackend(); N=500); function OptimalControlProblems.jackson( ::OptimalControlBackend, description::Symbol...; - N::Int=steps_number_data(:jackson), + grid_size::Int=grid_size_data(:jackson), + parameters::Union{Nothing, NamedTuple}=nothing, kwargs..., ) # parameters - tf = final_time_data(:jackson) - k1 = 1 - k2 = 10 - k3 = 1 + params = parameters_data(:jackson, parameters) + t0 = params[:t0] + tf = params[:tf] + k1 = params[:k1] + k2 = params[:k2] + k3 = params[:k3] + a_l = params[:a_l] + a_u = params[:a_u] + b_l = params[:b_l] + b_u = params[:b_u] + x₃_l = params[:x₃_l] + x₃_u = params[:x₃_u] + u_l = params[:u_l] + u_u = params[:u_u] + a_t0 = params[:a_t0] + b_t0 = params[:b_t0] + x₃_t0 = params[:x₃_t0] # model ocp = @def begin - t ∈ [0, tf], time - x ∈ R³, state + t ∈ [t0, tf], time + x = (a, b, x₃) ∈ R³, state u ∈ R, control - a = x[1] - b = x[2] + x(t0) == [a_t0, b_t0, x₃_t0] - x(0) == [1, 0, 0] - - [0, 0, 0] ≤ x(t) ≤ [1.1, 1.1, 1.1] - 0 ≤ u(t) ≤ 1 + [a_l, b_l, x₃_l] ≤ x(t) ≤ [a_u, b_u, x₃_u] + u_l ≤ u(t) ≤ u_u ẋ(t) == [ -u(t) * (k1 * a(t) - k2 * b(t)), @@ -60,11 +71,11 @@ function OptimalControlProblems.jackson( (1 - u(t)) * k3 * b(t), ] - -x[3](tf) → min + x[3](tf) → max end # initial guess - xinit = [0.1, 0.1, 0.1] # [a, b, x3] + xinit = [0.1, 0.1, 0.1] # [a, b, x₃] uinit = [0.1] # [u] init = (state=xinit, control=uinit) @@ -74,7 +85,7 @@ function OptimalControlProblems.jackson( description...; lagrange_to_mayer=false, init=init, - grid_size=N, + grid_size=grid_size, disc_method=:trapeze, kwargs..., ) diff --git a/ext/OptimalControlModels/moonlander.jl b/ext/OptimalControlModels/moonlander.jl index 18e0f601..44c5aecf 100644 --- a/ext/OptimalControlModels/moonlander.jl +++ b/ext/OptimalControlModels/moonlander.jl @@ -8,7 +8,7 @@ It returns both a discretised direct optimal control problem (DOCP) and the corr # Arguments - `::OptimalControlBackend`: Placeholder type specifying the OptimalControl backend or solver interface. -- `N::Int=500`: (Keyword) Number of discretisation points for the direct transcription grid. +- `grid_size::Int=500`: (Keyword) Number of discretisation points for the direct transcription grid. # Returns @@ -26,47 +26,69 @@ julia> docp = OptimalControlProblems.moonlander(OptimalControlBackend(); N=500); function OptimalControlProblems.moonlander( ::OptimalControlBackend, description::Symbol...; - N::Int=steps_number_data(:moonlander), + grid_size::Int=grid_size_data(:moonlander), + parameters::Union{Nothing, NamedTuple}=nothing, kwargs..., ) # parameters - target=[5.0, 5.0] - m = 1 - g = 9.81 - I = 0.1 - D = 1 - max_thrust = 2g + params = parameters_data(:moonlander, parameters) + t0 = params[:t0] + m = params[:m] + g = params[:g] + I = params[:I] + D = params[:D] + max_thrust = params[:max_thrust] + tf_l = params[:tf_l] + tf_u = params[:tf_u] + F₁_l = params[:F₁_l] + F₁_u = max_thrust + F₂_l = params[:F₂_l] + F₂_u = max_thrust + tf_l = params[:tf_l] + tf_u = params[:tf_u] + F₁_l = params[:F₁_l] + F₂_l = params[:F₂_l] + p₁_t0 = params[:p₁_t0] + p₂_t0 = params[:p₂_t0] + dp₁_t0 = params[:dp₁_t0] + dp₂_t0 = params[:dp₂_t0] + θ_t0 = params[:θ_t0] + dθ_t0 = params[:dθ_t0] + p₁_tf = params[:p₁_tf] + p₂_tf = params[:p₂_tf] + dp₁_tf = params[:dp₁_tf] + dp₂_tf = params[:dp₂_tf] # define the problem ocp = @def begin # state, control and final time variables, and time tf ∈ R, variable - t ∈ [0, tf], time - x = (p1, p2, dp1, dp2, θ, dθ) ∈ R⁶, state - u = (F1, F2) ∈ R², control + t ∈ [t0, tf], time + x = (p₁, p₂, dp₁, dp₂, θ, dθ) ∈ R⁶, state + u = (F₁, F₂) ∈ R², control # final time constraint - 0.1 ≤ tf ≤ 1.0 + tf_l ≤ tf ≤ tf_u # control constraints - 0 ≤ F1(t) ≤ max_thrust, (F1_con) - 0 ≤ F2(t) ≤ max_thrust, (F2_con) + F₁_l ≤ F₁(t) ≤ F₁_u, (F₁_c) + F₂_l ≤ F₂(t) ≤ F₂_u, (F₂_c) # initial conditions - p1(0) == 0, (p1_ic) - p2(0) == 0, (p2_ic) - dp1(0) == 0, (dp1_ic) - dp2(0) == 0, (dp2_ic) - θ(0) == 0, (θ_ic) - dθ(0) == 0, (dθ_ic) + p₁(t0) == p₁_t0, (p₁_t0) + p₂(t0) == p₂_t0, (p₂_t0) + dp₁(t0) == dp₁_t0, (dp₁_t0) + dp₂(t0) == dp₂_t0, (dp₂_t0) + θ(t0) == θ_t0, (θ_t0) + dθ(t0) == dθ_t0, (dθ_t0) # final conditions - p1(tf) == target[1], (p1_fc) - p2(tf) == target[2], (p2_fc) - dp1(tf) == 0, (dp1_fc) - dp2(tf) == 0, (dp2_fc) + p₁(tf) == p₁_tf, (p₁_tf) + p₂(tf) == p₂_tf, (p₂_tf) + dp₁(tf) == dp₁_tf, (dp₁_tf) + dp₂(tf) == dp₂_tf, (dp₂_tf) # dynamics ẋ(t) == dynamics(x(t), u(t)) @@ -77,25 +99,25 @@ function OptimalControlProblems.moonlander( # dynamics function dynamics(x, u) - p1, p2, dp1, dp2, θ, dθ = x - F1, F2 = u + p₁, p₂, dp₁, dp₂, θ, dθ = x + F₁, F₂ = u F_r = [ - cos(θ) -sin(θ) p1 - sin(θ) cos(θ) p2 + cos(θ) -sin(θ) p₁ + sin(θ) cos(θ) p₂ 0 0 1 ] - F_tot = (F_r * [0; F1 + F2; 0])[1:2] - ddp1 = (1 / m) * F_tot[1] - ddp2 = (1 / m) * F_tot[2] - g - ddθ = (1 / I) * (D / 2) * (F2 - F1) + F_tot = (F_r * [0; F₁ + F₂; 0])[1:2] + ddp₁ = (1 / m) * F_tot[1] + ddp₂ = (1 / m) * F_tot[2] - g + ddθ = (1 / I) * (D / 2) * (F₂ - F₁) - return [dp1, dp2, ddp1, ddp2, dθ, ddθ] + return [dp₁, dp₂, ddp₁, ddp₂, dθ, ddθ] end # initial guess - xinit = [0.1, 0.1, 0.1, 0.1, 0.1, 0.1] # [p1, p2, dp1, dp2, θ, dθ] - uinit = [5.0, 5.0] # [F1, F2] + xinit = [0.1, 0.1, 0.1, 0.1, 0.1, 0.1] # [p₁, p₂, dp₁, dp₂, θ, dθ] + uinit = [5.0, 5.0] # [F₁, F₂] varinit = [0.5] # [tf] init = (state=xinit, control=uinit, variable=varinit) @@ -105,7 +127,7 @@ function OptimalControlProblems.moonlander( description...; lagrange_to_mayer=false, init=init, - grid_size=N, + grid_size=grid_size, disc_method=:trapeze, kwargs..., ) diff --git a/ext/OptimalControlModels/robbins.jl b/ext/OptimalControlModels/robbins.jl index 525307a2..b80d93c0 100644 --- a/ext/OptimalControlModels/robbins.jl +++ b/ext/OptimalControlModels/robbins.jl @@ -9,7 +9,7 @@ Reference: [Robbins Problem on BOCOP](https://github.com/control-toolbox/bocop/t # Arguments - `::OptimalControlBackend`: Placeholder type specifying the OptimalControl backend or solver interface. -- `N::Int=500`: (Keyword) Number of discretisation points for the direct transcription grid. +- `grid_size::Int=500`: (Keyword) Number of discretisation points for the direct transcription grid. # Returns @@ -27,26 +27,36 @@ julia> docp = OptimalControlProblems.robbins(OptimalControlBackend(); N=500); function OptimalControlProblems.robbins( ::OptimalControlBackend, description::Symbol...; - N::Int=steps_number_data(:robbins), + grid_size::Int=grid_size_data(:robbins), + parameters::Union{Nothing, NamedTuple}=nothing, kwargs..., ) # parameters - tf = final_time_data(:robbins) - α = 3 - β = 0 - γ = 0.5 + params = parameters_data(:robbins, parameters) + t0 = params[:t0] + tf = params[:tf] + α = params[:α] + β = params[:β] + γ = params[:γ] + x₁_l = params[:x₁_l] + x₁_t0 = params[:x₁_t0] + x₂_t0 = params[:x₂_t0] + x₃_t0 = params[:x₃_t0] + x₁_tf = params[:x₁_tf] + x₂_tf = params[:x₂_tf] + x₃_tf = params[:x₃_tf] # model ocp = @def begin - t ∈ [0, tf], time + t ∈ [t0, tf], time x ∈ R³, state u ∈ R, control - 0 ≤ x[1](t) ≤ Inf + x[1](t) ≥ x₁_l - x(0) == [1, -2, 0] - x(tf) == [0, 0, 0] + x(t0) == [x₁_t0, x₂_t0, x₃_t0] + x(tf) == [x₁_tf, x₂_tf, x₃_tf] ẋ(t) == [x[2](t), x[3](t), u(t)] @@ -54,7 +64,7 @@ function OptimalControlProblems.robbins( end # initial guess - xinit = [0.1, 0.1, 0.1] # [x1, x2, x3] + xinit = [0.1, 0.1, 0.1] # [x₁, x₂, x₃] uinit = [0.1] # [u] init = (state=xinit, control=uinit) @@ -64,7 +74,7 @@ function OptimalControlProblems.robbins( description...; lagrange_to_mayer=false, init=init, - grid_size=N, + grid_size=grid_size, disc_method=:trapeze, kwargs..., ) diff --git a/ext/OptimalControlModels/robot.jl b/ext/OptimalControlModels/robot.jl index 0680c283..660457d0 100644 --- a/ext/OptimalControlModels/robot.jl +++ b/ext/OptimalControlModels/robot.jl @@ -8,7 +8,7 @@ Reference: Robot arm problem on BOCOP [here](https://github.com/control-toolbox/ # Arguments - `::OptimalControlBackend`: Placeholder type specifying the OptimalControl backend or solver interface. -- `N::Int=250`: (Keyword) Number of discretisation points for the direct transcription grid. +- `grid_size::Int=250`: (Keyword) Number of discretisation points for the direct transcription grid. # Returns @@ -26,58 +26,84 @@ julia> docp = OptimalControlProblems.robot(OptimalControlBackend(); N=250); function OptimalControlProblems.robot( ::OptimalControlBackend, description::Symbol...; - N::Int=steps_number_data(:robot), + grid_size::Int=grid_size_data(:robot), + parameters::Union{Nothing, NamedTuple}=nothing, kwargs..., ) # parameters + params = parameters_data(:robot, parameters) + t0 = params[:t0] # total length of arm - L = 5 + L = params[:L] # Upper bounds on the controls - max_uρ = 1 - max_uθ = 1 - max_uϕ = 1 + uρ_l = params[:uρ_l] + uρ_u = params[:uρ_u] + uθ_l = params[:uθ_l] + uθ_u = params[:uθ_u] + uϕ_l = params[:uϕ_l] + uϕ_u = params[:uϕ_u] # Initial positions of the length and the angles for the robot arm - ρ0 = 4.5 - ϕ0 = π/4 - θf = 2π/3 + ρ_t0 = params[:ρ_t0] + θ_t0 = params[:θ_t0] + ϕ_t0 = params[:ϕ_t0] + dρ_t0 = params[:dρ_t0] + dθ_t0 = params[:dθ_t0] + dϕ_t0 = params[:dϕ_t0] + + # Final positions + ρ_tf = params[:ρ_tf] + θ_tf = params[:θ_tf] + ϕ_tf = params[:ϕ_tf] + dρ_tf = params[:dρ_tf] + dθ_tf = params[:dθ_tf] + dϕ_tf = params[:dϕ_tf] + + # + ρ_l = params[:ρ_l] + ρ_u = L + θ_l = params[:θ_l] + θ_u = params[:θ_u] + ϕ_l = params[:ϕ_l] + ϕ_u = params[:ϕ_u] + tf_l = params[:tf_l] ocp = @def begin tf ∈ R, variable - t ∈ [0, tf], time + t ∈ [t0, tf], time x = (ρ, dρ, θ, dθ, ϕ, dϕ) ∈ R⁶, state u = (uρ, uθ, uϕ) ∈ R³, control - tf ≥ 0.1 + tf ≥ tf_l # state constraints - 0 ≤ ρ(t) ≤ L, (ρ_con) - -π ≤ θ(t) ≤ π, (θ_con) - 0 ≤ ϕ(t) ≤ π, (ϕ_con) + ρ_l ≤ ρ(t) ≤ ρ_u, (ρ_c) + θ_l ≤ θ(t) ≤ θ_u, (θ_c) + ϕ_l ≤ ϕ(t) ≤ ϕ_u, (ϕ_c) # control constraints - -max_uρ ≤ uρ(t) ≤ max_uρ, (u_ρ_con) - -max_uθ ≤ uθ(t) ≤ max_uθ, (u_θ_con) - -max_uϕ ≤ uϕ(t) ≤ max_uϕ, (u_ϕ_con) + uρ_l ≤ uρ(t) ≤ uρ_u, (u_ρ_c) + uθ_l ≤ uθ(t) ≤ uθ_u, (u_θ_c) + uϕ_l ≤ uϕ(t) ≤ uϕ_u, (u_ϕ_c) # initial conditions - ρ(0) == ρ0, (ρ0_con) - ϕ(0) == ϕ0, (ϕ0_con) - θ(0) == 0, (θ0_con) - dθ(0) == 0, (dθ0_con) - dϕ(0) == 0, (dϕ0_con) - dρ(0) == 0, (dρ0_con) + ρ(t0) == ρ_t0, (ρ_t0) + θ(t0) == θ_t0, (θ_t0) + ϕ(t0) == ϕ_t0, (ϕ_t0) + dρ(t0) == dρ_t0, (dρ_t0) + dθ(t0) == dθ_t0, (dθ_t0) + dϕ(t0) == dϕ_t0, (dϕ_t0) # final conditions - ρ(tf) == ρ0, (ρf_con) - θ(tf) == θf, (θf_con) - ϕ(tf) == ϕ0, (ϕf_con) - dθ(tf) == 0, (dθf_con) - dϕ(tf) == 0, (dϕf_con) - dρ(tf) == 0, (dρf_con) + ρ(tf) == ρ_tf, (ρ_tf) + θ(tf) == θ_tf, (θ_tf) + ϕ(tf) == ϕ_tf, (ϕ_tf) + dρ(tf) == dρ_tf, (dρ_tf) + dθ(tf) == dθ_tf, (dθ_tf) + dϕ(tf) == dϕ_tf, (dϕ_tf) # aliases I_θ = ((L - ρ(t))^3 + ρ(t)^3) * sin(ϕ(t))^2 @@ -92,7 +118,7 @@ function OptimalControlProblems.robot( # initial guess tf = 1 - xinit = t -> [ρ0, 0, 2π/3 * (t/tf)^2, 4π/3 * (t/tf), ϕ0, 0] + xinit = t -> [ρ_t0, 0, 2π/3 * ((t - t0) / (tf - t0))^2, 4π/3 * ((t - t0) / (tf - t0)), ϕ_t0, 0] uinit = [0, 0, 0] init = (state=xinit, control=uinit, variable=tf) @@ -102,7 +128,7 @@ function OptimalControlProblems.robot( description...; lagrange_to_mayer=false, init=init, - grid_size=N, + grid_size=grid_size, disc_method=:trapeze, kwargs..., ) diff --git a/ext/OptimalControlModels/rocket.jl b/ext/OptimalControlModels/rocket.jl index 91e43e1a..839b33d6 100644 --- a/ext/OptimalControlModels/rocket.jl +++ b/ext/OptimalControlModels/rocket.jl @@ -8,7 +8,7 @@ Reference: Goddard Rocket Problem [here](https://github.com/control-toolbox/boco # Arguments - `::OptimalControlBackend`: Placeholder type specifying the OptimalControl backend or solver interface. -- `N::Int=500`: (Keyword) Number of discretisation points for the direct transcription grid. +- `grid_size::Int=500`: (Keyword) Number of discretisation points for the direct transcription grid. # Returns @@ -26,68 +26,77 @@ julia> docp = OptimalControlProblems.rocket(OptimalControlBackend(); N=500); function OptimalControlProblems.rocket( ::OptimalControlBackend, description::Symbol...; - N::Int=steps_number_data(:rocket), + grid_size::Int=grid_size_data(:rocket), + parameters::Union{Nothing, NamedTuple}=nothing, kwargs..., ) # parameters - h0 = 1 - v0 = 0 - m0 = 1 - g0 = 1 - Tc = 3.5 - hc = 500 - vc = 620 - mc = 0.6 - c = 0.5 * sqrt(g0 * h0) - mf = mc * m0 - Dc = 0.5 * vc * (m0 / g0) - Tmax = Tc * m0 * g0 + params = parameters_data(:rocket, parameters) + t0 = params[:t0] + h_t0 = params[:h_t0] + v_t0 = params[:v_t0] + m_t0 = params[:m_t0] + g0 = params[:g0] + Tc = params[:Tc] + hc = params[:hc] + vc = params[:vc] + mc = params[:mc] + T_l = params[:T_l] + tf_l = params[:tf_l] + + # + c = 0.5 * sqrt(g0 * h_t0) + m_tf = mc * m_t0 + Dc = 0.5 * vc * (m_t0 / g0) + Tmax = Tc * m_t0 * g0 # Model ocp = @def begin tf ∈ R, variable - t ∈ [0, tf], time + t ∈ [t0, tf], time x = (h, v, m) ∈ R³, state T ∈ R, control # state constraints - h(t) ≥ h0, (x1_con) - v(t) ≥ v0, (x2_con) - mf ≤ m(t) ≤ m0, (x3_con) + h(t) ≥ h_t0, (h_c) + v(t) ≥ v_t0, (v_c) + m_tf ≤ m(t) ≤ m_t0, (m_c) # control constraints - 0 ≤ T(t) ≤ Tmax, (Tcon) + T_l ≤ T(t) ≤ Tmax, (T_c) # time constraints - tf ≥ 0, (tf_con) + tf ≥ tf_l, (tf_c) # initial conditions - h(0) == h0, (x1_ic) - v(0) == v0, (x2_ic) - m(0) == m0, (x3_ic) + h(t0) == h_t0, (h_t0) + v(t0) == v_t0, (v_t0) + m(t0) == m_t0, (m_t0) # final conditions - m(tf) == mf, (x3_fc) + m(tf) == m_tf, (m_tf) # dynamics ẋ(t) == dynamics(h(t), v(t), m(t), T(t)) # objective - -h(tf) → min + h(tf) → max end # dynamics function dynamics(h, v, m, T) - D = (Dc * v^2 * exp(-hc * (h - h0)) / h0) - g = g0 * (h0 / h)^2 + D = (Dc * v^2 * exp(-hc * (h - h_t0)) / h_t0) + g = g0 * (h_t0 / h)^2 return [v, (T - D - m * g) / m, -T / c] end # initial guess - xinit = [[1, i / N * (1 - i / N), (mf - m0) * (i / N) + m0] for i in 0:N] - time_vec = LinRange(0, 1, N+1) - init = (time=time_vec, state=xinit, control=Tmax/2, variable=1) + N = grid_size + tf_init = 1 + xinit = [[1, i / N * (1 - i / N), (m_tf - m_t0) * (i / N) + m_t0] for i in 0:N] + time_vec = LinRange(0, tf_init, N+1) + init = (time=time_vec, state=xinit, control=Tmax/2, variable=tf_init) # discretise the optimal control problem docp = direct_transcription( @@ -95,7 +104,7 @@ function OptimalControlProblems.rocket( description...; lagrange_to_mayer=false, init=init, - grid_size=N, + grid_size=grid_size, disc_method=:trapeze, kwargs..., ) diff --git a/ext/OptimalControlModels/space_shuttle.jl b/ext/OptimalControlModels/space_shuttle.jl index c525fde0..b73e48a7 100644 --- a/ext/OptimalControlModels/space_shuttle.jl +++ b/ext/OptimalControlModels/space_shuttle.jl @@ -9,7 +9,7 @@ Note: No heating limit path constraint is included. # Arguments - `::OptimalControlBackend`: Placeholder type specifying the OptimalControl backend or solver interface. -- `N::Int=500`: (Keyword) Number of discretisation points for the direct transcription grid. +- `grid_size::Int=500`: (Keyword) Number of discretisation points for the direct transcription grid. # Returns @@ -27,105 +27,132 @@ julia> docp = OptimalControlProblems.space_shuttle(OptimalControlBackend(); N=50 function OptimalControlProblems.space_shuttle( ::OptimalControlBackend, description::Symbol...; - N::Int=steps_number_data(:space_shuttle), + grid_size::Int=grid_size_data(:space_shuttle), + parameters::Union{Nothing, NamedTuple}=nothing, kwargs..., ) - ## Global variables - w = 203000.0 # weight (lb) - g₀ = 32.174 # acceleration (ft/sec^2) - m = w / g₀ # mass (slug) + # Parameters + params = parameters_data(:space_shuttle, parameters) + t0 = params[:t0] + + ## + w = params[:w] + g₀ = params[:g₀] + m = w / g₀ # mass (slug) ## Aerodynamic and atmospheric forces on the vehicle - ρ₀ = 0.002378 - hᵣ = 23800 - Rₑ = 20902900 - μ = 0.14076539e17 - S = 2690 - a₀ = -0.20704 - a₁ = 0.029244 - b₀ = 0.07854 - b₁ = -0.61592e-2 - b₂ = 0.621408e-3 + ρ₀ = params[:ρ₀] + hᵣ = params[:hᵣ] + Rₑ = params[:Rₑ] + μ = params[:μ] + S = params[:S] + a₀ = params[:a₀] + a₁ = params[:a₁] + b₀ = params[:b₀] + b₁ = params[:b₁] + b₂ = params[:b₂] # - Δt_min = 3.5 - Δt_max = 4.5 - tf_min = 500*Δt_min - tf_max = 500*Δt_max + Δt_min = params[:Δt_min] + Δt_max = params[:Δt_max] + tf_l = grid_size*Δt_min + tf_u = grid_size*Δt_max ## Initial conditions - h_s = 2.6 # altitude (ft) / 1e5 - ϕ_s = deg2rad(0) # longitude (rad) - θ_s = deg2rad(0) # latitude (rad) - v_s = 2.56 # velocity (ft/sec) / 1e4 - γ_s = deg2rad(-1) # flight path angle (rad) - ψ_s = deg2rad(90) # azimuth (rad) - α_s = deg2rad(0) # angle of attack (rad) - β_s = deg2rad(0) # bank angle (rad) + h_t0 = params[:h_t0] + ϕ_t0 = params[:ϕ_t0] + θ_t0 = params[:θ_t0] + v_t0 = params[:v_t0] + γ_t0 = params[:γ_t0] + ψ_t0 = params[:ψ_t0] + + # for initial guess + α_s = params[:α_s] + β_s = params[:β_s] ## Final conditions, the so-called Terminal Area Energy Management (TAEM) - h_t = 0.8 # altitude (ft) / 1e5 - v_t = 0.25 # velocity (ft/sec) / 1e4 - γ_t = deg2rad(-5) # flight path angle (rad) + h_tf = params[:h_tf] + v_tf = params[:v_tf] + γ_tf = params[:γ_tf] + + ## + h_l = params[:h_l] + ϕ_l = params[:ϕ_l] + ϕ_u = params[:ϕ_u] + θ_l = params[:θ_l] + θ_u = params[:θ_u] + v_l = params[:v_l] + γ_l = params[:γ_l] + γ_u = params[:γ_u] + ψ_l = params[:ψ_l] + ψ_u = params[:ψ_u] + α_l = params[:α_l] + α_u = params[:α_u] + β_l = params[:β_l] + β_u = params[:β_u] + + ## Scalings + scaling_h = 1e5 + scaling_v = 1e4 # model ocp = @def begin - - ## define the problem + # define the problem tf ∈ R, variable - t ∈ [0, tf], time + t ∈ [t0, tf], time x = (scaled_h, ϕ, θ, scaled_v, γ, ψ) ∈ R⁶, state u = (α, β) ∈ R², control - ## constraints - # to help convergence and avoid domain value error - -2π ≤ ϕ(t) ≤ 2π - -2π ≤ ψ(t) ≤ 2π - - # final time constraints - tf_min ≤ tf ≤ tf_max - - # state constraints - 0 ≤ scaled_h(t) ≤ Inf, (scaled_h_con) - deg2rad(-89) ≤ θ(t) ≤ deg2rad(89), (θ_con) - 0 ≤ scaled_v(t) ≤ Inf, (scaled_v_con) - deg2rad(-89) ≤ γ(t) ≤ deg2rad(89), (γ_con) - - # control constraints - deg2rad(-90) ≤ α(t) ≤ deg2rad(90), (α_con) - deg2rad(-89) ≤ β(t) ≤ deg2rad(1), (β_con) - - # initial conditions - scaled_h(0) == h_s, (scaled_h0_con) - ϕ(0) == ϕ_s, (ϕ0_con) - θ(0) == θ_s, (θ0_con) - scaled_v(0) == v_s, (scaled_v0_con) - γ(0) == γ_s, (γ0_con) - ψ(0) == ψ_s, (ψ0_con) - - # final conditions - scaled_h(tf) == h_t, (scaled_hf_con) - scaled_v(tf) == v_t, (scaled_vf_con) - γ(tf) == γ_t, (γf_con) - - ## dynamics + # constraints + ## to help convergence and avoid domain value error + ϕ_l ≤ ϕ(t) ≤ ϕ_u + ψ_l ≤ ψ(t) ≤ ψ_u + + ## final time constraints + tf_l ≤ tf ≤ tf_u + + ## state constraints + scaled_h(t) ≥ h_l, (scaled_h_c) + θ_l ≤ θ(t) ≤ θ_u, (θ_c) + scaled_v(t) ≥ v_l, (scaled_v_c) + γ_l ≤ γ(t) ≤ γ_u, (γ_c) + + ## control constraints + α_l ≤ α(t) ≤ α_u, (α_c) + β_l ≤ β(t) ≤ β_u, (β_c) + + ## initial conditions + scaled_h(t0) == h_t0, (scaled_h_t0) + ϕ(t0) == ϕ_t0, (ϕ_t0) + θ(t0) == θ_t0, (θ_t0) + scaled_v(t0) == v_t0, (scaled_v_t0) + γ(t0) == γ_t0, (γ_t0) + ψ(t0) == ψ_t0, (ψ_t0) + + ## final conditions + scaled_h(tf) == h_tf, (scaled_h_tf) + scaled_v(tf) == v_tf, (scaled_v_tf) + γ(tf) == γ_tf, (γ_tf) + + # dynamics ẋ(t) == dynamics(x(t), u(t)) - ## objective - -θ(tf) → min + # objective + θ(tf) → max end - ## dynamics + # dynamics function dynamics(x, u) scaled_h, ϕ, θ, scaled_v, γ, ψ = x α, β = u - h = scaled_h * 1e5 - v = scaled_v * 1e4 + h = scaled_h * scaling_h + v = scaled_v * scaling_v ## Helper functions - c_D = b₀ + b₁ * rad2deg(α) + b₂ * (rad2deg(α)^2) - c_L = a₀ + a₁ * rad2deg(α) + r2dα = α * (180 / π) + c_D = b₀ + b₁ * r2dα + b₂ * (r2dα^2) + c_L = a₀ + a₁ * r2dα ρ = ρ₀ * exp(-h / hᵣ) D = (1 / 2) * c_D * S * ρ * (v^2) L = (1 / 2) * c_L * S * ρ * (v^2) @@ -142,21 +169,21 @@ function OptimalControlProblems.space_shuttle( (1 / (m * v * cos(γ))) * L * sin(β) + (v / (r * cos(θ))) * cos(γ) * sin(ψ) * sin(θ) - return [h_dot / 1e5, dϕ, dθ, v_dot / 1e4, γ_dot, ψ_dot] + return [h_dot / scaling_h, dϕ, dθ, v_dot / scaling_v, γ_dot, ψ_dot] end # initial guess: linear interpolation for h, v, gamma (NB. t0 = 0), constant for the rest # variable time step seems to be initialized at 1 in jump # note that ipopt will project the initial guess inside the bounds anyway. - tf_init = (tf_min+tf_max)/2 + tf_init = (tf_l+tf_u)/2 x_init = t -> [ - h_s + t / tf_init * (h_t - h_s), - ϕ_s, - θ_s, - v_s + t / tf_init * (v_t - v_s), - γ_s + t / tf_init * (γ_t - γ_s), - ψ_s, + h_t0 + (t - t0) / (tf_init - t0) * (h_tf - h_t0), + ϕ_t0, + θ_t0, + v_t0 + (t - t0) / (tf_init - t0) * (v_tf - v_t0), + γ_t0 + (t - t0) / (tf_init - t0) * (γ_tf - γ_t0), + ψ_t0, ] init = (state=x_init, control=[α_s, β_s], variable=[tf_init]) @@ -166,7 +193,7 @@ function OptimalControlProblems.space_shuttle( description...; lagrange_to_mayer=false, init=init, - grid_size=N, + grid_size=grid_size, disc_method=:trapeze, kwargs..., ) diff --git a/ext/OptimalControlModels/steering.jl b/ext/OptimalControlModels/steering.jl index 4b7cf772..c7e51f1d 100644 --- a/ext/OptimalControlModels/steering.jl +++ b/ext/OptimalControlModels/steering.jl @@ -8,7 +8,7 @@ The state vector has four components, and the control is a single scalar input. # Arguments - `::OptimalControlBackend`: Placeholder type specifying the OptimalControl backend or solver interface. -- `N::Int=500`: (Keyword) Number of discretisation points for the direct transcription grid. +- `grid_size::Int=500`: (Keyword) Number of discretisation points for the direct transcription grid. # Returns @@ -26,29 +26,44 @@ julia> docp = OptimalControlProblems.steering(OptimalControlBackend(); N=500); function OptimalControlProblems.steering( ::OptimalControlBackend, description::Symbol...; - N::Int=steps_number_data(:steering), + grid_size::Int=grid_size_data(:steering), + parameters::Union{Nothing, NamedTuple}=nothing, kwargs..., ) # parameters - a = 100 - u_min = -π/2 - u_max = π/2 - xs = zeros(4) - xf = [NaN, 5, 45, 0] + params = parameters_data(:steering, parameters) + t0 = params[:t0] + a = params[:a] + u_min = params[:u_min] + u_max = params[:u_max] + tf_l = params[:tf_l] + x₁_t0 = params[:x₁_t0] + x₂_t0 = params[:x₂_t0] + x₃_t0 = params[:x₃_t0] + x₄_t0 = params[:x₄_t0] + x₂_tf = params[:x₂_tf] + x₃_tf = params[:x₃_tf] + x₄_tf = params[:x₄_tf] # Model ocp = @def begin tf ∈ R, variable - t ∈ [0.0, tf], time + t ∈ [t0, tf], time x ∈ R⁴, state - u ∈ R¹, control - - tf ≥ 0, (tf_con) - x(0) == xs, (x_ic) - x(tf) == xf, (x_fc) - u_min ≤ u(t) ≤ u_max, (u_con) - + u ∈ R, control + + x₁(t0) == x₁_t0, (x₁_t0) + x₂(t0) == x₂_t0, (x₂_t0) + x₃(t0) == x₃_t0, (x₃_t0) + x₄(t0) == x₄_t0, (x₄_t0) + x₂(tf) == x₂_tf, (x₂_tf) + x₃(tf) == x₃_tf, (x₃_tf) + x₄(tf) == x₄_tf, (x₄_tf) + + tf ≥ tf_l, (tf_c) + u_min ≤ u(t) ≤ u_max, (u_c) + ẋ(t) == dynamics(x(t), u(t)) tf → min @@ -64,9 +79,9 @@ function OptimalControlProblems.steering( if i == 1 || i == 4 return 0.0 elseif i == 2 - return 5.0 * t + return 5.0 * (t-t0) elseif i == 3 - return 45.0 * t + return 45.0 * (t-t0) end end xinit = t -> [gen_x0(t, i) for i in 1:4] @@ -78,7 +93,7 @@ function OptimalControlProblems.steering( description...; lagrange_to_mayer=false, init=init, - grid_size=N, + grid_size=grid_size, disc_method=:trapeze, kwargs..., ) diff --git a/ext/OptimalControlModels/vanderpol.jl b/ext/OptimalControlModels/vanderpol.jl index 6cb15e82..36ff2156 100644 --- a/ext/OptimalControlModels/vanderpol.jl +++ b/ext/OptimalControlModels/vanderpol.jl @@ -8,7 +8,7 @@ The problem formulation can be found [here](https://github.com/control-toolbox/b # Arguments - `::OptimalControlBackend`: Placeholder type specifying the OptimalControl backend or solver interface. -- `N::Int=500`: (Keyword) Number of discretisation points for the direct transcription grid. +- `grid_size::Int=500`: (Keyword) Number of discretisation points for the direct transcription grid. # Returns @@ -26,30 +26,35 @@ julia> docp = OptimalControlProblems.vanderpol(OptimalControlBackend(); N=500); function OptimalControlProblems.vanderpol( ::OptimalControlBackend, description::Symbol...; - N::Int=steps_number_data(:vanderpol), + grid_size::Int=grid_size_data(:vanderpol), + parameters::Union{Nothing, NamedTuple}=nothing, kwargs..., ) # parameters - tf = final_time_data(:vanderpol) - ω = 1 - ε = 1 + params = parameters_data(:vanderpol, parameters) + t0 = params[:t0] + tf = params[:tf] + ω = params[:ω] + ε = params[:ε] + x₁_t0 = params[:x₁_t0] + x₂_t0 = params[:x₂_t0] # model ocp = @def begin - t ∈ [0, tf], time + t ∈ [t0, tf], time x ∈ R², state u ∈ R, control - x(0) == [1, 0] + x(t0) == [x₁_t0, x₂_t0] ẋ(t) == [x[2](t), ε * ω * (1 - x[1](t)^2) * x[2](t) - ω^2 * x[1](t) + u(t)] - + 0.5∫(x[1](t)^2 + x[2](t)^2 + u(t)^2) → min end # initial guess - xinit = [0.1, 0.1] # [x1, x2] + xinit = [0.1, 0.1] # [x₁, x₂] uinit = [0.1] # [u] init = (state=xinit, control=uinit) @@ -59,7 +64,7 @@ function OptimalControlProblems.vanderpol( description...; lagrange_to_mayer=false, init=init, - grid_size=N, + grid_size=grid_size, disc_method=:trapeze, kwargs..., ) diff --git a/ext/OptimalControlModels_s/beam_s.jl b/ext/OptimalControlModels_s/beam_s.jl new file mode 100644 index 00000000..167880b9 --- /dev/null +++ b/ext/OptimalControlModels_s/beam_s.jl @@ -0,0 +1,80 @@ +""" +$(TYPEDSIGNATURES) + +Constructs an **OptimalControl problem** representing the Beam problem using the OptimalControl backend. +The function sets up the state and control variables, boundary conditions, dynamics, path constraints, and the objective functional. +It then performs direct transcription to generate a discrete optimal control problem (DOCP) and the corresponding nonlinear programming (NLP) model. + +# Arguments + +- `::OptimalControlBackend`: Placeholder type to specify the OptimalControl backend or solver interface. +- `grid_size::Int=500`: (Keyword) Number of discretisation points for the direct transcription grid. + +# Returns + +- `docp`: The direct optimal control problem object, representing the discretised problem. +- `nlp`: The corresponding nonlinear programming model generated from the DOCP, ready for solver input. + +# Example + +```julia-repl +julia> using OptimalControlProblems + +julia> docp = OptimalControlProblems.beam(OptimalControlBackend(); N=100); +``` + +# References + +- BOCOP repository: https://github.com/control-toolbox/bocop/tree/main/bocop +""" +function OptimalControlProblems.beam_s( + ::OptimalControlBackend, + description::Symbol...; + grid_size::Int=grid_size_data(:beam), + parameters::Union{Nothing, NamedTuple}=nothing, + kwargs..., +) + + # parameters + params = parameters_data(:beam, parameters) + t0 = params[:t0] + tf = params[:tf] + x₁_l = params[:x₁_l] + x₁_u = params[:x₁_u] + x₁_t0 = params[:x₁_t0] + x₂_t0 = params[:x₂_t0] + x₁_tf = params[:x₁_tf] + x₂_tf = params[:x₂_tf] + + # model + ocp = @def begin + t ∈ [t0, tf], time + x ∈ R², state + u ∈ R, control + + x(t0) == [x₁_t0, x₂_t0] + x(tf) == [x₁_tf, x₂_tf] + x₁_l ≤ x₁(t) ≤ x₁_u + + ∂(x₁)(t) == x₂(t) + ∂(x₂)(t) == u(t) + + ∫(u(t)^2) → min + end + + # initial guess + init = (state=[0.05, 0.1], control=0.1) + + # discretise the optimal control problem + docp = direct_transcription( + ocp, + description...; + lagrange_to_mayer=false, + init=init, + grid_size=grid_size, + disc_method=:trapeze, + kwargs..., + ) + + return docp +end diff --git a/ext/OptimalControlModels_s/chain_s.jl b/ext/OptimalControlModels_s/chain_s.jl new file mode 100644 index 00000000..e450af2e --- /dev/null +++ b/ext/OptimalControlModels_s/chain_s.jl @@ -0,0 +1,100 @@ +""" +$(TYPEDSIGNATURES) + +Constructs an **OptimalControl problem** representing the Hanging Chain problem. +The function defines state and control variables, boundary conditions, and system dynamics, with the objective of minimising the vertical displacement of the chain's midpoint. +It performs direct transcription to produce a discretised optimal control problem (DOCP) and the corresponding nonlinear programming (NLP) model. + +# Arguments + +- `::OptimalControlBackend`: Placeholder type specifying the OptimalControl backend or solver interface. +- `grid_size::Int=500`: (Keyword) Number of discretisation points for the direct transcription grid. + +# Returns + +- `docp`: The direct optimal control problem object representing the discretised Hanging Chain problem. +- `nlp`: The corresponding nonlinear programming model obtained from the DOCP, suitable for numerical optimisation. + +# Example + +```julia-repl +julia> using OptimalControlProblems + +julia> docp = OptimalControlProblems.chain(OptimalControlBackend(); N=100); +``` + +# References + +- Formulation inspired by OptimalControl approach to variational problems and chain equilibrium. +- Original problem source: [BOCOP repository](https://github.com/control-toolbox/bocop/tree/main/bocop) +""" +function OptimalControlProblems.chain_s( + ::OptimalControlBackend, + description::Symbol...; + grid_size::Int=grid_size_data(:chain), + parameters::Union{Nothing, NamedTuple}=nothing, + kwargs..., +) + + # parameters + params = parameters_data(:chain, parameters) + t0 = params[:t0] + tf = params[:tf] + L = params[:L] + a = params[:a] + b = params[:b] + x₁_t0 = a + x₂_t0 = params[:x₂_t0] + x₃_t0 = params[:x₃_t0] + x₁_tf = b + x₃_tf = L + + # model + ocp = @def begin + t ∈ [t0, tf], time + x ∈ R³, state + u ∈ R, control + + # initial conditions + x₁(t0) == x₁_t0, (x₁_t0) + x₂(t0) == x₂_t0, (x₂_t0) + x₃(t0) == x₃_t0, (x₃_t0) + + # final conditions + x₁(tf) == x₁_tf, (x₁_tf) + x₃(tf) == x₃_tf, (x₃_tf) + + # dynamics + ∂(x₁)(t) == u(t) + ∂(x₂)(t) == x₁(t) * √(1 + u(t)^2) + ∂(x₃)(t) == √(1 + u(t)^2) + + # objective + x₂(tf) → min + end + + # initial guess + tmin = b > a ? 1 / 4 : 3 / 4 + xinit = + t -> [ + 4 * abs(b - a) * (t - t0) / (tf - t0) * (0.5 * (t - t0) / (tf - t0) - tmin) + a, + (4 * abs(b - a) * (t - t0) / (tf - t0) * (0.5 * (t - t0) / (tf - t0) - tmin) + a) * + (4 * abs(b - a) * ((t - t0) / (tf - t0) - tmin)), + 4 * abs(b - a) * ((t - t0) / (tf - t0) - tmin), + ] + uinit = t -> 4 * abs(b - a) * (t / tf - tmin) + init = (state=xinit, control=uinit) + + # discretise the optimal control problem + docp = direct_transcription( + ocp, + description...; + lagrange_to_mayer=false, + init=init, + grid_size=grid_size, + disc_method=:trapeze, + kwargs..., + ) + + return docp +end diff --git a/ext/OptimalControlModels_s/dielectrophoretic_particle_s.jl b/ext/OptimalControlModels_s/dielectrophoretic_particle_s.jl new file mode 100644 index 00000000..b6a4bde0 --- /dev/null +++ b/ext/OptimalControlModels_s/dielectrophoretic_particle_s.jl @@ -0,0 +1,84 @@ +""" +$(TYPEDSIGNATURES) + +Constructs an **OptimalControl problem** representing a dielectrophoretic particle system. +The function defines state and control variables, boundary conditions, and system dynamics, aiming to minimise the travel time of the particle between two points. +It performs direct transcription to produce a discretised optimal control problem (DOCP) and the corresponding nonlinear programming (NLP) model. + +# Arguments + +- `::OptimalControlBackend`: Placeholder type specifying the OptimalControl backend or solver interface. +- `grid_size::Int=500`: (Keyword) Number of discretisation points for the direct transcription grid. + +# Returns + +- `docp`: The direct optimal control problem object representing the dielectrophoretic particle problem. +- `nlp`: The corresponding nonlinear programming model obtained from the DOCP, suitable for numerical optimisation. + +# Example + +```julia-repl +julia> using OptimalControlProblems + +julia> docp = OptimalControlProblems.dielectrophoretic_particle(OptimalControlBackend(); N=100); +``` + +# References + +- Chang, D. E., Petit, N., & Rouchon, P. (2006). Time-optimal control of a particle in a dielectrophoretic system. *IEEE Transactions on Automatic Control*, 51(7), 1100-1114. [CPR2006] +- Formulation inspired by OptimalControl approach to time-optimal trajectory problems. +""" +function OptimalControlProblems.dielectrophoretic_particle_s( + ::OptimalControlBackend, + description::Symbol...; + grid_size::Int=grid_size_data(:dielectrophoretic_particle), + parameters::Union{Nothing, NamedTuple}=nothing, + kwargs..., +) + + # parameters + params = parameters_data(:dielectrophoretic_particle, parameters) + t0 = params[:t0] + x_t0 = params[:x_t0] + y_t0 = params[:y_t0] + x_tf = params[:x_tf] + α = params[:α] + c = params[:c] + u_l = params[:u_l] + u_u = params[:u_u] + tf_l = params[:tf_l] + + ocp = @def begin + tf ∈ R, variable + t ∈ [t0, tf], time + q = (x, y) ∈ R², state + u ∈ R, control + + x(t0) == x_t0, (x_t0) + y(t0) == y_t0, (y_t0) + x(tf) == x_tf, (x_tf) + tf ≥ tf_l, (tf_c) + u_l ≤ u(t) ≤ u_u, (u_c) + + ∂(x)(t) == y(t) * u(t) + α * u(t)^2 + ∂(y)(t) == -c * y(t) + u(t) + + tf → min + end + + # initial guess + init = (state=[1, 1], control=0.1, variable=5) + + # discretise the optimal control problem + docp = direct_transcription( + ocp, + description...; + lagrange_to_mayer=false, + init=init, + grid_size=grid_size, + disc_method=:trapeze, + kwargs..., + ) + + return docp +end diff --git a/ext/OptimalControlModels_s/double_oscillator_s.jl b/ext/OptimalControlModels_s/double_oscillator_s.jl new file mode 100644 index 00000000..0ab8631f --- /dev/null +++ b/ext/OptimalControlModels_s/double_oscillator_s.jl @@ -0,0 +1,89 @@ +""" +$(TYPEDSIGNATURES) + +Constructs an **OptimalControl problem** representing a double oscillator system. +The function defines state and control variables, system dynamics, boundary conditions, and an objective functional to be minimised. +It uses direct transcription to produce a discretised optimal control problem (DOCP) and the corresponding nonlinear programming (NLP) model. + +# Arguments + +- `::OptimalControlBackend`: Placeholder type specifying the OptimalControl backend or solver interface. +- `grid_size::Int=500`: (Keyword) Number of discretisation points for the direct transcription grid. + +# Returns + +- `docp`: The direct optimal control problem object representing the double oscillator system. +- `nlp`: The corresponding nonlinear programming model obtained from the DOCP, suitable for numerical optimisation. + +# Example + +```julia-repl +julia> using OptimalControlProblems + +julia> docp = OptimalControlProblems.double_oscillator(OptimalControlBackend(); N=100); +``` + +# References + +- Coudurier, C., Lepreux, O., & Petit, N. (2018). Optimal bang-bang control of a mechanical double oscillator using averaging methods. *IFAC-PapersOnLine*, 51(2), 49-54. [CLP2018] +- Formulation follows OptimalControl approach to mechanical oscillator trajectory optimisation. +""" +function OptimalControlProblems.double_oscillator_s( + ::OptimalControlBackend, + description::Symbol...; + grid_size::Int=grid_size_data(:double_oscillator), + parameters::Union{Nothing, NamedTuple}=nothing, + kwargs..., +) + + # parameters + params = parameters_data(:double_oscillator, parameters) + t0 = params[:t0] + tf = params[:tf] + m1 = params[:m1] + m2 = params[:m2] + c = params[:c] + k1 = params[:k1] + k2 = params[:k2] + u_l = params[:u_l] + u_u = params[:u_u] + x₁_t0 = params[:x₁_t0] + x₂_t0 = params[:x₂_t0] + + # model + ocp = @def begin + t ∈ [t0, tf], time + x ∈ R⁴, state + u ∈ R, control + + u_l ≤ u(t) ≤ u_u, (u_c) + x₁(t0) == x₁_t0, (x₁_t0) + x₂(t0) == x₂_t0, (x₂_t0) + + F = sin((t - t0) * 2π / (tf - t0)) + ∂(x₁)(t) == x₃(t) + ∂(x₂)(t) == x₄(t) + ∂(x₃)(t) == -(k1 + k2) / m1 * x₁(t) + k2 / m1 * x₂(t) + 1 / m1 * F + ∂(x₄)(t) == k2 / m2 * x₁(t) - k2 / m2 * x₂(t) - c * (1 - u(t)) / m2 * x₄(t) + + 0.5 * ∫(x₁(t)^2 + x₂(t)^2 + u(t)^2) → min + end + + # initial guess + xinit = [0.1, 0.1, 0.1, 0.1] # [x₁, x₂, x₃, x₄] + uinit = [0.1] # [u] + init = (state=xinit, control=uinit) + + # discretise the optimal control problem + docp = direct_transcription( + ocp, + description...; + lagrange_to_mayer=false, + init=init, + grid_size=grid_size, + disc_method=:trapeze, + kwargs..., + ) + + return docp +end diff --git a/ext/OptimalControlModels_s/ducted_fan_s.jl b/ext/OptimalControlModels_s/ducted_fan_s.jl new file mode 100644 index 00000000..91f88702 --- /dev/null +++ b/ext/OptimalControlModels_s/ducted_fan_s.jl @@ -0,0 +1,107 @@ +""" +The Ducted Fan Problem: + Implement the optimal control of a planar ducted fan. + Instance taken from [GP2009]. + The problem is formulated as an OptimalControl model. +Ref: Graichen, K., & Petit, N. (2009). Incorporating a class of constraints into the dynamics of optimal control problems. Optimal Control Applications and Methods, 30(6), 537-561. +""" +function OptimalControlProblems.ducted_fan_s( + ::OptimalControlBackend, + description::Symbol...; + grid_size::Int=grid_size_data(:ducted_fan), + parameters::Union{Nothing, NamedTuple}=nothing, + kwargs..., +) + + # parameters + params = parameters_data(:ducted_fan, parameters) + t0 = params[:t0] + r = params[:r] + J = params[:J] + m = params[:m] + mg = params[:mg] + μ = params[:μ] + α_l = params[:α_l] + α_u = params[:α_u] + u₁_l = params[:u₁_l] + u₁_u = params[:u₁_u] + u₂_l = params[:u₂_l] + u₂_u = params[:u₂_u] + tf_l = params[:tf_l] + x₁_t0 = params[:x₁_t0] + v₁_t0 = params[:v₁_t0] + x₂_t0 = params[:x₂_t0] + v₂_t0 = params[:v₂_t0] + α_t0 = params[:α_t0] + vα_t0 = params[:vα_t0] + x₁_tf = params[:x₁_tf] + v₁_tf = params[:v₁_tf] + x₂_tf = params[:x₂_tf] + v₂_tf = params[:v₂_tf] + α_tf = params[:α_tf] + vα_tf = params[:vα_tf] + + ocp = @def begin + tf ∈ R, variable + t ∈ [t0, tf], time + x = (x₁, v₁, x₂, v₂, α, vα) ∈ R⁶, state + u ∈ R², control + + # tf constraints + tf ≥ tf_l, (tf_c) + + # state constraints + α_l ≤ α(t) ≤ α_u, (α_c) + + # control constraints + u₁_l ≤ u₁(t) ≤ u₁_u, (u₁_c) + u₂_l ≤ u₂(t) ≤ u₂_u, (u₂_c) + + # initial constraints + x₁(t0) == x₁_t0, (x₁_t0) + v₁(t0) == v₁_t0, (v₁_t0) + x₂(t0) == x₂_t0, (x₂_t0) + v₂(t0) == v₂_t0, (v₂_t0) + α(t0) == α_t0, (α_t0) + vα(t0) == vα_t0, (vα_t0) + + # final constraints + x₁(tf) == x₁_tf, (x₁_tf) + v₁(tf) == v₁_tf, (v₁_tf) + x₂(tf) == x₂_tf, (x₂_tf) + v₂(tf) == v₂_tf, (v₂_tf) + α(tf) == α_tf , (α_tf ) + vα(tf) == vα_tf, (vα_tf) + + # dynamics + ∂(x₁)(t) == v₁(t) + ∂(v₁)(t) == (u₁(t) * cos(α(t)) - u₂(t) * sin(α(t))) / m + ∂(x₂)(t) == v₂(t) + ∂(v₂)(t) == (-mg + u₁(t) * sin(α(t)) + u₂(t) * cos(α(t))) / m + ∂(α)(t) == vα(t) + ∂(vα)(t) == r * u₁(t) / J + + # objective + (1 / tf) * ∫(2 * u₁(t)^2 + u₂(t)^2) + (μ * tf) → min + + end + + # initial guess + xinit = [0.1, 0.1, -0.1, 0.1, 0.1, 0.1] # [x₁, v₁, x₂, v₂, α, vα] + uinit = [0.1, 1] # [u₁, u₂] + varinit = [1.5] # [tf] + init = (state=xinit, control=uinit, variable=varinit) + + # DOCP + docp = direct_transcription( + ocp, + description...; + lagrange_to_mayer=false, + init=init, + grid_size=grid_size, + disc_method=:trapeze, + kwargs..., + ) + + return docp +end \ No newline at end of file diff --git a/ext/OptimalControlModels_s/electric_vehicle_s.jl b/ext/OptimalControlModels_s/electric_vehicle_s.jl new file mode 100644 index 00000000..d1cf2c40 --- /dev/null +++ b/ext/OptimalControlModels_s/electric_vehicle_s.jl @@ -0,0 +1,92 @@ +""" +$(TYPEDSIGNATURES) + +Constructs an **OptimalControl problem** for an electric vehicle trajectory. +The function defines state and control variables, vehicle dynamics, boundary conditions, and a cost functional representing energy and control effort. +It returns both a discretised direct optimal control problem (DOCP) and the corresponding nonlinear programming (NLP) model. + +# Arguments + +- `::OptimalControlBackend`: Placeholder type specifying the OptimalControl backend or solver interface. +- `grid_size::Int=500`: (Keyword) Number of discretisation points for the direct transcription grid. + +# Returns + +- `docp`: The direct optimal control problem object representing the electric vehicle trajectory optimisation. +- `nlp`: The corresponding nonlinear programming model obtained from the DOCP, suitable for numerical optimisation. + +# Example + +```julia-repl +julia> using OptimalControlProblems + +julia> docp = OptimalControlProblems.electric_vehicle(OptimalControlBackend(); N=500); +``` + +# References + +- Nicolas Petit and Antonio Sciarretta. "Optimal drive of electric vehicles using an inversion-based trajectory generation approach." IFAC Proceedings Volumes 44, no. 1 (2011): 14519-14526. [PS2011] +- Problem instance follows OptimalControl formulation for electric vehicle trajectory optimisation. +""" +function OptimalControlProblems.electric_vehicle_s( + ::OptimalControlBackend, + description::Symbol...; + grid_size::Int=grid_size_data(:electric_vehicle), + parameters::Union{Nothing, NamedTuple}=nothing, + kwargs..., +) + + # parameters + params = parameters_data(:electric_vehicle, parameters) + t0 = params[:t0] + tf = params[:tf] + b1 = params[:b1] + b2 = params[:b2] + h0 = params[:h0] + h1 = params[:h1] + h2 = params[:h2] + α0 = params[:α0] + α1 = params[:α1] + α2 = params[:α2] + α3 = params[:α3] + x_t0 = params[:x_t0] + v_t0 = params[:v_t0] + x_tf = params[:x_tf] + v_tf = params[:v_tf] + + # model + ocp = @def begin + t ∈ [t0, tf], time + y = (x, v) ∈ R², state + u ∈ R, control + + x(t0) == x_t0, (x_t0) + v(t0) == v_t0, (v_t0) + x(tf) == x_tf, (x_tf) + v(tf) == v_tf, (v_tf) + + road = α0 + α1 * x(t) + α2 * x(t)^2 + α3 * x(t)^3 + ∂(x)(t) == v(t) + ∂(v)(t) == h1 * u(t) - h2 * v(t)^2 - h0 - road + + ∫(b1 * u(t) * v(t) + b2 * u(t)^2) → min + end + + # initial guess + yinit = [0.1, 0.1] # [x, v] + uinit = [0.1] # [u] + init = (state=yinit, control=uinit) + + # discretise the optimal control problem + docp = direct_transcription( + ocp, + description...; + lagrange_to_mayer=false, + init=init, + grid_size=grid_size, + disc_method=:trapeze, + kwargs..., + ) + + return docp +end diff --git a/ext/OptimalControlModels_s/glider_s.jl b/ext/OptimalControlModels_s/glider_s.jl new file mode 100644 index 00000000..f2f66249 --- /dev/null +++ b/ext/OptimalControlModels_s/glider_s.jl @@ -0,0 +1,124 @@ +""" +$(TYPEDSIGNATURES) + +Constructs an **OptimalControl problem** for a hang glider trajectory. +The function defines state and control variables, glider dynamics in a thermal updraft, boundary conditions, and a cost functional aiming to maximise the final horizontal position. +It returns both a discretised direct optimal control problem (DOCP) and the corresponding nonlinear programming (NLP) model. + +# Arguments + +- `::OptimalControlBackend`: Placeholder type specifying the OptimalControl backend or solver interface. +- `grid_size::Int=500`: (Keyword) Number of discretisation points for the direct transcription grid. + +# Returns + +- `docp`: The direct optimal control problem object representing the hang glider trajectory optimisation. +- `nlp`: The corresponding nonlinear programming model obtained from the DOCP, suitable for numerical optimisation. + +# Example + +```julia-repl +julia> using OptimalControlProblems + +julia> docp = OptimalControlProblems.glider(OptimalControlBackend(); N=500); +``` + +# References + +- Original formulation from MadNLP/COPSBenchmark. +- Problem inspired by glider dynamics with thermal updraft and lift modelling. +""" +function OptimalControlProblems.glider_s( + ::OptimalControlBackend, + description::Symbol...; + grid_size::Int=grid_size_data(:glider), + parameters::Union{Nothing, NamedTuple}=nothing, + kwargs..., +) + + # parameters + params = parameters_data(:glider, parameters) + t0 = params[:t0] + x_t0 = params[:x_t0] + y_t0 = params[:y_t0] + y_tf = params[:y_tf] + vx_t0 = params[:vx_t0] + vx_tf = params[:vx_tf] + vy_t0 = params[:vy_t0] + vy_tf = params[:vy_tf] + u_c = params[:u_c] + r_t0 = params[:r_t0] + m = params[:m] + g = params[:g] + c0 = params[:c0] + c1 = params[:c1] + S = params[:S] + ρ = params[:ρ] + cL_min = params[:cL_min] + cL_max = params[:cL_max] + tf_l = params[:tf_l] + x_l = params[:x_l] + vx_l = params[:vx_l] + + # model + ocp = @def begin + tf ∈ R, variable + t ∈ [t0, tf], time + z = (x, y, vx, vy) ∈ R⁴, state + cL ∈ R, control + + # state constraints + x(t) ≥ x_l, (x_c) + vx(t) ≥ vx_l, (vx_c) + + # control constraints + cL_min ≤ cL(t) ≤ cL_max, (cL_c) + + # initial conditions + x(t0) == x_t0, (x0_t0) + y(t0) == y_t0, (y0_t0) + vx(t0) == vx_t0, (vx0_t0) + vy(t0) == vy_t0, (vy0_t0) + + # final conditions + tf ≥ tf_l + y(tf) == y_tf, (yf_tf) + vx(tf) == vx_tf, (vxf_tf) + vy(tf) == vy_tf, (vyf_tf) + + # dynamics + r = (x(t) / r_t0 - 2.5)^2 + UpD = u_c * (1 - r) * exp(-r) + w = vy(t) - UpD + v = √(vx(t)^2 + w^2) + D = 0.5 * (c0 + c1 * (cL(t)^2)) * ρ * S * (v^2) + L = 0.5 * cL(t) * ρ * S * (v^2) + + ∂(x)(t) == vx(t) + ∂(y)(t) == vy(t) + ∂(vx)(t) == -(L * w + D * vx(t)) / (m * v) + ∂(vy)(t) == (L * vx(t) - D * w) / (m * v) - g + + # objective + x(tf) → max + end + + # initial guess + tfinit = 1 + xinit = t -> [x_t0 + vx_t0 * t / tfinit, y_t0 + t / tfinit * (y_tf - y_t0), vx_t0, vy_t0] + uinit = cL_max / 2 + init = (state=xinit, control=uinit, variable=tfinit) + + # discretise the optimal control problem + docp = direct_transcription( + ocp, + description...; + lagrange_to_mayer=false, + init=init, + grid_size=grid_size, + disc_method=:trapeze, + kwargs..., + ) + + return docp +end diff --git a/ext/OptimalControlModels_s/insurance_s.jl b/ext/OptimalControlModels_s/insurance_s.jl new file mode 100644 index 00000000..5506fd62 --- /dev/null +++ b/ext/OptimalControlModels_s/insurance_s.jl @@ -0,0 +1,134 @@ +""" +$(TYPEDSIGNATURES) + +Constructs an **OptimalControl problem** for an insurance system optimisation. +The function defines state and control variables, system dynamics, constraints on insurance, expenses, revenue, health, utility, and auxiliary variables, and sets up a cost functional aimed at maximising expected utility over time. +It returns both a discretised direct optimal control problem (DOCP) and the corresponding nonlinear programming (NLP) model. + +# Arguments + +- `::OptimalControlBackend`: Placeholder type specifying the OptimalControl backend or solver interface. +- `grid_size::Int=500`: (Keyword) Number of discretisation points for the direct transcription grid. + +# Returns + +- `docp`: The direct optimal control problem object representing the insurance optimisation problem. +- `nlp`: The corresponding nonlinear programming model obtained from the DOCP, suitable for numerical optimisation. + +# Example + +```julia-repl +julia> using OptimalControlProblems + +julia> docp = OptimalControlProblems.insurance(OptimalControlBackend(); N=500); +``` + +# References + +- Problem formulation available at [Bocop repository](https://github.com/control-toolbox/bocop/tree/main/bocop) +""" +function OptimalControlProblems.insurance_s( + ::OptimalControlBackend, + description::Symbol...; + grid_size::Int=grid_size_data(:insurance), + parameters::Union{Nothing, NamedTuple}=nothing, + kwargs..., +) + + # parameters + params = parameters_data(:insurance, parameters) + t0 = params[:t0] + tf = params[:tf] + γ = params[:γ] + λ = params[:λ] + h0 = params[:h0] + w = params[:w] + s = params[:s] + k = params[:k] + σ = params[:σ] + α = params[:α] + I_l = params[:I_l] + I_u = params[:I_u] + m_l = params[:m_l] + m_u = params[:m_u] + h_l = params[:h_l] + h_u = params[:h_u] + R_l = params[:R_l] + H_l = params[:H_l] + U_l = params[:U_l] + dUdR_l = params[:dUdR_l] + P_l = params[:P_l] + I_t0 = params[:I_t0] + m_t0 = params[:m_t0] + x₃_t0 = params[:x₃_t0] + + # I: Insurance + # m: Expense + # R: Revenue + # H: Health + # U: Utility + + # Model + ocp = @def begin + P ∈ R, variable + t ∈ [t0, tf], time + x = (I, m, x₃) ∈ R³, state + u = (h, R, H, U, dUdR) ∈ R⁵, control + + # constraints + I_l ≤ I(t) ≤ I_u + m_l ≤ m(t) ≤ m_u + h_l ≤ h(t) ≤ h_u + R(t) ≥ R_l + H(t) ≥ H_l + U(t) ≥ U_l + dUdR(t) ≥ dUdR_l + P ≥ P_l + + x(t0) == [I_t0, m_t0, x₃_t0] + P - x₃(tf) == 0 + + # + ε = k * (t - t0) / (tf - t + 1) + + # illness distribution + fx = λ * exp(-λ * (t - t0)) + exp(-λ * (tf - t0)) / (tf - t0) + + # expense effect + v = m(t)^(α / 2) / (1 + m(t)^(α / 2)) + vprime = α / 2 * m(t)^(α / 2 - 1) / (1 + m(t)^(α / 2))^2 + + # constraints + R(t) - (w - P + I(t) - m(t) - ε) == 0 + H(t) - (h0 - γ * (t - t0) * (1 - v)) == 0 + U(t) - (1 - exp(-s * R(t)) + H(t)) == 0 + dUdR(t) - (s * exp(-s * R(t))) == 0 + + # dynamics + ∂(I)(t) == (1 - γ * (t - t0) * vprime / dUdR(t)) * h(t) + ∂(m)(t) == h(t) + ∂(x₃)(t) == (1 + σ) * I(t) * fx + + # objective + ∫(U(t) * fx) → max + end + + # initial guess + xinit = [0.1, 0.1, 0.1] # [I, m, x₃] + uinit = [0.1, 0.1, 0.1, 0.1, 0.1] # [h, R, H, U, dUdR] + varinit = [0.1] # [P] + init = (state=xinit, control=uinit, variable=varinit) + + # discretise the optimal control problem + docp = direct_transcription( + ocp, + description...; + lagrange_to_mayer=false, + init=init, + grid_size=grid_size, + disc_method=:trapeze, + kwargs..., + ) + + return docp +end diff --git a/ext/OptimalControlModels_s/jackson_s.jl b/ext/OptimalControlModels_s/jackson_s.jl new file mode 100644 index 00000000..bad8c194 --- /dev/null +++ b/ext/OptimalControlModels_s/jackson_s.jl @@ -0,0 +1,92 @@ +""" +$(TYPEDSIGNATURES) + +Constructs an **OptimalControl problem** for the Jackson benchmark model. +The function sets up state and control variables, system dynamics, bounds, initial and final conditions, and a cost functional aimed at minimising the third state variable at final time. +It returns both a discretised direct optimal control problem (DOCP) and the corresponding nonlinear programming (NLP) model. + +# Arguments + +- `::OptimalControlBackend`: Placeholder type specifying the OptimalControl backend or solver interface. +- `grid_size::Int=500`: (Keyword) Number of discretisation points for the direct transcription grid. + +# Returns + +- `docp`: The direct optimal control problem object representing the Jackson problem. +- `nlp`: The corresponding nonlinear programming model obtained from the DOCP, suitable for numerical optimisation. + +# Example + +```julia-repl +julia> using OptimalControlProblems + +julia> docp = OptimalControlProblems.jackson(OptimalControlBackend(); N=500); +``` + +# References + +- Problem formulation available at [Bocop repository](https://github.com/control-toolbox/bocop/tree/main/bocop) +""" +function OptimalControlProblems.jackson_s( + ::OptimalControlBackend, + description::Symbol...; + grid_size::Int=grid_size_data(:jackson), + parameters::Union{Nothing, NamedTuple}=nothing, + kwargs..., +) + + # parameters + params = parameters_data(:jackson, parameters) + t0 = params[:t0] + tf = params[:tf] + k1 = params[:k1] + k2 = params[:k2] + k3 = params[:k3] + a_l = params[:a_l] + a_u = params[:a_u] + b_l = params[:b_l] + b_u = params[:b_u] + x₃_l = params[:x₃_l] + x₃_u = params[:x₃_u] + u_l = params[:u_l] + u_u = params[:u_u] + a_t0 = params[:a_t0] + b_t0 = params[:b_t0] + x₃_t0 = params[:x₃_t0] + + # model + ocp = @def begin + t ∈ [t0, tf], time + x = (a, b, x₃) ∈ R³, state + u ∈ R, control + + x(t0) == [a_t0, b_t0, x₃_t0] + + [a_l, b_l, x₃_l] ≤ x(t) ≤ [a_u, b_u, x₃_u] + u_l ≤ u(t) ≤ u_u + + ∂(x₁)(t) == -u(t) * (k1 * a(t) - k2 * b(t)) + ∂(x₂)(t) == u(t) * (k1 * a(t) - k2 * b(t)) - (1 - u(t)) * k3 * b(t) + ∂(x₃)(t) == (1 - u(t)) * k3 * b(t) + + x₃(tf) → max + end + + # initial guess + xinit = [0.1, 0.1, 0.1] # [a, b, x₃] + uinit = [0.1] # [u] + init = (state=xinit, control=uinit) + + # discretise the optimal control problem + docp = direct_transcription( + ocp, + description...; + lagrange_to_mayer=false, + init=init, + grid_size=grid_size, + disc_method=:trapeze, + kwargs..., + ) + + return docp +end diff --git a/ext/OptimalControlModels_s/robbins_s.jl b/ext/OptimalControlModels_s/robbins_s.jl new file mode 100644 index 00000000..eb5da84f --- /dev/null +++ b/ext/OptimalControlModels_s/robbins_s.jl @@ -0,0 +1,85 @@ +""" +$(TYPEDSIGNATURES) + +Constructs an **OptimalControl problem** for the Robbins benchmark model. +This function defines the state and control variables, system dynamics, initial and final conditions, and the cost functional, which minimises a weighted sum of the state and control contributions. +It returns both a discretised direct optimal control problem (DOCP) and the corresponding nonlinear programming (NLP) model. +Reference: [Robbins Problem on BOCOP](https://github.com/control-toolbox/bocop/tree/main/bocop) + +# Arguments + +- `::OptimalControlBackend`: Placeholder type specifying the OptimalControl backend or solver interface. +- `grid_size::Int=500`: (Keyword) Number of discretisation points for the direct transcription grid. + +# Returns + +- `docp`: The direct optimal control problem object representing the Robbins problem. +- `nlp`: The corresponding nonlinear programming model obtained from the DOCP, suitable for numerical optimisation. + +# Example + +```julia-repl +julia> using OptimalControlProblems + +julia> docp = OptimalControlProblems.robbins(OptimalControlBackend(); N=500); +``` +""" +function OptimalControlProblems.robbins_s( + ::OptimalControlBackend, + description::Symbol...; + grid_size::Int=grid_size_data(:robbins), + parameters::Union{Nothing, NamedTuple}=nothing, + kwargs..., +) + + # parameters + params = parameters_data(:robbins, parameters) + t0 = params[:t0] + tf = params[:tf] + α = params[:α] + β = params[:β] + γ = params[:γ] + x₁_l = params[:x₁_l] + x₁_t0 = params[:x₁_t0] + x₂_t0 = params[:x₂_t0] + x₃_t0 = params[:x₃_t0] + x₁_tf = params[:x₁_tf] + x₂_tf = params[:x₂_tf] + x₃_tf = params[:x₃_tf] + + # model + ocp = @def begin + t ∈ [t0, tf], time + x ∈ R³, state + u ∈ R, control + + x[1](t) ≥ x₁_l + + x(t0) == [x₁_t0, x₂_t0, x₃_t0] + x(tf) == [x₁_tf, x₂_tf, x₃_tf] + + ∂(x₁)(t) == x₂(t) + ∂(x₂)(t) == x₃(t) + ∂(x₃)(t) == u(t) + + ∫(α * x₁(t) + β * x₁(t)^2 + γ * u(t)^2) → min + end + + # initial guess + xinit = [0.1, 0.1, 0.1] # [x₁, x₂, x₃] + uinit = [0.1] # [u] + init = (state=xinit, control=uinit) + + # discretise the optimal control problem + docp = direct_transcription( + ocp, + description...; + lagrange_to_mayer=false, + init=init, + grid_size=grid_size, + disc_method=:trapeze, + kwargs..., + ) + + return docp +end diff --git a/ext/OptimalControlModels_s/robot_s.jl b/ext/OptimalControlModels_s/robot_s.jl new file mode 100644 index 00000000..3c78c193 --- /dev/null +++ b/ext/OptimalControlModels_s/robot_s.jl @@ -0,0 +1,142 @@ +""" +$(TYPEDSIGNATURES) + +Constructs an **OptimalControl problem** for a robotic arm moving between two points. +This function defines the state and control variables, system dynamics, initial and final conditions, and the cost functional, which minimises the total time taken to perform the motion. +Reference: Robot arm problem on BOCOP [here](https://github.com/control-toolbox/bocop/tree/main/bocop) + +# Arguments + +- `::OptimalControlBackend`: Placeholder type specifying the OptimalControl backend or solver interface. +- `grid_size::Int=250`: (Keyword) Number of discretisation points for the direct transcription grid. + +# Returns + +- `docp`: The direct optimal control problem object representing the robot arm problem. +- `nlp`: The corresponding nonlinear programming model obtained from the DOCP, suitable for numerical optimisation. + +# Example + +```julia-repl +julia> using OptimalControlProblems + +julia> docp = OptimalControlProblems.robot(OptimalControlBackend(); N=250); +``` +""" +function OptimalControlProblems.robot_s( + ::OptimalControlBackend, + description::Symbol...; + grid_size::Int=grid_size_data(:robot), + parameters::Union{Nothing, NamedTuple}=nothing, + kwargs..., +) + + # parameters + params = parameters_data(:robot, parameters) + t0 = params[:t0] + + # total length of arm + L = params[:L] + + # Upper bounds on the controls + uρ_l = params[:uρ_l] + uρ_u = params[:uρ_u] + uθ_l = params[:uθ_l] + uθ_u = params[:uθ_u] + uϕ_l = params[:uϕ_l] + uϕ_u = params[:uϕ_u] + + # Initial positions of the length and the angles for the robot arm + ρ_t0 = params[:ρ_t0] + θ_t0 = params[:θ_t0] + ϕ_t0 = params[:ϕ_t0] + dρ_t0 = params[:dρ_t0] + dθ_t0 = params[:dθ_t0] + dϕ_t0 = params[:dϕ_t0] + + # Final positions + ρ_tf = params[:ρ_tf] + θ_tf = params[:θ_tf] + ϕ_tf = params[:ϕ_tf] + dρ_tf = params[:dρ_tf] + dθ_tf = params[:dθ_tf] + dϕ_tf = params[:dϕ_tf] + + # + ρ_l = params[:ρ_l] + ρ_u = L + θ_l = params[:θ_l] + θ_u = params[:θ_u] + ϕ_l = params[:ϕ_l] + ϕ_u = params[:ϕ_u] + tf_l = params[:tf_l] + + ocp = @def begin + tf ∈ R, variable + t ∈ [t0, tf], time + x = (ρ, dρ, θ, dθ, ϕ, dϕ) ∈ R⁶, state + u = (uρ, uθ, uϕ) ∈ R³, control + + tf ≥ tf_l + + # state constraints + ρ_l ≤ ρ(t) ≤ ρ_u, (ρ_c) + θ_l ≤ θ(t) ≤ θ_u, (θ_c) + ϕ_l ≤ ϕ(t) ≤ ϕ_u, (ϕ_c) + + # control constraints + uρ_l ≤ uρ(t) ≤ uρ_u, (u_ρ_c) + uθ_l ≤ uθ(t) ≤ uθ_u, (u_θ_c) + uϕ_l ≤ uϕ(t) ≤ uϕ_u, (u_ϕ_c) + + # initial conditions + ρ(t0) == ρ_t0, (ρ_t0) + θ(t0) == θ_t0, (θ_t0) + ϕ(t0) == ϕ_t0, (ϕ_t0) + dρ(t0) == dρ_t0, (dρ_t0) + dθ(t0) == dθ_t0, (dθ_t0) + dϕ(t0) == dϕ_t0, (dϕ_t0) + + # final conditions + ρ(tf) == ρ_tf, (ρ_tf) + θ(tf) == θ_tf, (θ_tf) + ϕ(tf) == ϕ_tf, (ϕ_tf) + dρ(tf) == dρ_tf, (dρ_tf) + dθ(tf) == dθ_tf, (dθ_tf) + dϕ(tf) == dϕ_tf, (dϕ_tf) + + # aliases + I_θ = ((L - ρ(t))^3 + ρ(t)^3) * sin(ϕ(t))^2 + I_ϕ = (L - ρ(t))^3 + ρ(t)^3 + + # dynamics + ∂(ρ)(t) == dρ(t) + ∂(dρ)(t) == uρ(t) / L + ∂(θ)(t) == dθ(t) + ∂(dθ)(t) == 3 * uθ(t) / I_θ + ∂(ϕ)(t) == dϕ(t) + ∂(dϕ)(t) == 3 * uϕ(t) / I_ϕ + + # objective + tf → min + end + + # initial guess + tf = 1 + xinit = t -> [ρ_t0, 0, 2π/3 * ((t - t0) / (tf - t0))^2, 4π/3 * ((t - t0) / (tf - t0)), ϕ_t0, 0] + uinit = [0, 0, 0] + init = (state=xinit, control=uinit, variable=tf) + + # discretise the optimal control problem + docp = direct_transcription( + ocp, + description...; + lagrange_to_mayer=false, + init=init, + grid_size=grid_size, + disc_method=:trapeze, + kwargs..., + ) + + return docp +end diff --git a/ext/OptimalControlModels_s/rocket_s.jl b/ext/OptimalControlModels_s/rocket_s.jl new file mode 100644 index 00000000..7e51b271 --- /dev/null +++ b/ext/OptimalControlModels_s/rocket_s.jl @@ -0,0 +1,110 @@ +""" +$(TYPEDSIGNATURES) + +Constructs an **OptimalControl problem** for the Goddard rocket. +This function defines the state variables (altitude, velocity, mass), the control variable (thrust), system dynamics, constraints, initial and final conditions, and the cost functional, which maximises the final altitude. +Reference: Goddard Rocket Problem [here](https://github.com/control-toolbox/bocop/tree/main/bocop) + +# Arguments + +- `::OptimalControlBackend`: Placeholder type specifying the OptimalControl backend or solver interface. +- `grid_size::Int=500`: (Keyword) Number of discretisation points for the direct transcription grid. + +# Returns + +- `docp`: The direct optimal control problem object representing the Goddard rocket problem. +- `nlp`: The corresponding nonlinear programming model obtained from the DOCP, suitable for numerical optimisation. + +# Example + +```julia-repl +julia> using OptimalControlProblems + +julia> docp = OptimalControlProblems.rocket(OptimalControlBackend(); N=500); +``` +""" +function OptimalControlProblems.rocket_s( + ::OptimalControlBackend, + description::Symbol...; + grid_size::Int=grid_size_data(:rocket), + parameters::Union{Nothing, NamedTuple}=nothing, + kwargs..., +) + + # parameters + params = parameters_data(:rocket, parameters) + t0 = params[:t0] + h_t0 = params[:h_t0] + v_t0 = params[:v_t0] + m_t0 = params[:m_t0] + g0 = params[:g0] + Tc = params[:Tc] + hc = params[:hc] + vc = params[:vc] + mc = params[:mc] + T_l = params[:T_l] + tf_l = params[:tf_l] + + # + c = 0.5 * sqrt(g0 * h_t0) + m_tf = mc * m_t0 + Dc = 0.5 * vc * (m_t0 / g0) + Tmax = Tc * m_t0 * g0 + + # Model + ocp = @def begin + tf ∈ R, variable + t ∈ [t0, tf], time + x = (h, v, m) ∈ R³, state + T ∈ R, control + + # state constraints + h(t) ≥ h_t0, (h_c) + v(t) ≥ v_t0, (v_c) + m_tf ≤ m(t) ≤ m_t0, (m_c) + + # control constraints + T_l ≤ T(t) ≤ Tmax, (T_c) + + # time constraints + tf ≥ tf_l, (tf_c) + + # initial conditions + h(t0) == h_t0, (h_t0) + v(t0) == v_t0, (v_t0) + m(t0) == m_t0, (m_t0) + + # final conditions + m(tf) == m_tf, (m_tf) + + # dynamics + D = (Dc * v(t)^2 * exp(-hc * (h(t) - h_t0)) / h_t0) + g = g0 * (h_t0 / h(t))^2 + ∂(h)(t) == v(t) + ∂(v)(t) == (T(t) - D - m(t) * g) / m(t) + ∂(m)(t) == -T(t) / c + + # objective + h(tf) → max + end + + # initial guess + N = grid_size + tf_init = 1 + xinit = [[1, i / N * (1 - i / N), (m_tf - m_t0) * (i / N) + m_t0] for i in 0:N] + time_vec = LinRange(0, tf_init, N+1) + init = (time=time_vec, state=xinit, control=Tmax/2, variable=tf_init) + + # discretise the optimal control problem + docp = direct_transcription( + ocp, + description...; + lagrange_to_mayer=false, + init=init, + grid_size=grid_size, + disc_method=:trapeze, + kwargs..., + ) + + return docp +end diff --git a/ext/OptimalControlModels_s/space_shuttle_s.jl b/ext/OptimalControlModels_s/space_shuttle_s.jl new file mode 100644 index 00000000..95a035ca --- /dev/null +++ b/ext/OptimalControlModels_s/space_shuttle_s.jl @@ -0,0 +1,189 @@ +""" +$(TYPEDSIGNATURES) + +Constructs an **OptimalControl problem** for the Space Shuttle reentry trajectory. +This function defines the state variables (altitude, longitude, latitude, velocity, flight path angle, azimuth), the control variables (angle of attack, bank angle), system dynamics, constraints, initial and terminal conditions, and the cost functional, which maximises the latitude (cross range) at the terminal point. +Reference: Original JuMP model formulation [here](https://jump.dev/JuMP.jl/stable/tutorials/nonlinear/space_shuttle_reentry_trajectory/). +Note: No heating limit path constraint is included. + +# Arguments + +- `::OptimalControlBackend`: Placeholder type specifying the OptimalControl backend or solver interface. +- `grid_size::Int=500`: (Keyword) Number of discretisation points for the direct transcription grid. + +# Returns + +- `docp`: The direct optimal control problem object representing the Space Shuttle reentry trajectory. +- `nlp`: The corresponding nonlinear programming model obtained from the DOCP, suitable for numerical optimisation. + +# Example + +```julia-repl +julia> using OptimalControlProblems + +julia> docp = OptimalControlProblems.space_shuttle(OptimalControlBackend(); N=500); +``` +""" +function OptimalControlProblems.space_shuttle_s( + ::OptimalControlBackend, + description::Symbol...; + grid_size::Int=grid_size_data(:space_shuttle), + parameters::Union{Nothing, NamedTuple}=nothing, + kwargs..., +) + + # Parameters + params = parameters_data(:space_shuttle, parameters) + t0 = params[:t0] + + ## + w = params[:w] + g₀ = params[:g₀] + m = w / g₀ # mass (slug) + + ## Aerodynamic and atmospheric forces on the vehicle + ρ₀ = params[:ρ₀] + hᵣ = params[:hᵣ] + Rₑ = params[:Rₑ] + μ = params[:μ] + S = params[:S] + a₀ = params[:a₀] + a₁ = params[:a₁] + b₀ = params[:b₀] + b₁ = params[:b₁] + b₂ = params[:b₂] + + # + Δt_min = params[:Δt_min] + Δt_max = params[:Δt_max] + tf_l = grid_size*Δt_min + tf_u = grid_size*Δt_max + + ## Initial conditions + h_t0 = params[:h_t0] + ϕ_t0 = params[:ϕ_t0] + θ_t0 = params[:θ_t0] + v_t0 = params[:v_t0] + γ_t0 = params[:γ_t0] + ψ_t0 = params[:ψ_t0] + + # for initial guess + α_s = params[:α_s] + β_s = params[:β_s] + + ## Final conditions, the so-called Terminal Area Energy Management (TAEM) + h_tf = params[:h_tf] + v_tf = params[:v_tf] + γ_tf = params[:γ_tf] + + ## + h_l = params[:h_l] + ϕ_l = params[:ϕ_l] + ϕ_u = params[:ϕ_u] + θ_l = params[:θ_l] + θ_u = params[:θ_u] + v_l = params[:v_l] + γ_l = params[:γ_l] + γ_u = params[:γ_u] + ψ_l = params[:ψ_l] + ψ_u = params[:ψ_u] + α_l = params[:α_l] + α_u = params[:α_u] + β_l = params[:β_l] + β_u = params[:β_u] + + ## Scalings + scaling_h = 1e5 + scaling_v = 1e4 + + # model + ocp = @def begin + # define the problem + tf ∈ R, variable + t ∈ [t0, tf], time + x = (scaled_h, ϕ, θ, scaled_v, γ, ψ) ∈ R⁶, state + u = (α, β) ∈ R², control + + # constraints + ## to help convergence and avoid domain value error + ϕ_l ≤ ϕ(t) ≤ ϕ_u + ψ_l ≤ ψ(t) ≤ ψ_u + + ## final time constraints + tf_l ≤ tf ≤ tf_u + + ## state constraints + scaled_h(t) ≥ h_l, (scaled_h_c) + θ_l ≤ θ(t) ≤ θ_u, (θ_c) + scaled_v(t) ≥ v_l, (scaled_v_c) + γ_l ≤ γ(t) ≤ γ_u, (γ_c) + + ## control constraints + α_l ≤ α(t) ≤ α_u, (α_c) + β_l ≤ β(t) ≤ β_u, (β_c) + + ## initial conditions + scaled_h(t0) == h_t0, (scaled_h_t0) + ϕ(t0) == ϕ_t0, (ϕ_t0) + θ(t0) == θ_t0, (θ_t0) + scaled_v(t0) == v_t0, (scaled_v_t0) + γ(t0) == γ_t0, (γ_t0) + ψ(t0) == ψ_t0, (ψ_t0) + + ## final conditions + scaled_h(tf) == h_tf, (scaled_h_tf) + scaled_v(tf) == v_tf, (scaled_v_tf) + γ(tf) == γ_tf, (γ_tf) + + # Helper functions + h = scaled_h(t) * scaling_h + v = scaled_v(t) * scaling_v + r2dα = α(t) * (180 / π) + c_D = b₀ + b₁ * r2dα + b₂ * r2dα^2 + c_L = a₀ + a₁ * r2dα + ρ = ρ₀ * exp(-h / hᵣ) + D = (1 / 2) * c_D * S * ρ * (v^2) + L = (1 / 2) * c_L * S * ρ * (v^2) + r = Rₑ + h + g = μ / (r^2) + + # dynamics + ∂(scaled_h)(t) == v * sin(γ(t)) / scaling_h + ∂(ϕ)(t) == (v / r) * cos(γ(t)) * sin(ψ(t)) / cos(θ(t)) + ∂(θ)(t) == (v / r) * cos(γ(t)) * cos(ψ(t)) + ∂(scaled_v)(t) == (-(D / m) - g * sin(γ(t))) / scaling_v + ∂(γ)(t) == (L / (m * v)) * cos(β(t)) + cos(γ(t)) * ((v / r) - (g / v)) + ∂(ψ)(t) == (1 / (m * v * cos(γ(t)))) * L * sin(β(t)) + (v / (r * cos(θ(t)))) * cos(γ(t)) * sin(ψ(t)) * sin(θ(t)) + + # objective + θ(tf) → max + end + + # initial guess: linear interpolation for h, v, gamma (NB. t0 = 0), constant for the rest + # variable time step seems to be initialized at 1 in jump + # note that ipopt will project the initial guess inside the bounds anyway. + tf_init = (tf_l+tf_u)/2 + x_init = + t -> [ + h_t0 + (t - t0) / (tf_init - t0) * (h_tf - h_t0), + ϕ_t0, + θ_t0, + v_t0 + (t - t0) / (tf_init - t0) * (v_tf - v_t0), + γ_t0 + (t - t0) / (tf_init - t0) * (γ_tf - γ_t0), + ψ_t0, + ] + init = (state=x_init, control=[α_s, β_s], variable=[tf_init]) + + # discretise the optimal control problem + docp = direct_transcription( + ocp, + description...; + lagrange_to_mayer=false, + init=init, + grid_size=grid_size, + disc_method=:trapeze, + kwargs..., + ) + + return docp +end diff --git a/ext/OptimalControlModels_s/steering_s.jl b/ext/OptimalControlModels_s/steering_s.jl new file mode 100644 index 00000000..c9578db4 --- /dev/null +++ b/ext/OptimalControlModels_s/steering_s.jl @@ -0,0 +1,102 @@ +""" +$(TYPEDSIGNATURES) + +Constructs an **OptimalControl problem** for steering a particle along a trajectory. +The objective is to minimise the total time taken to reach a specified terminal state, subject to control limits and particle dynamics. +The state vector has four components, and the control is a single scalar input. + +# Arguments + +- `::OptimalControlBackend`: Placeholder type specifying the OptimalControl backend or solver interface. +- `grid_size::Int=500`: (Keyword) Number of discretisation points for the direct transcription grid. + +# Returns + +- `docp`: The direct optimal control problem object representing the particle steering problem. +- `nlp`: The corresponding nonlinear programming model obtained from the DOCP, suitable for numerical optimisation. + +# Example + +```julia-repl +julia> using OptimalControlProblems + +julia> docp = OptimalControlProblems.steering(OptimalControlBackend(); N=500); +``` +""" +function OptimalControlProblems.steering_s( + ::OptimalControlBackend, + description::Symbol...; + grid_size::Int=grid_size_data(:steering), + parameters::Union{Nothing, NamedTuple}=nothing, + kwargs..., +) + + # parameters + params = parameters_data(:steering, parameters) + t0 = params[:t0] + a = params[:a] + u_min = params[:u_min] + u_max = params[:u_max] + tf_l = params[:tf_l] + x₁_t0 = params[:x₁_t0] + x₂_t0 = params[:x₂_t0] + x₃_t0 = params[:x₃_t0] + x₄_t0 = params[:x₄_t0] + x₂_tf = params[:x₂_tf] + x₃_tf = params[:x₃_tf] + x₄_tf = params[:x₄_tf] + + # Model + ocp = @def begin + tf ∈ R, variable + t ∈ [t0, tf], time + x ∈ R⁴, state + u ∈ R, control + + x₁(t0) == x₁_t0, (x₁_t0) + x₂(t0) == x₂_t0, (x₂_t0) + x₃(t0) == x₃_t0, (x₃_t0) + x₄(t0) == x₄_t0, (x₄_t0) + x₂(tf) == x₂_tf, (x₂_tf) + x₃(tf) == x₃_tf, (x₃_tf) + x₄(tf) == x₄_tf, (x₄_tf) + + tf ≥ tf_l, (tf_c) + u_min ≤ u(t) ≤ u_max, (u_c) + + ∂(x₁)(t) == x₃(t) + ∂(x₂)(t) == x₄(t) + ∂(x₃)(t) == a * cos(u(t)) + ∂(x₄)(t) == a * sin(u(t)) + + tf → min + end + + # dynamics + + # initial guess + function gen_x0(t, i) + if i == 1 || i == 4 + return 0.0 + elseif i == 2 + return 5.0 * (t-t0) + elseif i == 3 + return 45.0 * (t-t0) + end + end + xinit = t -> [gen_x0(t, i) for i in 1:4] + init = (state=xinit, control=0, variable=1) + + # discretise the optimal control problem + docp = direct_transcription( + ocp, + description...; + lagrange_to_mayer=false, + init=init, + grid_size=grid_size, + disc_method=:trapeze, + kwargs..., + ) + + return docp +end diff --git a/ext/OptimalControlModels_s/vanderpol_s.jl b/ext/OptimalControlModels_s/vanderpol_s.jl new file mode 100644 index 00000000..d27f0934 --- /dev/null +++ b/ext/OptimalControlModels_s/vanderpol_s.jl @@ -0,0 +1,74 @@ +""" +$(TYPEDSIGNATURES) + +Constructs an **OptimalControl problem** for the Van der Pol oscillator with a control input. +The objective is to minimise a quadratic cost composed of the state and control effort over a fixed time horizon. +The problem formulation can be found [here](https://github.com/control-toolbox/bocop/tree/main/bocop). + +# Arguments + +- `::OptimalControlBackend`: Placeholder type specifying the OptimalControl backend or solver interface. +- `grid_size::Int=500`: (Keyword) Number of discretisation points for the direct transcription grid. + +# Returns + +- `docp`: The direct optimal control problem object representing the Van der Pol problem. +- `nlp`: The corresponding nonlinear programming model obtained from the DOCP, suitable for numerical optimisation. + +# Example + +```julia-repl +julia> using OptimalControlProblems + +julia> docp = OptimalControlProblems.vanderpol(OptimalControlBackend(); N=500); +``` +""" +function OptimalControlProblems.vanderpol_s( + ::OptimalControlBackend, + description::Symbol...; + grid_size::Int=grid_size_data(:vanderpol), + parameters::Union{Nothing, NamedTuple}=nothing, + kwargs..., +) + + # parameters + params = parameters_data(:vanderpol, parameters) + t0 = params[:t0] + tf = params[:tf] + ω = params[:ω] + ε = params[:ε] + x₁_t0 = params[:x₁_t0] + x₂_t0 = params[:x₂_t0] + + # model + ocp = @def begin + t ∈ [t0, tf], time + x ∈ R², state + u ∈ R, control + + x(t0) == [x₁_t0, x₂_t0] + + ∂(x₁)(t) == x₂(t) + ∂(x₂)(t) == ε * ω * (1 - x₁(t)^2) * x₂(t) - ω^2 * x₁(t) + u(t) + + 0.5∫(x₁(t)^2 + x₂(t)^2 + u(t)^2) → min + end + + # initial guess + xinit = [0.1, 0.1] # [x₁, x₂] + uinit = [0.1] # [u] + init = (state=xinit, control=uinit) + + # discretise the optimal control problem + docp = direct_transcription( + ocp, + description...; + lagrange_to_mayer=false, + init=init, + grid_size=grid_size, + disc_method=:trapeze, + kwargs..., + ) + + return docp +end diff --git a/src/OptimalControlProblems.jl b/src/OptimalControlProblems.jl index 08335ffa..83d2a57a 100644 --- a/src/OptimalControlProblems.jl +++ b/src/OptimalControlProblems.jl @@ -2,7 +2,19 @@ module OptimalControlProblems using CTBase using CTDirect -import CTModels: CTModels, time_grid, state, control, costate, iterations +import CTModels: + CTModels, + time_grid, + state, + control, + costate, + iterations, + control_components, + control_dimension, + state_components, + state_dimension, + variable_components, + variable_dimension import ExaModels: ExaModels, ExaModel, variable, objective using DocStringExtensions using OrderedCollections: OrderedDict @@ -10,15 +22,13 @@ using SolverCore import ADNLPModels: ADNLPModels, ADNLPModel # ----------------- -# SHOULD NO BE HERE -nlp_model(docp::CTDirect.DOCP) = docp.nlp -ocp_model(docp::CTDirect.DOCP) = docp.ocp +# SHOULD NO BE HERE ("triiiiiiiiiiiiiiit !!!" - bruit de sifflet) function build_ocp_solution( docp::CTDirect.DOCP, nlp_solution::SolverCore.AbstractExecutionStats ) - nlp_model_backend = if nlp_model(docp) isa ADNLPModel + nlp_model_backend = if docp.nlp isa ADNLPModel CTDirect.ADNLPBackend() - elseif nlp_model(docp) isa ExaModel + elseif docp.nlp isa ExaModel CTDirect.ExaBackend() else throw(CTBase.IncorrectArgument("The NLP model is of unknown type.")) @@ -26,61 +36,72 @@ function build_ocp_solution( return CTDirect.build_OCP_solution(docp, nlp_solution; nlp_model=nlp_model_backend) end -export nlp_model, ocp_model, build_ocp_solution +export build_ocp_solution # """ $(TYPEDEF) -Abstract type for all optimal control problem model back-ends. +Abstract type for all optimal control problem model backends. """ abstract type AbstractModelBackend end """ $(TYPEDEF) -Back-end for modelling optimal control problems using the JuMP optimisation framework. +Backend for modelling optimal control problems using the JuMP optimisation framework. """ struct JuMPBackend <: AbstractModelBackend end """ $(TYPEDEF) -Back-end for modelling optimal control problems using the OptimalControl.jl package. +Backend for modelling optimal control problems using the OptimalControl.jl package. """ struct OptimalControlBackend <: AbstractModelBackend end # weak dependencies weakdeps = Dict(OptimalControlBackend => :OptimalControl, JuMPBackend => :JuMP) -# path to problems -path = joinpath(dirname(@__FILE__), "..", "ext", "MetaData") +# Create the list of problems +function make_list_of_problems() -# ------- Problem Definitions ------- -files = filter(x -> x[(end - 2):end] == ".jl", readdir(path)) -for file in files - problem = Symbol(file[1:(end - 3)]) + # path to problems + path = joinpath(dirname(@__FILE__), "..", "ext", "MetaData") - # Build the docstring string explicitly here - doc = """ - $(TYPEDSIGNATURES) + # ------- Problem Definitions ------- + files = filter(x -> x[(end - 2):end] == ".jl", readdir(path)) + + # collect all the problems + list_of_problems = Symbol[] + for file in files + problem = Symbol(file[1:(end - 3)]) + push!(list_of_problems, problem) + end - Defines the optimal control problem `$(string(problem))` for a given back-end. + # exclude the following problems + problems_to_exclude = [ + :bioreactor, + :cart_pendulum, + :dielectrophoretic_particle, + :moonlander, + ] + list_of_problems = setdiff(list_of_problems, problems_to_exclude) - # Arguments + return tuple(list_of_problems...), path - - `model_backend::T`: The modelling back-end, subtype of `AbstractModelBackend`. - - `N::Int=0`: Number of discretisation steps (optional). +end - # Returns +const LIST_OF_PROBLEMS, METADATA_PATH = make_list_of_problems() - - Throws an `ExtensionError` if the required back-end is not available. +for problem in LIST_OF_PROBLEMS + problem_s = Symbol(problem, :_s) - # Example + # Build the docstring string explicitly here + doc = """ + $(TYPEDSIGNATURES) - ```julia-repl - julia> $(string(problem))(JuMPBackend(); N=20) - ERROR: ExtensionError(:JuMP) + This method throws an `ExtensionError` and is called if the required backend is not available. ``` """ @@ -94,344 +115,307 @@ for file in files end eval(code) + + doc_s = """ + $(TYPEDSIGNATURES) + + This method throws an `ExtensionError` and is called if the required backend is not available. + ``` + """ + + code_s = quote + @doc $doc_s function $problem_s( + model_backend::T, args...; kwargs... + ) where {T<:AbstractModelBackend} + throw(CTBase.ExtensionError(weakdeps[T])) + end + export $problem_s + end + + eval(code_s) end # ------- Problem Metadata ------- -for file in files - include(joinpath(path, file)) +for problem in LIST_OF_PROBLEMS + include(joinpath(METADATA_PATH, "$problem.jl")) end -number_of_problems = length(files) - -const infos = [ - :name - :N - :minimise - :state_name - :costate_name - :control_name - :variable_name - :final_time + +const METADATA_INFOS = [ + :grid_size + :parameters ] -const types = [ - String, +const METADATA_TYPES = [ Int, - Bool, - Vector{String}, - Vector{String}, - Vector{String}, - Union{Vector{String},Nothing}, - Tuple{Symbol,Union{Float64,Int}}, + Union{Nothing,NamedTuple}, ] -""" -metadata::Dict() - -Dictionary containing metadata for all available optimal control problems. - -The following keys are valid: - -- `name::String`: the problem name. -- `N::Int`: the default number of steps. -- `minimise::Bool`: indicates whether the objective function is minimised (`true`) or maximised (`false`). -- `state_name::Vector{String}`: names of the state components. -- `costate_name::Vector{String}`: names of the differential constraints to obtain the costate (dual variables associated with the differential constraints). -- `control_name::Vector{String}`: names of the control components. -- `variable_name::Union{Vector{String},Nothing}`: names of the optimisation variables, or `nothing` if no such variable exists. -- `final_time::Tuple{Symbol, Union{Float64, Int}}`: of the form `(type, value_or_index)`, where: - - `type` is either `:fixed` or `:free`. - - `value_or_index` is the index in `variable` if the final time is free, or its value if it is fixed. - -# Example - -```julia-repl -julia> metadata[:my_problem][:name] -"My Problem" -``` -""" -const metadata = Dict() +const METADATA_STORAGE = OrderedDict() -for i in 1:number_of_problems - file_key = Symbol(split(files[i], ".")[1]) - metadata[file_key] = OrderedDict() - for (data, T) in zip(infos, types) - value = eval(Meta.parse("$(file_key)_meta"))[data] +for problem in LIST_OF_PROBLEMS + METADATA_STORAGE[problem] = OrderedDict() + for (data, T) in zip(METADATA_INFOS, METADATA_TYPES) + value = eval(Meta.parse("$(problem)_meta"))[data] if !(value isa T) error("Type mismatch: Expected $(T) for $(data), but got $(typeof(value))") end - if data == :final_time - if (value[1] != :fixed) && (value[1] != :free) - error("Incorrect value: Expected free or :fixed for $(value[1])") - end - end - metadata[file_key][data] = value + METADATA_STORAGE[problem][data] = value end end -# ------- Available Problems Function ------- """ $(TYPEDSIGNATURES) -Returns the list of available optimal control problems. - -# Returns - -- `Vector{Symbol}`: A vector of problem names as symbols. +Return the dictionary containing the metadata of all available optimal control problems. # Example ```julia-repl -julia> OptimalControlProblems.problems() -[:problem1, :problem2, :problem3] +julia> metadata() ``` """ -function problems()::Vector{Symbol} - - # - list_of_problems = Symbol[] - - # collect all the problems - files = filter(x -> x[(end - 2):end] == ".jl", readdir(path)) - for file in files - problem = Symbol(file[1:(end - 3)]) - push!(list_of_problems, problem) - end - - # # exclude the following problems - # problems_to_exclude = [ - - # ] - # list_of_problems = setdiff(list_of_problems, problems_to_exclude) - - return list_of_problems -end +metadata() = METADATA_STORAGE """ $(TYPEDSIGNATURES) -Retrieve the discretised time grid from a JuMP model. +Return a dictionary containing the metadata of `problem`. -# Arguments - -- `::Symbol`: Problem name. -- `model`: JuMP model object. +To get specific data, the following keys are valid: -# Returns - -- Throws `ExtensionError(:JuMP)` since JuMP support must be extended. +- `grid_size::Int`: the default number of steps. For example: +```julia +:grid_size => 500, +``` +- `parameters::Union{Nothing,NamedTuple}`: the list of parameters. For example: +```julia +:parameters => ( + t0 = 0, + tf = 1, + x₁_l = 0, + x₁_u = 0.1, + x₁_t0 = 0, + x₂_t0 = 1, + x₁_tf = 0, + x₂_tf = -1, +), +``` # Example ```julia-repl -julia> time_grid(:problem1, model) -ERROR: ExtensionError(:JuMP) +julia> data = metadata(:beam) +julia> data[:grid_size] +500 ``` """ -function time_grid(::Symbol, model) - throw(CTBase.ExtensionError(:JuMP)) +function metadata(problem::Symbol) + !(problem ∈ keys(METADATA_STORAGE)) && throw(CTBase.IncorrectArgument("There is no problem named $problem in metadata. To get the list of available problems, make julia> metadata()")) + return METADATA_STORAGE[problem] end +# ------- Available Problems Function ------- """ $(TYPEDSIGNATURES) -Retrieve the state trajectory from a JuMP model. - -# Arguments - -- `::Symbol`: Problem name. -- `model`: JuMP model object. +Returns the list of available optimal control problems. # Returns -- Throws `ExtensionError(:JuMP)` since JuMP support must be extended. +- `Vector{Symbol}`: A vector of problem names as symbols. # Example ```julia-repl -julia> state(:problem1, model) -ERROR: ExtensionError(:JuMP) +julia> OptimalControlProblems.problems() +[:problem1, :problem2, :problem3] ``` """ -function state(::Symbol, model) - throw(CTBase.ExtensionError(:JuMP)) +function problems()::Vector{Symbol} + return Symbol[LIST_OF_PROBLEMS...] end """ $(TYPEDSIGNATURES) -Retrieve the costate (adjoint variables) from a JuMP model. +Return the number of discretisation steps, from the metadata, for a given optimal control problem. # Arguments -- `::Symbol`: Problem name. -- `model`: JuMP model object. +- `problem::Symbol`: The name of the problem. # Returns -- Throws `ExtensionError(:JuMP)` since JuMP support must be extended. +- `Int`: The number of discretisation steps (`N`) of the specified problem. # Example ```julia-repl -julia> costate(:problem1, model) -ERROR: ExtensionError(:JuMP) +julia> grid_size_data(:beam) +500 ``` """ -function costate(::Symbol, model) - throw(CTBase.ExtensionError(:JuMP)) +function grid_size_data(problem::Symbol) + return metadata(problem)[:grid_size] end """ $(TYPEDSIGNATURES) -Retrieve the control trajectory from a JuMP model. +Merge two `Nothing` values. # Arguments -- `::Symbol`: Problem name. -- `model`: JuMP model object. +- `::Nothing`: first argument. +- `::Nothing`: second argument. # Returns -- Throws `ExtensionError(:JuMP)` since JuMP support must be extended. +- `nothing::Nothing`: always returns `nothing`. # Example ```julia-repl -julia> control(:problem1, model) -ERROR: ExtensionError(:JuMP) +julia> merge(nothing, nothing) +nothing ``` """ -function control(::Symbol, model) - throw(CTBase.ExtensionError(:JuMP)) -end +merge(::Nothing, ::Nothing) = nothing """ $(TYPEDSIGNATURES) -Retrieve optimisation variables from a JuMP model. +Merge a `NamedTuple` with `nothing`. # Arguments -- `::Symbol`: Problem name. -- `model`: JuMP model object. +- `A::NamedTuple`: a named tuple to keep. +- `::Nothing`: placeholder, ignored. # Returns -- Throws `ExtensionError(:JuMP)` since JuMP support must be extended. +- `::NamedTuple`: returns `A` unchanged. # Example ```julia-repl -julia> variable(:problem1, model) -ERROR: ExtensionError(:JuMP) +julia> merge((a=1,), nothing) +(a = 1,) ``` """ -function variable(::Symbol, model) - throw(CTBase.ExtensionError(:JuMP)) -end +merge(A::NamedTuple, ::Nothing) = A """ $(TYPEDSIGNATURES) -Retrieve objective value from a JuMP model. +Throw an error when attempting to merge `nothing` with a `NamedTuple`. # Arguments -- `::Symbol`: Problem name. -- `model`: JuMP model object. +- `::Nothing`: indicates there is no data to merge. +- `::NamedTuple`: the data that cannot be merged. # Returns -- Throws `ExtensionError(:JuMP)` since JuMP support must be extended. +- This function always throws `CTBase.UnauthorizedCall`. # Example ```julia-repl -julia> objective(:problem1, model) -ERROR: ExtensionError(:JuMP) +julia> merge(nothing, (a=1,)) +ERROR: CTBase.UnauthorizedCall("There is nothing to merge.") ``` """ -function objective(::Symbol, model) - throw(CTBase.ExtensionError(:JuMP)) -end +merge(::Nothing, ::NamedTuple) = throw(CTBase.UnauthorizedCall("There is nothing to merge.")) """ $(TYPEDSIGNATURES) -Retrieve the number of iterations from a JuMP model. +Merge two `NamedTuple`s, with the second one overriding keys from the first when duplicated. # Arguments -- `::Symbol`: Problem name. -- `model`: JuMP model object. +- `A::NamedTuple`: first set of key–value pairs. +- `B::NamedTuple`: second set of key–value pairs, takes precedence if keys overlap. # Returns -- Throws `ExtensionError(:JuMP)` since JuMP support must be extended. +- `::NamedTuple`: merged named tuple containing keys from both `A` and `B`. # Example ```julia-repl -julia> iterations(:problem1, model) -ERROR: ExtensionError(:JuMP) +julia> merge((a=1, b=2), (b=3, c=4)) +(a = 1, b = 3, c = 4) ``` """ -function iterations(::Symbol, model) - throw(CTBase.ExtensionError(:JuMP)) +function merge(A::NamedTuple, B::NamedTuple) + f(;kwargs...) = kwargs + return NamedTuple(f(; A..., B...)) end """ $(TYPEDSIGNATURES) -Return the fixed final time, from the metadata, associated with a given optimal control problem. +Return the parameter set associated with a given problem. # Arguments -- `problem::Symbol`: The name of the problem, used as a key in the global `metadata` dictionary. +- `problem::Symbol`: the name of the problem whose parameters are requested. # Returns -- `Float64`: The fixed final time of the specified problem. +- `::Union{Nothing,NamedTuple}`: the parameters of the problem, or `nothing` if none exist. # Example ```julia-repl -julia> final_time_data(:beam) -10.0 +julia> parameters_data(:beam) +(t0 = 0, tf = 1, ...) ``` """ -function final_time_data(problem::Symbol) - @assert metadata[problem][:final_time][1] == :fixed - return metadata[problem][:final_time][2] +function parameters_data(problem::Symbol) + return metadata(problem)[:parameters] end """ $(TYPEDSIGNATURES) -Return the number of discretisation steps, from the metadata, for a given optimal control problem. +Return the parameter set associated with a given problem, optionally merged with user-supplied parameters. # Arguments -- `problem::Symbol`: The name of the problem, used as a key in the global `metadata` dictionary. +- `problem::Symbol`: the name of the problem. +- `parameters::Union{Nothing,NamedTuple}`: user-supplied parameters to override or extend the defaults. + If `nothing`, returns the default parameters unchanged. # Returns -- `Int`: The number of discretisation steps (`N`) of the specified problem. +- `::Union{Nothing,NamedTuple}`: the merged parameters. + Throws `CTBase.UnauthorizedCall` if attempting to merge with a problem that has no parameters. # Example ```julia-repl -julia> steps_number_data(:beam) -500 +julia> parameters_data(:beam, (tf = 2,)) +(t0 = 0, tf = 2, ...) ``` """ -function steps_number_data(problem::Symbol) - return metadata[problem][:N] +function parameters_data(problem::Symbol, parameters::Union{Nothing, NamedTuple}) + try + return merge(parameters_data(problem), parameters) + catch e + if e isa CTBase.UnauthorizedCall + throw(CTBase.UnauthorizedCall("There is no parameters to merge in problem: $problem.")) + else + rethrow(e) + end + end end export JuMPBackend, OptimalControlBackend, problems export time_grid, state, costate, control, variable, iterations, objective -export metadata, final_time_data, steps_number_data +export control_components, control_dimension, state_components, state_dimension, variable_components, variable_dimension +export metadata, grid_size_data, parameters_data end diff --git a/test/Project.toml b/test/Project.toml index 2e076448..5183c77b 100644 --- a/test/Project.toml +++ b/test/Project.toml @@ -5,6 +5,7 @@ CTDirect = "790bbbee-bee9-49ee-8912-a9de031322d5" Interpolations = "a98d9a8b-a2ab-59e6-89dd-64a1c18fca59" Ipopt = "b6b21f68-93f8-5de0-b562-5493be1d77c9" JuMP = "4076af6c-e467-56ae-b986-b466b2749572" +MadNLPMumps = "3b83494e-c0a4-4895-918b-9157a7a085a1" NLPModels = "a4795742-8479-5a88-8948-cc11e1c8c1a6" NLPModelsIpopt = "f4238b75-b362-5c4c-b852-0801c9a21d71" OptimalControl = "5f98b655-cc9a-415a-b60e-744165666948" @@ -15,12 +16,14 @@ Test = "8dfed614-e22c-5e08-85e1-65c5234f0b40" [compat] Aqua = "0.8" CTBase = "0.16" -CTDirect = "0.16" +CTDirect = "0.17" Interpolations = "0.15, 0.16" Ipopt = "1" JuMP = "1" +MadNLPMumps = "0.5" NLPModels = "0.21" NLPModelsIpopt = "0.10" OptimalControl = "1" Plots = "1" +Printf = "1" Test = "1" diff --git a/test/figures/init/beam.pdf b/test/figures/init/beam.pdf index 2280a2a2..51c756cc 100644 Binary files a/test/figures/init/beam.pdf and b/test/figures/init/beam.pdf differ diff --git a/test/figures/init/chain.pdf b/test/figures/init/chain.pdf index d8981612..23db459f 100644 Binary files a/test/figures/init/chain.pdf and b/test/figures/init/chain.pdf differ diff --git a/test/figures/init/double_oscillator.pdf b/test/figures/init/double_oscillator.pdf index e862b180..daeb8c19 100644 Binary files a/test/figures/init/double_oscillator.pdf and b/test/figures/init/double_oscillator.pdf differ diff --git a/test/figures/init/ducted_fan.pdf b/test/figures/init/ducted_fan.pdf index affcf175..ee01eb91 100644 Binary files a/test/figures/init/ducted_fan.pdf and b/test/figures/init/ducted_fan.pdf differ diff --git a/test/figures/init/electric_vehicle.pdf b/test/figures/init/electric_vehicle.pdf index 376ce411..77f7fe81 100644 Binary files a/test/figures/init/electric_vehicle.pdf and b/test/figures/init/electric_vehicle.pdf differ diff --git a/test/figures/init/glider.pdf b/test/figures/init/glider.pdf index 12cfa497..3ed4210d 100644 Binary files a/test/figures/init/glider.pdf and b/test/figures/init/glider.pdf differ diff --git a/test/figures/init/insurance.pdf b/test/figures/init/insurance.pdf index 6acc78d2..c66051d6 100644 Binary files a/test/figures/init/insurance.pdf and b/test/figures/init/insurance.pdf differ diff --git a/test/figures/init/jackson.pdf b/test/figures/init/jackson.pdf index b98e0192..a83e7568 100644 Binary files a/test/figures/init/jackson.pdf and b/test/figures/init/jackson.pdf differ diff --git a/test/figures/init/robbins.pdf b/test/figures/init/robbins.pdf index 2936a89c..fb4e0dd4 100644 Binary files a/test/figures/init/robbins.pdf and b/test/figures/init/robbins.pdf differ diff --git a/test/figures/init/robot.pdf b/test/figures/init/robot.pdf index 4cc3b6c1..c5faba0e 100644 Binary files a/test/figures/init/robot.pdf and b/test/figures/init/robot.pdf differ diff --git a/test/figures/init/rocket.pdf b/test/figures/init/rocket.pdf index 7f1a025e..dea45751 100644 Binary files a/test/figures/init/rocket.pdf and b/test/figures/init/rocket.pdf differ diff --git a/test/figures/init/space_shuttle.pdf b/test/figures/init/space_shuttle.pdf index 7dfa0923..942f460b 100644 Binary files a/test/figures/init/space_shuttle.pdf and b/test/figures/init/space_shuttle.pdf differ diff --git a/test/figures/init/steering.pdf b/test/figures/init/steering.pdf index 5d798268..2306324d 100644 Binary files a/test/figures/init/steering.pdf and b/test/figures/init/steering.pdf differ diff --git a/test/figures/init/vanderpol.pdf b/test/figures/init/vanderpol.pdf index ed3e83e9..812ec21d 100644 Binary files a/test/figures/init/vanderpol.pdf and b/test/figures/init/vanderpol.pdf differ diff --git a/test/figures/solution/beam.pdf b/test/figures/solution/beam.pdf index 50bedd09..4b75e9b1 100644 Binary files a/test/figures/solution/beam.pdf and b/test/figures/solution/beam.pdf differ diff --git a/test/figures/solution/chain.pdf b/test/figures/solution/chain.pdf index b60dc74f..48d54152 100644 Binary files a/test/figures/solution/chain.pdf and b/test/figures/solution/chain.pdf differ diff --git a/test/figures/solution/double_oscillator.pdf b/test/figures/solution/double_oscillator.pdf index 1fadf310..628542dd 100644 Binary files a/test/figures/solution/double_oscillator.pdf and b/test/figures/solution/double_oscillator.pdf differ diff --git a/test/figures/solution/ducted_fan.pdf b/test/figures/solution/ducted_fan.pdf index 8b20d6dc..bdafed3d 100644 Binary files a/test/figures/solution/ducted_fan.pdf and b/test/figures/solution/ducted_fan.pdf differ diff --git a/test/figures/solution/electric_vehicle.pdf b/test/figures/solution/electric_vehicle.pdf index 75ed5771..9ad456c2 100644 Binary files a/test/figures/solution/electric_vehicle.pdf and b/test/figures/solution/electric_vehicle.pdf differ diff --git a/test/figures/solution/glider.pdf b/test/figures/solution/glider.pdf index ada9b5f6..69205b7a 100644 Binary files a/test/figures/solution/glider.pdf and b/test/figures/solution/glider.pdf differ diff --git a/test/figures/solution/insurance.pdf b/test/figures/solution/insurance.pdf index 503d82e1..eed6a523 100644 Binary files a/test/figures/solution/insurance.pdf and b/test/figures/solution/insurance.pdf differ diff --git a/test/figures/solution/jackson.pdf b/test/figures/solution/jackson.pdf index 19880023..599f7b09 100644 Binary files a/test/figures/solution/jackson.pdf and b/test/figures/solution/jackson.pdf differ diff --git a/test/figures/solution/robbins.pdf b/test/figures/solution/robbins.pdf index 7d64f510..54215222 100644 Binary files a/test/figures/solution/robbins.pdf and b/test/figures/solution/robbins.pdf differ diff --git a/test/figures/solution/robot.pdf b/test/figures/solution/robot.pdf index d88efe26..00e68d9d 100644 Binary files a/test/figures/solution/robot.pdf and b/test/figures/solution/robot.pdf differ diff --git a/test/figures/solution/rocket.pdf b/test/figures/solution/rocket.pdf index f05ba447..eb5f8948 100644 Binary files a/test/figures/solution/rocket.pdf and b/test/figures/solution/rocket.pdf differ diff --git a/test/figures/solution/space_shuttle.pdf b/test/figures/solution/space_shuttle.pdf index 453ba5e5..4e574f53 100644 Binary files a/test/figures/solution/space_shuttle.pdf and b/test/figures/solution/space_shuttle.pdf differ diff --git a/test/figures/solution/steering.pdf b/test/figures/solution/steering.pdf index dd15cf2f..c715f95a 100644 Binary files a/test/figures/solution/steering.pdf and b/test/figures/solution/steering.pdf differ diff --git a/test/figures/solution/vanderpol.pdf b/test/figures/solution/vanderpol.pdf index b28704f9..a69f922c 100644 Binary files a/test/figures/solution/vanderpol.pdf and b/test/figures/solution/vanderpol.pdf differ diff --git a/test/runtests.jl b/test/runtests.jl index aa1491bf..c753edc6 100644 --- a/test/runtests.jl +++ b/test/runtests.jl @@ -5,6 +5,7 @@ using Ipopt using JuMP using NLPModels using NLPModelsIpopt +using MadNLPMumps using OptimalControl using OptimalControlProblems using Test @@ -22,22 +23,19 @@ const MAX_ITER = 1000 const MAX_WALL_TIME = 500.0 # Collect all the problems from OptimalControlProblems -path = joinpath(dirname(@__FILE__), "..", "ext", "MetaData") -list_of_problems = [] -files = filter(x -> x[(end - 2):end] == ".jl", readdir(path)) -for file in files - problem = Symbol(file[1:(end - 3)]) - push!(list_of_problems, problem) -end +list_of_problems = OptimalControlProblems.problems() # Remove from the tests the following problems # problems_to_exclude = [ -# :quadrotor +# :bioreactor, # no need to remove here since already removed in OptimalControlProblems.jl +# :cart_pendulum, # no need to remove here since already removed in OptimalControlProblems.jl +# :dielectrophoretic_particle, # no need to remove here since already removed in OptimalControlProblems.jl +# :moonlander, # no need to remove here since already removed in OptimalControlProblems.jl # ] # list_of_problems = setdiff(list_of_problems, problems_to_exclude) # list_of_problems = [ -# :truck_trailer +# :jackson, # ] # The list of all the problems to test @@ -55,9 +53,11 @@ const VERBOSE = true # print or not details during tests :kwargs, :JuMP, # convergence tests for JuMP models :OptimalControl, # convergence tests for OptimalControl models + :OptimalControl_s, # convergence tests for OptimalControl models :init, # comparison between OptimalControl and JuMP: init - :solution, # comparison between OptimalControl and JuMP: solution + # :solution, # comparison between OptimalControl and JuMP: solution :quick, # quick comparison: objective rel error only + :parameters, # tests with different parameters values, does no depend on `list_of_problems` ) @testset "$(name)" verbose=VERBOSE begin test_name = Symbol(:test_, name) diff --git a/test/test_JuMP.jl b/test/test_JuMP.jl index 2f0ac509..dad687a0 100644 --- a/test/test_JuMP.jl +++ b/test/test_JuMP.jl @@ -2,7 +2,7 @@ function test_JuMP() for f in LIST_OF_PROBLEMS @testset "$(f)" verbose=VERBOSE begin - N = metadata[f][:N] + grid_size = metadata(f)[:grid_size] # do we keep or remove the problem from the list keep_problem = true @@ -12,7 +12,7 @@ function test_JuMP() DEBUG && println("│") # Set up the model - nlp = OptimalControlProblems.eval(f)(JuMPBackend(); N=N) + nlp = OptimalControlProblems.eval(f)(JuMPBackend(); grid_size=grid_size) set_optimizer(nlp, Ipopt.Optimizer) set_silent(nlp) set_optimizer_attribute(nlp, "tol", TOL) @@ -22,28 +22,43 @@ function test_JuMP() set_optimizer_attribute(nlp, "max_wall_time", MAX_WALL_TIME) set_optimizer_attribute(nlp, "sb", SB) + # check existence of required metadata + nlp_keys = keys(object_dictionary(nlp)) + @test :time_grid ∈ nlp_keys + @test :state_components ∈ nlp_keys + @test :costate_components ∈ nlp_keys + @test :control_components ∈ nlp_keys + @test :variable_components ∈ nlp_keys + + # check if the keys from the components names exists + components = [:state_components, :costate_components, :control_components, :variable_components] + for c ∈ components + if !(isnothing(nlp[c])) + for e ∈ nlp[c] + @test Symbol(e) ∈ nlp_keys + end + end + end + # Solve the model - DEBUG && println("├─ Solve") - DEBUG && println("│") print(" First solve: "); @time optimize!(nlp) print(" Second solve: "); @time optimize!(nlp) - DEBUG && println("│") # Infos - DEBUG && println("├─ Infos") - DEBUG && println("│") - DEBUG && println("│ termination_status: ", termination_status(nlp)) - DEBUG && println("│ objective: ", objective_value(nlp)) - DEBUG && println("│ iterations: ", barrier_iterations(nlp)) DEBUG && println("│") + DEBUG && print( + "│ termination_status: ", termination_status(nlp), + ", objective: ", objective_value(nlp), + ", iterations: ", barrier_iterations(nlp) + ) # Test res = @my_test_broken termination_status(nlp) == MOI.LOCALLY_SOLVED keep_problem = keep_problem && res - DEBUG && res && println("│ \033[1;32mTest Passed\033[0m") - DEBUG && !res && println("│ \033[1;31mTest Failed\033[0m") + DEBUG && res && println(", \033[1;32mPass\033[0m") + DEBUG && !res && println(", \033[1;31mFail\033[0m") DEBUG && println("│") DEBUG && println("└─") diff --git a/test/test_OptimalControl.jl b/test/test_OptimalControl.jl index 0596c966..c380febb 100644 --- a/test/test_OptimalControl.jl +++ b/test/test_OptimalControl.jl @@ -1,6 +1,6 @@ # test_OptimalControl_optimality function test_OptimalControl() - kwargs = Dict( + options_ipopt = Dict( :print_level => 0, :tol => TOL, :mu_strategy => MU_STRATEGY, @@ -11,7 +11,7 @@ function test_OptimalControl() for f in LIST_OF_PROBLEMS @testset "$(f)" verbose=VERBOSE begin - N = metadata[f][:N] + grid_size = metadata(f)[:grid_size] # do we keep or remove the problem from the list keep_problem = true @@ -21,31 +21,24 @@ function test_OptimalControl() DEBUG && println("│") # Set up the model - docp = OptimalControlProblems.eval(f)(OptimalControlBackend(); N=N) + docp = OptimalControlProblems.eval(f)(OptimalControlBackend(); grid_size=grid_size) nlp = nlp_model(docp) # Solve the model - DEBUG && println("├─ Solve") - DEBUG && println("│") print(" First solve: "); - @time sol = NLPModelsIpopt.ipopt(nlp; kwargs...) + @time sol = NLPModelsIpopt.ipopt(nlp; options_ipopt...) print(" Second solve: "); - @time sol = NLPModelsIpopt.ipopt(nlp; kwargs...) - DEBUG && println("│") + @time sol = NLPModelsIpopt.ipopt(nlp; options_ipopt...) # Infos - DEBUG && println("├─ Infos") DEBUG && println("│") - DEBUG && println("│ sol.status: ", sol.status) - DEBUG && println("│ objective: ", sol.objective) - DEBUG && println("│ iterations: ", sol.iter) - DEBUG && println("│") - + DEBUG && print( "│ sol.status: ", sol.status, ", objective: ", sol.objective, ", iterations: ", sol.iter) + # Test res = @my_test_broken (sol.status == :first_order || sol.status == :acceptable) keep_problem = keep_problem && res - DEBUG && res && println("│ \033[1;32mTest Passed\033[0m") - DEBUG && !res && println("│ \033[1;31mTest Failed\033[0m") + DEBUG && res && println(", \033[1;32mPass\033[0m") + DEBUG && !res && println(", \033[1;31mFail\033[0m") DEBUG && println("│") DEBUG && println("└─") diff --git a/test/test_OptimalControl_s.jl b/test/test_OptimalControl_s.jl new file mode 100644 index 00000000..088823a1 --- /dev/null +++ b/test/test_OptimalControl_s.jl @@ -0,0 +1,53 @@ +# test_OptimalControl_optimality +function test_OptimalControl_s() + options_madnlp = Dict( + :print_level => MadNLP.ERROR, + :tol => TOL, + #:mu_strategy => MU_STRATEGY, + #:sb => SB, + :max_iter => MAX_ITER, + :max_wall_time => MAX_WALL_TIME, + :linear_solver => MumpsSolver, + ) + + for f in LIST_OF_PROBLEMS + @testset "$(f)" verbose=VERBOSE begin + grid_size = metadata(f)[:grid_size] + + # do we keep or remove the problem from the list + keep_problem = true + + # + DEBUG && println("\n", "┌─ ", string(f), " (OptimalControl_s)") + DEBUG && println("│") + + # Set up the model + docp = OptimalControlProblems.eval(Symbol(f, :_s))(OptimalControlBackend(), :madnlp, :exa; grid_size=grid_size) + nlp = nlp_model(docp) + + # Solve the model + print(" First solve: "); + @time sol = madnlp(nlp; options_madnlp...) + print(" Second solve: "); + @time sol = madnlp(nlp; options_madnlp...) + + # Infos + DEBUG && println("│") + DEBUG && print( "│ sol.status: ", sol.status, ", objective: ", sol.objective, ", iterations: ", sol.iter) + + # Test + res = @my_test_broken (sol.status == MadNLP.SOLVE_SUCCEEDED) #:first_order || sol.status == :acceptable) + keep_problem = keep_problem && res + DEBUG && res && println(", \033[1;32mPass\033[0m") + DEBUG && !res && println(", \033[1;31mFail\033[0m") + DEBUG && println("│") + DEBUG && println("└─") + + # do we keep or remove the problem from the list + if !keep_problem + global LIST_OF_PROBLEMS_FINAL + LIST_OF_PROBLEMS_FINAL = setdiff(LIST_OF_PROBLEMS_FINAL, [f]) + end + end + end +end diff --git a/test/test_kwargs.jl b/test/test_kwargs.jl index 0d2249a5..f415cce2 100644 --- a/test/test_kwargs.jl +++ b/test/test_kwargs.jl @@ -1,5 +1,5 @@ function test_kwargs() - N = 2 + grid_size = 2 scheme = :euler solver_backend = :madnlp optimiser = Ipopt.Optimizer @@ -10,10 +10,18 @@ function test_kwargs() # OptimalControl model docp = OptimalControlProblems.eval(f)( - OptimalControlBackend(), solver_backend; grid_size=N, disc_method=scheme + OptimalControlBackend(), solver_backend; grid_size=grid_size, disc_method=scheme ) @test docp isa CTDirect.DOCP - @test docp.time.steps == N + @test docp.time.steps == grid_size + @test docp.discretization isa CTDirect.Euler + + # OptimalControl_s model + docp = OptimalControlProblems.eval(Symbol(f, :_s))( + OptimalControlBackend(), :madnlp, :exa; grid_size=grid_size, disc_method=scheme + ) + @test docp isa CTDirect.DOCP + @test docp.time.steps == grid_size @test docp.discretization isa CTDirect.Euler # JuMP model diff --git a/test/test_parameters.jl b/test/test_parameters.jl new file mode 100644 index 00000000..6e642527 --- /dev/null +++ b/test/test_parameters.jl @@ -0,0 +1,11 @@ +function test_parameters() + + @testset "merge (parameters)" verbose=VERBOSE begin + @test OptimalControlProblems.merge(nothing, nothing) === nothing + @test OptimalControlProblems.merge((tf = 1,), nothing ) == (tf = 1,) + @test OptimalControlProblems.merge((tf = 1,), (tf = 2,)) == (tf = 2,) + @test OptimalControlProblems.merge((tf = 1, a = 2), (tf = 2,)) == (tf = 2, a = 2) + @test_throws CTBase.UnauthorizedCall OptimalControlProblems.merge(nothing, (tf = 1,)) + end + +end \ No newline at end of file diff --git a/test/test_quick.jl b/test/test_quick.jl index d83959b0..b33417ab 100644 --- a/test/test_quick.jl +++ b/test/test_quick.jl @@ -7,7 +7,7 @@ function test_quick() ε_abs_objective = 1e-6 # options for solvers - kwargs = Dict( + kwargs_ipopt = Dict( :print_level => 0, :tol => TOL, :mu_strategy => MU_STRATEGY, @@ -16,24 +16,27 @@ function test_quick() :max_wall_time => MAX_WALL_TIME, ) + kwargs_madnlp = Dict( + :print_level => MadNLP.ERROR, + :tol => TOL, + #:mu_strategy => MU_STRATEGY, + #:sb => SB, + :max_iter => MAX_ITER, + :max_wall_time => MAX_WALL_TIME, + :linear_solver => MumpsSolver, + ) + max_r_err = -Inf # relative error max for f in LIST_OF_PROBLEMS - N = metadata[f][:N] + grid_size = metadata(f)[:grid_size] @testset "$(string(f)) (objective)" verbose=VERBOSE begin DEBUG && println("\n", "┌─ ", string(f)) DEBUG && println("│") - ########## OptimalControl ########## - docp = OptimalControlProblems.eval(f)(OptimalControlBackend(); N=N) - nlp = nlp_model(docp) - nlp_sol = NLPModelsIpopt.ipopt(nlp; kwargs...) - sol = build_ocp_solution(docp, nlp_sol) - o_oc = objective(sol) - ############### JuMP ############### - nlp = OptimalControlProblems.eval(f)(JuMPBackend(); N=N) + nlp = OptimalControlProblems.eval(f)(JuMPBackend(); grid_size=grid_size) set_optimizer(nlp, Ipopt.Optimizer) set_silent(nlp) set_optimizer_attribute(nlp, "tol", TOL) @@ -45,26 +48,68 @@ function test_quick() optimize!(nlp) o_jp = objective_value(nlp) - ############### TEST ############### - # objective - o_di = abs(o_oc-o_jp) - o_bd = max(0.5*(abs(o_oc) + abs(o_jp))*ε_rel_objective, ε_abs_objective) + ########## OptimalControl ########## + docp = OptimalControlProblems.eval(f)(OptimalControlBackend(); grid_size=grid_size) + nlp = nlp_model(docp) + nlp_sol = NLPModelsIpopt.ipopt(nlp; kwargs_ipopt...) + sol = build_ocp_solution(docp, nlp_sol) + o_oc = objective(sol) + ########## OptimalControl_s ########## + docp = OptimalControlProblems.eval(Symbol(f, :_s))(OptimalControlBackend(), :madnlp, :exa; grid_size=grid_size) + nlp = nlp_model(docp) + ocp = ocp_model(docp) + nlp_sol = madnlp(nlp; kwargs_madnlp...) + sol = build_ocp_solution(docp, nlp_sol) + o_os = criterion(ocp) == :min ? objective(sol) : -objective(sol) + + ############### TEST ############### DEBUG && println("├─ objective") DEBUG && println("│") - DEBUG && println("│ o_oc = ", o_oc) DEBUG && println("│ o_jp = ", o_jp) - DEBUG && println("│ r_err = ", o_di/(0.5*(abs(o_oc) + abs(o_jp)))) - DEBUG && println("│ a_err = ", o_di) - DEBUG && println("│ bound = ", o_bd) + DEBUG && println("│ o_oc = ", o_oc) + DEBUG && println("│ o_os = ", o_os) + DEBUG && println("│") + + # comparison JuMP and OptimalControl + A = o_oc + B = o_jp + + o_di = abs(A-B) + o_bd = max(0.5*(abs(A) + abs(B))*ε_rel_objective, ε_abs_objective) + max_r_err = max(max_r_err, o_di/(0.5*(abs(A) + abs(B)))) + + DEBUG && println("│ JuMP vs OptimalControl") + DEBUG && println("│") + DEBUG && println("│ r_err = ", o_di/(0.5*(abs(A) + abs(B)))) + DEBUG && println("│ a_err = ", o_di) + DEBUG && println("│ bound = ", o_bd) res = @my_test_broken o_di < o_bd - DEBUG && res && println("│ \033[1;32mTest Passed\033[0m") - DEBUG && !res && println("│ \033[1;31mTest Failed\033[0m") + DEBUG && res && println("│ \033[1;32mTest Passed\033[0m") + DEBUG && !res && println("│ \033[1;31mTest Failed\033[0m") + DEBUG && println("│") + + # comparison JuMP and OptimalControl_s + A = o_os + B = o_jp + + o_di = abs(A-B) + o_bd = max(0.5*(abs(A) + abs(B))*ε_rel_objective, ε_abs_objective) + max_r_err = max(max_r_err, o_di/(0.5*(abs(A) + abs(B)))) + + DEBUG && println("│ JuMP vs OptimalControl_s") DEBUG && println("│") + DEBUG && println("│ r_err = ", o_di/(0.5*(abs(A) + abs(B)))) + DEBUG && println("│ a_err = ", o_di) + DEBUG && println("│ bound = ", o_bd) + + res = @my_test_broken o_di < o_bd - max_r_err = max(max_r_err, o_di/(0.5*(abs(o_oc) + abs(o_jp)))) + DEBUG && res && println("│ \033[1;32mTest Passed\033[0m") + DEBUG && !res && println("│ \033[1;31mTest Failed\033[0m") + DEBUG && println("│") # DEBUG && println("└─") diff --git a/test/utils.jl b/test/utils.jl index 7dd4e43e..9c559de1 100644 --- a/test/utils.jl +++ b/test/utils.jl @@ -79,13 +79,15 @@ julia> comparison(max_iter=100, test_name=:solution) """ function comparison(; max_iter, test_name) + test_tag(res) = res ? "\033[1;32mPASS\033[0m" : "\033[1;31mFAIL\033[0m" + # available_test_names = [:init, :solution, :iter1] test_name ∈ available_test_names || error("test_name must belong to ", available_test_names) # comparison Parameters: tolerances - ε_rel_grid = 1e-6 + ε_rel_grid = 1e-2 ε_abs_grid = 1e-6 ε_rel_objective = 1e-4 @@ -97,8 +99,11 @@ function comparison(; max_iter, test_name) ε_rel_control = 1e-1 ε_abs_control = 1e-6 - # options for solvers - Options = Dict( + ε_rel_variable = 1e-1 + ε_abs_variable = 1e-6 + + # options_ipopt for solvers + options_ipopt = Dict( :print_level => 0, :tol => TOL, :mu_strategy => MU_STRATEGY, @@ -107,110 +112,254 @@ function comparison(; max_iter, test_name) :max_wall_time => MAX_WALL_TIME, ) + options_madnlp = Dict( + :print_level => MadNLP.ERROR, + :tol => TOL, + #:mu_strategy => MU_STRATEGY, + #:sb => SB, + :max_iter => max_iter, + :max_wall_time => MAX_WALL_TIME, + :linear_solver => MumpsSolver, + ) + + # + function test_L2_i(i, times, A, B, A_name, B_name; ε_abs, ε_rel) + yi_oc, yi_jp = [B[k][i] for k in eachindex(times)], [A[k][i] for k in eachindex(times)] + L2_di = L2_norm(times, yi_oc - yi_jp) + L2_bd = max(0.5*(L2_norm(times, yi_oc)+L2_norm(times, yi_jp))*ε_rel, ε_abs) + res = @my_test_broken L2_di < L2_bd + r_err = L2_di / (0.5*(L2_norm(times, yi_oc)+L2_norm(times, yi_jp))) + DEBUG && @printf("│ → %s vs %s: r_err=%.3e a_err=%.3e bound=%.3e %s\n", A_name, B_name, r_err, L2_di, L2_bd, test_tag(res)) + return res + end + + function test_L2_i(i, times, A, B, A_name, B_name, keep_problem; ε_abs, ε_rel) + res = test_L2_i(i, times, A, B, A_name, B_name; ε_abs=ε_abs, ε_rel=ε_rel) + return keep_problem && res + end + + # + function test_abs(A, B, A_name, B_name; ε_abs, ε_rel) + tf_di = abs(A - B) + tf_bd = max(0.5*(A+B)*ε_rel, ε_abs) + res = @my_test_broken tf_di < tf_bd + r_err = tf_di / (0.5*(A+B)) + DEBUG && @printf("│ → %s: %.3e %s: %.3e r_err=%.3e a_err=%.3e bound=%.3e %s\n", A_name, A, B_name, B, r_err, tf_di, tf_bd, test_tag(res)) + return res + end + + function test_abs(A, B, A_name, B_name, keep_problem; ε_abs, ε_rel) + res = test_abs(A, B, A_name, B_name; ε_abs=ε_abs, ε_rel=ε_rel) + return res && keep_problem + end + + function test_abs(A, B, A_name, B_name, keep_problem, test_grid_ok; ε_abs, ε_rel) + res = test_abs(A, B, A_name, B_name; ε_abs=ε_abs, ε_rel=ε_rel) + return res && keep_problem, res && test_grid_ok + end + + function test_abs_i(i, A, B, A_name, B_name; ε_abs, ε_rel) + return test_abs(A[i], B[i], A_name, B_name; ε_abs, ε_rel) + end + + function test_abs_i(i, A, B, A_name, B_name, keep_problem; ε_abs, ε_rel) + return test_abs(A[i], B[i], A_name, B_name, keep_problem; ε_abs, ε_rel) + end + + # + function test_int(A, B, A_name, B_name) + res = @my_test_broken A == B + DEBUG && @printf("│ → %s: %d %s: %d %s\n", A_name, A, B_name, B, test_tag(res)) + return res + end + + function test_int(A, B, A_name, B_name, keep_problem) + res = test_int(A, B, A_name, B_name) + return res && keep_problem + end + + function test_int(A, B, A_name, B_name, keep_problem, test_grid_ok) + res = test_int(A, B, A_name, B_name) + return res && keep_problem, res && test_grid_ok + end + + # + function test_components(A, B, A_name, B_name) + res = @my_test_broken A == B + DEBUG && @printf("│ → %s vs %s %s -- ", A_name, B_name, test_tag(res)) + DEBUG && println(A, " vs ", B) + return res + end + + function test_components(A, B, A_name, B_name, keep_problem) + res = test_components(A, B, A_name, B_name) + return res && keep_problem + end + + function test_components(A, B, A_name, B_name, keep_problem, test_grid_ok) + res = test_components(A, B, A_name, B_name) + return res && keep_problem, res && test_grid_ok + end + + # + function test_length(A, B, A_name, B_name) + return test_int(length(A), length(B), A_name, B_name) + end + + function test_length(A, B, A_name, B_name, keep_problem, test_grid_ok) + res = test_length(A, B, A_name, B_name) + return res && keep_problem, res && test_grid_ok + end + + # + function test_grid_max_error(A, B, A_name, B_name, keep_problem, test_grid_ok) + ti_di_max, ti_bd_max, itera_max = 0, NaN, 0 + for i in eachindex(B) + ti_di = B[i] - A[i] + ti_bd = max(0.5*(abs(B[i])+abs(A[i]))*ε_rel_grid, ε_abs_grid) + res = @my_test_broken abs(ti_di) < ti_bd + keep_problem = keep_problem && res + test_grid_ok = test_grid_ok && res + if abs(ti_di) ≥ abs(ti_di_max) + ti_di_max, ti_bd_max, itera_max = ti_di, ti_bd, i + end + end + r_err = abs(ti_di_max)/(0.5*(abs(B[itera_max])+abs(A[itera_max]))) + res = r_err<1.0 + DEBUG && @printf("│ → %s vs %s: iter=%d r_err=%.3e a_err=%.3e bound=%.3e %s\n", A_name, B_name, itera_max, r_err, abs(ti_di_max), ti_bd_max, test_tag(res)) + return keep_problem && res, test_grid_ok && res + end + # we loop over the problems for f in LIST_OF_PROBLEMS - N = metadata[f][:N] # get default N - x_vars = metadata[f][:state_name] - p_vars = metadata[f][:costate_name] - u_vars = metadata[f][:control_name] - v_vars = metadata[f][:variable_name] + + grid_size = metadata(f)[:grid_size] # get default number of steps @testset "$(string(f)) ($(string(test_name)))" verbose=VERBOSE begin DEBUG && println("\n┌─ ", string(f), " (", string(test_name), ")") DEBUG && println("│") - ########## OptimalControl ########## - docp = OptimalControlProblems.eval(f)(OptimalControlBackend(); N=N) - nlp_oc = nlp_model(docp) - nlp_sol = NLPModelsIpopt.ipopt(nlp_oc; Options...) - sol = build_ocp_solution(docp, nlp_sol) - sol_oc = deepcopy(sol) # for plotting - - t_oc = time_grid(sol) - x_oc = state(sol).(t_oc) - u_oc = control(sol).(t_oc) - o_oc = objective(sol) - i_oc = iterations(sol) - v_oc = variable(sol) - ############### JuMP ############### - nlp_jp = OptimalControlProblems.eval(f)(JuMPBackend(); N=N) + nlp_jp = OptimalControlProblems.eval(f)(JuMPBackend(); grid_size=grid_size) set_optimizer(nlp_jp, Ipopt.Optimizer) set_silent(nlp_jp) - set_optimizer_attribute(nlp_jp, "tol", Options[:tol]) - set_optimizer_attribute(nlp_jp, "max_iter", Options[:max_iter]) - set_optimizer_attribute(nlp_jp, "mu_strategy", Options[:mu_strategy]) + set_optimizer_attribute(nlp_jp, "tol", options_ipopt[:tol]) + set_optimizer_attribute(nlp_jp, "max_iter", options_ipopt[:max_iter]) + set_optimizer_attribute(nlp_jp, "mu_strategy", options_ipopt[:mu_strategy]) set_optimizer_attribute(nlp_jp, "linear_solver", "mumps") - set_optimizer_attribute(nlp_jp, "max_wall_time", Options[:max_wall_time]) - set_optimizer_attribute(nlp_jp, "sb", Options[:sb]) + set_optimizer_attribute(nlp_jp, "max_wall_time", options_ipopt[:max_wall_time]) + set_optimizer_attribute(nlp_jp, "sb", options_ipopt[:sb]) optimize!(nlp_jp) - t_jp = time_grid(f, nlp_jp) - x_jp = state(f, nlp_jp).(t_jp) - u_jp = control(f, nlp_jp).(t_jp) - o_jp = objective(f, nlp_jp) - i_jp = iterations(f, nlp_jp) - v_jp = variable(f, nlp_jp) - p_jp = costate(f, nlp_jp).(t_jp) + t_jp = time_grid(nlp_jp) + x_jp = state(nlp_jp).(t_jp) + u_jp = control(nlp_jp).(t_jp) + o_jp = objective(nlp_jp) + i_jp = iterations(nlp_jp) + v_jp = variable(nlp_jp) + p_jp = costate(nlp_jp).(t_jp) + nb_var_jp = num_variables(nlp_jp) + nb_con_jp = num_constraints(nlp_jp; count_variable_in_set_constraints=false) + x_vars_jp = state_components(nlp_jp) + u_vars_jp = control_components(nlp_jp) + v_vars_jp = variable_components(nlp_jp) + # + x_vars = x_vars_jp + u_vars = u_vars_jp + v_vars = v_vars_jp + + ########## OptimalControl ########## + docp = OptimalControlProblems.eval(f)(OptimalControlBackend(); grid_size=grid_size) + nlp_oc = nlp_model(docp) + ocp_oc = ocp_model(docp) + nlp_sol = NLPModelsIpopt.ipopt(nlp_oc; options_ipopt...) + sol_oc = build_ocp_solution(docp, nlp_sol) + + t_oc = time_grid(sol_oc) + x_oc = state(sol_oc).(t_oc) + u_oc = control(sol_oc).(t_oc) + o_oc = objective(sol_oc) + i_oc = iterations(sol_oc) + v_oc = variable(sol_oc) + nb_var_oc = get_nvar(nlp_oc) + nb_con_oc = get_ncon(nlp_oc) + x_vars_oc = state_components(ocp_oc) + u_vars_oc = control_components(ocp_oc) + v_vars_oc = variable_components(ocp_oc) + + ########## OptimalControl_s ########## + model_backend = :exa # :adnlp + docp = OptimalControlProblems.eval(Symbol(f, :_s))(OptimalControlBackend(), :madnlp, model_backend; grid_size=grid_size) + nlp_os = nlp_model(docp) + ocp_os = ocp_model(docp) + nlp_sol = madnlp(nlp_os; options_madnlp...) + sol_os = build_ocp_solution(docp, nlp_sol) + + t_os = time_grid(sol_os) + x_os = state(sol_os).(t_os) + u_os = control(sol_os).(t_os) + o_os = criterion(ocp_os) == :min ? objective(sol_os) : -objective(sol_os) + i_os = iterations(sol_os) + v_os = variable(sol_os) + nb_var_os = get_nvar(nlp_os) + nb_con_os = get_ncon(nlp_os) + x_vars_os = state_components(ocp_os) + u_vars_os = control_components(ocp_os) + v_vars_os = variable_components(ocp_os) ########## Iterations ########## - DEBUG && println("├─ Iterations → OC: ", i_oc, ", JP: ", i_jp) + DEBUG && @printf("├─ Iterations\n") + DEBUG && @printf("│ → JP: %d OC: %d OS: %d\n", i_jp, i_oc, i_os) + # keep_problem = true + ########## Components Names ########## + if test_name == :init + @testset "nlp" verbose=VERBOSE begin + DEBUG && @printf("├─ Components names\n") + keep_problem = test_components(x_vars_jp, x_vars_oc, "state : JP", "OC", keep_problem) + keep_problem = test_components(x_vars_jp, x_vars_os, "state : JP", "OS", keep_problem) + keep_problem = test_components(u_vars_jp, u_vars_oc, "control : JP", "OC", keep_problem) + keep_problem = test_components(u_vars_jp, u_vars_os, "control : JP", "OS", keep_problem) + keep_problem = test_components(v_vars_jp, v_vars_oc, "variable: JP", "OC", keep_problem) + keep_problem = test_components(v_vars_jp, v_vars_os, "variable: JP", "OS", keep_problem) + end + end + ########## Variables / Constraints ########## @testset "nlp" verbose=VERBOSE begin - nb_var_oc, nb_var_jp = get_nvar(nlp_oc), num_variables(nlp_jp) - res = @my_test_broken nb_var_oc == nb_var_jp - keep_problem = keep_problem && res - DEBUG && @printf("├─ Variables → OC: %d JP: %d %s\n", nb_var_oc, nb_var_jp, - res ? "\033[1;32mPASS\033[0m" : "\033[1;31mFAIL\033[0m") - - nb_con_oc = get_ncon(nlp_oc) - nb_con_jp = num_constraints(nlp_jp; count_variable_in_set_constraints=false) - res = @my_test_broken nb_con_oc == nb_con_jp - keep_problem = keep_problem && res - DEBUG && @printf("├─ Constraints → OC: %d JP: %d %s\n", nb_con_oc, nb_con_jp, - res ? "\033[1;32mPASS\033[0m" : "\033[1;31mFAIL\033[0m") + DEBUG && @printf("├─ Variables\n") + keep_problem = test_int(nb_var_jp, nb_var_oc, "JP", "OC", keep_problem) + keep_problem = test_int(nb_var_jp, nb_var_os, "JP", "OS", keep_problem) + + DEBUG && @printf("├─ Constraints\n") + keep_problem = test_int(nb_con_jp, nb_con_oc, "JP", "OC", keep_problem) + keep_problem = test_int(nb_con_jp, nb_con_os, "JP", "OS", keep_problem) end ########## Time Grid ########## test_grid_ok = true @testset "grid" verbose=VERBOSE begin + + # ---------------------------- # final time - tf_di = abs(t_oc[end] - t_jp[end]) - tf_bd = max(0.5*(t_oc[end]+t_jp[end])*ε_rel_grid, ε_abs_grid) - res = @my_test_broken tf_di < tf_bd - r_err = tf_di / (0.5*(t_oc[end]+t_jp[end])) - DEBUG && @printf("├─ Final time → OC: %.3e JP: %.3e\n", t_oc[end], t_jp[end]) - DEBUG && @printf("│ r_err=%.3e a_err=%.3e bound=%.3e %s\n", - r_err, tf_di, tf_bd, - res ? "\033[1;32mPASS\033[0m" : "\033[1;31mFAIL\033[0m") + DEBUG && @printf("├─ Final time\n") + keep_problem, test_grid_ok = test_abs(t_jp[end], t_oc[end], "JP", "OC", keep_problem, test_grid_ok; ε_abs=ε_abs_grid, ε_rel=ε_rel_grid) + keep_problem, test_grid_ok = test_abs(t_jp[end], t_os[end], "JP", "OS", keep_problem, test_grid_ok; ε_abs=ε_abs_grid, ε_rel=ε_rel_grid) + # ---------------------------- # length of the grids - res = @my_test_broken length(t_oc) == length(t_jp) - keep_problem = keep_problem && res - test_grid_ok = test_grid_ok && res - DEBUG && @printf("├─ Grid length → OC: %d JP: %d %s\n", length(t_oc), length(t_jp), - res ? "\033[1;32mPASS\033[0m" : "\033[1;31mFAIL\033[0m") + DEBUG && @printf("├─ Grid length\n") + keep_problem, test_grid_ok = test_length(t_jp, t_oc, "JP", "OC", keep_problem, test_grid_ok) + keep_problem, test_grid_ok = test_length(t_jp, t_os, "JP", "OS", keep_problem, test_grid_ok) + # ---------------------------- # max error if test_grid_ok - ti_di_max, ti_bd_max, itera_max = 0, NaN, 0 - for i in eachindex(t_oc) - ti_di = t_oc[i] - t_jp[i] - ti_bd = max(0.5*(abs(t_oc[i])+abs(t_jp[i]))*ε_rel_grid, ε_abs_grid) - res = @my_test_broken abs(ti_di) < ti_bd - keep_problem = keep_problem && res - test_grid_ok = test_grid_ok && res - if abs(ti_di) ≥ abs(ti_di_max) - ti_di_max, ti_bd_max, itera_max = ti_di, ti_bd, i - end - end - r_err = abs(ti_di_max)/(0.5*(abs(t_oc[itera_max])+abs(t_jp[itera_max]))) - DEBUG && @printf("├─ Grid max error → iter=%d r_err=%.3e a_err=%.3e bound=%.3e %s\n", - itera_max, r_err, abs(ti_di_max), ti_bd_max, - r_err<1.0 ? "\033[1;32mPASS\033[0m" : "\033[1;31mFAIL\033[0m") + DEBUG && @printf("├─ Grid max error\n") + keep_problem, test_grid_ok = test_grid_max_error(t_jp, t_oc, "JP", "OC", keep_problem, test_grid_ok) + keep_problem, test_grid_ok = test_grid_max_error(t_jp, t_os, "JP", "OS", keep_problem, test_grid_ok) end end @@ -219,16 +368,10 @@ function comparison(; max_iter, test_name) @testset "state" verbose=VERBOSE begin DEBUG && println("├─ States") for i in eachindex(x_vars) + DEBUG && @printf("│ %-6s\n", x_vars[i]) @testset "$(x_vars[i])" verbose=VERBOSE begin - xi_oc, xi_jp = [x_oc[k][i] for k in eachindex(t_oc)], [x_jp[k][i] for k in eachindex(t_jp)] - L2_di = L2_norm(t_oc, xi_oc - xi_jp) - L2_bd = max(0.5*(L2_norm(t_oc, xi_oc)+L2_norm(t_oc, xi_jp))*ε_rel_state, ε_abs_state) - res = @my_test_broken L2_di < L2_bd - keep_problem = keep_problem && res - r_err = L2_di / (0.5*(L2_norm(t_oc, xi_oc)+L2_norm(t_oc, xi_jp))) - DEBUG && @printf("│ %-6s r_err=%.3e a_err=%.3e bound=%.3e %s\n", - x_vars[i], r_err, L2_di, L2_bd, - res ? "\033[1;32mPASS\033[0m" : "\033[1;31mFAIL\033[0m") + keep_problem = test_L2_i(i, t_jp, x_jp, x_oc, "JP", "OC", keep_problem; ε_abs=ε_abs_state, ε_rel=ε_rel_state) + keep_problem = test_L2_i(i, t_jp, x_jp, x_os, "JP", "OS", keep_problem; ε_abs=ε_abs_state, ε_rel=ε_rel_state) end end end @@ -239,16 +382,12 @@ function comparison(; max_iter, test_name) @testset "control" verbose=VERBOSE begin DEBUG && println("├─ Controls") for i in eachindex(u_vars) + DEBUG && @printf("│ %-6s\n", u_vars[i]) @testset "$(u_vars[i])" verbose=VERBOSE begin - ui_oc, ui_jp = [u_oc[k][i] for k in eachindex(t_oc)], [u_jp[k][i] for k in eachindex(t_jp)] - L2_di = L2_norm(t_oc, ui_oc - ui_jp) - L2_bd = max(0.5*(L2_norm(t_oc, ui_oc)+L2_norm(t_oc, ui_jp))*ε_rel_control, ε_abs_control) - res = @my_test_broken L2_di < L2_bd - keep_problem = keep_problem && res - r_err = L2_di / (0.5*(L2_norm(t_oc, ui_oc)+L2_norm(t_oc, ui_jp))) - DEBUG && @printf("│ %-6s r_err=%.3e a_err=%.3e bound=%.3e %s\n", - u_vars[i], r_err, L2_di, L2_bd, - res ? "\033[1;32mPASS\033[0m" : "\033[1;31mFAIL\033[0m") + if !(test_name == :solution && f == :jackson) + keep_problem = test_L2_i(i, t_jp, u_jp, u_oc, "JP", "OC", keep_problem; ε_abs=ε_abs_control, ε_rel=ε_rel_control) + keep_problem = test_L2_i(i, t_jp, u_jp, u_os, "JP", "OS", keep_problem; ε_abs=ε_abs_control, ε_rel=ε_rel_control) + end end end end @@ -259,32 +398,20 @@ function comparison(; max_iter, test_name) @testset "variable" verbose=VERBOSE begin DEBUG && println("├─ Variables") for i in eachindex(v_vars) + DEBUG && @printf("│ %-6s\n", v_vars[i]) @testset "$(v_vars[i])" verbose=VERBOSE begin - vi_oc, vi_jp = v_oc[i], v_jp[i] - vi_di = abs(vi_oc-vi_jp) - vi_bd = max(0.5*(abs(vi_oc)+abs(vi_jp))*ε_rel_control, ε_abs_control) - res = @my_test_broken vi_di < vi_bd - keep_problem = keep_problem && res - r_err = vi_di / (0.5*(abs(vi_oc)+abs(vi_jp))) - DEBUG && @printf("│ %-6s r_err=%.3e a_err=%.3e bound=%.3e %s\n", - v_vars[i], r_err, vi_di, vi_bd, - res ? "\033[1;32mPASS\033[0m" : "\033[1;31mFAIL\033[0m") + keep_problem = test_abs_i(i, v_jp, v_oc, "JP", "OC", keep_problem; ε_abs=ε_abs_variable, ε_rel=ε_rel_variable) + keep_problem = test_abs_i(i, v_jp, v_os, "JP", "OS", keep_problem; ε_abs=ε_abs_variable, ε_rel=ε_rel_variable) end end end end ########## Objective ########## + DEBUG && println("├─ Objective") @testset "objective" verbose=VERBOSE begin - o_di = abs(o_oc-o_jp) - o_bd = max(0.5*(abs(o_oc)+abs(o_jp))*ε_rel_objective, ε_abs_objective) - res = @my_test_broken o_di < o_bd - keep_problem = keep_problem && res - r_err = o_di / (0.5*(abs(o_oc)+abs(o_jp))) - DEBUG && println("├─ Objective") - DEBUG && @printf("│ r_err=%.3e a_err=%.3e bound=%.3e %s\n", - r_err, o_di, o_bd, - res ? "\033[1;32mPASS\033[0m" : "\033[1;31mFAIL\033[0m") + keep_problem = test_abs(o_jp, o_oc, "JP", "OC", keep_problem; ε_abs=ε_abs_objective, ε_rel=ε_rel_objective) + keep_problem = test_abs(o_jp, o_os, "JP", "OS", keep_problem; ε_abs=ε_abs_objective, ε_rel=ε_rel_objective) end DEBUG && println("└─") @@ -300,9 +427,9 @@ function comparison(; max_iter, test_name) n = length(x_vars) m = length(u_vars) - @assert(length(p_vars)==n) # OptimalControl + color = 1 labelOC = if (test_name == :solution) "OptimalControl: " * string(i_oc) * " it" else @@ -310,11 +437,11 @@ function comparison(; max_iter, test_name) end plt = plot( sol_oc; - state_style=(color=1,), - costate_style=(color=1, legend=:none), - control_style=(color=1, legend=:none), - path_style=(color=1, legend=:none), - dual_style=(color=1, legend=:none), + state_style=(color=color,), + costate_style=(color=color, legend=:none), + control_style=(color=color, legend=:none), + path_style=(color=color, legend=:none), + dual_style=(color=color, legend=:none), size=(900, 220*(n+m)), label=labelOC, leftmargin=20mm, @@ -323,22 +450,45 @@ function comparison(; max_iter, test_name) plot!(plt[i]; legend=:none) end + # OptimalControl_s + color = 2 + labelOC = if (test_name == :solution) + "OptimalControl_s: " * string(i_oc) * " it" + else + "OptimalControl_s" + end + plot!( + plt, + sol_os; + linestyle=:dot, + state_style=(color=color,), + costate_style=(color=color, legend=:none), + control_style=(color=color, legend=:none), + path_style=(color=color, legend=:none), + dual_style=(color=color, legend=:none), + label=labelOC, + ) + for i in 2:n + plot!(plt[i]; legend=:none) + end + # JuMP + color = 3 labelJP = (test_name == :solution) ? "JuMP: " * string(i_jp) * " it" : "JuMP" for i in eachindex(x_vars) # state xi_jp = [x_jp[k][i] for k in eachindex(t_jp)] label = i == 1 ? labelJP : :none - plot!(plt[i], t_jp, xi_jp; color=2, linestyle=:dash, label=label) + plot!(plt[i], t_jp, xi_jp; color=color, linestyle=:dash, label=label) end - for i in eachindex(p_vars) # costate + for i in eachindex(x_vars) # costate pi_jp = [p_jp[k][i] for k in eachindex(t_jp)] - plot!(plt[n + i], t_jp, -pi_jp; color=2, linestyle=:dash, label=:none) + plot!(plt[n + i], t_jp, -pi_jp; color=color, linestyle=:dash, label=:none) end for i in eachindex(u_vars) # control ui_jp = [u_jp[k][i] for k in eachindex(t_jp)] - plot!(plt[2n + i], t_jp, ui_jp; color=2, linestyle=:dash, label=:none) + plot!(plt[2n + i], t_jp, ui_jp; color=color, linestyle=:dash, label=:none) end # save figure