Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
9 changes: 9 additions & 0 deletions docs/contributing.md
Original file line number Diff line number Diff line change
Expand Up @@ -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.
33 changes: 33 additions & 0 deletions docs/explanation/numerical-conventions.md
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
112 changes: 112 additions & 0 deletions docs/guides/numpy.md
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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).
Loading
Loading