MA0301/exam_template/python/Relations.py

294 lines
10 KiB
Python

from sys import argv
from pathlib import Path
from common import printc, printerr, replaceContent
import Graph
# Increase if hasse diagram becomes too clobbered
def pairToString(pair):
if str(pair[0]).isdigit() or str(pair[1]).isdigit():
return f'({pair[0]},{pair[1]})'
else:
return pair[0] + pair[1]
def stringToPair(string):
if string[0] == '(':
return tuple(string.split(',')[0][1:], string.split(',')[0][:-1])
else:
return tuple(list(string))
def textLatex(string):
return '\\text{' + string + '}'
def mathModeListLatex(elements):
if len(elements) > 7 or len(elements) and max(len(str(e)) for e in elements) > 10:
return '\\begin{gather*}\n' \
+ ' \\\\\n'.join(elements) + '\n' \
+ '\\end{gather*}'
else:
return f'\\[ {", ".join(elements)} \\]'
class Relation:
def __init__(self, pairs): # pair = (str,str)
self.pairs = set(pairs)
def __str__(self):
return f'\\{{ {", ".join(x + y for x,y in self.pairs)} \}}'
@classmethod
def fromString(cls, string):
relations = (stringToPair(x) for x in string.split(' '))
return cls(relations)
@classmethod
def fromDivisibilityPosetTo(cls, n):
pairs = set()
for dst in range(n):
pairs.add((dst, dst))
for src in range(1, dst):
if dst % src == 0:
pairs.add((src, dst))
return cls(pairs)
def isReflexive(self, verbose=False):
elements = set(list(sum(self.pairs, ())))
result = all((x,x) in self.pairs for x in elements)
if not result:
printc('Not reflexive, missing following pairs:', color='green')
missingPairs = [(x,x) for x in elements if (x,x) not in self.pairs]
print(f'\\[ {", ".join(pairToString(p) for p in missingPairs)} \\]')
if verbose:
return (False, missingPairs)
if verbose:
return (True, [(x,x) for x in elements if (x,x) in self.pairs])
return result
def isSymmetric(self, verbose=False):
result = all((y,x) in self.pairs for x,y in self.pairs)
if not result:
printc('Not symmetric, missing following pairs:', color='green')
missingPairs = [(x,y) for x,y in self.pairs if (y,x) not in self.pairs]
print(f'\\[ {", ".join(pairToString(p) for p in missingPairs)} \\]')
if verbose:
return (False, missingPairs)
if verbose:
return (True, [((x,y), (y,x)) for x,y in self.pairs if (y,x) in self.pairs and x < y])
return result
def isAntiSymmetric(self, verbose=False):
result = not any((y,x) in self.pairs and y != x for x,y in self.pairs)
if not result:
printc('Not antisymmetric, following pairs are symmetric:', color='green')
symmetricPairs = [((x,y), (y,x)) for x,y in self.pairs if (y,x) in self.pairs and y > x]
print(f'\\[ {", ".join(f"""{pairToString(p)}{textLatex(" and ")}{pairToString(q)}""" for p,q in symmetricPairs)} \\]')
if verbose:
return (False, symmetricPairs)
if verbose:
return (True, [])
return result
def isTransitive(self, verbose=False):
result = True
transitivePairs = []
nonTransitivePairs = []
for x,y in self.pairs:
for z,w in self.pairs:
if not (y != z or ((x,w) in self.pairs)):
nonTransitivePairs.append(((x,y), (z,w)))
result = False
elif (y == z and x != y != w and ((x,w) in self.pairs)):
transitivePairs.append(((x,y), (z,w)))
if not result:
printc('Not transitive, following pairs are missing its transitive counterpart:', color='green')
print(f'\\[ {", ".join(f"""{pairToString(p)}{textLatex(" and ")}{pairToString(q)}{textLatex(" without ")}{pairToString((p[0], q[1]))}""" for p,q in nonTransitivePairs)} \\]')
if verbose:
return (False, nonTransitivePairs)
if verbose:
return (True, transitivePairs)
return result
def isEquivalenceRelation(self, verbose=False):
if verbose:
isReflexive, reflexivePairs = self.isReflexive(verbose=True)
isSymmetric, symmetricPairs = self.isSymmetric(verbose=True)
isTransitive, transitivePairs = self.isTransitive(verbose=True)
if not isReflexive:
return 'The relation is not an equivalence relation, because it is not reflexive.'
+ ' The following elements should be related:\n\n'
+ f'\\[ {", ".join(pairToString(p) for p in reflexivePairs)} \\]'
if not isSymmetric:
return 'The relation is not an equivalence relation, because it is not symmetric.'
+ ' It is missing the following symmetric pairs of relations\n\n'
+ f'\\[ {", ".join(f"""{pairToString(p)}{textLatex(" and ")}{pairToString(q)}""" for p,q in symmetricPairs)} \\]'
if not isTransitive:
return 'The relation is not an equivalence relation, because it is not transitive.'
+ ' The following pairs of relations are missing its transitive counterpart\n\n'
+ f'\\[ {", ".join(f"""{pairToString(p)}{textLatex(" and ")}{pairToString(q)}{textLatex(" without ")}{pairToString((p[0], q[1]))}""" for p,q in transitivePairs)} \\]'
rxStr = mathModeListLatex([pairToString(p) for p in reflexivePairs])
smStr = mathModeListLatex([f"""{pairToString(p)}{textLatex(" and ")}{pairToString(q)}""" for p,q in symmetricPairs])
trStr = mathModeListLatex([f"""{pairToString(p)}{textLatex(" and ")}{pairToString(q)}{textLatex(" with ")}{pairToString((p[0], q[1]))}""" for p,q in transitivePairs])
return replaceContent(
(rxStr, smStr, trStr),
'Relations/EquivalenceProof',
lambda temp, cont: temp.replace('%REFLEXIVE', cont[0]).replace('%SYMMETRIC', cont[1]).replace('%TRANSITIVE', cont[2])
)
return self.isReflexive() and self.isSymmetric() and self.isTransitive()
def isPartialOrder(self, verbose=False):
result = self.isReflexive() and self.isAntiSymmetric() and self.isTransitive()
if result:
hasse= self.getHassePairs(checkIfPartialOrder=False)
min_el = set(a for a,b in hasse if a not in list(zip(*hasse))[1])
printc(f"Minimal elements: $\{{ {', '.join(str(e) for e in min_el)} \}}$ \\\\")
max_el = set(v for k,v in hasse if v not in (x for x,_ in hasse))
printc(f"Maximal elements: $\{{ {', '.join(str(e) for e in max_el)} \}}$" )
if verbose:
isReflexive, reflexivePairs = self.isReflexive(verbose=True)
isAntiSymmetric, antiSymmetricPairs = self.isAntiSymmetric(verbose=True)
isTransitive, transitivePairs = self.isTransitive(verbose=True)
if not isReflexive:
return 'The relation is not a partial order, because it is not reflexive.'
+ ' The following elements should be related:\n\n'
+ f'\\[ {", ".join(pairToString(p) for p in reflexivePairs)} \\]'
if not isAntiSymmetric:
return 'The relation is not a partial order, because it is not antisymmetric.'
+ ' The following relations are symmetric\n\n'
+ f'\\[ {", ".join(f"""{pairToString(p)}{textLatex(" and ")}{pairToString(q)}""" for p,q in antiSymmetricPairs)} \\]'
if not isTransitive:
return 'The relation is not a partial order, because it is not transitive.'
+ ' The following pairs of relations are missing its transitive counterpart\n\n'
+ f'\\[ {", ".join(f"""{pairToString(p)}{textLatex(" and ")}{pairToString(q)}{textLatex(" without ")}{pairToString((p[0], q[1]))}""" for p,q in transitivePairs)} \\]'
rxStr = mathModeListLatex([pairToString(p) for p in reflexivePairs])
trStr = mathModeListLatex([f"""{pairToString(p)}{textLatex(" and ")}{pairToString(q)}{textLatex(" with ")}{pairToString((p[0], q[1]))}""" for p,q in transitivePairs])
return replaceContent(
(rxStr, trStr),
'Relations/PosetProof',
lambda temp, cont: temp.replace('%REFLEXIVE', cont[0]).replace('%TRANSITIVE', cont[1])
)
return result
def getHassePairs(self, checkIfPartialOrder=True):
if checkIfPartialOrder and not self.isPartialOrder():
printerr('This is not a partial order')
return
nonReflexivePairs = set((x,y) for x,y in self.pairs if x != y)
hassePairs = set()
for x1, y1 in nonReflexivePairs:
for x2, y2 in nonReflexivePairs:
if y1 == x2:
hassePairs.add((x1, y2))
return nonReflexivePairs - hassePairs
def latexifyHasseDiagram(self):
hasse = self.getHassePairs()
keys = set(item for tup in hasse for item in tup)
y_pos = dict.fromkeys(keys, 0)
i = 0
while len(next_row := [val for key,val in hasse if key in [x for x,y in y_pos.items() if y == i] ]) != 0:
for item in next_row:
y_pos[item] = i + 1
i += 1
inv_ypos = dict()
for key in set(y_pos.values()):
inv_ypos[key] = [x for x,y in y_pos.items() if y == key]
output = []
for y in inv_ypos.keys():
for i, n in enumerate(inv_ypos[y]):
output.append(f'\\node ({n}) at ({i - len(inv_ypos[y])/2}, {y * 0.5 * (len(hasse) ** 0.4)}) {{${n}$}};')
output.append('')
for x,y in hasse:
output.append(f'\\draw ({x}) -- ({y});')
return '\n'.join(output)
def latexifyGraph(self):
if self.isEquivalenceRelation():
graphType = 'undirected'
pairs = [(x,y) for x,y in self.pairs if x != y]
else:
graphType = 'directed'
pairs = self.pairs
nodes = set()
for x,y in pairs:
nodes.add(x)
nodes.add(y)
edges = "\n".join(pairToString(p) for p in pairs)
processingString = f'toGraph\n\n{graphType}\n\n{ " ".join(nodes) }\n\n{edges}'
return Graph.processFileContent(processingString)
def processFileContent(raw):
outputType, inp = raw.split('\n\n')
if inp.startswith('divisibilityPosetTo'):
n = int(inp.split(' ')[1])
relation = Relation.fromDivisibilityPosetTo(n)
else:
relation = Relation.fromString(inp)
if outputType == 'proveEquivalence':
content = relation.isEquivalenceRelation(verbose=True)
return content
elif outputType == 'provePoset':
content = relation.isPartialOrder(verbose=True)
return content
elif outputType == 'hasseDiagram':
content = relation.latexifyHasseDiagram()
return replaceContent(content, 'Relations/Hasse')
elif outputType == 'graph':
content = relation.latexifyGraph()
return content
if __name__ == '__main__':
pass
# inp = 'AA BB CC DD AB AC AD BC BD CD'
# relations = [stringToPair(p) for p in inp.split(' ')]
# # print(Relation(relations).isEquivalenceRelation())
# print(Relation(relations).isPartialOrder())
# print(Relation.fromDivisibilityPosetTo(30).isPartialOrder())