Displaying models and solutions
The CTModels.Display module provides Base.show extensions that render Model and 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
Displaying a built Model
When you print a 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:
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 modelThe (autonomous) optimal control problem is of the form:
minimize J(x, u) = ∫ f⁰(x(t), u(t)) dt, over [0.0, 1.0]
subject to
ẋ(t) = f(x(t), u(t)), t in [0.0, 1.0] a.e.,
ϕ₋ ≤ ϕ(x(0.0), x(1.0)) ≤ ϕ₊,
x₋ ≤ x(t) ≤ x₊,
u₋ ≤ u(t) ≤ u₊,
where x(t) ∈ R² and u(t) ∈ R.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 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:
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 formThe (autonomous) optimal control problem is of the form:
minimize J(x, u) = ∫ f⁰(x(t), u(t)) dt, over [0.0, 1.0]
subject to
ẋ(t) = f(x(t), u(t)), t in [0.0, 1.0] a.e.,
where x(t) ∈ R and u(t) ∈ R.An empty PreModel produces no output:
CTModels.PreModel() # nothing is printedAbstract (symbolic) definitions
If a Definition has been attached to the model via 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), this section is skipped silently.
Displaying component types
The same display conventions are used by the concrete component types that make up a model. The types are normally inspected through their public accessors, while show provides a compact visual summary of their structure:
julia> state_model = CTModels.Components.StateModel("x", ["x₁", "x₂"])
StateModel
├─ name: x
├─ dimension: 2
└─ components: x₁, x₂
julia> CTModels.Components.name(state_model)
"x"
julia> CTModels.Components.components(state_model)
2-element Vector{String}:
"x₁"
"x₂"
julia> CTModels.Components.dimension(state_model)
2state_modelStateModel
├─ name: x
├─ dimension: 2
└─ components: x₁, x₂A solution component indicates that its value is a callable trajectory without expanding the closure. The value and metadata remain available through accessors:
julia> state_solution = CTModels.Components.StateModelSolution("x", ["x₁"], t -> 2t)
StateModelSolution
├─ name: x
├─ dimension: 1
├─ components: x₁
└─ value: <callable>
julia> CTModels.Components.value(state_solution)(0.5)
1.0
julia> CTModels.Components.name(state_solution)
"x"
julia> CTModels.Components.dimension(state_solution)
1state_solutionStateModelSolution
├─ name: x
├─ dimension: 1
├─ components: x₁
└─ value: <callable>Time models, objectives, and constraints follow the same pattern. Their summaries expose semantic fields while the accessors provide the programmatic interface:
julia> times_model = CTModels.Components.TimesModel(
CTModels.Components.FixedTimeModel(0.0, "t₀"),
CTModels.Components.FreeTimeModel(2, "tf"),
"t",
)
TimesModel
├─ initial: FixedTimeModel(name=t₀, time=0.0)
├─ final: FreeTimeModel(name=tf, index=2)
└─ time_name: t
julia> CTModels.Components.initial(times_model)
FixedTimeModel
├─ name: t₀
└─ time: 0.0
julia> CTModels.Components.final(times_model)
FreeTimeModel
├─ name: tf
└─ index: 2
julia> CTModels.Components.initial_time_name(times_model)
"t₀"
julia> CTModels.Components.final_time_name(times_model)
"tf"times_modelTimesModel
├─ initial: FixedTimeModel(name=t₀, time=0.0)
├─ final: FreeTimeModel(name=tf, index=2)
└─ time_name: tjulia> objective_model = CTModels.Components.MayerObjectiveModel((x0, xf, v) -> xf[1], :min)
MayerObjectiveModel
├─ criterion: min
└─ mayer: <callable>
julia> CTModels.Components.criterion(objective_model)
:min
julia> CTModels.Components.mayer(objective_model)
#14 (generic function with 1 method)objective_modelMayerObjectiveModel
├─ criterion: min
└─ mayer: <callable>For solution support objects, the display summarizes grid sizes, optional solver metadata, and dual presence rather than printing all numerical data:
julia> grid = CTModels.UnifiedTimeGridModel(collect(range(0.0, 1.0; length=5)))
UnifiedTimeGridModel
├─ points: 5
├─ first: 0.0
└─ last: 1.0
julia> infos = CTModels.SolverInfos(12, :first_order, "converged", true, 1e-8, Dict{Symbol,Any}())
SolverInfos
├─ status: first_order
├─ successful: true
├─ iterations: 12
├─ message: converged
└─ constraints violation: 1.0e-8
julia> grid
UnifiedTimeGridModel
├─ points: 5
├─ first: 0.0
└─ last: 1.0
julia> infos
SolverInfos
├─ status: first_order
├─ successful: true
├─ iterations: 12
├─ message: converged
└─ constraints violation: 1.0e-8The corresponding accessors can be used without relying on the stored fields:
CTModels.time_grid_model(
CTModels.build_solution(
ocp,
collect(range(0.0, 1.0; length=3)),
zeros(3, 2),
zeros(3, 1),
Float64[],
zeros(3, 2);
objective=0.0,
successful=true,
),
)UnifiedTimeGridModel
├─ points: 3
├─ first: 0.0
└─ last: 1.0Plotting solutions
The Display module registers a RecipesBase.plot method for AbstractSolution. Without Plots.jl loaded, calling it throws an ExtensionError:
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))
endUndefVarErrorWhen Plots.jl is loaded, the CTModelsPlots extension provides full plot recipes. See Plotting for details.
See also
Building a model — how to assemble a
PreModeland callbuild.Plotting — plot recipes for solutions.
Model— API reference for the model type.PreModel— API reference for the pre-model type.