Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions docs/make.jl
Original file line number Diff line number Diff line change
Expand Up @@ -79,6 +79,7 @@ with_api_reference(src_dir, ext_dir) do api_pages
"Dynamics & Objective" => "model/dynamics_objective.md",
"Constraints" => "model/constraints.md",
"Building a Model" => "model/building.md",
"Displaying Models" => "model/display.md",
],
"Solutions" => [
"Overview" => "solution/overview.md",
Expand Down
1 change: 1 addition & 0 deletions docs/src/getting-started.md
Original file line number Diff line number Diff line change
Expand Up @@ -143,6 +143,7 @@ init = CTModels.build_initial_guess(ocp,
| 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) |
| Displaying models in the REPL | [Displaying Models](model/display.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/overview.md) |
Expand Down
1 change: 1 addition & 0 deletions docs/src/index.md
Original file line number Diff line number Diff line change
Expand Up @@ -157,6 +157,7 @@ The documentation consists of:
control-toolbox contributors:
- [Optimal control problems](model/overview.md): the `PreModel → build → Model` pipeline,
types & traits, components, dynamics, objective, constraints.
- [Displaying models](model/display.md): `Base.show` and mathematical rendering of models.
- [Solutions](solution/overview.md): the `Solution` anatomy, time grids, trajectories, duals.
- [Initial guesses](initial_guess/overview.md): the `Init` pipeline and warm-starting.
- [Serialization & extensions](serialization/overview.md): export/import and plotting.
Expand Down
15 changes: 15 additions & 0 deletions docs/src/initial_guess/formats.md
Original file line number Diff line number Diff line change
Expand Up @@ -94,3 +94,18 @@ init.control(0.25)

How the dimensions are checked, and how to warm-start from a previous solution, is covered in
[Validation & warm-start](validation.md).

## Per-component constructors

Behind the convenience of `build_initial_guess`, each component is built by a dedicated
constructor that normalises the input to a callable:

| Constructor | Purpose |
|---|---|
| [`initial_state`](@ref CTModels.Init.initial_state) | normalise a state guess (function, vector, scalar, `nothing`, or `(T, X)` grid pair) |
| [`initial_control`](@ref CTModels.Init.initial_control) | normalise a control guess (same shapes) |
| [`initial_variable`](@ref CTModels.Init.initial_variable) | normalise a variable guess (scalar, vector, or `nothing`) |

When `nothing` is passed, each constructor returns a built-in default (constant `0.1` for
non-empty components, `Float64[]` for empty ones). A 2-tuple `(time_grid, data_matrix)` is
interpreted as **grid data** that gets interpolated linearly in time.
5 changes: 5 additions & 0 deletions docs/src/initial_guess/overview.md
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,11 @@ raw data ──► pre_initial_guess / initial_guess ──► InitialGuess (unv
that does both: it accepts many input formats, converts them, and validates dimensions against
the [`Model`](@ref CTModels.Models.Model).

The type hierarchy mirrors the pipeline:
[`AbstractInitialGuess`](@ref CTModels.Init.AbstractInitialGuess) → [`InitialGuess`](@ref CTModels.Init.InitialGuess)
and
[`AbstractPreInitialGuess`](@ref CTModels.Init.AbstractPreInitialGuess) → [`PreInitialGuess`](@ref CTModels.Init.PreInitialGuess).

## Reading order

| Page | Topic | Key symbols |
Expand Down
7 changes: 7 additions & 0 deletions docs/src/model/building.md
Original file line number Diff line number Diff line change
Expand Up @@ -53,8 +53,15 @@ CTModels.initial_time(ocp)
CTModels.final_time(ocp)
CTModels.is_autonomous(ocp)
CTModels.has_lagrange_cost(ocp)
CTModels.has_abstract_definition(ocp)
CTModels.is_abstractly_defined(ocp)
CTModels.isempty_constraints(ocp)
```

The optional ExaModels builder can be retrieved with
[`get_build_examodel`](@ref CTModels.Models.get_build_examodel); it returns `nothing`
when no builder was attached.

Missing a required piece is caught at build time. Here state is set but times, dynamics, objective and the time-dependence flag are all missing:

```@example building
Expand Down
13 changes: 12 additions & 1 deletion docs/src/model/components.md
Original file line number Diff line number Diff line change
Expand Up @@ -78,7 +78,9 @@ CTModels.final_time(pre2.times, [0.0, 1.5])
```

For a free time the value is read from ``v``, hence
[`final_time`](@ref CTModels.Components.final_time) takes the variable vector. See
[`final_time`](@ref CTModels.Components.final_time) takes the variable vector. The
companion accessor [`initial_time`](@ref CTModels.Components.initial_time) works the
same way. See
[Types and traits](types_and_traits.md) for the `Fixed`/`Free` distinction.

## Naming rules
Expand Down Expand Up @@ -129,3 +131,12 @@ end # hide

These rules guarantee that a label like `:a` resolves unambiguously to one component when
reading a solution or a constraint.

## Symbolic definitions

A [`Definition`](@ref CTModels.Components.Definition) wraps a Julia `Expr` that captures the
original problem definition (e.g. from a macro-based DSL). The wrapped expression is
retrieved with [`expression`](@ref CTModels.Components.expression); an
[`EmptyDefinition`](@ref CTModels.Components.EmptyDefinition) returns an empty block
`:(begin end)`. Use [`definition!`](@ref CTModels.Building.definition!) on the `PreModel`
to attach one before building.
14 changes: 14 additions & 0 deletions docs/src/model/constraints.md
Original file line number Diff line number Diff line change
Expand Up @@ -59,6 +59,20 @@ CTModels.dim_control_constraints_box(ocp)
CTModels.dim_variable_constraints_box(ocp)
```

The raw constraint tuples can also be retrieved:

```@repl cons
CTModels.path_constraints_nl(ocp)
CTModels.boundary_constraints_nl(ocp)
CTModels.state_constraints_box(ocp)
CTModels.control_constraints_box(ocp)
CTModels.variable_constraints_box(ocp)
```

Use [`isempty_constraints`](@ref CTModels.Models.isempty_constraints) to check whether the
model has any constraints at all, and [`constraint`](@ref CTModels.Models.constraint) to
retrieve a single constraint by label.

## Labels and aliases

Before building, constraints live in a [`ConstraintsDictType`](@ref CTModels.Components.ConstraintsDictType)
Expand Down
144 changes: 144 additions & 0 deletions docs/src/model/display.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,144 @@
# Displaying models and solutions

```@meta
CurrentModule = CTModels
```

The `CTModels.Display` module provides `Base.show` extensions that render
[`Model`](@ref CTModels.Models.Model) and [`PreModel`](@ref CTModels.Building.PreModel)
objects in a human-readable mathematical format. It also hosts a
`RecipesBase.plot` stub that is specialised by the `CTModelsPlots` extension
when `Plots.jl` is loaded.

## Setup

```@setup display
using CTModels
```

## Displaying a built Model

When you print a [`Model`](@ref CTModels.Models.Model) in the REPL (or any
`IO` context with `MIME"text/plain"`), CTModels renders the OCP in standard
mathematical notation — objective, dynamics, constraints, and variable spaces:

```@example display
pre = CTModels.PreModel()

CTModels.variable!(pre, 0)
CTModels.time!(pre; t0=0.0, tf=1.0)
CTModels.state!(pre, 2)
CTModels.control!(pre, 1)

function dynamics!(r, t, x, u, v)
r[1] = x[2]
r[2] = u[1]
return nothing
end
CTModels.dynamics!(pre, dynamics!)

CTModels.objective!(pre, :min; lagrange=(t, x, u, v) -> u[1]^2)

function 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=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)

CTModels.time_dependence!(pre; autonomous=true)
ocp = CTModels.build(pre)

ocp # displays the model
```

The output includes:

- An **(autonomous)** or **(non autonomous)** qualifier.
- The **objective** `J(x, u) = …` with Mayer and/or Lagrange terms.
- The **dynamics** `ẋ(t) = f(t, x(t), u(t))`.
- **Constraint lines** for path, boundary, and box constraints (only those
that are present).
- A **where** clause listing the state, control, and variable spaces.

## Displaying a PreModel

A [`PreModel`](@ref CTModels.Building.PreModel) can be displayed at any stage
of construction. If the problem is not yet consistent (missing components,
incomplete declarations), only the abstract definition — if any — is shown.
Once the pre-model is consistent, the full mathematical formulation is
rendered:

```@example display
pre2 = CTModels.PreModel()
CTModels.variable!(pre2, 0)
CTModels.time!(pre2; t0=0.0, tf=1.0)
CTModels.state!(pre2, 1)
CTModels.control!(pre2, 1)
CTModels.dynamics!(pre2, (r, t, x, u, v) -> (r[1] = u[1]; return nothing))
CTModels.objective!(pre2, :min; lagrange=(t, x, u, v) -> u[1]^2)
CTModels.time_dependence!(pre2; autonomous=true)

pre2 # consistent PreModel displays the mathematical form
```

An empty `PreModel` produces no output:

```@example display
CTModels.PreModel() # nothing is printed
```

## Abstract (symbolic) definitions

If a [`Definition`](@ref CTModels.Components.Definition) has been attached to
the model via [`definition!`](@ref CTModels.Building.definition!), its
symbolic expression is printed under an "Abstract definition:" header before
the mathematical formulation. This is useful when the OCP originates from a
macro-based DSL (e.g. OptimalControl.jl) that stores the original user code.

When no definition is set ([`EmptyDefinition`](@ref CTModels.Components.EmptyDefinition)),
this section is skipped silently.

## Plotting solutions

The `Display` module registers a `RecipesBase.plot` method for
[`AbstractSolution`](@ref CTModels.Solutions.AbstractSolution). Without
`Plots.jl` loaded, calling it throws an `ExtensionError`:

```@example display
using CTModels
sol = CTModels.build_solution(
ocp,
collect(range(0.0, 1.0; length=10)),
zeros(10, 2),
zeros(10, 1),
Float64[],
zeros(10, 2);
objective=0.0,
iterations=0,
constraints_violation=0.0,
message="",
status=:dummy,
successful=true,
)

try
CTModels.plot(sol)
catch e
println(typeof(e))
end
```

When `Plots.jl` is loaded, the `CTModelsPlots` extension provides full plot
recipes. See [Plotting](@ref) for details.

## See also

- [Building a model](building.md) — how to assemble a `PreModel` and call `build`.
- [Plotting](../serialization/plotting.md) — plot recipes for solutions.
- [`Model`](@ref CTModels.Models.Model) — API reference for the model type.
- [`PreModel`](@ref CTModels.Building.PreModel) — API reference for the pre-model type.
1 change: 1 addition & 0 deletions docs/src/model/overview.md
Original file line number Diff line number Diff line change
Expand Up @@ -35,6 +35,7 @@ The OCP layer is organised by responsibility across four modules:
| [Dynamics and objective](dynamics_objective.md) | The equations of motion and cost | [`dynamics!`](@ref CTModels.Building.dynamics!), [`objective!`](@ref CTModels.Building.objective!) |
| [Constraints](constraints.md) | Path, boundary and box constraints | [`constraint!`](@ref CTModels.Building.constraint!) |
| [Building a model](building.md) | Freezing the `PreModel` | [`build`](@ref CTModels.Building.build), [`Model`](@ref CTModels.Models.Model) |
| [Displaying models](display.md) | `Base.show` and mathematical rendering | `Model`, `PreModel` display |

## Qualified access

Expand Down
3 changes: 3 additions & 0 deletions docs/src/model/types_and_traits.md
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,7 @@ The pattern is uniform: a *definition* type (structure only) and a *solution* ty
| [`AbstractVariableModel`](@ref CTModels.Components.AbstractVariableModel) | [`VariableModel`](@ref CTModels.Components.VariableModel) | [`VariableModelSolution`](@ref CTModels.Components.VariableModelSolution) | [`EmptyVariableModel`](@ref CTModels.Components.EmptyVariableModel) |
| [`AbstractTimeModel`](@ref CTModels.Components.AbstractTimeModel) | [`FixedTimeModel`](@ref CTModels.Components.FixedTimeModel) / [`FreeTimeModel`](@ref CTModels.Components.FreeTimeModel) | — | — |
| [`AbstractObjectiveModel`](@ref CTModels.Components.AbstractObjectiveModel) | [`MayerObjectiveModel`](@ref CTModels.Components.MayerObjectiveModel) / [`LagrangeObjectiveModel`](@ref CTModels.Components.LagrangeObjectiveModel) / [`BolzaObjectiveModel`](@ref CTModels.Components.BolzaObjectiveModel) | — | — |
| [`AbstractConstraintsModel`](@ref CTModels.Components.AbstractConstraintsModel) | [`ConstraintsModel`](@ref CTModels.Components.ConstraintsModel) | — | — |
| [`AbstractDefinition`](@ref CTModels.Components.AbstractDefinition) | [`Definition`](@ref CTModels.Components.Definition) | — | [`EmptyDefinition`](@ref CTModels.Components.EmptyDefinition) |

The **empty sentinel** lets dispatch stay total: a control-free problem carries an
Expand Down Expand Up @@ -103,6 +104,8 @@ exposing the concrete type:
|---|---|
| Is ``t_0`` fixed / free? | [`has_fixed_initial_time`](@ref CTModels.Components.has_fixed_initial_time) / [`has_free_initial_time`](@ref CTModels.Components.has_free_initial_time) |
| Is ``t_f`` fixed / free? | [`has_fixed_final_time`](@ref CTModels.Components.has_fixed_final_time) / [`has_free_final_time`](@ref CTModels.Components.has_free_final_time) |
| Is ``t_0`` fixed / free? (predicate form) | [`is_initial_time_fixed`](@ref CTModels.Components.is_initial_time_fixed) / [`is_initial_time_free`](@ref CTModels.Components.is_initial_time_free) |
| Is ``t_f`` fixed / free? (predicate form) | [`is_final_time_fixed`](@ref CTModels.Components.is_final_time_fixed) / [`is_final_time_free`](@ref CTModels.Components.is_final_time_free) |

```@repl types
CTModels.has_fixed_initial_time(ocp)
Expand Down
6 changes: 4 additions & 2 deletions docs/src/serialization/export_import.md
Original file line number Diff line number Diff line change
Expand Up @@ -82,7 +82,9 @@ mechanism used when [building a solution](../solution/trajectories.md).

!!! note "Low-level tags"

The format keyword dispatches internally on the tags `CTModels.JSON3Tag()` and
`CTModels.JLD2Tag()`. The extension methods
The format keyword dispatches internally on the tags
[`JLD2Tag`](@ref CTModels.Serialization.JLD2Tag) and
[`JSON3Tag`](@ref CTModels.Serialization.JSON3Tag), both subtypes of
[`AbstractTag`](@ref CTModels.Serialization.AbstractTag). The extension methods
`export_ocp_solution(CTModels.JLD2Tag(), sol; …)` etc. are what each `ext/` file
implements; the `format=` wrapper is the public, dependency-free entry point.
38 changes: 37 additions & 1 deletion docs/src/solution/duals.md
Original file line number Diff line number Diff line change
Expand Up @@ -72,6 +72,40 @@ The whole path multiplier is reached directly through the
CTModels.path_constraints_dual(sol)(0.5)
```

Boundary-constraint multipliers are read with
[`boundary_constraints_dual`](@ref CTModels.Solutions.boundary_constraints_dual):

```@repl duals
CTModels.boundary_constraints_dual(sol)
```

### Box-constraint duals

Box constraints on state, control, and variable components have separate lower-bound
and upper-bound dual accessors:

| Accessor | Returns |
|---|---|
| [`state_constraints_lb_dual`](@ref CTModels.Solutions.state_constraints_lb_dual) | state lower-bound duals |
| [`state_constraints_ub_dual`](@ref CTModels.Solutions.state_constraints_ub_dual) | state upper-bound duals |
| [`control_constraints_lb_dual`](@ref CTModels.Solutions.control_constraints_lb_dual) | control lower-bound duals |
| [`control_constraints_ub_dual`](@ref CTModels.Solutions.control_constraints_ub_dual) | control upper-bound duals |
| [`variable_constraints_lb_dual`](@ref CTModels.Solutions.variable_constraints_lb_dual) | variable lower-bound duals |
| [`variable_constraints_ub_dual`](@ref CTModels.Solutions.variable_constraints_ub_dual) | variable upper-bound duals |

Their dimensions are queried with
[`dim_dual_state_constraints_box`](@ref CTModels.Solutions.dim_dual_state_constraints_box),
[`dim_dual_control_constraints_box`](@ref CTModels.Solutions.dim_dual_control_constraints_box),
and
[`dim_dual_variable_constraints_box`](@ref CTModels.Solutions.dim_dual_variable_constraints_box).

### Checking for duals

A solution produced by a solver carries duals; a solution built by a flow does not.
Use [`has_duals`](@ref CTModels.Solutions.has_duals) to test this — when it returns
`false`, the dual model is an [`EmptyDualModel`](@ref CTModels.Solutions.EmptyDualModel)
and all dual accessors return `nothing`.

!!! note "Box duals are indexed per component"

For state/control/variable **box** constraints, the stored duals are indexed by *primal
Expand All @@ -92,5 +126,7 @@ CTModels.successful(sol)
CTModels.constraints_violation(sol)
```

`infos(sol)` returns the `Dict{Symbol,Any}` of any extra solver-specific data. These fields
The original [`Model`](@ref CTModels.Models.Model) can be retrieved from a solution with
[`model`](@ref CTModels.Solutions.model), and `infos(sol)` returns the `Dict{Symbol,Any}`
of any extra solver-specific data. These fields
are what [Serialization](../serialization/overview.md) writes to disk alongside the trajectories.
5 changes: 3 additions & 2 deletions docs/src/solution/overview.md
Original file line number Diff line number Diff line change
Expand Up @@ -82,11 +82,12 @@ CTModels.successful(sol)

| Field group | Accessor(s) | Stored as |
|---|---|---|
| time grid | [`time_grid`](@ref CTModels.Components.time_grid) | [`AbstractTimeGridModel`](@ref CTModels.Solutions.AbstractTimeGridModel) |
| time grid | [`time_grid`](@ref CTModels.Components.time_grid), `is_empty`, [`is_empty_time_grid`](@ref CTModels.Solutions.is_empty_time_grid) | [`AbstractTimeGridModel`](@ref CTModels.Solutions.AbstractTimeGridModel) |
| state / control / costate | [`state`](@ref CTModels.Components.state), [`control`](@ref CTModels.Components.control), [`costate`](@ref CTModels.Components.costate) | callables `t → …` |
| variable / objective | [`variable`](@ref CTModels.Components.variable), [`objective`](@ref CTModels.Components.objective) | value |
| duals | [`dual`](@ref CTModels.Solutions.dual), [`DualModel`](@ref CTModels.Solutions.DualModel) | callables / vectors |
| duals | [`dual`](@ref CTModels.Solutions.dual), [`DualModel`](@ref CTModels.Solutions.DualModel), [`has_duals`](@ref CTModels.Solutions.has_duals) | callables / vectors |
| diagnostics | [`iterations`](@ref CTModels.Solutions.iterations), [`status`](@ref CTModels.Solutions.status), [`successful`](@ref CTModels.Solutions.successful) | [`SolverInfos`](@ref CTModels.Solutions.SolverInfos) |
| model | [`model`](@ref CTModels.Solutions.model) | [`Model`](@ref CTModels.Models.Model) |

Each accessor dispatches on a typed field, so reading a solution never inspects raw
closures. The following pages take each group in turn.
7 changes: 7 additions & 0 deletions docs/src/solution/time_grids.md
Original file line number Diff line number Diff line change
Expand Up @@ -94,3 +94,10 @@ CTModels.clean_component_symbols((:states, :controls, :costate, :constraint, :du

This is why box-constraint duals share the state/control grids, and path-constraint duals the
`:path` grid — the mapping is centralised in one place rather than scattered across accessors.

## Checking for emptiness

`is_empty` returns `true` for an
[`EmptyTimeGridModel`](@ref CTModels.Solutions.EmptyTimeGridModel) and `false` otherwise.
The convenience [`is_empty_time_grid`](@ref CTModels.Solutions.is_empty_time_grid) calls
`is_empty` on the solution's time grid model directly.
Loading