Skip to content

Data: typed function wrappers

The CTBase.Data submodule provides typed wrappers that carry a Julia function together with its trait metadata (see the Traits guide). Each wrapper knows, at the type level, whether it depends on time, whether it depends on an extra variable, and whether it is evaluated in-place.

Overview

TypeMathematical objectOut-of-place callIn-place
Data.VectorField  X([t, ]x[, v])
Data.Hamiltonian  H([t, ]x, p[, v])
Data.PseudoHamiltonian   H̃([t, ]x, p, u[, v])
Data.ComposedHamiltonian  H([t, ]x, p[, v])
Data.HamiltonianVectorField  HVF([t, ]x, p[, v])
Data.ControlledVectorField   fc([t, ]x, u[, v])
Data.ComposedVectorField  g([t, ]x[, v])
Data.ControlLaw  u([t, ]⋯[, v])
Data.PathConstraint  g([t, ]⋯[, v])
Data.Multiplier  μ([t, ]x, p[, v])

The mathematical spaces are:

  •   — the state space ( state dimensions).

  •   — the control space ( control dimensions).

  •    — the cotangent bundle (state + costate, dimensions).

  • — the optimisation variable space ( variable dimensions; absent when fixed).

  • — scalars; — constraint values ( constraint dimensions).

  • — the domain depends on an additional trait (feedback or constraint-kind).

In the call patterns, brackets […] denote optional arguments controlled by traits:

  • t is present when is_autonomous=false (non-autonomous).

  • v is present when is_variable=true (non-fixed).

  • depends on an additional trait: feedback for ControlLaw (see below), constraint-kind for PathConstraint (see below).

All ten share the same time-dependence and variable-dependence trait axes. VectorField and HamiltonianVectorField also carry a mutability trait (in-place / out-of-place); ControlLaw carries a feedback trait; PathConstraint carries a constraint-kind trait (see Traits).


VectorField

A VectorField wraps a Julia function representing the dynamics  .

The natural out-of-place call is X([t, ]x[, v]), where t and v are optional arguments controlled by the time-dependence and variable-dependence traits. A uniform call X(t, x, v) always works regardless of traits (unused arguments are ignored). In-place is supported: the derivative buffer dx is prepended, giving X!(dx, [t, ]x[, v]).

Construction

julia
using CTBase.Data, CTBase.Traits

# Autonomous, fixed (default): X(x)
vf1 = Data.VectorField(x -> -x)

# Non-autonomous, fixed: X(t, x)
vf2 = Data.VectorField((t, x) -> t .* x; is_autonomous=false)

# Autonomous, non-fixed: X(x, v)
vf3 = Data.VectorField((x, v) -> x .* v; is_variable=true)

# Non-autonomous, non-fixed: X(t, x, v)
vf4 = Data.VectorField((t, x, v) -> t .* x .+ v; is_autonomous=false, is_variable=true)

vf1
VectorField: autonomous, fixed (no variable), out-of-place
  natural call: f(x)
  uniform call: f(t, x, v)

The show output summarises the traits and both call signatures (natural and uniform).

In-place construction

Prefer out-of-place for clarity. Use in-place when avoiding allocations matters:

julia
# In-place Autonomous/Fixed: f(dx, x) — mutability auto-detected
vf_ip = Data.VectorField((dx, x) -> (dx .= -x; nothing))
Traits.mutability(vf_ip)
CTBase.Traits.InPlace

If the function has multiple methods, auto-detection fails; pass is_inplace explicitly:

julia
f_multi(x) = -x
f_multi(dx, x) = (dx .= -x; nothing)

vf_explicit = Data.VectorField(f_multi; is_inplace=true)
VectorField: autonomous, fixed (no variable), in-place
  natural call: f(dx, x)
  uniform call: f(dx, t, x, v)

Calling

julia
x0 = [1.0, 0.5]

vf1(x0)                    # natural call
vf1(0.0, x0, nothing)      # uniform call (ignores t and v)
vf2(0.5, x0)               # non-autonomous natural call
2-element Vector{Float64}:
 0.5
 0.25

Hamiltonian

A Hamiltonian wraps a scalar function  .

The natural call is H([t, ]x, p[, v]), where t and v are optional arguments controlled by the time-dependence and variable-dependence traits. A uniform call H(t, x, p, v) always works regardless of traits. In-place is not supported (scalar return value).

Construction

julia
# Autonomous, fixed (default): H(x, p)
h1 = Data.Hamiltonian((x, p) -> dot(p, x))

# Non-autonomous, fixed: H(t, x, p)
h2 = Data.Hamiltonian((t, x, p) -> t * dot(p, x); is_autonomous=false)

# Autonomous, non-fixed: H(x, p, v)
h3 = Data.Hamiltonian((x, p, v) -> dot(p, x) * v[1]; is_variable=true)

h1
Hamiltonian: autonomous, fixed (no variable)
  natural call: h(x, p)
  uniform call: h(t, x, p, v)

Calling

julia
x0, p0 = [1.0, 0.5], [0.3, 0.7]

h1(x0, p0)                # natural call
h1(0.0, x0, p0, nothing)  # uniform call
h2(0.5, x0, p0)           # non-autonomous
0.32499999999999996

HamiltonianVectorField

A HamiltonianVectorField wraps the map     when the derivatives are provided explicitly (no automatic differentiation).

The natural out-of-place call is HVF([t, ]x, p[, v]), where t and v are optional arguments controlled by the time-dependence and variable-dependence traits. A uniform call HVF(t, x, p, v) always works regardless of traits. In-place is supported: the derivative buffers dx, dp are prepended, giving HVF!(dx, dp, [t, ]x, p[, v]).

Use this when the Hamiltonian equations are known analytically. When only the scalar Hamiltonian is available and the derivatives must be obtained by automatic differentiation, use Data.Hamiltonian instead.

Construction

julia
# Harmonic oscillator H = (x²+p²)/2:  ẋ = p, ṗ = -x
hvf = Data.HamiltonianVectorField((x, p) -> (p, -x))

# With time dependence
hvf_na = Data.HamiltonianVectorField((t, x, p) -> (p, -x .* t); is_autonomous=false)

hvf
HamiltonianVectorField: autonomous, fixed (no variable), out-of-place
  natural call: f(x, p)
  uniform call: f(t, x, p, v)

Calling

julia
dx, dp = hvf(x0, p0)   # natural call: returns (ẋ, ṗ)
(dx, dp)
([0.3, 0.7], [-1.0, -0.5])

The variable_costate keyword

For non-fixed Hamiltonian vector fields, the call accepts an optional variable_costate keyword (default false).

When false (default), the out-of-place call returns (dx, dp); when true, it returns the extended tuple (dx, dp, dpv) where dpv = ∂ṗ/∂v is the derivative of the costate equations with respect to the variable v. For in-place fields, dpv is passed as an optional pre-allocated buffer (dpv=nothing skips the computation):

julia
# Inner function that implements the variable_costate path.
# H(x, p, v) = v[1] * dot(p, x)  →  ẋ = v[1]*x,  ṗ = -v[1]*p,  ∂ṗ/∂v[1] = -p
function f_vc(x, p, v; variable_costate=false)
    dx = v[1] .* x
    dp = -v[1] .* p
    variable_costate ? (dx, dp, -p) : (dx, dp)
end

hvf_nf = Data.HamiltonianVectorField(f_vc; is_variable=true)
v0 = [2.0]

dx, dp = hvf_nf(x0, p0, v0)                            # returns (ẋ, ṗ)
(dx, dp)
([2.0, 1.0], [-0.6, -1.4])
julia
dx, dp, dpv = hvf_nf(x0, p0, v0; variable_costate=true)  # returns (ẋ, ṗ, ∂ṗ/∂v)
(dx, dp, dpv)
([2.0, 1.0], [-0.6, -1.4], [-0.3, -0.7])

For in-place non-fixed fields the signature is:

julia
dpv = zeros(length(p0))
hvf_ip_nf(dx, dp, x, p, v; dpv=dpv, variable_costate=true)  # fills dx, dp, dpv in place

This is only supported by inner functions that implement the variable_costate keyword path — typically those derived from a Data.Hamiltonian by automatic differentiation. For a plain user-supplied function that does not accept variable_costate, passing true raises a CTBase.Exceptions.PreconditionError (see the Exceptions guide).


PseudoHamiltonian

A PseudoHamiltonian wraps a scalar function   that extends the standard Hamiltonian with an explicit control argument .

The natural call is H̃([t, ]x, p, u[, v]), where t and v are optional arguments controlled by the time-dependence and variable-dependence traits. A uniform call H̃(t, x, p, u, v) always works regardless of traits. In-place is not supported (scalar return value).

Unlike Data.Hamiltonian, which encodes the control implicitly, a pseudo-Hamiltonian takes the control as an additional argument. This enables dynamic closed-loop flows where the control is computed from the pseudo-Hamiltonian's maximisation condition (PMP stationarity:  ).

Construction

julia
# Autonomous, fixed (default): H̃(x, p, u)
ph1 = Data.PseudoHamiltonian((x, p, u) -> dot(p, x) + u^2)

# Non-autonomous, fixed: H̃(t, x, p, u)
ph2 = Data.PseudoHamiltonian((t, x, p, u) -> t * dot(p, x) + u^2; is_autonomous=false)

# Autonomous, non-fixed: H̃(x, p, u, v)
ph3 = Data.PseudoHamiltonian((x, p, u, v) -> dot(p, x) + u^2 + v[1]; is_variable=true)

ph1
PseudoHamiltonian: autonomous, fixed (no variable)
  natural call: h̃(x, p, u)
  uniform call: h̃(t, x, p, u, v)

Calling

julia
x0, p0, u0 = [1.0, 0.5], [0.3, 0.7], 2.0

ph1(x0, p0, u0)                    # natural call
ph1(0.0, x0, p0, u0, nothing)      # uniform call
ph2(0.5, x0, p0, u0)               # non-autonomous
4.325

The dynamics trait of a PseudoHamiltonian is always Traits.HamiltonianDynamics, since pseudo-Hamiltonians involve both state and costate.


ComposedHamiltonian

A ComposedHamiltonian is obtained by eliminating the control from a Data.PseudoHamiltonian with a dynamic closed-loop Data.ControlLaw:

The natural call is H([t, ]x, p[, v]), where the time/variable dependences are the join of the pseudo-Hamiltonian and the control law — NonAutonomous/NonFixed win. A uniform call H(t, x, p, v) always works regardless of traits. In-place is not supported (scalar return value).

It subtypes Data.AbstractHamiltonian, so it is a Hamiltonian and can be used anywhere one is expected.

Construction

julia
# Pseudo-Hamiltonian H̃(x, p, u) = p⋅x + u²  (scalar state/costate/control)
ph = Data.PseudoHamiltonian((x, p, u) -> p * x + u^2)

# Dynamic closed-loop law u(x, p) = -p
law = Data.DynClosedLoop((x, p) -> -p)

# Composed Hamiltonian: H(x, p) = H̃(x, p, -p) = p⋅x + p²
H = Data.ComposedHamiltonian(ph, law)

H
ComposedHamiltonian: autonomous, fixed (no variable)
  natural call: h(x, p)
  uniform call: h(t, x, p, v)

The composed time/variable dependences are the join of the two inputs — NonAutonomous/NonFixed win. A time-varying feedback on an autonomous pseudo-Hamiltonian correctly yields a NonAutonomous composed Hamiltonian.

Calling

julia
x0, p0 = 1.0, 0.5

H(x0, p0)                    # natural call
H(0.0, x0, p0, nothing)      # uniform call
0.75

Getters

julia
Data.pseudo_hamiltonian(H)   # the underlying PseudoHamiltonian
Data.control_law(H)          # the DynClosedLoop law
ControlLaw: dyn-closed-loop, autonomous, fixed (no variable)
  natural call: u(x, p)
  uniform call: u(t, x, p, v)

The constructor rejects non-DynClosedLoop laws (OpenLoop, ClosedLoop) with a MethodError — those are the state-space path (ComposedVectorField).


ControlLaw

A ControlLaw wraps a function that provides the control input for an optimal control problem. The feedback trait (see Traits) determines which primal variables the control law depends on, and the time/variable traits add t and v as for other data types.

Three user-facing constructors fix the feedback trait:

ConstructorFeedback traitOut-of-place call
Data.OpenLoopOpenLoopFeedbacku([t][, v])
Data.ClosedLoopClosedLoopFeedbacku([t, ]x[, v])
Data.DynClosedLoopDynClosedLoopFeedbacku([t, ]x, p[, v])

Construction

julia
# Open-loop, autonomous, fixed (default): u()
u_ol = Data.OpenLoop(() -> 1.0)

# Closed-loop, autonomous, fixed: u(x)
u_cl = Data.ClosedLoop(x -> -x)

# Dynamic closed-loop, autonomous, fixed: u(x, p)
u_dcl = Data.DynClosedLoop((x, p) -> -x - p)

u_ol
ControlLaw: open-loop, autonomous, fixed (no variable)
  natural call: u()
  uniform call: u(t, v)

Non-autonomous and variable variants are constructed with the same keywords as other data types:

julia
u_ol_na = Data.OpenLoop((t, v) -> t * v; is_autonomous=false, is_variable=true)
ControlLaw: open-loop, non-autonomous, variable
  natural call: u(t, v)
  uniform call: u(t, v)

Calling

Every ControlLaw is callable via its natural signature (matching the traits) and via a uniform signature that depends on the feedback trait:

FeedbackNaturalUniform
OpenLoopu([t][, v])u(t, v)
ClosedLoopu([t, ]x[, v])u(t, x, v)
DynClosedLoopu([t, ]x, p[, v])u(t, x, p, v)
julia
u_ol()                         # natural call
u_ol(0.0, nothing)             # uniform call

u_cl(x0)                       # natural call
u_cl(0.0, x0, nothing)         # uniform call

u_dcl(x0, p0)                  # natural call
u_dcl(0.0, x0, p0, nothing)    # uniform call
-1.5

The feedback trait determines the dynamics trait: open-loop and closed-loop control laws carry Traits.StateDynamics, while dynamic closed-loop control laws carry Traits.HamiltonianDynamics.


PathConstraint

A PathConstraint wraps a function that evaluates a path constraint along the trajectory. The constraint-kind trait (see Traits) determines which primal variables the constraint depends on, and the time/variable traits add t and v as for other data types.

Three user-facing constructors fix the constraint kind:

ConstructorConstraint kindOut-of-place call
Data.StateConstraintStateConstraintKindg([t, ]x[, v])
Data.ControlConstraintControlConstraintKindg([t, ]u[, v])
Data.MixedConstraintMixedConstraintKindg([t, ]x, u[, v])

Construction

julia
# State constraint, autonomous, fixed (default): g(x)
g_state = Data.StateConstraint(x -> x[1])

# Control constraint, autonomous, fixed: g(u)
g_ctrl = Data.ControlConstraint(u -> u[1])

# Mixed constraint, autonomous, fixed: g(x, u)
g_mixed = Data.MixedConstraint((x, u) -> x[1] + u[1])

# Non-autonomous state constraint: g(t, x)
g_state_na = Data.StateConstraint((t, x) -> t * x[1]; is_autonomous=false)

g_state
PathConstraint: state, autonomous, fixed (no variable)
  natural call: g(x)
  uniform call: g(t, x, u, v)

Calling

Every PathConstraint is callable via its natural signature (matching the traits) and via a uniform signature g(t, x, u, v) that ignores unused arguments:

julia
x0, u0 = [1.0, 0.5], [2.0]

g_state(x0)                    # natural call
g_state(0.0, x0, u0, nothing)  # uniform call

g_ctrl(u0)                     # natural call
g_mixed(x0, u0)                # natural call
3.0

The constraint kind can be queried with the predicate functions Traits.is_state_constraint, Traits.is_control_constraint, and Traits.is_mixed_constraint:

julia
Traits.is_state_constraint(g_state), Traits.is_control_constraint(g_ctrl), Traits.is_mixed_constraint(g_mixed)
(true, true, true)

Multiplier

A Multiplier wraps a function returning the Lagrange multiplier associated with a path constraint.

The natural call is μ([t, ]x, p[, v]), where t and v are optional arguments controlled by the time-dependence and variable-dependence traits. A uniform call μ(t, x, p, v) always works regardless of traits. In-place is not supported.

It has the same call structure as a Data.Hamiltonian — it depends on the state and costate — but carries no dynamics semantics of its own.

Construction

julia
# Autonomous, fixed (default): μ(x, p)
μ1 = Data.Multiplier((x, p) -> x[1])

# Non-autonomous, fixed: μ(t, x, p)
μ2 = Data.Multiplier((t, x, p) -> t * x[1]; is_autonomous=false)

μ1
Multiplier: autonomous, fixed (no variable)
  natural call: μ(x, p)
  uniform call: μ(t, x, p, v)

Calling

julia
x0, p0 = [1.0, 0.5], [0.3, 0.7]

μ1(x0, p0)                    # natural call
μ1(0.0, x0, p0, nothing)      # uniform call
μ2(0.5, x0, p0)               # non-autonomous natural call
0.5

ControlledVectorField

A ControlledVectorField wraps a function that returns the state derivative with an explicit control argument . It is the state-space analogue of Data.PseudoHamiltonian: where a pseudo-Hamiltonian carries the control alongside the costate, a controlled vector field carries the control alongside the state.

The natural call is fc([t, ]x, u[, v]), where t and v are optional arguments controlled by the time-dependence and variable-dependence traits. A uniform call fc(t, x, u, v) always works regardless of traits. In-place is not supported (always out-of-place).

It carries Traits.StateDynamics.

Construction

julia
# Autonomous, fixed (default): fc(x, u)
fc1 = Data.ControlledVectorField((x, u) -> -x + u)

# Non-autonomous, fixed: fc(t, x, u)
fc2 = Data.ControlledVectorField((t, x, u) -> t * x + u; is_autonomous=false)

# Autonomous, non-fixed: fc(x, u, v)
fc3 = Data.ControlledVectorField((x, u, v) -> v * x + u; is_variable=true)

# Scalar state and control
x0, u0 = 1.0, 2.0

fc1
ControlledVectorField: autonomous, fixed (no variable)
  natural call: fc(x, u)
  uniform call: fc(t, x, u, v)

Calling

julia
fc1(x0, u0)                       # natural call
fc1(0.0, x0, u0, nothing)         # uniform call
fc2(0.5, x0, u0)                  # non-autonomous natural call
2.5

ComposedVectorField

A ComposedVectorField is obtained by eliminating the control from a Data.ControlledVectorField with an open-loop or closed-loop Data.ControlLaw:

where the control is u(t, v) for an open-loop law and u(t, x, v) for a closed-loop law. It is the state-space analogue of Data.ComposedHamiltonian.

The natural call is g([t, ]x[, v]), where the time/variable dependences are the join of the controlled vector field and the control law — NonAutonomous/NonFixed win. A uniform call g(t, x, v) always works regardless of traits. In-place is not supported (always out-of-place).

It subtypes Data.AbstractVectorField with OutOfPlace mutability, so it is a vector field usable anywhere one is expected.

Construction

julia
# Controlled vector field fc(x, u) = -x + u  (scalar state/control)
fc = Data.ControlledVectorField((x, u) -> -x + u)

# Closed-loop law u(x) = -x
law = Data.ClosedLoop(x -> -x)

# Composed vector field: g(x) = fc(x, -x) = -2x
g = Data.ComposedVectorField(fc, law)

g
ComposedVectorField: autonomous, fixed (no variable), out-of-place
  natural call: f(x)
  uniform call: f(t, x, v)

The composed time/variable dependences are the join of the two inputs — NonAutonomous/NonFixed win.

Calling

julia
g(3.0)                     # natural call: g(x) = -2x
g(0.0, 3.0, nothing)       # uniform call (t, x, v)
-6.0

Getters

julia
Data.controlled_vector_field(g)   # the underlying ControlledVectorField
Data.control_law(g)               # the ClosedLoop law
ControlLaw: closed-loop, autonomous, fixed (no variable)
  natural call: u(x)
  uniform call: u(t, x, v)

The constructor rejects DynClosedLoop laws with a MethodError — that is the Hamiltonian path (ComposedHamiltonian).


Querying traits

Because the trait metadata lives in the type, it can be recovered from any data object through the Traits accessors — with no runtime cost:

julia
Traits.time_dependence(vf2)      # NonAutonomous
CTBase.Traits.NonAutonomous
julia
Traits.variable_dependence(vf3)  # NonFixed
CTBase.Traits.NonFixed
julia
Traits.mutability(vf_ip)         # InPlace
CTBase.Traits.InPlace

The boolean predicates follow generically:

julia
Traits.is_autonomous(vf1), Traits.is_variable(vf1), Traits.is_inplace(vf1)
(true, false, false)

Each data family also reports its dynamics trait via Traits.dynamics_trait:

julia
Traits.dynamics_trait(vf1), Traits.dynamics_trait(h1), Traits.dynamics_trait(hvf)
(CTBase.Traits.StateDynamics, CTBase.Traits.HamiltonianDynamics, CTBase.Traits.HamiltonianDynamics)

A VectorField carries Traits.StateDynamics, while both Hamiltonian and HamiltonianVectorField carry Traits.HamiltonianDynamics.


Typed constructors

A lower-level constructor takes the trait types directly (no keyword inference). It is used internally by code that produces typed data objects programmatically:

julia
vf_typed = Data.VectorField(x -> 2x, Traits.Autonomous, Traits.Fixed, Traits.OutOfPlace)
Traits.time_dependence(vf_typed)
CTBase.Traits.Autonomous

The same positional form exists for Data.Hamiltonian (Hamiltonian(f, TD, VD)), Data.PseudoHamiltonian (PseudoHamiltonian(f, TD, VD)), Data.HamiltonianVectorField (HamiltonianVectorField(f, TD, VD, MD)), and Data.ControlLaw (ControlLaw(f, FB, TD, VD)), Data.ControlledVectorField (ControlledVectorField(f, TD, VD)), Data.PathConstraint (PathConstraint(f, K, TD, VD)), Data.Multiplier (Multiplier(f, TD, VD)).


See also