DiscreteNeuralNets/src/polymorphisms.py
2025-07-20 14:20:25 -06:00

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)))