forked from caten/DiscreteNeuralNets
304 lines
11 KiB
Python
304 lines
11 KiB
Python
"""
|
|
Polymorphisms
|
|
"""
|
|
from relations import Relation
|
|
from operations import Operation, Projection
|
|
import random
|
|
import numpy
|
|
from pathlib import Path
|
|
import json
|
|
import itertools
|
|
|
|
|
|
def quarter_turn(rel):
|
|
"""
|
|
Rotate a binary relation by a quarter turn counterclockwise.
|
|
|
|
Args:
|
|
rel (Relation): The binary relation to be rotated.
|
|
|
|
Returns:
|
|
Relation: The same relation rotated by a quarter turn counterclockwise.
|
|
"""
|
|
|
|
return Relation(((rel.universe_size - tup[1], tup[0]) for tup in rel),
|
|
rel.universe_size, rel.arity)
|
|
|
|
|
|
class RotationAutomorphism(Operation):
|
|
"""
|
|
An automorphism of the Hamming graph obtained by rotating an image.
|
|
"""
|
|
|
|
def __init__(self, k=1):
|
|
"""
|
|
Create a rotation automorphism.
|
|
|
|
Argument:
|
|
k (int): The number of quarter turns by which to rotate the image
|
|
counterclockwise.
|
|
"""
|
|
|
|
def func(x):
|
|
for _ in range(k % 4):
|
|
x = quarter_turn(x)
|
|
return x
|
|
|
|
Operation.__init__(self, 1, func=func)
|
|
|
|
|
|
class ReflectionAutomorphism(Operation):
|
|
"""
|
|
An automorphism of the Hamming graph obtained by reflecting an image across
|
|
its vertical axis of symmetry.
|
|
"""
|
|
|
|
def __init__(self):
|
|
"""
|
|
Create a reflection automorphism.
|
|
"""
|
|
|
|
Operation.__init__(self, 1,
|
|
lambda rel: Relation(((rel.universe_size - tup[0], tup[1])
|
|
for tup in rel), rel.universe_size, rel.arity))
|
|
|
|
|
|
class SwappingAutomorphism(Operation):
|
|
"""
|
|
An automorphism of the Hamming graph obtained by taking the componentwise
|
|
xor with a fixed relation.
|
|
"""
|
|
|
|
def __init__(self, b):
|
|
"""
|
|
Create a swapping automorphism for a given relation.
|
|
|
|
Argument:
|
|
b (Relation): The fixed relation used for swapping. This must have
|
|
the same universe and arity as the argument passed to the
|
|
automorphism.
|
|
"""
|
|
|
|
Operation.__init__(self, 1, lambda a: a ^ b)
|
|
|
|
|
|
class BlankingEndomorphism(Operation):
|
|
"""
|
|
An endomorphism of the Hamming graph obtained by taking the intersection
|
|
with a fixed relation.
|
|
"""
|
|
|
|
def __init__(self, b):
|
|
"""
|
|
Create a blanking endomorphism for a relation.
|
|
|
|
Argument:
|
|
b (Relation): The fixed relation used for blanking pixels.
|
|
"""
|
|
|
|
Operation.__init__(self, 1, lambda a: a & b)
|
|
|
|
|
|
def indicator_polymorphism(tup, a, b):
|
|
"""
|
|
Perform an indicator polymorphism where the output is either an empty
|
|
relation or a relation containing a single tuple.
|
|
|
|
Args:
|
|
tup (tuple of int): The single tuple in question.
|
|
a (iterable of Relation): A sequence of relations, thought of as inputs
|
|
to the polymorphism.
|
|
b (iterable of Relation): A sequence of Relations with which dot
|
|
products are to be taken, thought of as constants. This should be
|
|
the same length as `a`.
|
|
|
|
Returns:
|
|
Relation: The relation obtained by applying the indicator polymorphism.
|
|
"""
|
|
|
|
a = tuple(a)
|
|
universe_size = a[0].universe_size
|
|
if all(rel[0].dot(rel[1]) for rel in zip(a, b)):
|
|
return Relation((tup,), universe_size)
|
|
else:
|
|
return Relation(tuple(), universe_size, len(tup))
|
|
|
|
|
|
class IndicatorPolymorphism(Operation):
|
|
"""
|
|
Create a polymorphism of the Hamming graph by taking dot products with
|
|
fixed relations.
|
|
"""
|
|
|
|
def __init__(self, tup, b):
|
|
"""
|
|
Create an indicator polymorphism where the output is either an empty
|
|
relation or a relation containing a single tuple.
|
|
|
|
Args:
|
|
tup (tuple of int): The single tuple in question.
|
|
b (iterable of Relation): A sequence of Relations with which dot
|
|
products are to be taken, thought of as constants. Should
|
|
contain at least one entry.
|
|
"""
|
|
|
|
Operation.__init__(self, len(b),
|
|
lambda *a: indicator_polymorphism(tup, a, b))
|
|
|
|
|
|
def dominion_polymorphism(dominion_filename, dominion_num,
|
|
homomorphism_filename, a, b):
|
|
"""
|
|
Perform a dominion polymorphism by using the given files.
|
|
|
|
Args:
|
|
dominion_filename (str): The name of the file where dominions are
|
|
stored.
|
|
dominion_num (int): The number of the dominion to use.
|
|
homomorphism_filename (str): The name of the file where homomorphisms
|
|
are stored.
|
|
a (Relation): The first argument to the polymorphism.
|
|
b (Relation): The second argument to the polymorphism.
|
|
|
|
Returns:
|
|
Relation: The result of performing the dominion polymorphism.
|
|
"""
|
|
|
|
with open(str(Path(__file__).parent.resolve()) +
|
|
'//..//output//{}.json'.format(dominion_filename), 'r') as read_file:
|
|
unprocessed_dominion = itertools.islice(read_file, dominion_num,
|
|
dominion_num + 1).__next__()
|
|
tree_num, dominion = json.loads(unprocessed_dominion)
|
|
label = dominion[len(a)][len(b)]
|
|
with open(str(Path(__file__).parent.resolve()) +
|
|
'//..//output//{}.json'.format(homomorphism_filename), 'r') as read_file:
|
|
for line in read_file:
|
|
line_tree_num, values = json.loads(line)
|
|
# Note that this always takes the first homomorphism in a given
|
|
# file with a given tree number, even if more than one is present.
|
|
if line_tree_num == tree_num:
|
|
return Relation(values[label], a.universe_size, a.arity)
|
|
|
|
|
|
class DominionPolymorphism(Operation):
|
|
"""
|
|
A dominion polymorphism.
|
|
"""
|
|
|
|
def __init__(self, dominion_filename, dominion_num, homomorphism_filename):
|
|
"""
|
|
Create a dominion polymorphism which uses a specified dominion in
|
|
memory.
|
|
|
|
Arguments:
|
|
dominion_filename (str): The name of the file where dominions are
|
|
stored.
|
|
dominion_num (int): The number of the dominion to use.
|
|
homomorphism_filename (str): The name of the file where
|
|
homomorphisms are stored.
|
|
"""
|
|
|
|
Operation.__init__(self, 2,
|
|
lambda a, b: dominion_polymorphism(dominion_filename, dominion_num,
|
|
homomorphism_filename, a, b))
|
|
|
|
|
|
def polymorphism_neighbor_func(op, num_of_neighbors, constant_relations,
|
|
dominion_filename=None, homomorphism_filename=None):
|
|
"""
|
|
Find the neighbors of a given polymorphism of the Hamming graph. Currently,
|
|
this assumes relations are all binary. There is also an implicit assumption
|
|
here that dominion polymorphisms should be binary operations. This could be
|
|
changed as well, but likely is not necessary.
|
|
|
|
Arguments:
|
|
homomorphism_filename:
|
|
op (Operation): A Hamming graph polymorphism operation.
|
|
num_of_neighbors (int): The amount of possible neighbors to generate.
|
|
constant_relations (iterable of Relation): An iterable of relations to
|
|
use as constants. This is assumed to have nonzero length.
|
|
dominion_filename (None | str): The name of the file where dominions
|
|
are stored, or None. If None, dominion polymorphisms are not used.
|
|
homomorphism_filename (None | str): The name of the file where
|
|
homomorphisms are stored, or None. If None, dominion polymorphisms
|
|
are not used.
|
|
|
|
Yields:
|
|
Operation: A neighboring operation to the given one.
|
|
"""
|
|
|
|
endomorphisms = []
|
|
endomorphisms += [RotationAutomorphism(k) for k in range(4)]
|
|
endomorphisms.append(ReflectionAutomorphism())
|
|
endomorphisms.append('Swapping')
|
|
endomorphisms.append('Blanking')
|
|
constant_relations = tuple(constant_relations)
|
|
universe_size = constant_relations[0].universe_size
|
|
arity = constant_relations[0].arity
|
|
yield op
|
|
for _ in range(num_of_neighbors):
|
|
twist = random.randint(0, 1)
|
|
if twist:
|
|
endomorphisms_to_use = (op.arity + 1)*[RotationAutomorphism(0)]
|
|
twist_spot = random.randint(0, op.arity - 1)
|
|
endomorphisms_to_use[twist_spot] = random.choice(endomorphisms)
|
|
for i in range(len(endomorphisms_to_use)):
|
|
if endomorphisms_to_use[i] == 'Blanking':
|
|
endomorphisms_to_use[i] = \
|
|
BlankingEndomorphism(random.choice(constant_relations))
|
|
if endomorphisms_to_use[i] == 'Swapping':
|
|
endomorphisms_to_use[i] = \
|
|
SwappingAutomorphism(random.choice(constant_relations))
|
|
for i in range(len(endomorphisms_to_use) - 1):
|
|
endomorphisms_to_use[i] = \
|
|
endomorphisms_to_use[i][Projection(op.arity, i)]
|
|
yield endomorphisms_to_use[-1][op[endomorphisms_to_use[:-1]]]
|
|
else:
|
|
if op.arity == 1:
|
|
random_endomorphism = random.choice(endomorphisms)
|
|
if random_endomorphism == 'Blanking':
|
|
random_endomorphism = \
|
|
BlankingEndomorphism(random.choice(constant_relations))
|
|
if random_endomorphism == 'Swapping':
|
|
random_endomorphism = \
|
|
SwappingAutomorphism(random.choice(constant_relations))
|
|
yield random_endomorphism
|
|
if op.arity >= 2:
|
|
# Choose a dominion polymorphism 50% of the time, if they are
|
|
# available.
|
|
if random.randint(0, 1) and dominion_filename \
|
|
and homomorphism_filename:
|
|
with open(str(Path(__file__).parent.resolve()) + \
|
|
'//..//output//{}.json'.format(dominion_filename), 'r') \
|
|
as read_file:
|
|
num_of_dominions = sum(1 for _ in read_file)
|
|
dominion_num = random.randint(0, num_of_dominions - 1)
|
|
yield DominionPolymorphism(dominion_filename, dominion_num,
|
|
homomorphism_filename)
|
|
else:
|
|
# The universe size and relation arity for the indicator
|
|
# polymorphisms is read off from the `constant_relations`.
|
|
yield IndicatorPolymorphism(tuple(
|
|
random.randrange(universe_size) for _ in range(arity)),
|
|
random.choices(constant_relations, k=op.arity))
|
|
|
|
|
|
def hamming_loss(x, y):
|
|
"""
|
|
Compute the average Hamming loss between two iterables of relations. It is
|
|
assumed that `x` and `y` have the same length and that corresponding pairs
|
|
of relations in `x` and `y` have the same arity so that their symmetric
|
|
difference may be taken. In practice, this is always used when all the
|
|
relations belonging to `x` and `y` have the same arity.
|
|
|
|
Args:
|
|
x (iterable of Relation): A sequence of relations.
|
|
y (iterable of Relation): Another sequence of relations.
|
|
|
|
Returns:
|
|
numpy.float64: The average size of the symmetric difference of
|
|
corresponding pairs of relations in `x` and `y`.
|
|
"""
|
|
|
|
return numpy.average(tuple(len(rel0 ^ rel1) for (rel0, rel1) in zip(x, y)))
|