Near-equality and tolerance arithmetic for floating-point numbers
Project description
Neon
Near-equality and tolerance arithmetic for floating-point numbers.
The Problem
Floating-point arithmetic is broken by design. Everyone ships these bugs:
# Bug: floating-point comparison
>>> 0.1 + 0.2 == 0.3
False # Wrong! Should be True.
# Bug: near-zero comparison
>>> 1e-16 == 0
False # Maybe should be "close enough"?
# Bug: unsafe division
>>> x / y
ZeroDivisionError # What if y is zero or near-zero?
# Bug: naive tolerance breaks for large numbers
>>> abs(1000000.0 - 1000000.1) < 0.0001
True # Wrong! This is a 0.00001% difference, not "near equal"
Neon handles these correctly:
from neon import compare, safe, clamp, ulp, inspect
compare.near(0.1 + 0.2, 0.3) # → True
compare.near_zero(1e-16) # → True
safe.div(1, 0, default=0.0) # → 0.0
clamp.to_zero(1e-15) # → 0.0
ulp.diff(1.0, ulp.next(1.0)) # → 1 (one ULP apart)
inspect.check(float('nan')) # → "Value is NaN - invalid calculation result" ✨ v1.1.0
Installation
pip install elemental-neon
Zero dependencies — uses only the Python standard library.
When to Use Neon
Use neon when:
- You need correct floating-point comparisons (within tolerance)
- You're debugging NaN/Inf issues in production or validating FP8/FP16 quantization ✨ v1.1.0
- You're doing safe division that handles zero gracefully
- You want to snap near-zero values to exactly zero
- You need ULP-based comparisons for precise float arithmetic
- You're building numerical algorithms, financial calculations, or scientific computing
- You want improved summation precision with math.fsum()
- You need to process batches of floats with safe operations ✨ v1.1.0
Don't use neon when:
- You need arbitrary-precision arithmetic — use
decimal.Decimal - You need symbolic math — use
sympy - You need interval arithmetic — use
mpmath - Standard
==comparison is actually what you want (rare for floats)
API Reference
neon.inspect ✨ New in v1.1.0
Production debugging tools for floating-point issues and low-precision dtype validation.
| Function | Description |
|---|---|
check(x) |
Quick health check, returns warning or None |
check_many(values) |
Batch health summary with risk assessment |
compare_debug(a, b) |
Explains why floats differ + recommends fix |
div_debug(a, b) |
Debugs division issues |
analyze(values) |
Comprehensive analysis with recommendations |
precision_loss(got, expected) |
Detects precision loss |
safe_for_dtype(x, target) |
Check if value safe for target dtype |
analyze_for_dtype(values, target) |
Batch analysis for dtype conversion |
compare_dtypes(values, targets) |
Compare safety across multiple dtypes |
Supported dtypes: fp32, fp16, bf16, fp8_e4m3, fp8_e5m2
from neon import inspect as ni
# Quick health check
if issue := ni.check(result):
print(f"Problem: {issue}")
# → "Value is NaN - invalid calculation result"
# Debug why floats differ
ni.compare_debug(0.1 + 0.2, 0.3)
# → "Values differ by 5.551115123e-17 (1 ULP). Use neon.compare.near() for tolerance comparison."
# Debug division issues
ni.div_debug(1.0, 0.0)
# → "Division by zero detected. Use neon.safe.div(a, b, default=...) to handle gracefully."
# Analyze batch of values
report = ni.analyze([1.0, 5e-324, float('nan'), 2.0])
print(report)
# → Analysis of 4 values:
# Normal: 2 (50.0%)
# Denormal: 1 (25.0%)
# NaN: 1 (25.0%)
# Precision Risk: HIGH
# Validate FP8 quantization before converting model
weights = model.get_weights().flatten().tolist()
report = ni.analyze_for_dtype(weights, target='fp8_e4m3')
if report.overflow > 0:
print(f"WARNING: {report.recommendation}")
# → "WARNING: 15% of values overflow FP8 range (max ±448). Consider clipping."
# Compare multiple dtypes
comparison = ni.compare_dtypes(activations, targets=['fp16', 'bf16', 'fp8_e4m3'])
print(comparison.recommendation)
# → "Use BF16 for best balance (0% overflow, LOW precision risk)"
Use cases:
- Debug NaN/Inf in production calculations
- Understand precision loss in numerical algorithms
- Validate model weights before FP8/FP16 quantization
- Post-mortem analysis of training failures
neon.compare
Comparison functions for approximate equality.
| Function | Description |
|---|---|
near(a, b, *, rel_tol=1e-9, abs_tol=1e-9) |
True if a and b are approximately equal |
near_zero(x, *, abs_tol=1e-9) |
True if x is approximately zero |
less_or_near(a, b, *, rel_tol, abs_tol) |
True if a < b or a ≈ b |
greater_or_near(a, b, *, rel_tol, abs_tol) |
True if a > b or a ≈ b |
compare(a, b, *, rel_tol, abs_tol) |
Returns -1, 0, or 1 (spaceship operator) |
all_near(pairs, *, rel_tol, abs_tol) |
True if all pairs are near |
is_integer(x, *, abs_tol=1e-9) |
True if x is near an integer |
near_many(pairs, *, rel_tol, abs_tol) |
Batch comparison |
near_zero_many(values, *, abs_tol) |
Batch near-zero check ✨ v1.1.0 |
is_integer_many(values, *, abs_tol) |
Batch integer check ✨ v1.1.0 |
from neon import compare
compare.near(0.1 + 0.2, 0.3) # → True
compare.near(1.0, 1.001, rel_tol=1e-2) # → True
compare.near(1.0, 1.001, rel_tol=1e-4) # → False
compare.near_zero(1e-15) # → True
compare.near_zero(1e-5) # → False
compare.is_integer(3.0000000001) # → True
compare.is_integer(3.1) # → False
compare.compare(1.0, 1.0 + 1e-12) # → 0 (near)
compare.compare(1.0, 2.0) # → -1 (less)
compare.compare(2.0, 1.0) # → 1 (greater)
# Batch operations
pairs = [(0.1 + 0.2, 0.3), (1.0, 2.0)]
compare.near_many(pairs) # → [True, False]
compare.all_near([(1.0, 1.0), (2.0, 2.0)]) # → True
Special value semantics:
near(nan, nan)→False(NaN is not near anything, including itself)near(inf, inf)→True(same infinity)near(-inf, inf)→Falsenear(inf, 1e308)→False(infinity is not near any finite number)near(0.0, -0.0)→True
neon.clamp
Clamping and snapping functions.
| Function | Description |
|---|---|
to_zero(x, *, abs_tol=1e-9) |
Snap to 0.0 if near zero |
to_int(x, *, abs_tol=1e-9) |
Snap to nearest int if near it |
to_value(x, target, *, rel_tol, abs_tol) |
Snap to target if near it |
to_range(x, lo, hi) |
Clamp x to [lo, hi] |
to_values(x, targets, *, rel_tol, abs_tol) |
Snap to nearest target |
to_zero_many(values, *, abs_tol) |
Batch snap to zero |
to_int_many(values, *, abs_tol) |
Batch snap to integer ✨ v1.1.0 |
to_range_many(values, lo, hi) |
Batch clamp to range ✨ v1.1.0 |
from neon import clamp
clamp.to_zero(1e-15) # → 0.0
clamp.to_zero(0.1) # → 0.1
clamp.to_int(2.9999999999) # → 3.0 (float, not int)
clamp.to_int(2.5) # → 2.5
clamp.to_value(0.333333333, 1/3) # → 0.3333333333333333 (exact 1/3)
clamp.to_range(5, 0, 10) # → 5
clamp.to_range(-5, 0, 10) # → 0
clamp.to_range(15, 0, 10) # → 10
clamp.to_values(0.499999999, [0.0, 0.5, 1.0]) # → 0.5
clamp.to_values(0.3, [0.0, 0.5, 1.0]) # → 0.3 (not near any)
neon.safe
Safe arithmetic with graceful edge case handling.
| Function | Description |
|---|---|
div(a, b, *, default=None, zero_tol=0.0) |
Safe division |
div_or_zero(a, b, *, zero_tol=0.0) |
Safe division, returns 0.0 on zero |
div_or_inf(a, b, *, zero_tol=0.0) |
Safe division, returns ±inf on zero |
mod(a, b, *, default=None, zero_tol=0.0) |
Safe modulo |
sqrt(x, *, default=None) |
Safe sqrt, handles negative |
log(x, *, base=None, default=None) |
Safe log, handles non-positive |
pow(base, exp, *, default=None) |
Safe power, handles edge cases |
sum_exact(values) |
Precise summation using math.fsum() |
mean_exact(values) |
Mean using math.fsum() |
div_many(a_values, b_values, *, default, zero_tol) |
Batch safe division ✨ v1.1.0 |
sqrt_many(values, *, default) |
Batch safe sqrt ✨ v1.1.0 |
log_many(values, *, base, default) |
Batch safe log ✨ v1.1.0 |
pow_many(bases, exps, *, default) |
Batch safe power ✨ v1.1.0 |
from neon import safe
safe.div(6, 3) # → 2.0
safe.div(1, 0) # → None
safe.div(1, 0, default=0.0) # → 0.0
safe.div_or_zero(1, 0) # → 0.0
safe.div_or_inf(1, 0) # → inf
safe.div_or_inf(-1, 0) # → -inf
safe.div(1, 1e-15, zero_tol=1e-10) # → None (b is "near zero")
safe.sqrt(4) # → 2.0
safe.sqrt(-1) # → None
safe.sqrt(-1, default=0.0) # → 0.0
safe.log(0) # → None
safe.log(100, base=10) # → 2.0
# Kahan summation for precision
values = [1e16, 1.0, -1e16]
sum(values) # → 0.0 (wrong! lost precision)
safe.sum_exact(values) # → 1.0 (correct)
safe.sum_exact([0.1] * 10) # → 1.0 (precise)
neon.ulp
ULP (Unit in the Last Place) operations — the distance between adjacent floats.
| Function | Description |
|---|---|
of(x) |
Returns the ULP of x |
diff(a, b) |
Distance in ULPs between a and b |
within(a, b, *, max_ulps=4) |
True if within max_ulps |
next(x) |
Next representable float above x |
prev(x) |
Next representable float below x |
add(x, n) |
Move n ULPs from x |
of_many(values) |
Batch ULP calculation ✨ v1.1.0 |
diff_many(a_values, b_values) |
Batch ULP distance ✨ v1.1.0 |
within_many(a_values, b_values, *, max_ulps) |
Batch ULP comparison ✨ v1.1.0 |
from neon import ulp
ulp.of(1.0) # → 2.220446049250313e-16
ulp.of(1e10) # → 1.9073486328125e-06
ulp.of(0.0) # → 5e-324 (smallest denormal)
ulp.diff(1.0, 1.0 + 2.2e-16) # → 1 (one ULP)
ulp.within(1.0, 1.0 + 1e-15) # → True (within 4 ULPs)
ulp.within(1.0, 1.0001) # → False
ulp.next(1.0) # → 1.0000000000000002
ulp.prev(1.0) # → 0.9999999999999999
ulp.add(1.0, 10) # → 1.0 + 10 ULPs
# Round-trip
ulp.next(ulp.prev(1.0)) == 1.0 # → True
Exceptions
Neon provides a hierarchy of exceptions for precise error handling:
from neon import (
NeonError, # Base class for all neon errors
InvalidValueError, # NaN or invalid input
EmptyInputError, # Empty input where not allowed
)
# Catch specific errors
try:
ulp.of(float('nan'))
except InvalidValueError as e:
print(f"Invalid: {e.value}") # → Invalid: nan
# Or catch all neon errors
try:
safe.sum_exact([])
except NeonError:
print("Neon operation failed")
Cookbook
Validate user input with tolerance
from neon import compare
def validate_percentage(value: float) -> bool:
"""Check if value is approximately 0-100."""
return compare.less_or_near(0, value) and compare.less_or_near(value, 100)
validate_percentage(99.9999999999) # → True
validate_percentage(100.0000000001) # → True
validate_percentage(100.1) # → False
Clean up near-zero values in data
from neon import clamp
# Remove floating-point noise from calculations
data = [0.0, 1e-16, 0.5, -1e-15, 1.0]
cleaned = clamp.to_zero_many(data)
# → [0.0, 0.0, 0.5, 0.0, 1.0]
Safe division in financial calculations
from neon import safe
def calculate_roi(profit: float, investment: float) -> float | None:
"""Calculate return on investment, handling zero investment."""
return safe.div(profit, investment, default=None)
calculate_roi(1000, 5000) # → 0.2 (20% ROI)
calculate_roi(1000, 0) # → None (can't divide by zero)
Snap to grid values in UI
from neon import clamp
def snap_to_grid(x: float, grid_values: list[float]) -> float:
"""Snap x to nearest grid line if close enough."""
return clamp.to_values(x, grid_values, rel_tol=0.01)
snap_to_grid(0.505, [0.0, 0.5, 1.0]) # → 0.5 (snapped)
snap_to_grid(0.6, [0.0, 0.5, 1.0]) # → 0.6 (not near any grid)
Precise summation for accounting
from neon import safe
# Adding many small currency amounts
transactions = [0.01, 0.01, 0.01] * 100 # 300 pennies
# Naive sum loses precision
naive_total = sum(transactions) # → 2.9999999999999982 (wrong!)
# Kahan summation preserves it
exact_total = safe.sum_exact(transactions) # → 3.0 (correct)
Check if floats are "close enough" with ULPs
from neon import ulp
# Sometimes you need ULP-level precision
a = 1.0
b = ulp.add(a, 2) # Exactly 2 ULPs away
ulp.within(a, b, max_ulps=4) # → True
ulp.within(a, b, max_ulps=1) # → False
Why Neon?
Floating-point comparison is hard. Everyone gets it wrong:
# The bug everyone ships
>>> 0.1 + 0.2 == 0.3
False # Wrong! They're "close enough"
>>> 1e-16 == 0
False # Maybe should be True?
>>> abs(a - b) < 0.0001
# Breaks for large numbers! 1000000 vs 1000000.1 would be "near"
What Neon does differently
- Zero dependencies — stdlib only
- Pure functions — no state, easy to test
- Explicit tolerances — no magic defaults
- IEEE 754 aware — proper NaN, inf, denormal handling
- Fail loudly — raises exceptions for invalid inputs
- math.fsum() — improved precision for sums using Python's C implementation
- ULP operations — direct access to float representation
Math Model
Neon uses relative and absolute tolerance for comparisons, following the same algorithm as Python's math.isclose():
abs(a - b) <= max(rel_tol * max(abs(a), abs(b)), abs_tol)
- Relative tolerance (
rel_tol): Scales with magnitude (good for large numbers) - Absolute tolerance (
abs_tol): Fixed threshold (good for near-zero)
For ULP operations, Neon uses math.nextafter() (Python 3.9+) to manipulate the actual float representation.
For summation, Neon uses math.fsum() (Python's C-optimized compensated summation) to reduce floating-point error accumulation.
Use Cases
- Floating-point comparisons with tolerance
- Safe division and arithmetic
- Cleaning near-zero values from data
- Snapping values to targets (UI grids, rounding)
- ULP-based precision testing
- Improved summation precision
Performance
Neon is designed for low-latency float operations (benchmarked on Python 3.12, M1 Mac):
compare.near(): ~0.15µs (~6.7M ops/sec)clamp.to_zero(): ~0.12µs (~8.3M ops/sec)safe.div(): ~0.18µs (~5.6M ops/sec)ulp.of(): ~0.25µs (~4.0M ops/sec)
Batch operations (*_many() functions) provide ~100x overhead reduction for lists of 100+ values.
Not optimized for: Vectorized operations on millions of floats. For that, use NumPy.
Numerical Precision
Neon uses standard Python float (IEEE 754 double precision):
- Precision: ~15-17 significant decimal digits
- Range: ~±1.8e308
- Smallest positive: ~5e-324 (denormal)
Edge cases:
NaNandInfinityinputs are handled explicitly- ULP operations work correctly near zero (denormals)
- math.fsum() reduces but doesn't eliminate all rounding errors
- Tolerances default to
rel_tol=1e-9, abs_tol=1e-9(better than math.isclose's abs_tol=0.0)
Not suitable for: Applications requiring arbitrary precision or guaranteed decimal accuracy (use decimal.Decimal).
What Neon Does NOT Do
To set clear expectations:
- ❌ Arbitrary precision — use
decimal.Decimalormpmath - ❌ Symbolic math — use
sympy - ❌ Interval arithmetic — use
mpmath.iv - ❌ Fast vectorized operations — use NumPy
- ❌ Complex numbers — use Python's
complexor NumPy - ❌ Unit handling — use
pintorastropy.units
Neon handles tolerance arithmetic for floats. For other numerical needs, use specialized libraries.
License
MIT
Project details
Release history Release notifications | RSS feed
Download files
Download the file for your platform. If you're not sure which to choose, learn more about installing packages.
Source Distribution
Built Distribution
Filter files by name, interpreter, ABI, and platform.
If you're not sure about the file name format, learn more about wheel file names.
Copy a direct link to the current filters
File details
Details for the file elemental_neon-1.1.0.tar.gz.
File metadata
- Download URL: elemental_neon-1.1.0.tar.gz
- Upload date:
- Size: 74.7 kB
- Tags: Source
- Uploaded using Trusted Publishing? No
- Uploaded via: twine/6.2.0 CPython/3.9.6
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
120abeab86cb59b6371bddce07a82871dd06843e0bc9f218c8143950769caaa6
|
|
| MD5 |
5d01f9e5c6e161c42eb16ba12f09343e
|
|
| BLAKE2b-256 |
93aadb4fd14f8e2056d3257b0f8f9611c3a25380a1a7030e8b127e1c806f8fbb
|
File details
Details for the file elemental_neon-1.1.0-py3-none-any.whl.
File metadata
- Download URL: elemental_neon-1.1.0-py3-none-any.whl
- Upload date:
- Size: 22.0 kB
- Tags: Python 3
- Uploaded using Trusted Publishing? No
- Uploaded via: twine/6.2.0 CPython/3.9.6
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
bc17e0174032fa5d45ee45b8a95f18c44e61c9e2ced30fe72ca8c6f591b511c4
|
|
| MD5 |
2c7512ae12aa14ff45a3a78211c84d26
|
|
| BLAKE2b-256 |
16ef465b8e26a96813756da4aee57fb6908d144107ed5fc14f91efaa1d385ef4
|