Rationalising Denominators 1: Fractional Powers

Posted on by Chris Warburton

Posts in this series:

Powers

Products

Sums

Ratios


Introduction

I’ve spent the last few weeks playing around with radicals, looking for a simple representation that will fit neatly into my Ivory Tower library. After a few false starts, I’ve cobbled together a neat little Python library, which I thought was worth sharing across a few blog posts.

Powers

We’ll start by defining a Power as a pair of numbers, which we’ll call a base and an exponent:

The base can be any natural number, i.e. a whole number which is either positive or zero.

The exponent can be a Fraction (a numerator over a denominator), but must obey certain rules:

Here’s a simple implementation in Python:

from fractions import Fraction

def Power(base: int, exp: Fraction) -> Tuple[int, Fraction]:
    assert base >= 0, f"Power {base}^{exp} cannot have negative base"
    assert exp >= 0, f"Power {base}^{exp} cannot have negative exponent"
    if base == 0:
        assert exp != 0, f"Power 0^0 is undefined"
    return (base, exp)

I’ve given this function an uppercased name, to indicate that we’ll use Power as a type annotation as well as for constructing values. Here are some constants for this Power type, as well as for exponents:

zero = Fraction(0, 1)
one = Fraction(1, 1)
half = Fraction(1, 2)

power_zero = Power(0, one)
power_one = Power(1, one)

Normalisation

Thankfully, Python’s Fraction will automatically reduce values to their “normal form”, e.g. calling Fraction(2, 4) will return the value Fraction(1, 2). However, there are other redundancies in our Power type that will not simplify automatically; especially values involving the numbers 0 and 1. For example the following values all represent the number 1:

Powers of zero

When the base is 0, we don’t allow the exponent to be 0 (since that’s not well-defined mathematically). For every other exponent, there is redundancy, since 0 raised to any non-zero power is 0. We can avoid this redundancy by choosing a particular exponent to be “normal”, and replace all other exponents of 0 with the normal exponent. I’ll pick the number 1 to be our normal exponent and add the following lines to our Power function to perform this normalisation:

    if base == 0:
        exp = one

Powers of one

When the base is 1, we can add 1 to the exponent without changing the overall value; since that corresponds to multiplying the result by the base, which in this case means multiplying by 1, which is redundant. We can reduce these exponents to avoid this redundancy, by repeatedly taking away 1 until it becomes less than 1. This corresponds to the modulo operation, with modulus of 1:

    if base == 1:
        exp = exp % 1

Zeroth powers

Our final normalisation applies when the exponent is 0: any non-zero number raised to the power of 0 gives a result of 1. Hence we can choose a “normal” value for the base, say the number 1, and replace any other base with that:

    if exp == 0:
        base = 1

This complements the previous rule, since the modulo operation could result in the value Power(1, Fraction(0, 1)).

Conclusion

Here’s our overall implementation of Power:

from fractions import Fraction

def Power(base: int, exp: Fraction) -> Tuple[Base, Exponent]:
    assert base >= 0, f"Power {base}^{exp} cannot have negative base"
    assert exp >= 0, f"Power {base}^{exp} cannot have negative exponent"
    if base == 0:
        assert exp != 0, f"Power 0^0 is undefined"
        # Normalise all other powers of 0 to 0^1, since they're equivalent
        exp = one
    if base == 1:
        # Remove whole powers of 1, since they just multiply by one
        exp = exp % 1
    if exp == 0:
        # Anything to the power of zero is one. Normalise to 1^1.
        base, exp = (1, one)
    return (base, exp)

zero = Fraction(0, 1)
one = Fraction(1, 1)
half = Fraction(1, 2)

power_zero = Power(0, Fraction(1, 1))
power_one = Power(1, Fraction(1, 1))

So far this is a pretty simple way to represent numbers, but it turns out to be quite powerful. We’ve implemented some normalisation steps, but there are still some redundancies; e.g. the number 4 can be represented in many ways, like:

In the next post we’ll extend this to products of powers.