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