Elementwise Arithmetic, Comparison, and Selection¶
Every typed array in the SimpleArray family computes elementwise: arithmetic
through the add, sub, mul, and div method families, comparison through
eq, ne, lt, le, gt, and ge together with the rich-comparison
operators, and selection through argwhere and where. This page defines
those operations, and it is where the family stands farthest from numpy:
arithmetic is spelled as method calls under strict operand rules, while
comparison and selection are converging to numpy semantics per the policy of
the family overview. The table at the center of the page gives
the spellings of each family side by side.
Arithmetic Methods¶
add, sub, mul, and div compute a new array elementwise from the
receiver and one operand, leaving the receiver untouched. The operand is
either an array of exactly the same class or a scalar:
narr1 = np.array([1, 2, 3], dtype='float64')
narr2 = np.array([10, 20, 30], dtype='float64')
sarr1 = solvcon.SimpleArrayFloat64(array=narr1)
sarr2 = solvcon.SimpleArrayFloat64(array=narr2)
sres = sarr1.add(sarr2)
assert sres.ndarray.tolist() == [11.0, 22.0, 33.0]
sres = sarr1.mul(2.0)
assert sres.ndarray.tolist() == [2.0, 4.0, 6.0]
assert sarr1.ndarray.tolist() == [1.0, 2.0, 3.0] # receiver unchanged
The result has the shape and element type of the receiver, and the operations run over the whole storage, ghost region included, over the partition that Indexing, Shape, and Layout defines.
Operand Rules¶
An array operand must be the same typed class as the receiver: there is no
dtype promotion, so adding a SimpleArrayFloat32 to a SimpleArrayFloat64
raises TypeError from the binding’s argument matching, where numpy would
promote both sides to float64. The shapes must also be identical: there is
no broadcasting, and a mismatch raises ValueError naming the operation and
both shapes:
lhs = solvcon.SimpleArrayFloat64((2, 3), value=1)
rhs = solvcon.SimpleArrayFloat64((3, 2), value=2)
lhs.add(rhs)
# ValueError: SimpleArray::add(): shape mismatch: this=(2, 3)
# other=(3, 2)
Both restrictions diverge from numpy, whose arithmetic promotes dtypes and broadcasts shapes. Whether the family should grow promotion or broadcasting is an open decision; this page records only the strict rules.
A scalar operand converts to the element type through pybind11 conversion,
following the same rules as scalar assignment in
Indexing, Shape, and Layout: a Python int converts to a
floating-point element, but a float operand on an integer array raises
TypeError instead of truncating.
Integer Division Keeps the Dtype¶
div always produces the receiver’s element type. On the integer classes the
quotient is the C++ integer quotient, truncated toward zero, where numpy
true_divide promotes to float64:
sarr1 = solvcon.SimpleArrayInt32(array=np.array([7, 8], dtype='int32'))
sarr2 = solvcon.SimpleArrayInt32(array=np.array([2, 2], dtype='int32'))
assert sarr1.div(sarr2).ndarray.tolist() == [3, 4]
assert np.divide(sarr1.ndarray, sarr2.ndarray).tolist() == [3.5, 4.0]
Boolean Arithmetic¶
On SimpleArrayBool the arithmetic reduces to logic, matching the numpy
behavior for add and mul: add is elementwise logical or and mul is
elementwise logical and. sub and div are unsupported and raise
RuntimeError; numpy also rejects boolean subtraction, with TypeError.
Because the methods delegate to their in-place forms, the message names the
in-place spelling even for the copying call:
sarr1 = solvcon.SimpleArrayBool(array=np.array([True, False]))
sarr2 = solvcon.SimpleArrayBool(array=np.array([True, True]))
assert sarr1.add(sarr2).ndarray.tolist() == [True, True]
assert sarr1.mul(sarr2).ndarray.tolist() == [True, False]
sarr1.sub(sarr2)
# RuntimeError: SimpleArray<bool>::isub(): boolean value doesn't
# support this operation
The complex classes bind the four methods with the C++ complex arithmetic;
their scalar operands take the complex scalar types of
Zero-Copy between C++ and Python. The Python built-in
complex and the numpy complex scalars are rejected with TypeError: no
implicit conversion is registered for the arithmetic operands, unlike element
writes, which accept both spellings.
In-Place Methods¶
iadd, isub, imul, and idiv apply the same elementwise update to the
receiver itself. They take the same operands under the same rules as the
copying forms, including the same-class and same-shape requirements, and a
failed validation leaves the receiver untouched:
sarr = solvcon.SimpleArrayFloat64(array=np.array([1.0, 2.0]))
sarr.iadd(10.0)
assert sarr.ndarray.tolist() == [11.0, 12.0]
The methods return None: the mutation is the whole effect. This diverges
from the numpy augmented operators, whose a += b statement rebinds a to
the returned array. Whether the methods should instead return the receiver to
support chaining is an open decision recorded in the binding source; do not
rely on the return value.
No Arithmetic Operators¶
The arithmetic operators are not bound: +, -, *, / and their augmented
forms +=, -=, *=, /= all raise TypeError:
sarr1 = solvcon.SimpleArrayFloat64(3, value=1.0)
sarr2 = solvcon.SimpleArrayFloat64(3, value=2.0)
sarr1 + sarr2
# TypeError: unsupported operand type(s) for +:
# '_solvcon.SimpleArrayFloat64' and '_solvcon.SimpleArrayFloat64'
This diverges from numpy, where the operators are the primary spelling. Whether the family should bind them is an open decision: unlike the comparison operators below, no converged target is on record, so the method calls remain the only spelling.
The abs Method¶
abs() returns a new array of the same class holding the elementwise absolute
value, leaving the receiver untouched. On the signed integer and
floating-point classes it matches numpy.absolute; on SimpleArrayBool and
the unsigned classes the values are already non-negative and the result is a
plain copy, as in numpy:
sarr = solvcon.SimpleArrayInt64(shape=(3, 2), value=-2)
assert sarr.abs().sum() == 12
On the complex classes abs() currently returns an unchanged copy, where
numpy.absolute computes the elementwise magnitude in the matching
floating-point dtype. Whether complex abs() should compute the magnitude,
which would change the result’s element type, is an open decision. The
__abs__ protocol is not bound, so the built-in abs(sarr) raises
TypeError on every class.
The SIMD Variants¶
add_simd, sub_simd, mul_simd, div_simd and the in-place iadd_simd,
isub_simd, imul_simd, idiv_simd are performance-explicit aliases of the
plain forms: they route through the runtime-dispatched SIMD kernels, and the
desired numerics are identical to the plain spellings. On the numeric classes,
element types without a vector kernel fall back to the generic implementation
with the same results.
On SimpleArrayBool the two groups currently differ. The in-place variants
delegate to the plain forms and keep the boolean semantics of the arithmetic
methods above; the copying variants bypass that boolean guard and run the
numeric kernels over the boolean elements, so add_simd and mul_simd still
land on the logical or and and, but sub_simd and div_simd return
integer-style values where the plain sub and div raise RuntimeError. The
boolean guard of the plain forms is the evident intent, so extending it to the
copying variants is target behavior.
The operand rules differ from the plain forms in one way: the SIMD variants
take only an array operand, so a scalar raises TypeError. The same-class and
same-shape requirements are those of the plain forms, with the variant name in
the mismatch message (SimpleArray::add_simd(): shape mismatch: ...), and the
in-place variants return None like their plain counterparts:
sarr1 = solvcon.SimpleArrayInt32(array=np.arange(5, dtype='int32'))
sarr2 = solvcon.SimpleArrayInt32(array=np.arange(5, dtype='int32'))
assert sarr1.add_simd(sarr2).ndarray.tolist() == [0, 2, 4, 6, 8]
Comparison¶
The Comparison Methods¶
eq and ne are bound on every typed class; lt, le, gt, and ge are
bound on every non-complex class, including SimpleArrayBool. Each takes an
array operand of the same class and shape or a scalar, and returns a
SimpleArrayBool of the receiver’s shape holding the elementwise result:
narr = np.array([1, 2, 3, 3], dtype='int32')
sarr = solvcon.SimpleArrayInt32(array=narr)
sres = sarr.eq(3)
assert type(sres) is solvcon.SimpleArrayBool
assert sres.ndarray.tolist() == [False, False, True, True]
A shape mismatch on the array operand raises ValueError with the same
message form as the arithmetic methods
(SimpleArray::ne(): shape mismatch: this=(8) other=(3)).
The Bound Operators¶
The rich-comparison operators map to the methods, so ==, !=, <, <=,
>, and >= compare elementwise and return a SimpleArrayBool, like numpy
and unlike the Python default of identity comparison:
narr1 = np.array([1, 2, 3, 3], dtype='int32')
narr2 = np.array([1, 0, 3, 9], dtype='int32')
sarr1 = solvcon.SimpleArrayInt32(array=narr1)
sarr2 = solvcon.SimpleArrayInt32(array=narr2)
sres = sarr1 == sarr2
assert sres.ndarray.tolist() == [True, False, True, False]
sres = sarr1 < 3
assert sres.ndarray.tolist() == [True, True, False, False]
The elementwise semantics match numpy for the supported operands. The operand scope is what still separates the two: the array operand must be the same class and shape, with no promotion and no broadcasting.
Complex Arrays Leave the Ordering Unbound¶
Ordering is undefined for complex numbers, so the complex classes bind neither
the ordering methods nor the ordering operators; numpy raises TypeError for
<, <=, >, >= on a complex ndarray, and the unbound operators make the
classes behave the same way. Equality stays elementwise:
narr = np.array([1 + 2j, 3 + 4j], dtype='complex128')
sarr = solvcon.SimpleArrayComplex128(array=narr)
assert not hasattr(sarr, 'lt')
sarr < sarr
# TypeError: '<' not supported between instances of ...
assert (sarr == sarr).ndarray.tolist() == [True, True]
NaN Comparison¶
NaN follows the IEEE 754 rules, matching numpy: the ordering comparisons and
eq report False against NaN in every direction, including NaN against NaN,
and ne reports True:
narr = np.array([np.nan, 1.0], dtype='float64')
sarr = solvcon.SimpleArrayFloat64(array=narr)
assert sarr.eq(sarr).ndarray.tolist() == [False, True]
assert sarr.ne(sarr).ndarray.tolist() == [True, False]
Unsupported Operands¶
When the right-hand side of == or != is neither a same-class array nor a
convertible scalar, the binding reports the comparison as not implemented and
Python falls back to its default identity semantics, returning a plain bool
instead of an array:
sarr = solvcon.SimpleArrayInt32(array=np.arange(3, dtype='int32'))
assert (sarr == None) is False # the identity fallback
assert (sarr != "not an array") is True
A SimpleArray of a different class is such an operand, so comparing
mixed-dtype arrays with == yields the identity False where numpy would
promote and compare elementwise; the convergence tag covers this gap. The
ordering operators have no identity fallback in Python, so the same
unsupported operands raise TypeError there.
The Spelling Table¶
The table gives one row per operation family: the member-call form, the operator form, and the numpy counterpart. “none” marks a spelling that does not exist; the SIMD row has no numpy counterpart at all.
Family |
Member call |
Operator |
Numpy form |
|---|---|---|---|
Arithmetic |
|
none |
|
In-place arithmetic |
|
none |
|
SIMD arithmetic |
|
none |
none |
Absolute value |
|
none |
|
Equality |
|
|
|
Ordering |
|
|
|
Index selection |
|
none |
|
Value selection |
|
none |
|
The arithmetic rows differ from numpy in the spelling and the operand rules, and no target is committed for them, per the open decisions above. The absolute-value row agrees except for the complex open decision. For comparison and selection the elementwise semantics already agree on same-dtype operands; what still differs is the operand scope, promotion, and broadcasting.
Selection¶
Comparison produces a SimpleArrayBool, and the two selection operations
consume it: argwhere maps the true elements to their indices, and where
merges two arrays under the condition. Both are converging to the numpy usage,
with the method-on-the-condition spelling standing in for the numpy free
functions.
The argwhere Method¶
argwhere() returns a SimpleArrayUint64 of shape (count, ndim) holding
one row of indices per selected element, in row-major order, equal to the
numpy argwhere result:
narr = np.array([[1, 3, 5, 7, 9],
[2, 4, 6, 8, 10],
[1, 10, 1, 10, 1]], dtype='float64')
sarr = solvcon.SimpleArrayFloat64(array=narr)
ret = sarr.eq(10).argwhere()
assert (ret.ndarray == np.argwhere(narr == 10)).all()
The method is bound on every typed class and selects the nonzero elements, so
the boolean array from a comparison is the intended, tested condition form:
sarr.eq(10).argwhere() is the counterpart of np.argwhere(narr == 10).
Reductions, Statistics, Sorting, and Searching covers the
method beside argmin and argmax; this page fixes only its role as the
index-selection half of comparison.
The where Method¶
where(x, y) exists only on SimpleArrayBool; on the other classes the
attribute does not exist and Python raises AttributeError. The receiver is
the condition, and the result takes x where the condition is true and y
elsewhere, equal to np.where(cond, x, y):
narr = np.arange(12, dtype='float64').reshape(3, 4)
cond = solvcon.SimpleArrayBool(array=(narr < 6))
x = solvcon.SimpleArrayFloat64(array=(narr + 1))
y = solvcon.SimpleArrayFloat64(array=(narr * 10))
ret = cond.where(x, y)
assert (ret.ndarray == np.where(narr < 6, narr + 1, narr * 10)).all()
where follows the logical indices of its operands, so it is defined and
verified on non-contiguous and mixed-layout operands, and it composes with the
ghost partition: the condition and both operands must carry the same nghost,
and the result preserves it.
The operand rules are strict, diverging from the numpy scalar and broadcast
operands until the convergence completes. x and y must be SimpleArray
objects of the same dtype and the condition’s shape, drawn from the integer
and floating-point classes; boolean and complex operands are not supported.
Each violation raises ValueError:
cond = solvcon.SimpleArrayBool((1, 3), value=True)
cond.where(1.0, 2.0)
# ValueError: SimpleArray::where(): x and y must be SimpleArray
x64 = solvcon.SimpleArrayFloat64((1, 3), value=1)
x32 = solvcon.SimpleArrayFloat32((1, 3), value=2)
cond.where(x64, x32)
# ValueError: SimpleArray::where(): x and y must have the same dtype
xs = solvcon.SimpleArrayInt32((1, 2), value=2)
cond.where(xs, xs)
# ValueError: SimpleArray::where(): shape mismatch: condition=(1, 3)
# x=(1, 2) y=(1, 2)
cond.where(cond, cond)
# ValueError: SimpleArray::where(): unsupported dtype
Except for where, the elementwise kernels of this page address their
elements through the linear storage; the verified scope is contiguous
operands, which every array allocated by the constructors of
Construction and Data Types satisfies.