The SimpleArray Data Model
SimpleArray has a direct goal: build a NumPy-like array that can outperform
NumPy on Solvcon's numerical workloads. It does not reproduce the whole NumPy
API. Instead, it keeps a simplified version of NumPy's view architecture, then
exposes those facts to an operation layer that selects a kernel and
independently decides whether to pack an input.
From Python to C++
Python call
SimpleArrayFloat64(array=view)
↓ pybind11 array= binding
C++ factory
make_array_from_numpy(py::array&)
↓ returns
SimpleArray<double> state
├─ SignedStrideLayout → mdspan
├─ ArrayPropertyHelper
│ → index / assign
└─ MatmulPlan + MatmulExecutor
→ kernel + packing
The Python call constructs the C++ object directly:
import numpy as np
import solvcon
base = np.arange(6, dtype=np.float64).reshape(2, 3)
view = base[::-1, ::-1]
a = solvcon.SimpleArrayFloat64(array=view)
WrapSimpleArray<double> registers the pybind11 class. This contiguous part of
the binding chain connects construction, the buffer protocol, cloning, and
NumPy export to C++ functions:
.def(py::init(&WrapSimpleArray::make_array_from_numpy), py::arg("array"))
.def_buffer(&property_helper::get_buffer_info)
.def("clone",
[](wrapped_type const & self)
{ return wrapped_type(self); })
.def_property_readonly(
"ndarray",
[](wrapped_type & self)
{ return to_ndarray(self); })
Later in the same binding chain, assignment is connected separately:
pybind11 converts view to py::array &, calls make_array_from_numpy(), and
keeps the returned SimpleArray<double> behind the Python handle.
__setitem__ calls the C++ parser, while ndarray calls to_ndarray() for the
opposite direction.
How Python finds the C++ class
The compiled extension initializes the C++ bindings:
The buffer initializer registers SimpleArray<double> under its Python
name:
WrapSimpleArray<double>::commit(
mod, "SimpleArrayFloat64", "SimpleArrayFloat64");
solvcon.core loads that symbol from _solvcon, and solvcon.__init__
re-exports it as solvcon.SimpleArrayFloat64.
Two terms in this guide describe roles, not additional classes. Descriptor
means the state stored by SimpleArray<T>: buffer, logical origin, shape,
strides, and ghost/body state. Adapter means boundary code that constructs
or presents that state in another interface. make_array_from_numpy() and
to_ndarray() form the NumPy boundary. as_mdspan() is the C++ view adapter,
and SignedStrideLayout supplies its signed-stride mapping. WrapSimpleArray
is the pybind11 registration helper, and ArrayPropertyHelper handles Python
keys. The operation plan is a consumer of the descriptor, not another adapter.
The descriptor in code
std::shared_ptr<buffer_type> m_buffer;
shape_type m_shape;
shape_type m_stride;
value_type * m_logical_data = nullptr;
ssize_t m_nghost = 0;
value_type * m_body = nullptr;
ConcreteBuffer owns or retains one byte span. The fields have distinct roles:
| field | meaning | reversed-view example |
|---|---|---|
data() |
beginning of the retained buffer | root + 0 |
logical_data() |
descriptor coordinate (0,...,0) |
root + 5 |
shape |
valid coordinate domain | (2,3) |
stride |
element offset for one axis step | (-3,-1) |
body() |
body origin on axis 0 | logical_data + nghost * stride[0] |
Keeping data() and logical_data() separate is essential. The first belongs
to storage lifetime; the second belongs to coordinate mapping. In the reversed
example, coordinate (0,0) moves to root+5 and both axis directions change,
but no values move.
Every coordinate uses the same rule:
For the reversed view:
Inside the NumPy import adapter
make_array_from_numpy() receives the py::array from the binding. NumPy
reports byte strides, so the adapter converts them to element strides and finds
the complete reachable byte envelope:
solvcon::detail::shape_type shape;
solvcon::detail::shape_type stride;
constexpr auto itemsize = static_cast<ssize_t>(sizeof(value_type));
ssize_t byte_span_begin = 0;
ssize_t byte_span_end = 0;
bool has_element = true;
for (ssize_t i = 0; i < arr_in.ndim(); ++i)
{
shape.push_back(arr_in.shape(i));
ssize_t const byte_stride = arr_in.strides(i);
if (byte_stride % itemsize != 0)
{
throw std::runtime_error(
std::format("NumPy byte stride {} in dimension {} is not divisible by item size {}",
byte_stride, i, itemsize));
}
stride.push_back(byte_stride / itemsize);
if (shape[i] == 0)
{
has_element = false;
continue;
}
ssize_t const axis_byte_offset = (shape[i] - 1) * byte_stride;
if (axis_byte_offset < 0)
{
byte_span_begin += axis_byte_offset;
}
else
{
byte_span_end += axis_byte_offset;
}
}
if (!has_element)
{
byte_span_begin = 0;
byte_span_end = 0;
}
After retaining an ndarray base-chain anchor and checking alignment, the adapter constructs the storage and descriptor:
char * storage_ptr = view_ptr + byte_span_begin;
const size_t storage_nbytes = has_element
? static_cast<size_t>(byte_span_end - byte_span_begin + itemsize)
: 0;
const auto data_offset = static_cast<size_t>(-byte_span_begin);
auto remover = std::make_unique<ConcreteBufferNdarrayRemover>(owner);
const auto buffer = ConcreteBuffer::construct(storage_nbytes, storage_ptr, std::move(remover));
return wrapped_type(shape, stride, buffer, data_offset, array_order);
The envelope may contain padding holes, so its byte size need not equal
prod(shape) * sizeof(T). logical_data is reconstructed from data_offset.
Lifetime and copy
The imported view and a NumPy round trip share the same bytes:
a[1, 2] = -1
assert base[0, 0] == -1
roundtrip = a.ndarray
assert np.shares_memory(base, roundtrip)
clone() is the explicit deep-copy path. It clones the complete retained
ConcreteBuffer and preserves the logical_data - data offset, shape, and
strides.
Next steps
- Signed-stride mdspan turns this descriptor into a C++ view.
- Indexing and assignment translates Python coordinates before applying the same address rule.
- Matmul will use the descriptor for planning and dispatch.
Sources
_solvconpybind11 module entry point- Python loads symbols from the compiled extension
- The package re-exports the core symbols
SimpleArrayFloat64class registration- NumPy constructor and Python method bindings
to_ndarray()NumPy export adapterSimpleArray<T>descriptor membersdata(),logical_data(), andbody()accessorsSimpleArray<T>layout validation- NumPy view importer