arraybridge 0.3.4__tar.gz → 0.3.7__tar.gz
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- {arraybridge-0.3.4 → arraybridge-0.3.7}/PKG-INFO +16 -1
- {arraybridge-0.3.4 → arraybridge-0.3.7}/README.md +14 -0
- {arraybridge-0.3.4 → arraybridge-0.3.7}/docs/source/index.rst +2 -0
- arraybridge-0.3.7/docs/source/release_035_validation.rst +51 -0
- arraybridge-0.3.7/docs/source/release_036_validation.rst +58 -0
- {arraybridge-0.3.4 → arraybridge-0.3.7}/pyproject.toml +1 -0
- {arraybridge-0.3.4 → arraybridge-0.3.7}/src/arraybridge/__init__.py +1 -1
- arraybridge-0.3.7/src/arraybridge/array_operations.py +450 -0
- {arraybridge-0.3.4 → arraybridge-0.3.7}/src/arraybridge/decorators.py +41 -16
- {arraybridge-0.3.4 → arraybridge-0.3.7}/src/arraybridge/types.py +38 -7
- arraybridge-0.3.7/tests/test_callable_dtype_default.py +63 -0
- arraybridge-0.3.7/tests/test_durable_decorator_context.py +50 -0
- arraybridge-0.3.7/tests/test_native_geometry.py +65 -0
- arraybridge-0.3.4/src/arraybridge/array_operations.py +0 -396
- {arraybridge-0.3.4 → arraybridge-0.3.7}/.github/workflows/ci.yml +0 -0
- {arraybridge-0.3.4 → arraybridge-0.3.7}/.github/workflows/coverage-pages.yml +0 -0
- {arraybridge-0.3.4 → arraybridge-0.3.7}/.github/workflows/gpu-tests.yml +0 -0
- {arraybridge-0.3.4 → arraybridge-0.3.7}/.github/workflows/publish.yml +0 -0
- {arraybridge-0.3.4 → arraybridge-0.3.7}/.gitignore +0 -0
- {arraybridge-0.3.4 → arraybridge-0.3.7}/.readthedocs.yml +0 -0
- {arraybridge-0.3.4 → arraybridge-0.3.7}/ADDING_NEW_FRAMEWORKS.md +0 -0
- {arraybridge-0.3.4 → arraybridge-0.3.7}/CONTRIBUTING.md +0 -0
- {arraybridge-0.3.4 → arraybridge-0.3.7}/LICENSE +0 -0
- {arraybridge-0.3.4 → arraybridge-0.3.7}/TESTING.md +0 -0
- {arraybridge-0.3.4 → arraybridge-0.3.7}/docs/Makefile +0 -0
- {arraybridge-0.3.4 → arraybridge-0.3.7}/docs/archive/CI_ARTIFACT_UPDATE.md +0 -0
- {arraybridge-0.3.4 → arraybridge-0.3.7}/docs/archive/CI_CUPY_FIX.md +0 -0
- {arraybridge-0.3.4 → arraybridge-0.3.7}/docs/archive/CI_IMPROVEMENTS.md +0 -0
- {arraybridge-0.3.4 → arraybridge-0.3.7}/docs/archive/CONFLICT_VISUALIZATION.md +0 -0
- {arraybridge-0.3.4 → arraybridge-0.3.7}/docs/archive/COVERAGE_AUDIT_PLAN.md +0 -0
- {arraybridge-0.3.4 → arraybridge-0.3.7}/docs/archive/DEPLOYMENT_SUMMARY.md +0 -0
- {arraybridge-0.3.4 → arraybridge-0.3.7}/docs/archive/GPU_TESTING_SETUP.md +0 -0
- {arraybridge-0.3.4 → arraybridge-0.3.7}/docs/archive/MERGE_CONFLICT_RESOLUTION.md +0 -0
- {arraybridge-0.3.4 → arraybridge-0.3.7}/docs/archive/MIGRATION_SUMMARY.md +0 -0
- {arraybridge-0.3.4 → arraybridge-0.3.7}/docs/archive/QUICK_FIX_GUIDE.txt +0 -0
- {arraybridge-0.3.4 → arraybridge-0.3.7}/docs/archive/README.md +0 -0
- {arraybridge-0.3.4 → arraybridge-0.3.7}/docs/archive/RESOLVED_FILES.md +0 -0
- {arraybridge-0.3.4 → arraybridge-0.3.7}/docs/ci-cd.md +0 -0
- {arraybridge-0.3.4 → arraybridge-0.3.7}/docs/source/.gitignore +0 -0
- {arraybridge-0.3.4 → arraybridge-0.3.7}/docs/source/Makefile +0 -0
- {arraybridge-0.3.4 → arraybridge-0.3.7}/docs/source/README.md +0 -0
- {arraybridge-0.3.4 → arraybridge-0.3.7}/docs/source/advanced_topics.rst +0 -0
- {arraybridge-0.3.4 → arraybridge-0.3.7}/docs/source/api_reference.rst +0 -0
- {arraybridge-0.3.4 → arraybridge-0.3.7}/docs/source/ci-cd.rst +0 -0
- {arraybridge-0.3.4 → arraybridge-0.3.7}/docs/source/conf.py +0 -0
- {arraybridge-0.3.4 → arraybridge-0.3.7}/docs/source/contributing.rst +0 -0
- {arraybridge-0.3.4 → arraybridge-0.3.7}/docs/source/converters.rst +0 -0
- {arraybridge-0.3.4 → arraybridge-0.3.7}/docs/source/decorators.rst +0 -0
- {arraybridge-0.3.4 → arraybridge-0.3.7}/docs/source/examples/basic_conversion.rst +0 -0
- {arraybridge-0.3.4 → arraybridge-0.3.7}/docs/source/examples/decorators.rst +0 -0
- {arraybridge-0.3.4 → arraybridge-0.3.7}/docs/source/examples/index.rst +0 -0
- {arraybridge-0.3.4 → arraybridge-0.3.7}/docs/source/examples/multi_framework.rst +0 -0
- {arraybridge-0.3.4 → arraybridge-0.3.7}/docs/source/gpu_features.rst +0 -0
- {arraybridge-0.3.4 → arraybridge-0.3.7}/docs/source/installation.rst +0 -0
- {arraybridge-0.3.4 → arraybridge-0.3.7}/docs/source/quickstart.rst +0 -0
- {arraybridge-0.3.4 → arraybridge-0.3.7}/docs/source/stack_utils.rst +0 -0
- {arraybridge-0.3.4 → arraybridge-0.3.7}/docs/source/user_guide.rst +0 -0
- {arraybridge-0.3.4 → arraybridge-0.3.7}/paper.bib +0 -0
- {arraybridge-0.3.4 → arraybridge-0.3.7}/paper.md +0 -0
- {arraybridge-0.3.4 → arraybridge-0.3.7}/scripts/release.py +0 -0
- {arraybridge-0.3.4 → arraybridge-0.3.7}/scripts/update_version.py +0 -0
- {arraybridge-0.3.4 → arraybridge-0.3.7}/scripts/verify_release_ready.py +0 -0
- {arraybridge-0.3.4 → arraybridge-0.3.7}/src/arraybridge/array_geometry.py +0 -0
- {arraybridge-0.3.4 → arraybridge-0.3.7}/src/arraybridge/array_payload.py +0 -0
- {arraybridge-0.3.4 → arraybridge-0.3.7}/src/arraybridge/converters.py +0 -0
- {arraybridge-0.3.4 → arraybridge-0.3.7}/src/arraybridge/converters_registry.py +0 -0
- {arraybridge-0.3.4 → arraybridge-0.3.7}/src/arraybridge/dtype_scaling.py +0 -0
- {arraybridge-0.3.4 → arraybridge-0.3.7}/src/arraybridge/exceptions.py +0 -0
- {arraybridge-0.3.4 → arraybridge-0.3.7}/src/arraybridge/framework_config.py +0 -0
- {arraybridge-0.3.4 → arraybridge-0.3.7}/src/arraybridge/framework_ops.py +0 -0
- {arraybridge-0.3.4 → arraybridge-0.3.7}/src/arraybridge/gpu_cleanup.py +0 -0
- {arraybridge-0.3.4 → arraybridge-0.3.7}/src/arraybridge/oom_recovery.py +0 -0
- {arraybridge-0.3.4 → arraybridge-0.3.7}/src/arraybridge/slice_processing.py +0 -0
- {arraybridge-0.3.4 → arraybridge-0.3.7}/src/arraybridge/stack_utils.py +0 -0
- {arraybridge-0.3.4 → arraybridge-0.3.7}/src/arraybridge/utils.py +0 -0
- {arraybridge-0.3.4 → arraybridge-0.3.7}/tests/__init__.py +0 -0
- {arraybridge-0.3.4 → arraybridge-0.3.7}/tests/conftest.py +0 -0
- {arraybridge-0.3.4 → arraybridge-0.3.7}/tests/test_array_geometry.py +0 -0
- {arraybridge-0.3.4 → arraybridge-0.3.7}/tests/test_converters.py +0 -0
- {arraybridge-0.3.4 → arraybridge-0.3.7}/tests/test_converters_registry.py +0 -0
- {arraybridge-0.3.4 → arraybridge-0.3.7}/tests/test_decorators.py +0 -0
- {arraybridge-0.3.4 → arraybridge-0.3.7}/tests/test_dtype_scaling.py +0 -0
- {arraybridge-0.3.4 → arraybridge-0.3.7}/tests/test_exceptions.py +0 -0
- {arraybridge-0.3.4 → arraybridge-0.3.7}/tests/test_framework_config.py +0 -0
- {arraybridge-0.3.4 → arraybridge-0.3.7}/tests/test_gpu_cleanup.py +0 -0
- {arraybridge-0.3.4 → arraybridge-0.3.7}/tests/test_integration.py +0 -0
- {arraybridge-0.3.4 → arraybridge-0.3.7}/tests/test_oom_recovery.py +0 -0
- {arraybridge-0.3.4 → arraybridge-0.3.7}/tests/test_registry_integration.py +0 -0
- {arraybridge-0.3.4 → arraybridge-0.3.7}/tests/test_slice_processing.py +0 -0
- {arraybridge-0.3.4 → arraybridge-0.3.7}/tests/test_stack_utils.py +0 -0
- {arraybridge-0.3.4 → arraybridge-0.3.7}/tests/test_types.py +0 -0
- {arraybridge-0.3.4 → arraybridge-0.3.7}/tests/test_utils.py +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: arraybridge
|
|
3
|
-
Version: 0.3.
|
|
3
|
+
Version: 0.3.7
|
|
4
4
|
Summary: Unified API for NumPy, CuPy, PyTorch, TensorFlow, JAX, and pyclesperanto with automatic memory type conversion
|
|
5
5
|
Project-URL: Homepage, https://github.com/OpenHCSDev/arraybridge
|
|
6
6
|
Project-URL: Documentation, https://arraybridge.readthedocs.io
|
|
@@ -36,6 +36,7 @@ Requires-Dist: cupy>=10.0; extra == 'cupy'
|
|
|
36
36
|
Provides-Extra: dev
|
|
37
37
|
Requires-Dist: black>=23.0; extra == 'dev'
|
|
38
38
|
Requires-Dist: build>=1.0; extra == 'dev'
|
|
39
|
+
Requires-Dist: dill>=0.3.8; extra == 'dev'
|
|
39
40
|
Requires-Dist: mypy>=1.0; extra == 'dev'
|
|
40
41
|
Requires-Dist: packaging>=23.0; extra == 'dev'
|
|
41
42
|
Requires-Dist: pytest-cov>=4.0; extra == 'dev'
|
|
@@ -142,3 +143,17 @@ pip install "arraybridge[cupy]"
|
|
|
142
143
|
```
|
|
143
144
|
|
|
144
145
|
Documentation: <https://arraybridge.readthedocs.io>
|
|
146
|
+
|
|
147
|
+
### Native array geometry
|
|
148
|
+
|
|
149
|
+
`MemoryType.reshape(array, shape)` and `MemoryType.broadcast_to(array, shape)`
|
|
150
|
+
operate in the array's framework and device. `MemoryType.ones_like(reference,
|
|
151
|
+
shape=..., dtype=...)` allocates on the reference's device; omitting `shape`
|
|
152
|
+
retains its shape. Reshape and broadcast preserve views where the backend can
|
|
153
|
+
represent them. Broadcasting is not a promise of a writable, independent buffer.
|
|
154
|
+
|
|
155
|
+
These operations never silently project pixels to NumPy. Pyclesperanto cannot
|
|
156
|
+
provide native changed-shape reshape or broadcasting, so those requests raise
|
|
157
|
+
`NotImplementedError`. Its native ones allocation supports one to three
|
|
158
|
+
dimensions and its supported integer/float32 dtypes; boolean allocation is
|
|
159
|
+
explicitly unsupported. Existing conversion and disk boundaries remain separate.
|
|
@@ -71,3 +71,17 @@ pip install "arraybridge[cupy]"
|
|
|
71
71
|
```
|
|
72
72
|
|
|
73
73
|
Documentation: <https://arraybridge.readthedocs.io>
|
|
74
|
+
|
|
75
|
+
### Native array geometry
|
|
76
|
+
|
|
77
|
+
`MemoryType.reshape(array, shape)` and `MemoryType.broadcast_to(array, shape)`
|
|
78
|
+
operate in the array's framework and device. `MemoryType.ones_like(reference,
|
|
79
|
+
shape=..., dtype=...)` allocates on the reference's device; omitting `shape`
|
|
80
|
+
retains its shape. Reshape and broadcast preserve views where the backend can
|
|
81
|
+
represent them. Broadcasting is not a promise of a writable, independent buffer.
|
|
82
|
+
|
|
83
|
+
These operations never silently project pixels to NumPy. Pyclesperanto cannot
|
|
84
|
+
provide native changed-shape reshape or broadcasting, so those requests raise
|
|
85
|
+
`NotImplementedError`. Its native ones allocation supports one to three
|
|
86
|
+
dimensions and its supported integer/float32 dtypes; boolean allocation is
|
|
87
|
+
explicitly unsupported. Existing conversion and disk boundaries remain separate.
|
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
ArrayBridge 0.3.5 release preparation
|
|
2
|
+
=====================================
|
|
3
|
+
|
|
4
|
+
Parent integration owner; base ea3f2a4cc91c4810d12343f58f85c1195e1a41e6.
|
|
5
|
+
The reviewed callable-native dtype default and durable decorator-context
|
|
6
|
+
changes are already merged through PR2 and PR3. This release preparation changes
|
|
7
|
+
only the existing package version authority from 0.3.4 to 0.3.5 and records
|
|
8
|
+
validation. It adds no implementation, compatibility API or parallel registry.
|
|
9
|
+
|
|
10
|
+
OpenHCS PR217 requires ``dtype_config_default``. A fresh PyPI ArrayBridge0.3.4
|
|
11
|
+
wheel raises TypeError at its BaSiC adapter's decoration. Its recorded local
|
|
12
|
+
source candidate has that API, but is not a public release. OpenHCS now requires
|
|
13
|
+
ArrayBridge>=0.3.5,<0.4 so ordinary installation cannot select the old API.
|
|
14
|
+
|
|
15
|
+
Persistent evidence::
|
|
16
|
+
|
|
17
|
+
/home/ts/wt/openhcs-issue-batch-20260929/artifact-parent-20261001
|
|
18
|
+
|
|
19
|
+
Focused provider-free checks, using the existing Python3.12 environment::
|
|
20
|
+
|
|
21
|
+
PYTHONPATH=src PYTEST_DISABLE_PLUGIN_AUTOLOAD=1 python -B -m pytest \
|
|
22
|
+
-o addopts='' -q tests/test_callable_dtype_default.py \
|
|
23
|
+
tests/test_durable_decorator_context.py
|
|
24
|
+
|
|
25
|
+
All seven tests pass in 0.12 seconds. They retain native floating-point values,
|
|
26
|
+
explicit runtime dtype overrides, unchanged default preservation, rejection of
|
|
27
|
+
untyped declarations and durable serialization without thread-local leakage.
|
|
28
|
+
The original complete-suite acceptance of the reviewed feature PRs is separate;
|
|
29
|
+
these seven checks do not claim a full framework/GPU matrix.
|
|
30
|
+
|
|
31
|
+
The wheel was built through ``uv build --offline --wheel`` from this source
|
|
32
|
+
using cached build requirements. Twine6.2 accepts the resulting metadata2.4
|
|
33
|
+
wheel. An initial no-isolation build lacked Hatchling in the installed OpenHCS
|
|
34
|
+
environment and failed; it did not install packages or count as a pass.
|
|
35
|
+
|
|
36
|
+
A fresh process loaded that exact 0.3.5 wheel archive ahead of the actual
|
|
37
|
+
installed OpenHCS candidate and executed the real BaSiC adapter on 24 synthetic
|
|
38
|
+
32x32 SITE observations. It returned finite float32 correction with fractional
|
|
39
|
+
pixels, preserved source pixels and matched the same-fit correction formula.
|
|
40
|
+
Flatfield RMSE against the known synthetic shading field is 0.0007167315491296268.
|
|
41
|
+
``arraybridge-035-installed-fit.log`` records the actual wheel and site-packages
|
|
42
|
+
import paths. This establishes local wheel/API interoperability, not registry
|
|
43
|
+
resolution, a full installed MCP rerun or biological accuracy. The separate
|
|
44
|
+
installed MCP journey is retained with OpenHCS PR217.
|
|
45
|
+
|
|
46
|
+
Publication is not performed by this PR. The parent requested separate owner
|
|
47
|
+
approval for the companion ArrayBridge0.3.5 publication. No tag was created,
|
|
48
|
+
workflow dispatched, registry upload performed or release availability claimed.
|
|
49
|
+
After approval, merge normally, tag the reviewed main version and use the
|
|
50
|
+
existing trusted publishing workflow. Verify the actual registry wheel before
|
|
51
|
+
claiming ordinary OpenHCS dependency installation.
|
|
@@ -0,0 +1,58 @@
|
|
|
1
|
+
ArrayBridge typed thread-local release correction
|
|
2
|
+
=================================================
|
|
3
|
+
|
|
4
|
+
Parent integration owner. Actual failed original 0.3.5 publisher:
|
|
5
|
+
https://github.com/OpenHCSDev/ArrayBridge/actions/runs/36946585391.
|
|
6
|
+
Ruff and Black passed; mypy rejected decorators.py:284 because current()
|
|
7
|
+
returned Any from the dynamic threading.local context slot. No wheel was
|
|
8
|
+
published. The failure and original v0.3.5 tag are preserved unchanged.
|
|
9
|
+
|
|
10
|
+
The existing ThreadGPUContext runtime owner now uses a threading.local
|
|
11
|
+
subclass declaring its sole context slot. The original per-thread lazy
|
|
12
|
+
initialization and stream identity remain; current() consumes the declared
|
|
13
|
+
optional value, not hasattr plus an untyped foreign attribute. No cast,
|
|
14
|
+
type-ignore, fallback reader, new context registry or serialization path.
|
|
15
|
+
BOUND-7: declare the native storage boundary instead of probing raw attributes.
|
|
16
|
+
Native threading.local retains thread isolation. Durable decorated callables
|
|
17
|
+
continue to refer to the original importable ThreadGPUContext owner.
|
|
18
|
+
|
|
19
|
+
This patch uses version0.3.6 rather than moving/reusing an existing release tag.
|
|
20
|
+
The already reviewed dtype_config_default behavior is unchanged. OpenHCS's
|
|
21
|
+
>=0.3.5,<0.4 requirement admits this corrected patch release normally.
|
|
22
|
+
|
|
23
|
+
Retained bounded local red mypy reproduces the exact hosted no-any-return
|
|
24
|
+
error: exit1,2.65s,96.91MiB. Green mypy checks all17 source files with no
|
|
25
|
+
errors: exit0,2.75s,97.14MiB. Ruff passes src/scripts; Black leaves20 files
|
|
26
|
+
unchanged. Existing interpreter/tooling only; no dependency installation.
|
|
27
|
+
|
|
28
|
+
Original supervisor logs/commands reside at:
|
|
29
|
+
/home/ts/wt/openhcs-issue-batch-20260929/s1-installed-20261001/
|
|
30
|
+
arraybridge036-mypy-red and arraybridge036-mypy-green. Kernel512MiB,
|
|
31
|
+
no swap, oneCPU/thread,60-second shard. Source behavior and release artifact
|
|
32
|
+
verification are recorded below when complete; no registry success claimed
|
|
33
|
+
by source checks alone.
|
|
34
|
+
|
|
35
|
+
Original focused behavior suite passes11 tests,0.30s pytest/0.82s total,
|
|
36
|
+
52.7MiB. It covers thread-local context identity/isolation, framework/device
|
|
37
|
+
stream identity, actual NumPy dtype controls, and dill restore of unpublished
|
|
38
|
+
decorated callables with a live unpicklable runtime handle. Assertions and the
|
|
39
|
+
existing three test files are unchanged; no real GPU runtime is needed here.
|
|
40
|
+
|
|
41
|
+
Original packaged R0 against merged e9aaa262 at source33d5a99f reports zero
|
|
42
|
+
nonzero deltas across both changed product files, exit0,1.93s/43.73MiB. The
|
|
43
|
+
original global NRA R1 remains a separate unfinished OpenHCS tool-owner check;
|
|
44
|
+
this bounded ratchet does not claim that global analysis completed.
|
|
45
|
+
|
|
46
|
+
Original cached offline uv build produces wheel and sdist, exit0,
|
|
47
|
+
1.26s/66.22MiB. Twine accepts both. Local wheel SHA256:
|
|
48
|
+
f7fa23f5dcf1925742592109b396fe17662fb56f15e1755a304a45c90e79a249;
|
|
49
|
+
local sdist SHA256:
|
|
50
|
+
a558dcb5827876725c854cb367a7598765a55a43e658d82678db4f2c541663d4.
|
|
51
|
+
These local bytes are not assumed equal to the independently hosted rebuild.
|
|
52
|
+
|
|
53
|
+
Addresses ArrayBridge issue5. Original source proof does not establish a full
|
|
54
|
+
GPU matrix. The unchanged original publisher owns the complete hosted check,
|
|
55
|
+
build and trusted upload. Merge this reviewed source checkpoint without waiting
|
|
56
|
+
on optional CI, push a new annotated v0.3.6 at actual merged main, and verify
|
|
57
|
+
PyPI installer visibility and the hosted wheel source/API before admitting the
|
|
58
|
+
consumer. Do not replace v0.3.5 or replay the failed original publisher.
|
|
@@ -0,0 +1,450 @@
|
|
|
1
|
+
"""Native array operations carried by MemoryType declarations."""
|
|
2
|
+
|
|
3
|
+
from abc import ABC, abstractmethod
|
|
4
|
+
from collections.abc import Sequence
|
|
5
|
+
from typing import Any
|
|
6
|
+
|
|
7
|
+
import numpy as np
|
|
8
|
+
|
|
9
|
+
_SCALING_RANGES: dict[str, float | tuple[float, float]] = {
|
|
10
|
+
"uint8": 255.0,
|
|
11
|
+
"uint16": 65535.0,
|
|
12
|
+
"uint32": 4294967295.0,
|
|
13
|
+
"int16": (65535.0, 32768.0),
|
|
14
|
+
"int32": (4294967295.0, 2147483648.0),
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
def _dtype_name(dtype: Any) -> str:
|
|
19
|
+
declared_name = getattr(dtype, "name", None)
|
|
20
|
+
if declared_name is not None:
|
|
21
|
+
return str(declared_name)
|
|
22
|
+
return getattr(dtype, "__name__", str(dtype).rsplit(".", maxsplit=1)[-1])
|
|
23
|
+
|
|
24
|
+
|
|
25
|
+
def _numpy_dtype_name(dtype: Any) -> str:
|
|
26
|
+
return str(np.dtype(dtype).name)
|
|
27
|
+
|
|
28
|
+
|
|
29
|
+
def _torch_dtype_name(dtype: Any) -> str:
|
|
30
|
+
return str(dtype).rsplit(".", maxsplit=1)[-1]
|
|
31
|
+
|
|
32
|
+
|
|
33
|
+
def _tensorflow_dtype_name(dtype: Any) -> str:
|
|
34
|
+
numpy_dtype = getattr(dtype, "as_numpy_dtype", dtype)
|
|
35
|
+
return str(np.dtype(numpy_dtype).name)
|
|
36
|
+
|
|
37
|
+
|
|
38
|
+
def _scaled_values(result: Any, result_min: Any, result_max: Any, target_dtype: Any) -> Any:
|
|
39
|
+
normalized = (result - result_min) / (result_max - result_min)
|
|
40
|
+
range_info = _SCALING_RANGES.get(_dtype_name(target_dtype))
|
|
41
|
+
if range_info is None:
|
|
42
|
+
return normalized
|
|
43
|
+
if isinstance(range_info, tuple):
|
|
44
|
+
scale, offset = range_info
|
|
45
|
+
return normalized * scale - offset
|
|
46
|
+
return normalized * range_info
|
|
47
|
+
|
|
48
|
+
|
|
49
|
+
def _clamp_bounds(target_dtype: Any) -> tuple[float, float] | None:
|
|
50
|
+
range_info = _SCALING_RANGES.get(_dtype_name(target_dtype))
|
|
51
|
+
if range_info is None:
|
|
52
|
+
return None
|
|
53
|
+
if isinstance(range_info, tuple):
|
|
54
|
+
scale, offset = range_info
|
|
55
|
+
return -offset, scale - offset - 128
|
|
56
|
+
return 0, range_info
|
|
57
|
+
|
|
58
|
+
|
|
59
|
+
def _mapped_dtype(target_dtype: Any, module: Any) -> Any:
|
|
60
|
+
try:
|
|
61
|
+
dtype_name = np.dtype(target_dtype).name
|
|
62
|
+
except TypeError as error:
|
|
63
|
+
raise TypeError(f"Unsupported target dtype {target_dtype!r}") from error
|
|
64
|
+
mapped = getattr(module, dtype_name, None)
|
|
65
|
+
if mapped is None:
|
|
66
|
+
module_name = getattr(module, "__name__", type(module).__name__)
|
|
67
|
+
raise TypeError(f"{module_name} does not expose dtype {dtype_name}")
|
|
68
|
+
return mapped
|
|
69
|
+
|
|
70
|
+
|
|
71
|
+
class ArrayOperations(ABC):
|
|
72
|
+
"""Native array semantics selected by the existing MemoryType declaration."""
|
|
73
|
+
|
|
74
|
+
@staticmethod
|
|
75
|
+
@abstractmethod
|
|
76
|
+
def to_numpy(data: Any, module: Any) -> Any:
|
|
77
|
+
"""Project pixels to the explicit host boundary."""
|
|
78
|
+
|
|
79
|
+
@staticmethod
|
|
80
|
+
@abstractmethod
|
|
81
|
+
def from_numpy(data: Any, module: Any, device_id: int) -> Any:
|
|
82
|
+
"""Admit host pixels on the selected framework device."""
|
|
83
|
+
|
|
84
|
+
@staticmethod
|
|
85
|
+
@abstractmethod
|
|
86
|
+
def scale_dtype(result: Any, target_dtype: Any, module: Any) -> Any:
|
|
87
|
+
"""Apply the framework's existing intensity conversion law."""
|
|
88
|
+
|
|
89
|
+
@staticmethod
|
|
90
|
+
def dtype_name(dtype: Any) -> str:
|
|
91
|
+
return _numpy_dtype_name(dtype)
|
|
92
|
+
|
|
93
|
+
@staticmethod
|
|
94
|
+
def stack(values: Sequence[Any], module: Any) -> Any:
|
|
95
|
+
return module.stack(tuple(values), axis=0)
|
|
96
|
+
|
|
97
|
+
@staticmethod
|
|
98
|
+
def cast(data: Any, dtype: Any, module: Any) -> Any:
|
|
99
|
+
return data.astype(dtype, copy=False)
|
|
100
|
+
|
|
101
|
+
@staticmethod
|
|
102
|
+
def logical_and(left: Any, right: Any, module: Any) -> Any:
|
|
103
|
+
return module.logical_and(left, right)
|
|
104
|
+
|
|
105
|
+
@staticmethod
|
|
106
|
+
def reshape(data: Any, shape: tuple[int, ...], module: Any) -> Any:
|
|
107
|
+
return data.reshape(shape)
|
|
108
|
+
|
|
109
|
+
@staticmethod
|
|
110
|
+
def broadcast_to(data: Any, shape: tuple[int, ...], module: Any) -> Any:
|
|
111
|
+
return module.broadcast_to(data, shape)
|
|
112
|
+
|
|
113
|
+
@staticmethod
|
|
114
|
+
def ones_like(reference: Any, shape: tuple[int, ...], dtype: Any, module: Any) -> Any:
|
|
115
|
+
return module.ones(shape, dtype=dtype)
|
|
116
|
+
|
|
117
|
+
|
|
118
|
+
class NumpyArrayOperations(ArrayOperations):
|
|
119
|
+
"""Numpy native operation leaves."""
|
|
120
|
+
|
|
121
|
+
@staticmethod
|
|
122
|
+
def to_numpy(data: Any, module: Any) -> Any:
|
|
123
|
+
del module
|
|
124
|
+
return data
|
|
125
|
+
|
|
126
|
+
@staticmethod
|
|
127
|
+
def from_numpy(data: Any, module: Any, device_id: int) -> Any:
|
|
128
|
+
del module, device_id
|
|
129
|
+
return data
|
|
130
|
+
|
|
131
|
+
@staticmethod
|
|
132
|
+
def scale_dtype(result: Any, target_dtype: Any, module: Any) -> Any:
|
|
133
|
+
if not hasattr(result, "dtype"):
|
|
134
|
+
return result
|
|
135
|
+
if not (
|
|
136
|
+
module.issubdtype(result.dtype, module.floating)
|
|
137
|
+
and module.issubdtype(target_dtype, module.integer)
|
|
138
|
+
):
|
|
139
|
+
return result.astype(target_dtype)
|
|
140
|
+
result_min = result.min()
|
|
141
|
+
result_max = result.max()
|
|
142
|
+
if result_max <= result_min:
|
|
143
|
+
return result.astype(target_dtype)
|
|
144
|
+
scaled = _scaled_values(result, result_min, result_max, target_dtype)
|
|
145
|
+
bounds = _clamp_bounds(target_dtype)
|
|
146
|
+
if bounds is not None:
|
|
147
|
+
scaled = module.clip(scaled, *bounds)
|
|
148
|
+
return scaled.astype(target_dtype)
|
|
149
|
+
|
|
150
|
+
|
|
151
|
+
class CupyArrayOperations(ArrayOperations):
|
|
152
|
+
"""Cupy native operation leaves."""
|
|
153
|
+
|
|
154
|
+
@staticmethod
|
|
155
|
+
def to_numpy(data: Any, module: Any) -> Any:
|
|
156
|
+
del module
|
|
157
|
+
return data.get()
|
|
158
|
+
|
|
159
|
+
@staticmethod
|
|
160
|
+
def from_numpy(data: Any, module: Any, device_id: int) -> Any:
|
|
161
|
+
del device_id
|
|
162
|
+
return module.array(data)
|
|
163
|
+
|
|
164
|
+
@staticmethod
|
|
165
|
+
def scale_dtype(result: Any, target_dtype: Any, module: Any) -> Any:
|
|
166
|
+
if not hasattr(result, "dtype"):
|
|
167
|
+
return result
|
|
168
|
+
if not (
|
|
169
|
+
module.issubdtype(result.dtype, module.floating)
|
|
170
|
+
and not module.issubdtype(target_dtype, module.floating)
|
|
171
|
+
):
|
|
172
|
+
return result.astype(target_dtype)
|
|
173
|
+
result_min = module.min(result)
|
|
174
|
+
result_max = module.max(result)
|
|
175
|
+
if result_max <= result_min:
|
|
176
|
+
return result.astype(target_dtype)
|
|
177
|
+
scaled = _scaled_values(result, result_min, result_max, target_dtype)
|
|
178
|
+
bounds = _clamp_bounds(target_dtype)
|
|
179
|
+
if bounds is not None:
|
|
180
|
+
scaled = module.clip(scaled, *bounds)
|
|
181
|
+
return scaled.astype(target_dtype)
|
|
182
|
+
|
|
183
|
+
|
|
184
|
+
class TorchArrayOperations(ArrayOperations):
|
|
185
|
+
"""Torch native operation leaves."""
|
|
186
|
+
|
|
187
|
+
@staticmethod
|
|
188
|
+
def to_numpy(data: Any, module: Any) -> Any:
|
|
189
|
+
del module
|
|
190
|
+
return data.cpu().numpy()
|
|
191
|
+
|
|
192
|
+
@staticmethod
|
|
193
|
+
def from_numpy(data: Any, module: Any, device_id: int) -> Any:
|
|
194
|
+
host_data = (
|
|
195
|
+
np.ascontiguousarray(data)
|
|
196
|
+
if any(stride < 0 for stride in getattr(data, "strides", ()))
|
|
197
|
+
else data
|
|
198
|
+
)
|
|
199
|
+
return module.from_numpy(host_data).to(f"cuda:{device_id}")
|
|
200
|
+
|
|
201
|
+
@staticmethod
|
|
202
|
+
def scale_dtype(result: Any, target_dtype: Any, module: Any) -> Any:
|
|
203
|
+
if not hasattr(result, "dtype"):
|
|
204
|
+
return result
|
|
205
|
+
mapped = _mapped_dtype(target_dtype, module)
|
|
206
|
+
floats = (module.float16, module.float32, module.float64)
|
|
207
|
+
if not (result.dtype in floats and np.issubdtype(np.dtype(target_dtype), np.integer)):
|
|
208
|
+
return result.to(mapped)
|
|
209
|
+
result_min = result.min()
|
|
210
|
+
result_max = result.max()
|
|
211
|
+
if result_max <= result_min:
|
|
212
|
+
return result.to(mapped)
|
|
213
|
+
scaled = _scaled_values(result, result_min, result_max, target_dtype)
|
|
214
|
+
bounds = _clamp_bounds(target_dtype)
|
|
215
|
+
if bounds is not None:
|
|
216
|
+
scaled = module.clamp(scaled, min=bounds[0], max=bounds[1])
|
|
217
|
+
return scaled.to(mapped)
|
|
218
|
+
|
|
219
|
+
@staticmethod
|
|
220
|
+
def stack(values: Sequence[Any], module: Any) -> Any:
|
|
221
|
+
return module.stack(tuple(values), dim=0)
|
|
222
|
+
|
|
223
|
+
@staticmethod
|
|
224
|
+
def cast(data: Any, dtype: Any, module: Any) -> Any:
|
|
225
|
+
return data.to(dtype=_mapped_dtype(dtype, module))
|
|
226
|
+
|
|
227
|
+
@staticmethod
|
|
228
|
+
def dtype_name(dtype: Any) -> str:
|
|
229
|
+
return str(dtype).rsplit(".", maxsplit=1)[-1]
|
|
230
|
+
|
|
231
|
+
@staticmethod
|
|
232
|
+
def broadcast_to(data: Any, shape: tuple[int, ...], module: Any) -> Any:
|
|
233
|
+
return data.expand(shape)
|
|
234
|
+
|
|
235
|
+
@staticmethod
|
|
236
|
+
def ones_like(reference: Any, shape: tuple[int, ...], dtype: Any, module: Any) -> Any:
|
|
237
|
+
return module.ones(shape, dtype=_mapped_dtype(dtype, module), device=reference.device)
|
|
238
|
+
|
|
239
|
+
|
|
240
|
+
class TensorflowArrayOperations(ArrayOperations):
|
|
241
|
+
"""Tensorflow native operation leaves."""
|
|
242
|
+
|
|
243
|
+
@staticmethod
|
|
244
|
+
def to_numpy(data: Any, module: Any) -> Any:
|
|
245
|
+
del module
|
|
246
|
+
return data.numpy()
|
|
247
|
+
|
|
248
|
+
@staticmethod
|
|
249
|
+
def from_numpy(data: Any, module: Any, device_id: int) -> Any:
|
|
250
|
+
del device_id
|
|
251
|
+
return module.convert_to_tensor(data)
|
|
252
|
+
|
|
253
|
+
@staticmethod
|
|
254
|
+
def scale_dtype(result: Any, target_dtype: Any, module: Any) -> Any:
|
|
255
|
+
if not hasattr(result, "dtype"):
|
|
256
|
+
return result
|
|
257
|
+
mapped = _mapped_dtype(target_dtype, module)
|
|
258
|
+
floats = (module.float16, module.float32, module.float64)
|
|
259
|
+
if not (result.dtype in floats and np.issubdtype(np.dtype(target_dtype), np.integer)):
|
|
260
|
+
return module.cast(result, mapped)
|
|
261
|
+
result_min = module.reduce_min(result)
|
|
262
|
+
result_max = module.reduce_max(result)
|
|
263
|
+
if result_max <= result_min:
|
|
264
|
+
return module.cast(result, mapped)
|
|
265
|
+
scaled = _scaled_values(result, result_min, result_max, target_dtype)
|
|
266
|
+
bounds = _clamp_bounds(target_dtype)
|
|
267
|
+
if bounds is not None:
|
|
268
|
+
scaled = module.clip_by_value(scaled, *bounds)
|
|
269
|
+
return module.cast(scaled, mapped)
|
|
270
|
+
|
|
271
|
+
@staticmethod
|
|
272
|
+
def cast(data: Any, dtype: Any, module: Any) -> Any:
|
|
273
|
+
return module.cast(data, _mapped_dtype(dtype, module))
|
|
274
|
+
|
|
275
|
+
@staticmethod
|
|
276
|
+
def reshape(data: Any, shape: tuple[int, ...], module: Any) -> Any:
|
|
277
|
+
return module.reshape(data, shape)
|
|
278
|
+
|
|
279
|
+
@staticmethod
|
|
280
|
+
def ones_like(reference: Any, shape: tuple[int, ...], dtype: Any, module: Any) -> Any:
|
|
281
|
+
with module.device(reference.device):
|
|
282
|
+
return module.ones(shape, dtype=_mapped_dtype(dtype, module))
|
|
283
|
+
|
|
284
|
+
|
|
285
|
+
class JaxArrayOperations(ArrayOperations):
|
|
286
|
+
"""Jax native operation leaves."""
|
|
287
|
+
|
|
288
|
+
@staticmethod
|
|
289
|
+
def to_numpy(data: Any, module: Any) -> Any:
|
|
290
|
+
del module
|
|
291
|
+
return np.asarray(data)
|
|
292
|
+
|
|
293
|
+
@staticmethod
|
|
294
|
+
def from_numpy(data: Any, module: Any, device_id: int) -> Any:
|
|
295
|
+
devices = tuple(device for device in module.devices() if device.platform == "gpu")
|
|
296
|
+
return module.device_put(data, devices[device_id])
|
|
297
|
+
|
|
298
|
+
@staticmethod
|
|
299
|
+
def scale_dtype(result: Any, target_dtype: Any, module: Any) -> Any:
|
|
300
|
+
if not hasattr(result, "dtype"):
|
|
301
|
+
return result
|
|
302
|
+
if np.dtype(target_dtype) == np.dtype(np.float64):
|
|
303
|
+
x64_enabled = getattr(module.config, "x64_enabled", None)
|
|
304
|
+
if x64_enabled is None:
|
|
305
|
+
x64_enabled = module.config.read("jax_enable_x64")
|
|
306
|
+
if not x64_enabled:
|
|
307
|
+
raise ValueError(
|
|
308
|
+
"JAX float64 output requires x64 mode; set JAX_ENABLE_X64=true before import"
|
|
309
|
+
)
|
|
310
|
+
jnp = module.numpy
|
|
311
|
+
mapped = _mapped_dtype(target_dtype, jnp)
|
|
312
|
+
floats = (jnp.float16, jnp.float32, jnp.float64)
|
|
313
|
+
if not (result.dtype in floats and np.issubdtype(np.dtype(target_dtype), np.integer)):
|
|
314
|
+
return result.astype(mapped)
|
|
315
|
+
result_min = jnp.min(result)
|
|
316
|
+
result_max = jnp.max(result)
|
|
317
|
+
if result_max <= result_min:
|
|
318
|
+
return result.astype(mapped)
|
|
319
|
+
scaled = _scaled_values(result, result_min, result_max, target_dtype)
|
|
320
|
+
bounds = _clamp_bounds(target_dtype)
|
|
321
|
+
if bounds is not None:
|
|
322
|
+
scaled = jnp.clip(scaled, *bounds)
|
|
323
|
+
return scaled.astype(mapped)
|
|
324
|
+
|
|
325
|
+
@staticmethod
|
|
326
|
+
def stack(values: Sequence[Any], module: Any) -> Any:
|
|
327
|
+
return module.numpy.stack(tuple(values), axis=0)
|
|
328
|
+
|
|
329
|
+
@staticmethod
|
|
330
|
+
def cast(data: Any, dtype: Any, module: Any) -> Any:
|
|
331
|
+
return data.astype(_mapped_dtype(dtype, module.numpy))
|
|
332
|
+
|
|
333
|
+
@staticmethod
|
|
334
|
+
def logical_and(left: Any, right: Any, module: Any) -> Any:
|
|
335
|
+
return module.numpy.logical_and(left, right)
|
|
336
|
+
|
|
337
|
+
@staticmethod
|
|
338
|
+
def broadcast_to(data: Any, shape: tuple[int, ...], module: Any) -> Any:
|
|
339
|
+
return module.numpy.broadcast_to(data, shape)
|
|
340
|
+
|
|
341
|
+
@staticmethod
|
|
342
|
+
def ones_like(reference: Any, shape: tuple[int, ...], dtype: Any, module: Any) -> Any:
|
|
343
|
+
return module.numpy.ones(shape, dtype=dtype, device=reference.device)
|
|
344
|
+
|
|
345
|
+
|
|
346
|
+
class PyclesperantoArrayOperations(ArrayOperations):
|
|
347
|
+
"""Pyclesperanto native operation leaves."""
|
|
348
|
+
|
|
349
|
+
@staticmethod
|
|
350
|
+
def to_numpy(data: Any, module: Any) -> Any:
|
|
351
|
+
return module.pull(data)
|
|
352
|
+
|
|
353
|
+
@staticmethod
|
|
354
|
+
def from_numpy(data: Any, module: Any, device_id: int) -> Any:
|
|
355
|
+
del device_id
|
|
356
|
+
return module.push(data)
|
|
357
|
+
|
|
358
|
+
@staticmethod
|
|
359
|
+
def scale_dtype(result: Any, target_dtype: Any, module: Any) -> Any:
|
|
360
|
+
if not hasattr(result, "dtype"):
|
|
361
|
+
return result
|
|
362
|
+
target_is_int = np.issubdtype(np.dtype(target_dtype), np.integer)
|
|
363
|
+
if not (np.issubdtype(result.dtype, np.floating) and target_is_int):
|
|
364
|
+
return module.push(module.pull(result).astype(target_dtype))
|
|
365
|
+
result_min = float(module.minimum_of_all_pixels(result))
|
|
366
|
+
result_max = float(module.maximum_of_all_pixels(result))
|
|
367
|
+
if result_max <= result_min:
|
|
368
|
+
return module.push(module.pull(result).astype(target_dtype))
|
|
369
|
+
normalized = module.subtract_image_from_scalar(result, scalar=result_min)
|
|
370
|
+
normalized = module.multiply_image_and_scalar(
|
|
371
|
+
normalized,
|
|
372
|
+
scalar=1.0 / (result_max - result_min),
|
|
373
|
+
)
|
|
374
|
+
range_info = _SCALING_RANGES.get(_dtype_name(target_dtype))
|
|
375
|
+
if isinstance(range_info, tuple):
|
|
376
|
+
scale, offset = range_info
|
|
377
|
+
scaled = module.multiply_image_and_scalar(normalized, scalar=scale)
|
|
378
|
+
scaled = module.subtract_image_from_scalar(scaled, scalar=offset)
|
|
379
|
+
elif range_info is not None:
|
|
380
|
+
scaled = module.multiply_image_and_scalar(normalized, scalar=range_info)
|
|
381
|
+
else:
|
|
382
|
+
scaled = normalized
|
|
383
|
+
host_values = module.pull(scaled)
|
|
384
|
+
bounds = _clamp_bounds(target_dtype)
|
|
385
|
+
if bounds is not None:
|
|
386
|
+
host_values = np.clip(host_values, *bounds)
|
|
387
|
+
return module.push(host_values.astype(target_dtype))
|
|
388
|
+
|
|
389
|
+
@staticmethod
|
|
390
|
+
def stack(values: Sequence[Any], module: Any) -> Any:
|
|
391
|
+
if not values:
|
|
392
|
+
raise ValueError("Cannot stack an empty pyclesperanto sequence")
|
|
393
|
+
if len(values) == 1:
|
|
394
|
+
source = values[0]
|
|
395
|
+
result = module.create((1, *source.shape), dtype=source.dtype)
|
|
396
|
+
return module.copy_slice(source, result, 0)
|
|
397
|
+
result = values[0]
|
|
398
|
+
for value in values[1:]:
|
|
399
|
+
result = module.concatenate_along_z(result, value)
|
|
400
|
+
return result
|
|
401
|
+
|
|
402
|
+
@staticmethod
|
|
403
|
+
def cast(data: Any, dtype: Any, module: Any) -> Any:
|
|
404
|
+
return module.push(module.pull(data).astype(dtype, copy=False))
|
|
405
|
+
|
|
406
|
+
@staticmethod
|
|
407
|
+
def logical_and(left: Any, right: Any, module: Any) -> Any:
|
|
408
|
+
return module.push(np.logical_and(module.pull(left), module.pull(right)))
|
|
409
|
+
|
|
410
|
+
@staticmethod
|
|
411
|
+
def reshape(data: Any, shape: tuple[int, ...], module: Any) -> Any:
|
|
412
|
+
if tuple(data.shape) == shape:
|
|
413
|
+
return data
|
|
414
|
+
raise NotImplementedError(
|
|
415
|
+
"pyclesperanto does not provide native array reshape; its reshape downloads pixels"
|
|
416
|
+
)
|
|
417
|
+
|
|
418
|
+
@staticmethod
|
|
419
|
+
def broadcast_to(data: Any, shape: tuple[int, ...], module: Any) -> Any:
|
|
420
|
+
if tuple(data.shape) == shape:
|
|
421
|
+
return data
|
|
422
|
+
raise NotImplementedError("pyclesperanto does not provide native array broadcasting")
|
|
423
|
+
|
|
424
|
+
@staticmethod
|
|
425
|
+
def ones_like(reference: Any, shape: tuple[int, ...], dtype: Any, module: Any) -> Any:
|
|
426
|
+
if np.dtype(dtype).name not in {
|
|
427
|
+
"float32",
|
|
428
|
+
"int8",
|
|
429
|
+
"int16",
|
|
430
|
+
"int32",
|
|
431
|
+
"uint8",
|
|
432
|
+
"uint16",
|
|
433
|
+
"uint32",
|
|
434
|
+
}:
|
|
435
|
+
raise NotImplementedError(f"pyclesperanto cannot allocate native dtype {dtype!r}")
|
|
436
|
+
if not 1 <= len(shape) <= 3:
|
|
437
|
+
raise NotImplementedError(
|
|
438
|
+
"pyclesperanto native allocation requires one to three dimensions"
|
|
439
|
+
)
|
|
440
|
+
result = module.create(shape, dtype=dtype, device=reference.device)
|
|
441
|
+
module.set(result, scalar=1, device=reference.device)
|
|
442
|
+
return result
|
|
443
|
+
|
|
444
|
+
|
|
445
|
+
NUMPY_OPERATIONS = NumpyArrayOperations()
|
|
446
|
+
CUPY_OPERATIONS = CupyArrayOperations()
|
|
447
|
+
TORCH_OPERATIONS = TorchArrayOperations()
|
|
448
|
+
TENSORFLOW_OPERATIONS = TensorflowArrayOperations()
|
|
449
|
+
JAX_OPERATIONS = JaxArrayOperations()
|
|
450
|
+
PYCLESPERANTO_OPERATIONS = PyclesperantoArrayOperations()
|