Cantera  3.1.0a1
Storage.cpp
Go to the documentation of this file.
1 /**
2  * @file Storage.cpp
3  * Definition file for class Storage.
4  */
5 
6 // This file is part of Cantera. See License.txt in the top-level directory or
7 // at https://cantera.org/license.txt for license and copyright information.
8 
9 #include "cantera/base/AnyMap.h"
10 #include "cantera/base/Storage.h"
11 
12 #if CT_USE_HDF5
13 
14 #if CT_USE_SYSTEM_HIGHFIVE
15  #include <highfive/H5Attribute.hpp>
16  #include <highfive/H5DataSet.hpp>
17  #include <highfive/H5DataSpace.hpp>
18  #include <highfive/H5DataType.hpp>
19  #include <highfive/H5File.hpp>
20  #include <highfive/H5Group.hpp>
21 #else
22  #include "cantera/ext/HighFive/H5Attribute.hpp"
23  #include "cantera/ext/HighFive/H5DataSet.hpp"
24  #include "cantera/ext/HighFive/H5DataSpace.hpp"
25  #include "cantera/ext/HighFive/H5DataType.hpp"
26  #include "cantera/ext/HighFive/H5File.hpp"
27  #include "cantera/ext/HighFive/H5Group.hpp"
28 #endif
29 
30 namespace h5 = HighFive;
31 
32 #ifdef CT_USE_HIGHFIVE_BOOLEAN
33 // HighFive 2.7.1 introduces stable native boolean support
34 typedef h5::details::Boolean H5Boolean;
35 #else
36 // Use custom Boolean definition mimicking h5py approach
37 // HighFive <=2.6.2 lacks boolean support and HighFive 2.7.0 uses alternate definition
38 enum class H5Boolean {
39  HighFiveFalse = 0,
40  HighFiveTrue = 1,
41 };
42 
43 h5::EnumType<H5Boolean> create_enum_boolean() {
44  return {{"FALSE", H5Boolean::HighFiveFalse}, {"TRUE", H5Boolean::HighFiveTrue}};
45 }
46 
47 HIGHFIVE_REGISTER_TYPE(H5Boolean, ::create_enum_boolean)
48 #endif
49 
50 #endif
51 
52 namespace Cantera
53 {
54 
55 #if CT_USE_HDF5
56 
57 Storage::Storage(string fname, bool write) : m_write(write)
58 {
59  if (m_write) {
60  m_file = make_unique<h5::File>(fname, h5::File::OpenOrCreate);
61  } else {
62  m_file = make_unique<h5::File>(fname, h5::File::ReadOnly);
63  }
64 }
65 
66 Storage::~Storage()
67 {
68  m_file->flush();
69 }
70 
71 void Storage::setCompressionLevel(int level)
72 {
73  if (level < 0 || level > 9) {
74  throw CanteraError("Storage::setCompressionLevel",
75  "Invalid compression level '{}' (needs to be 0..9).", level);
76  }
77  m_compressionLevel = level;
78 }
79 
80 bool Storage::hasGroup(const string& id) const
81 {
82  if (!m_file->exist(id)) {
83  return false;
84  }
85  if (m_file->getObjectType(id) != h5::ObjectType::Group) {
86  return false;
87  }
88  return true;
89 }
90 
91 bool Storage::checkGroupRead(const string& id) const
92 {
93  vector<string> tokens;
94  tokenizePath(id, tokens);
95  string grp = tokens[0];
96  if (!hasGroup(grp)) {
97  throw CanteraError("Storage::checkGroupRead",
98  "No group with id '{}' found at root.", grp);
99  }
100 
101  string path = grp;
102  h5::Group sub = m_file->getGroup(grp);
103  tokens.erase(tokens.begin());
104  for (auto& grp : tokens) {
105  if (!hasGroup(path + "/" + grp)) {
106  throw CanteraError("Storage::checkGroupRead",
107  "No group with id '{}' found at '{}'.", grp, path);
108  }
109  path += "/" + grp;
110  sub = sub.getGroup(grp);
111  }
112  return true;
113 }
114 
115 bool Storage::checkGroupWrite(const string& id, bool permissive)
116 {
117  if (!m_write) {
118  throw CanteraError("Storage::checkGroupWrite",
119  "Cannot write to file opened in read mode.");
120  }
121  if (id == "") {
122  throw CanteraError("Storage::checkGroupWrite",
123  "Cannot write to empty group id '' (root location).");
124  }
125  if (!m_file->exist(id)) {
126  if (!permissive) {
127  throw CanteraError("Storage::checkGroupWrite",
128  "Specified group with id '{}' does not exist.", id);
129  }
130  m_file->createGroup(id);
131  return false;
132  }
133  if (m_file->getObjectType(id) != h5::ObjectType::Group) {
134  throw CanteraError("Storage::checkGroupWrite",
135  "Unable to write to existing object with id '{}'.", id);
136  }
137  return true;
138 }
139 
140 bool Storage::checkGroup(const string& id, bool permissive)
141 {
142  try {
143  if (m_write) {
144  return checkGroupWrite(id, permissive);
145  }
146  return checkGroupRead(id);
147  } catch (const CanteraError& err) {
148  if (permissive) {
149  return false;
150  }
151  throw CanteraError("Storage::checkGroup", err.getMessage());
152  } catch (const std::exception& err) {
153  if (permissive) {
154  return false;
155  }
156  // convert HighFive exception
157  throw CanteraError("Storage::checkGroup",
158  "Encountered exception for group '{}':\n{}", id, err.what());
159  }
160 }
161 
162 void Storage::deleteGroup(const string& id)
163 {
164  try {
165  m_file->unlink(id);
166  } catch (const std::exception& err) {
167  // convert HighFive exception
168  throw CanteraError("Storage::deleteGroup",
169  "Encountered exception while deleting group '{}':\n{}", id, err.what());
170  }
171 }
172 
173 pair<size_t, set<string>> Storage::contents(const string& id) const
174 {
175  try {
176  checkGroupRead(id);
177  } catch (const CanteraError& err) {
178  throw CanteraError("Storage::contents",
179  "Caught exception for group '{}':\n", id, err.getMessage());
180  }
181  h5::Group sub = m_file->getGroup(id);
182  set<string> names;
183  size_t nDims = npos;
184  size_t nElements = 0;
185  for (auto& name : sub.listObjectNames()) {
186  if (sub.getObjectType(name) == h5::ObjectType::Dataset) {
187  h5::DataSpace space = sub.getDataSet(name).getSpace();
188  names.insert(name);
189  if (space.getNumberDimensions() < nDims) {
190  nDims = space.getNumberDimensions();
191  nElements = space.getElementCount();
192  }
193  }
194  }
195  if (nDims != 1 && nDims != npos) {
196  throw NotImplementedError("Storage::content",
197  "Encountered invalid data with {} dimensions.", nDims);
198  }
199  return std::make_pair(nElements, names);
200 }
201 
202 AnyMap readH5Attributes(const h5::Group& sub, bool recursive)
203 {
204  // restore meta data from attributes
205  AnyMap out;
206  for (auto& name : sub.listAttributeNames()) {
207  h5::Attribute attr = sub.getAttribute(name);
208  h5::DataType dtype = attr.getDataType();
209  h5::DataTypeClass dclass = dtype.getClass();
210  if (dclass == h5::DataTypeClass::Float) {
211  if (attr.getSpace().getElementCount() > 1) {
212  vector<double> values;
213  attr.read(values);
214  out[name] = values;
215  } else {
216  double value;
217  attr.read(value);
218  out[name] = value;
219  }
220  } else if (dclass == h5::DataTypeClass::Integer) {
221  if (attr.getSpace().getElementCount() > 1) {
222  vector<long int> values;
223  attr.read(values);
224  out[name] = values;
225  } else {
226  long int value;
227  attr.read(value);
228  out[name] = value;
229  }
230  } else if (dclass == h5::DataTypeClass::String) {
231  if (attr.getSpace().getElementCount() > 1) {
232  vector<string> values;
233  attr.read(values);
234  out[name] = values;
235  } else {
236  string value;
237  attr.read(value);
238  out[name] = value;
239  }
240  } else if (dclass == h5::DataTypeClass::Enum) {
241  // only booleans are supported
242  if (attr.getSpace().getElementCount() > 1) {
243  vector<H5Boolean> values;
244  attr.read(values);
245  vector<bool> bValues;
246  for (auto v : values) {
247  bValues.push_back(bool(v));
248  }
249  out[name] = bValues;
250  } else {
251  H5Boolean value;
252  attr.read(value);
253  out[name] = bool(value);
254  }
255  } else {
256  throw NotImplementedError("readH5Attributes",
257  "Unable to read attribute '{}' with type '{}'", name, dtype.string());
258  }
259  }
260 
261  if (recursive) {
262  for (auto& name : sub.listObjectNames()) {
263  if (sub.getObjectType(name) == h5::ObjectType::Group) {
264  out[name] = readH5Attributes(sub.getGroup(name), recursive);
265  }
266  }
267  }
268 
269  return out;
270 }
271 
272 bool Storage::hasAttribute(const string& id, const string& attr) const
273 {
274  try {
275  checkGroupRead(id);
276  } catch (const CanteraError& err) {
277  throw CanteraError("Storage::hasAttribute",
278  "Caught exception for group '{}':\n", id, err.getMessage());
279  }
280  h5::Group sub = m_file->getGroup(id);
281  auto names = sub.listAttributeNames();
282  return std::find(names.begin(), names.end(), attr) != names.end();
283 }
284 
285 AnyMap Storage::readAttributes(const string& id, bool recursive) const
286 {
287  try {
288  checkGroupRead(id);
289  h5::Group sub = m_file->getGroup(id);
290  return readH5Attributes(sub, recursive);
291  } catch (const Cantera::NotImplementedError& err) {
292  throw NotImplementedError("Storage::readAttribute",
293  "{} in group '{}'.", err.getMessage(), id);
294  } catch (const CanteraError& err) {
295  throw CanteraError("Storage::readAttribute",
296  "Caught exception for group '{}':\n", id, err.getMessage());
297  }
298 }
299 
300 void writeH5Attributes(h5::Group sub, const AnyMap& meta)
301 {
302  for (auto& [name, item] : meta) {
303  if (sub.hasAttribute(name)) {
304  throw NotImplementedError("writeH5Attributes",
305  "Unable to overwrite existing Attribute '{}'", name);
306  }
307  if (item.is<long int>()) {
308  int value = item.asInt();
309  h5::Attribute attr = sub.createAttribute<long int>(
310  name, h5::DataSpace::From(value));
311  attr.write(value);
312  } else if (item.is<double>()) {
313  double value = item.asDouble();
314  h5::Attribute attr = sub.createAttribute<double>(
315  name, h5::DataSpace::From(value));
316  attr.write(value);
317  } else if (item.is<string>()) {
318  string value = item.asString();
319  h5::Attribute attr = sub.createAttribute<string>(
320  name, h5::DataSpace::From(value));
321  attr.write(value);
322  } else if (item.is<bool>()) {
323  bool bValue = item.asBool();
324  H5Boolean value = bValue ?
325  H5Boolean::HighFiveTrue : H5Boolean::HighFiveFalse;
326  h5::Attribute attr = sub.createAttribute<H5Boolean>(
327  name, h5::DataSpace::From(value));
328  attr.write(value);
329  } else if (item.is<vector<long int>>()) {
330  auto values = item.as<vector<long int>>();
331  h5::Attribute attr = sub.createAttribute<long int>(
332  name, h5::DataSpace::From(values));
333  attr.write(values);
334  } else if (item.is<vector<double>>()) {
335  auto values = item.as<vector<double>>();
336  h5::Attribute attr = sub.createAttribute<double>(
337  name, h5::DataSpace::From(values));
338  attr.write(values);
339  } else if (item.is<vector<string>>()) {
340  auto values = item.as<vector<string>>();
341  h5::Attribute attr = sub.createAttribute<string>(
342  name, h5::DataSpace::From(values));
343  attr.write(values);
344  } else if (item.is<vector<bool>>()) {
345  auto bValue = item.as<vector<bool>>();
346  vector<H5Boolean> values;
347  for (auto b : bValue) {
348  values.push_back(b ?
349  H5Boolean::HighFiveTrue : H5Boolean::HighFiveFalse);
350  }
351  h5::Attribute attr = sub.createAttribute<H5Boolean>(
352  name, h5::DataSpace::From(values));
353  attr.write(values);
354  } else if (item.is<AnyMap>()) {
355  // step into recursion
356  auto value = item.as<AnyMap>();
357  auto grp = sub.createGroup(name);
358  writeH5Attributes(grp, value);
359  } else {
360  throw NotImplementedError("writeH5Attributes",
361  "Unable to write attribute '{}' with type '{}'",
362  name, item.type_str());
363  }
364  }
365 }
366 
367 void Storage::writeAttributes(const string& id, const AnyMap& meta)
368 {
369  try {
370  checkGroupWrite(id, false);
371  h5::Group sub = m_file->getGroup(id);
372  writeH5Attributes(sub, meta);
373  } catch (const Cantera::NotImplementedError& err) {
374  throw NotImplementedError("Storage::writeAttribute",
375  "{} in group '{}'.", err.getMessage(), id);
376  } catch (const CanteraError& err) {
377  // rethrow with public method attribution
378  throw CanteraError("Storage::writeAttributes", "{}", err.getMessage());
379  } catch (const std::exception& err) {
380  // convert HighFive exception
381  throw CanteraError("Storage::writeAttributes",
382  "Encountered exception for group '{}':\n{}", id, err.what());
383  }
384 }
385 
386 AnyValue Storage::readData(const string& id,
387  const string& name, size_t rows, size_t cols) const
388 {
389  try {
390  checkGroupRead(id);
391  } catch (const CanteraError& err) {
392  throw CanteraError("Storage::readData",
393  "Caught exception for group '{}':\n", id, err.getMessage());
394  }
395  h5::Group sub = m_file->getGroup(id);
396  if (!sub.exist(name)) {
397  throw CanteraError("Storage::readData",
398  "DataSet '{}' not found in group '{}'.", name, id);
399  }
400  h5::DataSet dataset = sub.getDataSet(name);
401  h5::DataSpace space = dataset.getSpace();
402  size_t ndim = space.getNumberDimensions();
403  if (cols == 0 && ndim != 1) {
404  throw CanteraError("Storage::readData",
405  "Shape of DataSet '{}' is inconsistent; expected one dimensions but "
406  "received {}.", name, ndim);
407  } else if (cols != 0 && cols != npos && ndim != 2) {
408  throw CanteraError("Storage::readData",
409  "Shape of DataSet '{}' is inconsistent; expected two dimensions but "
410  "received {}.", name, ndim);
411  }
412  if (ndim == 0 || ndim > 2) {
413  throw NotImplementedError("Storage::readData",
414  "Cannot process DataSet '{}' as data has {} dimensions.", name, ndim);
415  }
416  const auto& shape = space.getDimensions();
417  if (shape[0] != rows) {
418  throw CanteraError("Storage::readData",
419  "Shape of DataSet '{}' is inconsistent; expected {} rows "
420  "but received {}.", name, rows, shape[0]);
421  }
422  if (cols != 0 && cols != npos && shape[1] != cols) {
423  throw CanteraError("Storage::readData",
424  "Shape of DataSet '{}' is inconsistent; expected {} columns "
425  "but received {}.", name, cols, shape[1]);
426  }
427  AnyValue out;
428  const auto datatype = dataset.getDataType().getClass();
429  if (datatype == h5::DataTypeClass::Float) {
430  try {
431  if (ndim == 1) {
432  vector<double> data;
433  dataset.read(data);
434  out = data;
435  } else { // ndim == 2
436  vector<vector<double>> data;
437  dataset.read(data);
438  out = data;
439  }
440  } catch (const std::exception& err) {
441  throw NotImplementedError("Storage::readData",
442  "Encountered HighFive exception for DataSet '{}' in group '{}':\n{}",
443  name, id, err.what());
444  }
445  } else if (datatype == h5::DataTypeClass::Integer) {
446  try {
447  if (ndim == 1) {
448  vector<long int> data;
449  dataset.read(data);
450  out = data;
451  } else { // ndim == 2
452  vector<vector<long int>> data;
453  dataset.read(data);
454  out = data;
455  }
456  } catch (const std::exception& err) {
457  throw NotImplementedError("Storage::readData",
458  "Encountered HighFive exception for DataSet '{}' in group '{}':\n{}",
459  name, id, err.what());
460  }
461  } else if (datatype == h5::DataTypeClass::String) {
462  try {
463  if (ndim == 1) {
464  vector<string> data;
465  dataset.read(data);
466  out = data;
467  } else { // ndim == 2
468  vector<vector<string>> data;
469  dataset.read(data);
470  out = data;
471  }
472  } catch (const std::exception& err) {
473  throw NotImplementedError("Storage::readData",
474  "Encountered HighFive exception for DataSet '{}' in group '{}':\n{}",
475  name, id, err.what());
476  }
477  } else {
478  throw NotImplementedError("Storage::readData",
479  "DataSet '{}' is not readable.", name);
480  }
481  return out;
482 }
483 
484 void Storage::writeData(const string& id, const string& name, const AnyValue& data)
485 {
486  try {
487  checkGroupWrite(id, false);
488  } catch (const CanteraError& err) {
489  // rethrow with public method attribution
490  throw CanteraError("Storage::writeData", "{}", err.getMessage());
491  } catch (const std::exception& err) {
492  // convert HighFive exception
493  throw CanteraError("Storage::writeData",
494  "Encountered exception for group '{}':\n{}", id, err.what());
495  }
496  h5::Group sub = m_file->getGroup(id);
497  if (sub.exist(name)) {
498  throw NotImplementedError("Storage::writeData",
499  "Unable to overwrite existing DataSet '{}' in group '{}'.", name, id);
500  }
501  size_t size = data.vectorSize();
502  auto [rows, cols] = data.matrixShape();
503  if (size == npos && rows == npos) {
504  throw CanteraError("Storage::writeData",
505  "Cannot write DataSet '{}' in group '{}' as input data with type\n"
506  "'{}'\nis neither a vector nor a matrix.", name, id, data.type_str());
507  }
508  vector<size_t> dims{data.vectorSize()};
509  if (data.isVector<long int>()) {
510  h5::DataSet dataset = sub.createDataSet<long int>(name, h5::DataSpace(dims));
511  dataset.write(data.asVector<long int>());
512  return;
513  }
514  if (data.isVector<double>()) {
515  h5::DataSet dataset = sub.createDataSet<double>(name, h5::DataSpace(dims));
516  dataset.write(data.asVector<double>());
517  return;
518  }
519  if (data.isVector<string>()) {
520  h5::DataSet dataset = sub.createDataSet<string>(name, h5::DataSpace(dims));
521  dataset.write(data.asVector<string>());
522  return;
523  }
524  if (cols != npos) {
525  dims.clear();
526  dims.push_back(rows);
527  dims.push_back(cols);
528  } else {
529  throw NotImplementedError("Storage::writeData",
530  "Cannot write DataSet '{}' in group '{}' as input data with type\n"
531  "'{}'\nis not supported.", name, id, data.type_str());
532  }
533  if (m_compressionLevel) {
534  // Set chunk size to single chunk and apply compression level; for caveats, see
535  // https://stackoverflow.com/questions/32994766/compressed-files-bigger-in-h5py
536  h5::DataSpace space(dims, dims); //{h5::DataSpace::UNLIMITED, dims[1]});
537  h5::DataSetCreateProps props;
538  props.add(h5::Chunking(vector<hsize_t>{dims[0], dims[1]}));
539  props.add(h5::Deflate(m_compressionLevel));
540  if (data.isVector<vector<long int>>()) {
541  h5::DataSet dataset = sub.createDataSet<long int>(name, space, props);
542  dataset.write(data.asVector<vector<long int>>());
543  } else if (data.isVector<vector<double>>()) {
544  h5::DataSet dataset = sub.createDataSet<double>(name, space, props);
545  dataset.write(data.asVector<vector<double>>());
546  } else if (data.isVector<vector<string>>()) {
547  h5::DataSet dataset = sub.createDataSet<string>(name, space, props);
548  dataset.write(data.asVector<vector<string>>());
549  } else {
550  throw NotImplementedError("Storage::writeData",
551  "Cannot write DataSet '{}' in group '{}' as input data with type\n"
552  "'{}'\nis not supported.", name, id, data.type_str());
553  }
554  } else {
555  h5::DataSpace space(dims);
556  if (data.isVector<vector<long int>>()) {
557  h5::DataSet dataset = sub.createDataSet<long int>(name, space);
558  dataset.write(data.asVector<vector<long int>>());
559  } else if (data.isVector<vector<double>>()) {
560  h5::DataSet dataset = sub.createDataSet<double>(name, space);
561  dataset.write(data.asVector<vector<double>>());
562  } else if (data.isVector<vector<string>>()) {
563  h5::DataSet dataset = sub.createDataSet<string>(name, space);
564  dataset.write(data.asVector<vector<string>>());
565  } else {
566  throw NotImplementedError("Storage::writeData",
567  "Cannot write DataSet '{}' in group '{}' as input data with type\n"
568  "'{}'\nis not supported.", name, id, data.type_str());
569  }
570  }
571 }
572 
573 #else
574 
575 Storage::Storage(string fname, bool write)
576 {
577  throw CanteraError("Storage::Storage",
578  "Saving to HDF requires HighFive installation.");
579 }
580 
581 Storage::~Storage()
582 {
583 }
584 
585 void Storage::setCompressionLevel(int level)
586 {
587  throw CanteraError("Storage::setCompressionLevel",
588  "Saving to HDF requires HighFive installation.");
589 }
590 
591 bool Storage::hasGroup(const string& id) const
592 {
593  throw CanteraError("Storage::hasGroup",
594  "Saving to HDF requires HighFive installation.");
595 }
596 
597 bool Storage::checkGroup(const string& id, bool permissive)
598 {
599  throw CanteraError("Storage::checkGroup",
600  "Saving to HDF requires HighFive installation.");
601 }
602 
603 void Storage::deleteGroup(const string& id)
604 {
605  throw CanteraError("Storage::deleteGroup",
606  "Saving to HDF requires HighFive installation.");
607 }
608 
609 pair<size_t, set<string>> Storage::contents(const string& id) const
610 {
611  throw CanteraError("Storage::contents",
612  "Saving to HDF requires HighFive installation.");
613 }
614 
615 bool Storage::hasAttribute(const string& id, const string& attr) const
616 {
617  throw CanteraError("Storage::hasAttribute",
618  "Saving to HDF requires HighFive installation.");
619 }
620 
621 AnyMap Storage::readAttributes(const string& id, bool recursive) const
622 {
623  throw CanteraError("Storage::readAttributes",
624  "Saving to HDF requires HighFive installation.");
625 }
626 
627 void Storage::writeAttributes(const string& id, const AnyMap& meta)
628 {
629  throw CanteraError("Storage::writeAttributes",
630  "Saving to HDF requires HighFive installation.");
631 }
632 
633 AnyValue Storage::readData(const string& id,
634  const string& name, size_t rows, size_t cols) const
635 {
636  throw CanteraError("Storage::readData",
637  "Saving to HDF requires HighFive installation.");
638 }
639 
640 void Storage::writeData(const string& id,
641  const string& name, const AnyValue& data)
642 {
643  throw CanteraError("Storage::writeData",
644  "Saving to HDF requires HighFive installation.");
645 }
646 
647 #endif
648 
649 }
A map of string keys to values whose type can vary at runtime.
Definition: AnyMap.h:427
A wrapper for a variable whose type is determined at runtime.
Definition: AnyMap.h:86
Base class for exceptions thrown by Cantera classes.
Definition: ctexceptions.h:66
virtual string getMessage() const
Method overridden by derived classes to format the error message.
An error indicating that an unimplemented function has been called.
Definition: ctexceptions.h:195
void tokenizePath(const string &in_val, vector< string > &v)
This function separates a string up into tokens according to the location of path separators.
Namespace for the Cantera kernel.
Definition: AnyMap.cpp:564
const size_t npos
index returned by functions to indicate "no position"
Definition: ct_defs.h:180