Skip to content

Building a model

build turns a fully-declared, mutable PreModel into an immutable Model. build_model is an alias of build.

Prerequisites

Before building, the pre-model must be consistent: times, state, complete dynamics, objective and the time-dependence flag must all be set. The check is __is_consistent; build raises a structured error if a piece is missing. Control and variable are optional — their absence is represented by the EmptyControlModel / EmptyVariableModel sentinels.

Two extra declarations complete the problem:

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)

ocp = CTModels.build(pre)
The (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.

What a built Model guarantees

Building freezes the structure and exposes it through accessors. Because every component is a concrete typed field, queries are type-stable and need no closure inspection:

julia
julia> CTModels.state_dimension(ocp)
2

julia> CTModels.control_dimension(ocp)
1

julia> CTModels.variable_dimension(ocp)
0

julia> CTModels.initial_time(ocp)
0.0

julia> CTModels.final_time(ocp)
1.0

julia> CTModels.is_autonomous(ocp)
true

julia> CTModels.has_lagrange_cost(ocp)
true

julia> CTModels.has_abstract_definition(ocp)
false

julia> CTModels.is_abstractly_defined(ocp)
false

julia> CTModels.isempty_constraints(ocp)
true

The optional ExaModels builder can be retrieved with 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:

julia
incomplete = CTModels.PreModel()
CTModels.state!(incomplete, 1)
julia
julia> CTModels.build(incomplete)        # no times, dynamics, objective…
PreconditionError  #build#20, build.jl:461

│  Times must be set before building model

│  Reason   time horizon has not been defined yet

│  Context  build function - times validation
│  Hint     Call times!(pre_ocp, t0, tf) or times!(pre_ocp, N) before building
└─

A pre-model that has everything except the time_dependence! call is also rejected:

julia
pre_no_td = CTModels.PreModel()
CTModels.variable!(pre_no_td, 0)
CTModels.time!(pre_no_td; t0=0.0, tf=1.0)
CTModels.state!(pre_no_td, 1)
CTModels.control!(pre_no_td, 1)
CTModels.dynamics!(pre_no_td, (r, t, x, u, v) -> (r[1] = u[1]; nothing))
CTModels.objective!(pre_no_td, :min; lagrange=(t, x, u, v) -> u[1]^2)
julia
julia> CTModels.build(pre_no_td)          # time_dependence! not called
PreconditionError  #build#20, build.jl:491

│  Time dependence must be set before building model

│  Reason   autonomous status has not been defined yet

│  Context  build function - time dependence validation
│  Hint     Call time_dependence!(pre_ocp, autonomous=true/false) before building
└─

Where build sits in the pipeline

PreModel ──validate──► consistent? ──freeze──► Model
   │                       │                      │
state!/…              __is_consistent        immutable, typed
time_dependence!      name uniqueness        accessor surface
definition! (opt.)    dynamics complete

The immutable Model is the object every downstream package consumes: it is the input to Solutions, Initial guesses and Serialization.