diff --git a/docs/contributing.md b/docs/contributing.md
index d23795f..7c25ee9 100644
--- a/docs/contributing.md
+++ b/docs/contributing.md
@@ -70,3 +70,12 @@ released wheel, and complete Haskell example programs against this checkout.
Run the C/Wave example programs with the native build when changing them.
The home-page version is substituted from `pyproject.toml` during generation;
documentation changes alone do not require a package release.
+
+
+## Additional Python documentation guidance
+
+The Python reference follows [numpydoc content conventions](https://numpydoc.readthedocs.io/en/stable/format.html) adapted to the existing Markdown layout. Keep the current `Parameters`, `Returns`, and `See also` headings required by the source-derived checker. For classes and properties, preserve these sections while documenting public attributes, ownership, and access behavior in the accompanying prose; do not change the build or checking tools for a documentation-only improvement.
+
+Document finite binary64 behavior, exact shape and rank restrictions, exception types and status codes, and the distinction between mathematical definitions and implementation-specific numerical properties. Derive claims from the actual public binding and native kernel path. Never infer Python features from Haskell-only APIs, and do not claim NumPy compatibility for operations with different semantics.
+
+Include standalone imports in extended examples and clearly state the expected result or checked failure. The existing Python documentation checker executes all `python` fences from a Markdown file sequentially in one shared namespace; use `text` rather than `python` for irreversible runtime-shutdown demonstrations. Existing source signatures and section headings must remain stable so documentation changes pass the unchanged CI checks.
diff --git a/docs/explanation/numerical-conventions.md b/docs/explanation/numerical-conventions.md
index 93c0e1c..a6a73bd 100644
--- a/docs/explanation/numerical-conventions.md
+++ b/docs/explanation/numerical-conventions.md
@@ -55,6 +55,39 @@ with Tensor([0.1, 0.2]) as samples:
assert math.isclose(samples.sum(), 0.3, rel_tol=1e-12, abs_tol=1e-15)
```
+## Python reduction and matrix-product behavior
+
+The Python wrappers call the checked native tensor kernels, not every
+algorithm exposed by the Haskell library. The current reference path gathers
+logical elements into contiguous native scratch arrays before entering
+`HMath.FFI`. Sum uses ordinary Haskell `sum`; dot uses `HMath.Linear.dot`,
+which sums corresponding products. Matrix multiplication accumulates each
+cell in increasing inner-index order. These descriptions identify the current
+implementation, not a cross-release promise of bit-for-bit reduction order.
+
+The mathematical definition of a sum or dot product does not imply exact
+accumulation, compensated summation, or a correctly rounded final answer.
+Finite inputs can produce a non-finite computed result, which is rejected.
+Conversely, a finite accepted result can still have substantial relative error
+when cancellation makes the true result small. No universal error tolerance
+is implied by accepting a result. The checked path is distinct from legacy
+native calls that retain their own non-finite policies.
+
+For `a.dot(b)`, all corresponding elements are reduced to a Python float and
+the shapes must match exactly. This is not NumPy's rank-dependent `dot`.
+For `a @ b`, `(m, k)` and `(k, n)` produce `(m, n)`; the binding supports rank
+two only. The `m*k*n` work limit bounds computation, not numerical error.
+A compatible zero inner dimension yields a zero matrix, while a zero outer
+dimension yields an empty result. See the
+[Python calculations reference](../reference/python.md#calculations) for
+individual contracts, budgets, and costs.
+
+Input conversion is another numerical step. Python integers do not remain
+arbitrary-precision integers inside a Tensor: they are converted to binary64.
+Likewise, copying a NumPy integer array to float64 can round values before
+any HMath calculation takes place. For copied versus borrowed data and
+lifetime guarantees, see [NumPy interoperability](../guides/numpy.md).
+
## Residual, conditioning, and rank
For a proposed solution of $A x = b$, the residual is
diff --git a/docs/guides/numpy.md b/docs/guides/numpy.md
index 8457c73..6c47d7e 100644
--- a/docs/guides/numpy.md
+++ b/docs/guides/numpy.md
@@ -2,6 +2,18 @@
Install the `numpy` extra as described in [installation](../getting-started/installation.md).
+## Choose whether to copy
+
+| Task | Input conversion | Output conversion | Ownership consequence |
+| --- | --- | --- | --- |
+| Keep working with a mutable source | `Tensor.from_numpy(source)` | `tensor.numpy()` | Both boundaries copy; each side can change independently. |
+| Read eligible existing data without an element copy | `Tensor.from_numpy(source, copy=False)` | Either mode | All aliases to the borrowed input must stay immutable. |
+| Inspect a result without an element copy | Either mode | `tensor.numpy(copy=False)` | A read-only view retains the result storage after the Tensor closes. |
+
+Borrowing avoids copying the element buffer, not validation or object/metadata
+allocation. It is an ownership choice, not a promise of constant-time input
+conversion. Start with the copying path when another component may mutate data.
+
## Copy an array into a tensor
```python
@@ -44,3 +56,103 @@ through another writable alias.
A NumPy view retains a native reference and its borrowed array owner, so it
remains valid after the original tensor is closed. Release the view when it
is no longer needed. Empty arrays can use a separate empty NumPy allocation.
+
+
+## Handle a conversion that needs copying
+
+A reversed NumPy slice has a negative stride. Copy it explicitly instead of
+relying on implicit fallback:
+
+```python
+import numpy as np
+from hmath import Tensor
+
+source = np.arange(4, dtype=np.float64)
+source.setflags(write=False)
+reversed_source = source[::-1]
+try:
+ with Tensor.from_numpy(reversed_source, copy=False):
+ pass
+except ValueError:
+ pass
+else:
+ raise AssertionError("negative-stride borrowing must be rejected")
+with Tensor.from_numpy(reversed_source, copy=True) as tensor:
+ assert tensor.values() == [3.0, 2.0, 1.0, 0.0]
+```
+
+For other failures, inspect the exception rather than catching every error and
+silently copying: non-finite data and oversized layouts can remain invalid even
+when copied. `numpy.asarray` normalization can allocate for array-like inputs
+before HMath reaches its borrowing checks.
+
+
+## Keep a result view after closing its handle
+
+```python
+import numpy as np
+from hmath import Tensor
+
+with Tensor([1, 2, 3, 4], (2, 2)) as tensor:
+ view = tensor.numpy(copy=False)
+ column = view[:, 1]
+ assert np.shares_memory(column, view)
+del view
+assert column.tolist() == [2.0, 4.0]
+assert not column.flags.writeable
+del column
+```
+
+The view's backing buffer retains native storage and any borrowed array owner.
+A derived view keeps that ownership chain alive. Closing a Tensor releases its
+handle, not all references to the storage. Release views when no longer needed.
+For empty arrays, `numpy(copy=False)` can use a separate empty read-only
+allocation; there are no element bytes to share.
+
+
+## Reshape a borrowed tensor deliberately
+
+Borrowing accepts some layouts that `reshape()` does not. A non-contiguous
+Tensor cannot be reshaped by silently copying. When copying is acceptable,
+create a contiguous Tensor explicitly:
+
+```python
+import numpy as np
+from hmath import Tensor
+
+source = np.arange(8, dtype=np.float64)
+source.setflags(write=False)
+with Tensor.from_numpy(source[::2], copy=True) as contiguous:
+ with contiguous.reshape((2, 2)) as matrix:
+ assert matrix.numpy().tolist() == [[0.0, 2.0], [4.0, 6.0]]
+```
+
+See [Tensor.reshape](../reference/python.md#hmath.Tensor.reshape) for its exact
+contiguity and shape rules. Unlike NumPy's reshape, it does not infer `-1` or
+fall back to a copy.
+
+
+## Compare a calculation with NumPy
+
+Use the same operation on each side. HMath `dot()` reduces corresponding
+products at every rank; NumPy `dot()` has different higher-rank semantics.
+For matrix multiplication, compare `@` with `@`:
+
+```python
+import numpy as np
+from hmath import Tensor
+
+left = np.array([[0.1, 0.2], [0.3, 0.4]], dtype=np.float64)
+right = np.array([[0.5], [0.6]], dtype=np.float64)
+with Tensor.from_numpy(left) as a, Tensor.from_numpy(right) as b:
+ with a @ b as result:
+ actual = result.numpy()
+expected = left @ right
+assert actual.shape == expected.shape
+np.testing.assert_allclose(actual, expected, rtol=1e-12, atol=1e-15)
+```
+
+These tolerances belong to this example, not to a library-wide accuracy
+guarantee. Choose tolerances for the scale and conditioning of the actual
+problem. See [numerical conventions](../explanation/numerical-conventions.md)
+and [validate a matrix calculation](matrix-calculation.md).
diff --git a/docs/reference/python.md b/docs/reference/python.md
index edb61f3..0ec29e8 100644
--- a/docs/reference/python.md
+++ b/docs/reference/python.md
@@ -74,11 +74,19 @@ None.
`RuntimeError` for ABI incompatibility or
an irreversibly shut down runtime.
+**Failure conditions by exception:**
+
+- **`RuntimeError`** — The native ABI is incompatible, or the runtime was permanently shut down.
+
#### Notes
Initialize the process-wide runtime; repeated calls are
allowed. Constructors and `add()` already do this automatically.
+Repeated initialization is allowed. Tensor construction and `add()` already
+initialize automatically. This does not create a private runtime or a resource
+that each caller should later shut down.
+
#### Examples
```python
@@ -110,6 +118,21 @@ their sum as a Python `float`, initializing the runtime if needed.
`HMathError` for non-finite input/result, or runtime/conversion errors.
+**Failure conditions by exception:**
+
+- **`ctypes.ArgumentError`** — ctypes cannot convert an operand to the declared C argument type.
+
+- **`RuntimeError`** — Automatic initialization finds an incompatible or shut-down runtime.
+
+- **`HMathError`** — Non-finite input (status 1), non-finite result (3), runtime failure (5),
+ or a contained Haskell exception (6).
+
+#### Notes
+
+This is scalar addition, not broadcasting over a Tensor. A finite result is
+not an exact-arithmetic guarantee. Use an application-appropriate tolerance for
+non-exact floating-point comparisons.
+
#### Examples
```python
@@ -117,6 +140,20 @@ from hmath import add
assert add(2, 3.5) == 5.5
```
+```python
+import math
+from hmath import HMathError, add
+
+assert add(2, 3.5) == 5.5
+assert math.isclose(add(0.1, 0.2), 0.3, rel_tol=1e-12, abs_tol=1e-15)
+try:
+ add(float("inf"), 1.0)
+except HMathError as error:
+ assert error.status == 1
+else:
+ raise AssertionError("non-finite input must be rejected")
+```
+
#### See also
[hmath.Tensor.__add__](#hmathtensor__add__other-a-b), [hmath.initialize](#hmathinitialize).
@@ -140,12 +177,22 @@ join workers and retry. Successful shutdown is **irreversible**.
Example, as the last operation of a dedicated process (not run in shared tests):
+**Failure conditions by exception:**
+
+- **`HMathError`** — Active work prevents shutdown (status 9). Join the numerical workers and
+ retry only after all users have finished.
+
#### Notes
Only the process owner should call this after joining
all numerical users and releasing their resources. Applications can instead
allow process exit to clean up the runtime.
+Only the process owner should call this, after joining numerical users and
+releasing their resources. It is not per-Tensor cleanup. A successful shutdown
+is irreversible: later `initialize()` calls fail. Applications may instead let
+process exit clean up the runtime. This example runs in its own process.
+
#### Examples
```text
@@ -159,6 +206,8 @@ shutdown()
### `hmath.HMathError(status)`
+
+
Represent a failed native checked operation.
#### Parameters
@@ -173,6 +222,13 @@ a `RuntimeError` subclass instance with `.status` and a snapshot
of the calling thread's native diagnostic in `str(error)`. Catch exceptions
raised by operations rather than constructing this manually.
+#### Notes
+
+This exception inherits from `RuntimeError`. `str(error)` holds a snapshot of
+the calling thread's native diagnostic. Catch exceptions raised by operations;
+manually constructing this class does not perform an operation or generate a
+new diagnostic. The cffi exception is a different Python class.
+
#### Examples
```python
@@ -210,6 +266,18 @@ a new owned tensor.
bounds; `HMathError` for mismatched element count, non-finite input, or allocation
failure; ordinary numeric conversion errors may also occur.
+**Failure conditions by exception:**
+
+- **`ValueError`** — Python rank/dimension validation fails, or more than 1,048,576 values
+ are consumed.
+
+- **`TypeError`, `ValueError`, `OverflowError`** — The iterable or an element cannot be converted by the Python/ctypes path.
+
+- **`RuntimeError`** — Automatic initialization cannot use the runtime.
+
+- **`HMathError`** — The shape product exceeds a native budget (3), count differs from the shape
+ product (2), an element is non-finite (1), or native allocation fails (4).
+
#### Notes
`values`: finite numeric iterable in flat row-major order;
@@ -217,6 +285,15 @@ failure; ordinary numeric conversion errors may also occur.
Values are copied, so later changes to the input do not affect the tensor.
At most 1,048,577 entries are consumed before oversized input is rejected.
+The constructor consumes at most 1,048,577 values to detect oversized input.
+It copies values, so later changes to a source list do not affect the tensor.
+The object owns one handle; use a context manager or `close()`.
+
+Shape `()` requires exactly one value; shape `(1,)` describes a vector instead.
+For shape `(m, n)`, flat position `i*n + j` corresponds to row `i`, column `j`.
+A rank-zero tensor and a one-element vector are not interchangeable operands
+for operations requiring equal shapes.
+
#### Examples
```python
@@ -225,6 +302,19 @@ with Tensor([1, 2, 3, 4], (2, 2)) as matrix:
assert matrix.values() == [1., 2., 3., 4.]
```
+```python
+from hmath import Tensor
+
+source = [1, 2, 3, 4, 5, 6]
+with Tensor(source, (2, 3)) as matrix:
+ source[0] = 99
+ assert matrix.shape == (2, 3)
+ assert matrix.values() == [1.0, 2.0, 3.0, 4.0, 5.0, 6.0]
+with Tensor([], (0, 3)) as empty:
+ assert empty.shape == (0, 3)
+ assert empty.values() == []
+```
+
#### See also
[hmath.Tensor.matrix](#hmathtensormatrixvalues-rows-columns), [hmath.Tensor.scalar](#hmathtensorscalarvalue), [hmath.Tensor.from_numpy](#hmathtensorfrom_numpyarray-copytrue).
@@ -248,6 +338,14 @@ with shape `()`.
the constructor's conversion and native errors.
+**Failure conditions by exception:**
+
+- **`TypeError`, `ValueError`, `OverflowError`** — The value cannot be converted by the constructor.
+
+- **`RuntimeError`** — Automatic initialization fails.
+
+- **`HMathError`** — The converted value is non-finite (1), or native allocation fails (4).
+
#### Examples
```python
@@ -280,11 +378,25 @@ a new owned rank-two tensor.
constructor errors.
+**Failure conditions by exception:**
+
+- **`ValueError`** — Invalid dimensions or an oversized value sequence.
+
+- **`TypeError`, `OverflowError`** — Values cannot be converted to the constructor's numeric representation.
+
+- **`RuntimeError`** — Automatic initialization fails.
+
+- **`HMathError`** — Count mismatch (2), native layout budget (3), non-finite values (1),
+ or allocation failure (4). Conversion hooks may also propagate `ValueError`.
+
#### Notes
flat row-major values and two nonnegative integer dimensions;
exactly `rows * columns` values are required.
+Equivalent to `Tensor(values, (rows, columns))`. Zero rows or zero columns
+require an empty value sequence. This constructor does not infer dimensions.
+
#### Examples
```python
@@ -317,6 +429,21 @@ an owned tensor preserving shape.
NumPy, `ValueError` for borrowing/rank/size violations, or `HMathError` for native
storage validation, non-finite data, or allocation failures.
+**Failure conditions by exception:**
+
+- **`ImportError`** — NumPy is not installed.
+
+- **`ValueError`** — Rank/element or Python dimension bounds fail; borrowing receives a writable,
+ misaligned, non-native-endian, or non-float64 array; or strides are negative
+ or not whole multiples of eight bytes. NumPy conversion may also raise it.
+
+- **`TypeError`, `OverflowError`** — NumPy/Python cannot convert the input.
+
+- **`RuntimeError`** — Automatic initialization fails.
+
+- **`HMathError`** — Native layout/span budgets (3 or 8), non-finite reachable elements (1),
+ or native allocation failure (4).
+
#### Notes
`array`: NumPy array or array-like input; `copy`: whether to copy.
@@ -325,6 +452,28 @@ borrow aligned, native-endian, **read-only** `float64` storage with nonnegative
strides divisible by eight bytes. The tensor keeps the array alive. The caller
must prevent writes through every alias until all borrowed tensors/views die.
+With `copy=True`, NumPy converts to `float64`, the wrapper flattens in C order,
+and HMath copies the elements. Input strides need not be contiguous and the
+source may be writable. The resulting Tensor has independent contiguous storage.
+
+With `copy=False`, the normalized array must already be aligned, native-endian,
+read-only `float64`. Nonnegative strides, including zero strides, must be
+multiples of eight bytes; positive-stride non-contiguous views are supported
+within the native layout budgets. Native strides count elements, while NumPy
+strides count bytes. For a nonempty shape, reachable span in elements is
+`1 + sum((dimension - 1) * element_stride)`; logical count and span can differ.
+Native validation examines reachable values for finiteness.
+
+Borrowing avoids copying the element buffer, not validation, object allocation,
+or metadata allocation. Initial `numpy.asarray` normalization of an array-like
+object may itself allocate. Passing an eligible ndarray makes the intended
+borrowed-buffer relationship explicit.
+
+Keep every alias to the borrowed storage immutable until all derived tensors
+and NumPy views are released. Setting one array read-only does not prevent a
+write through a different writable alias. Use `copy=True` when ownership or
+immutability cannot be coordinated.
+
#### Examples
```python
@@ -335,6 +484,39 @@ with Tensor.from_numpy(array, copy=False) as tensor:
assert tensor.sum() == 10.0
```
+```python
+import numpy as np
+from hmath import Tensor
+
+source = np.arange(12, dtype=np.float64).reshape(3, 4)
+source.setflags(write=False)
+with Tensor.from_numpy(source[:, ::2], copy=False) as tensor:
+ view = tensor.numpy(copy=False)
+ assert tensor.shape == (3, 2)
+ assert tensor.values() == [0.0, 2.0, 4.0, 6.0, 8.0, 10.0]
+ assert np.shares_memory(view, source)
+ assert not view.flags.writeable
+assert view.sum() == 30.0 # The view survives closing this Tensor handle.
+del view
+```
+
+```python
+import numpy as np
+from hmath import Tensor
+
+source = np.arange(4, dtype=np.float64) # Writable.
+try:
+ with Tensor.from_numpy(source, copy=False):
+ pass
+except ValueError:
+ pass
+else:
+ raise AssertionError("writable borrowing must be rejected")
+with Tensor.from_numpy(source) as copied:
+ source[0] = 99
+ assert copied.values()[0] == 0.0
+```
+
#### See also
[hmath.Tensor.numpy](#hmathtensornumpycopytrue), [hmath.Tensor](#hmathtensorvalues-shapenone), [hmath.Tensor.close](#hmathtensorclose).
@@ -343,6 +525,8 @@ with Tensor.from_numpy(array, copy=False) as tensor:
### `hmath.Tensor.shape`
+
+
Read the tensor’s dimensions.
#### Parameters
@@ -359,6 +543,17 @@ including `()` for scalars.
`RuntimeError` after close or
`HMathError` if native metadata retrieval fails.
+**Failure conditions by exception:**
+
+- **`RuntimeError`** — The handle is closed.
+
+- **`HMathError`** — Native metadata retrieval fails.
+
+#### Notes
+
+Shape `()` denotes one scalar, while a shape containing zero denotes an
+empty tensor. Reading the property does not copy the element buffer.
+
#### Examples
```python
@@ -387,6 +582,18 @@ row-major order, including for strided input; `[]` for empty tensors.
closed-handle or native copy errors.
+**Failure conditions by exception:**
+
+- **`RuntimeError`** — The handle is closed.
+
+- **`HMathError`** — Native copy validation fails.
+
+#### Notes
+
+Strided input is gathered in logical order, not raw storage-span order.
+The list remains valid after the Tensor closes. Copying N elements takes
+O(N) output space; this is not a zero-copy access method.
+
#### Examples
```python
@@ -418,12 +625,30 @@ Return a NumPy copy or a read-only view.
`ImportError` without NumPy,
closed-handle errors, or native metadata/data errors.
+**Failure conditions by exception:**
+
+- **`ImportError`** — NumPy is unavailable.
+
+- **`RuntimeError`** — The handle is closed.
+
+- **`HMathError`** — Native data, metadata, or copy retrieval fails.
+
#### Notes
`copy`: `True` for a writable independent copy, `False` for a
read-only native view with the tensor's shape and strides. Views retain storage
and borrowed owners even after this tensor closes. Do not mutate borrowed aliases.
+A copy is writable and unaffected by closing the tensor. A view is read-only
+and retains both native storage and any borrowed NumPy owner. Closing the
+original Tensor therefore does not invalidate the view or its derived views.
+For empty storage, `copy=False` may return a separate empty read-only NumPy
+allocation; there are no elements whose sharing can be demonstrated.
+
+Read-only views do not make other aliases to borrowed storage read-only.
+The borrowing immutability contract still applies. View construction allocates
+Python/NumPy metadata even when it does not copy elements.
+
#### Examples
```python
@@ -434,6 +659,21 @@ assert not view.flags.writeable
del view
```
+```python
+import numpy as np
+from hmath import Tensor
+
+with Tensor([1, 2, 3]) as tensor:
+ copied = tensor.numpy()
+ view = tensor.numpy(copy=False)
+ copied[0] = 99
+ assert tensor.values() == [1.0, 2.0, 3.0]
+ assert not np.shares_memory(copied, view)
+assert view.tolist() == [1.0, 2.0, 3.0]
+assert not view.flags.writeable
+del view
+```
+
#### See also
[hmath.Tensor.from_numpy](#hmathtensorfrom_numpyarray-copytrue), [hmath.Tensor.values](#hmathtensorvalues), [hmath.Tensor.close](#hmathtensorclose).
@@ -457,6 +697,31 @@ a `float`, or `0.0` for an empty tensor.
closed-handle errors or native arithmetic/runtime errors, including
non-finite results.
+**Failure conditions by exception:**
+
+- **`RuntimeError`** — A required Tensor handle is closed.
+
+- **`HMathError`** — Non-finite computed result (3), native scratch/result allocation failure (4),
+ unavailable runtime (5), or a contained Haskell exception (6).
+
+#### Notes
+
+For N logical elements, the mathematical operation is
+
+$$
+s = \sum_{i=0}^{N-1} x_i.
+$$
+
+This method has no `axis`, `dtype`, or `keepdims` argument: even a matrix is
+reduced to a Python float. A scalar Tensor contributes its one value.
+The native boundary gathers values in logical row-major order and the current
+Haskell reference kernel uses ordinary `sum`, not a documented compensated
+summation algorithm. Reduction work is O(N); the current boundary also uses
+O(N) gathered storage. An empty reduction has constant overhead.
+
+Finite-result checking does not eliminate rounding or cancellation, and no
+universal relative-error or correctly-rounded-result guarantee is made.
+
#### Examples
```python
@@ -464,6 +729,18 @@ with Tensor([1, 2, 3]) as tensor:
assert tensor.sum() == 6.0
```
+```python
+import math
+from hmath import Tensor
+
+with Tensor([1, 2, 3, 4], (2, 2)) as matrix:
+ assert matrix.sum() == 10.0
+with Tensor([], (0, 3)) as empty:
+ assert empty.sum() == 0.0
+with Tensor([0.1, 0.2]) as samples:
+ assert math.isclose(samples.sum(), 0.3, rel_tol=1e-12, abs_tol=1e-15)
+```
+
#### See also
[hmath.Tensor.dot](#hmathtensordotother).
@@ -487,11 +764,36 @@ a `float`, or `0.0` for matching empty tensors.
`HMathError` for shape/arithmetic/runtime errors; closed operands
raise `RuntimeError`.
+**Failure conditions by exception:**
+
+- **`RuntimeError`** — A required Tensor handle is closed.
+
+- **`HMathError`** — Non-finite computed result (3), native scratch/result allocation failure (4),
+ unavailable runtime (5), or a contained Haskell exception (6).
+
+- **`HMathError` with status 2** — The operand shapes differ, even when their total element counts are equal.
+
#### Notes
a live tensor with exactly the same shape. Sum corresponding
products over the entire tensor; this is a scalar reduction at every rank.
+For equal shapes and logical flattened elements, the mathematical definition is
+
+$$
+d = \sum_{i=0}^{N-1} a_i b_i.
+$$
+
+This is not NumPy's dimension-dependent `dot`: two matrices produce a scalar
+sum of elementwise products, not a matrix product. Use `a @ b` for matrix
+multiplication. There is no broadcasting or mixing of `hmath.Tensor` and
+`hmath_cffi.Tensor` operands.
+
+The current native boundary gathers both inputs; the Haskell reference kernel
+forms elementwise products and sums them. Arithmetic work and gathered input
+storage are O(N), with ordinary binary64 rounding. It does not promise
+compensated accumulation or exact products.
+
#### Examples
```python
@@ -499,6 +801,28 @@ with Tensor([1, 2, 3]) as a, Tensor([4, 5, 6]) as b:
assert a.dot(b) == 32.0
```
+```python
+from hmath import HMathError, Tensor
+
+with Tensor([1, 2, 3, 4], (2, 2)) as a:
+ with Tensor([5, 6, 7, 8], (2, 2)) as b:
+ assert a.dot(b) == 70.0
+ with a @ b as product:
+ assert product.values() == [19.0, 22.0, 43.0, 50.0]
+ with Tensor([1, 2, 3, 4], (4,)) as flat:
+ try:
+ a.dot(flat)
+ except HMathError as error:
+ assert error.status == 2
+ else:
+ raise AssertionError("equal counts do not imply equal shapes")
+```
+
+#### References
+
+[NumPy dot](https://numpy.org/doc/stable/reference/generated/numpy.dot.html)
+documents the different NumPy semantics; it is not a compatibility claim.
+
#### See also
[hmath.Tensor.sum](#hmathtensorsum), [hmath.Tensor.__matmul__](#hmathtensor__matmul__other-a-b).
@@ -521,6 +845,22 @@ a new owned tensor of elementwise sums. Inputs are unchanged.
native shape/allocation/arithmetic/runtime or closed-handle errors.
+**Failure conditions by exception:**
+
+- **`RuntimeError`** — A required Tensor handle is closed.
+
+- **`HMathError`** — Non-finite computed result (3), native scratch/result allocation failure (4),
+ unavailable runtime (5), or a contained Haskell exception (6).
+
+- **`HMathError` with status 2** — The shapes do not match.
+
+#### Notes
+
+For each logical position, `result[i] = a[i] + b[i]`. Scalars represented
+as rank-zero tensors can be added to other rank-zero tensors. `tensor + 1`
+is not a supported scalar-broadcast operation. Work and result storage are O(N),
+with additional O(N) scratch storage in the current native boundary.
+
#### Examples
```python
@@ -552,12 +892,43 @@ a new owned matrix.
native rank/inner-dimension,
size/work budget, allocation, arithmetic/runtime, or closed-handle errors.
+**Failure conditions by exception:**
+
+- **`RuntimeError`** — A required Tensor handle is closed.
+
+- **`HMathError`** — Non-finite computed result (3), native scratch/result allocation failure (4),
+ unavailable runtime (5), or a contained Haskell exception (6).
+
+- **`HMathError` with status 2** — An operand is not rank two or the inner dimensions differ.
+
+- **`HMathError` with status 3** — Output size or the scalar-operation work budget is exceeded.
+
#### Notes
a live rank-two tensor. Shapes `(m, k)` and `(k, n)` produce
`(m, n)`. Work is bounded by **16,777,216** scalar multiply-add steps (`m*k*n`).
This binding does not expose batched multiplication.
+The mathematical definition is
+
+$$
+C_{ij} = \sum_{p=0}^{k-1} A_{ip} B_{pj},
+\qquad C \in \mathbb{R}^{m \times n}.
+$$
+
+The output must contain at most **1,048,576** elements and `m*k*n` must not
+exceed **16,777,216**. This is a work budget, not a precision target or a promise
+of hardware fused multiply-add instructions. Vectors, batched products, and
+broadcasting are not supported by this binding.
+
+For `k == 0`, compatible `(m, 0)` and `(0, n)` inputs yield an `(m, n)` zero
+matrix, subject to the output budget. A zero outer dimension produces an empty
+result. The current Haskell reference loop accumulates each cell in increasing
+inner-index order. It performs O(m*k*n) arithmetic and produces O(m*n) output;
+gathering inputs and preparing output also costs O(m*k + k*n + m*n), including
+when an arithmetic dimension is zero. Scratch buffers are used. Do not infer
+BLAS acceleration from other Haskell modules.
+
#### Examples
```python
@@ -566,12 +937,28 @@ with Tensor.matrix([1, 2, 3, 4], 2, 2) as a:
assert squared.values() == [7., 10., 15., 22.]
```
+```python
+from hmath import Tensor
+
+with Tensor.matrix([1, 2, 3, 4, 5, 6], 2, 3) as a:
+ with Tensor.matrix([7, 8, 9, 10, 11, 12], 3, 2) as b:
+ with a @ b as result:
+ assert result.shape == (2, 2)
+ assert result.values() == [58.0, 64.0, 139.0, 154.0]
+with Tensor([], (2, 0)) as a, Tensor([], (0, 3)) as b:
+ with a @ b as result:
+ assert result.shape == (2, 3)
+ assert result.values() == [0.0] * 6
+```
+
#### See also
[hmath.Tensor.matrix](#hmathtensormatrixvalues-rows-columns), [hmath.Tensor.dot](#hmathtensordotother).
### `hmath.Tensor.reshape(shape)`
+
+
Create a new shape over the same contiguous storage.
#### Parameters
@@ -589,11 +976,29 @@ a new owned handle sharing storage; both handles can close independently.
`ValueError` for invalid dimensions, `HMathError` for incompatible
count or non-contiguous input, or `RuntimeError` after close.
+**Failure conditions by exception:**
+
+- **`ValueError`** — The Python shape/rank contract is violated.
+
+- **`TypeError`** — `shape` is not iterable.
+
+- **`RuntimeError`** — This handle is closed.
+
+- **`HMathError`** — Count mismatch or non-contiguous storage (2), native layout budget (3),
+ or new-handle allocation failure (4).
+
#### Notes
new dimensions with unchanged element count. Storage must be
contiguous. Negative dimensions, including `-1` for inference, are unsupported.
+No element reordering or copy fallback is performed. Contiguity is checked
+by the native layout validator: singleton dimensions do not constrain strides,
+and empty tensors are considered contiguous. A positive-stride borrowed view
+can still be non-contiguous and rejected. The reshaped handle retains any
+borrowed owner. Validation and metadata construction depend on rank, not on
+copying all elements.
+
#### Examples
```python
@@ -603,6 +1008,32 @@ with reshaped:
assert reshaped.shape == (2, 2)
```
+```python
+from hmath import Tensor
+
+with Tensor([1, 2, 3, 4]) as flat:
+ reshaped = flat.reshape((2, 2))
+with reshaped:
+ assert reshaped.shape == (2, 2)
+ assert reshaped.values() == [1.0, 2.0, 3.0, 4.0]
+```
+
+```python
+import numpy as np
+from hmath import HMathError, Tensor
+
+source = np.arange(8, dtype=np.float64)
+source.setflags(write=False)
+with Tensor.from_numpy(source[::2], copy=False) as strided:
+ try:
+ with strided.reshape((2, 2)):
+ pass
+ except HMathError as error:
+ assert error.status == 2
+ else:
+ raise AssertionError("non-contiguous reshape must be rejected")
+```
+
#### See also
[hmath.Tensor.shape](#hmathtensorshape), [hmath.Tensor](#hmathtensorvalues-shapenone).
@@ -622,6 +1053,13 @@ None.
`None`; repeated calls are harmless. Other retained handles/views
remain valid. Using this closed handle raises `RuntimeError`.
+#### Notes
+
+After closing, handle-dependent operations raise `RuntimeError`. Retained
+Tensor handles and NumPy views remain valid. This does not shut down the
+process-wide runtime. Closing is synchronized with primary-interface handle
+operations, not with arbitrary external writes to borrowed storage.
+
#### Examples
```python
@@ -630,6 +1068,20 @@ tensor.close()
tensor.close()
```
+```python
+from hmath import Tensor
+
+tensor = Tensor([1])
+tensor.close()
+tensor.close()
+try:
+ tensor.values()
+except RuntimeError:
+ pass
+else:
+ raise AssertionError("closed handles must not be used")
+```
+
#### See also
[hmath.Tensor.__enter__](#hmathtensor__enter__), [hmath.Tensor.__exit__](#hmathtensor__exit__args), [hmath.Tensor.__del__](#hmathtensor__del__).
@@ -652,10 +1104,22 @@ this creates no additional reference.
`RuntimeError` after close.
See the context-manager examples above.
+**Failure conditions by exception:**
+
+- **`RuntimeError`** — The handle is already closed.
+
#### See also
[hmath.Tensor.close](#hmathtensorclose), [hmath.Tensor.__exit__](#hmathtensor__exit__args).
+#### Notes
+
+Use `with tensor:` or `with Tensor(...) as tensor:`. Nested contexts over
+the same object do not create nested ownership: the inner exit closes that
+handle. Create an independent retained result when independent lifetimes are needed.
+
+
+
### `hmath.Tensor.__exit__(*args)`
Close the tensor when leaving a context.
@@ -692,6 +1156,13 @@ fallback. Collection timing is unspecified; use `with` for deterministic cleanup
[hmath.Tensor.close](#hmathtensorclose).
+#### Notes
+
+Calls `close()` as a fallback. Do not rely on collection timing or call
+this method directly as the usual cleanup API. Use `with` or `close()`.
+
+
+
## Optional cffi interface
Install `hmath-python[cffi]` to use `hmath_cffi`. Both wrappers share the native
@@ -719,6 +1190,18 @@ Python float.
`hmath_cffi.HMathError` for native failures or ordinary
conversion errors. Import initializes the runtime and checks ABI 1.
+**Failure conditions by exception:**
+
+- **`TypeError`, `OverflowError`** — cffi cannot convert an operand; conversion hooks can propagate other exceptions.
+
+- **`hmath_cffi.HMathError`** — Non-finite input (1), non-finite result (3), unavailable runtime (5),
+ or contained Haskell exception (6).
+
+#### Notes
+
+Unlike primary `add()`, this function does not itself reinitialize the runtime.
+Initialization happens during module import.
+
#### Examples
```python
@@ -732,6 +1215,8 @@ assert hc.add(2, 3) == 5.0
### `hmath_cffi.HMathError(status)`
+
+
Represent a failed native checked operation.
#### Parameters
@@ -746,10 +1231,33 @@ an exception with `.status`
and a snapshot of the current native error message. Catch this class for cffi
errors; it is a separate class from `hmath.HMathError`.
+#### Examples
+
+```python
+import hmath_cffi as hc
+
+try:
+ with hc.Tensor([1, 2], (3,)):
+ pass
+except hc.HMathError as error:
+ assert error.status == 2
+else:
+ raise AssertionError("shape mismatch must be rejected")
+```
+
#### See also
[hmath.HMathError](#hmathhmatherrorstatus), [hmath_cffi.Tensor](#hmath_cffitensorvalues-shapenone), [hmath_cffi.Tensor.close](#hmath_cffitensorclose).
+#### Notes
+
+Inherits from `RuntimeError`, but is not `hmath.HMathError`. Catch the
+exception class belonging to the interface being used. The message snapshots
+the calling thread's diagnostic; manual construction reads the current native
+message rather than performing a new operation.
+
+
+
### `hmath_cffi.check(status)`
Convert a raw native status into a Python exception.
@@ -769,12 +1277,27 @@ Convert a raw native status into a Python exception.
`hmath_cffi.HMathError` for nonzero. Call immediately after the raw
call so its thread-local error message has not been replaced.
+**Failure conditions by exception:**
+
+- **`hmath_cffi.HMathError`** — The supplied status is nonzero; `.status` is the supplied code.
+
#### See also
[hmath_cffi.Tensor](#hmath_cffitensorvalues-shapenone), [hmath_cffi.Tensor.close](#hmath_cffitensorclose).
+#### Notes
+
+The wrapper uses this helper internally. If using it with a raw checked
+call, invoke it immediately on the same thread so another operation cannot
+replace the diagnostic. High-level operations already call it; users do not
+need to wrap `Tensor.sum()` or `add()` in another `check()`.
+
+
+
### `hmath_cffi.Tensor(values, shape=None)`
+
+
Copy flat values into a new immutable tensor.
#### Parameters
@@ -794,6 +1317,23 @@ stored as a tuple.
`ValueError` for Python bounds,
`hmath_cffi.HMathError` for native input/allocation errors, or conversion errors.
+**Failure conditions by exception:**
+
+- **`ValueError`** — Rank, Python dimension, or input-count validation fails.
+
+- **`TypeError`, `OverflowError`** — cffi cannot convert values; conversion hooks can propagate other exceptions.
+
+- **`hmath_cffi.HMathError`** — Count mismatch (2), layout budget (3), non-finite data (1), or native
+ allocation failure (4).
+
+#### Notes
+
+Values are copied. The constructor consumes at most 1,048,577 values to
+detect oversized input. Use `with` or `close()`; do not close this handle
+concurrently with an operation using it. Runtime initialization occurs on
+module import. Garbage-collection cleanup is a fallback, not a deterministic
+replacement for explicit cleanup.
+
#### Examples
```python
@@ -803,6 +1343,15 @@ with hc.Tensor([1, 2, 3, 4], (2, 2)) as a:
assert result.values() == [7., 10., 15., 22.]
```
+```python
+import hmath_cffi as hc
+
+with hc.Tensor([1, 2, 3, 4], (2, 2)) as matrix:
+ assert matrix.shape == (2, 2)
+ with matrix @ matrix as squared:
+ assert squared.values() == [7.0, 10.0, 15.0, 22.0]
+```
+
#### See also
[hmath.Tensor](#hmathtensorvalues-shapenone), [hmath_cffi.Tensor.close](#hmath_cffitensorclose), [hmath_cffi.check](#hmath_cfficheckstatus).
@@ -824,6 +1373,12 @@ an independent row-major `list[float]`.
`RuntimeError` after close or `hmath_cffi.HMathError` on copy failure.
Use `.values()` inside the context-manager example above.
+**Failure conditions by exception:**
+
+- **`RuntimeError`** — The handle is closed.
+
+- **`hmath_cffi.HMathError`** — Native copy validation fails.
+
#### See also
[hmath.Tensor.values](#hmathtensorvalues), [hmath_cffi.Tensor](#hmath_cffitensorvalues-shapenone), [hmath_cffi.Tensor.close](#hmath_cffitensorclose).
@@ -845,10 +1400,24 @@ scalar sum, or `0.0` for an empty tensor.
native arithmetic/runtime or closed-handle errors. For example,
`hc.Tensor([1, 2])` sums to `3.0`; close the owner after use.
+**Failure conditions by exception:**
+
+- **`RuntimeError`** — A required Tensor handle is closed.
+
+- **`hmath_cffi.HMathError`** — Non-finite computed result (3), native scratch/result allocation failure (4),
+ unavailable runtime (5), or a contained Haskell exception (6).
+
#### See also
[hmath.Tensor.sum](#hmathtensorsum), [hmath_cffi.Tensor](#hmath_cffitensorvalues-shapenone), [hmath_cffi.Tensor.close](#hmath_cffitensorclose).
+#### Notes
+
+The whole-tensor reduction, numerical behavior, and native scratch costs
+are the same as primary `Tensor.sum()`. No axis argument is exposed.
+
+
+
### `hmath_cffi.Tensor.dot(other)`
Reduce corresponding products of two identically shaped tensors.
@@ -869,10 +1438,26 @@ of elementwise products.
native shape/arithmetic/runtime or
closed-handle errors. The primary interface's dot example also applies to cffi.
+**Failure conditions by exception:**
+
+- **`RuntimeError`** — A required Tensor handle is closed.
+
+- **`hmath_cffi.HMathError`** — Non-finite computed result (3), native scratch/result allocation failure (4),
+ unavailable runtime (5), or a contained Haskell exception (6).
+
+- **`hmath_cffi.HMathError` with status 2** — Shapes differ.
+
#### See also
[hmath.Tensor.dot](#hmathtensordotother), [hmath_cffi.Tensor](#hmath_cffitensorvalues-shapenone), [hmath_cffi.Tensor.close](#hmath_cffitensorclose).
+#### Notes
+
+This is the same whole-tensor scalar reduction as primary `dot`, not
+matrix multiplication. See that entry for the equation and numerical behavior.
+
+
+
### `hmath_cffi.Tensor.__add__(other)` — `a + b`
Add two tensors element by element.
@@ -893,6 +1478,15 @@ elementwise sum without broadcasting.
native shape/allocation/
arithmetic/runtime or closed-handle errors. Use with `with a + b as result:`.
+**Failure conditions by exception:**
+
+- **`RuntimeError`** — A required Tensor handle is closed.
+
+- **`hmath_cffi.HMathError`** — Non-finite computed result (3), native scratch/result allocation failure (4),
+ unavailable runtime (5), or a contained Haskell exception (6).
+
+- **`hmath_cffi.HMathError` with status 2** — Shapes differ.
+
#### See also
[hmath.Tensor.__add__](#hmathtensor__add__other-a-b), [hmath_cffi.Tensor](#hmath_cffitensorvalues-shapenone), [hmath_cffi.Tensor.close](#hmath_cffitensorclose).
@@ -916,10 +1510,27 @@ a new owned matrix.
the same shape/work-bound and
native/lifecycle errors as primary `@`. See the executable cffi example above.
+**Failure conditions by exception:**
+
+- **`RuntimeError`** — A required Tensor handle is closed.
+
+- **`hmath_cffi.HMathError`** — Non-finite computed result (3), native scratch/result allocation failure (4),
+ unavailable runtime (5), or a contained Haskell exception (6).
+
+- **`hmath_cffi.HMathError`** — Rank/inner-dimension mismatch (2), or output/work budget exceeded (3).
+
#### See also
[hmath.Tensor.__matmul__](#hmathtensor__matmul__other-a-b), [hmath_cffi.Tensor](#hmath_cffitensorvalues-shapenone), [hmath_cffi.Tensor.close](#hmath_cffitensorclose).
+#### Notes
+
+The primary matrix-product definition and budgets apply: at most
+1,048,576 output elements and 16,777,216 scalar multiply-add steps.
+There is no batching or vector promotion.
+
+
+
### `hmath_cffi.Tensor.close()`
Release this tensor handle deterministically.
@@ -938,6 +1549,14 @@ this tensor. Other independent owners remain valid.
[hmath.Tensor.close](#hmathtensorclose), [hmath_cffi.Tensor](#hmath_cffitensorvalues-shapenone), [hmath_cffi.check](#hmath_cfficheckstatus).
+#### Notes
+
+Do not call concurrently with an operation using this handle. Numerical
+operations and `values()` fail after close; cached `.shape` remains readable.
+The shared runtime is not shut down.
+
+
+
### `hmath_cffi.Tensor.__enter__()`
Enter a context that owns this tensor handle.
@@ -954,10 +1573,21 @@ the same live tensor in `with`.
`RuntimeError` after close. No new native owner is created.
+**Failure conditions by exception:**
+
+- **`RuntimeError`** — The handle is closed.
+
#### See also
[hmath.Tensor.__enter__](#hmathtensor__enter__), [hmath_cffi.Tensor](#hmath_cffitensorvalues-shapenone), [hmath_cffi.Tensor.close](#hmath_cffitensorclose).
+#### Notes
+
+As with the primary interface, nested contexts on the same object do not
+create independent handle lifetimes.
+
+
+
### `hmath_cffi.Tensor.__exit__(*args)`
Close the tensor when leaving a context.
@@ -977,6 +1607,78 @@ tensor on block exit without suppressing exceptions.
[hmath.Tensor.__exit__](#hmathtensor__exit__args), [hmath_cffi.Tensor](#hmath_cffitensorvalues-shapenone), [hmath_cffi.Tensor.close](#hmath_cffitensorclose).
+## Public exception and cffi attributes
+
+The following public attributes are also available on the Python objects.
+
+### `hmath.HMathError.status`
+
+
+
+Read the native status captured by an exception.
+
+#### Type
+
+`int`.
+
+#### See also
+
+[hmath.HMathError](#hmath.HMathError), [native status definitions](c.md).
+
+#### Notes
+
+The value describes the failed native call, not a later operation. Prefer the
+status and exception class over exact diagnostic text when classifying failures.
+
+### `hmath_cffi.HMathError.status`
+
+
+
+Read the native status captured by a cffi exception.
+
+#### Type
+
+`int`.
+
+#### See also
+
+[hmath_cffi.HMathError](#hmath_cffi.HMathError), [native status definitions](c.md).
+
+#### Notes
+
+The same native numeric code definitions apply to both wrappers, even
+though their Python exception classes differ.
+
+### `hmath_cffi.Tensor.shape`
+
+
+
+Read the cached shape of a cffi Tensor.
+
+#### Type
+
+`tuple[int, ...]`.
+
+#### See also
+
+[hmath_cffi.Tensor](#hmath_cffi.Tensor), [hmath.Tensor.shape](#hmath.Tensor.shape).
+
+#### Notes
+
+Unlike the primary interface's property, this is a stored Python attribute
+and remains readable after `close()`. Treat it as read-only: assigning to it
+does not reshape native storage and can make Python metadata inconsistent.
+
+#### Examples
+
+```python
+import hmath_cffi as hc
+
+with hc.Tensor([1, 2]) as tensor:
+ assert tensor.shape == (2,)
+assert tensor.shape == (2,) # Cached metadata, not evidence of a live handle.
+```
+
## Low-level cffi attributes
`hmath_cffi.ffi` is the configured `cffi.FFI` object; `hmath_cffi.lib` is the