From f80742ca74b60c30996bfb725c2da014315a6d0b Mon Sep 17 00:00:00 2001 From: Olivier Cots Date: Mon, 29 Jun 2026 00:01:20 +0200 Subject: [PATCH] docs: migrate to DocumenterVitepress + audit all docstrings - docs/Project.toml: add DocumenterVitepress, LiveServer - docs/make.jl: switch to MarkdownVitepress format, new nav, bases.txt guard - docs/api_reference.jl: refactor to CTBase pattern (modules_config loop, unified Internals page, subdirectory="api") - .github/workflows/Documentation.yml: restrict to stable version tags - docs/src/.vitepress/config.mts: inject nav array + CT remote CSS/JS assets - docs/src/getting-started.md: new guide (install, mental model, walkthrough) - docs/src/index.md: fix module overview table - docs/src/model/index.md: fix layer table + add text fence language - docs/src/model/components.md: fix broken OCP/Validation/ reference - docs/src/model/types_and_traits.md: fix broken philosophy link - docs/src/serialization/index.md: rename to Extensions, add text fence Docstring audit (qualify all julia-repl examples, standardize sections): - Building/state.jl, control.jl, variable.jl, times.jl, objective.jl - Building/dynamics.jl: replace non-standard sections, clean up examples - Building/time_dependence.jl: fix backslash escape, standardize sections - Building/constraints.jl, build.jl: fix wrong API signatures in examples - Building/name_validation.jl: qualify private function examples - Solutions/build_solution.jl, solution_types.jl: qualify time_grid examples - ext/CTModelsJLD.jl, CTModelsJSON.jl: qualify export/import examples --- .github/workflows/Documentation.yml | 3 +- docs/.gitignore | 4 + docs/Project.toml | 4 + docs/api_reference.jl | 397 +++++++----------- docs/make.jl | 94 ++--- docs/package.json | 20 + docs/src/.vitepress/config.mts | 101 +++++ docs/src/.vitepress/julia-repl-transformer.ts | 54 +++ docs/src/.vitepress/mathjax-plugin.ts | 142 +++++++ docs/src/.vitepress/theme/docstrings.css | 51 +++ docs/src/.vitepress/theme/index.ts | 43 ++ docs/src/.vitepress/theme/style.css | 323 ++++++++++++++ docs/src/components/AuthorBadge.vue | 139 ++++++ docs/src/components/Authors.vue | 28 ++ docs/src/components/SidebarDrawerToggle.vue | 110 +++++ docs/src/components/VersionPicker.vue | 125 ++++++ docs/src/getting-started.md | 150 +++++++ docs/src/index.md | 5 +- docs/src/model/components.md | 4 +- docs/src/model/index.md | 13 +- docs/src/model/types_and_traits.md | 4 +- docs/src/serialization/index.md | 4 +- ext/CTModelsJLD.jl | 10 +- ext/CTModelsJSON.jl | 10 +- src/Building/build.jl | 47 ++- src/Building/constraints.jl | 16 +- src/Building/control.jl | 28 +- src/Building/dynamics.jl | 130 ++---- src/Building/name_validation.jl | 27 +- src/Building/objective.jl | 15 +- src/Building/state.jl | 16 +- src/Building/time_dependence.jl | 23 +- src/Building/times.jl | 14 +- src/Building/variable.jl | 5 +- src/Solutions/build_solution.jl | 24 +- src/Solutions/solution_types.jl | 33 +- 36 files changed, 1671 insertions(+), 545 deletions(-) create mode 100644 docs/.gitignore create mode 100644 docs/package.json create mode 100644 docs/src/.vitepress/config.mts create mode 100644 docs/src/.vitepress/julia-repl-transformer.ts create mode 100644 docs/src/.vitepress/mathjax-plugin.ts create mode 100644 docs/src/.vitepress/theme/docstrings.css create mode 100644 docs/src/.vitepress/theme/index.ts create mode 100644 docs/src/.vitepress/theme/style.css create mode 100644 docs/src/components/AuthorBadge.vue create mode 100644 docs/src/components/Authors.vue create mode 100644 docs/src/components/SidebarDrawerToggle.vue create mode 100644 docs/src/components/VersionPicker.vue create mode 100644 docs/src/getting-started.md diff --git a/.github/workflows/Documentation.yml b/.github/workflows/Documentation.yml index df496e29..3f3532be 100644 --- a/.github/workflows/Documentation.yml +++ b/.github/workflows/Documentation.yml @@ -4,7 +4,8 @@ on: push: branches: - main - tags: '*' + tags: + - 'v[0-9]+\.[0-9]+\.[0-9]+' # v0.5.0 yes, v0.5.0-beta no pull_request: types: [labeled, opened, synchronize, reopened] diff --git a/docs/.gitignore b/docs/.gitignore new file mode 100644 index 00000000..0587d740 --- /dev/null +++ b/docs/.gitignore @@ -0,0 +1,4 @@ +build/ +node_modules/ +package-lock.json +Manifest.toml \ No newline at end of file diff --git a/docs/Project.toml b/docs/Project.toml index ef28ea6a..4810bed4 100644 --- a/docs/Project.toml +++ b/docs/Project.toml @@ -1,16 +1,20 @@ [deps] CTBase = "54762871-cc72-4466-b8e8-f6c8b58076cd" Documenter = "e30172f5-a6a5-5a46-863b-614d45cd2de4" +DocumenterVitepress = "4710194d-e776-4893-9690-8d956a29c365" JLD2 = "033835bb-8acc-5ee8-8aae-3f567f8a3819" JSON3 = "0f8b85d8-7281-11e9-16c2-39a750bddbf1" +LiveServer = "16fef848-5104-11e9-1b77-fb7a48bbb589" MarkdownAST = "d0879d2d-cac2-40c8-9cee-1863dc0c7391" Plots = "91a5bcdd-55d7-5caf-9e0b-520d859cae80" [compat] CTBase = "0.26" Documenter = "1" +DocumenterVitepress = "0.3" JLD2 = "0.6" JSON3 = "1" +LiveServer = "1" MarkdownAST = "0.1" Plots = "1" julia = "1.10" diff --git a/docs/api_reference.jl b/docs/api_reference.jl index 0645ea44..62c877ea 100644 --- a/docs/api_reference.jl +++ b/docs/api_reference.jl @@ -1,10 +1,9 @@ # ============================================================================== -# CTModels API Reference Generator -# ============================================================================== -# -# This module provides functions to generate API reference documentation -# for CTModels.jl, following the pattern established in CTBase.jl. +# CTModels API Reference Manager # +# One CTBase.automatic_reference_documentation call per documented page. +# Keep the file lists in sync with src// and ext/ when files +# are added, removed, or renamed. # ============================================================================== """ @@ -14,11 +13,9 @@ Generate the API reference documentation for CTModels. Returns the list of pages. """ function generate_api_reference(src_dir::String, ext_dir::String) - # Helper to build absolute paths src(files...) = [abspath(joinpath(src_dir, f)) for f in files] ext(files...) = [abspath(joinpath(ext_dir, f)) for f in files] - # Symbols to exclude from documentation EXCLUDE_SYMBOLS = Symbol[ :include, :eval, @@ -28,246 +25,156 @@ function generate_api_reference(src_dir::String, ext_dir::String) :is_empty, :time_ns, ] - - CTModelsPlots = Base.get_extension(CTModels, :CTModelsPlots) - CTModelsJSON = Base.get_extension(CTModels, :CTModelsJSON) - CTModelsJLD = Base.get_extension(CTModels, :CTModelsJLD) - - pages = [ - # ─────────────────────────────────────────────────────────────────── - # Components — foundational types and basic accessors - # ─────────────────────────────────────────────────────────────────── - CTBase.automatic_reference_documentation(; - subdirectory=".", - primary_modules=[ - CTModels.Components => src( - joinpath("Components", "types.jl"), - joinpath("Components", "aliases.jl"), - joinpath("Components", "accessors.jl"), - joinpath("Components", "times_accessors.jl"), - joinpath("Components", "objective_accessors.jl"), - joinpath("Components", "constraints_accessors.jl"), - ), - ], - external_modules_to_document=[CTModels], - exclude=EXCLUDE_SYMBOLS, - public=true, - private=true, + EXCLUDE_INTERNALS = vcat( + EXCLUDE_SYMBOLS, + Symbol[:DOCTYPE_ABSTRACT_TYPE, :DOCTYPE_CONSTANT, :DOCTYPE_FUNCTION, + :DOCTYPE_MACRO, :DOCTYPE_MODULE, :DOCTYPE_STRUCT], + ) + + # ── Shared config: one entry per submodule ──────────────────────────────── + modules_config = [ + ( + mod=CTModels.Components, title="Components", - title_in_menu="Components", - filename="api_ocp_types", + filename="components", + files=src( + joinpath("Components", "types.jl"), + joinpath("Components", "aliases.jl"), + joinpath("Components", "accessors.jl"), + joinpath("Components", "times_accessors.jl"), + joinpath("Components", "objective_accessors.jl"), + joinpath("Components", "constraints_accessors.jl"), + ), ), - # ─────────────────────────────────────────────────────────────────── - # Models — AbstractModel, Model and its accessors - # ─────────────────────────────────────────────────────────────────── - CTBase.automatic_reference_documentation(; - subdirectory=".", - primary_modules=[ - CTModels.Models => src( - joinpath("Models", "constraint_functors.jl"), - joinpath("Models", "model.jl"), - ), - ], - external_modules_to_document=[CTModels], - exclude=EXCLUDE_SYMBOLS, - public=true, - private=true, + ( + mod=CTModels.Models, title="Models", - title_in_menu="Models", - filename="api_ocp_components", + filename="models", + files=src( + joinpath("Models", "constraint_functors.jl"), + joinpath("Models", "model.jl"), + ), ), - # ─────────────────────────────────────────────────────────────────── - # Building — PreModel, mutators, build - # ─────────────────────────────────────────────────────────────────── - CTBase.automatic_reference_documentation(; - subdirectory=".", - primary_modules=[ - CTModels.Building => src( - joinpath("Building", "constraint_composers.jl"), - joinpath("Building", "pre_model.jl"), - joinpath("Building", "state.jl"), - joinpath("Building", "control.jl"), - joinpath("Building", "variable.jl"), - joinpath("Building", "times.jl"), - joinpath("Building", "dynamics.jl"), - joinpath("Building", "objective.jl"), - joinpath("Building", "constraints.jl"), - joinpath("Building", "definition.jl"), - joinpath("Building", "time_dependence.jl"), - joinpath("Building", "build.jl"), - joinpath("Building", "defaults.jl"), - joinpath("Building", "name_validation.jl"), - ), - ], - external_modules_to_document=[CTModels], - exclude=EXCLUDE_SYMBOLS, - public=true, - private=true, + ( + mod=CTModels.Building, title="Building", - title_in_menu="Building", - filename="api_ocp_building", + filename="building", + files=src( + joinpath("Building", "constraint_composers.jl"), + joinpath("Building", "pre_model.jl"), + joinpath("Building", "state.jl"), + joinpath("Building", "control.jl"), + joinpath("Building", "variable.jl"), + joinpath("Building", "times.jl"), + joinpath("Building", "dynamics.jl"), + joinpath("Building", "objective.jl"), + joinpath("Building", "constraints.jl"), + joinpath("Building", "definition.jl"), + joinpath("Building", "time_dependence.jl"), + joinpath("Building", "build.jl"), + joinpath("Building", "defaults.jl"), + joinpath("Building", "name_validation.jl"), + ), ), - # ─────────────────────────────────────────────────────────────────── - # Solutions — Solution types, build_solution, duals - # ─────────────────────────────────────────────────────────────────── - CTBase.automatic_reference_documentation(; - subdirectory=".", - primary_modules=[ - CTModels.Solutions => src( - joinpath("Solutions", "solution_types.jl"), - joinpath("Solutions", "dual_functors.jl"), - joinpath("Solutions", "build_solution.jl"), - joinpath("Solutions", "dual_model.jl"), - joinpath("Solutions", "interpolation_helpers.jl"), - joinpath("Solutions", "discretization_utils.jl"), - ), - ], - external_modules_to_document=[CTModels], - exclude=EXCLUDE_SYMBOLS, - public=true, - private=true, + ( + mod=CTModels.Solutions, title="Solutions", - title_in_menu="Solutions", - filename="api_ocp_core", + filename="solutions", + files=src( + joinpath("Solutions", "solution_types.jl"), + joinpath("Solutions", "dual_functors.jl"), + joinpath("Solutions", "build_solution.jl"), + joinpath("Solutions", "dual_model.jl"), + joinpath("Solutions", "interpolation_helpers.jl"), + joinpath("Solutions", "discretization_utils.jl"), + ), ), - # ─────────────────────────────────────────────────────────────────── - # Display - # ─────────────────────────────────────────────────────────────────── - CTBase.automatic_reference_documentation(; - subdirectory=".", - primary_modules=[ - CTModels.Display => src( - joinpath("Display", "Display.jl"), - joinpath("Display", "ansi.jl"), - joinpath("Display", "definition.jl"), - joinpath("Display", "mathematical.jl"), - joinpath("Display", "model.jl"), - joinpath("Display", "pre_model.jl"), - joinpath("Display", "solution.jl"), - ), - CTModelsPlots => - ext("CTModelsPlots.jl", "plot.jl", "plot_utils.jl", "plot_default.jl"), - ], - external_modules_to_document=[Plots], - exclude=EXCLUDE_SYMBOLS, - public=true, - private=true, - title="Display, Plots", - title_in_menu="Display, Plots", - filename="api_display", + ( + mod=CTModels.Display, + title="Display", + filename="display", + files=src( + joinpath("Display", "Display.jl"), + joinpath("Display", "ansi.jl"), + joinpath("Display", "definition.jl"), + joinpath("Display", "mathematical.jl"), + joinpath("Display", "model.jl"), + joinpath("Display", "pre_model.jl"), + joinpath("Display", "solution.jl"), + ), ), - # ─────────────────────────────────────────────────────────────────── - # Serialization - # ─────────────────────────────────────────────────────────────────── - CTBase.automatic_reference_documentation(; - subdirectory=".", - primary_modules=[ - CTModels.Serialization => src( - joinpath("Serialization", "Serialization.jl"), - joinpath("Serialization", "export_import.jl"), - joinpath("Serialization", "types.jl"), - joinpath("Serialization", "reconstruction_helpers.jl"), - ), - CTModelsJSON => ext("CTModelsJSON.jl"), - CTModelsJLD => ext("CTModelsJLD.jl"), - ], - exclude=EXCLUDE_SYMBOLS, - public=true, - private=true, - title="Serialization, JSON & JLD2", - title_in_menu="Serialization, JSON & JLD2", - filename="api_serialization", + ( + mod=CTModels.Serialization, + title="Serialization", + filename="serialization", + files=src( + joinpath("Serialization", "Serialization.jl"), + joinpath("Serialization", "export_import.jl"), + joinpath("Serialization", "types.jl"), + joinpath("Serialization", "reconstruction_helpers.jl"), + ), ), - # ─────────────────────────────────────────────────────────────────── - # InitialGuess - # ─────────────────────────────────────────────────────────────────── + ( + mod=CTModels.Init, + title="Init", + filename="init", + files=src( + joinpath("Init", "Init.jl"), + joinpath("Init", "types.jl"), + joinpath("Init", "api.jl"), + joinpath("Init", "init_functors.jl"), + joinpath("Init", "builders.jl"), + joinpath("Init", "state.jl"), + joinpath("Init", "control.jl"), + joinpath("Init", "variable.jl"), + joinpath("Init", "validation.jl"), + joinpath("Init", "utils.jl"), + ), + ), + ] + + # ── Public pages: one flat page per submodule ───────────────────────────── + pages = [ CTBase.automatic_reference_documentation(; - subdirectory=".", - primary_modules=[ - CTModels.Init => src( - joinpath("Init", "Init.jl"), - joinpath("Init", "types.jl"), - joinpath("Init", "api.jl"), - joinpath("Init", "init_functors.jl"), - joinpath("Init", "builders.jl"), - joinpath("Init", "state.jl"), - joinpath("Init", "control.jl"), - joinpath("Init", "variable.jl"), - joinpath("Init", "validation.jl"), - joinpath("Init", "utils.jl"), - ), - ], + subdirectory="api", + primary_modules=[cfg.mod => cfg.files], + external_modules_to_document=[CTModels], exclude=EXCLUDE_SYMBOLS, public=true, - private=true, - title="InitialGuess", - title_in_menu="InitialGuess", - filename="api_initial_guess", - ), + private=false, + title=cfg.title, + title_in_menu=cfg.title, + filename=cfg.filename, + ) for cfg in modules_config ] - # ─────────────────────────────────────────────────────────────────── - # Extensions (conditional) - # ─────────────────────────────────────────────────────────────────── - - # # CTModelsPlots extension - # CTModelsPlots = Base.get_extension(CTModels, :CTModelsPlots) - # if !isnothing(CTModelsPlots) - # push!( - # pages, - # CTBase.automatic_reference_documentation(; - # subdirectory=".", - # primary_modules=[ - # CTModelsPlots => ext("plot.jl", "plot_utils.jl", "plot_default.jl") - # ], - # external_modules_to_document=[CTModels], - # exclude=EXCLUDE_SYMBOLS, - # public=true, - # private=true, - # title="CTModelsPlots", - # title_in_menu="Plots Extension", - # filename="api_plots_extension", - # ), - # ) - # end + # ── Internals: all private symbols in one page, sections by module ──────── + internals_modules = Any[cfg.mod => cfg.files for cfg in modules_config] - # # CTModelsJSON extension - # CTModelsJSON = Base.get_extension(CTModels, :CTModelsJSON) - # if !isnothing(CTModelsJSON) - # push!( - # pages, - # CTBase.automatic_reference_documentation(; - # subdirectory=".", - # primary_modules=[CTModelsJSON => ext("CTModelsJSON.jl")], - # external_modules_to_document=[CTModels], - # exclude=EXCLUDE_SYMBOLS, - # public=true, - # private=true, - # title="CTModelsJSON", - # title_in_menu="JSON Extension", - # filename="api_json_extension", - # ), - # ) - # end + # Conditional extensions + for (sym, files) in [ + (:CTModelsPlots, ext("CTModelsPlots.jl", "plot.jl", "plot_utils.jl", "plot_default.jl")), + (:CTModelsJSON, ext("CTModelsJSON.jl")), + (:CTModelsJLD, ext("CTModelsJLD.jl")), + ] + extmod = Base.get_extension(CTModels, sym) + isnothing(extmod) || push!(internals_modules, extmod => files) + end - # # CTModelsJLD extension - # CTModelsJLD = Base.get_extension(CTModels, :CTModelsJLD) - # if !isnothing(CTModelsJLD) - # push!( - # pages, - # CTBase.automatic_reference_documentation(; - # subdirectory=".", - # primary_modules=[CTModelsJLD => ext("CTModelsJLD.jl")], - # external_modules_to_document=[CTModels], - # exclude=EXCLUDE_SYMBOLS, - # public=true, - # private=true, - # title="CTModelsJLD", - # title_in_menu="JLD2 Extension", - # filename="api_jld_extension", - # ), - # ) - # end + push!( + pages, + CTBase.automatic_reference_documentation(; + subdirectory="api", + primary_modules=internals_modules, + external_modules_to_document=[CTModels], + exclude=EXCLUDE_INTERNALS, + public=false, + private=true, + title="Internals", + title_in_menu="Internals", + filename="internals", + ), + ) return pages end @@ -282,39 +189,19 @@ function with_api_reference(f::Function, src_dir::String, ext_dir::String) try f(pages) finally - # Clean up generated files - # The pages are Pairs: "Title" => "filename.md" (relative to build? no, relative to src) - # automatic_reference_documentation returns "filename" which is relative to docs/src if subdirectory="." - - # We need to reconstruct the full path to delete them. - # Assuming they are in docs/src (which is where makedocs runs from?) - # Wait, makedocs options say subdirectory=".". - # Typically automatic_reference_documentation writes to joinpath(@__DIR__, "src", subdirectory, filename.md) ?? - # I need to check where automatic_reference_documentation writes. - # Assuming we are running from docs/ (where make.jl is). - - # Let's assume the files are in `docs/src`. docs_src = abspath(joinpath(@__DIR__, "src")) - - function cleanup_pages(pages) + function cleanup(pages) for p in pages content = last(p) if content isa AbstractString - # file path - filename = content - fname = endswith(filename, ".md") ? filename : filename * ".md" + fname = endswith(content, ".md") ? content : content * ".md" full_path = joinpath(docs_src, fname) - if isfile(full_path) - rm(full_path) - println("Removed temporary API doc: $full_path") - end + isfile(full_path) && rm(full_path) elseif content isa Vector - # nested pages - cleanup_pages(content) + cleanup(content) end end end - - cleanup_pages(pages) + cleanup(pages) end end diff --git a/docs/make.jl b/docs/make.jl index 4345b79b..42edf607 100644 --- a/docs/make.jl +++ b/docs/make.jl @@ -1,47 +1,33 @@ -# to run the documentation generation: -# julia --project=. docs/make.jl +# to run the documentation generation: julia --project=. docs/make.jl +# to serve the documentation (option 1 — handles clean URLs natively): +# npx serve docs/build/1 --listen 5173 +# to serve the documentation (option 2 — Julia only): +# julia --project=docs -e 'using LiveServer; LiveServer.serve(dir="docs/build/1", single_page=true)' +# note: single_page=true is required so that reloading /getting-started serves the correct HTML pushfirst!(LOAD_PATH, joinpath(@__DIR__)) pushfirst!(LOAD_PATH, joinpath(@__DIR__, "..")) using Documenter +using DocumenterVitepress using CTModels using CTBase -using Plots -using JSON3 -using JLD2 using Markdown using MarkdownAST: MarkdownAST # ═══════════════════════════════════════════════════════════════════════════════ # Configuration # ═══════════════════════════════════════════════════════════════════════════════ -# if draft is true, then the julia code from .md is not executed -# to disable the draft mode in a specific markdown file, use the following: -#= -```@meta -Draft = false -``` -=# -draft = false # Draft mode: if true, @example blocks in markdown are not executed +draft = false # Draft mode: if true, @example blocks in markdown are not executed # ═══════════════════════════════════════════════════════════════════════════════ -# Load extensions +# Extensions # ═══════════════════════════════════════════════════════════════════════════════ -const CTModelsPlots = Base.get_extension(CTModels, :CTModelsPlots) -const CTModelsJSON = Base.get_extension(CTModels, :CTModelsJSON) -const CTModelsJLD = Base.get_extension(CTModels, :CTModelsJLD) const DocumenterReference = Base.get_extension(CTBase, :DocumenterReference) if !isnothing(DocumenterReference) DocumenterReference.reset_config!() end -Modules = [Plots, CTModelsPlots, CTModelsJSON, CTModelsJLD] -for Module in Modules - isnothing(DocMeta.getdocmeta(Module, :DocTestSetup)) && - DocMeta.setdocmeta!(Module, :DocTestSetup, :(using $Module); recursive=true) -end - # ═══════════════════════════════════════════════════════════════════════════════ # Paths # ═══════════════════════════════════════════════════════════════════════════════ @@ -61,40 +47,35 @@ with_api_reference(src_dir, ext_dir) do api_pages remotes=nothing, warnonly=[:cross_references], sitename="CTModels.jl", - format=Documenter.HTML(; - repolink="https://" * repo_url, - prettyurls=false, - assets=[ - asset("https://control-toolbox.org/assets/css/documentation.css"), - asset("https://control-toolbox.org/assets/js/documentation.js"), - ], - size_threshold_ignore=["api_ocp_building_public.md"], + format=DocumenterVitepress.MarkdownVitepress(; + repo=repo_url, devbranch="main", devurl="dev", sidebar_drawer=true ), pages=[ - "Introduction" => "index.md", - "Optimal control problems" => [ - "Overview" => "model/index.md", - "Types and traits" => "model/types_and_traits.md", - "Components" => "model/components.md", - "Dynamics and objective" => "model/dynamics_objective.md", - "Constraints" => "model/constraints.md", - "Building a model" => "model/building.md", + # index.md is the VitePress root — not listed here + "Getting Started" => "getting-started.md", + "OCP Model" => [ + "Overview" => "model/index.md", + "Types & Traits" => "model/types_and_traits.md", + "Components" => "model/components.md", + "Dynamics & Objective" => "model/dynamics_objective.md", + "Constraints" => "model/constraints.md", + "Building a Model" => "model/building.md", ], "Solutions" => [ - "Overview" => "solution/index.md", - "Time grids" => "solution/time_grids.md", - "Trajectories" => "solution/trajectories.md", - "Duals & diagnostics" => "solution/duals.md", + "Overview" => "solution/index.md", + "Time Grids" => "solution/time_grids.md", + "Trajectories" => "solution/trajectories.md", + "Duals & Diagnostics" => "solution/duals.md", ], - "Initial guesses" => [ - "Overview" => "initial_guess/index.md", - "Input formats" => "initial_guess/formats.md", - "Validation & warm-start" => "initial_guess/validation.md", + "Initial Guesses" => [ + "Overview" => "initial_guess/index.md", + "Input Formats" => "initial_guess/formats.md", + "Validation" => "initial_guess/validation.md", ], - "Serialization & extensions" => [ - "Overview" => "serialization/index.md", - "Export & import" => "serialization/export_import.md", - "Plotting" => "serialization/plotting.md", + "Extensions" => [ + "Overview" => "serialization/index.md", + "Export & Import" => "serialization/export_import.md", + "Plotting" => "serialization/plotting.md", ], "API Reference" => api_pages, ], @@ -102,4 +83,13 @@ with_api_reference(src_dir, ext_dir) do api_pages end # ═══════════════════════════════════════════════════════════════════════════════ -deploydocs(; repo=repo_url * ".git", devbranch="main") +# Deploy documentation to GitHub Pages +# ═══════════════════════════════════════════════════════════════════════════════ +bases_file = joinpath(@__DIR__, "build", "bases.txt") +if isfile(bases_file) + DocumenterVitepress.deploydocs(; + repo=repo_url * ".git", devbranch="main", push_preview=true + ) +else + @info "Skipping deployment: no bases were built (prerelease with existing higher stable release)." +end diff --git a/docs/package.json b/docs/package.json new file mode 100644 index 00000000..04639191 --- /dev/null +++ b/docs/package.json @@ -0,0 +1,20 @@ +{ + "devDependencies": { + "@types/node": "^25.3.5", + "@types/markdown-it-footnote": "^3.0.4" + }, + "scripts": { + "docs:dev": "vitepress dev build/.documenter", + "docs:build": "vitepress build build/.documenter", + "docs:preview": "vitepress preview build/.documenter" + }, + "dependencies": { + "@nolebase/vitepress-plugin-enhanced-readabilities": "^2.18.2", + "@mdit/plugin-mathjax": "^0.26.1", + "@mdit/plugin-tex": "^0.24.1", + "markdown-it-footnote": "^4.0.0", + "markdown-it": "^14.1.0", + "vitepress": "^1.6.4", + "vitepress-plugin-tabs": "^0.8.0" + } +} diff --git a/docs/src/.vitepress/config.mts b/docs/src/.vitepress/config.mts new file mode 100644 index 00000000..c8d95959 --- /dev/null +++ b/docs/src/.vitepress/config.mts @@ -0,0 +1,101 @@ +import { defineConfig } from 'vitepress' +import { tabsMarkdownPlugin } from 'vitepress-plugin-tabs' +import { mathjaxPlugin } from './mathjax-plugin' +import { juliaReplTransformer } from './julia-repl-transformer' +import footnote from "markdown-it-footnote"; +import path from 'path' + +const mathjax = mathjaxPlugin() + +function getBaseRepository(base: string): string { + if (!base || base === '/') return '/'; + const parts = base.split('/').filter(Boolean); + return parts.length > 0 ? `/${parts[0]}/` : '/'; +} + +const baseTemp = { + base: 'REPLACE_ME_DOCUMENTER_VITEPRESS',// TODO: replace this in makedocs! +} + +const nav = [ + { text: 'Home', link: '/index' }, + { component: 'VersionPicker' } +] + +// https://vitepress.dev/reference/site-config +export default defineConfig({ + base: 'REPLACE_ME_DOCUMENTER_VITEPRESS',// TODO: replace this in makedocs! + title: 'REPLACE_ME_DOCUMENTER_VITEPRESS', + description: 'REPLACE_ME_DOCUMENTER_VITEPRESS', + lastUpdated: true, + cleanUrls: true, + outDir: 'REPLACE_ME_DOCUMENTER_VITEPRESS', // This is required for MarkdownVitepress to work correctly... + head: [ + ['link', { rel: 'icon', href: 'REPLACE_ME_DOCUMENTER_VITEPRESS_FAVICON' }], + ['link', { rel: 'stylesheet', href: 'https://control-toolbox.org/assets/css/vitepress-documentation.css' }], + ['script', {src: `${getBaseRepository(baseTemp.base)}versions.js`}], + ['script', {src: 'https://control-toolbox.org/assets/js/vitepress-documentation.js'}], + ['script', {src: `${baseTemp.base}siteinfo.js`}] + ], + + markdown: { + codeTransformers: [juliaReplTransformer()], + config(md) { + md.use(tabsMarkdownPlugin); + md.use(footnote); + mathjax.markdownConfig(md); + }, + theme: { + light: "github-light", + dark: "github-dark" + }, + }, + vite: { + plugins: [ + mathjax.vitePlugin, + ], + define: { + __DEPLOY_ABSPATH__: JSON.stringify('REPLACE_ME_DOCUMENTER_VITEPRESS_DEPLOY_ABSPATH'), + }, + resolve: { + alias: { + '@': path.resolve(__dirname, '../components') + } + }, + optimizeDeps: { + exclude: [ + '@nolebase/vitepress-plugin-enhanced-readabilities/client', + 'vitepress', + '@nolebase/ui', + ], + }, + ssr: { + noExternal: [ + // If there are other packages that need to be processed by Vite, you can add them here. + '@nolebase/vitepress-plugin-enhanced-readabilities', + '@nolebase/ui', + ], + }, + }, + themeConfig: { + outline: 'deep', + logo: 'REPLACE_ME_DOCUMENTER_VITEPRESS', + search: { + provider: 'local', + options: { + detailedView: true + } + }, + nav, + sidebar: 'REPLACE_ME_DOCUMENTER_VITEPRESS', + sidebarDrawer: 'REPLACE_ME_DOCUMENTER_VITEPRESS_SIDEBAR_DRAWER', + editLink: 'REPLACE_ME_DOCUMENTER_VITEPRESS', + socialLinks: [ + { icon: 'github', link: 'REPLACE_ME_DOCUMENTER_VITEPRESS' } + ], + footer: { + message: 'Made with DocumenterVitepress.jl
', + copyright: `© Copyright ${new Date().getUTCFullYear()}.` + } + } +}) diff --git a/docs/src/.vitepress/julia-repl-transformer.ts b/docs/src/.vitepress/julia-repl-transformer.ts new file mode 100644 index 00000000..2f372e74 --- /dev/null +++ b/docs/src/.vitepress/julia-repl-transformer.ts @@ -0,0 +1,54 @@ +import type { ShikiTransformer } from "shiki" + +type PromptKind = "julia" | "pkg" | null + +export function juliaReplTransformer(): ShikiTransformer { + let promptInfoByLine: Array<{ len: number; kind: PromptKind }> = [] + let isJuliaBlock = false + const rules: Array<{ kind: PromptKind; re: RegExp }> = [ + { kind: "julia", re: /^julia>/ }, + { kind: "pkg", re: /^(\([^)]*\)\s*)?pkg>/ }, // handles (@v1.9) pkg> + ] + + function classify(line: string): { len: number; kind: PromptKind } { + for (const r of rules) { + const m = line.match(r.re) + if (m) return { len: m[0].length, kind: r.kind } + } + + return { len: 0, kind: null } + } + + return { + name: "julia-repl-prompts", + + preprocess(code, options) { + isJuliaBlock = options.lang === "julia" + return code + }, + + tokens(tokens) { + if (!isJuliaBlock) { + promptInfoByLine = [] + return + } + + promptInfoByLine = tokens.map((lineTokens) => { + const line = lineTokens.map((t) => t.content).join("") + return classify(line) + }) + }, + + span(node, line, col) { + if (!isJuliaBlock) return + + const info = promptInfoByLine[line - 1] + if (!info || !info.kind || info.len <= 0) return + + if (col < info.len) { + this.addClassToHast(node, "repl-prompt") + this.addClassToHast(node, `repl-prompt-${info.kind}`) + } + }, + } +} diff --git a/docs/src/.vitepress/mathjax-plugin.ts b/docs/src/.vitepress/mathjax-plugin.ts new file mode 100644 index 00000000..4307d32d --- /dev/null +++ b/docs/src/.vitepress/mathjax-plugin.ts @@ -0,0 +1,142 @@ +// adapter from https://github.com/orgs/vuepress-theme-hope/discussions/5178#discussioncomment-15642629 +// mathjax-plugin.ts +// @ts-ignore +import MathJax from '@mathjax/src' +import type { Plugin as VitePlugin } from 'vite' +import type MarkdownIt from 'markdown-it' +import { tex as mdTex } from '@mdit/plugin-tex' + +const mathjaxStyleModuleID = 'virtual:mathjax-styles.css' + +interface MathJaxOptions { + font?: string +} + +async function initializeMathJax(options: MathJaxOptions = {}) { + const font = options.font || 'mathjax-newcm' + + const config: any = { + loader: { + load: [ + 'input/tex', + 'output/svg', + '[tex]/boldsymbol', + '[tex]/braket', + '[tex]/mathtools', + ], + paths: { mathjax: '@mathjax/src/bundle' }, + }, + tex: { + tags: 'ams', + packages: { + '[+]': ['boldsymbol', 'braket', 'mathtools'], + }, + }, + output: { + font, + displayOverflow: 'linebreak', + mtextInheritFont: true, + }, + svg: { + fontCache: 'none', // critical: avoids async font loading + }, + } + + await MathJax.init(config) + await MathJax.startup.document.outputJax.font.loadDynamicFiles() +} + +export function mathjaxPlugin(options: MathJaxOptions = {}) { + let adaptor: any + let initialized = false + + async function ensureInitialized() { + if (!initialized) { + await initializeMathJax(options) + adaptor = MathJax.startup.adaptor + initialized = true + } + } + + function renderMath(content: string, displayMode: boolean): string { + if (!initialized) { + throw new Error('MathJax not initialized') + } + + const node = MathJax.tex2svg(content, { display: displayMode }) + + // Prevent Vue from touching MathJax output + adaptor.setAttribute(node, 'v-pre', '') + + let html = adaptor.outerHTML(node) + + // Preserve spaces inside mjx-break (SVG only) + html = html.replace( + /(.*?)<\/mjx-break>/g, + (_: string, attr: string, inner: string) => + `${inner.replace(/ /g, ' ')}`, + ) + + // Wrap only display equations (not inline math) + html = html.replace( + /(]*display="true"[^>]*>)([\s\S]*?)(<\/mjx-container>)/, + '
$1$2$3
' + ) + + return html + } + + function getMathJaxStyles(): string { + return initialized + ? adaptor.textContent(MathJax.svgStylesheet()) || '' + : '' + } + + function resetMathJax(): void { + if (!initialized) return + MathJax.texReset() + MathJax.typesetClear() + } + + function viteMathJax(): VitePlugin { + const virtualModuleID = '\0' + mathjaxStyleModuleID + + return { + name: 'mathjax-styles', + + resolveId(id) { + if (id === mathjaxStyleModuleID) { + return virtualModuleID + } + }, + + async load(id) { + if (id === virtualModuleID) { + await ensureInitialized() + return getMathJaxStyles() + } + }, + } + } + + function mdMathJax(md: MarkdownIt): void { + mdTex(md, { + render: renderMath, + }) + + const orig = md.render + md.render = function (...args) { + resetMathJax() + return orig.apply(this, args) + } + } + + const init = ensureInitialized() + + return { + vitePlugin: viteMathJax(), + markdownConfig: mdMathJax, + styleModuleID: mathjaxStyleModuleID, + init, + } +} \ No newline at end of file diff --git a/docs/src/.vitepress/theme/docstrings.css b/docs/src/.vitepress/theme/docstrings.css new file mode 100644 index 00000000..4d992460 --- /dev/null +++ b/docs/src/.vitepress/theme/docstrings.css @@ -0,0 +1,51 @@ +.jldocstring.custom-block { + border: 1px solid var(--vp-c-gray-2); + color: var(--vp-c-text-1); + overflow: hidden; +} + +.jldocstring.custom-block summary { + font-weight: 700; + cursor: pointer; + user-select: none; + margin: 0 0 8px; +} +.jldocstring.custom-block summary a { + pointer-events: none; + text-decoration: none; +} + +.jldocstring.custom-block .source-link { + border: 1px solid var(--vp-c-gray-2); + border-radius: 4px; + text-decoration: none; + background-color: #414040; + float: right; + opacity: 0; + visibility: hidden; + transform: translateY(-5px); + transition: all 0.5s cubic-bezier(0.25, 0.1, 0.25, 1); +} + +.jldocstring.custom-block .source-link a { + text-decoration: none; + color: #e5e5e5; +} + +.jldocstring.custom-block .source-link a:hover { + text-decoration: underline; +} + +.jldocstring.custom-block:hover .source-link { + opacity: 1; + visibility: visible; + transform: translateY(0); +} + +@media (max-width: 768px) { + .jldocstring.custom-block .source-link { + opacity: 1; + visibility: visible; + transform: translateY(0); + } +} diff --git a/docs/src/.vitepress/theme/index.ts b/docs/src/.vitepress/theme/index.ts new file mode 100644 index 00000000..ec0a8cad --- /dev/null +++ b/docs/src/.vitepress/theme/index.ts @@ -0,0 +1,43 @@ +// .vitepress/theme/index.ts +import { h } from 'vue' +import DefaultTheme from 'vitepress/theme' +import type { Theme as ThemeConfig } from 'vitepress' +import 'virtual:mathjax-styles.css'; + +import { + NolebaseEnhancedReadabilitiesMenu, + NolebaseEnhancedReadabilitiesScreenMenu, +} from '@nolebase/vitepress-plugin-enhanced-readabilities/client' + +import VersionPicker from "@/VersionPicker.vue" +import AuthorBadge from '@/AuthorBadge.vue' +import Authors from '@/Authors.vue' +import SidebarDrawerToggle from '@/SidebarDrawerToggle.vue' + +import { enhanceAppWithTabs } from 'vitepress-plugin-tabs/client' + +import '@nolebase/vitepress-plugin-enhanced-readabilities/client/style.css' +import './style.css' // You could setup your own, or else a default will be copied. +import './docstrings.css' // You could setup your own, or else a default will be copied. + +export const Theme: ThemeConfig = { + extends: DefaultTheme, + Layout() { + return h(DefaultTheme.Layout, null, { + 'nav-bar-content-after': () => [ + h(NolebaseEnhancedReadabilitiesMenu), // Enhanced Readabilities menu + ], + // A enhanced readabilities menu for narrower screens (usually smaller than iPad Mini) + 'nav-screen-content-after': () => h(NolebaseEnhancedReadabilitiesScreenMenu), + // Sidebar drawer toggle button (to the left of search bar) + 'nav-bar-content-before': () => h(SidebarDrawerToggle), + }) + }, + enhanceApp({ app, router, siteData }) { + enhanceAppWithTabs(app); + app.component('VersionPicker', VersionPicker); + app.component('AuthorBadge', AuthorBadge) + app.component('Authors', Authors) + } +} +export default Theme \ No newline at end of file diff --git a/docs/src/.vitepress/theme/style.css b/docs/src/.vitepress/theme/style.css new file mode 100644 index 00000000..76f87af7 --- /dev/null +++ b/docs/src/.vitepress/theme/style.css @@ -0,0 +1,323 @@ +/* Customize default theme styling by overriding CSS variables: +https://github.com/vuejs/vitepress/blob/main/src/client/theme-default/styles/vars.css */ +/* Example */ +/* https://github.com/vuejs/vitepress/blob/main/template/.vitepress/theme/style.css */ + +.VPHero .clip { + white-space: pre; + max-width: 600px; +} + +/* Fonts */ +@font-face { + font-family: JuliaMono-Regular; + src: url("https://cdn.jsdelivr.net/gh/cormullion/juliamono/webfonts/JuliaMono-Regular.woff2"); +} + +:root { +scroll-behavior: smooth; +/* Typography */ +--vp-font-family-base: "Barlow", "Inter var experimental", "Inter var", + -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, Oxygen, Ubuntu, + Cantarell, "Fira Sans", "Droid Sans", "Helvetica Neue", sans-serif; + +/* Code Snippet font */ +--vp-font-family-mono: JuliaMono-Regular, monospace; +} + +/* Disable contextual alternates (kind of like ligatures but different) in monospace, + which turns `/>` to an up arrow and `|>` (the Julia pipe symbol) to an up arrow as well. */ +.mono-no-substitutions { +font-family: "JuliaMono-Regular", monospace; +font-feature-settings: "calt" off; +} + +.mono-no-substitutions-alt { +font-family: "JuliaMono-Regular", monospace; +font-variant-ligatures: none; +} + +pre, code { +font-family: "JuliaMono-Regular", monospace; +font-feature-settings: "calt" off; +} + +/* Colors */ +:root { + --julia-blue: #4063D8; + --julia-purple: #9558B2; + --julia-red: #CB3C33; + --julia-green: #389826; + + --vp-c-brand: #0087d7; + --vp-c-brand-1: #0890df; + --vp-c-brand-2: #0599ef; + --vp-c-brand-3: #0c9ff4; + --vp-c-brand-light: #0087d7; + --vp-c-brand-dark: #5fd7ff; + --vp-c-brand-dimm: #212425; + + /* Greens */ + --vp-dark-green: #155f3e; /* Main accent green */ + --vp-dark-green-dark: #2b855c; + --vp-dark-green-light: #42d392; + --vp-dark-green-lighter: #35eb9a; + /* Complementary Colors */ + --vp-dark-gray: #1e1e1e; + --vp-dark-gray-soft: #2a2a2a; + --vp-dark-gray-mute: #242424; + --vp-light-gray: #d1d5db; + --vp-tip-bg: rgb(254, 254, 254); + + /* Text Colors */ + --vp-dark-text: #e5e5e5; /* Primary text color */ + --vp-dark-subtext: #c1c1c1; /* Subtle text */ + --vp-source-text: #e5e5e5; + /* custom tip */ + --vp-custom-block-tip-border: var(--vp-c-brand-light); + --vp-custom-block-tip-bg: var(--vp-tip-bg); +} + + /* Component: Button */ +:root { + --vp-button-brand-border: var(--vp-light-gray); + --vp-button-brand-bg: var(--vp-c-brand-light); + --vp-button-brand-hover-border: var(--vp-c-bg-alt); + --vp-button-brand-hover-bg: var(--julia-blue); +} + +/* Component: Home */ +:root { + --vp-home-hero-name-color: transparent; + --vp-home-hero-name-background: -webkit-linear-gradient( + 120deg, + #9558B2 30%, + #CB3C33 + ); + + --vp-home-hero-image-background-image: linear-gradient( + -145deg, + #9558b282 30%, + #3798269a 30%, + #cb3d33e3 + ); + --vp-home-hero-image-filter: blur(40px); +} + +/* Hero Section */ +:root.dark { + --vp-home-hero-name-color: transparent; + --vp-home-hero-name-background: -webkit-linear-gradient( + 120deg, + var(--julia-purple) 15%, + var(--vp-dark-green-light), + var(--vp-dark-green) + + ); + --vp-home-hero-image-background-image: linear-gradient( + -45deg, + var(--vp-dark-green) 30%, + var(--vp-dark-green-light), + var(--vp-dark-gray) 30% + ); + --vp-home-hero-image-filter: blur(56px); +} + +:root.dark { + /* custom tip */ + --vp-custom-block-tip-border: var(--vp-dark-green-dark); + --vp-custom-block-tip-text: var(--vp-dark-subtext); + --vp-custom-block-tip-bg: var(--vp-dark-gray-mute); +} + +/** + * Colors links + * -------------------------------------------------------------------------- */ + +.dark { + --vp-c-brand: var(--vp-dark-green-light); + --vp-button-brand-border: var(--vp-dark-green-lighter); + --vp-button-brand-bg: var(--vp-dark-green); + --vp-c-brand-1: var(--vp-dark-green-light); + --vp-c-brand-2: var(--vp-dark-green-lighter); + --vp-c-brand-3: var(--vp-dark-green); +} + +@media (min-width: 640px) { + :root { + --vp-home-hero-image-filter: blur(56px); + } +} + +@media (min-width: 960px) { + :root { + --vp-home-hero-image-filter: blur(72px); + } +} +/* Component: MathJax */ + + +.mjx-scroll-wrapper { + display: block; + max-width: 100%; + overflow-x: auto; + overflow-y: hidden; + text-align: center; +} + +mjx-container { + display: inline-block; + padding: 0.5rem 0; + margin: auto 2px -2px; + overflow: visible !important; /* Override MathJax's overflow */ + text-align: left; +} + +mjx-container > svg { + display: inline-block; + height: auto; +} + +mjx-container > svg[data-labels="true"] { + position: absolute; + left: 0; + top: 0; + width: 100%; + height: 100%; + pointer-events: none; +} + +/* @mdit/plugin-mathjax theming refs */ + +mjx-container svg path { + fill: currentColor !important; + stroke: currentColor !important; +} + +.MathJax_ref { + color: var(--vp-c-brand-1); +} + +.MathJax_ref:hover { + color: var(--vp-c-brand-2); + cursor: pointer; +} + +/* ===== Custom colors for input and output code blocks ===== */ +:root { + /* --vp-c-bg-input: #eef0f3; */ + --vp-c-bg-output: #fbfbfb; + --vp-c-bg-output-outline: #e2e2e3; +} + +.dark { + /* --vp-c-bg-input: #1a1a1a; */ + --vp-c-bg-output: #1a1a1a; + --vp-c-bg-output-outline: #2e2e32; +} + +/* +.language-julia { + background-color: var(--vp-c-bg-input) !important; +} +*/ + +.language- { + background-color: var(--vp-c-bg-output) !important; + outline: 1px solid var(--vp-c-bg-output-outline); +} + +/* Julia REPL prompt syntax highlighting */ +/* prompt token behavior */ +.vp-doc .shiki .repl-prompt { + opacity: 1.0; + user-select: none; +} + +:root:not(.dark) .vp-doc .shiki .repl-prompt-julia { + color: var(--vp-dark-green); +} + +:root:not(.dark) .vp-doc .shiki .repl-prompt-pkg { + color: var(--vp-c-brand-light); +} + +.dark .vp-doc .shiki .repl-prompt-julia { + color: var(--vp-dark-green-light); +} + +.dark .vp-doc .shiki .repl-prompt-pkg { + color: var(--vp-c-brand-dark); +} + +/* ===== Sidebar Drawer Toggle ===== */ +/* Smooth transitions for sidebar collapse/expand on desktop */ +@media (min-width: 960px) { + .VPSidebar { + transition: transform 0.35s cubic-bezier(0.4, 0, 0.2, 1), + opacity 0.25s ease; + } + + .VPContent.has-sidebar { + transition: padding-left 0.35s cubic-bezier(0.4, 0, 0.2, 1), + padding-right 0.35s cubic-bezier(0.4, 0, 0.2, 1); + } + + /* Smooth transitions for navbar elements (needed for expand-back too) */ + .VPNavBar.has-sidebar .content { + transition: padding-left 0.35s cubic-bezier(0.4, 0, 0.2, 1) !important; + } + + .VPNavBar.has-sidebar .divider { + transition: padding-left 0.35s cubic-bezier(0.4, 0, 0.2, 1) !important; + } + + .VPNavBar.has-sidebar .title { + transition: width 0.35s cubic-bezier(0.4, 0, 0.2, 1), + padding 0.35s cubic-bezier(0.4, 0, 0.2, 1), + opacity 0.25s ease !important; + } + + /* Collapsed state: zero the sidebar width variable so all layout formulas + (VitePress defaults + Enhanced Readabilities plugin) adjust automatically */ + html.sidebar-drawer-collapsed { + --vp-sidebar-width: 0px !important; + } + + html.sidebar-drawer-collapsed .VPSidebar { + transform: translateX(-100%); + opacity: 0; + pointer-events: none; + } + + /* Original width mode only: let content fill space up to the aside */ + html.sidebar-drawer-collapsed + body:not(.VPNolebaseEnhancedReadabilitiesLayoutSwitchFullWidth):not(.VPNolebaseEnhancedReadabilitiesLayoutSwitchSidebarWidthAdjustableOnly):not(.VPNolebaseEnhancedReadabilitiesLayoutSwitchBothWidthAdjustable) + .VPDoc.has-sidebar .container { + display: flex; + justify-content: center; + max-width: 992px; + } + + html.sidebar-drawer-collapsed + body:not(.VPNolebaseEnhancedReadabilitiesLayoutSwitchFullWidth):not(.VPNolebaseEnhancedReadabilitiesLayoutSwitchSidebarWidthAdjustableOnly):not(.VPNolebaseEnhancedReadabilitiesLayoutSwitchBothWidthAdjustable) + .VPDoc.has-sidebar .content-container { + max-width: none; + } + + /* Collapse the navbar title area (site logo + name) */ + html.sidebar-drawer-collapsed .VPNavBar.has-sidebar .title, + html.sidebar-drawer-collapsed .VPNavBar.has-sidebar > .wrapper > .container > .title { + width: 0 !important; + overflow: hidden; + opacity: 0; + padding: 0 !important; + pointer-events: none; + } + + /* Move drawer button closer to the edge */ + html.sidebar-drawer-collapsed .VPNavBar.has-sidebar .content, + html.sidebar-drawer-collapsed .VPNavBar.has-sidebar > .wrapper > .container > .content { + padding-left: 8px !important; + } +} \ No newline at end of file diff --git a/docs/src/components/AuthorBadge.vue b/docs/src/components/AuthorBadge.vue new file mode 100644 index 00000000..a64b0afd --- /dev/null +++ b/docs/src/components/AuthorBadge.vue @@ -0,0 +1,139 @@ + + + + + \ No newline at end of file diff --git a/docs/src/components/Authors.vue b/docs/src/components/Authors.vue new file mode 100644 index 00000000..ee7920b4 --- /dev/null +++ b/docs/src/components/Authors.vue @@ -0,0 +1,28 @@ + + + + + \ No newline at end of file diff --git a/docs/src/components/SidebarDrawerToggle.vue b/docs/src/components/SidebarDrawerToggle.vue new file mode 100644 index 00000000..96a10b0b --- /dev/null +++ b/docs/src/components/SidebarDrawerToggle.vue @@ -0,0 +1,110 @@ + + + + + + diff --git a/docs/src/components/VersionPicker.vue b/docs/src/components/VersionPicker.vue new file mode 100644 index 00000000..d03b2e84 --- /dev/null +++ b/docs/src/components/VersionPicker.vue @@ -0,0 +1,125 @@ + + + + + + + \ No newline at end of file diff --git a/docs/src/getting-started.md b/docs/src/getting-started.md new file mode 100644 index 00000000..f0519685 --- /dev/null +++ b/docs/src/getting-started.md @@ -0,0 +1,150 @@ +# Getting Started + +```@meta +CurrentModule = CTModels +``` + +## Installation + +CTModels.jl is typically installed as a dependency of another package in the ecosystem +(e.g. [OptimalControl.jl](https://github.com/control-toolbox/OptimalControl.jl)). +To install it directly: + +```julia +import Pkg +Pkg.add("CTModels") +``` + +**Requires Julia ≥ 1.10.** + +## Mental Model + +CTModels is the **mathematical model layer** of the control-toolbox ecosystem. +It provides: + +- **Types** and **building blocks** for states, controls, variables, time grids, constraints, and cost functionals. +- An immutable `Model` / `Solution` hierarchy for optimal control problems and their numerical solutions. +- Tools to build **initial guesses** for warm-starting a solver. +- Optional extensions for **serialization** (JSON, JLD2) and **plotting**. + +Two things to keep in mind: + +1. **No top-level exports.** `using CTModels` loads the package but brings no symbols + into scope. Every symbol is accessed via its qualified path: + ```julia + CTModels.Building.state! # ✓ always works + CTModels.Solutions.build_solution + CTModels.Init.build_initial_guess + ``` +2. **`PreModel → build → Model` pipeline.** An OCP is assembled incrementally on a mutable + `PreModel`, then frozen into an immutable `Model` by `build`. The `Model` is the object + every downstream package (solver, initial-guess builder, serializer) consumes. + +## 5-Minute Walkthrough + +### Building an optimal control problem + +We solve the *beam* problem: minimise ``\int_0^1 u(t)^2\,\mathrm{d}t`` subject to +``\dot{x} = (x_2, u)``, fixed boundary conditions, and box constraints. + +```@example gs +using CTModels + +# 1. Mutable pre-model +pre = CTModels.PreModel() + +# 2. Declare the spaces (must be done before dynamics/objective) +CTModels.variable!(pre, 0) # no optimisation variable +CTModels.time!(pre; t0=0.0, tf=1.0) +CTModels.state!(pre, 2) # x ∈ ℝ² +CTModels.control!(pre, 1) # u ∈ ℝ + +# 3. Dynamics ẋ = (x₂, u) — in-place form +function beam_dynamics!(r, t, x, u, v) + r[1] = x[2] + r[2] = u[1] + return nothing +end +CTModels.dynamics!(pre, beam_dynamics!) + +# 4. Lagrange cost ∫ u² → min +CTModels.objective!(pre, :min; lagrange=(t, x, u, v) -> u[1]^2) + +# 5. Constraints +function beam_boundary!(r, x0, xf, v) + r[1] = x0[1]; r[2] = x0[2] - 1 + r[3] = xf[1]; r[4] = xf[2] + 1 + return nothing +end +CTModels.constraint!(pre, :boundary; f=beam_boundary!, lb=zeros(4), ub=zeros(4), label=:bc) +CTModels.constraint!(pre, :state; rg=1:1, lb=[0.0], ub=[0.1], label=:x1_box) +CTModels.constraint!(pre, :control; rg=1:1, lb=[-10.0], ub=[10.0], label=:u_box) + +# 6. Mark autonomous and freeze into an immutable Model +CTModels.time_dependence!(pre; autonomous=true) +ocp = CTModels.build(pre) +``` + +The built `Model` exposes its structure through accessors: + +```@example gs +(CTModels.state_dimension(ocp), + CTModels.control_dimension(ocp), + CTModels.is_autonomous(ocp), + CTModels.has_lagrange_cost(ocp)) +``` + +### Assembling a solution + +`build_solution` is the bridge between a solver's raw arrays and a `Solution` object. +Here we fabricate arrays to illustrate the interface: + +```@example gs +N = 101 +T = collect(range(0.0, 1.0; length=N)) +X = hcat(cos.(T), -sin.(T)) # N×2 state samples +U = reshape(-cos.(T), N, 1) # N×1 control samples +P = zeros(N, 2) # N×2 costate samples + +sol = CTModels.build_solution(ocp, T, X, U, Float64[], P; + objective=0.5, + iterations=10, + constraints_violation=1e-9, + message="Solve_Succeeded", + status=:Solve_Succeeded, + successful=true, +) +``` + +Trajectories are returned as callables (interpolated from the samples): + +```@example gs +x = CTModels.state(sol) +(x(0.5), + CTModels.objective(sol), + CTModels.successful(sol)) +``` + +### Building an initial guess + +```@example gs +init = CTModels.build_initial_guess(ocp, + (state=t -> [0.0, 0.0], control=t -> [0.1]) +) +(init.state(0.5), init.control(0.5)) +``` + +## Next Steps + +| Topic | Guide | +| :--- | :--- | +| Types, traits, and the noun architecture | [Types & Traits](model/types_and_traits.md) | +| Declaring spaces (state, control, variable, time) | [Components](model/components.md) | +| Dynamics and objective | [Dynamics & Objective](model/dynamics_objective.md) | +| Path, boundary, and box constraints | [Constraints](model/constraints.md) | +| Freezing a `PreModel` into a `Model` | [Building a Model](model/building.md) | +| Reading state, control, costate trajectories | [Trajectories](solution/trajectories.md) | +| Dual variables and solver diagnostics | [Duals & Diagnostics](solution/duals.md) | +| Warm-starting with initial guesses | [Initial Guesses](initial_guess/index.md) | +| Saving and loading solutions | [Export & Import](serialization/export_import.md) | +| Full API reference | API Reference (left sidebar) | diff --git a/docs/src/index.md b/docs/src/index.md index dbebc011..1159d42e 100644 --- a/docs/src/index.md +++ b/docs/src/index.md @@ -51,7 +51,10 @@ objects you manipulate when formulating an optimal control problem. | Module | Responsibility | |--------|---------------| -| `CTModels.OCP` | Types and builders for optimal control problems and solutions | +| `CTModels.Components` | Foundational types: state, control, variable, times, constraints | +| `CTModels.Models` | Immutable `Model` type and its accessor methods | +| `CTModels.Building` | `PreModel`, component mutators (`state!`, `control!`, …), `build` | +| `CTModels.Solutions` | `Solution` types, `build_solution`, dual model, interpolation | | `CTModels.Display` | `Base.show` extensions for models and solutions | | `CTModels.Serialization` | `export_ocp_solution` / `import_ocp_solution` (JLD2, JSON) | | `CTModels.Init` | Initial guess construction and validation | diff --git a/docs/src/model/components.md b/docs/src/model/components.md index 0aee703b..ecb4dd5e 100644 --- a/docs/src/model/components.md +++ b/docs/src/model/components.md @@ -78,8 +78,8 @@ For a free time the value is read from ``v``, hence ## Naming rules -Names must be **unique across all components** of the problem. The validation -([`OCP/Validation/`](building.md)) rejects empty names, duplicates within a declaration, and +Names must be **unique across all components** of the problem. The validation in +[`CTModels.Building`](@ref CTModels.Building) (see [`__validate_name_uniqueness`](@ref CTModels.Building.__validate_name_uniqueness)) rejects empty names, duplicates within a declaration, and collisions with names already declared elsewhere: ```@example components diff --git a/docs/src/model/index.md b/docs/src/model/index.md index cfa9c778..09725516 100644 --- a/docs/src/model/index.md +++ b/docs/src/model/index.md @@ -12,20 +12,19 @@ An OCP is assembled incrementally into a mutable CTModels builds a model through a **three-stage pipeline**: -``` +```text PreModel → declare components → build → Model (mutable) state!/control!/... (immutable) ``` The OCP layer is organised by responsibility across four modules: -| Layer | Subdirectory | What it provides | +| Module | Directory | What it provides | |---|---|---| -| **Types** | `OCP/Types/` | Component, model and solution types ([`StateModel`](@ref CTModels.Components.StateModel), [`Model`](@ref CTModels.Models.Model), …) | -| **Core** | `OCP/Core/` | Defaults and the [`TimeDependence`](@ref CTModels.Components.TimeDependence) trait | -| **Validation** | `OCP/Validation/` | Name-uniqueness checks across components | -| **Components** | `OCP/Components/` | The `state!`, `control!`, `dynamics!`, … declaration verbs | -| **Building** | `OCP/Building/` | [`build`](@ref CTModels.Building.build) (model) and [`build_solution`](@ref CTModels.Solutions.build_solution) | +| `CTModels.Components` | `src/Components/` | Component types ([`StateModel`](@ref CTModels.Components.StateModel), [`TimeDependence`](@ref CTModels.Components.TimeDependence), …) and their accessors | +| `CTModels.Models` | `src/Models/` | Immutable [`Model`](@ref CTModels.Models.Model) type and all model accessor methods | +| `CTModels.Building` | `src/Building/` | [`PreModel`](@ref CTModels.Building.PreModel), declaration verbs (`state!`, `control!`, …), name validation, [`build`](@ref CTModels.Building.build) | +| `CTModels.Solutions` | `src/Solutions/` | [`build_solution`](@ref CTModels.Solutions.build_solution), solution types, dual model, interpolation | ## Reading order diff --git a/docs/src/model/types_and_traits.md b/docs/src/model/types_and_traits.md index 89c740b6..bc8ef086 100644 --- a/docs/src/model/types_and_traits.md +++ b/docs/src/model/types_and_traits.md @@ -118,5 +118,5 @@ method and break as soon as a third axis appears (the combinatorial explosion of - the public surface stays the *nouns* (`StateModel`, `Model`, …) and the *extractors* (`is_autonomous`, `has_free_final_time`), never the raw parameters. -This mirrors the ecosystem-wide design described in the package philosophy -(`dev/philosophy/types-traits-interfaces.md`). +This mirrors the ecosystem-wide design described in the +[control-toolbox Handbook](https://github.com/control-toolbox/Handbook/blob/main/PHILOSOPHY.md). diff --git a/docs/src/serialization/index.md b/docs/src/serialization/index.md index a50e50c5..25bd1fb9 100644 --- a/docs/src/serialization/index.md +++ b/docs/src/serialization/index.md @@ -1,4 +1,4 @@ -# Serialization & extensions +# Extensions ```@meta CurrentModule = CTModels @@ -20,7 +20,7 @@ live in the core; their **implementations** live in the extension. Until the tri loaded, calling a wrapper raises a descriptive `CTBase.ExtensionError` — the core never hard- depends on JSON3, JLD2 or Plots. -``` +```text core wrapper ──(trigger pkg loaded?)──► extension method │ │ export_ocp_solution no ─► CTBase.ExtensionError diff --git a/ext/CTModelsJLD.jl b/ext/CTModelsJLD.jl index 2db91007..4d067dad 100644 --- a/ext/CTModelsJLD.jl +++ b/ext/CTModelsJLD.jl @@ -30,8 +30,9 @@ serialization warnings for function objects. # Example ```julia-repl -julia> using JLD2 -julia> export_ocp_solution(JLD2Tag(), sol; filename="mysolution") +julia> using JLD2, CTModels + +julia> CTModels.export_ocp_solution(CTModels.JLD2Tag(), sol; filename="mysolution") # → creates "mysolution.jld2" ``` @@ -72,8 +73,9 @@ reconstructs it using `build_solution` from the discretized data. # Example ```julia-repl -julia> using JLD2 -julia> sol = import_ocp_solution(JLD2Tag(), model; filename="mysolution") +julia> using JLD2, CTModels + +julia> sol = CTModels.import_ocp_solution(CTModels.JLD2Tag(), model; filename="mysolution") ``` # Notes diff --git a/ext/CTModelsJSON.jl b/ext/CTModelsJSON.jl index bfcd8089..2bffd417 100644 --- a/ext/CTModelsJSON.jl +++ b/ext/CTModelsJSON.jl @@ -188,8 +188,9 @@ The exported JSON includes the time grid, state, control, costate, objective, so # Example ```julia-repl -julia> using JSON3 -julia> export_ocp_solution(JSON3Tag(), sol; filename="mysolution") +julia> using JSON3, CTModels + +julia> CTModels.export_ocp_solution(CTModels.JSON3Tag(), sol; filename="mysolution") # → creates "mysolution.json" ``` """ @@ -269,8 +270,9 @@ Handles both vector and matrix encodings of signals. If dual fields are missing # Example ```julia-repl -julia> using JSON3 -julia> sol = import_ocp_solution(JSON3Tag(), model; filename="mysolution") +julia> using JSON3, CTModels + +julia> sol = CTModels.import_ocp_solution(CTModels.JSON3Tag(), model; filename="mysolution") ``` """ function CTModels.import_ocp_solution( diff --git a/src/Building/build.jl b/src/Building/build.jl index 7e46b93f..f74eb19b 100644 --- a/src/Building/build.jl +++ b/src/Building/build.jl @@ -417,29 +417,38 @@ instance, incorporating optional components like control, variable, and constrai # Returns - `CTModels.Models.Model`: A fully constructed model ready for solving. -# Example without control -```julia -using CTModels.Building +# Examples -pre_ocp = PreModel() -times!(pre_ocp, 0.0, 1.0, 100) -state!(pre_ocp, 2, "x", ["x1", "x2"]) -dynamics!(pre_ocp, (t, x, u) -> [-x[2], x[1]]) -objective!(pre_ocp, :min, mayer=(x0, xf) -> xf[1]^2) -model = build(pre_ocp) -CTModels.Models.control_dimension(model) # 0 -``` +Minimal Mayer problem (no control): -# Example with control ```julia -using CTModels.Building +using CTModels + +pre = CTModels.PreModel() +CTModels.variable!(pre, 0) +CTModels.time!(pre; t0=0.0, tf=1.0) +CTModels.state!(pre, 2, "x", ["x1", "x2"]) +CTModels.dynamics!(pre, (r, t, x, u, v) -> (r[1] = -x[2]; r[2] = x[1]; nothing)) +CTModels.objective!(pre, :min; mayer=(x0, xf, v) -> xf[1]^2) +CTModels.time_dependence!(pre; autonomous=true) +model = CTModels.build(pre) +CTModels.control_dimension(model) # 0 +``` -pre_ocp = PreModel() -times!(pre_ocp, 0.0, 1.0, 100) -state!(pre_ocp, 2, "x", ["x1", "x2"]) -control!(pre_ocp, 1, "u", ["u1"]) -dynamics!(pre_ocp, (dx, t, x, u, v) -> dx .= x + u) -model = build(pre_ocp) +Bolza problem with control: + +```julia +using CTModels + +pre = CTModels.PreModel() +CTModels.variable!(pre, 0) +CTModels.time!(pre; t0=0.0, tf=1.0) +CTModels.state!(pre, 2) +CTModels.control!(pre, 1) +CTModels.dynamics!(pre, (r, t, x, u, v) -> (r[1] = x[2]; r[2] = u[1]; nothing)) +CTModels.objective!(pre, :min; lagrange=(t, x, u, v) -> u[1]^2) +CTModels.time_dependence!(pre; autonomous=true) +model = CTModels.build(pre) ``` # Throws diff --git a/src/Building/constraints.jl b/src/Building/constraints.jl index c9268866..f58a00bd 100644 --- a/src/Building/constraints.jl +++ b/src/Building/constraints.jl @@ -43,9 +43,11 @@ If `f` is provided, then: # Example ```julia-repl -# Example of adding a state constraint -julia> ocp_constraints = Dict() -julia> __constraint!(ocp_constraints, :state, 3, 2, 1, lb=[0.0], ub=[1.0], label=:my_constraint) +julia> using CTModels + +julia> ocp_constraints = CTModels.Components.ConstraintsDictType() + +julia> CTModels.Building.__constraint!(ocp_constraints, :state, 3, 2, 1; rg=1:2, lb=[-1.0, -1.0], ub=[1.0, 1.0], label=:x_box); ``` """ function __constraint!( @@ -267,11 +269,15 @@ Add a constraint to a pre-model. See [`CTModels.Building.__constraint!`](@ref) f - `ub`: The upper bound of the constraint. It can be a number or a vector. - `label`: The label of the constraint. It must be unique in the pre-model. -# Example +# Examples ```julia-repl julia> using CTModels -julia> ocp = PreModel(); constraint!(ocp, :control, rg=1:2, lb=[0.0], ub=[1.0], label=:control_constraint) +julia> ocp = CTModels.PreModel(); CTModels.variable!(ocp, 0); CTModels.time!(ocp; t0=0, tf=1); + +julia> CTModels.state!(ocp, 2); CTModels.control!(ocp, 2); + +julia> CTModels.constraint!(ocp, :control; rg=1:2, lb=[-1.0, -1.0], ub=[1.0, 1.0], label=:u_box); ``` # Throws diff --git a/src/Building/control.jl b/src/Building/control.jl index 7abe5491..e053d00f 100644 --- a/src/Building/control.jl +++ b/src/Building/control.jl @@ -18,26 +18,24 @@ This function sets the control dimension and optionally allows specifying the co ```julia-repl julia> using CTModels -julia> ocp = PreModel(); control!(ocp, 1) -julia> control_dimension(ocp) -1 -julia> control_components(ocp) -["u"] - -julia> ocp = PreModel(); control!(ocp, 1, "v") -julia> control_components(ocp) +julia> ocp = CTModels.PreModel(); CTModels.control!(ocp, 1); + +julia> CTModels.control_dimension(ocp), CTModels.control_components(ocp) +(1, ["u"]) + +julia> ocp = CTModels.PreModel(); CTModels.control!(ocp, 1, "v"); + +julia> CTModels.control_components(ocp) ["v"] -julia> ocp = PreModel(); control!(ocp, 2) -julia> control_components(ocp) +julia> ocp = CTModels.PreModel(); CTModels.control!(ocp, 2); + +julia> CTModels.control_components(ocp) ["u₁", "u₂"] -julia> ocp = PreModel(); control!(ocp, 2, :v) -julia> control_components(ocp) -["v₁", "v₂"] +julia> ocp = CTModels.PreModel(); CTModels.control!(ocp, 2, "v", ["a", "b"]); -julia> ocp = PreModel(); control!(ocp, 2, "v", ["a", "b"]) -julia> control_components(ocp) +julia> CTModels.control_components(ocp) ["a", "b"] ``` diff --git a/src/Building/dynamics.jl b/src/Building/dynamics.jl index 1ab79c80..92b8b148 100644 --- a/src/Building/dynamics.jl +++ b/src/Building/dynamics.jl @@ -1,23 +1,25 @@ """ $(TYPEDSIGNATURES) -Set the full dynamics of the optimal control problem `ocp` using the function `f`. +Set the full dynamics of the optimal control problem `ocp` using the in-place function `f`. + +The dynamics have the signature `f!(r, t, x, u, v)` where `r` is the output buffer (filled +in-place), `t` is the time, `x` the state, `u` the control (or `nothing` for control-free +problems), and `v` the optimisation variable. # Arguments - `ocp::PreModel`: The optimal control problem being defined. -- `f::Function`: A function that defines the complete system dynamics. - -# Preconditions -- The state and times must be set before calling this function. -- Control is **optional**: problems without control input (dimension 0) are supported. -- No dynamics must have been set previously. +- `f::Function`: In-place function `f!(r, t, x, u, v)` defining the complete dynamics. -# Behavior -This function assigns `f` as the complete dynamics of the system. It throws an error -if the state or times are not yet set, or if dynamics have already been set. +# Returns +- `Nothing` # Throws -- `Exceptions.PreconditionError`: If called out of order or in an invalid state. +- `Exceptions.PreconditionError`: If state has not been set yet. +- `Exceptions.PreconditionError`: If times have not been set yet. +- `Exceptions.PreconditionError`: If dynamics have already been set. + +See also: [`CTModels.Building.objective!`](@ref), [`CTModels.Building.time_dependence!`](@ref). """ function dynamics!(ocp::PreModel, f::Function)::Nothing Core.@ensure __is_state_set(ocp) Exceptions.PreconditionError( @@ -48,43 +50,27 @@ end """ $(TYPEDSIGNATURES) -Add a partial dynamics function `f` to the optimal control problem `ocp`, applying to the -subset of state indices specified by the range `rg`. +Add a partial dynamics function for a range of state indices in `ocp`. + +The partial right-hand side fills `r[1:length(rg)]` (local buffer view). Ranges must tile +`1:n` without overlap; completeness is verified by [`CTModels.Building.build`](@ref) via +[`CTModels.Building.__is_dynamics_complete`](@ref). # Arguments - `ocp::PreModel`: The optimal control problem being defined. -- `rg::AbstractRange{<:Int}`: Range of state indices to which `f` applies. -- `f::Function`: A function describing the dynamics over the specified state indices. - -# Preconditions -- The state and times must be set before calling this function. -- Control is **optional**: problems without control input (dimension 0) are supported. -- The full dynamics must not yet be complete. -- No overlap is allowed between `rg` and existing dynamics index ranges. - -# Behavior -This function appends the tuple `(rg, f)` to the list of partial dynamics. It ensures -that the specified indices are not already covered and that the system is in a valid -configuration for adding partial dynamics. - -# Throws -- `Exceptions.PreconditionError`: If the state or times are not yet set -- `Exceptions.PreconditionError`: If the dynamics are already defined completely -- `Exceptions.PreconditionError`: If any index in `rg` overlaps with an existing dynamics range -- `Exceptions.IncorrectArgument`: If an index in `rg` is out of bounds +- `rg::AbstractRange{<:Int}`: State index range covered by `f`. +- `f::Function`: In-place function `f!(r, t, x, u, v)` updating `r[1:length(rg)]`. # Returns - `Nothing` -See also: [`CTModels.Building.time_dependence!`](@ref), [`CTModels.Building.objective!`](@ref), [`CTModels.Building.constraint!`](@ref). - -# Example -```julia-repl -julia> using CTModels +# Throws +- `Exceptions.PreconditionError`: If state or times have not been set yet. +- `Exceptions.PreconditionError`: If complete dynamics have already been set. +- `Exceptions.PreconditionError`: If `rg` overlaps with an existing dynamics range. +- `Exceptions.IncorrectArgument`: If any index in `rg` is out of bounds. -julia> ocp = PreModel(); dynamics!(ocp, 1:2, (out, t, x, u, v) -> out .= x[1:2] .+ u[1:2]) -julia> dynamics!(ocp, 3:3, (out, t, x, u, v) -> out .= x[3] * v[1]) -``` +See also: [`CTModels.Building.dynamics!`](@ref), [`CTModels.Building.objective!`](@ref). """ function dynamics!(ocp::PreModel, rg::AbstractRange{<:Int}, f::Function)::Nothing Core.@ensure __is_state_set(ocp) Exceptions.PreconditionError( @@ -160,37 +146,22 @@ end """ $(TYPEDSIGNATURES) -Define partial dynamics for a single state variable index in an optimal control problem. +Convenience wrapper: add partial dynamics for a single state index `i`. -This is a convenience method for defining dynamics affecting only one element of the state vector. It wraps the scalar index `i` into a range `i:i` and delegates to the general partial dynamics method. +Equivalent to `CTModels.Building.dynamics!(ocp, i:i, f)`. # Arguments - `ocp::PreModel`: The optimal control problem being defined. -- `i::Integer`: The index of the state variable to which the function `f` applies. -- `f::Function`: A function of the form `(out, t, x, u, v) -> ...`, which updates the scalar output `out[1]` in-place. - -# Behavior -This is equivalent to calling: -```julia-repl -julia> dynamics!(ocp, i:i, f) -``` - -# Throws -- `Exceptions.PreconditionError`: If the model is not properly initialized -- `Exceptions.PreconditionError`: If the index `i` overlaps with existing dynamics -- `Exceptions.PreconditionError`: If a full dynamics function is already defined -- `Exceptions.IncorrectArgument`: If the index `i` is out of bounds - -# Example -```julia-repl -julia> using CTModels - -julia> ocp = PreModel(); dynamics!(ocp, 3, (out, t, x, u, v) -> out[1] = x[3]^2 + u[1]) -``` +- `i::Integer`: State index covered by `f`. +- `f::Function`: In-place function `f!(r, t, x, u, v)` updating `r[1]`. # Returns - `Nothing` +# Throws +- `Exceptions.PreconditionError`: If state, times, or dynamics preconditions are violated. +- `Exceptions.IncorrectArgument`: If `i` is out of bounds. + See also: [`CTModels.Building.dynamics!`](@ref) (range-based version). """ function dynamics!(ocp::PreModel, i::Integer, f::Function)::Nothing @@ -200,37 +171,18 @@ end """ $(TYPEDSIGNATURES) -Build a combined dynamics function from multiple parts. +Build a single combined in-place dynamics function from ordered partial parts. -This function constructs an in-place dynamics function `dyn!` by composing several sub-functions, each responsible for updating a specific segment of the output vector. +Used internally by [`CTModels.Building.build`](@ref) after all partial dynamics calls +have been collected. Each part function updates its assigned slice of the output vector +via a `@view`, avoiding copies. # Arguments -- `parts::Vector{<:Tuple{<:AbstractRange{<:Int}, <:Function}}`: - A vector of tuples, where each tuple contains: - - A range specifying the indices in the output vector `val` that the corresponding function updates. - - A function `f` with the signature `(output_segment, t, x, u, v)`, which updates the slice of `val` indicated by the range. +- `parts::Vector{<:Tuple{<:AbstractRange{<:Int},<:Function}}`: Ordered vector of + `(range, f!)` pairs; each `f!(r, t, x, u, v)` fills `r` = `view(val, range)`. # Returns -- `dyn!`: A function with signature `(val, t, x, u, v)` that updates the full output vector `val` in-place by applying each part function to its assigned segment. - -# Details -- The returned `dyn!` function calls each part function with a view of `val` restricted to the assigned range. This avoids unnecessary copying and allows efficient updates of sub-vectors. -- Each part function is expected to modify its output segment in-place. - -# Example -```julia-repl -# Define two sub-dynamics functions -julia> f1(out, t, x, u, v) = out .= x[1:2] .+ u[1:2] -julia> f2(out, t, x, u, v) = out .= x[3] * v - -# Combine them into one dynamics function affecting different parts of the output vector -julia> parts = [(1:2, f1), (3:3, f2)] -julia> dyn! = __build_dynamics_from_parts(parts) - -val = zeros(3) -julia> dyn!(val, 0.0, [1.0, 2.0, 3.0], [0.5, 0.5], 2.0) -julia> println(val) # prints [1.5, 2.5, 6.0] -``` +- `Function`: Combined `dyn!(val, t, x, u, v)` that applies all parts in order. """ function __build_dynamics_from_parts( parts::Vector{<:Tuple{<:AbstractRange{<:Int},<:Function}} diff --git a/src/Building/name_validation.jl b/src/Building/name_validation.jl index 7fe40d7a..78f9faa3 100644 --- a/src/Building/name_validation.jl +++ b/src/Building/name_validation.jl @@ -16,10 +16,11 @@ Returns a vector containing: # Example ```julia-repl -julia> ocp = PreModel() -julia> state!(ocp, 2, "x", ["x₁", "x₂"]) -julia> control!(ocp, 1, "u") -julia> __collect_used_names(ocp) +julia> using CTModels + +julia> ocp = CTModels.PreModel(); CTModels.state!(ocp, 2, "x", ["x₁", "x₂"]); CTModels.control!(ocp, 1, "u"); + +julia> CTModels.Building.__collect_used_names(ocp) 4-element Vector{String}: "x" "x₁" @@ -81,12 +82,14 @@ excluding the component's own current names from the check. # Example ```julia-repl -julia> ocp = PreModel() -julia> state!(ocp, 2, "x", ["x₁", "x₂"]) -julia> __has_name_conflict(ocp, "x", :none) +julia> using CTModels + +julia> ocp = CTModels.PreModel(); CTModels.state!(ocp, 2, "x", ["x₁", "x₂"]); + +julia> CTModels.Building.__has_name_conflict(ocp, "x", :none) true -julia> __has_name_conflict(ocp, "y", :none) +julia> CTModels.Building.__has_name_conflict(ocp, "y", :none) false ``` @@ -141,9 +144,11 @@ Performs comprehensive validation: # Example ```julia-repl -julia> ocp = PreModel() -julia> state!(ocp, 2, "x", ["x₁", "x₂"]) -julia> __validate_name_uniqueness(ocp, "x", ["u"], :control) # Would throw if "x" conflicts +julia> using CTModels + +julia> ocp = CTModels.PreModel(); CTModels.state!(ocp, 2, "x", ["x₁", "x₂"]); + +julia> CTModels.Building.__validate_name_uniqueness(ocp, "y", ["u"], :control) # "y" is unique: succeeds ``` See also: [`CTModels.Building.__has_name_conflict`](@ref), [`CTModels.Building.__collect_used_names`](@ref). diff --git a/src/Building/objective.jl b/src/Building/objective.jl index 60ea9bde..156fb5b6 100644 --- a/src/Building/objective.jl +++ b/src/Building/objective.jl @@ -22,13 +22,14 @@ Set the objective of the optimal control problem. ```julia-repl julia> using CTModels -julia> function mayer(x0, xf, v) - return x0[1] + xf[1] + v[1] - end -julia> function lagrange(t, x, u, v) - return x[1] + u[1] + v[1] - end -julia> ocp = PreModel(); objective!(ocp, :min, mayer=mayer, lagrange=lagrange) +julia> ocp = CTModels.PreModel() + +julia> CTModels.state!(ocp, 1); CTModels.control!(ocp, 1); CTModels.variable!(ocp, 1); CTModels.time!(ocp; t0=0, tf=1); + +julia> mayer(x0, xf, v) = x0[1] + xf[1] + v[1] +julia> lagrange(t, x, u, v) = x[1] + u[1] + v[1] + +julia> CTModels.objective!(ocp, :min; mayer=mayer, lagrange=lagrange); ``` # Throws diff --git a/src/Building/state.jl b/src/Building/state.jl index 3f86f9c2..653d06f0 100644 --- a/src/Building/state.jl +++ b/src/Building/state.jl @@ -22,24 +22,24 @@ names: ```julia-repl julia> using CTModels -julia> ocp = PreModel(); state!(ocp, 1); +julia> ocp = CTModels.PreModel(); CTModels.state!(ocp, 1); -julia> state_dimension(ocp), state_components(ocp) +julia> CTModels.state_dimension(ocp), CTModels.state_components(ocp) (1, ["x"]) -julia> ocp = PreModel(); state!(ocp, 2); +julia> ocp = CTModels.PreModel(); CTModels.state!(ocp, 2); -julia> state_dimension(ocp), state_components(ocp) +julia> CTModels.state_dimension(ocp), CTModels.state_components(ocp) (2, ["x₁", "x₂"]) -julia> ocp = PreModel(); state!(ocp, 2, "y"); +julia> ocp = CTModels.PreModel(); CTModels.state!(ocp, 2, "y"); -julia> state_dimension(ocp), state_components(ocp) +julia> CTModels.state_dimension(ocp), CTModels.state_components(ocp) (2, ["y₁", "y₂"]) -julia> ocp = PreModel(); state!(ocp, 2, "y", ["u", "v"]); +julia> ocp = CTModels.PreModel(); CTModels.state!(ocp, 2, "y", ["u", "v"]); -julia> state_dimension(ocp), state_components(ocp) +julia> CTModels.state_dimension(ocp), CTModels.state_components(ocp) (2, ["u", "v"]) ``` diff --git a/src/Building/time_dependence.jl b/src/Building/time_dependence.jl index 5004bf75..d3d478cc 100644 --- a/src/Building/time_dependence.jl +++ b/src/Building/time_dependence.jl @@ -3,30 +3,27 @@ $(TYPEDSIGNATURES) Set the time dependence of the optimal control problem `ocp`. +Must be called exactly once, after declaring the spaces and dynamics but before +calling [`CTModels.Building.build`](@ref). + # Arguments - `ocp::PreModel`: The optimal control problem being defined. -- `autonomous::Bool`: Indicates whether the system is autonomous (`true`) or time-dependent (`false`). - -# Preconditions -- The time dependence must not have been set previously. +- `autonomous::Bool`: `true` for an autonomous system ``\\dot{x}=f(x,u,v)``, + `false` for a non-autonomous system ``\\dot{x}=f(t,x,u,v)``. -# Behavior -This function sets the `autonomous` field of the model to indicate whether the system's dynamics -explicitly depend on time. It can only be called once. +# Returns +- `Nothing` # Throws -- `Exceptions.PreconditionError`: If the time dependence has already been set. +- `Exceptions.PreconditionError`: If time dependence has already been set. -# Example +# Examples ```julia-repl julia> using CTModels -julia> ocp = PreModel(); time_dependence!(ocp; autonomous=true) +julia> ocp = CTModels.PreModel(); CTModels.time_dependence!(ocp; autonomous=true); ``` -# Returns -- `Nothing` - See also: [`CTModels.Building.time!`](@ref), [`CTModels.Building.dynamics!`](@ref). """ function time_dependence!(ocp::PreModel; autonomous::Bool)::Nothing diff --git a/src/Building/times.jl b/src/Building/times.jl index 98512011..c5db4091 100644 --- a/src/Building/times.jl +++ b/src/Building/times.jl @@ -24,24 +24,20 @@ When a time is free, then, one must provide the corresponding index of the ocp v ```julia-repl julia> using CTModels -julia> ocp = PreModel(); time!(ocp; t0=0, tf=1) # Fixed t0 and fixed tf +julia> ocp = CTModels.PreModel(); CTModels.variable!(ocp, 0); CTModels.time!(ocp; t0=0, tf=1) # Fixed t0 and fixed tf -julia> ocp = PreModel(); time!(ocp; t0=0, indf=2) # Fixed t0 and free tf +julia> ocp = CTModels.PreModel(); CTModels.variable!(ocp, 2); CTModels.time!(ocp; t0=0, indf=2) # Fixed t0 and free tf -julia> ocp = PreModel(); time!(ocp; ind0=2, tf=1) # Free t0 and fixed tf - -julia> ocp = PreModel(); time!(ocp; ind0=2, indf=3) # Free t0 and free tf +julia> ocp = CTModels.PreModel(); CTModels.variable!(ocp, 2); CTModels.time!(ocp; ind0=1, tf=1) # Free t0 and fixed tf ``` When a solution is plotted, the name of the time variable appears (`"t"` by default). To name the time variable `"s"`: ```julia-repl -julia> using CTModels - -julia> ocp = PreModel(); time!(ocp; t0=0, tf=1, time_name="s") # time_name as a String +julia> ocp = CTModels.PreModel(); CTModels.variable!(ocp, 0); CTModels.time!(ocp; t0=0, tf=1, time_name="s") # time_name as a String -julia> ocp = PreModel(); time!(ocp; t0=0, tf=1, time_name=:s) # time_name as a Symbol +julia> ocp = CTModels.PreModel(); CTModels.variable!(ocp, 0); CTModels.time!(ocp; t0=0, tf=1, time_name=:s) # time_name as a Symbol ``` # Throws diff --git a/src/Building/variable.jl b/src/Building/variable.jl index aec489a0..f3bd7142 100644 --- a/src/Building/variable.jl +++ b/src/Building/variable.jl @@ -18,8 +18,9 @@ This function registers a named variable (e.g. "state", "control", or other) to ```julia-repl julia> using CTModels -julia> ocp = PreModel(); variable!(ocp, 1, "v") -julia> ocp = PreModel(); variable!(ocp, 2, "v", ["v₁", "v₂"]) +julia> ocp = CTModels.PreModel(); CTModels.variable!(ocp, 1, "v"); + +julia> ocp = CTModels.PreModel(); CTModels.variable!(ocp, 2, "v", ["v₁", "v₂"]); ``` # Throws diff --git a/src/Solutions/build_solution.jl b/src/Solutions/build_solution.jl index ce8fa345..7e0fd19b 100644 --- a/src/Solutions/build_solution.jl +++ b/src/Solutions/build_solution.jl @@ -1229,19 +1229,17 @@ Return the time grid for a specific component. # Returns - `TimesDisc`: The time grid for the specified component -# Behavior -- For `UnifiedTimeGridModel`: Returns the unique time grid for any component -- For `MultipleTimeGridModel`: Returns the specific time grid for the component +Returns the unique time grid for any component (all components share the same grid). # Throws -- `IncorrectArgument`: If component is not one of the valid symbols +- `IncorrectArgument`: If component is not one of the valid symbols. # Examples ```julia-repl -julia> time_grid(sol, :state) # Works for both unified and multiple grids -julia> time_grid(sol, :control) # Works for both unified and multiple grids -julia> time_grid(sol, :costate) # Maps to :state grid -julia> time_grid(sol, :dual) # Maps to :path grid +julia> CTModels.time_grid(sol, :state) # unified grid +julia> CTModels.time_grid(sol, :control) # same unified grid +julia> CTModels.time_grid(sol, :costate) # :costate → :state mapping +julia> CTModels.time_grid(sol, :dual) # :dual → :path mapping ``` """ function Components.time_grid( @@ -1296,14 +1294,14 @@ Return the time grid for a specific component in solutions with multiple time gr - `TimesDisc`: The time grid for the specified component # Throws -- `IncorrectArgument`: If component is not one of the valid symbols +- `IncorrectArgument`: If component is not one of the valid symbols. # Examples ```julia-repl -julia> time_grid(sol, :state) # Get state time grid -julia> time_grid(sol, :control) # Get control time grid -julia> time_grid(sol, :costate) # Maps to state time grid -julia> time_grid(sol, :dual) # Maps to path time grid +julia> CTModels.time_grid(sol, :state) # state time grid +julia> CTModels.time_grid(sol, :control) # control time grid +julia> CTModels.time_grid(sol, :costate) # :costate → :state +julia> CTModels.time_grid(sol, :dual) # :dual → :path ``` """ function Components.time_grid( diff --git a/src/Solutions/solution_types.jl b/src/Solutions/solution_types.jl index 82dcd527..c82923f5 100644 --- a/src/Solutions/solution_types.jl +++ b/src/Solutions/solution_types.jl @@ -91,15 +91,11 @@ Construct a `MultipleTimeGridModel` with keyword arguments for each component ti # Example ```julia-repl -julia> T_state = LinRange(0, 1, 101) -julia> T_control = LinRange(0, 1, 51) -julia> T_costate = LinRange(0, 1, 76) -julia> mtgm = MultipleTimeGridModel( - state=T_state, - control=T_control, - costate=T_costate, - path=T_state -) +julia> using CTModels + +julia> T_s = LinRange(0, 1, 101); T_u = LinRange(0, 1, 51); T_p = LinRange(0, 1, 76); + +julia> mtgm = CTModels.MultipleTimeGridModel(state=T_s, control=T_u, costate=T_p, path=T_s); ``` """ function MultipleTimeGridModel(; @@ -123,25 +119,14 @@ $(TYPEDSIGNATURES) Clean and standardize component symbols for time grid access. -# Behavior -- Maps all component symbols to their canonical time grid: `:state`, `:control`, `:costate`, or `:path`. -- `:costate`, `:costates` map to `:costate` (costate has its own grid). -- `:dual`, `:duals`, `:constraint`, `:constraints`, `:cons` map to `:path`. -- `:state_box_constraint(s)` maps to `:state`. -- `:control_box_constraint(s)` maps to `:control`. -- Removes duplicate symbols. +Maps `:states`→`:state`, `:duals`→`:path`, `:costate`→`:costate`, etc. +Duplicates are removed and the result is a canonical `Tuple{Symbol...}`. # Arguments -- `description`: A tuple of symbols passed by the user, typically from time grid access. +- `description`: Tuple of component symbols from the caller. # Returns -- A cleaned `Tuple{Symbol...}` of unique, standardized symbols. - -# Example -```julia-repl -julia> clean_component_symbols((:states, :controls, :costate, :constraint, :duals)) -# → (:state, :control, :costate, :path) -``` +- `Tuple{Symbol...}`: Cleaned, unique, canonical symbols. """ function clean_component_symbols(description) # map all component symbols to their canonical time grid