diff --git a/docs/src/core/index.rst b/docs/src/core/index.rst index 2a3691316..66bbcffb7 100644 --- a/docs/src/core/index.rst +++ b/docs/src/core/index.rst @@ -8,6 +8,7 @@ WIP :maxdepth: 2 reference/c/index + reference/cxx/index reference/json-formats units diff --git a/docs/src/core/reference/cxx/index.rst b/docs/src/core/reference/cxx/index.rst new file mode 100644 index 000000000..9a4a7add3 --- /dev/null +++ b/docs/src/core/reference/cxx/index.rst @@ -0,0 +1,17 @@ +.. _cxx-api-core: + +C++ API reference +================= + +WIP + +The functions and types provided in ``metatomic.hpp`` can be grouped in four +main groups: + +.. toctree:: + :maxdepth: 1 + + system + model + plugin + misc diff --git a/docs/src/core/reference/cxx/misc.rst b/docs/src/core/reference/cxx/misc.rst new file mode 100644 index 000000000..26ba29607 --- /dev/null +++ b/docs/src/core/reference/cxx/misc.rst @@ -0,0 +1,14 @@ +Miscellaneous +============= + + +Error handling +^^^^^^^^^^^^^^ + +.. doxygenclass:: metatomic::Error + + +Unit conversion +^^^^^^^^^^^^^^^ + +.. doxygenfunction:: metatomic::unit_conversion_factor diff --git a/docs/src/core/reference/cxx/model.rst b/docs/src/core/reference/cxx/model.rst new file mode 100644 index 000000000..75338c89d --- /dev/null +++ b/docs/src/core/reference/cxx/model.rst @@ -0,0 +1,2 @@ +Model +===== diff --git a/docs/src/core/reference/cxx/plugin.rst b/docs/src/core/reference/cxx/plugin.rst new file mode 100644 index 000000000..67cd50b04 --- /dev/null +++ b/docs/src/core/reference/cxx/plugin.rst @@ -0,0 +1,2 @@ +Plugin system +============= diff --git a/docs/src/core/reference/cxx/system.rst b/docs/src/core/reference/cxx/system.rst new file mode 100644 index 000000000..3dcbaeea1 --- /dev/null +++ b/docs/src/core/reference/cxx/system.rst @@ -0,0 +1,2 @@ +System +====== diff --git a/docs/src/core/units.rst b/docs/src/core/units.rst index c9415ed9f..6c50603ca 100644 --- a/docs/src/core/units.rst +++ b/docs/src/core/units.rst @@ -6,21 +6,25 @@ Units Models in metatensor can use arbitrary units for their inputs and outputs. The unit conversion system allows models to specify the units they expect and receive data in any compatible unit, with automatic conversion handled by -:c:func:`mta_execute_model`. +during model execution. -The :c:func:`mta_unit_conversion_factor` function parses two unit expressions, -checks that they have compatible physical dimensions, and returns the -multiplicative conversion factor: +Unit parsing is handled by one of the following functions: -.. code-block:: c +- :c:func:`mta_unit_conversion_factor` in C +- :cpp:func:`metatomic::unit_conversion_factor` in C++ + +These functions parses two unit expressions, checks that they have compatible +physical dimensions, and returns the multiplicative conversion factor. For +example, in C++: + +.. code-block:: C++ // How many eV are in one kJ/mol? - double factor; - mta_unit_conversion_factor("kJ/mol", "eV", &factor); + double factor = metatomic::unit_conversion_factor("kJ/mol", "eV"); // factor ≈ 0.01036 // How many GPa are in one eV/A^3? - mta_unit_conversion_factor("eV/A^3", "GPa", &factor); + factor = metatomic::unit_conversion_factor("eV/A^3", "GPa"); // factor ≈ 160.22 If either (or both) unit strings are empty, the conversion returns ``1.0`` diff --git a/metatomic-core/include/metatomic.h b/metatomic-core/include/metatomic.h index 9273007d0..e7de4118b 100644 --- a/metatomic-core/include/metatomic.h +++ b/metatomic-core/include/metatomic.h @@ -380,18 +380,18 @@ void mta_string_free(mta_string_t string); const char *mta_string_view(mta_string_t string); /** - * Get the multiplicative conversion factor to use to convert from - * `from_unit` to `to_unit`. Both units are parsed as expressions (e.g. - * "kJ/mol/A^2", "(eV*u)^(1/2)") and their dimensions must match. + * Get the multiplicative conversion factor to use to convert from `from_unit` + * to `to_unit`. Both units are parsed as expressions (e.g. `kJ / mol / A^2`, + * `(eV * u)^(1/2)`) and their dimensions must match. * - * Unit expressions are built from base units combined with `*`, `/`, `^`, - * and parentheses. Unit lookup is case-insensitive, and whitespace is - * ignored. For example: + * @verbatim embed:rst:leading-asterisk * - * - `"kJ/mol"` -- energy per mole - * - `"eV/Angstrom^3"` -- pressure - * - `"(eV*u)^(1/2)"` -- momentum (fractional powers) - * - `"Hartree/Bohr"` -- force in atomic units + * .. seealso:: + * + * The general documentation for :ref:`units`, with the expression + * syntax and list of supported base units. + * + * @endverbatim * * @param from_unit A null-terminated C string containing the unit to convert from. * @param to_unit A null-terminated C string containing the unit to convert to. diff --git a/metatomic-core/include/metatomic.hpp b/metatomic-core/include/metatomic.hpp index 3b5c8ac2a..e41f09542 100644 --- a/metatomic-core/include/metatomic.hpp +++ b/metatomic-core/include/metatomic.hpp @@ -2,3 +2,4 @@ #include "metatomic/system.hpp" // IWYU pragma: export #include "metatomic/model.hpp" // IWYU pragma: export #include "metatomic/plugin.hpp" // IWYU pragma: export +#include "metatomic/errors.hpp" // IWYU pragma: export diff --git a/metatomic-core/include/metatomic/errors.hpp b/metatomic-core/include/metatomic/errors.hpp new file mode 100644 index 000000000..a926b388b --- /dev/null +++ b/metatomic-core/include/metatomic/errors.hpp @@ -0,0 +1,108 @@ +#pragma once + +#include +#include +#include +#include +#include + +#include + +namespace metatomic { + + /// Exception class used for all errors in metatomic + class Error: public std::runtime_error { + public: + /// Create a new MetatomicError with the given `message` + Error(const std::string& message): std::runtime_error(message) {} + }; + + namespace details { + /// Check if a return status from the C API indicates an error, and if it is + /// the case, throw an exception of type `metatomic::Error` with the last + /// error message from the library. + inline void check_status(mta_status_t status) { + if (status == MTA_SUCCESS) { + return; + } else if (status == MTA_MODEL_NOT_SUPPORTED_ERROR) { + const char* message = nullptr; + const char* origin = nullptr; + void* data = nullptr; + mta_last_error(&message, &origin, &data); + if (origin != nullptr &&std::strcmp(origin, "C++ exception") == 0 && data != nullptr) { + std::rethrow_exception(*static_cast(data)); + } else { + throw Error(message == nullptr ? "unknown error" : message); + } + } else { + const char* message = nullptr; + mta_last_error(&message, nullptr, nullptr); + throw Error(message == nullptr ? "unknown error" : message); + } + } + + /// Call the given `function` with the given `args` (the function should + /// return an `mta_status_t`), catching any C++ exception, and translating + /// them to native metatomic error code. + /// + /// This is required to prevent callbacks unwinding through the C API. + template + inline mta_status_t catch_exceptions(Function function, Args ...args) { + try { + function(std::move(args)...); + return MTA_SUCCESS; + } catch (...) { + auto* exception_ptr = new std::exception_ptr(std::current_exception()); + + const char* message = nullptr; + try { + std::rethrow_exception(*exception_ptr); + } catch (const std::exception& e) { + message = e.what(); + } catch (...) { + message = "C++ code threw an exception that was not a std::exception"; + } + + auto status = mta_set_last_error( + message, + "C++ exception", + exception_ptr, + [](void *ptr) { delete static_cast(ptr); } + ); + + if (status != MTA_SUCCESS) { + // If we failed to set the error, we are in a very bad state, + // but we should still try to report the original error + // message if possible. + std::fprintf(stderr, "INTERNAL ERROR: unable to set last error after C++ callback failure (status: %d). ", status); + if (message != nullptr) { + fprintf(stderr, "C++ error was: %s\n", message); + } else { + fprintf(stderr, "Unknown C++ error\n"); + } + delete exception_ptr; + } + + return MTA_MODEL_NOT_SUPPORTED_ERROR; + } + } + + /// Check if a pointer allocated by the C API is null, and if it is the + /// case, throw an exception of type `metatomic::Error` with the last + /// error message from the library. + inline void check_pointer(const void* pointer) { + if (pointer == nullptr) { + const char* message = nullptr; + const char* origin = nullptr; + void* data = nullptr; + mta_last_error(&message, &origin, &data); + if (std::strcmp(origin, "C++ exception") == 0 && data != nullptr) { + std::rethrow_exception(*static_cast(data)); + } else { + throw Error(message); + } + } + } + } // namespace details + +} // namespace metatomic diff --git a/metatomic-core/include/metatomic/utils.hpp b/metatomic-core/include/metatomic/utils.hpp index 1cae91bdf..38f6aaf79 100644 --- a/metatomic-core/include/metatomic/utils.hpp +++ b/metatomic-core/include/metatomic/utils.hpp @@ -1,7 +1,36 @@ #pragma once +#include + #include +#include namespace metatomic { + /// Get the multiplicative conversion factor to use to convert from + /// `from_unit` to `to_unit`. Both units are parsed as expressions + /// (e.g. `kJ / mol / A^2`, `(eV * u)^(1/2)`) and their dimensions must + /// match. + /// + /// @verbatim embed:rst:leading-slashes + /// + /// .. seealso:: + /// + /// The general documentation for :ref:`units`, with the expression + /// syntax and list of supported base units. + /// + /// @endverbatim + /// + /// @param from_unit the unit to convert from + /// @param to_unit the unit to convert to + inline double unit_conversion_factor( + const std::string& from_unit, + const std::string& to_unit + ) { + double conversion = 0.0; + + auto status = mta_unit_conversion_factor(from_unit.c_str(), to_unit.c_str(), &conversion); + details::check_status(status); + return conversion; + } } // namespace metatomic diff --git a/metatomic-core/src/c_api/utils.rs b/metatomic-core/src/c_api/utils.rs index 448c2b5d4..38e5222c4 100644 --- a/metatomic-core/src/c_api/utils.rs +++ b/metatomic-core/src/c_api/utils.rs @@ -147,18 +147,18 @@ pub unsafe extern "C" fn mta_string_view( return result; } -/// Get the multiplicative conversion factor to use to convert from -/// `from_unit` to `to_unit`. Both units are parsed as expressions (e.g. -/// "kJ/mol/A^2", "(eV*u)^(1/2)") and their dimensions must match. +/// Get the multiplicative conversion factor to use to convert from `from_unit` +/// to `to_unit`. Both units are parsed as expressions (e.g. `kJ / mol / A^2`, +/// `(eV * u)^(1/2)`) and their dimensions must match. /// -/// Unit expressions are built from base units combined with `*`, `/`, `^`, -/// and parentheses. Unit lookup is case-insensitive, and whitespace is -/// ignored. For example: +/// @verbatim embed:rst:leading-asterisk /// -/// - `"kJ/mol"` -- energy per mole -/// - `"eV/Angstrom^3"` -- pressure -/// - `"(eV*u)^(1/2)"` -- momentum (fractional powers) -/// - `"Hartree/Bohr"` -- force in atomic units +/// .. seealso:: +/// +/// The general documentation for :ref:`units`, with the expression +/// syntax and list of supported base units. +/// +/// @endverbatim /// /// @param from_unit A null-terminated C string containing the unit to convert from. /// @param to_unit A null-terminated C string containing the unit to convert to. diff --git a/metatomic-core/tests/misc.cpp b/metatomic-core/tests/misc.cpp index 93baecb67..0028f1b88 100644 --- a/metatomic-core/tests/misc.cpp +++ b/metatomic-core/tests/misc.cpp @@ -3,6 +3,7 @@ #include #include "metatomic.h" +#include "metatomic.hpp" TEST_CASE("Version macros") { @@ -48,30 +49,52 @@ TEST_CASE("mta_string_t") { mta_string_free(nullptr); } -TEST_CASE("mta_unit_conversion_factor") { - double factor = 0.0; - - // same unit -> factor = 1.0 - auto status = mta_unit_conversion_factor("m", "m", &factor); - REQUIRE(status == MTA_SUCCESS); - CHECK(factor == 1.0); - - // kJ/mol -> eV - CHECK(mta_unit_conversion_factor("kJ/mol", "eV", &factor) == MTA_SUCCESS); - CHECK(factor == Approx(0.010364269656262174).epsilon(1e-15)); - - // dimension mismatch -> error - status = mta_unit_conversion_factor("m", "kg", &factor); - REQUIRE(status != MTA_SUCCESS); - - const char* error_msg = nullptr; - mta_last_error(&error_msg, nullptr, nullptr); - CHECK(std::string(error_msg) == - "invalid parameter: dimension mismatch in unit conversion: " - "'m' has dimension [L] but 'kg' has dimension [M]"); +TEST_CASE("unit conversion factor") { + SECTION("C API") { + double factor = 0.0; + + // same unit -> factor = 1.0 + auto status = mta_unit_conversion_factor("m", "m", &factor); + REQUIRE(status == MTA_SUCCESS); + CHECK(factor == 1.0); + + // kJ/mol -> eV + CHECK(mta_unit_conversion_factor("kJ/mol", "eV", &factor) == MTA_SUCCESS); + CHECK(factor == Approx(0.010364269656262174).epsilon(1e-15)); + + // dimension mismatch -> error + status = mta_unit_conversion_factor("m", "kg", &factor); + REQUIRE(status != MTA_SUCCESS); + + const char* error_msg = nullptr; + mta_last_error(&error_msg, nullptr, nullptr); + CHECK(std::string(error_msg) == + "invalid parameter: dimension mismatch in unit conversion: " + "'m' has dimension [L] but 'kg' has dimension [M]" + ); + } + + SECTION("C++ API") { + // same unit -> factor = 1.0 + auto factor = metatomic::unit_conversion_factor("m", "m"); + CHECK(factor == 1.0); + + // kJ/mol -> eV + factor = metatomic::unit_conversion_factor("kJ/mol", "eV"); + CHECK(factor == Approx(0.010364269656262174).epsilon(1e-15)); + + // dimension mismatch -> error + try{ + factor = metatomic::unit_conversion_factor("m", "kg"); + } + catch(metatomic::Error& e){ + CHECK(std::string(e.what()) == "invalid parameter: dimension mismatch in unit conversion: 'm' has dimension [L] but 'kg' has dimension [M]"); + } + } } -TEST_CASE("mta_format_metadata") { + +TEST_CASE("metatdata formatting") { std::string json =R"({ "type": "metatomic_model_metadata", "name": "name", @@ -88,7 +111,7 @@ TEST_CASE("mta_format_metadata") { REQUIRE(mta_string != nullptr); auto status = mta_format_metadata(json.c_str(), &mta_string); REQUIRE(status == MTA_SUCCESS); - const auto expected = R"(This is the name model + const auto* expected = R"(This is the name model ====================== Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor