Skip to content

Export & import ​

export_ocp_solution writes a Solution to disk; import_ocp_solution reads it back, given the Model it belongs to. Two formats are available, each behind its extension:

formatExtensionFile
:JLD (default)CTModelsJLD (JLD2)binary .jld2
:JSONCTModelsJSON (JSON)text .json
julia
using CTModels
using JSON      # CTModelsJSON
using JLD2       # CTModelsJLD

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; nothing))
CTModels.objective!(pre, :min; lagrange=(t, x, u, v) -> u^2)
CTModels.time_dependence!(pre; autonomous=true)
ocp = CTModels.build(pre)

N = 101
T = collect(range(0.0, 1.0; length=N))
sol = CTModels.build_solution(ocp, T, hcat(cos.(T), -sin.(T)),
    reshape(-cos.(T), N, 1), Float64[], zeros(N, 2);
    objective=0.5, iterations=10, constraints_violation=1e-9,
    message="ok", status=:optimal, successful=true)

Both formats round-trip ​

The filename is a base path; the extension is appended automatically.

julia
base = joinpath(tempdir(), "ctmodels_demo")

CTModels.export_ocp_solution(sol; filename=base, format=:JSON)
CTModels.export_ocp_solution(sol; filename=base, format=:JLD)

from_json = CTModels.import_ocp_solution(ocp; filename=base, format=:JSON)
from_jld  = CTModels.import_ocp_solution(ocp; filename=base, format=:JLD)
julia
julia> CTModels.objective(from_json)
0.5

julia> CTModels.objective(from_jld)
0.5

The reloaded solution behaves exactly like the original — its trajectories are again callables of time:

julia
x = CTModels.state(from_json)
julia
julia> x(0.5)
2-element Vector{Float64}:
  0.8775825618903728
 -0.479425538604203

Extension appending is idempotent ​

If filename already ends with the target extension (case-insensitively), it is left alone instead of being suffixed a second time:

julia
base_json = base * ".json"
CTModels.export_ocp_solution(sol; filename=base_json, format=:JSON)
julia
julia> isfile(base_json)
true

julia> isfile(base_json * ".json")
false

Importing with the wrong model ​

The file does not contain the model, only a signature of it (dimensions, names, labels of the constraints, fixed or free times, criterion). import_ocp_solution compares it with the model you pass, before rebuilding the solution:

  • if the solution cannot belong to this model (dimension of the state, control or variable, number or labels of the constraints, fixed versus free initial or final time), an IncorrectArgument lists all the differences between the file and the model, and asks for the model the solution was computed from;

  • if the differences cannot make the solution unusable (names of the components, criterion, type of cost, values of fixed times), a warning is logged and the solution is returned.

julia
import_ocp_solution(other_model; filename="solution")
# IncorrectArgument: The solution in the file does not match the model
#   Got       state dimension = 2 (file)
#   Expected  state dimension = 1 (model)
#   Hint      Pass the model the solution was computed from.

Files exported by older versions of CTModels carry no signature: only the dimensions and the time grids can then be checked, and an informational message says so.

How trajectories survive serialization ​

A trajectory is a function t -> x(t), which neither JSON nor JLD2 can store directly. CTModels works around this by resampling: before writing, each trajectory is evaluated on its time grid (the helpers of src/Solutions/discretization_utils.jl), and only the resulting arrays are persisted. On import, the arrays are wrapped back into interpolated callables — the same mechanism used when building a solution.

Low-level tags

The format keyword dispatches internally on the tags JLD2Tag and JSONTag, both subtypes of 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.

Switching structure ​

The switching structure of a solution (switching times, phase labels, left and right limits, see Switching structure) is saved with it, in both formats. When the file is read back, the trajectories are rebuilt phase by phase, so they keep their jumps. A file without switching structure, including one written by an older version, is read with an EmptySwitchingModel.