Indexing, Shape, and Layout

Every array in the SimpleArray family reads and writes single elements through the subscript operator, describes its layout through a set of properties, and manipulates its shape and memory order through reshape and the transpose family. This page defines those operations: which keys the subscript accepts, which right-hand sides assignment takes, what the layout properties report, and how the shape and layout of an array are changed. Arrays carrying a ghost region shift the index origin; the sections up to the layout conversions assume arrays without one, and the last section of this page defines the partition and every rule it changes.

Element Access

Element access matches numpy in the index arithmetic and the error behavior (negative wrapping and IndexError), and diverges from numpy in the subscript scope and the return type: a subscript must select exactly one element, and the result is a Python scalar, never a subarray or a view. A one-dimensional array takes a single integer, and a multi-dimensional array takes a full tuple with one integer per dimension:

sarr = solvcon.SimpleArrayFloat64((2, 3, 4))
sarr.ndarray.flat[:] = range(24)
assert sarr[1, 2, 3] == 23.0

sarr1d = solvcon.SimpleArrayInt32(7, value=3)
assert sarr1d[0] == 3

The returned scalar is the Python built-in matching the element type: float for the floating-point classes, int for the integer classes, and bool for SimpleArrayBool. The complex classes return solvcon’s own scalar types, as defined in Zero-Copy between C++ and Python. Numpy instead returns its own scalar types (numpy.float64 and friends); returning the plain Python scalar is the desired behavior.

Partial indexing does not produce subarrays. Where numpy resolves ndarr[0] on a three-dimensional array to a two-dimensional view, the SimpleArray classes require the index to address one element and raise IndexError otherwise:

sarr = solvcon.SimpleArrayFloat64((2, 3, 4))
sarr[0]
# IndexError: SimpleArray::normalize_index(): cannot use scalar index
# for 3-dimensional array
sarr[0, 1]
# IndexError: SimpleArray: dimension of input indices [0, 1] != array
# dimension 3

Negative Indices

Negative indices wrap from the end of each dimension, matching the Python sequence convention that numpy also follows: a negative index i resolves to n + i for a dimension of length n, so the valid interval per dimension is [-n, n):

sarr = solvcon.SimpleArrayFloat64((4, 3, 2))
sarr.ndarray.flat[:] = range(24)
assert sarr[-1, -1, -1] == 23.0
assert sarr[-4, -3, -2] == 0.0
sarr[-1, -1, -1] = 230.0
assert sarr[3, 2, 1] == 230.0

The first axis is the exception, and only when the array carries a ghost region. The negative indices there address the ghost elements rather than wrapping: index -1 is the last ghost element and index -nghost the first, so the wrap begins one position further down, at -nghost - 1. The error messages of this page carry the arithmetic in their nghost term, which is 0 on the ghost-free arrays assumed here. The last section of this page defines the shifted interval in full.

Out-of-Range Errors

An index outside the valid interval raises IndexError, matching numpy’s exception type. The message names the offending index and the violated bound:

sarr = solvcon.SimpleArrayFloat64(3)
sarr[3]
# IndexError: SimpleArray: index 3 >= 3 (shape[0]: 3 - nghost: 0)
sarr[-4]
# IndexError: SimpleArray: index -4 < -nghost - shape[0]: -3

sarr = solvcon.SimpleArrayFloat64((2, 3, 4))
sarr[0, 3, 0]
# IndexError: SimpleArray: dim 1 in [0, 3, 0] >= shape[1]: 3

No Slices on Read

__getitem__ accepts no slice and no ellipsis; only the integer and integer-tuple forms above exist. Passing a slice raises TypeError from the binding’s argument matching:

sarr = solvcon.SimpleArrayFloat64(6)
sarr[0:3]
# TypeError: __getitem__(): incompatible function arguments. ...

This diverges from numpy, where a slice returns a view sharing the memory of the source. Whether the family should grow slice reads, and whether such a read would return a sharing view or a copy, is an open decision; this page records only the current behavior. Until the decision lands, the zero-copy path to sliced reads is the ndarray property: sarr.ndarray[0:3] is a numpy view over the array’s memory.

Element and Region Assignment

__setitem__ accepts two families of keys: the scalar keys of the read path, assigning one element, and slice or ellipsis keys, assigning a whole region from a sequence. A key and value combination outside the two families raises RuntimeError; in particular a scalar value cannot be assigned to a slice key (numpy would broadcast it over the region). The message depends on the rejection path: a scalar on a lone slice or an ellipsis reports “unsupported operation.”, while a scalar on a tuple of slices fails earlier, in the key cast, with a pybind11 “Unable to cast” message.

Scalar Assignment

With an integer key on a one-dimensional array or a full integer tuple on a multi-dimensional array, the value is cast to the element type and stored:

sarr = solvcon.SimpleArrayFloat64((2, 3, 4))
sarr[0, 0, 0] = 10      # a Python int converts to float64
assert sarr[0, 0, 0] == 10.0

The cast follows pybind11 conversion, not numpy value coercion: a Python int converts to a floating-point element, but a value the element type cannot represent exactly is rejected with RuntimeError rather than truncated. Assigning 2.5 to an integer array or 300 to an int8 array both raise, where numpy truncates the float (storing 2) and raises OverflowError for the out-of-range integer. The complex classes accept both solvcon’s own complex scalars and Python or numpy complex values, as defined in Zero-Copy between C++ and Python.

Slice and Ellipsis Assignment

With a slice or ellipsis key, the right-hand side is a sequence whose elements fill the selected region. Four key shapes are accepted:

sarr = solvcon.SimpleArrayFloat64((2, 3, 4))
ndarr = np.arange(24, dtype='float64').reshape((2, 3, 4))

sarr[...] = ndarr                    # ellipsis: the whole array
sarr[0:1] = np.ones((1, 3, 4))       # lone slice: first dimension
sarr[::2, ::3, ::4] = np.zeros((1, 1, 1))  # tuple of slices
sarr[::2, ...] = np.ones((1, 3, 4))  # tuple mixing slices and ellipsis

A lone slice applies to the first dimension and the remaining dimensions take their full extent. In a tuple, slices fill dimensions from the left, an ellipsis expands to full-extent slices for the unnamed middle dimensions, and slices after the ellipsis fill from the right. Steps and negative bounds follow the Python slice rules. The syntax is validated: more slices than dimensions raise RuntimeError (“syntax error. dimensions mismatches”), more than one ellipsis raises RuntimeError (“syntax error. no more than one ellipsis.”), and a zero step raises ValueError (“slice step cannot be zero”).

Accepted Right-Hand Sides

The sequence on the right-hand side may be a numpy ndarray, a list, or a tuple (including nested lists and tuples, which convert through numpy.array):

sarr = solvcon.SimpleArrayFloat64((2, 3))
sarr[:, :] = [[1, 2, 3], [4, 5, 6]]
sarr[:1, :2] = ((7, 8),)

A SimpleArray is not accepted: assigning one array into a slice of another raises RuntimeError (“unsupported operation.”). This diverges from numpy, where an array of the library’s own kind is the most natural right-hand side. Whether the accepted set should grow SimpleArray sources is an open decision; the working spelling today routes through numpy, for example sarr[...] = other.ndarray.

Shape Checking

The shape of the right-hand side must equal the shape selected by the key exactly, dimension count included. There is no numpy-style broadcasting of scalars or lower-dimensional sources, which diverges from numpy assignment. A mismatch raises RuntimeError naming both shapes:

sarr = solvcon.SimpleArrayFloat64((4, 6, 8))
sarr[::2, ::3, ::4] = np.zeros((2, 3, 4))
# RuntimeError: Broadcast input array from shape(2, 3, 4) into
# shape(2, 2, 2)

Dtype Casting

The element type of a sequence right-hand side does not need to match the array: any dtype of the element-type table in Construction and Data Types is converted element-wise during the copy, so an int32 or float32 source fills a float64 array. Three conversions are refused. Mixing complex and non-complex types raises RuntimeError (“Cannot convert between complex and non-complex types”). A complex source fills only the complex array of the same precision, so a complex64 source into a SimpleArrayComplex128 also raises RuntimeError, reusing the same message even though both sides are complex. A dtype outside the table, such as a string dtype, raises RuntimeError (“input array data type not support!”).

Shape and Layout Properties

Five read-only properties describe the layout. shape, size, itemsize, and nbytes match numpy: the shape tuple, the total element count, the byte size of one element, and the total byte count. stride diverges from numpy: it counts elements where numpy strides counts bytes, per the convention defined in Construction and Data Types; Zero-Copy between C++ and Python shows the same view in both units:

sarr = solvcon.SimpleArrayFloat64((2, 3, 4))
assert sarr.shape == (2, 3, 4)
assert sarr.stride == (12, 4, 1)  # elements; numpy reports (96, 32, 8)
assert sarr.size == 24
assert sarr.nbytes == 192
plex = solvcon.SimpleArray((2, 3, 4), dtype='float64')
assert plex.itemsize == 8

On the typed classes, reading itemsize currently raises TypeError: the binding registers the zero-argument C++ getter as an instance property, which pybind11 rejects at access time. This is a defect, not an intended difference; the example above reads the property through the erased wrapper, where it works.

The len() Function

len() diverges from numpy: it returns the total element count, equal to size, for any dimensionality. Numpy returns the length of the first dimension:

sarr = solvcon.SimpleArrayFloat64((2, 3, 4))
assert len(sarr) == 24
assert len(sarr.ndarray) == 2  # numpy counts the first dimension

The desired behavior is the total element count: the arrays serve the solvers as element containers, and len() reports the container size the way len() does on a ConcreteBuffer or a collector. Construction and Data Types notes the divergence where the property first appears; this page carries the full statement.

Reshape

reshape(shape) returns a new array of the given shape over the same buffer. The receiver keeps its shape; the result shares the memory, so a write through either side is visible through the other:

sarr = solvcon.SimpleArrayFloat64((2, 3, 4))
sarr2 = sarr.reshape(24)
assert sarr2.shape == (24,)
sarr2[5] = 42.0
assert sarr[0, 1, 1] == 42.0
assert sarr.shape == (2, 3, 4)  # the receiver is unchanged

The shape argument takes a single integer or a tuple, like the constructors, and the result is always row-major. The element count of the new shape must equal the element count of the receiver; a mismatch raises RuntimeError:

sarr.reshape(23)
# RuntimeError: SimpleArray: cannot reshape size 24 into size 23

The result reads in the logical element order, matching numpy.reshape(order='C'), and the memory is shared only when that order is already the storage order. A receiver that is C-contiguous over a buffer holding exactly its elements yields a sharing view, as the first example shows; any other layout (transposed, Fortran-ordered, or strided) is copied element by element into a fresh dense buffer, and the copy is independent of the receiver:

sarr = solvcon.SimpleArrayFloat64(array=np.arange(6.).reshape((2, 3)))
sarr.transpose()
flat = sarr.reshape(6)
assert [flat[i] for i in range(6)] == [0.0, 3.0, 1.0, 4.0, 2.0, 5.0]
ndarr = np.arange(6.).reshape((2, 3))
assert ndarr.T.reshape(6).tolist() == [0.0, 3.0, 1.0, 4.0, 2.0, 5.0]
flat[0] = 9.0
assert sarr[0, 0] == 0.0          # the copy does not share memory

Numpy infers a dimension given as -1 and raises ValueError on a count mismatch; the SimpleArray reshape never infers and raises RuntimeError. It also rejects an array carrying a ghost region outright, because the split of the first axis has no image under a new shape; the ghost section states that rejection with its message.

Transpose

The transpose family diverges from numpy, which has no in-place transpose and returns sharing views from .transpose() and .T.

The transpose Method

The full signature is transpose(axis=None, inplace=True, copy=False). With axis=None all axes are reversed; with a tuple, the i-th new axis is sourced from the axis[i]-th old axis, following the numpy transpose axis convention except that the entries must be non-negative, where numpy also accepts negative axis numbers:

sarr = solvcon.SimpleArrayFloat64((2, 3, 4))
sarr.transpose()
assert sarr.shape == (4, 3, 2)

sarr = solvcon.SimpleArrayFloat64((2, 3, 4))
sarr.transpose((0, 2, 1))
assert sarr.shape == (2, 4, 3)

The axis tuple must have one entry per dimension and every entry must be a valid non-negative axis; violations raise RuntimeError (“SimpleArray::transpose: axis size mismatch” and “SimpleArray::transpose: axis out of range”). A repeated axis is not currently detected, where numpy raises on a repeated axis; rejecting the repeat is target behavior.

The inplace and copy flags select what is transposed and how:

  • inplace=True (default) transposes the receiver itself; inplace=False leaves the receiver untouched and transposes an independent deep copy.

  • copy=False (default) flips only the metadata: shape and stride are permuted and no element moves. Under the full axis reversal the flip of a C-contiguous source is F-contiguous; a partial permutation such as (0, 2, 1) generally yields a layout that is neither. copy=True physically rearranges the elements into a fresh C-contiguous buffer.

sarr = solvcon.SimpleArrayFloat64((2, 3, 4))
sarr.transpose()                  # metadata flip of sarr itself
assert sarr.is_f_contiguous

sarr = solvcon.SimpleArrayFloat64((2, 3, 4))
sarr.transpose(copy=True)         # physical transpose of sarr itself
assert sarr.is_c_contiguous

sarr = solvcon.SimpleArrayFloat64((2, 3, 4))
sarr2 = sarr.transpose(inplace=False)
assert sarr.shape == (2, 3, 4)    # the receiver is unchanged
assert sarr2.shape == (4, 3, 2)

The method also returns an array, and the returned object never shares memory with the receiver: even with inplace=True the return value is an independent deep copy taken after the mutation. Treat the in-place mutation as the primary effect and the return value as a detached copy. Whether the return value should instead be the receiver (to support chaining) is an open decision; do not rely on the returned object aliasing the receiver.

The transpose_copy Method

transpose_copy() returns a fresh C-contiguous array with the axes reversed and the elements physically rearranged, leaving the receiver untouched; it is the counterpart of numpy.ascontiguousarray(ndarr.T):

sarr = solvcon.SimpleArrayFloat64(array=np.arange(6.).reshape((2, 3)))
tc = sarr.transpose_copy()
assert tc.shape == (3, 2)
assert tc.is_c_contiguous
assert sarr.shape == (2, 3)

A zero- or one-dimensional array has no axes to reverse, so the result is a plain deep copy. Two applications round-trip: transposing the transpose reproduces the original shape and content.

The T Property

The T property returns a deep-copied transposed array: the buffer is cloned and the metadata of the clone is reversed, so the result never shares memory with the receiver and the receiver is unchanged. This diverges from numpy, where .T is a zero-copy view:

sarr = solvcon.SimpleArrayFloat64((2, 3), value=1.0)
t = sarr.T
assert t.shape == (3, 2)
assert t.is_f_contiguous          # metadata flip of a C-ordered clone
sarr[0, 0] = 5.0
assert t[0, 0] == 1.0             # the copy does not see the write

Contiguity

The is_c_contiguous and is_f_contiguous Properties

The two read-only properties report whether the stride describes a row-major (C) or column-major (Fortran) layout over the shape, matching the numpy flags.c_contiguous and flags.f_contiguous semantics under the property spelling of the family. Dimensions of extent one place no constraint, so a degenerate shape (a single row, column, or element) reports both as True, as numpy does. A zero-extent dimension places no constraint either, so every empty array reports both flags whatever its other extents are, again as numpy does:

sarr = solvcon.SimpleArrayFloat64((2, 3, 4))
assert sarr.is_c_contiguous and not sarr.is_f_contiguous
sarr.transpose()
assert sarr.is_f_contiguous and not sarr.is_c_contiguous
assert solvcon.SimpleArrayFloat64((1, 4)).is_f_contiguous
zarr = solvcon.SimpleArrayFloat64((0, 4))
assert zarr.is_c_contiguous and zarr.is_f_contiguous

Zero-Copy between C++ and Python records the wrap-side consequence of the same flags.

The to_row_major and to_column_major Conversions

to_row_major() and to_column_major() return a fresh array with the same shape and values whose stride is C-contiguous or F-contiguous respectively. The result is always a new buffer: when the receiver already has the requested layout the buffer is cloned, and otherwise a fresh buffer is allocated and the elements are copied in the new order. The receiver is never modified:

ndarr = np.arange(6, dtype='float64').reshape((2, 3))
sarr = solvcon.SimpleArrayFloat64(array=ndarr[::-1, ::-1])
rm = sarr.to_row_major()
assert rm.is_c_contiguous
cm = sarr.to_column_major()
assert cm.is_f_contiguous
assert cm.stride == (1, 2)

The numpy counterparts numpy.ascontiguousarray and numpy.asfortranarray return the input unchanged when it already has the requested layout; always returning an independent copy diverges from numpy, keeping buffer ownership explicit per the design stance of the family overview.

Ghost Region Support

“Ghost” elements are the elements indexed with a negative integer, in contrast to the “body” elements indexed with a non-negative integer. The nghost property splits the first axis of an array into a ghost region and a body. The ghost elements hold the boundary (halo) data, which lives in the same storage as the interior of a physical computing domain. The feature is entirely solvcon-specific and numpy has no counterpart to it. The rules below extend the ghost-free semantics that the rest of this page defines.

The Partition Model

Every array starts without a ghost region: nghost is 0 and has_ghost is False. Assigning a positive nghost designates the first nghost positions along the first axis as the ghost region and the remaining nbody positions as the body. No memory moves and the shape does not change; only the index origin shifts. Index 0 becomes the first body element, and the ghost elements sit at the negative indices -nghost through -1:

sarr = solvcon.SimpleArrayFloat64(24)
sarr.ndarray[:] = np.arange(24)
sarr.nghost = 10

assert sarr.has_ghost
assert sarr.nbody == 14
assert sarr.shape == (24,)   # the shape is unchanged
assert sarr[-10] == 0.0      # first ghost element
assert sarr[0] == 10.0       # first body element
assert sarr[13] == 23.0      # last body element

Only the first axis carries the partition. On a multi-dimensional array the later axes keep the plain index arithmetic of the element-access rules above:

sarr = solvcon.SimpleArrayFloat64((4, 3, 2))
sarr.ndarray.flat[:] = range(24)
sarr.nghost = 1

assert sarr.nbody == 3
assert sarr[-1, 0, 0] == 0.0    # the ghost row
assert sarr[0, 0, 0] == 6.0     # the first body row
assert sarr[0, -1, 0] == 10.0   # later axes wrap plainly

Valid Index Range and Wrapping

The valid interval on the first axis is [-(shape[0] + nghost), nbody). Indices in [-nghost, nbody) address the partition directly, as above. An index below -nghost wraps python-style over the storage: the ghost shift makes it negative relative to the start of storage, and the wrap adds shape[0], so index i resolves to storage position i + nghost + shape[0]. In particular sarr[-nghost - 1] is the last storage element, mirroring how sarr[-1] on a ghost-free array is the last element:

sarr = solvcon.SimpleArrayInt8(8)
sarr.ndarray[:] = np.arange(8, dtype='int8')
sarr.nghost = 3

assert sarr[-3] == 0     # first ghost element
assert sarr[-4] == 7     # wraps to the last storage element
assert sarr[-11] == 0    # wraps to the first storage element
sarr[-4] = 70
assert sarr.ndarray[7] == 70

An index outside the interval raises IndexError, and the message carries the ghost arithmetic. The one-dimensional form:

sarr = solvcon.SimpleArrayFloat64(24)
sarr.nghost = 10
sarr[14]
# IndexError: SimpleArray: index 14 >= 14 (shape[0]: 24 - nghost: 10)
sarr[-35]
# IndexError: SimpleArray: index -35 < -nghost - shape[0]: -34

The multi-dimensional form names the offending dimension:

sarr = solvcon.SimpleArrayFloat64((4, 3, 2))
sarr.nghost = 1
sarr[3, 0, 0]
# IndexError: SimpleArray: dim 0 in [3, 0, 0] >= nbody: 3
# (shape[0]: 4 - nghost: 1)
sarr[-6, 0, 0]
# IndexError: SimpleArray: dim 0 in [-6, 0, 0] < -nghost - shape[0]: -5

Both errors apply to reads and writes alike.

Setting the nghost Property

The nghost setter accepts any value from 0 through shape[0]. Setting it back to 0 removes the region, and setting it to the full first-axis extent makes the whole axis ghost:

sarr = solvcon.SimpleArrayInt8(10)
sarr.nghost = 10            # the whole first axis may be ghost
assert sarr.nbody == 0
sarr.nghost = 0             # zero removes the region
assert not sarr.has_ghost

Three violations raise IndexError. The value cannot exceed the first-axis extent, cannot be negative, and cannot be positive on a zero-dimensional array (which the message calls empty); an array whose first axis has zero extent falls under the shape(0) bound instead:

sarr = solvcon.SimpleArrayInt8(10)
sarr.nghost = 11
# IndexError: SimpleArray: cannot set nghost 11 > shape(0) 10
sarr.nghost = -1
# IndexError: SimpleArray: cannot set negative nghost -1
solvcon.SimpleArrayInt8(()).nghost = 1
# IndexError: SimpleArray: cannot set nghost 1 > 0 to an empty array

The has_ghost and nbody Properties

has_ghost reports whether nghost is nonzero. nbody counts the body positions along the first axis, shape[0] - nghost; it is not an element count, so a ghost-free (4, 3, 2) array reports nbody == 4 and the same array with nghost = 1 reports 3. A zero-dimensional array reports 0. Neither shape, size, nor len() changes with the partition; they keep describing the full storage, as the layout properties above define.

Ghost-Shifted Slice Assignment

The slice keys of __setitem__ interpret their explicit bounds on the first axis in the logical, ghost-shifted coordinates of this page: the parser adds nghost to an explicit start or stop bound and then applies the ordinary Python slice rules over the full first-axis extent. An omitted bound is not shifted; it means the storage edge, so with a forward step an omitted start begins at the first ghost element and an omitted stop runs to the end of storage. The stop bound 0 therefore selects exactly the ghost region, and the start bound 0 selects the body:

sarr = solvcon.SimpleArrayFloat64(shape=5, value=0)
sarr.nghost = 2

sarr[-2:0] = np.array([10.0, 11.0])        # the ghost region
sarr[0:] = np.array([12.0, 13.0, 14.0])    # the body
assert sarr.ndarray.tolist() == [10, 11, 12, 13, 14]

sarr[:0] = np.array([20.0, 21.0])          # also the ghost region
assert sarr.ndarray.tolist() == [20, 21, 12, 13, 14]

Because both bounds default to the storage edges, a bare slice, a stepped slice, or an ellipsis covers the whole storage including the ghost region, and a negative step reverses over it:

sarr = solvcon.SimpleArrayFloat64(shape=5, value=0)
sarr.nghost = 2
sarr[::2] = np.array([10.0, 11.0, 12.0])
assert sarr.ndarray.tolist() == [10, 0, 11, 0, 12]

sarr[...] = np.arange(5, dtype='float64')
assert sarr[-2] == 0.0 and sarr[2] == 4.0

In a tuple key only the first-axis slice is shifted; slices on the later axes keep the ghost-free semantics:

sarr = solvcon.SimpleArrayFloat64(shape=(5, 3), value=0)
sarr.nghost = 2
sarr[-2:0, ...] = np.arange(6, dtype='float64').reshape((2, 3))
assert (sarr.ndarray[0:2] == np.arange(6).reshape((2, 3))).all()

The accepted right-hand sides, the exact-shape check, and the dtype conversion rules are those of the assignment section above, unchanged by the partition.

Failure Preserves the Partition

A rejected assignment does not disturb the ghost setting. When the right-hand side fails the dtype conversion, the array keeps its nghost as before the statement:

sarr = solvcon.SimpleArrayFloat64(shape=(2, 2), value=0)
sarr.nghost = 1
sarr[...] = np.ones((2, 2), dtype='complex128')
# RuntimeError: Cannot convert between complex and non-complex types
assert sarr.nghost == 1

Ghost Regions on Strided Arrays

The partition composes with the strided layouts of Zero-Copy between C++ and Python. On an array wrapping a strided view, the ghost indices address the viewed elements, and a write through a ghost or wrapped index lands in the viewed region of the original memory:

base = np.arange(12, dtype='float64')
sarr = solvcon.SimpleArrayFloat64(array=base[::2])
sarr.nghost = 2

assert sarr[-2] == 0.0     # first viewed element
assert sarr[-3] == 10.0    # wraps to the last viewed element
sarr[-3] = 200.0
assert base[10] == 200.0

Ghost Regions and the Layout Operations

reshape refuses a ghosted array outright. The split of the first axis has no well-defined image under a new shape, so both the typed classes and the dtype-erased SimpleArray raise RuntimeError naming the ghost count:

sarr = solvcon.SimpleArrayFloat64(6)
sarr.nghost = 2
sarr.reshape((3, 2))
# RuntimeError: SimpleArray: cannot reshape an array with 2 ghost cells

The layout operations of this page split into two groups by how they build their result. Those that duplicate the storage as it lies, clone(), the T property, and to_row_major() or to_column_major() on a receiver that already has the requested layout, copy the whole storage and carry nghost to the result. The in-place transpose() keeps nghost as well, since it only permutes the metadata.

Those that physically rearrange the elements, transpose_copy(), transpose(copy=True), and a to_row_major() or to_column_major() that must reorder, reset nghost to 0 in the result, and their read is defective under a ghost region: the rearranging loop walks the shape from the body pointer, which sits nghost positions above the start of the storage, so it skips the ghost region and runs the same number of positions past the end. The trailing rows of the result hold uninitialized values.

sarr = solvcon.SimpleArrayFloat64((4, 3))
sarr.nghost = 2
assert sarr.clone().nghost == 2
assert sarr.to_row_major().nghost == 2   # already row-major: a copy
assert sarr.T.nghost == 2
assert sarr.transpose_copy().nghost == 0

Carrying nghost through a transpose reattaches the partition to a different axis, because the count is kept while the axes are permuted: the region that split the old first axis now splits the new one. The count is not rechecked against the new first-axis extent, so a permutation onto a shorter axis leaves nghost above it and drives nbody negative:

sarr = solvcon.SimpleArrayFloat64((4, 3))
sarr.nghost = 4
sarr.transpose()
assert sarr.shape == (3, 4) and sarr.nghost == 4
assert sarr.nbody == -1     # the partition no longer fits the axis

Caution

The nghost setter refuses a count above shape(0), so a transpose is the only way into that state. The valid interval of this page, [-(shape[0] + nghost), nbody), then lies entirely below zero: on the (3, 4) array above it is [-7, -1), so every non-negative index raises and so does -1, which the ghost range nominally covers. Whether a transpose should permute the partition with the axes, drop it as the copying transposes do, or reject a ghosted receiver is an open decision; until it lands, set nghost after transposing rather than before.