Data in and out#
ExactRef holds data in its own arrays so that every value is exact and no input can change during a calculation. You rarely build them by hand: operations accept Python lists, NumPy arrays and PyTorch tensors directly. This page is for when you need control over how data gets in and comes out.
Arrays#
An er.Array is one flat byte buffer plus a dtype, a shape and a byte
order. It is immutable. Every operation turns its inputs into arrays, and
rounded, tolerance() and the other results are arrays too.
import exactref as er
# A 2x3 float32 array from nested Python lists
x = er.asarray([[1.0, 2.0, 3.0], [4.0, 5.0, 6.0]], "float32")
# Its repr names the dtype, the shape and the byte order
print("array:", repr(x))
# The shape, the number of elements, the number of dimensions and the bytes held
print("shape:", x.shape)
print("size:", x.size)
print("ndim:", x.ndim)
print("nbytes:", x.nbytes)
# The dtype is a format object; name is its name as a string
print("dtype:", x.dtype.name)
array: Array(float32, shape=(2, 3), byteorder='little')
shape: (2, 3)
size: 6
ndim: 2
nbytes: 24
dtype: float32
Importing with asarray#
er.asarray(obj, dtype=None, *, rounding=None, overflow=None) accepts:
Python values and nested lists or tuples. Without a
dtype, bools become bool, ints int64, floats float64 and complex numbers complex128, and a value that format cannot hold exactly raises.Fraction,FloatValueandComplexValuevalues need an explicitdtype.Objects with the buffer protocol whose element format is known: NumPy arrays,
array.array,memoryview, andbytes(as uint8).CPU tensors with
__dlpack__: PyTorch, JAX and NumPy, bfloat16 and float8 included. A PyTorch tensor's lazy conjugate and negative views are applied first, and a tensor that requires grad is detached. A GPU tensor must be copied to the CPU first.An
Array, returned as it is, or aMutableArray, as a frozen copy.
Data that carries its own format is imported exactly. Give a dtype to
convert: each value is rounded once, with rounding (default: to nearest,
ties to even) and overflow saying how.
# 0.1 rounded to the nearest float32 value, the default
nearest = er.asarray([0.1], "float32")
print("nearest:", nearest.tolist())
# 0.1 rounded toward zero, to the float32 value just below it
toward_zero = er.asarray([0.1], "float32", rounding=er.Rounding.TOWARD_ZERO)
print("toward zero:", toward_zero.tolist())
nearest: [0.10000000149011612]
toward zero: [0.09999999403953552]
The five roundings are er.Rounding.NEAREST_EVEN, NEAREST_AWAY,
TOWARD_ZERO, TOWARD_POSITIVE and TOWARD_NEGATIVE.
Overflow#
In a format with infinity, a value past the largest finite value becomes
infinity, as IEEE 754 says. overflow="saturate" gives the largest finite
value instead:
# 1e6 is past float16's largest finite value, so it becomes infinity
print("default:", er.asarray([1e6], "float16").tolist())
# Clamped to the largest finite float16 value
print("saturate:", er.asarray([1e6], "float16", overflow="saturate").tolist())
default: [inf]
saturate: [65504.0]
Formats without infinity (float8_e4m3fn and the fnuz formats) round
within the values they have: to the largest value when rounding to nearest,
toward zero or toward it, and to NaN when rounding outward.
overflow="nan" gives NaN wherever IEEE 754 would give infinity, as some
kernels do in these formats:
# float8_e4m3fn has no infinity: rounding to nearest gives its largest value
print("default:", er.asarray([1000.0], "float8_e4m3fn").tolist())
# NaN where IEEE 754 would give infinity
print("nan:", er.asarray([1000.0], "float8_e4m3fn", overflow="nan").tolist())
default: [448.0]
nan: [nan]
Integers follow PyTorch: converting array data wraps on overflow by default,
like tensor.to, and Python values that do not fit raise, like
torch.tensor. overflow="saturate" clamps, and overflow="error" raises.
import numpy as np
# An int16 NumPy array holding 300, past int8's largest value, 127
data = np.array([300], dtype=np.int16)
# Array data wraps around modulo 256: 300 - 256 = 44
print("wrap:", er.asarray(data, "int8").tolist())
# Clamped to int8's largest value
print("saturate:", er.asarray(data, "int8", overflow="saturate").tolist())
# A Python int that does not fit raises
try:
er.asarray([300], "int8")
except OverflowError as error:
print(f"{type(error).__name__}: {error}")
wrap: [44]
saturate: [127]
OverflowError: 300 is outside the int8 range [-128, 127]
Converting to bool reads every nonzero value, NaN included, as True, as PyTorch does:
# Zero becomes False; NaN and 2.0 become True
print("bool:", er.asarray([0.0, float("nan"), 2.0], "bool").tolist())
bool: [False, True, True]
Reading arrays#
# Nested lists of Python values
print("tolist:", x.tolist())
# Every element in one flat list, row by row
print("scalars:", x.scalars())
# One element, at row 1, column 2
print("scalar:", x.scalar((1, 2)))
# The same elements in a new 3x2 shape
print("reshaped:", x.reshape((3, 2)).tolist())
tolist: [[1.0, 2.0, 3.0], [4.0, 5.0, 6.0]]
scalars: [1.0, 2.0, 3.0, 4.0, 5.0, 6.0]
scalar: 6.0
reshaped: [[1.0, 2.0], [3.0, 4.0], [5.0, 6.0]]
Python floats lose the difference between some values that matter here. For those, read the raw encoding or the exact value:
# A 2x3 bfloat16 array from raw bits: five zeros, then 0x3F80, which is 1.0
a = er.Array.from_encodings("bfloat16", (2, 3), [0, 0, 0, 0, 0, 0x3F80])
# Element (1, 2) is the sixth, and each takes 2 bytes, so it starts at byte 10
print("byte offset:", a.byte_offset((1, 2)))
# Its raw bits as an unsigned int: 0x3F80 is 16256
print("encoding:", a.encoding((1, 2)))
# Its exact value, as a FloatValue: significand * 2**exponent
print("value:", repr(a.value((1, 2))))
# Negative zero keeps its sign
print("-0.0:", repr(er.asarray([-0.0]).value((0,))))
# Infinity is a kind of its own
print("inf:", repr(er.asarray([float("inf")]).value((0,))))
byte offset: 10
encoding: 16256
value: FloatValue(kind='finite', negative=False, significand=1, exponent=0)
-0.0: FloatValue(kind='finite', negative=True, significand=0, exponent=0)
inf: FloatValue(kind='infinite', negative=False, significand=0, exponent=0)
encodings() gives every element's raw bits and values() every element's
exact value, row-major. A FloatValue is
the exact value of one encoding: (-1)**negative * significand * 2**exponent when finite, with signed zeros, infinities and NaN kept.
to_fraction() gives it as a Fraction. A complex element's value is a
ComplexValue, a pair of FloatValue parts.
# The exact value of 0.1 rounded to float32
v = er.asarray([0.1], "float32").value((0,))
# The same value as a Fraction, 13421773 / 2**27
print("fraction:", repr(v.to_fraction()))
fraction: Fraction(13421773, 134217728)
Bool arrays, such as the ones covers() returns, have all() and any().
a.identical(b) is True when two arrays have the same dtype, shape and
encodings, whatever their byte order.
Building arrays#
er.Array.zeros(dtype, shape), er.Array.from_values(dtype, shape, values)
and er.Array.from_encodings(dtype, shape, encodings) build an array
directly. To edit one, take a MutableArray and freeze it when done:
# A mutable copy of an array of three float32 zeros
m = er.Array.zeros("float32", (3,)).mutable_copy()
# Store 0.1 at index 1, rounded to float32
m.set_value((1,), 0.1)
# freeze() returns an immutable copy
print("frozen:", m.freeze().tolist())
frozen: [0.0, 0.10000000149011612, 0.0]
set_value rounds the value to the array's format (it takes rounding and
overflow), and set_encoding stores raw bits. set_values,
set_encodings and set_bytes replace everything at once. To reinterpret raw
bytes as an array, use er.Array(dtype, shape, buffer).
Formats#
er.FORMATS maps each format's name to its format object, and
er.get_format(name) looks one up:
import textwrap
# Every format name, sorted and wrapped at 72 characters
print("formats:")
print(textwrap.fill(" ".join(sorted(er.FORMATS)), 72))
# The float32 format object
f = er.get_format("float32")
# Significand bits, counting the leading bit that is not stored
print("precision:", f.precision)
# The smallest normal value is 2**emin
print("emin:", f.emin)
# The largest finite value lies between 2**emax and 2**(emax + 1)
print("emax:", f.emax)
formats:
bfloat16 bool complex128 complex32 complex64 float16 float32 float64
float8_e3m4 float8_e4m3 float8_e4m3b11fnuz float8_e4m3fn float8_e4m3fnuz
float8_e5m2 float8_e5m2fnuz int16 int32 int64 int8 uint16 uint32 uint64
uint8
precision: 24
emin: -126
emax: 127
Formats are also available by name, such as er.bfloat16 and
er.float8_e4m3fn, and every dtype argument takes either a format or its
name. Formats and rounding lists each format's
range and special values.