From d91c5731ce0be2188d81a3c591bfcec0eca71244 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Juhani=20Krekel=C3=A4?= Date: Sun, 29 Apr 2018 15:00:35 +0300 Subject: [PATCH] First commit --- .gitignore | 2 + README | 1 + UNLICENSE | 24 +++++++ arithmetic.py | 176 ++++++++++++++++++++++++++++++++++++++++++++++++ sets.py | 180 ++++++++++++++++++++++++++++++++++++++++++++++++++ 5 files changed, 383 insertions(+) create mode 100644 .gitignore create mode 100644 README create mode 100644 UNLICENSE create mode 100644 arithmetic.py create mode 100644 sets.py diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..fffc64d --- /dev/null +++ b/.gitignore @@ -0,0 +1,2 @@ +__pycache__ +*.swp diff --git a/README b/README new file mode 100644 index 0000000..4b46be7 --- /dev/null +++ b/README @@ -0,0 +1 @@ +Random playing around with sets and set-based natural numbers diff --git a/UNLICENSE b/UNLICENSE new file mode 100644 index 0000000..69843e4 --- /dev/null +++ b/UNLICENSE @@ -0,0 +1,24 @@ +This is free and unencumbered software released into the public domain. + +Anyone is free to copy, modify, publish, use, compile, sell, or +distribute this software, either in source code form or as a compiled +binary, for any purpose, commercial or non-commercial, and by any +means. + +In jurisdictions that recognize copyright laws, the author or authors +of this software dedicate any and all copyright interest in the +software to the public domain. We make this dedication for the benefit +of the public at large and to the detriment of our heirs and +successors. We intend this dedication to be an overt act of +relinquishment in perpetuity of all present and future rights to this +software under copyright law. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, +EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF +MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. +IN NO EVENT SHALL THE AUTHORS BE LIABLE FOR ANY CLAIM, DAMAGES OR +OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, +ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR +OTHER DEALINGS IN THE SOFTWARE. + +For more information, please refer to [http://unlicense.org] diff --git a/arithmetic.py b/arithmetic.py new file mode 100644 index 0000000..d56ecd8 --- /dev/null +++ b/arithmetic.py @@ -0,0 +1,176 @@ +import functools +import time + +import sets + +# The encoding used is something I've seen referred to as Von Neuman indices +# Basically, an empty set is 0 and {a, b, c} is 2^a + 2^b + 2^c +# The numbers in the set are also encoded the same way +# For example: +# 13 = 2³ + 2² + 2⁰ +# → {3, 2, 0} +# 3 = 2¹ + 2⁰ +# 2 = 2¹ +# → {{1, 0}, {1}, 0} +# 1 = 2⁰ +# → {{{0}, 0}, {{0}}, 0} +# → {{{∅}, ∅}, {{∅}}, ∅} + +empty = sets.set() + +def I(x): + """Converts a number from set representation to an integer""" + # Algorith works by recursively calling itself + # I(∅) = 0 + # I({e₁, e₂, e₃, …}) = 2^I(e₁) + 2^I(e₂) + 2^I(e₃) + … + # For example: + # I({{{∅}, ∅}, {{∅}}, ∅}) + # → 2^I({{∅}, ∅}) + 2^I({{∅}}) + 2^I(∅) + # → 2^(2^I({∅}) + 2^I(∅)) + 2^2^I({∅}) + 2^0 + # → 2^(2^2^I(∅) + 2^0) + 2^2^2^I(∅) + 2^0 + # → 2^(2^2^0 + 2^0) + 2^2^2^0 + 2^0 + # → 2^(2^1 + 1) + 2^2^1 + 1 + # → 2^(2 + 1) + 2^2 + 1 + # → 2^3 + 2^2 + 1 + # → 8 + 4 + 1 + # → 13 + + if I == empty: + return 0 + else: + return sum(2**I(i) for i in x) + +def powers_of_two(x): + """Lists the powers of two the number is composed of""" + assert type(x) == int + + # Look for a power of two bigger than the one we were given + power = 0 + number = 1 + while number < x: + number *= 2 + power += 1 + + # Find all smaller powers of two + powers = [] + while x > 0: + if number <= x: + powers.append(power) + x -= number + + number //= 2 + power -= 1 + + return powers + +def J(x): + """Converts an integer into set representation""" + powers = powers_of_two(x) + return sets.set(J(i) for i in powers) + +def lesser(x, y): + only_x = x.difference(y) + only_y = y.difference(x) + + if only_x is empty and only_y is not empty: + return True + elif only_y is empty: + return False + else: + x_max_power = functools.reduce(lambda a, b: b if lesser(a,b) else a, only_x) + y_max_power = functools.reduce(lambda a, b: b if lesser(a,b) else a, only_y) + return lesser(x_max_power, y_max_power) + +def lshift(x, y): + return sets.set(add(i, y) for i in x) + +def double(x): + return lshift(x, J(1)) + +def rshift(x, y): + return sets.set(sub(i, y) for i in x) + +def add(x, y): + only_x = x.difference(y) + only_y = y.difference(x) + one = only_x.union(only_y) + both = x.intersection(y) + + if both is empty: + return one + else: + return add(one, double(both)) + +def sub(x, y): + assert not lesser(x, y) + only_x = x.difference(y) + only_y = y.difference(x) + + if only_y is empty: + return only_x + else: + return sub(add(only_x, only_y), double(only_y)) + +def mul(x, y): + total = J(0) + for power in x: + total = add(total, lshift(y, power)) + return total + +def divmod(x, y): + assert y is not empty + powers = [] + shift_amount = J(0) + shifted_divisor = y + + while lesser(shifted_divisor, x): + shift_amount = add(shift_amount, J(1)) + shifted_divisor = double(shifted_divisor) + + remaining_dividend = x + while True: + if not lesser(remaining_dividend, shifted_divisor): + powers.append(shift_amount) + remaining_dividend = sub(remaining_dividend, shifted_divisor) + + if shift_amount is empty: + break + + shift_amount = sub(shift_amount, J(1)) + shifted_divisor = rshift(shifted_divisor, J(1)) + + return sets.set(powers), remaining_dividend + +def fibonacci(): + a = J(0) + b = J(1) + + yield a + while True: + yield b + a, b = b, add(a, b) + +def py_fibonacci(): + a = 0 + b = 1 + + yield a + while True: + yield b + a, b = b, a + b + +if __name__ == '__main__': + start_time = time.monotonic() + limit = J(2**128) + for number in fibonacci(): + if lesser(limit, number): + break + #print(I(number), tuple(map(I, number)), str(number)) + print(time.monotonic() - start_time) + + start_time = time.monotonic() + limit = 2**128 + for number in py_fibonacci(): + if limit < number: + break + print(time.monotonic() - start_time) diff --git a/sets.py b/sets.py new file mode 100644 index 0000000..9bc166c --- /dev/null +++ b/sets.py @@ -0,0 +1,180 @@ +import functools +import threading +import weakref + +set_table = weakref.WeakValueDictionary() +set_table_lock = threading.Lock() + +@functools.total_ordering +class InternedSetObject: + def __init__(self, elements): + assert type(elements) == tuple + self.elements = elements + + def __contains__(self, element): + for i in self.elements: + if i == element: + return True + return False + + def __iter__(self): + return iter(self.elements) + + def union(self, other): + assert isinstance(other, InternedSetObject) + + # Since the elements of both sets are already sorted, we can just join them + elements = [] + own_index = 0 + other_index = 0 + while True: + if own_index == len(self.elements): + # Ran out of own elements, add those of the other and exit + elements.extend(other.elements[other_index:]) + break + + elif other_index == len(other.elements): + # Ran out of other's elements, add own and exit + elements.extend(self.elements[own_index:]) + break + + elif self.elements[own_index] == other.elements[other_index]: + # Both have the element, add it once (this takes care of deduplication) + elements.append(self.elements[own_index]) + own_index += 1 + other_index += 1 + + elif self.elements[own_index] < other.elements[other_index]: + # Our element goes first, add it + elements.append(self.elements[own_index]) + own_index += 1 + + else: + # Other's element goes first, add it + elements.append(other.elements[other_index]) + other_index += 1 + + return _new_set(tuple(elements)) + + def intersection(self, other): + assert isinstance(other, InternedSetObject) + + # This works with the same basic idea as union + # The only difference here is that only duplicate elements get added + elements = [] + own_index = 0 + other_index = 0 + while True: + if own_index == len(self.elements): + # Ran out of own elements, exit (since we don't have other's remaining elements) + break + + elif other_index == len(other.elements): + # Ran out of other's elements, exit (since other doesn't have our remaining elements) + break + + elif self.elements[own_index] == other.elements[other_index]: + # Both have the element, add it + elements.append(self.elements[own_index]) + own_index += 1 + other_index += 1 + + elif self.elements[own_index] < other.elements[other_index]: + # Our element goes first, skip it (since other doesn't have it) + own_index += 1 + + else: + # Other's element goes first, skip it (since we don't have it) + other_index += 1 + + return _new_set(tuple(elements)) + + def difference(self, other): + assert isinstance(other, InternedSetObject) + + # This works with the same basic ide as union + # The only difference here is that we never add anything from the other + elements = [] + own_index = 0 + other_index = 0 + while True: + if own_index == len(self.elements): + # Ran out of own elements, exit (since we don't want other's elements) + break + + elif other_index == len(other.elements): + # Ran out of other's elements, add own and exit + elements.extend(self.elements[own_index:]) + break + + elif self.elements[own_index] == other.elements[other_index]: + # Both have the element, skip it + own_index += 1 + other_index += 1 + + elif self.elements[own_index] < other.elements[other_index]: + # Our element goes first, add it + elements.append(self.elements[own_index]) + own_index += 1 + + else: + # Other's element goes first, skip it (since we don't want its element in the final) + other_index += 1 + + return _new_set(tuple(elements)) + + def __eq__(self, other): + assert (id(self) == id(other) and self is other) or id(self) != id(other) + return self is other + + def __lt__(self, other): + return id(self) < id(other) + + def __hash__(self): + return hash(id(self)) + + def __repr__(self): + name = 'InternedSetObject' + if __name__ != '__main__': + name = '%s.%s' % (__name__, name) + + return '%s(%s)' % (name, repr(self.elements)) + + def __str__(self): + if len(self.elements) > 0: + return '{%s}' % ', '.join(map(str, self.elements)) + else: + return '∅' + +def dedup_elements(elements): + """Deduplicates a sorted iterable and returns it as a tuple""" + deduplicated = [] + + for element in elements: + if len(deduplicated) > 0 and element == deduplicated[-1]: + continue + else: + deduplicated.append(element) + + return tuple(deduplicated) + +def _new_set(elements): + """Returns a set corresponding to elements list that is already sorted and deduped""" + global set_table, set_table_lock + + with set_table_lock: + if elements in set_table: + return set_table[elements] + else: + set_object = InternedSetObject(elements) + set_table[elements] = set_object + return set_object + +def set(elements = None): + """Returns an InternedSetObject. The same object will be returned for all equivalent sets.""" + if elements is not None: + elements = dedup_elements(sorted(elements)) + else: + elements = () + + return _new_set(elements)