Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Save / load OCP solution #144

Open
PierreMartinon opened this issue Jun 5, 2024 · 18 comments
Open

Save / load OCP solution #144

PierreMartinon opened this issue Jun 5, 2024 · 18 comments
Assignees
Labels
enhancement New feature or request

Comments

@PierreMartinon
Copy link
Member

PierreMartinon commented Jun 5, 2024

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:

  • native julia JLD2: here we save/load directly the julia object. The main drawback is that the format cannot really be used outside of julia (eg matlab or whatever other environment).
  • text based JSON: this one is much more portable, but here we don't save the original (functional) OCP solution, rather the interpolated data over the time grid. I added a simplified 'interpolated solution' struct to handle this, and ideally we can add a new constructor1 to recreate a functional OCP from this one, aiming to be as close as possible to the original OCP solution before saving.

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)

Footnotes

  1. this may be straightforward2 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
Copy link
Member Author

PierreMartinon commented Jun 5, 2024

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

Edit: footnote does not seem to work properly

@ocots
Copy link
Member

ocots commented Jun 5, 2024

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

@jbcaillau
Copy link
Member

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

Footnotes

  1. works like this

@PierreMartinon
Copy link
Member Author

PierreMartinon commented Jun 6, 2024

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.

@ocots ocots mentioned this issue Jun 15, 2024
13 tasks
@PierreMartinon
Copy link
Member Author

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

  • a load/save in julia JLD2 format, of the whole OCP solution
  • an export/read in JSON of a discretized version of the solution. Exporting outside of Julia is likely the most useful one, although we should be able to reconstruct a proper OCP solution from this if we provide some additional info...

The JLD2 part should be ready to move to CTBase.

@ocots
Copy link
Member

ocots commented Aug 13, 2024

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

@PierreMartinon
Copy link
Member Author

PierreMartinon commented Aug 14, 2024

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
Copy link
Member

ocots commented Aug 14, 2024

@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
Copy link
Member

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

@ocots
Copy link
Member

ocots commented Aug 15, 2024

ctinterpolate:

return Interpolations.linear_interpolation(x, f, extrapolation_bc=Interpolations.Line())

It is exported.

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

@jbcaillau
Copy link
Member

ctinterpolate:

return Interpolations.linear_interpolation(x, f, extrapolation_bc=Interpolations.Line())

It is exported.

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

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

@jbcaillau jbcaillau reopened this Aug 15, 2024
@PierreMartinon
Copy link
Member Author

@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
Copy link
Member Author

@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
Copy link
Member Author

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
Copy link
Member

ocots commented Aug 27, 2024

There is no copy of the ocp inside an OptimalControlSolution:

@with_kw mutable struct OptimalControlSolution <: AbstractOptimalControlSolution

@PierreMartinon
Copy link
Member Author

There is no copy of the ocp inside an OptimalControlSolution:

@with_kw mutable struct OptimalControlSolution <: AbstractOptimalControlSolution

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
Copy link
Member

ocots commented Aug 28, 2024

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
Copy link
Member

ocots commented Aug 28, 2024

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

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
enhancement New feature or request
Projects
None yet
Development

No branches or pull requests

3 participants