Built-in low-precision types¶
Pychop ships three ready-made classes that automatically chop to the
desired format after every arithmetic operation:
pychop.builtin.CPFloat– scalar (Pythonfloat-like)pychop.builtin.CPTensor–torch.Tensorsubclasspychop.builtin.CPArray–numpy.ndarraysubclass
All three work with any Chop (or FaultChop) instance and keep the
result inside the low-precision type.
Quick import¶
from pychop import Chop
from pychop.builtin import CPFloat, CPTensor, CPArray
pychop.backend('torch') # if using NumPy or JAX, switch to them correspondingly before the deployment
Common set-up¶
# Half-precision (IEEE-754 binary16) – subnormals enabled
half = Chop(exp_bits=5, sig_bits=10, subnormal=True, rmode=1)
# Under-flow-free half (tiny numbers become zero)
ufhalf = Chop(exp_bits=5, sig_bits=10, subnormal=False, rmode=1)
Scalar – CPFloat¶
- class pychop.builtin.CPFloat(value: int | float | complex | number, chopper: Any)[source]¶
Bases:
objectChopped-precision scalar.
Parameters¶
- valueint, float, complex, or numpy scalar
Input scalar value. It is chopped immediately at construction time.
- choppercallable
A Chop-like object. It must be callable and accept a backend-appropriate scalar container: - NumPy backend: expects a NumPy array/scalar - Torch backend: expects a torch.Tensor - JAX backend: expects a jax.Array
Attributes¶
- valuefloat
The chopped value stored as a Python scalar (typically float, but can be complex depending on backend and operation).
- choppercallable
The chopping/quantization operator.
Examples¶
Basic arithmetic stays chopped:
from pychop import Chop from pychop.builtin import CPFloat half = Chop(exp_bits=5, sig_bits=10, subnormal=True, rmode=1) a = CPFloat(1.234567, half) b = CPFloat(0.987654, half) c = a + b d = a * b / 2.0 - 0.1
NumPy ufunc interoperability:
import numpy as np x = CPFloat(1.234, half) y = np.sin(x) # -> CPFloat z = np.sqrt(x + 1.0) # -> CPFloat
Notes¶
Mixed-chopper operations are disallowed for CPFloat binary arithmetic and NumPy ufuncs.
For torch/jax specific functions (e.g., torch.sin), CPFloat will be coerced to a Python float unless you build explicit wrappers.
- chopper: Any¶
- item()[source]¶
Return the chopped value as a Python scalar.
Returns¶
- scalar
The stored chopped scalar.
- to_numpy(dtype=None) ndarray[source]¶
Convert to a NumPy 0-d array.
Parameters¶
- dtypenumpy dtype, default=None
Optional dtype to cast to.
Returns¶
- numpy.ndarray
A 0-d NumPy array containing the chopped value.
- value: Any¶
Example
PyTorch – CPTensor¶
- class pychop.builtin.CPTensor(input_tensor, chopper=None)[source]¶
Bases:
TensorA PyTorch tensor subclass that maintains chopped precision after torch ops.
Notes: - Most torch operations dispatch through __torch_function__ (Python-level).
We unwrap CPTensor -> base Tensor, call the original function, then chop and wrap outputs back to CPTensor.
This guarantees “chop after each torch.* call”, but NOT chopping internal steps inside fused kernels / LAPACK calls (similar to SciPy/JAX behavior).
Example
import torch
from pychop.builtin import CPTensor
pychop.backend('torch') # Switch to Torch backend
half = Chop(exp_bits=5, sig_bits=10, subnormal=True, rmode=1)
ufhalf = Chop(exp_bits=5, sig_bits=10, subnormal=False, rmode=1)
x = CPTensor(torch.tensor([1.1, 2.2, 3.3]), half)
y = CPTensor(torch.tensor([0.5, 1.5, 2.5]), half)
print(x) # CPTensor(tensor([1.1, 2.2, 3.3]), device=cpu, prec=half)
z = x + y
print(z) # CPTensor(tensor([1.6, 3.7, 5.8]), device=cpu, prec=half)
# broadcasting with a plain tensor
reg = torch.tensor([10.0, 20.0, 30.0])
w = x * reg
print(w) # CPTensor(tensor([11.0, 44.0, 99.0]), device=cpu, prec=half)
# matrix multiplication
A = CPTensor(torch.randn(4, 3), half)
B = CPTensor(torch.randn(3, 5), half)
C = A @ B
print(C.shape) # torch.Size([4, 5])
# GPU works out-of-the-box
if torch.cuda.is_available():
A = A.to('cuda')
B = B.to('cuda')
C = A @ B
print(C.device) # cuda:0
NumPy – CPArray¶
- class pychop.builtin.CPArray(input_array, chopper=None)[source]¶
Bases:
ndarrayA NumPy array subclass that maintains chopped precision after arithmetic ops.
Key behaviors: - Construction chops the input immediately. - NumPy ufuncs (+, -, *, /, etc.) are intercepted via __array_ufunc__:
compute on base ndarrays -> chop result -> wrap as CPArray.
- Matrix multiplication (@) is intercepted via __matmul__/__rmatmul__:
compute with np.matmul on base ndarrays -> wrap (constructor chops).
NumPy high-level functions (including np.linalg.*) are intercepted via __array_function__:
unwrap CPArray inputs to base ndarrays -> call func -> wrap numeric ndarray outputs back to CPArray and chop numeric scalar outputs.
Important safety notes: - __array_function__ MUST be conservative to avoid breaking NumPy internals
(printing/formatting, string/object dtypes, etc.).
We only chop/wrap numeric ndarrays (dtype.kind in “biufc”) and numeric scalars.
Example
import numpy as np
half = Chop(exp_bits=5, sig_bits=10, subnormal=True, rmode=1)
ufhalf = Chop(exp_bits=5, sig_bits=10, subnormal=False, rmode=1)
p = CPArray(np.array([10.0, 20.0, 30.0]), half)
q = CPArray(np.array([1.0, 2.0, 3.0]), half)
print(p) # CPArray([10. 20. 30.], prec=half)
r = p - q
print(r) # CPArray([ 9. 18. 27.], prec=half)
# element-wise with a normal ndarray
plain = np.array([0.5, 1.5, 2.5])
s = p * plain
print(s) # CPArray([ 5. 30. 75.], prec=half)
# linear-algebra (still chopped)
M = CPArray(np.random.rand(3, 4), half)
N = CPArray(np.random.rand(4, 2), half)
P = M @ N
print(P.shape) # (3, 2)
NumPy – CPJaxArray¶
- class pychop.builtin.CPJaxArray(input_array, chopper=None)[source]¶
Bases:
objectA JAX array wrapper that maintains chopped precision after arithmetic ops.
What it guarantees: - Binary ops implemented here (+, -, *, /, @, etc.) produce CPJaxArray and
chop the result after each op (via _from_result -> _safe_chop).
Can be passed into many JAX APIs because we implement __jax_array__.
What it does NOT guarantee: - JAX library algorithms (jnp.linalg.*, jax.scipy.*) will not “chop every
internal step” automatically. They will treat CPJaxArray as a JAX array (via __jax_array__), run in full precision, and return JAX arrays. Use chopwrap(…) below if you want chopped+wrapped outputs.
- property dtype¶
- property ndim¶
- property shape¶
- property size¶
Example
from pychop.builtin import CPJaxArray
pychop.backend('jax') # Switch to JAX backend
half = Chop(exp_bits=5, sig_bits=10, subnormal=True, rmode=1)
ufhalf = Chop(exp_bits=5, sig_bits=10, subnormal=False, rmode=1)
x = CPJaxArray(jnp.array([1.1, 2.2, 3.3]), half)
y = CPJaxArray(jnp.array([0.5, 1.5, 2.5]), half)
print(x) # CPJaxArray([1.1, 2.2, 3.3], prec=half)
z = x + y
print(z) # CPJaxArray([1.6, 3.7, 5.8], prec=half)
# broadcasting with a plain array
reg = jnp.array([10.0, 20.0, 30.0])
w = x * reg
print(w) # CPJaxArray([11.0, 44.0, 99.0], prec=half)
# matrix multiplication
gpu_devices = jax.devices('gpu')
if gpu_devices:
with jax.default_device(gpu_devices[0]):
A = CPJaxArray(jax.random.normal(jax.random.PRNGKey(0), (4, 3)), half)
B = CPJaxArray(jax.random.normal(jax.random.PRNGKey(1), (3, 5)), half)
C = A @ B
print(C.shape) # (4, 5)
# GPU works out-of-the-box (JAX auto-dispatches)
print(C.to_regular().devices()) # {CpuDevice()} or {CudaDevice()}
Under-flow-free (UF) formats¶
Just create a Chop with subnormal=False and pass it to any of the
three types:
uf = Chop(exp_bits=5, sig_bits=10, subnormal=False, rmode=1)
tiny = CPFloat(1e-40, uf) # becomes 0.0 (flushed)
print(tiny) # CPFloat(0.0, prec=uf)
huge = CPTensor(torch.tensor([1e30, 1e35]), uf)
print(huge) # CPTensor(tensor([1.0000e+30, inf]), ...)
Supported operations¶
All Python arithmetic operators (+ – * / // % **) and the usual
library functions are dispatched through the subclass machinery:
NumPy – any ufunc (
np.sin,np.exp,np.linalg.norm…)PyTorch – any
torch.*function (torch.nn.functional.relu,torch.matmul,torch.conv2d…)
The result is always chopped and returned as the same built-in type.
Note
Reductions (sum, mean, norm) return a scalar of the same
type when the input is a CPArray/CPTensor. If you need a plain
Python number, call .item() or float(...).
Pickling / serialization¶
All three classes implement __reduce_ex__ and can be pickled/unpickled
with the usual pickle module.
import pickle, io
buf = io.BytesIO()
pickle.dump(a, buf) # a is a CPFloat
buf.seek(0)
a2 = pickle.load(buf)
print(a2) # same value & chopper
Performance tip¶
Use the PyTorch backend (
pychop.backend('torch')) for GPU-accelerated chopping.Use the TensorFlow backend (
pychop.backend('tensorflow')) for TensorFlow/Keras workflows with STE-based gradient support.Use the NumPy backend (default) for pure-CPU workloads.
That’s it, simply drop the three imports into your code and you instantly get type-preserving low-precision arithmetic for scalars, NumPy arrays and PyTorch tensors!