diff --git a/CMakeLists.txt b/CMakeLists.txt index 37cfc53..3def6b7 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -57,6 +57,7 @@ target_sources(ndtbl include/ndtbl/types.hpp include/ndtbl/axis.hpp include/ndtbl/grid.hpp + include/ndtbl/exceptions.hpp include/ndtbl/field_group.hpp include/ndtbl/runtime_field_group.hpp include/ndtbl/diagnostics.hpp diff --git a/README.md b/README.md index 3185986..369a469 100644 --- a/README.md +++ b/README.md @@ -36,7 +36,6 @@ Use the Python package when you want a pip-installable Python interface for crea If you want to inspect `.ndtbl`files with a C++ command-line tool, use the C++ `ndtbl-inspect` executable built from `app/`. It is not prebuilt; it becomes available only after running the local CMake build. - ## 📋 Prerequisites Building the C++ project requires: @@ -141,6 +140,32 @@ unwanted values. Bounds handling is independent of interpolation order: queries outside the table domain can either clamp or throw according to the selected `bounds_policy`. +## C++ Error Handling + +The C++ API distinguishes invalid table data, system I/O failures, and invalid +object state through `ndtbl::FormatError`, `ndtbl::IOError`, and +`ndtbl::StateError`. All three derive from `ndtbl::Error` and provide the +standard `what()` message interface: + +```cpp +try { + const auto group = ndtbl::read_runtime_field_group<2>(path); + // Use group. +} catch (const ndtbl::FormatError& error) { + // The file is malformed, unsupported, truncated, or incompatible. +} catch (const ndtbl::IOError& error) { + // Opening, reading, mapping, or another system operation failed. +} catch (const ndtbl::StateError& error) { + // An operation required a populated runtime field group. +} catch (const ndtbl::Error& error) { + // Any other ndtbl-specific runtime failure. +} +``` + +Argument, bounds, and size failures continue to use the standard +`std::invalid_argument`, `std::out_of_range`, and `std::overflow_error` +categories. + ## 🐍 Python Package The repository also ships a separate Python package in [`python/ndtbl/`](https://github.com/thomasisensee/ndtbl/tree/main/python/ndtbl). diff --git a/app/ndtbl_inspect.cpp b/app/ndtbl_inspect.cpp index ad72a63..abdac3a 100644 --- a/app/ndtbl_inspect.cpp +++ b/app/ndtbl_inspect.cpp @@ -1,3 +1,5 @@ +// SPDX-License-Identifier: MIT + #include "ndtbl/ndtbl.hpp" #include diff --git a/benchmarks/lookup_benchmarks.cpp b/benchmarks/lookup_benchmarks.cpp index 39e0e48..c5d5e35 100644 --- a/benchmarks/lookup_benchmarks.cpp +++ b/benchmarks/lookup_benchmarks.cpp @@ -1,3 +1,5 @@ +// SPDX-License-Identifier: MIT + #include "ndtbl/ndtbl.hpp" #include @@ -200,7 +202,7 @@ make_queries(const std::array& axes, std::size_t count) for (std::size_t query = 0; query < count; ++query) { std::array coordinates = {}; for (std::size_t axis = 0; axis < Dim; ++axis) { - const double fraction = + const auto fraction = static_cast(((query + 17u * axis) % 997u) + 1u) / 998.0; coordinates[axis] = axes[axis].min() + fraction * (axes[axis].max() - axes[axis].min()); diff --git a/doc/cppapi.rst b/doc/cppapi.rst index 4af8ff9..a951c67 100644 --- a/doc/cppapi.rst +++ b/doc/cppapi.rst @@ -6,6 +6,18 @@ API reference ------------- +.. doxygenclass:: ndtbl::Error + :members: + +.. doxygenclass:: ndtbl::FormatError + :members: + +.. doxygenclass:: ndtbl::IOError + :members: + +.. doxygenclass:: ndtbl::StateError + :members: + .. doxygenclass:: ndtbl::Axis :members: diff --git a/include/ndtbl/axis.hpp b/include/ndtbl/axis.hpp index 3f4e335..23e60db 100644 --- a/include/ndtbl/axis.hpp +++ b/include/ndtbl/axis.hpp @@ -145,7 +145,7 @@ class Axis return min_; } - const double fraction = + const auto fraction = static_cast(index) / static_cast(size_ - 1); return min_ + fraction * (max_ - min_); } @@ -173,25 +173,6 @@ class Axis return coordinates_; } - /** - * @brief Check whether two axes describe the same grid support. - * - * @param other Axis to compare against. - * @return `true` if both axes represent the same support points. - */ - bool equivalent(const Axis& other) const - { - if (kind_ != other.kind_ || size_ != other.size_) { - return false; - } - - if (kind_ == axis_kind::uniform) { - return min_ == other.min_ && max_ == other.max_; - } - - return coordinates_ == other.coordinates_; - } - /** * @brief Locate the interpolation interval and upper weight for a query. * @@ -239,9 +220,9 @@ class Axis return std::make_pair(size_ - 2, 1.0); } - const std::vector::const_iterator upper = + const auto upper = std::upper_bound(coordinates_.begin(), coordinates_.end(), value); - const std::size_t lower_index = + const auto lower_index = static_cast(std::distance(coordinates_.begin(), upper) - 1); const double lower_value = coordinates_[lower_index]; const double upper_value = coordinates_[lower_index + 1]; diff --git a/include/ndtbl/detail/binary_io.hpp b/include/ndtbl/detail/binary_io.hpp index 81f6e31..e4419c5 100644 --- a/include/ndtbl/detail/binary_io.hpp +++ b/include/ndtbl/detail/binary_io.hpp @@ -3,6 +3,7 @@ #pragma once #include "ndtbl/detail/size_math.hpp" +#include "ndtbl/exceptions.hpp" #include "ndtbl/field_group.hpp" #include "ndtbl/metadata.hpp" @@ -41,9 +42,13 @@ static constexpr char file_magic[8] = { 'N', 'D', 'T', 'B', inline void write_bytes(std::ostream& os, const char* data, std::size_t size) { - os.write(data, static_cast(size)); + try { + os.write(data, static_cast(size)); + } catch (const std::ios_base::failure&) { + throw IOError("failed to write ndtbl payload"); + } if (!os.good()) { - throw std::runtime_error("failed to write ndtbl payload"); + throw IOError("failed to write ndtbl payload"); } } @@ -57,9 +62,19 @@ write_bytes(std::ostream& os, const char* data, std::size_t size) inline void read_bytes(std::istream& is, char* data, std::size_t size) { - is.read(data, static_cast(size)); + try { + is.read(data, static_cast(size)); + } catch (const std::ios_base::failure&) { + if (is.eof()) { + throw FormatError("unexpected end of ndtbl input"); + } + throw IOError("failed to read ndtbl payload"); + } if (!is.good()) { - throw std::runtime_error("failed to read ndtbl payload"); + if (is.eof()) { + throw FormatError("unexpected end of ndtbl input"); + } + throw IOError("failed to read ndtbl payload"); } } @@ -67,12 +82,12 @@ inline bool host_is_little_endian() { const std::uint16_t marker = 1u; - const unsigned char* bytes = reinterpret_cast(&marker); + const auto* bytes = reinterpret_cast(&marker); return bytes[0] == 1u; } template -inline void +void write_uint_le(std::ostream& os, UInt value) { static_assert(std::is_unsigned::value, @@ -86,7 +101,7 @@ write_uint_le(std::ostream& os, UInt value) } template -inline UInt +UInt read_uint_le(std::istream& is) { static_assert(std::is_unsigned::value, @@ -104,7 +119,7 @@ read_uint_le(std::istream& is) } template -inline void +void write_float_le(std::ostream& os, Float value) { static_assert(std::is_floating_point::value, @@ -120,7 +135,7 @@ write_float_le(std::ostream& os, Float value) } template -inline Float +Float read_float_le(std::istream& is) { static_assert(std::is_floating_point::value, @@ -130,19 +145,17 @@ read_float_le(std::istream& is) static_assert(std::numeric_limits::is_iec559, "ndtbl requires IEEE-754 floating-point types"); - const UInt bits = read_uint_le(is); + const auto bits = read_uint_le(is); Float value; std::memcpy(&value, &bits, sizeof(value)); return value; } template -struct payload_uint -{ - typedef typename std::conditional::type type; -}; +using payload_uint_t = + std::conditional_t; /** * @brief Write a length-prefixed UTF-8 string to a binary stream. @@ -153,7 +166,7 @@ struct payload_uint inline void write_string(std::ostream& os, const std::string& value) { - const std::uint64_t size = static_cast(value.size()); + const auto size = static_cast(value.size()); write_uint_le(os, size); write_bytes(os, value.data(), value.size()); } @@ -167,13 +180,13 @@ write_string(std::ostream& os, const std::string& value) inline std::string read_string(std::istream& is) { - const std::uint64_t size = read_uint_le(is); + const auto size = read_uint_le(is); if (size == 0) { return std::string(); } if (size > static_cast(std::numeric_limits::max())) { - throw std::runtime_error("ndtbl string length exceeds supported size"); + throw FormatError("ndtbl string length exceeds supported size"); } std::string value(static_cast(size), '\0'); @@ -185,15 +198,21 @@ inline void require_zero(std::uint64_t value, const std::string& what) { if (value != 0u) { - throw std::runtime_error("ndtbl " + what + " must be zero"); + throw FormatError("ndtbl " + what + " must be zero"); } } -inline constexpr std::size_t +constexpr std::size_t fixed_header_size() { - return sizeof(file_magic) + sizeof(std::uint8_t) + sizeof(std::uint8_t) + - sizeof(std::uint16_t) + sizeof(std::uint64_t) * 4u; + return sizeof(file_magic) + // File magic + sizeof(std::uint8_t) + // Format version + sizeof(std::uint8_t) + // Scalar type + sizeof(std::uint16_t) + // Reserved + sizeof(std::uint64_t) + // Payload offset + sizeof(std::uint64_t) + // Dimension + sizeof(std::uint64_t) + // Field count + sizeof(std::uint64_t); // Point count } inline std::size_t @@ -201,27 +220,34 @@ metadata_size(const GroupMetadata& metadata) { std::size_t total = fixed_header_size(); - for (std::size_t axis = 0; axis < metadata.axes.size(); ++axis) { - const Axis& axis_spec = metadata.axes[axis]; + for (const auto& axis_spec : metadata.axes) { total = checked_add_size(total, - sizeof(std::uint8_t) + sizeof(std::uint8_t) + - sizeof(std::uint16_t) + sizeof(std::uint64_t), + sizeof(std::uint8_t) + // Axis kind + sizeof(std::uint8_t) + // Reserved byte + sizeof(std::uint16_t) + // Reserved field + sizeof(std::uint64_t), // Axis extent "metadata size"); if (axis_spec.kind() == axis_kind::uniform) { - total = checked_add_size(total, sizeof(double) * 2u, "metadata size"); + total = checked_add_size(total, + sizeof(double) + // Minimum coordinate + sizeof(double), // Maximum coordinate + "metadata size"); } else { total = checked_add_size( total, - checked_multiply_size(axis_spec.size(), sizeof(double), "axis payload"), + checked_multiply_size(axis_spec.size(), + sizeof(double), // Axis coordinate + "axis payload"), "metadata size"); } } - for (std::size_t field = 0; field < metadata.field_names.size(); ++field) { - total = checked_add_size(total, sizeof(std::uint64_t), "metadata size"); - total = checked_add_size( - total, metadata.field_names[field].size(), "metadata size"); + for (const auto& field_name : metadata.field_names) { + total = checked_add_size(total, + sizeof(std::uint64_t), // Field name length + "metadata size"); + total = checked_add_size(total, field_name.size(), "metadata size"); } return total; @@ -231,9 +257,9 @@ inline std::size_t axis_point_count(const std::vector& axes) { std::size_t point_count = 1; - for (std::size_t axis = 0; axis < axes.size(); ++axis) { + for (const auto& axis : axes) { point_count = - checked_multiply_size(point_count, axes[axis].size(), "point count"); + checked_multiply_size(point_count, axis.size(), "point count"); } return point_count; } @@ -247,7 +273,7 @@ axis_point_count(const std::vector& axes) * @param payload Point-major interleaved field payload. */ template -inline void +void write_group_stream_impl(std::ostream& os, const GroupMetadata& metadata, const PayloadView& payload) @@ -293,8 +319,7 @@ write_group_stream_impl(std::ostream& os, write_uint_le( os, static_cast(metadata.point_count)); - for (std::size_t axis = 0; axis < metadata.axes.size(); ++axis) { - const Axis& axis_spec = metadata.axes[axis]; + for (const auto& axis_spec : metadata.axes) { write_uint_le(os, static_cast(axis_spec.kind())); write_uint_le(os, 0u); @@ -306,14 +331,14 @@ write_group_stream_impl(std::ostream& os, write_float_le(os, axis_spec.max()); } else { const std::vector coordinates = axis_spec.coordinates(); - for (std::size_t i = 0; i < coordinates.size(); ++i) { - write_float_le(os, coordinates[i]); + for (const auto coordinate : coordinates) { + write_float_le(os, coordinate); } } } - for (std::size_t field = 0; field < metadata.field_names.size(); ++field) { - write_string(os, metadata.field_names[field]); + for (const auto& field_name : metadata.field_names) { + write_string(os, field_name); } if (payload.size() != 0) { @@ -323,7 +348,7 @@ write_group_stream_impl(std::ostream& os, payload.byte_size()); } else { for (std::size_t index = 0; index < payload.size(); ++index) { - write_float_le::type>( + write_float_le>( os, payload.unchecked(index)); } } @@ -341,7 +366,7 @@ write_group_stream_impl(std::ostream& os, * const std::vector&) */ template -inline void +void write_group_stream_impl(std::ostream& os, const FieldGroup& group) { GroupMetadata metadata = { scalar_type_of(), @@ -355,7 +380,7 @@ write_group_stream_impl(std::ostream& os, const FieldGroup& group) } template -inline void +void write_group_stream_impl(std::ostream& os, const GroupMetadata& metadata, const std::vector& payload) @@ -374,11 +399,11 @@ verify_magic(std::istream& is) char magic[sizeof(file_magic)] = {}; read_bytes(is, magic, sizeof(magic)); if (!std::equal(magic, magic + sizeof(file_magic), file_magic)) { - throw std::runtime_error("invalid ndtbl magic header"); + throw FormatError("invalid ndtbl magic header"); } } -inline constexpr std::size_t +constexpr std::size_t scalar_size(scalar_type type) { if (type == scalar_type::float32) { @@ -387,7 +412,7 @@ scalar_size(scalar_type type) if (type == scalar_type::float64) { return sizeof(double); } - throw std::runtime_error("unsupported ndtbl scalar type"); + throw FormatError("unsupported ndtbl scalar type"); } struct parsed_group_layout @@ -409,9 +434,9 @@ read_group_layout_impl(std::istream& is) { verify_magic(is); - const std::uint8_t version = read_uint_le(is); + const auto version = read_uint_le(is); if (version != current_format_version) { - throw std::runtime_error("unsupported ndtbl version"); + throw FormatError("unsupported ndtbl version"); } GroupMetadata metadata; @@ -431,25 +456,28 @@ read_group_layout_impl(std::istream& is) metadata.axes.reserve(metadata.dimension); for (std::size_t axis = 0; axis < metadata.dimension; ++axis) { - const axis_kind kind = - static_cast(read_uint_le(is)); + const auto kind = static_cast(read_uint_le(is)); require_zero(read_uint_le(is), "axis reserved byte"); require_zero(read_uint_le(is), "axis reserved field"); const std::size_t extent = narrow_u64_to_size(read_uint_le(is), "axis extent"); - if (kind == axis_kind::uniform) { - const double min_value = read_float_le(is); - const double max_value = read_float_le(is); - metadata.axes.push_back(Axis::uniform(min_value, max_value, extent)); - } else if (kind == axis_kind::explicit_coordinates) { - std::vector coordinates(extent); - for (std::size_t i = 0; i < extent; ++i) { - coordinates[i] = read_float_le(is); + try { + if (kind == axis_kind::uniform) { + const auto min_value = read_float_le(is); + const auto max_value = read_float_le(is); + metadata.axes.push_back(Axis::uniform(min_value, max_value, extent)); + } else if (kind == axis_kind::explicit_coordinates) { + std::vector coordinates(extent); + for (std::size_t i = 0; i < extent; ++i) { + coordinates[i] = read_float_le(is); + } + metadata.axes.push_back(Axis::from_coordinates(coordinates)); + } else { + throw FormatError("unsupported ndtbl axis kind"); } - metadata.axes.push_back(Axis::from_coordinates(coordinates)); - } else { - throw std::runtime_error("unsupported ndtbl axis kind"); + } catch (const std::invalid_argument& error) { + throw FormatError(error.what()); } } @@ -459,17 +487,21 @@ read_group_layout_impl(std::istream& is) } if (axis_point_count(metadata.axes) != metadata.point_count) { - throw std::runtime_error("ndtbl point count does not match axis extents"); + throw FormatError("ndtbl point count does not match axis extents"); } - const std::istream::pos_type payload_position = is.tellg(); + std::istream::pos_type payload_position = std::istream::pos_type(-1); + try { + payload_position = is.tellg(); + } catch (const std::ios_base::failure&) { + throw IOError("failed to determine ndtbl payload offset"); + } if (payload_position < 0) { - throw std::runtime_error("failed to determine ndtbl payload offset"); + throw IOError("failed to determine ndtbl payload offset"); } - const std::size_t actual_payload_offset = - static_cast(payload_position); + const auto actual_payload_offset = static_cast(payload_position); if (actual_payload_offset != payload_offset) { - throw std::runtime_error("ndtbl payload offset does not match metadata"); + throw FormatError("ndtbl payload offset does not match metadata"); } parsed_group_layout layout; @@ -497,7 +529,7 @@ read_group_metadata_impl(std::istream& is) * @return Payload vector with `value_count` entries. */ template -inline std::vector +std::vector read_payload(std::istream& is, std::size_t value_count) { std::vector values(value_count); @@ -513,8 +545,7 @@ read_payload(std::istream& is, std::size_t value_count) } for (std::size_t index = 0; index < value_count; ++index) { - values[index] = - read_float_le::type>(is); + values[index] = read_float_le>(is); } return values; } @@ -527,7 +558,7 @@ read_payload(std::istream& is, std::size_t value_count) * @return Fixed-size axis array with `Dim` entries. */ template -inline std::array +std::array fixed_axes(const std::vector& axes) { if (axes.size() != Dim) { diff --git a/include/ndtbl/detail/mapped_payload.hpp b/include/ndtbl/detail/mapped_payload.hpp index 2ce0857..33c2903 100644 --- a/include/ndtbl/detail/mapped_payload.hpp +++ b/include/ndtbl/detail/mapped_payload.hpp @@ -15,6 +15,7 @@ #endif #include "ndtbl/diagnostics.hpp" +#include "ndtbl/exceptions.hpp" #include #include @@ -85,13 +86,13 @@ query_residency(const void* address, std::size_t length) const long page_size_long = sysconf(_SC_PAGESIZE); if (page_size_long <= 0) { - throw std::runtime_error("failed to query system page size for mincore"); + throw IOError("failed to query system page size for mincore"); } - const std::size_t page_size = static_cast(page_size_long); - const std::uintptr_t addr = reinterpret_cast(address); + const auto page_size = static_cast(page_size_long); + const auto addr = reinterpret_cast(address); const std::uintptr_t aligned_addr = addr - (addr % page_size); - const std::size_t delta = static_cast(addr - aligned_addr); + const auto delta = static_cast(addr - aligned_addr); if (length > std::numeric_limits::max() - delta) { throw std::overflow_error("payload residency range is too large"); } @@ -106,12 +107,12 @@ query_residency(const void* address, std::size_t length) if (mincore(reinterpret_cast(aligned_addr), total_pages * page_size, vec.data()) != 0) { - throw std::runtime_error(system_error_message("mincore failed")); + throw IOError(system_error_message("mincore failed")); } std::size_t resident_pages = 0; - for (std::size_t index = 0; index < vec.size(); ++index) { - if ((vec[index] & 1U) != 0U) { + for (const auto page : vec) { + if ((page & 1U) != 0U) { ++resident_pages; } } @@ -172,7 +173,7 @@ map_payload_bytes(const std::string& path, const int fd = open(path.c_str(), O_RDONLY); if (fd < 0) { - throw std::runtime_error( + throw IOError( system_error_message("failed to open ndtbl input file for mmap")); } @@ -181,25 +182,25 @@ map_payload_bytes(const std::string& path, const int saved_errno = errno; close(fd); errno = saved_errno; - throw std::runtime_error( + throw IOError( system_error_message("failed to stat ndtbl input file for mmap")); } - const std::uintmax_t file_size = static_cast(status.st_size); - const std::uintmax_t payload_end = + const auto file_size = static_cast(status.st_size); + const auto payload_end = static_cast(payload_offset) + payload_size; if (payload_end > file_size) { close(fd); - throw std::runtime_error("ndtbl file payload exceeds file size"); + throw FormatError("ndtbl file payload exceeds file size"); } const long page_size = sysconf(_SC_PAGESIZE); if (page_size <= 0) { close(fd); - throw std::runtime_error("failed to query system page size for mmap"); + throw IOError("failed to query system page size for mmap"); } - const std::size_t alignment = static_cast(page_size); + const auto alignment = static_cast(page_size); const std::size_t aligned_offset = payload_offset - (payload_offset % alignment); const std::size_t delta = payload_offset - aligned_offset; @@ -216,13 +217,12 @@ map_payload_bytes(const std::string& path, close(fd); if (mapping == MAP_FAILED) { errno = saved_errno; - throw std::runtime_error( - system_error_message("failed to map ndtbl payload")); + throw IOError(system_error_message("failed to map ndtbl payload")); } - const std::shared_ptr owner = + const auto owner = std::make_shared(mapping, mapping_length); - const std::uint8_t* const data = + const auto* const data = reinterpret_cast(mapping) + delta; return std::shared_ptr(owner, data); } diff --git a/include/ndtbl/detail/size_math.hpp b/include/ndtbl/detail/size_math.hpp index 25ab566..81d3f0b 100644 --- a/include/ndtbl/detail/size_math.hpp +++ b/include/ndtbl/detail/size_math.hpp @@ -15,7 +15,7 @@ inline std::size_t checked_multiply_size(std::size_t lhs, std::size_t rhs, const std::string& what) { if (lhs != 0 && rhs > std::numeric_limits::max() / lhs) { - throw std::runtime_error("ndtbl " + what + " exceeds supported size"); + throw std::overflow_error("ndtbl " + what + " exceeds supported size"); } return lhs * rhs; } @@ -24,7 +24,7 @@ inline std::size_t checked_add_size(std::size_t lhs, std::size_t rhs, const std::string& what) { if (rhs > std::numeric_limits::max() - lhs) { - throw std::runtime_error("ndtbl " + what + " exceeds supported size"); + throw std::overflow_error("ndtbl " + what + " exceeds supported size"); } return lhs + rhs; } @@ -34,7 +34,7 @@ narrow_u64_to_size(std::uint64_t value, const std::string& what) { if (value > static_cast(std::numeric_limits::max())) { - throw std::runtime_error("ndtbl " + what + " exceeds supported size"); + throw std::overflow_error("ndtbl " + what + " exceeds supported size"); } return static_cast(value); } diff --git a/include/ndtbl/exceptions.hpp b/include/ndtbl/exceptions.hpp new file mode 100644 index 0000000..5f698c3 --- /dev/null +++ b/include/ndtbl/exceptions.hpp @@ -0,0 +1,48 @@ +// SPDX-License-Identifier: MIT + +#pragma once + +#include + +namespace ndtbl { + +/** + * @brief Base class for ndtbl-specific runtime failures. + * + * Catch this type when all ndtbl format, I/O, and state failures should be + * handled uniformly. + */ +class Error : public std::runtime_error +{ +public: + using std::runtime_error::runtime_error; +}; + +/** + * @brief Invalid, unsupported, truncated, or loader-incompatible ndtbl data. + */ +class FormatError : public Error +{ +public: + using Error::Error; +}; + +/** + * @brief Filesystem, stream, memory-mapping, or operating-system failure. + */ +class IOError : public Error +{ +public: + using Error::Error; +}; + +/** + * @brief Operation requiring a populated ndtbl object was used on empty state. + */ +class StateError : public Error +{ +public: + using Error::Error; +}; + +} // namespace ndtbl diff --git a/include/ndtbl/field_group.hpp b/include/ndtbl/field_group.hpp index da84c5a..42f6500 100644 --- a/include/ndtbl/field_group.hpp +++ b/include/ndtbl/field_group.hpp @@ -79,12 +79,12 @@ class FieldGroup */ FieldGroup(const Grid& grid, const std::vector& field_names, - const PayloadView& interleaved_values, + PayloadView interleaved_values, std::shared_ptr payload_owner, bool payload_is_mmap = false) : grid_(grid) , field_names_(field_names) - , interleaved_values_(interleaved_values) + , interleaved_values_(std::move(interleaved_values)) , payload_owner_(std::move(payload_owner)) , payload_is_mmap_(payload_is_mmap) { @@ -155,7 +155,7 @@ class FieldGroup */ std::size_t field_index(const std::string& name) const { - const std::vector::const_iterator found = + const auto found = std::find(field_names_.begin(), field_names_.end(), name); if (found == field_names_.end()) { throw std::out_of_range("field not found in ndtbl field group"); @@ -400,7 +400,7 @@ class FieldGroup Output* results) const { for (std::size_t point = 0; point < Stencil::points; ++point) { - const Output weight = static_cast(stencil.weight(point)); + const auto weight = static_cast(stencil.weight(point)); const std::size_t base = stencil.point_index(point) * fields; for (std::size_t field = 0; field < fields; ++field) { const Stored value = values[base + field]; @@ -415,7 +415,7 @@ class FieldGroup Output* results) const { for (std::size_t point = 0; point < Stencil::points; ++point) { - const Output weight = static_cast(stencil.weight(point)); + const auto weight = static_cast(stencil.weight(point)); const std::size_t base = stencil.point_index(point) * fields; for (std::size_t field = 0; field < fields; ++field) { const Stored value = interleaved_values_.unchecked(base + field); @@ -426,7 +426,7 @@ class FieldGroup void adopt_owned_payload(std::vector&& interleaved_values) { - const std::shared_ptr> storage = + const auto storage = std::make_shared>(std::move(interleaved_values)); const Stored* const data = storage->empty() ? nullptr : storage->data(); interleaved_values_ = PayloadView(data, storage->size()); diff --git a/include/ndtbl/grid.hpp b/include/ndtbl/grid.hpp index 8622d6a..8815882 100644 --- a/include/ndtbl/grid.hpp +++ b/include/ndtbl/grid.hpp @@ -103,6 +103,8 @@ class TensorStencil template friend class Grid; + TensorStencil() = default; + std::array point_indices_; std::array weights_; }; @@ -224,22 +226,6 @@ class Grid */ std::size_t point_count() const { return point_count_; } - /** - * @brief Check whether another grid uses the same axes. - * - * @param other Grid to compare against. - * @return `true` if all axes are equivalent. - */ - bool equivalent(const Grid& other) const - { - for (std::size_t axis = 0; axis < Dim; ++axis) { - if (!axes_[axis].equivalent(other.axes_[axis])) { - return false; - } - } - return true; - } - /** * @brief Precompute the multilinear interpolation stencil for one query * point. @@ -268,12 +254,15 @@ class Grid } LinearStencil prepared; - for (std::size_t mask = 0; mask < LinearStencil::points; ++mask) { + // Enumerate all corners of the bracketing cell. Bit `axis` selects the + // lower (0) or upper (1) endpoint on that axis. + for (std::size_t corner_mask = 0; corner_mask < LinearStencil::points; + ++corner_mask) { double weight = 1.0; std::size_t linear_index = 0; for (std::size_t axis = 0; axis < Dim; ++axis) { - const bool use_upper = (mask & (std::size_t(1) << axis)) != 0; + const bool use_upper = (corner_mask & (std::size_t(1) << axis)) != 0; const std::size_t index = use_upper ? upper_indices[axis] : lower_indices[axis]; const double axis_weight = @@ -282,8 +271,8 @@ class Grid weight *= axis_weight; } - prepared.point_indices_[mask] = linear_index; - prepared.weights_[mask] = weight; + prepared.point_indices_[corner_mask] = linear_index; + prepared.weights_[corner_mask] = weight; } return prepared; @@ -296,9 +285,9 @@ class Grid * This method builds a local interpolation stencil using four support points * per axis. Along each axis, the one-dimensional weights are the cubic * Lagrange basis weights for the selected four axis coordinates: - * + * \f[ * L_i(x) = prod_{j != i} (x - x_j) / (x_i - x_j) - * + * \f] * The multidimensional stencil is formed as the tensor product of these * one-dimensional Lagrange weights. Consequently, the stencil contains * `4^Dim` table values. diff --git a/include/ndtbl/io.hpp b/include/ndtbl/io.hpp index 9b7f2d8..a40ccc1 100644 --- a/include/ndtbl/io.hpp +++ b/include/ndtbl/io.hpp @@ -4,6 +4,7 @@ #include "ndtbl/detail/binary_io.hpp" #include "ndtbl/detail/mapped_payload.hpp" +#include "ndtbl/exceptions.hpp" #include "ndtbl/runtime_field_group.hpp" #include @@ -12,7 +13,6 @@ #include #include #include -#include #include #include #include @@ -29,7 +29,7 @@ namespace ndtbl { * @see write_group(const std::string&, const FieldGroup&) */ template -inline void +void write_group_stream(std::ostream& os, const FieldGroup& group) { detail::write_group_stream_impl(os, group); @@ -50,7 +50,7 @@ write_group_stream(std::ostream& os, const FieldGroup& group) * @see write_group_stream(std::ostream&, const FieldGroup&) */ template -inline void +void write_group_stream(std::ostream& os, const GroupMetadata& metadata, const std::vector& interleaved_values) @@ -68,12 +68,12 @@ write_group_stream(std::ostream& os, * @see write_group_stream(std::ostream&, const FieldGroup&) */ template -inline void +void write_group(const std::string& path, const FieldGroup& group) { std::ofstream os(path.c_str(), std::ios::binary); if (!os.is_open()) { - throw std::runtime_error("failed to open ndtbl output file: " + path); + throw IOError("failed to open ndtbl output file: " + path); } write_group_stream(os, group); } @@ -90,14 +90,14 @@ write_group(const std::string& path, const FieldGroup& group) * const std::vector&) */ template -inline void +void write_group(const std::string& path, const GroupMetadata& metadata, const std::vector& interleaved_values) { std::ofstream os(path.c_str(), std::ios::binary); if (!os.is_open()) { - throw std::runtime_error("failed to open ndtbl output file: " + path); + throw IOError("failed to open ndtbl output file: " + path); } write_group_stream(os, metadata, interleaved_values); } @@ -112,13 +112,13 @@ write_group(const std::string& path, * @see RuntimeFieldGroup */ template -inline void +void write_group(const std::string& path, const RuntimeFieldGroup& group) { std::ofstream os(path.c_str(), std::ios::binary); if (!os.is_open()) { - throw std::runtime_error("failed to open ndtbl output file: " + path); + throw IOError("failed to open ndtbl output file: " + path); } group.write(os); } @@ -139,7 +139,7 @@ read_group_metadata(const std::string& path) { std::ifstream is(path.c_str(), std::ios::binary); if (!is.is_open()) { - throw std::runtime_error("failed to open ndtbl input file: " + path); + throw IOError("failed to open ndtbl input file: " + path); } return detail::read_group_metadata_impl(is); @@ -159,23 +159,21 @@ read_group_metadata(const std::string& path) * @see FieldGroup */ template -inline FieldGroup +FieldGroup read_field_group(const std::string& path) { std::ifstream is(path.c_str(), std::ios::binary); if (!is.is_open()) { - throw std::runtime_error("failed to open ndtbl input file: " + path); + throw IOError("failed to open ndtbl input file: " + path); } const detail::parsed_group_layout layout = detail::read_group_layout_impl(is); const GroupMetadata& metadata = layout.metadata; if (metadata.dimension != Dim) { - throw std::runtime_error( - "ndtbl file dimension does not match typed loader"); + throw FormatError("ndtbl file dimension does not match typed loader"); } if (metadata.value_type != scalar_type_of()) { - throw std::runtime_error( - "ndtbl file scalar type does not match typed loader"); + throw FormatError("ndtbl file scalar type does not match typed loader"); } const std::array axes = detail::fixed_axes(metadata.axes); @@ -192,8 +190,7 @@ read_field_group(const std::string& path) #else // Keep this non-const so the payload buffer can be moved into the read-only // FieldGroup storage instead of copied during load. - std::vector values = - detail::read_payload(is, layout.value_count); + auto values = detail::read_payload(is, layout.value_count); return FieldGroup(grid, metadata.field_names, std::move(values)); #endif } @@ -214,19 +211,18 @@ read_field_group(const std::string& path) * @see RuntimeFieldGroup */ template -inline RuntimeFieldGroup +RuntimeFieldGroup read_runtime_field_group(const std::string& path) { std::ifstream is(path.c_str(), std::ios::binary); if (!is.is_open()) { - throw std::runtime_error("failed to open ndtbl input file: " + path); + throw IOError("failed to open ndtbl input file: " + path); } const detail::parsed_group_layout layout = detail::read_group_layout_impl(is); const GroupMetadata& metadata = layout.metadata; if (metadata.dimension != Dim) { - throw std::runtime_error( - "ndtbl file dimension does not match typed loader"); + throw FormatError("ndtbl file dimension does not match typed loader"); } const std::array axes = detail::fixed_axes(metadata.axes); @@ -245,8 +241,7 @@ read_runtime_field_group(const std::string& path) #else // Keep this non-const so the payload buffer can be moved into the // read-only FieldGroup storage instead of copied during load. - std::vector values = - detail::read_payload(is, layout.value_count); + auto values = detail::read_payload(is, layout.value_count); return RuntimeFieldGroup( FieldGroup(grid, metadata.field_names, std::move(values))); #endif @@ -266,14 +261,13 @@ read_runtime_field_group(const std::string& path) #else // Keep this non-const so the payload buffer can be moved into the // read-only FieldGroup storage instead of copied during load. - std::vector values = - detail::read_payload(is, layout.value_count); + auto values = detail::read_payload(is, layout.value_count); return RuntimeFieldGroup( FieldGroup(grid, metadata.field_names, std::move(values))); #endif } - throw std::runtime_error("unsupported ndtbl scalar type"); + throw FormatError("unsupported ndtbl scalar type"); } } // namespace ndtbl diff --git a/include/ndtbl/ndtbl.hpp b/include/ndtbl/ndtbl.hpp index 4beeb48..91de7e7 100644 --- a/include/ndtbl/ndtbl.hpp +++ b/include/ndtbl/ndtbl.hpp @@ -4,6 +4,7 @@ #include "ndtbl/axis.hpp" #include "ndtbl/diagnostics.hpp" +#include "ndtbl/exceptions.hpp" #include "ndtbl/field_group.hpp" #include "ndtbl/grid.hpp" #include "ndtbl/io.hpp" diff --git a/include/ndtbl/payload.hpp b/include/ndtbl/payload.hpp index 21a29d7..5d20fc3 100644 --- a/include/ndtbl/payload.hpp +++ b/include/ndtbl/payload.hpp @@ -135,7 +135,7 @@ class PayloadView * @return Read-only view into `values`. */ template -inline PayloadView +PayloadView payload_view(const std::vector& values) { const Stored* data = values.empty() ? nullptr : values.data(); diff --git a/include/ndtbl/runtime_field_group.hpp b/include/ndtbl/runtime_field_group.hpp index ac2a8b5..5e164fe 100644 --- a/include/ndtbl/runtime_field_group.hpp +++ b/include/ndtbl/runtime_field_group.hpp @@ -2,6 +2,7 @@ #pragma once +#include "ndtbl/exceptions.hpp" #include "ndtbl/field_group.hpp" #include "ndtbl/types.hpp" @@ -9,7 +10,6 @@ #include #include #include -#include #include #include #include @@ -69,7 +69,7 @@ class RuntimeFieldGroup std::size_t field_count() const { if (!impl_) { - throw std::runtime_error("ndtbl field group is empty"); + throw StateError("ndtbl field group is empty"); } return impl_->field_count(); } @@ -82,7 +82,7 @@ class RuntimeFieldGroup scalar_type value_type() const { if (!impl_) { - throw std::runtime_error("ndtbl field group is empty"); + throw StateError("ndtbl field group is empty"); } return impl_->value_type(); } @@ -95,7 +95,7 @@ class RuntimeFieldGroup std::vector field_names() const { if (!impl_) { - throw std::runtime_error("ndtbl field group is empty"); + throw StateError("ndtbl field group is empty"); } return impl_->field_names(); } @@ -108,7 +108,7 @@ class RuntimeFieldGroup std::array axes() const { if (!impl_) { - throw std::runtime_error("ndtbl field group is empty"); + throw StateError("ndtbl field group is empty"); } return impl_->axes(); } @@ -122,7 +122,7 @@ class RuntimeFieldGroup std::size_t field_index(const std::string& field_name) const { if (!impl_) { - throw std::runtime_error("ndtbl field group is empty"); + throw StateError("ndtbl field group is empty"); } return impl_->field_index(field_name); } @@ -160,7 +160,7 @@ class RuntimeFieldGroup bounds_policy policy = bounds_policy::clamp) const { if (!impl_) { - throw std::runtime_error("ndtbl field group is empty"); + throw StateError("ndtbl field group is empty"); } impl_->evaluate_all_linear_into(coordinates, values, policy); } @@ -201,7 +201,7 @@ class RuntimeFieldGroup bounds_policy policy = bounds_policy::clamp) const { if (!impl_) { - throw std::runtime_error("ndtbl field group is empty"); + throw StateError("ndtbl field group is empty"); } impl_->evaluate_all_cubic_into(coordinates, values, policy); } @@ -214,7 +214,7 @@ class RuntimeFieldGroup residency_info payload_residency() const { if (!impl_) { - throw std::runtime_error("ndtbl field group is empty"); + throw StateError("ndtbl field group is empty"); } return impl_->payload_residency(); } @@ -228,7 +228,7 @@ class RuntimeFieldGroup void write(std::ostream& os) const { if (!impl_) { - throw std::runtime_error("ndtbl field group is empty"); + throw StateError("ndtbl field group is empty"); } impl_->write(os); } diff --git a/tests/CMakeLists.txt b/tests/CMakeLists.txt index a074bfa..22a7612 100644 --- a/tests/CMakeLists.txt +++ b/tests/CMakeLists.txt @@ -8,7 +8,7 @@ if(NOT TARGET Catch2::Catch2WithMain) FetchContent_Declare( Catch2 GIT_REPOSITORY https://github.com/catchorg/Catch2.git - GIT_TAG v3.15.1) + GIT_TAG v3.15.2) FetchContent_MakeAvailable(Catch2) endif() diff --git a/tests/ndtbl_interpolation_t.cpp b/tests/ndtbl_interpolation_t.cpp index ad3002f..0f4c8e5 100644 --- a/tests/ndtbl_interpolation_t.cpp +++ b/tests/ndtbl_interpolation_t.cpp @@ -1,3 +1,5 @@ +// SPDX-License-Identifier: MIT + #include "ndtbl/ndtbl.hpp" #include "test_support.hpp" @@ -13,6 +15,7 @@ #include #include #include +#include #include namespace { @@ -174,6 +177,8 @@ static_assert(ndtbl::LinearStencil<4>::points == 16, "4D linear interpolation should use 16 table points"); static_assert(ndtbl::CubicStencil<4>::points == 256, "4D cubic interpolation should use 256 table points"); +static_assert(!std::is_default_constructible>::value, + "interpolation stencils must be constructed by a grid"); TEST_CASE("grid rejects shape products exceeding supported size", "[grid][validation]") @@ -185,7 +190,7 @@ TEST_CASE("grid rejects shape products exceeding supported size", ndtbl::Axis::uniform(0.0, 1.0, huge_extent), }; - REQUIRE_THROWS_AS(ndtbl::Grid<2>(axes), std::runtime_error); + REQUIRE_THROWS_AS(ndtbl::Grid<2>(axes), std::overflow_error); } TEST_CASE("field group rejects payload shape products exceeding supported size", @@ -200,7 +205,7 @@ TEST_CASE("field group rejects payload shape products exceeding supported size", REQUIRE_THROWS_AS( (ndtbl::FieldGroup<1, double>(grid, { "A", "B" }, std::vector())), - std::runtime_error); + std::overflow_error); } TEST_CASE("payload view keeps checked access separate from typed fast access", @@ -223,9 +228,8 @@ TEST_CASE("field group evaluates unaligned byte-backed payloads", }; const std::vector payload = { 1.0, 3.0 }; - std::shared_ptr> storage = - std::make_shared>( - payload.size() * sizeof(double) + 1u); + auto storage = std::make_shared>( + payload.size() * sizeof(double) + 1u); std::uint8_t* const unaligned_data = storage->data() + 1u; std::memcpy(unaligned_data, payload.data(), payload.size() * sizeof(double)); diff --git a/tests/ndtbl_io_t.cpp b/tests/ndtbl_io_t.cpp index 3bd4af3..e11af67 100644 --- a/tests/ndtbl_io_t.cpp +++ b/tests/ndtbl_io_t.cpp @@ -1,3 +1,5 @@ +// SPDX-License-Identifier: MIT + #include "ndtbl/ndtbl.hpp" #include "test_support.hpp" @@ -9,10 +11,70 @@ #include #include #include +#include #include #include +#include #include +static_assert(std::is_base_of::value, + "ndtbl errors must expose the standard runtime-error API"); +static_assert(std::is_base_of::value, + "format errors must derive from the ndtbl error base"); +static_assert(std::is_base_of::value, + "I/O errors must derive from the ndtbl error base"); +static_assert(std::is_base_of::value, + "state errors must derive from the ndtbl error base"); + +TEST_CASE("ndtbl exceptions preserve messages and support base catches", + "[exceptions]") +{ + REQUIRE_THROWS_AS([]() { throw ndtbl::FormatError("invalid table"); }(), + ndtbl::Error); + REQUIRE_THROWS_AS([]() { throw ndtbl::IOError("failed read"); }(), + std::exception); + + const ndtbl::StateError error("empty group"); + REQUIRE(std::string(error.what()) == "empty group"); +} + +TEST_CASE("file and stream failures use ndtbl I/O errors", "[exceptions][io]") +{ + const std::string missing_path = ndtbl_test::temporary_path(); + std::remove(missing_path.c_str()); + REQUIRE_THROWS_AS(ndtbl::read_group_metadata(missing_path), ndtbl::IOError); + + const std::array axes = { + ndtbl::Axis::uniform(0.0, 1.0, 2), + }; + const ndtbl::FieldGroup<1, double> group( + ndtbl::Grid<1>(axes), { "A" }, { 0.0, 1.0 }); + std::ostringstream failed_output; + failed_output.setstate(std::ios::badbit); + REQUIRE_THROWS_AS(ndtbl::write_group_stream(failed_output, group), + ndtbl::IOError); +} + +TEST_CASE("empty runtime field groups use ndtbl state errors", + "[exceptions][field_group]") +{ + const ndtbl::RuntimeFieldGroup<1> group; + std::array values = { 0.0 }; + std::ostringstream output; + + REQUIRE_THROWS_AS(group.field_count(), ndtbl::StateError); + REQUIRE_THROWS_AS(group.value_type(), ndtbl::StateError); + REQUIRE_THROWS_AS(group.field_names(), ndtbl::StateError); + REQUIRE_THROWS_AS(group.axes(), ndtbl::StateError); + REQUIRE_THROWS_AS(group.field_index("A"), ndtbl::StateError); + REQUIRE_THROWS_AS(group.evaluate_all_linear_into({ 0.5 }, values.data()), + ndtbl::StateError); + REQUIRE_THROWS_AS(group.evaluate_all_cubic_into({ 0.5 }, values.data()), + ndtbl::StateError); + REQUIRE_THROWS_AS(group.payload_residency(), ndtbl::StateError); + REQUIRE_THROWS_AS(group.write(output), ndtbl::StateError); +} + TEST_CASE("typed loader round-trips metadata and float payloads", "[io]") { const std::array axes = { @@ -147,7 +209,7 @@ TEST_CASE("typed field group loader rejects wrong scalar type", "[io]") ndtbl::write_group(path, group); REQUIRE_THROWS_AS((ndtbl::read_field_group<1, float>(path)), - std::runtime_error); + ndtbl::FormatError); std::remove(path.c_str()); } @@ -291,7 +353,7 @@ TEST_CASE("runtime field group can be evaluated concurrently", "[io]") }; std::vector payload; for (std::size_t point = 0; point < axes[0].size(); ++point) { - const float coordinate = static_cast(axes[0].coordinate(point)); + const auto coordinate = static_cast(axes[0].coordinate(point)); payload.push_back(coordinate); payload.push_back(10.0f + 2.0f * coordinate); } @@ -309,8 +371,7 @@ TEST_CASE("runtime field group can be evaluated concurrently", "[io]") threads.push_back(std::thread([&, thread]() { std::array values = { 0.0, 0.0 }; for (std::size_t iteration = 0; iteration < iterations; ++iteration) { - const double coordinate = - static_cast((thread + iteration) % 16); + const auto coordinate = static_cast((thread + iteration) % 16); runtime.evaluate_all_linear_into({ coordinate }, values.data()); if (values[0] != Catch::Approx(coordinate) || values[1] != Catch::Approx(10.0 + 2.0 * coordinate)) { @@ -321,12 +382,12 @@ TEST_CASE("runtime field group can be evaluated concurrently", "[io]") })); } - for (std::size_t thread = 0; thread < threads.size(); ++thread) { - threads[thread].join(); + for (auto& thread : threads) { + thread.join(); } - for (std::size_t thread = 0; thread < failures.size(); ++thread) { - REQUIRE(failures[thread] == 0); + for (const auto failure : failures) { + REQUIRE(failure == 0); } } @@ -342,9 +403,9 @@ TEST_CASE("typed loader rejects mismatched dimensions", "[io]") ndtbl::write_group(path, group); REQUIRE_THROWS_AS(ndtbl::read_runtime_field_group<2>(path), - std::runtime_error); + ndtbl::FormatError); REQUIRE_THROWS_AS((ndtbl::read_field_group<2, double>(path)), - std::runtime_error); + ndtbl::FormatError); std::remove(path.c_str()); } @@ -366,7 +427,7 @@ TEST_CASE("typed loader rejects truncated payload files", "[io]") ndtbl_test::write_file_bytes(path, bytes); REQUIRE_THROWS_AS(ndtbl::read_runtime_field_group<1>(path), - std::runtime_error); + ndtbl::FormatError); std::remove(path.c_str()); } @@ -423,7 +484,30 @@ TEST_CASE("typed loader rejects nonzero reserved header fields", "[io]") bytes[10] = 1; ndtbl_test::write_file_bytes(path, bytes); - REQUIRE_THROWS_AS(ndtbl::read_group_metadata(path), std::runtime_error); + REQUIRE_THROWS_AS(ndtbl::read_group_metadata(path), ndtbl::FormatError); + + std::remove(path.c_str()); +} + +TEST_CASE("typed loader translates invalid file axes to format errors", "[io]") +{ + const std::array axes = { + ndtbl::Axis::uniform(0.0, 1.0, 2), + }; + const ndtbl::FieldGroup<1, double> group( + ndtbl::Grid<1>(axes), { "A" }, { 0.0, 1.0 }); + + const std::string path = ndtbl_test::temporary_path(); + ndtbl::write_group(path, group); + + std::vector bytes = ndtbl_test::read_file_bytes(path); + REQUIRE(bytes.size() > 55); + for (std::size_t index = 48; index < 56; ++index) { + bytes[index] = 0; + } + ndtbl_test::write_file_bytes(path, bytes); + + REQUIRE_THROWS_AS(ndtbl::read_group_metadata(path), ndtbl::FormatError); std::remove(path.c_str()); } @@ -446,7 +530,7 @@ TEST_CASE("typed loader rejects mismatched payload offsets", "[io]") } ndtbl_test::write_file_bytes(path, bytes); - REQUIRE_THROWS_AS(ndtbl::read_group_metadata(path), std::runtime_error); + REQUIRE_THROWS_AS(ndtbl::read_group_metadata(path), ndtbl::FormatError); std::remove(path.c_str()); } diff --git a/tests/test_support.hpp b/tests/test_support.hpp index a472958..25b58f3 100644 --- a/tests/test_support.hpp +++ b/tests/test_support.hpp @@ -1,3 +1,5 @@ +// SPDX-License-Identifier: MIT + #pragma once #include "ndtbl/axis.hpp" @@ -77,7 +79,7 @@ write_file_bytes(const std::string& path, const std::vector& bytes) } template -inline void +void append_uint_le(std::vector& bytes, UInt value) { for (std::size_t index = 0; index < sizeof(UInt); ++index) { @@ -102,7 +104,7 @@ append_float_le(std::vector& bytes, float value) } template -inline double +double linear_value(const std::array& coordinates, const std::array& coefficients, double intercept) @@ -115,7 +117,7 @@ linear_value(const std::array& coordinates, } template -inline std::array +std::array clamp_to_axes(const std::array& axes, const std::array& coordinates) { @@ -131,7 +133,7 @@ clamp_to_axes(const std::array& axes, } template -inline std::vector +std::vector build_linear_payload(const std::array& axes, const std::array& coeffs_a, double intercept_a,