Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
15 commits
Select commit Hold shift + click to select a range
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
21 changes: 21 additions & 0 deletions .github/workflows/release-pypi.yml
Original file line number Diff line number Diff line change
Expand Up @@ -138,3 +138,24 @@ jobs:
- name: Publish distributions to PyPI
run: |
uv publish dist/*

github-release:
name: Create GitHub Release
if: ${{ github.event_name == 'push' }}
needs: [build, publish-pypi]
runs-on: ubuntu-latest
permissions:
contents: write

steps:
- name: Download built distributions
uses: actions/download-artifact@v8
with:
name: python-package-distributions
path: dist/

- name: Create GitHub Release
uses: softprops/action-gh-release@v3
with:
generate_release_notes: true
files: dist/*
2 changes: 1 addition & 1 deletion .pre-commit-config.yaml
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
repos:
# Format C++ code with Clang-Format - automatically applying the changes
- repo: https://github.com/pre-commit/mirrors-clang-format
rev: v22.1.4
rev: v22.1.5
hooks:
- id: clang-format
args:
Expand Down
1 change: 1 addition & 0 deletions CMakeLists.txt
Original file line number Diff line number Diff line change
Expand Up @@ -39,6 +39,7 @@ target_sources(ndtbl
include/ndtbl/payload.hpp
include/ndtbl/io.hpp
include/ndtbl/ndtbl.hpp
include/ndtbl/detail/size_math.hpp
include/ndtbl/detail/binary_io.hpp
include/ndtbl/detail/mapped_payload.hpp
)
Expand Down
11 changes: 6 additions & 5 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -67,10 +67,11 @@ Relevant CMake options:
- `ndtbl_BUILD_DOCS`: build the documentation, default `ON` for top-level builds
- `ndtbl_ENABLE_MMAP`: enable POSIX-only `mmap`-backed payload reads, default `OFF`

When `ndtbl_ENABLE_MMAP=OFF` (the default), `read_group()` reads payload data
into owned heap storage. When `ndtbl_ENABLE_MMAP=ON`, supported POSIX builds
use read-only memory mapping instead, which can reduce heap usage for large
tables and enables shared memory access in multi-process environments.
When `ndtbl_ENABLE_MMAP=OFF` (the default), `read_field_group()` and
`read_runtime_field_group()` read payload data into owned heap storage. When
`ndtbl_ENABLE_MMAP=ON`, supported POSIX builds use read-only memory mapping
instead, which can reduce heap usage for large tables and enables shared memory
access in multi-process environments.

If you want to install the C++ headers and CMake package metadata:

Expand Down Expand Up @@ -159,7 +160,7 @@ runtime-erased combined lookup for representative 2D, 4D, and 6D tables. See
Build the benchmark target:

```bash
cmake -B build -Dndtbl_BUILD_BENCHMARKS=ON
cmake -B build -DCMAKE_BUILD_TYPE=Release -Dndtbl_BUILD_BENCHMARKS=ON
cmake --build build --target ndtbl_lookup_benchmarks
```

Expand Down
6 changes: 3 additions & 3 deletions benchmarks/lookup_benchmarks.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -50,13 +50,13 @@ template<std::size_t Dim>
struct LookupContext
{
ndtbl::Grid<Dim> grid;
ndtbl::FieldGroup<double, Dim> group;
ndtbl::FieldGroup<Dim, double> group;
ndtbl::RuntimeFieldGroup<Dim> runtime_group;
std::vector<std::array<double, Dim>> queries;
ndtbl::LinearStencil<Dim> prepared;

LookupContext(const ndtbl::Grid<Dim>& grid_in,
const ndtbl::FieldGroup<double, Dim>& group_in,
const ndtbl::FieldGroup<Dim, double>& group_in,
const std::vector<std::array<double, Dim>>& queries_in)
: grid(grid_in)
, group(group_in)
Expand Down Expand Up @@ -229,7 +229,7 @@ make_context(std::size_t extent,
const std::array<std::size_t, Dim> shape = filled_shape<Dim>(extent);
const std::array<ndtbl::Axis, Dim> axes = make_axes(shape, axis_kind);
const ndtbl::Grid<Dim> grid(axes);
const ndtbl::FieldGroup<double, Dim> group(
const ndtbl::FieldGroup<Dim, double> group(
grid, make_field_names<Dim>(field_count), make_payload(grid, field_count));
return LookupContext<Dim>(grid, group, make_queries(axes, query_count));
}
Expand Down
4 changes: 4 additions & 0 deletions doc/cppapi.rst
Original file line number Diff line number Diff line change
Expand Up @@ -35,3 +35,7 @@ API reference
.. doxygenenum:: ndtbl::bounds_policy

.. doxygenfunction:: ndtbl::read_group_metadata(const std::string &path)

.. doxygenfunction:: ndtbl::read_field_group(const std::string &path)

.. doxygenfunction:: ndtbl::read_runtime_field_group(const std::string &path)
8 changes: 8 additions & 0 deletions include/ndtbl/axis.hpp
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@
#include "ndtbl/types.hpp"

#include <algorithm>
#include <cmath>
#include <cstddef>
#include <iterator>
#include <stdexcept>
Expand All @@ -20,6 +21,9 @@ namespace ndtbl {
class Axis
{
public:
/**
* @brief Construct a single-point uniform axis at coordinate zero.
*/
Axis()
: kind_(axis_kind::uniform)
, size_(1)
Expand Down Expand Up @@ -201,6 +205,10 @@ class Axis
double value,
bounds_policy policy = bounds_policy::clamp) const
{
if (!std::isfinite(value)) {
throw std::out_of_range("axis query coordinate must be finite");
}

if (policy == bounds_policy::throw_error &&
(value < min_ || value > max_)) {
throw std::out_of_range("axis query coordinate outside bounds");
Expand Down
108 changes: 49 additions & 59 deletions include/ndtbl/detail/binary_io.hpp
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
#pragma once

#include "ndtbl/detail/size_math.hpp"
#include "ndtbl/field_group.hpp"
#include "ndtbl/metadata.hpp"

Expand All @@ -25,7 +26,8 @@ namespace detail {
* This constant is part of the binary file format implementation and is not
* intended to be consumed directly by library users.
*/
static const char file_magic[8] = { 'N', 'D', 'T', 'B', 'L', '\0', '\0', '\0' };
static constexpr char file_magic[8] = { 'N', 'D', 'T', 'B',
'L', '\0', '\0', '\0' };

/**
* @brief Write one exact byte sequence to a binary stream.
Expand Down Expand Up @@ -132,10 +134,10 @@ read_float_le(std::istream& is)
return value;
}

template<class Value>
template<class Stored>
struct payload_uint
{
typedef typename std::conditional<sizeof(Value) == sizeof(std::uint32_t),
typedef typename std::conditional<sizeof(Stored) == sizeof(std::uint32_t),
std::uint32_t,
std::uint64_t>::type type;
};
Expand Down Expand Up @@ -177,34 +179,6 @@ read_string(std::istream& is)
return value;
}

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<std::size_t>::max() / lhs) {
throw std::runtime_error("ndtbl " + what + " exceeds supported size");
}
return lhs * rhs;
}

inline std::size_t
checked_add_size(std::size_t lhs, std::size_t rhs, const std::string& what)
{
if (rhs > std::numeric_limits<std::size_t>::max() - lhs) {
throw std::runtime_error("ndtbl " + what + " exceeds supported size");
}
return lhs + rhs;
}

inline std::size_t
narrow_u64_to_size(std::uint64_t value, const std::string& what)
{
if (value >
static_cast<std::uint64_t>(std::numeric_limits<std::size_t>::max())) {
throw std::runtime_error("ndtbl " + what + " exceeds supported size");
}
return static_cast<std::size_t>(value);
}

inline void
require_zero(std::uint64_t value, const std::string& what)
{
Expand All @@ -213,7 +187,7 @@ require_zero(std::uint64_t value, const std::string& what)
}
}

inline std::size_t
inline constexpr std::size_t
fixed_header_size()
{
return sizeof(file_magic) + sizeof(std::uint8_t) + sizeof(std::uint8_t) +
Expand Down Expand Up @@ -251,19 +225,30 @@ metadata_size(const GroupMetadata& metadata)
return total;
}

inline std::size_t
axis_point_count(const std::vector<Axis>& axes)
{
std::size_t point_count = 1;
for (std::size_t axis = 0; axis < axes.size(); ++axis) {
point_count =
checked_multiply_size(point_count, axes[axis].size(), "point count");
}
return point_count;
}

/**
* @brief Internal implementation for writing raw metadata and payload data.
*
* @tparam Value Scalar payload type stored in the payload vector.
* @tparam Stored Scalar payload type stored in the payload vector.
* @param os Destination stream in binary mode.
* @param metadata File metadata describing the payload layout.
* @param payload Point-major interleaved field payload.
*/
template<class Value>
template<class Stored>
inline void
write_group_stream_impl(std::ostream& os,
const GroupMetadata& metadata,
const PayloadView<Value>& payload)
const PayloadView<Stored>& payload)
{
if (metadata.axes.size() != metadata.dimension) {
throw std::invalid_argument(
Expand All @@ -275,6 +260,16 @@ write_group_stream_impl(std::ostream& os,
"ndtbl metadata field count does not match field names");
}

if (metadata.value_type != scalar_type_of<Stored>()) {
throw std::invalid_argument(
"ndtbl metadata scalar type does not match payload type");
}

if (metadata.point_count != axis_point_count(metadata.axes)) {
throw std::invalid_argument(
"ndtbl point count does not match axis extents");
}

const std::size_t expected_values = checked_multiply_size(
metadata.point_count, metadata.field_count, "payload value count");
if (payload.size() != expected_values) {
Expand All @@ -284,7 +279,7 @@ write_group_stream_impl(std::ostream& os,
const std::size_t payload_offset = metadata_size(metadata);

write_bytes(os, file_magic, sizeof(file_magic));
write_uint_le<std::uint8_t>(os, 1u);
write_uint_le<std::uint8_t>(os, current_format_version);
write_uint_le<std::uint8_t>(os,
static_cast<std::uint8_t>(metadata.value_type));
write_uint_le<std::uint16_t>(os, 0u);
Expand Down Expand Up @@ -326,8 +321,8 @@ write_group_stream_impl(std::ostream& os,
payload.byte_size());
} else {
for (std::size_t index = 0; index < payload.size(); ++index) {
write_float_le<Value, typename payload_uint<Value>::type>(
os, payload[index]);
write_float_le<Stored, typename payload_uint<Stored>::type>(
os, payload.unchecked(index));
}
}
}
Expand All @@ -336,18 +331,18 @@ write_group_stream_impl(std::ostream& os,
/**
* @brief Internal implementation for writing a typed field group.
*
* @tparam Value Scalar payload type stored in the group.
* @tparam Stored Scalar payload type stored in the group.
* @tparam Dim Grid dimensionality of the group.
* @param os Destination stream in binary mode.
* @param group Typed field group to serialize.
* @see write_group_stream_impl(std::ostream&, const GroupMetadata&,
* const std::vector<Value>&)
* const std::vector<Stored>&)
*/
template<class Value, std::size_t Dim>
template<class Stored, std::size_t Dim>
inline void
write_group_stream_impl(std::ostream& os, const FieldGroup<Value, Dim>& group)
write_group_stream_impl(std::ostream& os, const FieldGroup<Dim, Stored>& group)
{
GroupMetadata metadata = { scalar_type_of<Value>(),
GroupMetadata metadata = { scalar_type_of<Stored>(),
Dim,
group.field_count(),
group.point_count(),
Expand All @@ -357,11 +352,11 @@ write_group_stream_impl(std::ostream& os, const FieldGroup<Value, Dim>& group)
write_group_stream_impl(os, metadata, group.interleaved_values());
}

template<class Value>
template<class Stored>
inline void
write_group_stream_impl(std::ostream& os,
const GroupMetadata& metadata,
const std::vector<Value>& payload)
const std::vector<Stored>& payload)
{
write_group_stream_impl(os, metadata, payload_view(payload));
}
Expand All @@ -381,7 +376,7 @@ verify_magic(std::istream& is)
}
}

inline std::size_t
inline constexpr std::size_t
scalar_size(scalar_type type)
{
if (type == scalar_type::float32) {
Expand Down Expand Up @@ -413,7 +408,7 @@ read_group_layout_impl(std::istream& is)
verify_magic(is);

const std::uint8_t version = read_uint_le<std::uint8_t>(is);
if (version != 1u) {
if (version != current_format_version) {
throw std::runtime_error("unsupported ndtbl version");
}

Expand Down Expand Up @@ -461,12 +456,7 @@ read_group_layout_impl(std::istream& is)
metadata.field_names.push_back(read_string(is));
}

std::size_t expected_point_count = 1;
for (std::size_t axis = 0; axis < metadata.axes.size(); ++axis) {
expected_point_count = checked_multiply_size(
expected_point_count, metadata.axes[axis].size(), "point count");
}
if (expected_point_count != metadata.point_count) {
if (axis_point_count(metadata.axes) != metadata.point_count) {
throw std::runtime_error("ndtbl point count does not match axis extents");
}

Expand Down Expand Up @@ -499,30 +489,30 @@ read_group_metadata_impl(std::istream& is)
/**
* @brief Read a contiguous payload block from a binary stream.
*
* @tparam Value Scalar payload type to deserialize.
* @tparam Stored Scalar payload type to deserialize.
* @param is Source stream positioned at the start of the payload.
* @param value_count Number of scalar values to read.
* @return Payload vector with `value_count` entries.
*/
template<class Value>
inline std::vector<Value>
template<class Stored>
inline std::vector<Stored>
read_payload(std::istream& is, std::size_t value_count)
{
std::vector<Value> values(value_count);
std::vector<Stored> values(value_count);
if (value_count == 0) {
return values;
}

if (host_is_little_endian()) {
read_bytes(is,
reinterpret_cast<char*>(values.data()),
values.size() * sizeof(Value));
values.size() * sizeof(Stored));
return values;
}

for (std::size_t index = 0; index < value_count; ++index) {
values[index] =
read_float_le<Value, typename payload_uint<Value>::type>(is);
read_float_le<Stored, typename payload_uint<Stored>::type>(is);
}
return values;
}
Expand Down
Loading
Loading