Daniel Schwabeneder fb27704d2b fix tests
2021-01-25 16:10:09 +01:00

648 lines
22 KiB
Julia

#HDF5 Plots: Save/replay plots to/from HDF5
#-------------------------------------------------------------------------------
#==Usage
===============================================================================
Write to .hdf5 file using:
p = plot(...)
Plots.hdf5plot_write(p, "plotsave.hdf5")
Read from .hdf5 file using:
pyplot() #Must first select backend
pread = Plots.hdf5plot_read("plotsave.hdf5")
display(pread)
==#
#==TODO
===============================================================================
1. Support more features.
- GridLayout known not to be working.
2. Improve error handling.
- Will likely crash if file format is off.
3. Save data in a folder parallel to "plot".
- Will make it easier for users to locate data.
- Use HDF5 reference to link data?
4. Develop an actual versioned file format.
- Should have some form of backward compatibility.
- Should be reliable for archival purposes.
5. Fix construction of plot object with hdf5plot_read.
- Layout doesn't seem to get transferred well (ex: `Plots._examples[40]`).
- Not building object correctly when backends do not natively support
a certain feature (ex: :steppre)
- No support for CategoricalArrays.* structures. But they appear to be
brought into `Plots._examples[25,30]` through DataFrames.jl - so we can't
really reference them in this code.
==#
"""
_hdf5_implementation
Create module (namespace) for implementing HDF5 "plots".
(Avoid name collisions, while keeping names short)
"""
module _hdf5_implementation #Tools required to implements HDF5 "plots"
import Dates
#Plots.jl imports HDF5 to main:
import ..HDF5
import ..HDF5: Group, Dataset
import ..Colors, ..Colorant
import ..PlotUtils.ColorSchemes.ColorScheme
import ..HDF5Backend
import ..HDF5PLOT_MAP_STR2TELEM, ..HDF5PLOT_MAP_TELEM2STR
import ..HDF5Plot_PlotRef, ..HDF5PLOT_PLOTREF
import ..BoundingBox, ..Extrema, ..Length
import ..RecipesPipeline.datetimeformatter
import ..PlotUtils.ColorPalette, ..PlotUtils.CategoricalColorGradient, ..PlotUtils.ContinuousColorGradient
import ..Surface, ..Shape, ..Arrow
import ..GridLayout, ..RootLayout
import ..Font, ..PlotText, ..SeriesAnnotations
import ..Axis, ..Subplot, ..Plot
import ..AKW, ..KW, ..DefaultsDict
import .._axis_defaults
import ..plot, ..plot!
#Types that already have built-in HDF5 support (just write out natively):
const HDF5_SupportedTypes = Union{Number, String}
#TODO: Types_HDF5Support
#Dispatch types:
struct CplxTuple; end #Identifies a "complex" tuple structure (not merely numbers)
#HDF5 reader will auto-detect type correctly:
struct HDF5_AutoDetect; end #See HDF5_SupportedTypes
#==
===============================================================================#
if length(HDF5PLOT_MAP_TELEM2STR) < 1
#Possible element types of high-level data types:
#(Used to add type information as an HDF5 string attribute)
#(Also used to dispatch appropriate read function through _read_typed())
_telem2str = Dict{String, Type}(
"NOTHING" => Nothing,
"SYMBOL" => Symbol,
"RGBA" => Colorant, #Write out any Colorant to an #RRGGBBAA string
"TUPLE" => Tuple,
"CTUPLE" => CplxTuple, #Tuple of complex structures
"EXTREMA" => Extrema,
"LENGTH" => Length,
"ARRAY" => Array, #Array{Any} (because Array{T<:Union{Number, String}} natively supported by HDF5)
#Sub-structure types:
"T_DATETIMEFORMATTER" => typeof(datetimeformatter),
#Sub-structure types:
"DEFAULTSDICT" => DefaultsDict,
"FONT" => Font,
"BOUNDINGBOX" => BoundingBox,
"GRIDLAYOUT" => GridLayout,
"ROOTLAYOUT" => RootLayout,
"SERIESANNOTATIONS" => SeriesAnnotations,
"PLOTTEXT" => PlotText,
"SHAPE" => Shape,
"ARROW" => Arrow,
"COLORSCHEME" => ColorScheme,
"COLORPALETTE" => ColorPalette,
"CONT_COLORGRADIENT" => ContinuousColorGradient,
"CAT_COLORGRADIENT" => CategoricalColorGradient,
"AXIS" => Axis,
"SURFACE" => Surface,
"SUBPLOT" => Subplot,
)
merge!(HDF5PLOT_MAP_STR2TELEM, _telem2str) #Faster to create than push!()??
merge!(HDF5PLOT_MAP_TELEM2STR, Dict{Type, String}(v=>k for (k,v) in HDF5PLOT_MAP_STR2TELEM))
end
#==Helper functions
===============================================================================#
h5plotpath(plotname::String) = "plots/$plotname"
#Version info
#NOTE: could cache output, but we seem to not want const declarations in backend files.
function _get_Plots_versionstr()
#Adds to load up time... Maybe a more efficient way??
try
deps = Pkg.dependencies()
uuid = Base.UUID("91a5bcdd-55d7-5caf-9e0b-520d859cae80") #Plots.jl
vinfo = deps[uuid].version
return "Source: Plots.jl v$vinfo"
catch
now = string(Dates.now()) #Use time in case it can help recover plot
return "Source: Plots.jl v? - $now"
end
end
function _hdf5_merge!(dest::AKW, src::AKW)
for (k, v) in src
if isa(v, Axis)
_hdf5_merge!(dest[k].plotattributes, v.plotattributes)
else
dest[k] = v
end
end
return
end
#_type_for_map returns the type to use with HDF5PLOT_MAP_TELEM2STR[], in case it is not concrete:
_type_for_map(::Type{T}) where T = T #Catch-all
_type_for_map(::Type{T}) where T<:BoundingBox = BoundingBox
_type_for_map(::Type{T}) where T<:ColorScheme = ColorScheme
_type_for_map(::Type{T}) where T<:Surface = Surface
#==Read/write things like type name in attributes
===============================================================================#
function _write_datatype_attr(ds::Union{Group, Dataset}, ::Type{T}) where T
typestr = HDF5PLOT_MAP_TELEM2STR[T]
HDF5.attributes(ds)["TYPE"] = typestr
end
function _read_datatype_attr(ds::Union{Group, Dataset})
if !Base.haskey(HDF5.attributes(ds), "TYPE")
return HDF5_AutoDetect
end
typestr = HDF5.read(HDF5.attributes(ds)["TYPE"])
return HDF5PLOT_MAP_STR2TELEM[typestr]
end
#Type parameter attributes:
function _write_typeparam_attr(ds::Dataset, v::Length{T}) where T
HDF5.attributes(ds)["TYPEPARAM"] = string(T) #Need to add units for Length
end
_read_typeparam_attr(ds::Dataset) = HDF5.read(HDF5.attributes(ds)["TYPEPARAM"])
function _write_length_attr(grp::Group, v::Vector) #of a vector
HDF5.attributes(grp)["LENGTH"] = length(v)
end
_read_length_attr(::Type{Vector}, grp::Group) = HDF5.read(HDF5.attributes(grp)["LENGTH"])
function _write_size_attr(grp::Group, v::Array) #of an array
HDF5.attributes(grp)["SIZE"] = [size(v)...]
end
_read_size_attr(::Type{Array}, grp::Group) = tuple(HDF5.read(HDF5.attributes(grp)["SIZE"])...)
#==_write_typed(): Simple (leaf) datatypes. (Labels with type name.)
===============================================================================#
#= No: write out struct instead!
function _write_typed(grp::Group, name::String, v::T) where T
tstr = string(T)
path = HDF5.name(grp) * "/" * name
@info("Type not supported: $tstr\npath: $path")
return
end
=#
#Default behaviour: Assumes value is supported by HDF5 format
function _write_typed(grp::Group, name::String, v::HDF5_SupportedTypes)
grp[name] = v
return #No need to _write_datatype_attr
end
function _write_typed(grp::Group, name::String, v::Nothing)
grp[name] = "nothing" #Redundancy check/easier to read HDF5 file
_write_datatype_attr(grp[name], Nothing)
end
function _write_typed(grp::Group, name::String, v::Symbol)
grp[name] = String(v)
_write_datatype_attr(grp[name], Symbol)
end
function _write_typed(grp::Group, name::String, v::Colorant)
vstr = "#" * Colors.hex(v, :RRGGBBAA)
grp[name] = vstr
_write_datatype_attr(grp[name], Colorant)
end
function _write_typed(grp::Group, name::String, v::Extrema)
grp[name] = [v.emin, v.emax] #More compact than writing struct
_write_datatype_attr(grp[name], Extrema)
end
function _write_typed(grp::Group, name::String, v::Length)
grp[name] = v.value
_write_datatype_attr(grp[name], Length)
_write_typeparam_attr(grp[name], v)
end
function _write_typed(grp::Group, name::String, v::typeof(datetimeformatter))
grp[name] = string(v) #Just write something that helps reader
_write_datatype_attr(grp[name], typeof(datetimeformatter))
end
function _write_typed(grp::Group, name::String, v::Array{T}) where T<:Number #Default for arrays
grp[name] = v
return #No need to _write_datatype_attr
end
function _write_typed(grp::Group, name::String, v::AbstractRange)
_write_typed(grp, name, collect(v)) #For now
end
#== Helper functions for writing complex data structures
===============================================================================#
#Write an array using HDF5 hierarchy (when not using simple numeric eltype):
function _write_harray(grp::Group, name::String, v::Array)
sgrp = HDF5.create_group(grp, name)
sz = size(v)
lidx = LinearIndices(sz)
for iter in eachindex(v)
coord = lidx[iter]
elem = v[iter]
idxstr = join(coord, "_")
_write_typed(sgrp, "v$idxstr", elem)
end
_write_size_attr(sgrp, v)
end
#Write Dict without tagging with type:
function _write(grp::Group, name::String, d::AbstractDict)
sgrp = HDF5.create_group(grp, name)
for (k, v) in d
kstr = string(k)
_write_typed(sgrp, kstr, v)
end
return
end
#Write out arbitrary `struct`s:
function _writestructgeneric(grp::Group, obj::T) where T
for fname in fieldnames(T)
v = getfield(obj, fname)
_write_typed(grp, String(fname), v)
end
return
end
#==_write_typed(): More complex structures. (Labels with type name.)
===============================================================================#
#Catch-all (default behaviour for `struct`s):
function _write_typed(grp::Group, name::String, v::T) where T
#NOTE: need "name" parameter so that call signature is same with built-ins
MT = _type_for_map(T)
try #Check to see if type is supported
typestr = HDF5PLOT_MAP_TELEM2STR[MT]
catch
@warn("HDF5Plots does not yet support structs of type `$MT`\n\n$grp")
return
end
#If attribute is supported and no writer is defined, then this should work:
objgrp = HDF5.create_group(grp, name)
_write_datatype_attr(objgrp, MT)
_writestructgeneric(objgrp, v)
end
function _write_typed(grp::Group, name::String, v::Array{T}) where T
_write_harray(grp, name, v)
_write_datatype_attr(grp[name], Array) #{Any}
end
function _write_typed(grp::Group, name::String, v::Tuple, ::Type{ELT}) where ELT<: Number #Basic Tuple
_write_typed(grp, name, [v...])
_write_datatype_attr(grp[name], Tuple)
end
function _write_typed(grp::Group, name::String, v::Tuple, ::Type) #CplxTuple
_write_harray(grp, name, [v...])
_write_datatype_attr(grp[name], CplxTuple)
end
_write_typed(grp::Group, name::String, v::Tuple) = _write_typed(grp, name, v, eltype(v))
function _write_typed(grp::Group, name::String, v::Dict)
#=
tstr = string(Dict)
path = HDF5.name(grp) * "/" * name
@info("Type not supported: $tstr\npath: $path")
return
=#
#No support for structures with Dicts yet
end
function _write_typed(grp::Group, name::String, d::DefaultsDict) #Typically for plot attributes
_write(grp, name, d)
_write_datatype_attr(grp[name], DefaultsDict)
end
function _write_typed(grp::Group, name::String, v::Axis)
sgrp = HDF5.create_group(grp, name)
#Ignore: sps::Vector{Subplot}
_write_typed(sgrp, "plotattributes", v.plotattributes)
_write_datatype_attr(sgrp, Axis)
end
function _write_typed(grp::Group, name::String, v::Subplot)
#Not for use in main "Plot.subplots[]" hierarchy. Just establishes reference with subplot_index.
sgrp = HDF5.create_group(grp, name)
_write_typed(sgrp, "index", v[:subplot_index])
_write_datatype_attr(sgrp, Subplot)
return
end
function _write_typed(grp::Group, name::String, v::Plot)
#Don't write plot references
end
#==_write(): Write out more complex structures
NOTE: No need to write out type information (inferred from hierarchy)
===============================================================================#
function _write(grp::Group, sp::Subplot{HDF5Backend})
_write_typed(grp, "attr", sp.attr)
listgrp = HDF5.create_group(grp, "series_list")
_write_length_attr(listgrp, sp.series_list)
for (i, series) in enumerate(sp.series_list)
#Just write .plotattributes part:
_write(listgrp, "$i", series.plotattributes)
end
return
end
function _write(grp::Group, plt::Plot{HDF5Backend})
_write_typed(grp, "attr", plt.attr)
listgrp = HDF5.create_group(grp, "subplots")
_write_length_attr(listgrp, plt.subplots)
for (i, sp) in enumerate(plt.subplots)
sgrp = HDF5.create_group(listgrp, "$i")
_write(sgrp, sp)
end
return
end
function hdf5plot_write(plt::Plot{HDF5Backend}, path::AbstractString; name::String="_unnamed")
HDF5.h5open(path, "w") do file
HDF5.write_dataset(file, "VERSION_INFO", _get_Plots_versionstr())
grp = HDF5.create_group(file, h5plotpath(name))
_write(grp, plt)
end
end
#== _read(): Read data, but not type information.
===============================================================================#
#Types with built-in HDF5 support:
_read(::Type{HDF5_AutoDetect}, ds::Dataset) = HDF5.read(ds)
function _read(::Type{Nothing}, ds::Dataset)
nstr = "nothing"
v = HDF5.read(ds)
if nstr != v
path = HDF5.name(ds)
throw(Meta.ParseError("_read(::Nothing, ::Group): Read $v != $nstr:\n$path"))
end
return nothing
end
_read(::Type{Symbol}, ds::Dataset) = Symbol(HDF5.read(ds))
_read(::Type{Colorant}, ds::Dataset) = parse(Colorant, HDF5.read(ds))
_read(::Type{Tuple}, ds::Dataset) = tuple(HDF5.read(ds)...)
function _read(::Type{Extrema}, ds::Dataset)
v = HDF5.read(ds)
return Extrema(v[1], v[2])
end
function _read(::Type{Length}, ds::Dataset)
TUNIT = Symbol(_read_typeparam_attr(ds))
v = HDF5.read(ds)
T = typeof(v)
return Length{TUNIT,T}(v)
end
_read(::Type{typeof(datetimeformatter)}, ds::Dataset) = datetimeformatter
#== Helper functions for reading in complex data structures
===============================================================================#
#When type is unknown, _read_typed() figures it out:
function _read_typed(grp::Group, name::String)
ds = grp[name]
t = _read_datatype_attr(ds)
return _read(t, ds)
end
#_readstructgeneric: Needs object values to be written out with _write_typed():
function _readstructgeneric(::Type{T}, grp::Group) where T
vlist = Array{Any}(nothing, fieldcount(T))
for (i, fname) in enumerate(fieldnames(T))
vlist[i] = _read_typed(grp, String(fname))
end
return T(vlist...)
end
#Read KW from group:
function _read(::Type{KW}, grp::Group)
d = KW()
gkeys = keys(grp)
for k in gkeys
try
v = _read_typed(grp, k)
d[Symbol(k)] = v
catch e
@show e
@show grp
@warn("Could not read field $k")
end
end
return d
end
#== _read(): More complex structures.
===============================================================================#
#Catch-all (default behaviour for `struct`s):
_read(T::Type, grp::Group) = _readstructgeneric(T, grp)
function _read(::Type{Array}, grp::Group) #Array{Any}
sz = _read_size_attr(Array, grp)
if tuple(0) == sz; return []; end
result = Array{Any}(undef, sz)
lidx = LinearIndices(sz)
for iter in eachindex(result)
coord = lidx[iter]
idxstr = join(coord, "_")
result[iter] = _read_typed(grp, "v$idxstr")
end
#Hack: Implicitly make Julia detect element type.
# (Should probably write it explicitly to file)
result = [elem for elem in result] #Potentially make more specific
return reshape(result, sz)
end
_read(::Type{CplxTuple}, grp::Group) = tuple(_read(Array, grp)...)
function _read(::Type{GridLayout}, grp::Group)
#parent = _read_typed(grp, "parent") #Can't use generic reader
parent = RootLayout() #TODO: support parent???
minpad = _read_typed(grp, "minpad")
bbox = _read_typed(grp, "bbox")
grid = _read_typed(grp, "grid")
widths = _read_typed(grp, "widths")
heights = _read_typed(grp, "heights")
attr = KW() #TODO support attr: _read_typed(grp, "attr")
return GridLayout(parent, minpad, bbox, grid, widths, heights, attr)
end
#Defaults depends on context. So: user must constructs with defaults, then read.
function _read(::Type{DefaultsDict}, grp::Group)
#User should set DefaultsDict.defaults to one of:
# _plot_defaults, _subplot_defaults, _axis_defaults, _series_defaults
path = HDF5.name(ds)
@warn("Cannot yet read DefaultsDict using _read_typed():\n $path\nCannot fully reconstruct plot.")
end
function _read(::Type{Axis}, grp::Group)
#1st arg appears to be ref to subplots. Seems to work without it.
return Axis([], DefaultsDict(_read(KW, grp["plotattributes"]), _axis_defaults))
end
function _read(::Type{Subplot}, grp::Group)
#Not for use in main "Plot.subplots[]" hierarchy. Just establishes reference with subplot_index.
idx = _read_typed(grp, "index")
return HDF5PLOT_PLOTREF.ref.subplots[idx]
end
#== _read(): Main plot structures
===============================================================================#
function _read(grp::Group, sp::Subplot)
listgrp = HDF5.open_group(grp, "series_list")
nseries = _read_length_attr(Vector, listgrp)
for i in 1:nseries
sgrp = HDF5.open_group(listgrp, "$i")
seriesinfo = _read(KW, sgrp)
plot!(sp, seriesinfo[:x], seriesinfo[:y]) #Add data & create data structures
_hdf5_merge!(sp.series_list[end].plotattributes, seriesinfo)
end
#Perform after adding series... otherwise values get overwritten:
agrp = HDF5.open_group(grp, "attr")
_hdf5_merge!(sp.attr, _read(KW, agrp))
return sp
end
function _read_plot(grp::Group)
listgrp = HDF5.open_group(grp, "subplots")
n = _read_length_attr(Vector, listgrp)
#Construct new plot, +allocate subplots:
plt = plot(layout=n)
HDF5PLOT_PLOTREF.ref = plt #Used when reading "layout"
agrp = HDF5.open_group(grp, "attr")
_hdf5_merge!(plt.attr, _read(KW, agrp))
for (i, sp) in enumerate(plt.subplots)
sgrp = HDF5.open_group(listgrp, "$i")
_read(sgrp, sp)
end
return plt
end
function hdf5plot_read(path::AbstractString; name::String="_unnamed")
HDF5.h5open(path, "r") do file
grp = HDF5.open_group(file, h5plotpath("_unnamed"))
return _read_plot(grp)
end
end
end #module _hdf5_implementation
#==Implement Plots.jl backend interface for HDF5Backend
===============================================================================#
is_marker_supported(::HDF5Backend, shape::Shape) = true
# Create the window/figure for this backend.
function _create_backend_figure(plt::Plot{HDF5Backend})
#Do nothing
end
# ---------------------------------------------------------------------------
# # this is called early in the pipeline, use it to make the plot current or something
# function _prepare_plot_object(plt::Plot{HDF5Backend})
# end
# ---------------------------------------------------------------------------
# Set up the subplot within the backend object.
function _initialize_subplot(plt::Plot{HDF5Backend}, sp::Subplot{HDF5Backend})
#Do nothing
end
# ---------------------------------------------------------------------------
# Add one series to the underlying backend object.
# Called once per series
# NOTE: Seems to be called when user calls plot()... even if backend
# plot, sp.o has not yet been constructed...
function _series_added(plt::Plot{HDF5Backend}, series::Series)
#Do nothing
end
# ---------------------------------------------------------------------------
# When series data is added/changed, this callback can do dynamic updates to the backend object.
# note: if the backend rebuilds the plot from scratch on display, then you might not do anything here.
function _series_updated(plt::Plot{HDF5Backend}, series::Series)
#Do nothing
end
# ---------------------------------------------------------------------------
# called just before updating layout bounding boxes... in case you need to prep
# for the calcs
function _before_layout_calcs(plt::Plot{HDF5Backend})
#Do nothing
end
# ----------------------------------------------------------------
# Set the (left, top, right, bottom) minimum padding around the plot area
# to fit ticks, tick labels, guides, colorbars, etc.
function _update_min_padding!(sp::Subplot{HDF5Backend})
#Do nothing
end
# ----------------------------------------------------------------
# Override this to update plot items (title, xlabel, etc), and add annotations (plotattributes[:annotations])
function _update_plot_object(plt::Plot{HDF5Backend})
#Do nothing
end
# ----------------------------------------------------------------
# Display/show the plot (open a GUI window, or browser page, for example).
function _display(plt::Plot{HDF5Backend})
msg = "HDF5 interface does not support `display()` function."
msg *= "\nUse `Plots.hdf5plot_write(::String)` method to write to .HDF5 \"plot\" file instead."
@warn(msg)
return
end
#==Interface actually required to use HDF5Backend
===============================================================================#
hdf5plot_write(plt::Plot{HDF5Backend}, path::AbstractString) = _hdf5_implementation.hdf5plot_write(plt, path)
hdf5plot_write(path::AbstractString) = _hdf5_implementation.hdf5plot_write(current(), path)
hdf5plot_read(path::AbstractString) = _hdf5_implementation.hdf5plot_read(path)
#Last line