Pickle support#

Added in version 0.12.0.

Starting from version 0.12.0, all the main classes in heyoka.py support serialisation via the standard Python pickle module. Before showing a couple of examples of serialisation in action, we need to emphasise a couple of very important caveats:

  • the serialisation format is platform-dependent and it also depends on the versions of heyoka.py, the heyoka C++ library, LLVM, and Boost. Thus, the serialised representation of heyoka.py objects is not portable across platforms or across different versions of heyoka.py or its dependencies. Do not try to use the serialised representation of heyoka.py objects as an exchange format, as this will result in undefined behaviour;

  • heyoka.py does not make any attempt to validate the state of a deserialised object. Thus, a maliciously-crafted pickle could be used to crash heyoka.py or even execute arbitrary code on the machine.

The last point is particularly important: because the integrator objects contain blobs of binary code, a maliciously-crafted pickle can easily be used to execute arbitrary code on the host machine.

Let us repeat again these warnings for visibility:

Warning

Do not load heyoka.py objects from untrusted pickles, as this could lead to the execution of malicious code.

Do not use heyoka.py pickles as a data exchange format, and make sure that all the pickles you load from have been produced with the same versions of heyoka.py, the heyoka C++ library, LLVM and Boost that you are currently using.

With these warnings out of the way, let us proceed to the code.

A simple example#

In order to illustrate the (de)serialisation workflow, we will be using our good old friend, the simple pendulum. We begin as usual with the definition of the symbolic variables and the integrator object:

import heyoka as hy

# Create the symbolic variables.
x, v = hy.make_vars("x", "v")

# Create the integrator object.
ta = hy.taylor_adaptive(
                        # Definition of the ODE system:
                        # x' = v
                        # v' = -9.8 * sin(x)
                        sys = [(x, v),
                               (v, -9.8 * hy.sin(x))],
                        # Initial conditions for x and v.
                        state = [0.05, 0.025])

We then integrate for a few timesteps, so that the time coordinate and the state will evolve from their initial values:

for _ in range(10):
    ta.step()

Let us print to screen the time and state:

print("Time : {}".format(ta.time))
print("State: {}".format(ta.state))
Time : 2.0916676360970685
State: [ 0.05035359 -0.01665554]

We can now proceed first to serialise ta into a bytes object …

import pickle
ta_pk = pickle.dumps(ta)

… and then to revive it into a new object:

ta_copy = pickle.loads(ta_pk)

We can verify that indeed the revived object contains the same data as ta:

print("Time : {}".format(ta_copy.time))
print("State: {}".format(ta_copy.state))
Time : 2.0916676360970685
State: [ 0.05035359 -0.01665554]

As an additional check, let us perform a few more integration steps on both integrators:

for _ in range(10):
    ta.step()
    ta_copy.step()

Let us compare them again:

print("Time (original) : {}".format(ta.time))
print("Time (copy)     : {}".format(ta_copy.time))
print("State (original): {}".format(ta.state))
print("State (copy    ): {}".format(ta_copy.state))
Time (original) : 4.175322858081083
Time (copy)     : 4.175322858081083
State (original): [ 0.04766883 -0.053436  ]
State (copy    ): [ 0.04766883 -0.053436  ]

On the serialisation of event callbacks#

For the (de)serialisation of event callbacks, heyoka.py by default employs internally the cloudpickle module instead of the standard pickle module. The motivation behind this choice is that cloudpickle is able to (de)serialise objects which the standard pickle module cannot. In particular, cloudpickle is able to (de)serialise lambdas and objects defined in an interactive session.

If, for any reason, cloudpickle is to be avoided, heyoka.py’s internal serialisation backend can be switched back to the standard pickle module via the set_serialisation_backend() function:

# Print the current serialisation backend.
print("Current backend: ", hy.get_serialization_backend())

# Switch to the standard pickle module.
hy.set_serialization_backend("pickle")
print("Current backend: ", hy.get_serialization_backend())

# Switch back to cloudpickle.
hy.set_serialization_backend("cloudpickle")
print("Current backend: ", hy.get_serialization_backend())
Current backend:  <module 'cloudpickle' from '/home/runner/local/lib/python3.10/site-packages/cloudpickle/__init__.py'>
Current backend:  <module 'pickle' from '/home/runner/local/lib/python3.10/pickle.py'>
Current backend:  <module 'cloudpickle' from '/home/runner/local/lib/python3.10/site-packages/cloudpickle/__init__.py'>