GridFormat 0.6.0
I/O-Library for grid-like data structures
Loading...
Searching...
No Matches
hdf_image_grid_writer.hpp
Go to the documentation of this file.
1// SPDX-FileCopyrightText: 2022-2023 Dennis Gläser <dennis.glaeser@iws.uni-stuttgart.de>
2// SPDX-License-Identifier: MIT
8#ifndef GRIDFORMAT_VTK_HDF_IMAGE_GRID_WRITER_HPP_
9#define GRIDFORMAT_VTK_HDF_IMAGE_GRID_WRITER_HPP_
10#if GRIDFORMAT_HAVE_HIGH_FIVE
11
12#include <ranges>
13#include <iterator>
14#include <algorithm>
15#include <utility>
16#include <tuple>
17
18#include <gridformat/common/exceptions.hpp>
19#include <gridformat/common/md_layout.hpp>
20#include <gridformat/common/concepts.hpp>
21#include <gridformat/common/matrix.hpp>
22#include <gridformat/common/ranges.hpp>
23#include <gridformat/common/type_traits.hpp>
24#include <gridformat/common/lvalue_reference.hpp>
25#include <gridformat/common/field_transformations.hpp>
26
27#include <gridformat/parallel/communication.hpp>
28#include <gridformat/parallel/concepts.hpp>
29
32
36
37namespace GridFormat {
38
39template<bool is_transient, Concepts::ImageGrid Grid, Concepts::Communicator Communicator = NullCommunicator>
40class VTKHDFImageGridWriterImpl : public GridDetail::WriterBase<is_transient, Grid>::type {
41 static constexpr int root_rank = 0;
42 static constexpr std::size_t dim = dimension<Grid>;
43 static constexpr std::size_t vtk_space_dim = 3;
44 static constexpr std::array<std::size_t, 2> version{1, 0};
45
46 using CT = CoordinateType<Grid>;
47 using IOContext = VTKHDF::IOContext;
48 using HDF5File = HDF5::File<Communicator>;
49
50 struct ImageSpecs {
51 const std::array<CT, dim> origin;
52 const std::array<CT, dim> spacing;
53 const std::array<std::size_t, dim> extents;
54
55 template<typename O, typename S, typename E>
56 ImageSpecs(O&& origin, S&& spacing, E&& extents)
57 : origin{Ranges::to_array<dim, CT>(std::forward<O>(origin))}
58 , spacing{Ranges::to_array<dim, CT>(std::forward<S>(spacing))}
59 , extents{Ranges::to_array<dim, std::size_t>(std::forward<E>(extents))}
60 {}
61 };
62
63 static constexpr WriterOptions writer_opts{
64 .use_structured_grid_ordering = true,
65 .append_null_terminator_to_strings = true
66 };
67
68 public:
69 explicit VTKHDFImageGridWriterImpl(LValueReferenceOf<const Grid> grid)
70 requires(std::is_same_v<Communicator, NullCommunicator>)
71 : GridWriter<Grid>(grid.get(), ".hdf", writer_opts)
72 {}
73
74 explicit VTKHDFImageGridWriterImpl(LValueReferenceOf<const Grid> grid, const Communicator& comm)
75 requires(std::is_copy_constructible_v<Communicator>)
76 : GridWriter<Grid>(grid.get(), ".hdf", writer_opts)
77 , _comm{comm}
78 {}
79
80 explicit VTKHDFImageGridWriterImpl(LValueReferenceOf<const Grid> grid,
81 std::string filename_without_extension,
83 .static_grid = true,
84 .static_meta_data = false
85 })
86 requires(is_transient && std::is_same_v<Communicator, NullCommunicator>)
87 : VTKHDFImageGridWriterImpl(grid.get(), NullCommunicator{}, filename_without_extension, std::move(opts))
88 {}
89
90 explicit VTKHDFImageGridWriterImpl(LValueReferenceOf<const Grid> grid,
91 const Communicator& comm,
92 std::string filename_without_extension,
94 .static_grid = true,
95 .static_meta_data = false
96 })
97 requires(is_transient && std::is_copy_constructible_v<Communicator>)
98 : TimeSeriesGridWriter<Grid>(grid.get(), writer_opts)
99 , _comm{comm}
100 , _timeseries_filename{std::move(filename_without_extension) + ".hdf"}
101 , _transient_opts{std::move(opts)} {
102 if (!_transient_opts.static_grid)
103 throw ValueError("Transient VTK-HDF ImageData files do not support evolving grids");
104 }
105
106 const Communicator& communicator() const {
107 return _comm;
108 }
109
110 private:
111 void _write(std::ostream&) const {
112 throw InvalidState("VTKHDFImageGridWriter does not support export into stream");
113 }
114
115 std::string _write([[maybe_unused]] double t) {
116 if constexpr (!is_transient)
117 throw InvalidState("This overload only works for transient output");
118
119 if (this->_step_count == 0)
120 HDF5File::clear(_timeseries_filename, _comm);
121
122 { // scope to close the file before writing meta data on rank 0 with serial I/O
123 HDF5File file{_timeseries_filename, _comm, HDF5File::Mode::append};
124 _write_to(file);
125 file.write_attribute(this->_step_count+1, "/VTKHDF/Steps/NSteps");
126 file.write(std::array{t}, "/VTKHDF/Steps/Values");
127 }
128 _write_meta_data(_timeseries_filename);
129 return _timeseries_filename;
130 }
131
132 void _write(const std::string& filename_with_ext) const {
133 if constexpr (is_transient)
134 throw InvalidState("This overload only works for non-transient output");
135 { // scope to close the file before writing meta data on rank 0 with serial I/O
136 HDF5File file{filename_with_ext, _comm, HDF5File::Mode::overwrite};
137 _write_to(file);
138 }
139 _write_meta_data(filename_with_ext);
140 }
141
142 // hdf5 cannot write variable-length strings into files opened for parallel I/O, not even from
143 // a single rank. Therefore, all meta data is written by rank 0 once the (parallel) file is closed,
144 // reopened with the default driver. Meta data is assumed to be the same on all ranks.
145 void _write_meta_data(const std::string& filename) const {
146 VTKHDF::write_on_root(filename, _comm, root_rank, [&] (HDF5::File<>& file) {
147 _write_meta_data(file);
148 });
149 }
150
151 void _write_meta_data(HDF5::File<>& file) const {
152 std::ranges::for_each(this->_meta_data_field_names(), [&] (const std::string& name) {
154 if constexpr (is_transient) {
155 if (this->_step_count > 0 && _transient_opts.static_meta_data)
157 else
158 VTKHDF::write_field_data_step(file, name, this->_get_meta_data_field_ptr(name));
159 } else {
160 VTKHDF::write_field_data(file, name, this->_get_meta_data_field_ptr(name));
161 }
162 });
163 }
164
165 void _write_to(HDF5File& file) const {
166 const ImageSpecs my_specs{origin(this->grid()), spacing(this->grid()), extents(this->grid())};
167 const auto [overall_specs, my_offset] = _get_image_specs(my_specs);
168 const auto cell_slice_base = _make_slice(
169 overall_specs.extents,
170 my_specs.extents,
171 my_offset
172 );
173
174 // in order to avoid overlapping hyperslabs for point data in parallel I/O,
175 // we only write the last entries of the slab (per direction) when our portion
176 // of the image is the last portion of the overall image in that direction.
177 const auto my_point_extents = Ranges::to_array<dim>(
178 std::views::iota(std::size_t{0}, dim)
179 | std::views::transform([&] (std::size_t dir) {
180 const auto overall_end = overall_specs.extents[dir];
181 const auto my_end = my_specs.extents[dir] + my_offset[dir];
182 return my_end < overall_end ? my_specs.extents[dir] : my_specs.extents[dir] + 1;
183 }));
184 const auto point_slice_base = _make_slice<true>(
185 overall_specs.extents,
186 my_point_extents,
187 my_offset
188 );
189
190 file.write_attribute(std::array<std::size_t, 2>{(is_transient ? 2 : 1), 0}, "/VTKHDF/Version");
191 file.write_attribute(Ranges::to_array<vtk_space_dim>(overall_specs.origin), "/VTKHDF/Origin");
192 file.write_attribute(Ranges::to_array<vtk_space_dim>(overall_specs.spacing), "/VTKHDF/Spacing");
193 file.write_attribute(VTK::CommonDetail::get_extents(overall_specs.extents), "/VTKHDF/WholeExtent");
194 file.write_attribute(_get_direction(), "/VTKHDF/Direction");
195 file.write_attribute("ImageData", "/VTKHDF/Type");
196
197 std::vector<std::size_t> non_zero_extents;
198 std::ranges::copy(
199 my_specs.extents | std::views::filter([] (auto e) { return e != 0; }),
200 std::back_inserter(non_zero_extents)
201 );
202
203 std::ranges::for_each(this->_point_field_names(), [&] (const std::string& name) {
205 auto field_ptr = _reshape(
206 VTK::make_vtk_field(this->_get_point_field_ptr(name)),
207 Ranges::incremented(non_zero_extents, 1) | std::views::reverse,
208 point_slice_base.count
209 );
210 _write_field(file, field_ptr, "/VTKHDF/PointData/" + name, point_slice_base);
211 });
212
213 std::ranges::for_each(this->_cell_field_names(), [&] (const std::string& name) {
215 auto field_ptr = _reshape(
216 VTK::make_vtk_field(this->_get_cell_field_ptr(name)),
217 non_zero_extents | std::views::reverse,
218 cell_slice_base.count
219 );
220 _write_field(file, field_ptr, "/VTKHDF/CellData/" + name, cell_slice_base);
221 });
222 }
223
224 template<std::ranges::range E, std::ranges::range S>
225 FieldPtr _reshape(FieldPtr f, const E& row_major_extents, S&& _slice_end) const {
226 auto flat = _flatten(f);
227 auto structured = _make_structured(flat, row_major_extents);
228 const auto structured_layout = structured->layout();
229
230 std::vector<std::size_t> slice_end;
231 std::ranges::copy(_slice_end, std::back_inserter(slice_end));
232 for (std::size_t i = slice_end.size(); i < structured_layout.dimension(); ++i)
233 slice_end.push_back(structured_layout.extent(i));
234 return transform(structured, FieldTransformation::take_slice({
235 .from = std::vector<std::size_t>(slice_end.size(), 0),
236 .to = slice_end
237 }));
238 }
239
240 FieldPtr _flatten(FieldPtr f) const {
241 const auto layout = f->layout();
242 if (layout.dimension() <= 2)
243 return f;
244 // vtk requires tensors to be made flat
245 auto nl = MDLayout{{layout.extent(0), layout.number_of_entries(1)}};
246 return transform(f, FieldTransformation::reshape_to(std::move(nl)));
247 }
248
249 template<std::ranges::range E>
250 FieldPtr _make_structured(FieldPtr f, const E& row_major_extents) const {
251 const auto layout = f->layout();
252 std::vector<std::size_t> target_layout(Ranges::size(row_major_extents));
253 std::ranges::copy(row_major_extents, target_layout.begin());
254 if (layout.dimension() > 1)
255 target_layout.push_back(layout.extent(1));
256 return transform(f, FieldTransformation::reshape_to(MDLayout{std::move(target_layout)}));
257 }
258
259 auto _get_image_specs(const ImageSpecs& piece_specs) const {
260 using OffsetType = std::ranges::range_value_t<decltype(piece_specs.extents)>;
261 if (Parallel::size(_comm) > 1) {
263 const auto all_origins = Parallel::gather(_comm, piece_specs.origin, root_rank);
264 const auto all_extents = Parallel::gather(_comm, piece_specs.extents, root_rank);
265 const auto is_negative_axis = VTK::CommonDetail::structured_grid_axis_orientation(piece_specs.spacing);
266 const auto [exts_begin, exts_end, whole_extent, origin] = helper.compute_extents_and_origin(
267 all_origins,
268 all_extents,
269 is_negative_axis,
270 basis(this->grid())
271 );
272
273 const auto my_whole_extent = Parallel::broadcast(_comm, whole_extent, root_rank);
274 const auto my_whole_origin = Parallel::broadcast(_comm, origin, root_rank);
275 const auto my_extent_offset = Parallel::scatter(_comm, Ranges::flat(exts_begin), root_rank);
276 return std::make_tuple(
277 ImageSpecs{my_whole_origin, piece_specs.spacing, my_whole_extent},
278 my_extent_offset
279 );
280 } else {
281 return std::make_tuple(piece_specs, std::vector<OffsetType>(dim, 0));
282 }
283 }
284
285 template<bool increment = false, typename TotalExtents, typename Extents, typename Offsets>
286 HDF5::Slice _make_slice(const TotalExtents& total_extents,
287 const Extents& extents,
288 const Offsets& offsets) const {
289 HDF5::Slice result;
290 result.total_size.emplace();
291
292 // use only those dimensions that are not zero
293 std::vector<bool> is_nonzero;
294 std::ranges::copy(
295 total_extents | std::views::transform([] (const auto& v) { return v != 0; }),
296 std::back_inserter(is_nonzero)
297 );
298 std::ranges::for_each(total_extents, [&, i=0] (const auto& value) mutable {
299 if (is_nonzero[i++])
300 result.total_size.value().push_back(value + (increment ? 1 : 0));
301 });
302 std::ranges::for_each(extents, [&, i=0] (const auto& value) mutable {
303 if (is_nonzero[i++])
304 result.count.push_back(value);
305 });
306 std::ranges::for_each(offsets, [&, i=0] (const auto& value) mutable {
307 if (is_nonzero[i++])
308 result.offset.push_back(value);
309 });
310
311 // slices in VTK are accessed with the last coordinate first (i.e. values[z][y][x])
312 std::ranges::reverse(result.total_size.value());
313 std::ranges::reverse(result.count);
314 std::ranges::reverse(result.offset);
315
316 return result;
317 }
318
319 auto _get_direction() const {
320 using T = MDRangeScalar<decltype(basis(this->grid()))>;
321 std::array<T, vtk_space_dim*vtk_space_dim> coefficients;
322 std::ranges::fill(coefficients, T{0});
323 std::ranges::for_each(
324 Matrix{basis(this->grid())}.transposed(),
325 [it = coefficients.begin()] (const std::ranges::range auto& row) mutable {
326 std::ranges::copy(row, it);
327 std::advance(it, vtk_space_dim);
328 }
329 );
330 return coefficients;
331 }
332
333 void _write_field(HDF5File& file,
334 FieldPtr field,
335 const std::string& path,
336 const HDF5::Slice& slice) const {
337 const std::size_t dimension_offset = is_transient ? 1 : 0;
338 std::vector<std::size_t> size(slice.total_size.value().size() + dimension_offset);
339 std::vector<std::size_t> count(slice.count.size() + dimension_offset);
340 std::vector<std::size_t> offset(slice.offset.size() + dimension_offset);
341 std::ranges::copy(slice.total_size.value(), std::ranges::begin(size | std::views::drop(dimension_offset)));
342 std::ranges::copy(slice.count, std::ranges::begin(count | std::views::drop(dimension_offset)));
343 std::ranges::copy(slice.offset, std::ranges::begin(offset | std::views::drop(dimension_offset)));
344
345 const auto layout = field->layout();
346 const bool is_vector_field = layout.dimension() > size.size() - dimension_offset;
347 if (is_vector_field)
348 std::ranges::for_each(
349 std::views::iota(size.size() - dimension_offset, layout.dimension()),
350 [&] (const std::size_t codim) {
351 size.push_back(layout.extent(codim));
352 count.push_back(layout.extent(codim));
353 offset.push_back(0);
354 }
355 );
356
357 if constexpr (is_transient) {
358 size.at(0) = 1;
359 count.at(0) = 1;
360 offset.at(0) = 0;
361 FieldPtr sub_field = transform(field, FieldTransformation::as_sub_field);
362 file.write(*sub_field, path, HDF5::Slice{
363 .offset = std::move(offset),
364 .count = std::move(count),
365 .total_size = std::move(size)
366 });
367 } else {
368 file.write(*field, path, HDF5::Slice{
369 .offset = std::move(offset),
370 .count = std::move(count),
371 .total_size = std::move(size)
372 });
373 }
374 }
375
376 Communicator _comm;
377 std::string _timeseries_filename = "";
378 VTK::HDFTransientOptions _transient_opts;
379};
380
385template<Concepts::ImageGrid G, Concepts::Communicator C = NullCommunicator>
386class VTKHDFImageGridWriter : public VTKHDFImageGridWriterImpl<false, G, C> {
387 using ParentType = VTKHDFImageGridWriterImpl<false, G, C>;
388 public:
389 using ParentType::ParentType;
390};
391
396template<Concepts::ImageGrid G, Concepts::Communicator C = NullCommunicator>
397class VTKHDFImageGridTimeSeriesWriter : public VTKHDFImageGridWriterImpl<true, G, C> {
398 using ParentType = VTKHDFImageGridWriterImpl<true, G, C>;
399 public:
400 using ParentType::ParentType;
401};
402
403template<Concepts::ImageGrid G>
405template<Concepts::ImageGrid G, Concepts::Communicator C>
407
408template<Concepts::ImageGrid G>
409VTKHDFImageGridTimeSeriesWriter(const G&, std::string, VTK::HDFTransientOptions = {}) -> VTKHDFImageGridTimeSeriesWriter<G, NullCommunicator>;
410template<Concepts::ImageGrid G, Concepts::Communicator C>
411VTKHDFImageGridTimeSeriesWriter(const G&, const C&, std::string, VTK::HDFTransientOptions = {}) -> VTKHDFImageGridTimeSeriesWriter<G, C>;
412
413
414namespace Traits {
415
416template<typename... Args>
417struct WritesConnectivity<VTKHDFImageGridWriter<Args...>> : public std::false_type {};
418
419template<typename... Args>
420struct WritesConnectivity<VTKHDFImageGridTimeSeriesWriter<Args...>> : public std::false_type {};
421
422} // namespace Traits
423} // namespace GridFormat
424
425#endif // GRIDFORMAT_HAVE_HIGH_FIVE
426#endif // GRIDFORMAT_VTK_HDF_IMAGE_GRID_WRITER_HPP_
Abstract base class for grid file writers.
Definition writer.hpp:323
Abstract base class for time series file writers.
Definition writer.hpp:364
Writer for the transient VTK HDF file format for image grids.
Definition hdf_image_grid_writer.hpp:397
Writer for the VTK HDF file format for image grids.
Definition hdf_image_grid_writer.hpp:386
Grid concepts.
Base classes for grid data writers.
std::shared_ptr< const Field > FieldPtr
Pointer type used by writers/readers for fields.
Definition field.hpp:205
Common functionality for writing VTK HDF files.
void write_field_data_step(HDF5::File<> &file, const std::string &name, FieldPtr field)
Append a time step to a field data array of a transient file.
Definition hdf_common.hpp:213
void write_field_data_step_referencing(std::size_t step_index, HDF5::File<> &file, const std::string &name)
Append a time step to a field data array of a transient file, referring to the data of an earlier ste...
Definition hdf_common.hpp:232
void write_field_data(HDF5::File<> &file, const std::string &name, FieldPtr field)
Write a field data array into a file that is not transient.
Definition hdf_common.hpp:199
void check_array_name(const std::string &name)
Throw if the given name cannot be used as a vtk-hdf array name.
Definition hdf_common.hpp:112
void write_on_root(const std::string &filename, const C &comm, int root, Write &&write)
Open the given file with the default (non-MPI) driver on the root rank and pass it to write.
Definition hdf_common.hpp:298
Helper function for writing parallel VTK files.
Can be specialized by writers in case the file format does not contain connectivity information.
Definition writer.hpp:37
Helper class to store processor offsets in parallel writes.
Definition hdf_common.hpp:51
Options for transient vtk-hdf file formats.
Definition hdf_common.hpp:40
Options that writer implementations can pass to the base class.
Definition writer.hpp:60
Common functionality for VTK writers.