Built-in low-precision types

Pychop ships three ready-made classes that automatically chop to the desired format after every arithmetic operation:

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: object

Chopped-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: Tensor

A 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).

to_regular()[source]

Return a base torch.Tensor view (drops CPTensor subclass).

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: ndarray

A 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.

to_regular()[source]

Return as a regular NumPy ndarray (drops CPArray subclass).

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: object

A 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
to_regular()[source]

View as a plain jax.Array.

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!