# nRAN: Multi-Term Rational-Addition Numbers

## 1. Definition

An **nRAN** is a number represented by a list of rational terms and a single offset:

\[
x = \sum_{i=1}^n \frac{a_i}{b_i} + c
\]

The triples \((a_i, b_i)\) are called **rational correction terms**, and \(c\) is the **base offset**. The **collapse** is the evaluation of the full sum.

With integer parameters, the value of an nRAN is still a rational number, but the representation carries far more structure than a single fraction.

## 2. Why n > 1 matters

A single RAN can separate one large offset from one small correction. An nRAN can separate **many scales at once**:

\[
x = 10^{15} + \frac{1}{10} + \frac{1}{100} + \frac{1}{1000} + \cdots + \frac{1}{10^{100}}
\]

In ordinary floating-point arithmetic, adding \(10^{-100}\) to \(10^{15}\) is lost immediately. In an nRAN with \(n=100\), each correction survives as a separate term until collapse.

This is a **finite rational expansion** — similar in spirit to floating-point expansions used in high-precision libraries, but using exact rational pairs instead of floating-point limbs.

## 3. Arithmetic

### Addition

\[
\left(\sum_i \frac{a_i}{b_i} + c\right) + \left(\sum_j \frac{d_j}{e_j} + f\right)
= \sum_i \frac{a_i}{b_i} + \sum_j \frac{d_j}{e_j} + (c + f)
\]

Terms are concatenated; offsets are added.

### Subtraction

\[
x - y = \sum_i \frac{a_i}{b_i} - \sum_j \frac{d_j}{e_j} + (c - f)
\]

Negate the other terms and add.

### Multiplication by scalar

\[
k \cdot \left(\sum_i \frac{a_i}{b_i} + c\right) = \sum_i \frac{k a_i}{b_i} + k c
\]

Preserves the number of terms.

### Full nRAN multiplication

\[
\left(\sum_i \frac{a_i}{b_i} + c\right)\left(\sum_j \frac{d_j}{e_j} + f\right)
= \sum_i\sum_j \frac{a_i d_j}{b_i e_j}
+ \sum_i \frac{a_i f}{b_i}
+ \sum_j \frac{c d_j}{e_j}
+ c f
\]

This produces up to \(n^2 + 2n + 1\) terms. For \(n=1024\), a naive multiplication would create over one million terms, so capacity management is essential.

### Division

Division \(x / y\) is handled by converting the divisor \(y\) into a single rational term, then multiplying by its reciprocal.

\[
y = \sum_j \frac{d_j}{e_j} + f = \frac{D}{E} + f = \frac{D + fE}{E}
\]

\[
\frac{1}{y} = \frac{E}{D + fE}
\]

Then \(x / y = x \cdot \frac{E}{D + fE}\).

## 4. Capacity management

A practical nRAN has a maximum capacity \(n_{\max}\). When an operation would exceed it, the system **compacts** by combining some rational terms. Strategies include:

- **Exact rational compaction**: combine all integer-parameter terms into a single fraction using exact arithmetic.
- **Same-denominator merge**: combine terms that share a denominator.
- **Magnitude-based merge**: combine the smallest terms first.
- **Collapse into offset**: add the rational sum to \(c\) as a last resort.

The choice of compaction strategy determines the accuracy/cost trade-off.

## 5. Python implementation

```python
from typing import List, Tuple, Union
from fractions import Fraction

Number = Union[int, float]


class nRAN:
    """
    Multi-term Rational-Addition Number.
    Represents sum_i (a_i / b_i) + c without collapsing.
    """

    def __init__(self,
                 terms: List[Tuple[Number, Number]] = None,
                 c: Number = 0,
                 max_n: int = None):
        self.terms = list(terms) if terms else []
        self.c = c
        self.max_n = max_n

        for a, b in self.terms:
            if b == 0:
                raise ValueError("b cannot be zero")

        if max_n is not None and len(self.terms) > max_n:
            self._compact()

    def collapse(self):
        """Force evaluation to a single number."""
        return sum(a / b for a, b in self.terms) + self.c

    def __repr__(self):
        return f"nRAN({self.terms}, c={self.c!r})"

    def __str__(self):
        if not self.terms:
            return str(self.c)
        parts = " + ".join(f"{a}/{b}" for a, b in self.terms)
        return f"{parts} + {self.c}"

    def _combine_rational_terms(self):
        """Combine integer-parameter terms into a single exact fraction."""
        int_terms = [(a, b) for a, b in self.terms
                     if isinstance(a, int) and isinstance(b, int)]
        float_terms = [(a, b) for a, b in self.terms
                       if not (isinstance(a, int) and isinstance(b, int))]

        if not int_terms:
            return float_terms

        total = sum(Fraction(a, b) for a, b in int_terms)
        return [(total.numerator, total.denominator)] + float_terms

    def _compact(self):
        """Reduce term count to respect max_n."""
        if self.max_n is None:
            return

        # First try exact rational compaction
        self.terms = self._combine_rational_terms()

        # If still over capacity, collapse the entire rational part into c
        if len(self.terms) > self.max_n:
            rational_value = sum(Fraction(a, b) for a, b in self.terms
                                 if isinstance(a, int) and isinstance(b, int))
            # Add any float terms by collapsing them
            float_sum = sum(a / b for a, b in self.terms
                            if not (isinstance(a, int) and isinstance(b, int)))
            self.terms = []
            self.c += float(rational_value) + float_sum

    def _after_op(self, result):
        """Helper to enforce capacity after an operation."""
        if self.max_n is not None:
            result._compact()
        return result

    def __add__(self, other):
        if isinstance(other, (int, float)):
            return nRAN(self.terms, self.c + other, self.max_n)
        if not isinstance(other, nRAN):
            return NotImplemented
        return self._after_op(
            nRAN(self.terms + other.terms, self.c + other.c, self.max_n)
        )

    def __sub__(self, other):
        if isinstance(other, (int, float)):
            return nRAN(self.terms, self.c - other, self.max_n)
        if not isinstance(other, nRAN):
            return NotImplemented
        neg_terms = [(-a, b) for a, b in other.terms]
        return self._after_op(
            nRAN(self.terms + neg_terms, self.c - other.c, self.max_n)
        )

    def __mul__(self, other):
        if isinstance(other, (int, float)):
            new_terms = [(a * other, b) for a, b in self.terms]
            return nRAN(new_terms, self.c * other, self.max_n)

        if not isinstance(other, nRAN):
            return NotImplemented

        # Cross terms: n^2 growth
        new_terms = []
        for a, b in self.terms:
            for d, e in other.terms:
                new_terms.append((a * d, b * e))

        # Cross with offsets
        for a, b in self.terms:
            new_terms.append((a * other.c, b))
        for d, e in other.terms:
            new_terms.append((self.c * d, e))

        return self._after_op(
            nRAN(new_terms, self.c * other.c, self.max_n)
        )

    def __truediv__(self, other):
        if isinstance(other, (int, float)):
            new_terms = [(a, b * other) for a, b in self.terms]
            return nRAN(new_terms, self.c / other, self.max_n)

        if not isinstance(other, nRAN):
            return NotImplemented

        # Convert other to a single rational, then multiply by reciprocal
        combined = other._combine_rational_terms()
        if len(combined) != 1:
            # Fallback: collapse divisor and divide
            return self * (1 / other.collapse())

        d, e = combined[0]
        # other = d/e + other.c = (d + other.c * e) / e
        denom = d + other.c * e
        reciprocal = nRAN([(e, denom)], 0, self.max_n)
        return self * reciprocal

    def __radd__(self, other):
        return self.__add__(other)

    def __rmul__(self, other):
        return self.__mul__(other)

    def __rsub__(self, other):
        return (self.__neg__()).__add__(other)

    def __neg__(self):
        return nRAN([(-a, b) for a, b in self.terms], -self.c, self.max_n)


# Demonstration
if __name__ == "__main__":
    # 1. Accumulate 100 small corrections to a large base
    x = nRAN([], 1e15, max_n=100)
    for i in range(1, 101):
        x = x + nRAN([(1, 10 ** i)], 0, max_n=100)

    print("nRAN collapse:", x.collapse())
    # Direct float arithmetic loses the small corrections
    direct = 1e15
    for i in range(1, 101):
        direct += 1 / (10 ** i)
    print("Direct float: ", direct)

    # 2. Show internal structure
    print("\nInternal nRAN has", len(x.terms), "terms + offset")

    # 3. Subtraction of nearly equal large numbers
    a = nRAN([(1, 1_000_000)], 1_000_000, max_n=10)
    b = nRAN([], 1_000_000, max_n=10)
    diff = a - b
    print("\nSubtraction:", diff)
    print("Collapsed:  ", diff.collapse())
    print("Direct float:", (1_000_000 + 1 / 1_000_000) - 1_000_000)
```

## 6. Behavior with large n

If you set `max_n = 1024`, the nRAN can hold up to 1024 separate rational corrections before any compaction. Addition is cheap because it only concatenates. Multiplication is expensive because it grows as \(n^2\).

The sweet spot is:

- **Use nRAN for repeated addition/subtraction** with many small corrections.
- **Use scalar multiplication** freely.
- **Avoid multiplying two nRANs** unless you have a compaction strategy that keeps the result bounded.
- **Collapse only at the end** of a chain of operations.

## 7. Relation to existing systems

nRANs are conceptually close to:

- **Floating-point expansions**: sums of floating-point numbers with decreasing magnitude.
- **Compensated arithmetic**: keeping correction terms separate.
- **Exact rational arithmetic**: combining all terms gives a single exact fraction.
- **Symbolic computation**: unevaluated sums of rational expressions.

The difference is that nRANs make the *capacity* explicit. You can choose \(n=1, 2, 64,\) or \(1024\) depending on how many scales your problem contains.

## 8. Open design question

What is the best compaction strategy when an nRAN exceeds its capacity?

- Combine the two smallest terms?
- Combine all terms that share a denominator?
- Always collapse the oldest terms first?
- Use a target precision and only collapse terms below the precision threshold?

The choice depends on whether you care most about exactness, speed, or controlling rounding error. That is the central research question for practical nRAN arithmetic.