Rationalising Denominators 1: Fractional Powers
Posts in this series:
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 exp
onent:
The base
can be any natural number,
i.e. a whole number which is either positive or zero.
The exp
onent can be a Fraction
(a numerator
over a denominator
), but must obey certain
rules:
- We do not allow negative
exp
onents - If the
base
is 0, theexp
onent cannot also be 0.
Here’s a simple implementation in Python:
from fractions import Fraction
from typing import Tuple
= int
Base = Fraction
Exponent
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 exp
onents:
= Fraction(0, 1)
zero = Fraction(1, 1)
one = Fraction(1, 2)
half
= Power(0, one)
power_zero = Power(1, zero) power_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:
- 1¹
- 1²
- 2⁰
Powers of zero
When the base
is 0, we don’t
allow the exp
onent to be 0 (since
that’s not well-defined mathematically). For every other exp
onent, there is redundancy, since 0
raised to any non-zero power is 0. For example, all of the following are
equivalent:
- 0¹
- 0²
- √0
We can avoid this redundancy by choosing a particular exp
onent to be “normal”, and replace
all other exp
onents of 0 with the
normal exp
onent. We can’t choose
an exp
onent 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"
= one exp
Zeroth powers
Our next normalisation rule applies when the exp
onent 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:
- 1⁰
- 2⁰
- 3⁰
We can avoid this redundancy in a similar way to before: choosing a
“normal” value for the base
, and
using that whenever the exp
onent
is 0. Again, we can’t choose the base
to be 0, so we’ll choose 1:
if exp == 0:
= 1 base
Powers of one
When the base
is 1, we can add
1 to the exp
onent 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:
- 1⁰
- 1¹
- 1²
We can avoid this redundancy by reducing the exp
onents whenever the base
is 1. We’ll do this using the modulo operation, with
modulus
of 1:
if base == 1:
= exp % 1 exp
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 exp
onent:
def power_is_rational(p: Power) -> bool:
= p
base, exp return exp.denominator == 1
Since we don’t allow negative exp
onents, every rational Power
is actually an int
eger, 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"
= p
base, exp 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
= int
Base = Fraction
Exponent
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
= one
exp if exp == 0:
# Anything else to the power of zero is one. Normalise to 1^0.
= 1
base if base == 1:
# Remove whole powers of 1, since they just multiply by one
= exp % 1
exp return (base, exp)
def power_is_rational(p: Power) -> bool:
= p
base, exp return exp.denominator == 1
def eval_power_int(p: Power) -> int:
assert power_is_rational(p), f"Can't eval {p} as int"
= p
base, exp return base**exp.numerator
= Fraction(0, 1)
zero = Fraction(1, 1)
one = Fraction(1, 2)
half
= Power(0, one)
power_zero = Power(1, zero) power_one
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:
- 4¹
- 2²
- √16
- etc.
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.