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 contiguous; 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 for a buffer that allocated its own memory.

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.