Signed-Stride mdspan
Kernels should consume coordinates, not reimplement NumPy layout rules.
std::mdspan provides that interface, but std::layout_stride::mapping
requires positive supplied strides. Solvcon keeps mdspan and its accessor, and
replaces only the mapping that connects coordinates to offsets.
The standard gap
LayoutPolicy::mapping, not
mdspan or its accessor.
A custom mapping must still return a nonnegative total offset because
default_accessor receives that offset as size_t.
The custom mapping in code
SignedStrideLayout keeps the signed per-axis strides. During construction it
finds the lowest and highest raw offsets and records the translation needed to
make every mapping result nonnegative:
constexpr void SignedStrideLayout::mapping<Extents>::initialize_offsets() noexcept
{
index_type min_offset = 0;
index_type max_offset = 0;
for (rank_type rank = 0; rank < extents_type::rank(); ++rank)
{
if (m_extents.extent(rank) == 0)
{
m_origin_offset = 0;
m_required_span_size = 0;
return;
}
index_type const axis_offset =
(m_extents.extent(rank) - 1) * m_strides[rank];
if (axis_offset < 0)
{
min_offset += axis_offset;
}
else
{
max_offset += axis_offset;
}
}
m_origin_offset = -min_offset;
m_required_span_size = max_offset - min_offset + 1;
}
Each axis is linear, so its extrema occur at index 0 or extent-1. The loop
sums those independent endpoint contributions.
The mapping applies the saved translation on every access:
index_type offset = m_origin_offset;
rank_type rank = 0;
((offset += static_cast<index_type>(indices) * m_strides[rank++]), ...);
return offset;
This implements one mapping equation:
For shape (2,3) and strides (-3,-1), the two axis endpoints contribute
-3 and -2. Therefore min=-5, origin_offset=5, and the descriptor
endpoints (0,0) and (1,2) map to 5 and 0.
Rebasing the data handle
Translating the mapping alone would change physical addresses. as_mdspan()
therefore moves the handle in the opposite direction:
auto const mapping = make_mdspan_mapping<N>();
value_type * data_handle = logical_data();
if (data_handle)
{
data_handle -= mapping.origin_offset();
}
return std::mdspan<value_type,
std::dextents<ssize_t, N>,
detail::SignedStrideLayout>(data_handle, mapping);
The two translations cancel:
where o is origin_offset. The adjusted handle is the lowest reachable
address. It need not equal data() if a general ConcreteBuffer contains an
unused prefix.
using Array = solvcon::SimpleArray<double>;
auto buffer = solvcon::ConcreteBuffer::construct(6 * sizeof(double));
Array a(
Array::shape_type{2, 3},
Array::shape_type{-3, -1},
buffer,
5 * sizeof(double));
auto view = a.as_mdspan<2>();
// &view[0, 0] == a.logical_data()
// &view[1, 2] == a.data()
Contracts and limitations
- Shape storage is signed, but each shape value must remain nonnegative.
- The required span is the envelope of reachable addresses and may contain holes.
- An empty extent produces a zero-size required span.
- The mapping reports uniqueness, but runtime code does not reject zero or overlapping strides. The caller must provide a unique, representable mapping.
- Offset arithmetic has no overflow check, and element access has no bounds check.
as_mdspan<N>()checks the runtime rank but does not own the array or buffer.nghostis not mdspan metadata. The body begins at mdspan indexnghoston axis 0.
Python assignment does not use this mdspan path. The next page translates a Python key into storage coordinates, then follows the same descriptor strides.