Rational Conversion Guide¶
This guide covers the LXRationalConverter for converting floating-point coefficients to rational numbers, essential for integer-only solvers and exact arithmetic.
Overview¶
Many optimization solvers, particularly integer-only modes of solvers like GLPK, require exact rational arithmetic instead of floating-point numbers. The LXRationalConverter provides efficient algorithms to approximate floats as fractions with configurable precision.
Why Rational Conversion?
Integer-only solvers (GLPK, some CPLEX modes) require rational coefficients
Avoid floating-point precision errors in sensitive problems
Enable symbolic computation integration
Ensure reproducible results across platforms
Key Features:
Three approximation algorithms (Farey, Continued Fraction, Stern-Brocot)
Configurable maximum denominator for precision control
Batch conversion for coefficient dictionaries
Error tracking and algorithm comparison
Tolerance handling for float comparisons
Quick Start¶
Basic Conversion¶
from lumix.utils import LXRationalConverter
# Create converter
converter = LXRationalConverter(max_denominator=10000)
# Convert float to fraction
frac = converter.to_rational(3.14159)
print(frac) # Fraction(355, 113)
print(f"Approximation: {float(frac):.6f}") # 3.141593
Batch Conversion¶
# Convert multiple coefficients
coeffs = {
"x1": 3.5,
"x2": 2.333,
"x3": 1.25,
"x4": 0.666
}
int_coeffs, common_denom = converter.convert_coefficients(coeffs)
print(f"Integer coefficients: {int_coeffs}")
# {'x1': 42, 'x2': 28, 'x3': 15, 'x4': 8}
print(f"Common denominator: {common_denom}")
# 12
# Verify: int_coeffs[key] / common_denom ≈ coeffs[key]
Configuration Options¶
Maximum Denominator¶
Controls the trade-off between approximation accuracy and denominator size:
# Smaller denominator - faster, less accurate
converter = LXRationalConverter(max_denominator=100)
frac = converter.to_rational(3.14159)
print(frac) # Fraction(22, 7) = 3.142857...
# Larger denominator - slower, more accurate
converter = LXRationalConverter(max_denominator=100000)
frac = converter.to_rational(3.14159)
print(frac) # Fraction(355, 113) = 3.141593...
Guidelines:
Small denominators (< 1000): Fast, suitable for rough approximations
Medium denominators (1000-10000): Good balance (recommended default)
Large denominators (> 10000): High accuracy, slower computation
Approximation Method¶
Choose between three algorithms:
# Farey sequence (default, recommended)
converter = LXRationalConverter(method="farey")
# Continued fractions
converter = LXRationalConverter(method="continued_fraction")
# Stern-Brocot tree
converter = LXRationalConverter(method="stern_brocot")
Algorithm Comparison:
Method |
Speed |
Accuracy |
Notes |
|---|---|---|---|
Farey |
⚡⚡⚡ Fastest |
✓✓✓ Best |
Recommended default |
Continued Fraction |
⚡⚡ Fast |
✓✓✓ Best |
Classic algorithm |
Stern-Brocot |
⚡⚡ Fast |
✓✓✓ Best |
Equivalent to Farey |
Float Tolerance¶
Control tolerance for float comparisons:
# Default tolerance
converter = LXRationalConverter(float_tolerance=1e-9)
# Stricter tolerance
converter = LXRationalConverter(float_tolerance=1e-12)
# Looser tolerance
converter = LXRationalConverter(float_tolerance=1e-6)
Approximation Algorithms¶
Farey Sequence Method¶
The Farey method uses mediant approximation with floor/ceil optimization. It’s the fastest and recommended default.
Algorithm Overview:
Initialize bounds with floor and ceiling of the target value
Compute mediant: (n₁ + n₂) / (d₁ + d₂)
Update bounds based on mediant position
Repeat until denominator exceeds maximum
Example:
converter = LXRationalConverter(max_denominator=20, method="farey")
frac, error = converter.to_rational(3.14159, return_error=True)
print(f"Fraction: {frac}") # 22/7
print(f"Value: {float(frac):.6f}") # 3.142857
print(f"Error: {error:.2e}") # 1.27e-03
Continued Fraction Method¶
Classic continued fraction expansion algorithm. Good balance of speed and accuracy.
Algorithm Overview:
Extract integer part and fractional part
Build continued fraction representation
Compute convergents until max denominator
Example:
converter = LXRationalConverter(method="continued_fraction")
frac = converter.to_rational(3.14159)
print(frac) # Fraction(355, 113)
Stern-Brocot Tree Method¶
Binary search through the Stern-Brocot tree. Mathematically equivalent to Farey but offers alternative algorithmic framing.
Example:
converter = LXRationalConverter(method="stern_brocot")
frac = converter.to_rational(3.14159)
print(frac) # Fraction(355, 113)
Error Tracking¶
Return Approximation Error¶
converter = LXRationalConverter(max_denominator=10000)
# Get fraction and error
frac, error = converter.to_rational(3.14159, return_error=True)
print(f"Fraction: {frac}")
print(f"Approximation: {float(frac):.8f}")
print(f"Target: 3.14159000")
print(f"Error: {error:.2e}")
Compare Methods¶
Compare all three methods for a given value:
converter = LXRationalConverter(max_denominator=10000)
results = converter.compare_methods(3.14159)
for method, (frac, error, time) in results.items():
print(f"{method:20} : {frac:10} | Error: {error:.2e} | Time: {time*1e6:.2f}μs")
# Output:
# farey : 355/113 | Error: 2.67e-07 | Time: 15.23μs
# continued_fraction : 355/113 | Error: 2.67e-07 | Time: 18.45μs
# stern_brocot : 355/113 | Error: 2.67e-07 | Time: 16.78μs
Practical Applications¶
GLPK Integer Solver¶
Convert model coefficients for GLPK’s exact rational mode:
from lumix import LXModel, LXVariable, LXLinearExpression
from lumix.utils import LXRationalConverter
# Build model with float coefficients
products = [
Product(id=1, profit=12.5),
Product(id=2, profit=8.333),
Product(id=3, profit=15.75)
]
model = LXModel("production")
production = LXVariable[Product, float]("x").from_data(products)
model.add_variable(production)
# Extract coefficients
obj_coeffs = {p.id: p.profit for p in products}
# Convert to rationals
converter = LXRationalConverter(max_denominator=10000)
int_coeffs, denom = converter.convert_coefficients(obj_coeffs)
print(f"Original: {obj_coeffs}")
print(f"Integer: {int_coeffs}")
print(f"Denominator: {denom}")
# Use int_coeffs with GLPK
# ... GLPK-specific solver code ...
Constraint Coefficient Conversion¶
Convert constraint coefficients:
# Resource usage coefficients
usage_coeffs = {
("Product_1", "Resource_A"): 2.5,
("Product_1", "Resource_B"): 1.333,
("Product_2", "Resource_A"): 3.75,
("Product_2", "Resource_B"): 0.666,
}
converter = LXRationalConverter(max_denominator=10000)
int_usage, denom = converter.convert_coefficients(usage_coeffs)
print(f"Integer usage coefficients: {int_usage}")
print(f"Common denominator: {denom}")
Exact Arithmetic¶
Ensure exact arithmetic for sensitive calculations:
from fractions import Fraction
converter = LXRationalConverter(max_denominator=1000000)
# Convert all coefficients to exact rationals
values = [0.1, 0.2, 0.3, 0.4, 0.5]
fractions = [converter.to_rational(v) for v in values]
# Sum with exact arithmetic (no rounding errors)
exact_sum = sum(fractions, start=Fraction(0))
print(f"Exact sum: {exact_sum}") # Fraction(3, 2) = 1.5
print(f"Float sum: {sum(values)}") # Might have rounding error
Advanced Techniques¶
Custom Tolerance Handling¶
Fine-tune tolerance for specific use cases:
# Very strict tolerance for financial calculations
converter = LXRationalConverter(
max_denominator=1000000,
float_tolerance=1e-15
)
price = 123.456789012345
frac = converter.to_rational(price)
print(f"Exact fraction: {frac}")
Iterative Refinement¶
Progressively increase denominator until error threshold is met:
def convert_with_error_bound(value: float, max_error: float = 1e-6):
"""Convert to rational with guaranteed error bound."""
max_denom = 10
while max_denom <= 1000000:
converter = LXRationalConverter(max_denominator=max_denom)
frac, error = converter.to_rational(value, return_error=True)
if error <= max_error:
return frac, max_denom
max_denom *= 10
raise ValueError(f"Could not meet error bound {max_error}")
# Usage
frac, denom = convert_with_error_bound(3.14159, max_error=1e-6)
print(f"Fraction: {frac} (max denom: {denom})")
Batch Optimization¶
Optimize denominator for batch of coefficients:
def optimize_batch_conversion(coeffs: dict, target_error: float = 1e-6):
"""Find optimal max_denominator for batch."""
for max_denom in [100, 1000, 10000, 100000]:
converter = LXRationalConverter(max_denominator=max_denom)
# Convert all
int_coeffs, common_denom = converter.convert_coefficients(coeffs)
# Check errors
max_error = max(
abs(int_coeffs[k] / common_denom - v)
for k, v in coeffs.items()
)
if max_error <= target_error:
return int_coeffs, common_denom, max_denom
raise ValueError(f"Could not meet error bound {target_error}")
# Usage
coeffs = {"x1": 3.14159, "x2": 2.71828, "x3": 1.41421}
int_coeffs, denom, max_denom = optimize_batch_conversion(coeffs)
print(f"Used max_denominator: {max_denom}")
Best Practices¶
Choose Appropriate Max Denominator
Balance accuracy vs. complexity:
# For typical LP problems converter = LXRationalConverter(max_denominator=10000) # Good default # For high-precision needs converter = LXRationalConverter(max_denominator=100000) # For quick approximations converter = LXRationalConverter(max_denominator=1000)
Use Batch Conversion
More efficient than individual conversions:
# Good: Batch conversion int_coeffs, denom = converter.convert_coefficients(coeffs) # Avoid: Individual conversions fracs = {k: converter.to_rational(v) for k, v in coeffs.items()}
Check Approximation Quality
Always verify error for critical applications:
frac, error = converter.to_rational(value, return_error=True) if error > acceptable_threshold: # Increase max_denominator or handle error
Use Farey Method
Unless you have specific algorithmic needs:
# Recommended converter = LXRationalConverter(method="farey")
Performance Considerations¶
Computation Time¶
Farey is fastest (typically 10-20μs per conversion)
Time increases with max_denominator
Batch conversion is more efficient than individual calls
Memory Usage¶
Minimal memory footprint
No caching of previous results
Safe for large-scale conversions
Accuracy vs. Speed¶
Trade-off between denominator size and speed:
import time
for max_denom in [100, 1000, 10000, 100000]:
converter = LXRationalConverter(max_denominator=max_denom)
start = time.perf_counter()
frac, error = converter.to_rational(3.14159, return_error=True)
elapsed = time.perf_counter() - start
print(f"Max denom: {max_denom:6} | Error: {error:.2e} | Time: {elapsed*1e6:.1f}μs")
Common Issues¶
Large Denominators¶
If denominators grow too large, reduce max_denominator:
# Problem: denominators too large
converter = LXRationalConverter(max_denominator=1000000)
int_coeffs, denom = converter.convert_coefficients(coeffs)
print(denom) # Might be very large!
# Solution: Use smaller max_denominator
converter = LXRationalConverter(max_denominator=10000)
Approximation Errors¶
If errors are too large, increase max_denominator:
# Check error
frac, error = converter.to_rational(value, return_error=True)
if error > threshold:
# Increase precision
converter = LXRationalConverter(max_denominator=100000)
frac, error = converter.to_rational(value, return_error=True)
See Also¶
LXRationalConverter- API referenceUtils Module API - Utils module API
Python fractions module: https://docs.python.org/3/library/fractions.html
GLPK documentation: https://www.gnu.org/software/glpk/