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

[RTM] New plotting! #887

Merged
merged 17 commits into from
Aug 5, 2024
Merged
Show file tree
Hide file tree
Changes from 16 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
32 changes: 30 additions & 2 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -73,6 +73,8 @@ As a tour guide that uses this binder, you can watch the `bioptim` workshop that

- [OptimalControlProgram](#class-optimalcontrolprogram)
- [NonLinearProgram](#class-nonlinearprogram)
- [VariationalOptimalControlProgram](#class-variationaloptimalcontrolprogram)
- [PlottingServer](#class-plottingserver)

</details>

Expand Down Expand Up @@ -167,6 +169,7 @@ As a tour guide that uses this binder, you can watch the `bioptim` workshop that
- [Solver](#enum-solver)
- [ControlType](#enum-controltype)
- [PlotType](#enum-plottype)
- [OnlineOptim](#enum-onlineoptim)
- [InterpolationType](#enum-interpolationtype)
- [Shooting](#enum-shooting)
- [CostType](#enum-costtype)
Expand Down Expand Up @@ -787,8 +790,17 @@ The `Solver` class can be used to select the nonlinear solver to solve the ocp:

Note that options can be passed to the solver parameter.
One can refer to their respective solver's documentation to know which options exist.
The `show_online_optim` parameter can be set to `True` so the graphs nicely update during the optimization.
It is expected to slow down the optimization a bit.
The `show_online_optim` parameter can be set to `True` so the graphs nicely update during the optimization with the default values.
One can also directly declare `online_optim` as an `OnlineOptim` parameter to customize the behavior of the plotter.
Note that `show_online_optim` and `online_optim` are mutually exclusive.
Please also note that `OnlineOptim.MULTIPROCESS` is not available on Windows and only none of them are available on Macos.
To see how to run the server on Windows, please refer to the `getting_started/pendulum.py` example.
It is expected to slow down the optimization a bit.
`show_options` can be also passed as a dict to the plotter to customize the plotter's behavior.
If `online_optim` is set to `SERVER`, then a server must be started manually by instantiating an `PlottingServer` class (see `ressources/plotting_server.py`).
The following keys are additional options when using `OnlineOptim.SERVER` and `OnlineOptim.MULTIPROCESS_SERVER`:
- `host`: the host to use (default is `localhost`)
- `port`: the port to use (default is `5030`)

Finally, one can save and load previously optimized values by using
```python
Expand Down Expand Up @@ -831,6 +843,12 @@ instead of `x_init` and `x_bounds`.
the OCP, you can access them with `sol.parameters["qdot_start"]` and `sol.parameters["qdot_end"]` at the end of the
optimization.

### Class: PlottingServer
If one wants to use the `OnlineOptim.SERVER` plotter, one can instantiate this class to start a server.
This is not mandatory as if `as_multiprocess` is set to `True` in the `show_options` dict [default behavior], this server is started automatically.
The advantage of starting the server manually is that one can plot online graphs on a remote machine.
An example of such a server is provided in `resources/plotting_server.py`.

## The model

Bioptim is designed to work with any model, as long as it inherits from the class `bioptim.Model`. Models built with `biorbd` are already compatible with `bioptim`.
Expand Down Expand Up @@ -1668,6 +1686,16 @@ INTEGRATED: Plot that links the points within an interval but is discrete betwee
STEP: Step plot, constant over an interval.
POINT: Point plot.

### Enum: OnlineOptim
The type of online plotter to use.

The accepted values are:
NONE: No online plotter.
DEFAULT: Use the default online plotter depending on the OS (MULTIPROCESS on Linux, MULTIPROCESS_SERVER on Windows and NONE on MacOS).
MULTIPROCESS: The online plotter is in a separate process.
SERVER: The online plotter is in a separate server.
MULTIPROCESS_SERVER: The online plotter using the server automatically setup on a separate process.

### Enum: InterpolationType
Defines wow a time-dependent variable is interpolated.
It is mainly used for phases time span.
Expand Down
3 changes: 3 additions & 0 deletions bioptim/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -208,6 +208,7 @@
MagnitudeType,
MultiCyclicCycleSolutions,
PhaseDynamics,
OnlineOptim,
)
from .misc.mapping import BiMappingList, BiMapping, Mapping, NodeMapping, NodeMappingList, SelectionMapping, Dependency
from .optimization.multi_start import MultiStart
Expand All @@ -229,3 +230,5 @@
from .optimization.stochastic_optimal_control_program import StochasticOptimalControlProgram
from .optimization.problem_type import SocpType
from .misc.casadi_expand import lt, le, gt, ge, if_else, if_else_zero

from .gui.online_callback_server import PlottingServer
8 changes: 4 additions & 4 deletions bioptim/examples/getting_started/pendulum.py
Original file line number Diff line number Diff line change
Expand Up @@ -9,8 +9,6 @@
appreciate it). Finally, once it finished optimizing, it animates the model using the optimal solution
"""

import platform

from bioptim import (
OptimalControlProgram,
DynamicsFcn,
Expand All @@ -26,6 +24,7 @@
BiorbdModel,
ControlType,
PhaseDynamics,
OnlineOptim,
)


Expand Down Expand Up @@ -149,8 +148,9 @@ def main():
# --- Print ocp structure --- #
ocp.print(to_console=False, to_graph=False)

# --- Solve the ocp. Please note that online graphics only works with the Linux operating system --- #
sol = ocp.solve(Solver.IPOPT(show_online_optim=platform.system() == "Linux"))
# --- Solve the ocp --- #
# Default is OnlineOptim.MULTIPROCESS on Linux, OnlineOptim.MULTIPROCESS_SERVER on Windows and None on MacOS
sol = ocp.solve(Solver.IPOPT(show_online_optim=OnlineOptim.DEFAULT))

# --- Show the results graph --- #
sol.print_cost()
Expand Down
Original file line number Diff line number Diff line change
@@ -1,12 +1,9 @@
import multiprocessing as mp
from abc import ABC, abstractmethod

from casadi import Callback, nlpsol_out, nlpsol_n_out, Sparsity
from matplotlib import pyplot as plt

from .plot import PlotOcp


class OnlineCallback(Callback):
class OnlineCallbackAbstract(Callback, ABC):
"""
CasADi interface of Ipopt callbacks

Expand All @@ -18,12 +15,6 @@ class OnlineCallback(Callback):
The number of optimization variables
ng: int
The number of constraints
queue: mp.Queue
The multiprocessing queue
plotter: ProcessPlotter
The callback for plotting for the multiprocessing
plot_process: mp.Process
The multiprocessing placeholder

Methods
-------
Expand All @@ -37,7 +28,7 @@ class OnlineCallback(Callback):
Get the name of the output variable
get_sparsity_in(self, i: int) -> tuple[int]
Get the sparsity of a specific variable
eval(self, arg: list | tuple) -> list[int]
eval(self, arg: list | tuple, force: bool = False) -> list[int]
Send the current data to the plotter
"""

Expand All @@ -63,20 +54,16 @@ def __init__(self, ocp, opts: dict = None, show_options: dict = None):
from ..interfaces.ipopt_interface import IpoptInterface

interface = IpoptInterface(ocp)
all_g, all_g_bounds = interface.dispatch_bounds()
all_g, _ = interface.dispatch_bounds()
self.ng = all_g.shape[0]

v = interface.ocp.variables_vector

self.construct("AnimateCallback", opts)

self.queue = mp.Queue()
self.plotter = self.ProcessPlotter(self.ocp)
self.plot_process = mp.Process(target=self.plotter, args=(self.queue, show_options), daemon=True)
self.plot_process.start()

@abstractmethod
def close(self):
self.plot_process.kill()
"""
Close the callback
"""

@staticmethod
def get_n_in() -> int:
Expand Down Expand Up @@ -155,7 +142,8 @@ def get_sparsity_in(self, i: int) -> tuple:
else:
return Sparsity(0, 0)

def eval(self, arg: list | tuple) -> list:
@abstractmethod
def eval(self, arg: list | tuple, enforce: bool = False) -> list[int]:
"""
Send the current data to the plotter

Expand All @@ -164,78 +152,11 @@ def eval(self, arg: list | tuple) -> list:
arg: list | tuple
The data to send

enforce: bool
If True, the client will block until the server is ready to receive new data. This is useful at the end of
the optimization to make sure the data are plot (and not discarded)

Returns
-------
A list of error index
"""
send = self.queue.put
args_dict = {}
for i, s in enumerate(nlpsol_out()):
args_dict[s] = arg[i]
send(args_dict)
return [0]

class ProcessPlotter(object):
"""
The plotter that interface PlotOcp and the multiprocessing

Attributes
----------
ocp: OptimalControlProgram
A reference to the ocp to show
pipe: mp.Queue
The multiprocessing queue to evaluate
plot: PlotOcp
The handler on all the figures

Methods
-------
callback(self) -> bool
The callback to update the graphs
"""

def __init__(self, ocp):
"""
Parameters
----------
ocp: OptimalControlProgram
A reference to the ocp to show
"""

self.ocp = ocp

def __call__(self, pipe: mp.Queue, show_options: dict):
"""
Parameters
----------
pipe: mp.Queue
The multiprocessing queue to evaluate
show_options: dict
The option to pass to PlotOcp
"""

if show_options is None:
show_options = {}
self.pipe = pipe
self.plot = PlotOcp(self.ocp, **show_options)
timer = self.plot.all_figures[0].canvas.new_timer(interval=10)
timer.add_callback(self.callback)
timer.start()
plt.show()

def callback(self) -> bool:
"""
The callback to update the graphs

Returns
-------
True if everything went well
"""

while not self.pipe.empty():
args = self.pipe.get()
self.plot.update_data(args)

for i, fig in enumerate(self.plot.all_figures):
fig.canvas.draw()
return True
Loading
Loading