Rationalising Denominators 1: Fractional Powers

Posted on by Chris Warburton

Posts in this series:

Powers

Products

Sums

Ratios

A word on notation

This page deals with a few different concepts, which I’ve tried to use in a consistent way: “numbers” are the platonic entities we want to talk about; mathematical notation is used to describe numbers; and Python code is used for our specific, concrete implementation. I’ve annotated the mathematical notation with its corresponding Python code, where relevant.


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
from typing import Tuple

Base = int
Exponent = 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"
    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, zero)

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. For example, all of the following are equivalent:

We can avoid this redundancy by choosing a particular exponent to be “normal”, and replace all other exponents of 0 with the normal exponent. We can’t choose an exponent of 0, since that’s forbidden by our assertion; so I’ll pick the number 1 and add the following lines to our Power function to perform this normalisation:

    if base == 0:
        assert exp != 0, f"Power 0^0 is undefined"
        exp = one

Zeroth powers

Our next normalisation rule applies when the exponent is 0. In this case, we’ve already seen that the base is not allowed to be 0; but any non-zero base raised to the power of 0 gives a result of 1, e.g. the following are equivalent:

We can avoid this redundancy in a similar way to before: choosing a “normal” value for the base, and using that whenever the exponent is 0. Again, we can’t choose the base to be 0, so we’ll choose 1:

    if exp == 0:
        base = 1

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). For example, all of these are equivalent:

We can avoid this redundancy by reducing the exponents whenever the base is 1. We’ll do this using the modulo operation, with modulus of 1:

    if base == 1:
        exp = exp % 1

This restricts its range to 0 <= exp < 1 by discarding any “whole part” of the Fraction. For whole numbers, like the examples above, this is equivalent to exp = 0; but we want to preserve any fractional part, since it will come in handy in later posts.

Notice that this rule complements the previous one, since they both turn different representations of the number 1 into its normal form 1⁰.

Helper Functions

Now that we have a normalised representation for Power values, it’s useful to define a couple of helper functions to work with them. First we’ll decide whether or not a given Power is rational, which we can figure out based on the denominator of its exponent:

def power_is_rational(p: Power) -> bool:
    base, exp = p
    return exp.denominator == 1

Since we don’t allow negative exponents, every rational Power is actually an integer, which we can calculate using the following function:

def eval_power_int(p: Power) -> int:
    assert power_is_rational(p), f"Can't eval {p} as int"
    base, exp = p
    return base**exp.numerator

These will be helpful in future posts.

Conclusion

Here’s our overall implementation of Power:

from fractions import Fraction
from typing import Tuple

Base = int
Exponent = 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 exp == 0:
        # Anything else to the power of zero is one. Normalise to 1^0.
        base = 1
    if base == 1:
        # Remove whole powers of 1, since they just multiply by one
        exp = exp % 1
    return (base, exp)

def power_is_rational(p: Power) -> bool:
    base, exp = p
    return exp.denominator == 1

def eval_power_int(p: Power) -> int:
    assert power_is_rational(p), f"Can't eval {p} as int"
    base, exp = p
    return base**exp.numerator

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

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

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.

The full code for this post is available in the fraction_powers module of my conjugate repo.