Skip to content

Commit b7a1d11

Browse files
authored
Add support for HDF5 read and write with PointSpaces (#2162)
* Add support for InputOuput with PointSpaces `write!` on Fields is now dispatched off the input field's space. When it is a point space, the local geometry data is written. When read_field is called, and no grid is present, then the field is assumed to be on a point space. Add function comments Simplify unit_point read/write test Make suggested changes to unit_point and docstrings clean up point space test * Add `read_type` to read field eltypes When the reader recreates a field from a HDF5, it called eval on a parsed input string. This replaces that with a function that ensures there are no function calls inside the parsed string before eval'ing it. This is based on old HDF5.jl code. make suggested read_type changes Revert field_eltype to value_type * Change value_type attribute to field_eltype Change the value_type attribute, used by the HDF5 reader and writer, to field_eltype. Backwards support is maintained and tested. Remove HDF5 import from test import climacomms backend
1 parent aa49047 commit b7a1d11

File tree

7 files changed

+240
-9
lines changed

7 files changed

+240
-9
lines changed

.buildkite/pipeline.yml

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1149,6 +1149,15 @@ steps:
11491149
- group: "Unit: InputOutput"
11501150
steps:
11511151

1152+
- label: "Unit: read_type"
1153+
key: unit_read_type
1154+
command: "julia --color=yes --check-bounds=yes --project=.buildkite test/InputOutput/unit_read_type.jl"
1155+
env:
1156+
CLIMACOMMS_DEVICE: "CUDA"
1157+
agents:
1158+
slurm_gpus: 1
1159+
slurm_mem: 3GB
1160+
11521161
- label: "Unit: spectralelement2d"
11531162
key: unit_spectralelement2d
11541163
command: "julia --color=yes --check-bounds=yes --project=.buildkite test/InputOutput/unit_spectralelement2d.jl"

NEWS.md

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,13 @@ ClimaCore.jl Release Notes
44
main
55
-------
66

7+
- Added support for InputOutput with PointSpaces
8+
PR [2162](https://github.com/CliMA/ClimaCore.jl/pull/2162).
9+
10+
- Reading a `Field` with `HDF5Reader` will not call `eval ∘ Meta.parse` on the raw
11+
`value_type` attribute. Instead, the new `read_type` function is used, which prevents
12+
the execution of arbitrary code. PR [2162](https://github.com/CliMA/ClimaCore.jl/pull/2162).
13+
714
- Improved `show` for spaces, and added `show` for grids. PR [2202](https://github.com/CliMA/ClimaCore.jl/pull/2202).
815

916
v0.14.26

src/InputOutput/readers.jl

Lines changed: 60 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -41,6 +41,42 @@ function get_key(file, key)
4141
end
4242
end
4343

44+
"""
45+
read_type(ts::AbstractString)
46+
47+
Parse a string `ts` into an expression, and then evaluate it into a type if it is a valid type expression.
48+
"""
49+
function read_type(ts::AbstractString)
50+
type_expr = Meta.parse(ts)
51+
if is_type_expr(type_expr)
52+
return eval(type_expr)
53+
end
54+
error("$ts cannot be parsed into a valid type")
55+
end
56+
57+
"""
58+
is_type_expr(expr)
59+
60+
Check if an expression is a type expression, with no function calls or other non-type
61+
expressions, with the expection of @NamedTuple.
62+
This function is based on the JLD.jl `is_valid_type_exp`.
63+
See https://github.com/JuliaIO/JLD.jl/blob/80ac89643e3ad87545e48f4d361a00a29cdf4e2f/src/JLD.jl#L922
64+
"""
65+
is_type_expr(s::Symbol) = true
66+
is_type_expr(q::QuoteNode) = is_type_expr(q.value)
67+
function is_type_expr(expr::Expr)
68+
if expr.head == :macrocall && expr.args[1] == Symbol("@NamedTuple")
69+
# skip the LineNumberNode, as eval does nothing for it
70+
if length(expr.args) > 2
71+
return all(map(is_type_expr, expr.args[3:end]))
72+
end
73+
return true
74+
end
75+
return expr.head in (:curly, :., :tuple, :braces, Symbol("::")) &&
76+
all(map(is_type_expr, expr.args))
77+
end
78+
is_type_expr(t) = isbits(t)
79+
4480
"""
4581
HDF5Reader(filename::AbstractString[, context::ClimaComms.AbstractCommsContext])
4682
HDF5Reader(::Function, filename::AbstractString[, context::ClimaComms.AbstractCommsContext])
@@ -182,6 +218,7 @@ function _scan_data_layout(layoutstring::AbstractString)
182218
"VIJHF",
183219
"VIFH",
184220
"VIHF",
221+
"DataF",
185222
) "datalayout is $layoutstring"
186223
layoutstring == "IJFH" && return DataLayouts.IJFH
187224
layoutstring == "IJHF" && return DataLayouts.IJHF
@@ -192,6 +229,7 @@ function _scan_data_layout(layoutstring::AbstractString)
192229
layoutstring == "VF" && return DataLayouts.VF
193230
layoutstring == "VIJFH" && return DataLayouts.VIJFH
194231
layoutstring == "VIJHF" && return DataLayouts.VIJHF
232+
layoutstring == "DataF" && return DataLayouts.DataF
195233
return DataLayouts.VIFH
196234
end
197235

@@ -513,11 +551,23 @@ function read_field(reader::HDF5Reader, name::AbstractString)
513551
staggering = Grids.CellFace()
514552
end
515553
space = Spaces.space(grid, staggering)
516-
else
554+
elseif haskey(attrs(obj), "space")
517555
space = read_space(reader, attrs(obj)["space"])
556+
else
557+
# if the there is no grid, then the field is on a PointSpace
558+
lg_obj = reader.file["local_geometry_data/$name"]
559+
ArrayType = ClimaComms.array_type(ClimaComms.device(reader.context))
560+
lg_data = ArrayType(read(lg_obj))
561+
# because it is a point space, the data layout of local_geometry_data is always DataF
562+
lg_type = read_type(attrs(lg_obj)["local_geometry_type"])
563+
local_geometry_data = DataLayouts.DataF{lg_type}(lg_data)
564+
space = Spaces.PointSpace(local_geometry_data)
565+
topology = nothing
566+
end
567+
if !(space isa Spaces.AbstractPointSpace)
568+
topology = Spaces.topology(space)
569+
ArrayType = ClimaComms.array_type(topology)
518570
end
519-
topology = Spaces.topology(space)
520-
ArrayType = ClimaComms.array_type(topology)
521571
data_layout = attrs(obj)["data_layout"]
522572
has_horizontal = occursin('I', data_layout)
523573
DataLayout = _scan_data_layout(data_layout)
@@ -535,14 +585,20 @@ function read_field(reader::HDF5Reader, name::AbstractString)
535585
# For when `Nh` is added back to the type space
536586
# Nhd = Nh_dim(data_layout)
537587
# Nht = Nhd == -1 ? () : (size(data, Nhd),)
538-
ElType = eval(Meta.parse(attrs(obj)["value_type"]))
588+
# The `value_type` attribute is deprecated. here we mantain backwards compatibility
589+
ElType = read_type(
590+
haskey(attrs(obj), "field_eltype") ?
591+
attrs(obj)["field_eltype"] : attrs(obj)["value_type"],
592+
)
539593
if data_layout in ("VIJFH", "VIFH")
540594
Nv = size(data, 1)
541595
# values = DataLayout{ElType, Nv, Nij, Nht...}(data) # when Nh is in type-domain
542596
values = DataLayout{ElType, Nv, Nij}(data)
543597
elseif data_layout in ("VF",)
544598
Nv = size(data, 1)
545599
values = DataLayout{ElType, Nv}(data)
600+
elseif data_layout in ("DataF",)
601+
values = DataLayout{ElType}(data)
546602
else
547603
# values = DataLayout{ElType, Nij, Nht...}(data) # when Nh is in type-domain
548604
values = DataLayout{ElType, Nij}(data)

src/InputOutput/writers.jl

Lines changed: 80 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -451,16 +451,91 @@ function write_new!(
451451
end
452452

453453
# write fields
454+
"""
455+
write!(writer::HDF5Writer, field::Fields.Field, name::AbstractString)
456+
457+
Write the `field` to the HDF5 in `writer` and assign it the given `name`.
458+
"""
454459
function write!(writer::HDF5Writer, field::Fields.Field, name::AbstractString)
460+
write!(writer, field, name, axes(field))
461+
end
462+
463+
"""
464+
write!(
465+
writer::HDF5Writer,
466+
field::Fields.Field,
467+
name::AbstractString,
468+
space::Spaces.AbstractPointSpace,
469+
)
470+
471+
Write a `Field`, with `axes` of type `PointSpace`, to the HDF5 file. The field
472+
is written to the `fields` group in the file, with the name `name`. The local
473+
geometry data of the `PointSpace` is written to the `local_geometry_data` group
474+
with name `name`.
475+
"""
476+
function write!(
477+
writer::HDF5Writer,
478+
field::Fields.Field,
479+
name::AbstractString,
480+
space::Spaces.AbstractPointSpace,
481+
)
482+
array = parent(field)
483+
lg_data = Grids.local_geometry_data(space)
484+
lg_type = Grids.local_geometry_type(typeof(space))
485+
lg_array = parent(lg_data)
486+
dataset = create_dataset(
487+
writer.file,
488+
"fields/$name",
489+
datatype(eltype(array)),
490+
dataspace(size(array)),
491+
)
492+
dataset[:] = array
493+
write_attribute(dataset, "type", "Field")
494+
write_attribute(
495+
dataset,
496+
"data_layout",
497+
string(nameof(typeof(Fields.field_values(field)))),
498+
)
499+
write_attribute(dataset, "field_eltype", string(eltype(field)))
500+
local_geometry_dataset = create_dataset(
501+
writer.file,
502+
"local_geometry_data/$name",
503+
datatype(eltype(array)),
504+
dataspace(size(lg_array)),
505+
)
506+
local_geometry_dataset[:] = lg_array
507+
write_attribute(
508+
local_geometry_dataset,
509+
"local_geometry_type",
510+
string(lg_type),
511+
)
512+
end
513+
514+
"""
515+
write!(
516+
writer::HDF5Writer,
517+
field::Fields.Field,
518+
name::AbstractString,
519+
space::Spaces.AbstractSpace,
520+
)
521+
522+
Write an object of type 'Field' and name 'name' to the HDF5 file.
523+
"""
524+
function write!(
525+
writer::HDF5Writer,
526+
field::Fields.Field,
527+
name::AbstractString,
528+
space::Spaces.AbstractSpace,
529+
)
455530
values = Fields.field_values(field)
456-
space = axes(field)
531+
array = parent(field)
532+
nd = ndims(array)
533+
457534
staggering = Spaces.staggering(space)
535+
topology = Spaces.topology(space)
458536
grid = Spaces.grid(space)
459537
grid_name = write!(writer, grid)
460538

461-
array = parent(field)
462-
topology = Spaces.topology(space)
463-
nd = ndims(array)
464539
if topology isa Topologies.Topology2D &&
465540
!(writer.context isa ClimaComms.SingletonCommsContext)
466541
nelems = Topologies.nelems(topology)
@@ -491,7 +566,7 @@ function write!(writer::HDF5Writer, field::Fields.Field, name::AbstractString)
491566
"data_layout",
492567
string(nameof(typeof(Fields.field_values(field)))),
493568
)
494-
write_attribute(dataset, "value_type", string(eltype(field)))
569+
write_attribute(dataset, "field_eltype", string(eltype(field)))
495570
write_attribute(dataset, "grid", grid_name)
496571
if !isnothing(staggering)
497572
write_attribute(

test/InputOutput/unit_point.jl

Lines changed: 34 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,34 @@
1+
using Test
2+
import ClimaCore: Spaces, Fields, InputOutput, Geometry
3+
using ClimaComms
4+
const comms_ctx = ClimaComms.context()
5+
6+
7+
@testset "HDF5 write/read test for 0d PointSpace" begin
8+
9+
FT = Float32
10+
11+
space = Spaces.PointSpace(comms_ctx, Geometry.ZPoint(FT(1)))
12+
field_0d = Fields.local_geometry_field(space)
13+
Y = Fields.FieldVector(; p = field_0d)
14+
15+
filename = tempname()
16+
17+
InputOutput.HDF5Writer(filename, comms_ctx) do writer
18+
InputOutput.write!(writer, "Y" => Y) # write field vector from hdf5 file
19+
end
20+
21+
InputOutput.HDF5Reader(filename, comms_ctx) do reader
22+
restart_Y = InputOutput.read_field(reader, "Y") # read fieldvector from hdf5 file
23+
@test restart_Y == Y # test if restart is exact
24+
# test if space is the same by comparing local geometry data
25+
# note that the read spaces are not "===" to the written spaces because there is no grid to cache
26+
lg_point = Spaces.local_geometry_data(axes(Y.p))[]
27+
lg_restart = Spaces.local_geometry_data(axes(restart_Y.p))[]
28+
@test typeof(lg_point) == typeof(lg_restart)
29+
@test all(
30+
getproperty(lg_point, p) == getproperty(lg_restart, p) for
31+
p in propertynames(lg_point)
32+
)
33+
end
34+
end

test/InputOutput/unit_read_type.jl

Lines changed: 48 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,48 @@
1+
using Test
2+
import ClimaCore
3+
import ClimaCore: Fields, InputOutput
4+
using ClimaComms
5+
ClimaComms.@import_required_backends
6+
include(
7+
joinpath(pkgdir(ClimaCore), "test", "TestUtilities", "TestUtilities.jl"),
8+
)
9+
import .TestUtilities as TU
10+
11+
compare_read_type(x) = InputOutput.read_type(string(eltype(x))) == eltype(x)
12+
@testset "Read field element types" begin
13+
context = ClimaComms.context()
14+
FT = Float64
15+
16+
for space in TU.all_spaces(FT; context)
17+
lg_field = Fields.local_geometry_field(space)
18+
@test compare_read_type(lg_field)
19+
coord_field = Fields.coordinate_field(space)
20+
@test compare_read_type(coord_field)
21+
end
22+
23+
space = TU.ColumnCenterFiniteDifferenceSpace(FT; context)
24+
@test compare_read_type(fill((1.0, 2.0, (3.0, 4.0, (5.0,))), space))
25+
@test compare_read_type(fill((; a = FT(0), b = (; c = FT(1))), space))
26+
# test that attempting to read a type from a string with an expression that executes code throws an error
27+
@test_throws ErrorException InputOutput.read_type("1+2")
28+
@test_throws ErrorException InputOutput.read_type("Base.rm(\"foo.jl\")")
29+
end
30+
31+
@testset "backwards compatibility with `value_type` attribute" begin
32+
context = ClimaComms.context()
33+
FT = Float64
34+
35+
space = TU.ColumnCenterFiniteDifferenceSpace(FT; context)
36+
Y = fill((1.0, 2.0, (3.0, 4.0, (5.0,))), space)
37+
filename = tempname()
38+
InputOutput.HDF5Writer(filename, context) do writer
39+
InputOutput.write!(writer, "Y" => Y) # write field vector from hdf5 file
40+
obj = writer.file["fields/Y"]
41+
InputOutput.HDF5.delete_attribute(obj, "field_eltype") # remove the field_eltype attribute
42+
InputOutput.HDF5.write_attribute(obj, "value_type", string(eltype(Y))) # add the deprecated value_type attribute
43+
end
44+
InputOutput.HDF5Reader(filename, context) do reader
45+
restart_Y = InputOutput.read_field(reader, "Y")
46+
@test eltype(restart_Y) == eltype(Y)
47+
end
48+
end

test/runtests.jl

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -96,6 +96,7 @@ UnitTest("Hypsography - 3d sphere" ,"Hypsography/3dsphere.jl"),
9696
UnitTest("Remapping" ,"Operators/remapping.jl"),
9797
UnitTest("Limiter" ,"Limiters/limiter.jl"),
9898
UnitTest("InputOutput - hdf5" ,"InputOutput/unit_hdf5.jl"),
99+
UnitTest("InputOutput - parse_type" ,"InputOutput/unit_read_type.jl"),
99100
UnitTest("InputOutput - spectralelement2d" ,"InputOutput/unit_spectralelement2d.jl"),
100101
UnitTest("InputOutput - hybrid2dbox" ,"InputOutput/unit_hybrid2dbox.jl"),
101102
UnitTest("InputOutput - hybrid2dbox_topography" ,"InputOutput/unit_hybrid2dbox_topography.jl"),
@@ -104,6 +105,7 @@ UnitTest("InputOutput - hybrid3dbox" ,"InputOutput/unit_hybrid3dbo
104105
UnitTest("InputOutput - hybrid3dcubedsphere" ,"InputOutput/unit_hybrid3dcubedsphere.jl"),
105106
UnitTest("InputOutput - hybrid3dcubedsphere_topo" ,"InputOutput/unit_hybrid3dcubedsphere_topography.jl"),
106107
UnitTest("InputOutput - finitedifferences" ,"InputOutput/unit_finitedifference.jl"),
108+
UnitTest("InputOutput - pointspaces" ,"InputOutput/unit_point.jl"),
107109
UnitTest("Array interpolation" ,"Remapping/interpolate_array.jl"),
108110
UnitTest("Array interpolation" ,"Remapping/distributed_remapping.jl"),
109111
UnitTest("Aqua" ,"aqua.jl"),

0 commit comments

Comments
 (0)