I/O: Saving and Loading Solution Data

The ability to save and load solutions is important for handling large datasets and analyzing the results over multiple Julia sessions. This page explains the existing functionality for doing so.

Tabular Data: IterableTables

An interface to IterableTables.jl is provided. This IterableTables link allows you to use a solution type as the data source to convert to other tabular data formats. For example, let's solve a 4x2 system of ODEs and get the DataFrame:

import OrdinaryDiffEq as ODE, DataFrames
f_2dlinear = (du, u, p, t) -> du .= 1.01u;
tspan = (0.0, 1.0)
prob = ODE.ODEProblem(f_2dlinear, rand(2, 2), tspan);
sol = ODE.solve(prob, ODE.Euler(); dt = 1 // 2^(4));
df = DataFrames.DataFrame(sol)
17×5 DataFrame
Rowtimestampvalue1value2value3value4
Float64Float64Float64Float64Float64
10.00.2869610.2146310.5001580.0100907
20.06250.3050760.228180.531730.0107276
30.1250.3243340.2425830.5652950.0114048
40.18750.3448070.2578960.600980.0121247
50.250.3665730.2741760.6389170.0128901
60.31250.3897130.2914840.6792480.0137038
70.3750.4143140.3098830.7221260.0145689
80.43750.4404670.3294450.767710.0154885
90.50.4682720.3502410.8161720.0164662
100.56250.4978320.372350.8676920.0175057
110.6250.5292570.3958550.9224660.0186107
120.68750.5626670.4208430.9806960.0197855
130.750.5981850.4474091.04260.0210345
140.81250.6359450.4756511.108420.0223623
150.8750.6760890.5056771.178390.0237739
160.93750.7187680.5375981.252770.0252746
171.00.764140.5715331.331850.0268701

If we set syms in the DiffEqFunction, then those names will be used:

f = ODE.ODEFunction(f_2dlinear, syms = [:a, :b, :c, :d])
prob = ODE.ODEProblem(f, rand(2, 2), (0.0, 1.0));
sol = ODE.solve(prob, ODE.Euler(); dt = 1 // 2^(4));
df = DataFrames.DataFrame(sol)
17×5 DataFrame
Rowtimestampabcd
Float64Float64Float64Float64Float64
10.00.07814440.4347690.9898950.567102
20.06250.08307730.4622141.052380.6029
30.1250.08832150.4913911.118810.640958
40.18750.09389680.522411.189440.681419
50.250.09982410.5553871.264520.724433
60.31250.1061250.5904461.344350.770163
70.3750.1128250.6277181.429210.818779
80.43750.1199470.6673421.519430.870465
90.50.1275180.7094681.615340.925413
100.56250.1355680.7542541.717310.98383
110.6250.1441260.8018661.825711.04593
120.68750.1532240.8524841.940961.11196
130.750.1628960.9062972.063481.18215
140.81250.1731790.9635072.193741.25677
150.8750.1841111.024332.332221.33611
160.93750.1957321.088992.479441.42045
171.00.2080881.157732.635961.51012

Many modeling frameworks will automatically set syms for this feature. Additionally, this data can be saved to a CSV:

import CSV
CSV.write("out.csv", df)
"out.csv"

JLD2 and BSON.jl

JLD2.jl and BSON.jl will work with the full solution type if you bring the required functions back into scope before loading. For example, if we save the solution:

sol = ODE.solve(prob, ODE.Euler(); dt = 1 // 2^(4))
import JLD2
JLD2.@save "out.jld2" sol
┌ Warning: Attempting to store Main.var"#1#2".
JLD2 only stores functions by name.
 This may not be useful for anonymous functions.
@ JLD2 ~/.cache/julia-buildkite-plugin/depots/0185fce3-4489-413a-a934-123dd653ef61/packages/JLD2/SgtOb/src/data/writing_datatypes.jl:447
┌ Warning: Attempting to store ODEFunction{true, SciMLBase.FullSpecialize, Main.var"#1#2", LinearAlgebra.UniformScaling{Bool}, Nothing, Nothing, Nothing, Nothing, Nothing, Nothing, Nothing, Nothing, Nothing, Nothing, Nothing, typeof(SciMLBase.DEFAULT_OBSERVED), Nothing, SymbolicIndexingInterface.SymbolCache{Dict{Symbol, Int64}, Nothing, Nothing, Nothing, Dict{Any, Any}}, Nothing, Nothing}.
JLD2 only stores functions by name.
 This may not be useful for anonymous functions.
@ JLD2 ~/.cache/julia-buildkite-plugin/depots/0185fce3-4489-413a-a934-123dd653ef61/packages/JLD2/SgtOb/src/data/writing_datatypes.jl:447
┌ Warning: Attempting to store Main.var"#1#2".
JLD2 only stores functions by name.
 This may not be useful for anonymous functions.
@ JLD2 ~/.cache/julia-buildkite-plugin/depots/0185fce3-4489-413a-a934-123dd653ef61/packages/JLD2/SgtOb/src/data/writing_datatypes.jl:447

then we can get the full solution type back, interpolations and all, if we load the dependent functions first:

# New session
import JLD2
import OrdinaryDiffEq as ODE
JLD2.@load "out.jld2" sol
1-element Vector{Symbol}:
 :sol

The example with BSON.jl is:

sol = ODE.solve(prob, ODE.Euler(); dt = 1 // 2^(4))
import BSON
BSON.bson("test.bson", Dict(:sol => sol))
# New session
import OrdinaryDiffEq as ODE
import BSON
# BSON.load("test.bson") # currently broken: https://github.com/JuliaIO/BSON.jl/issues/109

If you load it without the DE function then for some algorithms the interpolation may not work, and for all algorithms you'll need at least a solver package or SciMLBase.jl in scope in order for the solution interface (plot recipes, array indexing, etc.) to work. If none of these are put into scope, the solution type will still load and hold all of the values (so sol.u and sol.t will work), but none of the interface will be available.

If you want a copy of the solution that contains no function information you can use the function SciMLBase.strip_solution(sol). This will return a copy of the solution that doesn't have any functions, which you can serialize and deserialize without having any of the problems that typically come with serializing functions.

JLD

Don't use JLD. It's dead. Julia types can be saved via JLD.jl. However, they cannot save types which have functions, which means that the solution type is currently not compatible with JLD.

import JLD
JLD.save("out.jld", "sol", sol)