# Surreal Numbers In Python 3

## Generating Day 2

So far, we’ve generated 3 forms that we’re familiar with: `0`, `1`, and `-1`, represented by `{|}`, `{0|}` and `{|0}` respectively. We also have a batch of new numbers, that we’re not quite sure of:

``````{1|}{−1|} {0, 1|} {0, −1|} {−1, 1|}
{−1, 0, 1|} {|1} {| − 1} {|0, 1}
{|0, −1} {| − 1, 1} {| − 1, 0, 1} {−1|0}
{−1|1} {−1|0, 1} {0|1} {−1, 0|1}
``````

Moving beyond Day 1, the numbers begin to follow a more intuitive pattern. We can read Surreal numbers as follows:

`If S is a surreal number consisting of {a|b}, then S is the number *in-between* a and b`. I’m going to start using a physical “number line” as an analog for the Real Numbers, as it’s easier to concieve of the `|` in the surreal-notation to be akin to a marker on an analog scale, where the number can be read by looking at the numbers surrounding the marker-line. Therefore, I will be using “left” and “right” as analogs for “less-than” and “greater-than” respectively.

We can then conceptualize previous numbers, such as `{0|} is the number to the right of 0, which is 1`. We can then carry this rule further on. `If S = {1|}, then S is the number to the right of 1, which is 2`. We’ve now added `2` to our list of forms. Furthermore, we can take the negative of the statement. `If S = {|-1}, then S is the number to the left of -1, which is negative 2`. Another form for our collection, -2.

By this point, you may have noticed that using entire sets for the left and right values of a surreal is a bit redundant. Equivalent forms yield equivalent numbers, so something like:

`{-1, 0, 1|} === {1|} = 2` and `{|0, 1} === {|0} = -1` and finally `{-1, 0| 2} === {0|2} = 1` Essentially, we only care about `max(left)` and `min(right)` to define a number.

We can add these shortcuts to our `Surreal` class as follows

``````    @property
def xl(self):
return max(self.left_set or (None,))

@property
def xr(self):
return min(self.right_set or (None,))
``````

## Rationals

Great, so we can generate all the integers. But even grade-schoolers know about decimals/fractions. Where do those numbers come from in our system? If we remember the bathroom-scale analog, we can think of the `|` (pipe) as a sort of arrow/indicator, where we can see surrounding numbers to generate further ones. We can start to make some rational numbers by looking “where they live”, in-between integers.

Thus, we can think of the following surreal, `{0|1}`, as “the number between 0 and 1”, or `1/2`. Similarly, `-1/2` is represented by `{-1|0}`. We can now add these rationals to our forms, to look even “further” in.

`{1/2|1} = 3/4`, born on day 3. `{1/2|3/4} = 5/8`. If you notice, our denominator will always be a power-of-2, so we call these the `Dyadic Rationals`.

## Actually coding this

Conway uses a recursive function to define his conversion. It follows the common logic of splitting the problem into the base-cases and recursive-case. With surreals, the base cases are `-1, 0, and 1`. As such, we’ll include special `classmethods` to act as constructors for these special cases.

``````    @classmethod
def zero(cls):
return cls(0, nothing, nothing)

@classmethod
def one(cls):
return cls(1, (0,), nothing)

@classmethod
def neg_one(cls):
return cls(1, nothing, (0,))
``````

We can then start to write a `__float__` function, allowing for easy conversion of Surreals to their Real Number counterparts. We’ll start by coding the base-cases:

``````    def __float__(self):
if self == Surreal.zero():
return 0.
elif self == Surreal.one():
return 1.
elif self == Surreal.neg_one():
return -1.
else:
raise NotImplementedError()
``````

In order to cover the rest of the number-line, we need to come up with a general-purpose algorithm. Remember previously, we defined 3 cases where we could make a “form” for our collection: left-number, right-number, and in-between-number, where the first 2 increment/decrement the number, and the last finds the number in-between the two numbers given.

``````		...
else:
if len(self.left_set) == 0:  # Left number
return min(self.right_set) - 1
elif len(self.right_set) == 0:  # Right number
return max(self.left_set) + 1
else:  # In-between
return (min(self.right_set) + max(self.left_set)) / 2
``````

## Going back

We should be able to do the inverse of float-conversion, where we can pass a number to our class, and it converts it to a valid Surreal. However, we can’t simply go from a decimal number to a Surreal; we’re only able to generate numbers from a certain domain, so it stands to reason that using the same rules, we can only convert certain numbers back into Surreals. We can generate all integers (`k(n+1) = k(n) + 1`) and all dyadic rationals (`k(n+1) = (2(k(n) + 1) / 2^n`).

To start, we’ll use a simpler example that follows an easy pattern- the integers. We know that a positive integer-number `i` can be expressed in Surreal form as `{i-1|}`. Negative integers simply flip which set is used, as `i === {|i+1}`. Encapsulating our base-case of `zero`, we get the following code:

``````
@classmethod
def from_int(cls, i: int):
if i == 0:
return Surreal.zero()
elif i > 0:
return Surreal(abs(i), (i-1,), nothing)
elif i < 0:
return Surreal(abs(i), nothing,(i+1,))
else:
raise Exception("NaN")
``````

Dyadics are a bit harder than the plain integers, as one needs to find an odd-integer numerator N, and a denominator that’s a power-of-2. Without going through the details of solving the equations, we find that the surreal form of dyadic `x` is `{ x - 1/2^{k} | x + 1/2^{k} }`. This will produce a number whose numeric-average is equal to `x`.

`(x - 1/2^{k}) + (x + 1/2^{k})/2 = 2x/2 = x`

We can use Python’s built-in `Fraction` class to help conver to rational-fractions, and store the `numerator` and `denominator` separately. We can also use `math.log2` to help us find `k`.

``````
@classmethod
if isinstance(f, float):
f = Fraction(Decimal(f))
k = log2(f.denominator)
n = abs(f.numerator//2)+1
return Surreal(n, (f - (1/2)**k,), (f + (1/2)**k,))
``````

To check, we can `assert` some quick tests:

With all this in place, we can finally create a Surreal generation-function that includes the dyadic-rationals:

Full Jupyter notebook here

Written on June 4, 2021