control-toolbox / CTBase.jl

Fundamentals of the control-toolbox ecosystem
http://control-toolbox.org/CTBase.jl/
MIT License
12 stars 3 forks source link

Save / load OCP solution #144

Open PierreMartinon opened 5 months ago

PierreMartinon commented 5 months ago

Hi @ocots @jbcaillau,

While working on a GUI and also following a request from a user (Frank), I did a first version of save / load features. The idea is simply to be able to store an OCP solution on disk after a solve, and conversely to load an existing file. The main difficulty is that an OCP solution is a rather elaborate object, containing interpolated functions in particular. This complicates the export in a generic text-like format. Also, data i/o in Julia is still evoving quite a bit. For now I have 2 formats:

I put the first draft in CTDirect but this will eventually move to CTBase since the OCP solution is there. Usage looks like this

# save / load solution in JLD2 format
save_OCP_solution(sol, filename_prefix="solution_test")
sol4 = load_OCP_solution("solution_test")
plot(sol4, show=true)
println(sol.objective == sol4.objective)

# save / load discrete solution in JSON format
# NB. we recover here a JSON Object...
save_OCP_solution(sol, filename_prefix="solution_test", format="JSON")
sol_disc_reloaded = load_OCP_solution("solution_test", format="JSON")
println(sol.objective == sol_disc_reloaded.objective)

[^1]: this may be straightforward[^2] since I had already split the OCP Solution constructor in two: one called with the standard ipopt solution, and an internal one that takes raw vectors (T,X,U, multipliers etc) from a parsed solution. I'll try to see if we can use this second constructor starting from the JSON object containing the interpolated solution.

[^2]: (footnote test) Actually a bit more work is needed on the split to make the raw constructor even more generic, since it currently still takes the DOCP. it should be feasible as it is mostly for dimensions, although some boolean indicators regarding the constraints type are also used. I guess we can go back to manually checking for 'nothing' values in unused fields. I'll need to think a bit about this.

PierreMartinon commented 5 months ago

Questions, remarks, suggestions. Don't be shy.

Edit: footnote does not seem to work properly

ocots commented 5 months ago

I have a stagiaire making a webapp for our toolbox. Maybe we can brainstorm.

jbcaillau commented 5 months ago

Questions, remarks, suggestions. Don't be shy. Nice @PierreMartinon not much to say right now

Edit: footnote does not seem to work properly footnote corrected 🙂 [^1]

[^1]: works like this

PierreMartinon commented 5 months ago

Thanks for the footnote help :D

Forgot to add a nice suggestion from Olivier: have several methods for the solution constructor depending on the available info in context, such as the ocp.

Also the JSON3 version is more an export than a save since the data is not identical (vectors vs functions), so I'll probably split the two at some point.

PierreMartinon commented 5 months ago

FYI, the two formats have been split, now we have

The JLD2 part should be ready to move to CTBase.

ocots commented 3 months ago

@PierreMartinon Is it done or note? Can we close the issue?

PierreMartinon commented 3 months ago

Both the load/save in JLD2 and the export/read in JSON (a more limited discrete version of the solution) are in the CTDirectExt package extension. Some tests in test_misc.jl

I propose we wait a bit for the move into CTBase, in particular I'd like a standard function to discretize an OptimalControlSolution for text-like formats. Ideally we would find a way to get the exact discrete solution from the DOCP if possible & wanted: currently we start by interpolating the discrete solution, so even if we re-discretize later it would not necessarily be identical.

Maybe add optional vector arguments in OptimalControlSolution that would save the original discrete variables if available (eg direct method) and left empty if not ? Exporting a discrete solution could then choose between this original one if present, or re-discretize along a given grid.

ocots commented 3 months ago

@PierreMartinon Do you need more than:

T = sol.times         # T = [t0, t1, ...]
X = sol.state.(T)     # X = [x0, x1, ...]
U = sol.control.(T)
P = sol.costate.(T)

and

x = ctinterpolate(T, ...)
p = ctinterpolate(T, ...)
u = ctinterpolate(T, ...)
jbcaillau commented 3 months ago

@ocots I guess that ctinterpolate uses Interpolation.jl and is not exported?

ocots commented 3 months ago

ctinterpolate:

https://github.com/control-toolbox/CTBase.jl/blob/eda78721b75475024cdd427e03e2ba172080d640/src/utils.jl#L151

It is exported.

Actually, it is a linear interpolation with a linear extrapolation.

jbcaillau commented 3 months ago

ctinterpolate:

https://github.com/control-toolbox/CTBase.jl/blob/eda78721b75475024cdd427e03e2ba172080d640/src/utils.jl#L151

It is exported.

Actually, it is a linear interpolation with a linear extrapolation.

should be kept internal according to me. minor point, though.

PierreMartinon commented 3 months ago

@PierreMartinon Do you need more than:

T = sol.times         # T = [t0, t1, ...]
X = sol.state.(T)     # X = [x0, x1, ...]
U = sol.control.(T)
P = sol.costate.(T)

and

x = ctinterpolate(T, ...)
p = ctinterpolate(T, ...)
u = ctinterpolate(T, ...)

You lost me there :D

What I meant is, we currently have the functions state, control, costate, and I'd like to have additional vector state_d, control_d, costate_d, to store discrete trajectories. This discrete part would be used when exporting to JSON text format, instead of the discrete solution struct I currently use.

In the case of direct methods these vectors would store the original discrete solution, and in other cases it would simply default to apply the functions to a given time grid.

Ah, while we're on OptimalControlSolution, here is the list of keys for the different constraints and multipliers (other than costate), since these vectors and functions are currently saved in the infos dictionary of the solution. Maybe we could add explicit fields in the struct instead of dumping everything in the dictionary ? Note: I chose to return functions (ie interpolate) for everything related to path constraints, including 'boxes' on state and control variables, and keep vectors for boundary conditions and optimization variables constraints.

Vectors: :boundary_constraints, :mult_boundary_constraints, :variable_constraints, :mult_variable_constraints, :mult_variable_box_lower, :mult_variable_box_upper

Functions: :control_constraints, :mult_control_constraints, :state_constraints, :mult_state_constraints, :mixed_constraints, :mult_mixed_constraints, :mult_state_box_lower, :mult_state_box_upper, :mult_control_box_lower, :mult_control_box_upper

PierreMartinon commented 3 months ago

@jbcaillau On a slightly related topic, when building the boundary constraints in the OCP model, could we set the ordering to always be initial conditions first, followed by final conditions ? I noticed this is not necessarily the case while testing the constraints/multipliers parsing.

PierreMartinon commented 2 months ago

Updated the JSON part.

At some point we'll probably move these 4 functions from CTDirectExt to an extension for CTBase, since this is about manipulating OCP solutions.

Note: importing a solution in JSON (text) format requires the corresponding OCP as well, in practice for dimensions, and also because our OptimalControlSolution contains a copy of the OCP. And the OCP is too complex to be saved in text format.

ocots commented 2 months ago

There is no copy of the ocp inside an OptimalControlSolution:

https://github.com/control-toolbox/CTBase.jl/blob/76194968f5a33cdb047e94d5eb8b99f6d3c988fd/src/optimal_control_solution-type.jl#L20

PierreMartinon commented 2 months ago

There is no copy of the ocp inside an OptimalControlSolution:

https://github.com/control-toolbox/CTBase.jl/blob/76194968f5a33cdb047e94d5eb8b99f6d3c988fd/src/optimal_control_solution-type.jl#L20

Oh you're right, I read too quickly. So we only use the ocp to retrieve the block at line 45 here: https://github.com/control-toolbox/CTBase.jl/blob/76194968f5a33cdb047e94d5eb8b99f6d3c988fd/src/optimal_control_solution-setters.jl ?

If yes we could add a method taking a named tuple or something similar for this block instead of the ocp. The constructor with the ocp would call this new one, passing the values from the ocp in the tuple. And the new method could be called when the full ocp is not available, eg when reading from a json, since this data block could be saved as text !

@ocots Can you confirm this or did I miss something ?

ocots commented 2 months ago

Yes indeed, only for that:

    # data from ocp 
    sol.initial_time_name = ocp.initial_time_name
    sol.final_time_name = ocp.final_time_name
    sol.time_name = ocp.time_name
    sol.control_dimension = ocp.control_dimension
    sol.control_components_names = ocp.control_components_names
    sol.control_name = ocp.control_name
    sol.state_dimension = ocp.state_dimension
    sol.state_components_names = ocp.state_components_names
    sol.state_name = ocp.state_name
    sol.variable_dimension = ocp.variable_dimension
    sol.variable_components_names = ocp.variable_components_names
    sol.variable_name = ocp.variable_name
ocots commented 2 months ago

You can make an issue (Developers), create a branch from the issue and make a PR if you want :-)