Memory Buffers

The raw memory layer of the SimpleArray family consists of two untyped byte buffers. ConcreteBuffer owns a fixed-size block of contiguous memory and is the storage that every typed array sits on. BufferExpander is a growable staging buffer for code that must accumulate bytes before the final size is known, and can hand its content over as a ConcreteBuffer.

ConcreteBuffer

ConcreteBuffer is an untyped byte buffer: no dtype, no shape, and no stride. Once constructed, its size never changes.

Construction

A buffer is constructed with a byte count and an optional alignment:

buf = solvcon.ConcreteBuffer(1024)
buf = solvcon.ConcreteBuffer(1024, alignment=64)

The memory is allocated immediately and is not initialized. The nbytes property returns the byte count and alignment returns the requested alignment. Valid alignment values are 0 (the default, no specific alignment), 16, 32, and 64 bytes; any other value raises ValueError. When a non-zero alignment is requested, the byte count must be a multiple of the alignment, or the allocation raises ValueError. A zero-byte buffer is permitted and records the requested alignment without allocating.

Size and Byte Access

The buffer sizes and indexes like a Python sequence of bytes. len(buf) equals buf.nbytes. Indexing with buf[i] reads one byte and assignment buf[i] = value writes one byte; the element type is a signed 8-bit integer. Indexing at or beyond the size raises IndexError:

buf = solvcon.ConcreteBuffer(10)
for it in range(len(buf)):
    buf[it] = it
buf[10]  # IndexError: ConcreteBuffer: index 10 is out of bounds ...

Iteration follows from indexing, so list(buf) returns the byte values.

Sharing with Numpy

ConcreteBuffer implements the Python buffer protocol, exposing its memory as a one-dimensional int8 sequence. A numpy array built on the buffer shares the memory rather than copying it:

import numpy as np
buf = solvcon.ConcreteBuffer(10)
ndarr = np.array(buf, copy=False)  # dtype int8, shape (10,)
buf[3] = 7
assert ndarr[3] == 7

The ndarray property is a shortcut that returns such an int8 view directly. The returned array holds a reference to the buffer, so the memory stays alive as long as the array does.

Cloning

clone() returns a deep copy: a new buffer with the same byte count, the same alignment, and a copy of the content. Writes to either buffer are not visible in the other.

Wrapping a Numpy Array

The second constructor form wraps the memory of an existing numpy array instead of allocating:

ndarr = np.arange(24, dtype='float64').reshape((2, 3, 4))
buf = solvcon.ConcreteBuffer(array=ndarr)
assert buf.nbytes == ndarr.nbytes

The dtype and shape of the source array do not matter, but the array must be C- or F-contiguous; other layouts raise ValueError. The buffer covers its nbytes bytes and shares them zero-copy, so writing through one side is visible on the other. The constructor also accepts the optional alignment keyword, validated against the same set (0, 16, 32, or 64); no size-multiple check applies because nothing is allocated. The buffer holds a reference to the source array, tying the lifetime of the memory to the Python object. The is_from_python property reports the provenance: it is True for a buffer wrapping a numpy array and False otherwise, including a buffer over memory that is owned by something other than a numpy array.

BufferExpander

BufferExpander is an untyped byte buffer that grows. It distinguishes its size (the bytes currently in use) from its capacity (the bytes allocated). Its role is a staging area: accumulate bytes while the final length is unknown, then hand the result over as a ConcreteBuffer for fixed-size access. The internal expandable memory is never exposed to other components.

Construction

Three forms are supported:

ep = solvcon.BufferExpander()          # empty: size 0, capacity 0
ep = solvcon.BufferExpander(10)        # size 10, capacity 10
ep = solvcon.BufferExpander(buf)       # copy of a ConcreteBuffer

The third form initializes size and capacity to the byte count of the given ConcreteBuffer and copies its content; the expander does not alias the source buffer, so later writes to the expander leave the source unchanged. The sized and buffer-copying forms accept an optional alignment keyword with the same valid values as ConcreteBuffer (0, 16, 32, or 64); the empty form fixes the alignment at 0. The read-only alignment property returns the value given at construction.

Growing

reserve(cap) grows the capacity to at least cap bytes while keeping the size and the existing content; when cap does not exceed the current capacity it does nothing. Capacity never shrinks. expand(length) reserves length bytes and then sets the size to length:

ep = solvcon.BufferExpander()
ep.reserve(10)   # capacity 10, size still 0
ep.expand(10)    # capacity 10, size 10

The capacity property returns the allocated byte count and len(ep) returns the size.

Byte Access

Indexing works as on ConcreteBuffer: ep[i] reads and ep[i] = value writes single signed 8-bit bytes. Bounds are checked against the size, not the capacity, so a reserved-but-unexpanded region is not addressable and raises IndexError.

Producing a ConcreteBuffer

Two methods convert the staged bytes into a ConcreteBuffer, differing in whether the result shares memory with the expander:

  • copy_concrete(cap=0) returns an independent copy. The new buffer holds max(cap, len(ep)) bytes with the staged content copied in; later writes on either side do not affect the other.

  • as_concrete(cap=0) converts the expander in place. The expander adopts a ConcreteBuffer as its storage and returns it; from then on the two objects share memory, so a write through one is visible through the other. When the expander is not yet concrete, the adopted buffer holds max(cap, len(ep)) bytes; when it already is, cap is ignored and the existing buffer is returned.

clone() returns a new independent BufferExpander with a copy of the content.

The is_concrete property reports whether the expander is currently backed by a ConcreteBuffer. It is False for a freshly expanded buffer, True after as_concrete(), and True for an expander constructed from a ConcreteBuffer:

ep = solvcon.BufferExpander(10)
assert not ep.is_concrete
cbuf = ep.as_concrete()
assert ep.is_concrete
cbuf[0] = 42
assert ep[0] == 42  # memory is shared

Growing the capacity after as_concrete() reallocates the storage and detaches the expander from the concrete buffer: is_concrete drops back to False and the previously returned buffer keeps the old memory.