From f0b7f00632cb8e8de3d893643b2a28c5caadb9dd Mon Sep 17 00:00:00 2001 From: solaman Date: Tue, 31 Mar 2015 22:08:53 -0500 Subject: [PATCH 1/4] Adding my project to repo --- .project | 11 + src/solamanDedekind/.gitignore | 28 ++ .../DedekindLattice/DedekindAnalysis.py | 25 ++ .../DedekindLattice/DedekindLattice.py | 141 ++++++++++ .../DedekindLatticeTest.py | 25 ++ .../DedekindLatticeTest/DedekindNodeTest.py | 124 +++++++++ .../DedekindLatticeTest/__init__.py | 0 .../DedekindLatticeTest/n_4.world_57344.dot | 53 ++++ .../DedekindLatticeTest/n_4.world_57344.pdf | Bin 0 -> 10360 bytes .../DedekindLatticeTest/writeToDotTest.dot | 53 ++++ .../DedekindLattice/DedekindNode.py | 256 ++++++++++++++++++ .../GeneratedDedekindLattices/preserve.txt | 6 + .../DedekindLattice/TransitionError.py | 16 ++ src/solamanDedekind/README.md | 10 + 14 files changed, 748 insertions(+) create mode 100644 .project create mode 100644 src/solamanDedekind/.gitignore create mode 100644 src/solamanDedekind/DedekindLattice/DedekindAnalysis.py create mode 100644 src/solamanDedekind/DedekindLattice/DedekindLattice.py create mode 100644 src/solamanDedekind/DedekindLattice/DedekindLatticeTest/DedekindLatticeTest.py create mode 100644 src/solamanDedekind/DedekindLattice/DedekindLatticeTest/DedekindNodeTest.py create mode 100644 src/solamanDedekind/DedekindLattice/DedekindLatticeTest/__init__.py create mode 100644 src/solamanDedekind/DedekindLattice/DedekindLatticeTest/n_4.world_57344.dot create mode 100644 src/solamanDedekind/DedekindLattice/DedekindLatticeTest/n_4.world_57344.pdf create mode 100644 src/solamanDedekind/DedekindLattice/DedekindLatticeTest/writeToDotTest.dot create mode 100644 src/solamanDedekind/DedekindLattice/DedekindNode.py create mode 100644 src/solamanDedekind/DedekindLattice/GeneratedDedekindLattices/preserve.txt create mode 100644 src/solamanDedekind/DedekindLattice/TransitionError.py create mode 100644 src/solamanDedekind/README.md diff --git a/.project b/.project new file mode 100644 index 0000000..64dc59f --- /dev/null +++ b/.project @@ -0,0 +1,11 @@ + + + DedekindNumber + + + + + + + + diff --git a/src/solamanDedekind/.gitignore b/src/solamanDedekind/.gitignore new file mode 100644 index 0000000..002098f --- /dev/null +++ b/src/solamanDedekind/.gitignore @@ -0,0 +1,28 @@ +# Compiled source # +################### +*.com +*.class +*.dll +*.exe +*.o +*.so +*.pyc + +# OS generated files # +###################### +.DS_Store +.DS_Store? +._* +.Spotlight-V100 +.Trashes +ehthumbs.db +Thumbs.db + +# Output Files # +################ +DedekindLattice/GeneratedDedekindLattices/* +!DedekindLattice/GeneratedDedekindLattices/preserve.txt + +# Experimental Files # +###################### +**/.noworkflow/** \ No newline at end of file diff --git a/src/solamanDedekind/DedekindLattice/DedekindAnalysis.py b/src/solamanDedekind/DedekindLattice/DedekindAnalysis.py new file mode 100644 index 0000000..4eaa890 --- /dev/null +++ b/src/solamanDedekind/DedekindLattice/DedekindAnalysis.py @@ -0,0 +1,25 @@ +''' +Created on Mar 13, 2015 + +@author: Solaman +''' +import cProfile +from DedekindNode import DedekindNode +from DedekindLattice import DedekindLattice + +node = DedekindNode(4, [15]) +lattice = DedekindLattice(5) + +def analyzeDedekindNode(): + cProfile.run("node.generatePossibleConfigurations()") + +def analyzeDedekindLattice(): + cProfile.run("lattice.fillLattice()") + +def analyzeFindUniqueFunctions(): + cProfile.run("lattice.findUniqueFunctions()") + +if __name__ == '__main__': + #analyzeDedekindNode() + #analyzeDedekindLattice() + analyzeFindUniqueFunctions() \ No newline at end of file diff --git a/src/solamanDedekind/DedekindLattice/DedekindLattice.py b/src/solamanDedekind/DedekindLattice/DedekindLattice.py new file mode 100644 index 0000000..b2a740e --- /dev/null +++ b/src/solamanDedekind/DedekindLattice/DedekindLattice.py @@ -0,0 +1,141 @@ +''' +Created on Feb 26, 2015 + +@author: Solaman +''' +import TransitionError +from DedekindNode import DedekindNode + +class DedekindLattice(object): + ''' + We aim to generate the Dedekind Lattices using this class. Namely, + We want to generate all monotone boolean functions given an n input size. + Currently, we will aim to only generate the lattices in working memory. + Future implementations will hopefully be able to dynamically + Generate a node of a given Lattice. + ''' + + + def __init__(self, inputSize): + ''' + Constructor. For now, we will store each monotone boolean function + as an object. Future implementations will store them as a single bit + for lean memory usage + ''' + if inputSize < 0: + raise Exception("Input size must be greater than or equal to 0") + self.lattice = {} + + #bit mask refers to the possible bit values + #of a given configuration. E.G. boolean functions with 4 inputs + #Will have a bit mask of 0xF + self.bitMask = 2**(inputSize) - 1 + self.inputSize = inputSize + + self.emptyFunction = DedekindNode(self.inputSize, []) + self.lattice[ self.emptyFunction.getIndex()] = self.emptyFunction + + self.baseFunction = DedekindNode(self.inputSize, [self.bitMask]) + self.lattice[ self.baseFunction.getIndex()] = self.baseFunction + + def getNextNode(self): + ''' + Returns the most recently added node to the queue + if it is not empty. + ''' + if self.nodeList == []: + return None + node = self.nodeList.pop() + children = node.generateChildren() + for child in children: + self.nodeList.append(child) + self.lattice[child.getIndex()] = child + return node + + + def fillLattice(self): + self.nodeList = [] + self.nodeList.append(self.baseFunction) + while self.getNextNode() != None: + x = 1 + self.monotoneCount= len( self.lattice.values()) + + def findUniqueFunctions(self): + #We don't need to compute functions that are isomorphisms of each other. + #We store each function by there level, and then counts by the number of possible children for the function + #It is proven that functions by level that have the same number of possible children are isomorphisms + #{"level" : {"isomorphismCount": count, "children" : children } } + self.nodeList = [] + functionCount = 1 + self.childrenCounts = {} + + self.baseFunction.isVisited = False + self.baseFunction.parent = None + self.nodeList.append(self.baseFunction) + + while self.nodeList != []: + node = self.nodeList.pop() + if node.isVisited == True: + if node.parent == None: + functionCount += node.childrenCount + print functionCount + continue + else: + node.parent.childrenCount += node.childrenCount + key = self.getKey(node) + self.childrenCounts[ node.level][key] = node.childrenCount + continue + + node.level = len(node.acceptedConfigurations) - 1 + if node.level not in self.childrenCounts: + self.childrenCounts [ node.level] = {} + + node.possibleConfigurations = node._generatePossibleConfigurations() + node.possibleConfigurationSize = len(node.possibleConfigurations) + key = self.getKey(node) + if key in self.childrenCounts[node.level]: + node.parent.childrenCount += self.childrenCounts[node.level][ key] + else: + children = node.generateChildren() + node.childrenCount = 1 + node.isVisited = True + self.nodeList.append(node) + for child in children: + child.parent = node + child.isVisited = False + self.nodeList.append(child) + + def getKey(self, node): + return str(len(node.acceptedConfigurations[node.level])) + "-" + str(node.possibleConfigurationSize) + + + + + + def generateDotFiles(self): + import os + directoryName = os.path.join("GeneratedDedekindLattices", str(self.inputSize) + "_DedekindLattice") + if not os.path.exists(directoryName): + os.mkdir(directoryName) + updateTime = self.monotoneCount/10 + generatedFiles = 0 + for function in self.lattice.itervalues(): + function.writeToDotFile(directoryName) + generatedFiles += 1 + if generatedFiles % updateTime == 0: + print generatedFiles, " written so far" + + + +if __name__ == "__main__": + import sys + if len(sys.argv) < 2: + print "Error: must provide the input size of the Dedekind Lattice" + inputSize = sys.argv[1] + dedekind = DedekindLattice(int(inputSize)) + dedekind.fillLattice() + + if len(sys.argv) > 2: + if sys.argv[2] == "true": + dedekind.generateDotFiles() + print dedekind.monotoneCount \ No newline at end of file diff --git a/src/solamanDedekind/DedekindLattice/DedekindLatticeTest/DedekindLatticeTest.py b/src/solamanDedekind/DedekindLattice/DedekindLatticeTest/DedekindLatticeTest.py new file mode 100644 index 0000000..6b7de90 --- /dev/null +++ b/src/solamanDedekind/DedekindLattice/DedekindLatticeTest/DedekindLatticeTest.py @@ -0,0 +1,25 @@ +''' +Created on Mar 2, 2015 + +@author: Solaman +''' +import unittest +from DedekindLattice import DedekindLattice + +class Test(unittest.TestCase): + + def setUp(self): + self.dedekindLattice = DedekindLattice(3) + + + def tearDown(self): + pass + + + def testConstructor(self): + pass + + +if __name__ == "__main__": + #import sys;sys.argv = ['', 'Test.testConstructor'] + unittest.main() \ No newline at end of file diff --git a/src/solamanDedekind/DedekindLattice/DedekindLatticeTest/DedekindNodeTest.py b/src/solamanDedekind/DedekindLattice/DedekindLatticeTest/DedekindNodeTest.py new file mode 100644 index 0000000..bb0aa28 --- /dev/null +++ b/src/solamanDedekind/DedekindLattice/DedekindLatticeTest/DedekindNodeTest.py @@ -0,0 +1,124 @@ +''' +Created on Mar 3, 2015 + +@author: Solaman +''' +import unittest +from DedekindNode import DedekindNode + +class Test(unittest.TestCase): + + + def setUp(self): + self.invalidConfigurations = [15, 14, 13, 4 ] + self.unorderedConfigurations = [14, 13, 15] + self.orderedConfigurationsByLevel = [ [15], \ + [14, 13] \ + ] + self.orderedConfigurations = [15, 14, 13] + self.levelOneConfigurations = [ 14, 13, 11, 7] + + self.dedekindNode = DedekindNode(4, self.orderedConfigurations) + + def tearDown(self): + pass + + def testGeneratePossibleConfigurationsFullLevel(self): + ''' + Given that we have all combinations of a level k, + Do we get all combinations at level k+1? + (here, input size n=4, so we have k=2 and k+1=3) + ''' + configurationsUpToLevel3 = [15, 14, 13, 11, 7, 10, 12, 6, 5, 9, 3] + node = DedekindNode(4, configurationsUpToLevel3) + possibleCombinations = node._generatePossibleConfigurations() + self.assertEquals(set(possibleCombinations), set([8, 4, 2, 1])) + + def testGeneratePossibleConfigurationsSatisfiedLevelNoOutput(self): + ''' + When at level k, possible level k+1 configurations are a boolean + "and" of k+1 of the level k configurations such that they belong + at level k+1. E.G. + 11100 & + 00111 & + 01110 = + 00100 + Which is not a valid k+1 configuration, so it is not included + ''' + configurations = [31, 30, 29, 27, 23, 15, 28, 7, 14] + node = DedekindNode(5, configurations) + possibleCombinations = node._generatePossibleConfigurations() + self.assertEquals(possibleCombinations, []) + + def testGeneratePossibleConfigurationsSatisfiedLevelOutput(self): + ''' + When at level k, possible level k+1 configurations are a boolean + "and" of k+1 of the level k configurations such that they belong + at level k+1. E.G. + 11100 & + 01110 & + 01101 = + 01100 + Which is a valid k+1 configuration, so it should be included + ''' + configurations = [31, 30, 29, 27, 23, 15, 28, 14, 13] + node = DedekindNode(5, configurations) + possibleCombinations = node._generatePossibleConfigurations() + self.assertEquals(possibleCombinations, [12]) + + configurations= [31, 30, 29, 27, 23, 15, 28, 7, 14, 22, 13 ] + node = DedekindNode(5, configurations) + possibleCombinations = node._generatePossibleConfigurations() + self.assertEquals( set(possibleCombinations), set([12, 6])) + + def testGenerateChildren(self): + ''' + Because GenerateChildren relies on generatePossibleConfigurations, + it is sufficient to test that, given that there are more than one child + they are generated + ''' + configurations = [31, 30, 29, 27, 23, 15, 28, 7, 14, 22, 13 ] + node = DedekindNode(5, configurations) + children = node.generateChildren() + children = sorted(children, key = lambda child: child.getIndex()) + self.assertEquals( set(children[0].acceptedConfigurations[-1]), set([6])) + self.assertEquals( set(children[1].acceptedConfigurations[-1]), set([12])) + self.assertEquals( set(children[2].acceptedConfigurations[-1]), set([6, 12])) + + + def testGetLevelOneConfigurations(self): + self.assertEquals(self.dedekindNode.getLevelOneConfigurations(), self.levelOneConfigurations) + + def testGetIndex(self): + self.assertEquals(self.dedekindNode.getIndex(), 57344) + + def testAcceptedConfigurationsAsList(self): + self.assertEquals(self.dedekindNode.acceptedConfigurationsAsList(), self.orderedConfigurations) + + def testIsConsistent(self): + configurations = [31, 30, 29, 27, 23, 15, 28, 7, 14, 22, 13 ] + node = DedekindNode(5, configurations) + self.assertTrue( node.isConsistent(29)) + self.assertFalse(node.isConsistent(8)) + +# def testUnorderedConfigurations(self): +# dedekindNode = DedekindNode( 4, self.unorderedConfigurations) +# self.assertEquals( dedekindNode.acceptedConfigurations, self.orderedConfigurationsByLevel) + + def testBadConfigurations(self): + ''' + Tests if the constructor will raise an error if a bad list + of configurations is given (one configuration is two levels above all others) + ''' + self.assertRaises( Exception, DedekindNode, (4, self.invalidConfigurations) ) + + def testWriteToDotFile(self): + self.dedekindNode.writeToDotFile("") + testFile = open("writeToDotTest.dot").read() + toTestFile = open("n_4.world_57344.dot").read() + self.assertEquals(testFile, toTestFile) + + +if __name__ == "__main__": + #import sys;sys.argv = ['', 'Test.testBadConfigurations'] + unittest.main() \ No newline at end of file diff --git a/src/solamanDedekind/DedekindLattice/DedekindLatticeTest/__init__.py b/src/solamanDedekind/DedekindLattice/DedekindLatticeTest/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/src/solamanDedekind/DedekindLattice/DedekindLatticeTest/n_4.world_57344.dot b/src/solamanDedekind/DedekindLattice/DedekindLatticeTest/n_4.world_57344.dot new file mode 100644 index 0000000..b828616 --- /dev/null +++ b/src/solamanDedekind/DedekindLattice/DedekindLatticeTest/n_4.world_57344.dot @@ -0,0 +1,53 @@ +digraph{ + rankdir=BT + node[shape=circle, style=filled, label=""] + edge[dir=none] +1111 [ color = green, label = "1111"] +0111 [ color = red, label = "0111"] +1011 [ color = red, label = "1011"] +1101 [ color = green, label = "1101"] +1110 [ color = green, label = "1110"] +0011 [ color = red, label = "0011"] +0101 [ color = red, label = "0101"] +0110 [ color = red, label = "0110"] +1001 [ color = red, label = "1001"] +1010 [ color = red, label = "1010"] +1100 [ color = red, label = "1100"] +0001 [ color = red, label = "0001"] +0010 [ color = red, label = "0010"] +0100 [ color = red, label = "0100"] +1000 [ color = red, label = "1000"] +0000 [ color = red, label = "0000"] +1111 -> 0111 +1111 -> 1011 +1111 -> 1101 +1111 -> 1110 +0111 -> 0011 +1011 -> 0011 +0111 -> 0101 +1101 -> 0101 +0111 -> 0110 +1110 -> 0110 +1011 -> 1001 +1101 -> 1001 +1011 -> 1010 +1110 -> 1010 +1101 -> 1100 +1110 -> 1100 +0011 -> 0001 +0101 -> 0001 +1001 -> 0001 +0011 -> 0010 +0110 -> 0010 +1010 -> 0010 +0101 -> 0100 +0110 -> 0100 +1100 -> 0100 +1001 -> 1000 +1010 -> 1000 +1100 -> 1000 +0001 -> 0000 +0010 -> 0000 +0100 -> 0000 +1000 -> 0000 +} \ No newline at end of file diff --git a/src/solamanDedekind/DedekindLattice/DedekindLatticeTest/n_4.world_57344.pdf b/src/solamanDedekind/DedekindLattice/DedekindLatticeTest/n_4.world_57344.pdf new file mode 100644 index 0000000000000000000000000000000000000000..2b580d694a8ca210e4a7295da1ce314160c34e55 GIT binary patch literal 10360 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 V_Kv?I&dtpUJ7`EHt{{Q>{{Um}a~c2u literal 0 HcmV?d00001 diff --git a/src/solamanDedekind/DedekindLattice/DedekindLatticeTest/writeToDotTest.dot b/src/solamanDedekind/DedekindLattice/DedekindLatticeTest/writeToDotTest.dot new file mode 100644 index 0000000..b828616 --- /dev/null +++ b/src/solamanDedekind/DedekindLattice/DedekindLatticeTest/writeToDotTest.dot @@ -0,0 +1,53 @@ +digraph{ + rankdir=BT + node[shape=circle, style=filled, label=""] + edge[dir=none] +1111 [ color = green, label = "1111"] +0111 [ color = red, label = "0111"] +1011 [ color = red, label = "1011"] +1101 [ color = green, label = "1101"] +1110 [ color = green, label = "1110"] +0011 [ color = red, label = "0011"] +0101 [ color = red, label = "0101"] +0110 [ color = red, label = "0110"] +1001 [ color = red, label = "1001"] +1010 [ color = red, label = "1010"] +1100 [ color = red, label = "1100"] +0001 [ color = red, label = "0001"] +0010 [ color = red, label = "0010"] +0100 [ color = red, label = "0100"] +1000 [ color = red, label = "1000"] +0000 [ color = red, label = "0000"] +1111 -> 0111 +1111 -> 1011 +1111 -> 1101 +1111 -> 1110 +0111 -> 0011 +1011 -> 0011 +0111 -> 0101 +1101 -> 0101 +0111 -> 0110 +1110 -> 0110 +1011 -> 1001 +1101 -> 1001 +1011 -> 1010 +1110 -> 1010 +1101 -> 1100 +1110 -> 1100 +0011 -> 0001 +0101 -> 0001 +1001 -> 0001 +0011 -> 0010 +0110 -> 0010 +1010 -> 0010 +0101 -> 0100 +0110 -> 0100 +1100 -> 0100 +1001 -> 1000 +1010 -> 1000 +1100 -> 1000 +0001 -> 0000 +0010 -> 0000 +0100 -> 0000 +1000 -> 0000 +} \ No newline at end of file diff --git a/src/solamanDedekind/DedekindLattice/DedekindNode.py b/src/solamanDedekind/DedekindLattice/DedekindNode.py new file mode 100644 index 0000000..eeb35d4 --- /dev/null +++ b/src/solamanDedekind/DedekindLattice/DedekindNode.py @@ -0,0 +1,256 @@ +''' +Created on Mar 3, 2015 + +@author: Solaman +''' +from itertools import combinations as genCombinations +from subprocess import call +import os + +#Used for Dot file generation + #Once a node is written to a dot file + #A full Node is created to help +fullNodes = {} + + #Used for Dot file generation + #Once a node is written to a dot file + #labels for each configuration are made +configurationLabelss = {} + + #Used for Dot file Generation + #Once a node is written to a dot file + #edges between configurations are written as dot edges. +dotEdgess = {} + +#To avoid cost of calculating configuration levels continuously, +#We will calculate the levels from the very beginning. +configurationLevelss = {} + +class DedekindNode(object): + ''' + A boolean function. It is up to the user to ensure that it is monotone. + ''' + + + def __init__(self, inputSize, acceptedConfigurations, nodeToCopy = None): + ''' + Each function has a set of accepted configurations. To aid in the book keeping of this algorithm, + each accepted configuration is stored by "level", namely, how many of the given inputs must be "off" + in the configuration. so if the configuration 0b0001 is accepted and the bitmask is 0b1111, + then the configuration would be stored at the 3rd level. + For rudimentary checking, the accepted configurations are added by descending level. + ''' + self.childrenSize = 0 + self.inputSize = inputSize + self.bitMask = self.bitMask = 2**(inputSize) - 1 + + if nodeToCopy != None: + temp = nodeToCopy.acceptedConfigurationsAsList() + temp.extend(acceptedConfigurations) + acceptedConfigurations = temp + self.acceptedConfigurations = [] + + for acceptedConfiguration in acceptedConfigurations: + level = self.getConfigurationLevel( acceptedConfiguration) + if level > len( self.acceptedConfigurations): + exceptionMessage = "Cannot add configurations beyond the highest level + 1" \ + + "\n attempted level: " + str(level) \ + +"\n level limit: " + str(len(self.acceptedConfigurations)) + raise Exception( exceptionMessage) + + if level == len(self.acceptedConfigurations): + self.acceptedConfigurations.append( [acceptedConfiguration]) + else: + self.acceptedConfigurations[level].append( acceptedConfiguration) + + self.index = -1 + + def isConsistent(self, configuration): + ''' + With regards to System Diagnostics, this function tests if the given configuration + is "consistent" with the node/function. I.e. is the configuration an accepted one? + ''' + if ( self.getIndex() & 1 << configuration ) == 0: + return False + else: + return True + + def acceptedConfigurationsAsList(self): + acceptedConfigurations = [] + for level in self.acceptedConfigurations: + acceptedConfigurations.extend(level) + + return acceptedConfigurations + + def _generatePossibleConfigurations(self): + ''' + Generates possible configurations to add to the function such that the new functions would be monotone + (given that this function is monotone). We observe that this can be done by level combinations. + E.G. if the input size is 4, and the last level of the function is 1->[0b1011, 0b0111], then the children + of the node in the lattice can have the level 2 configuration [0b0011]. + (if any other, then it would not be monotone). + ''' + possibleConfigurations = [] + newMaxLevel = len(self.acceptedConfigurations) + + #current max configuration level is [self.bitMask] + if newMaxLevel == 1: + possibleConfigurations = self.getLevelOneConfigurations() + + #Entire Dedekind Node Lattice is filled, can add [0] as an accepted configuration + elif newMaxLevel == self.inputSize \ + and len(self.acceptedConfigurations[-1]) == self.inputSize: + possibleConfigurations = [0] + #current max configuration level is [] (none are accepted) + elif newMaxLevel == 0: + return [] + elif newMaxLevel< self.inputSize: + combinations = genCombinations(self.acceptedConfigurations[-1], newMaxLevel) + possibleConfigurations = [] + for combination in combinations: + possibleConfiguration = self.bitMask + for configuration in combination: + possibleConfiguration &= configuration + if self.getConfigurationLevel(possibleConfiguration) == newMaxLevel: + possibleConfigurations.append(possibleConfiguration) + return possibleConfigurations + + return possibleConfigurations + + def getLevelOneConfigurations(self): + ''' + Possible level One configurations are generated uniquely from all others. Given that the current + function only accepts the state where all inputs are "on", possible level one configurations are any + input such that one input is "off". + E.G. if input size is 4, the children of {0b1111} can potentially have any of {0b0111, 0b1011, 0b1101, 0b1110} + ''' + levelOneConfigurations = [] + for inputIndex in range(0, self.inputSize): + levelOneConfigurations.append( (1 << inputIndex) ^ self.bitMask ) + + return levelOneConfigurations + + + def generateChildren(self): + ''' + Generates all Dedekind Nodes that would be considered the children of this + DedekindNode + ''' + children = [] + possibleConfigurations = self._generatePossibleConfigurations() + + for numberOfConfigurations in range(1, len(possibleConfigurations) + 1): + combinations = genCombinations(possibleConfigurations, numberOfConfigurations) + for combination in combinations: + children.append( DedekindNode(self.inputSize, combination, self)) + + return children + + def getIndex(self): + ''' + If we treat each configuration as its own integer value, we can combine each value into an integer + of size 2**inputSize bits. E.G. if the input size is 4, then each configuration has a value between 0-15. + So an integer of 16 bits, where each bit is for each configuration, will represent the function + and its accepted configurations. Since this value is unique, we can also use it as an index for the function + ''' + if self.index != -1: + return self.index + index = 0 + for configurationLevel in self.acceptedConfigurations: + for configuration in configurationLevel: + index |= (1 << configuration) + + self.index = index + return index + + def getConfigurationLevel(self, configuration): + global configurationLevelss + if self.inputSize not in configurationLevelss: + configurationLevels = [0] *(self.bitMask + 1) + for index in range (0, self.bitMask + 1): + configurationLevels[index] = hamming_distance(self.bitMask, index) + + configurationLevelss[self.inputSize] = configurationLevels + return configurationLevelss[self.inputSize][configuration] + + def writeToDotFile(self, writeLocation): + global fullNodes, configurationLabelss, dotEdgess + + dotFileName = os.path.join(writeLocation, "n_" + str(self.inputSize)\ + + "." + "world_" + str(self.getIndex())\ + + ".dot") + dotFile = open( dotFileName, "w") + dotFile.write("""digraph{ + rankdir=BT + node[shape=circle, style=filled, label=""] + edge[dir=none]\n""") + + self.initDotVariables() + fullNode = fullNodes[self.inputSize] + configurationLabels = configurationLabelss[self.inputSize] + dotEdges = dotEdgess[self.inputSize] + + configurationList = self.acceptedConfigurationsAsList() + for configuration in fullNode.acceptedConfigurationsAsList(): + if configuration in configurationList: + dotFile.write( configurationLabels[configuration] +" [ color = green, "\ + + "label = \""+ configurationLabels[configuration] + "\"]\n") + else: + dotFile.write( configurationLabels[configuration] +" [ color = red, "\ + + "label = \""+ configurationLabels[configuration] + "\"]\n") + + dotFile.write(dotEdges) + dotFile.write("}") + + dotFile.close() + + pngFileName = dotFileName[:-3]+"pdf" + call("dot -Tpdf " + os.getcwd()+"\\" + dotFileName + " -o " + os.getcwd() + "\\"+ pngFileName, shell=True) + + def initDotVariables(self): + global fullNodes, configurationLabelss, dotEdgess + ''' + Helper function used to set up variables used for writing + a Dedekind Node to a dot file. + ''' + if self.inputSize in fullNodes: + return + configurationLabels = {} + dotEdges = "" + + #Take accepted configurations as integers and convert them to binary strings for labeling + configurations = range(0, self.bitMask + 1) + configurations = sorted(configurations, key = lambda configuration: self.getConfigurationLevel(configuration)) + fullNode = DedekindNode(self.inputSize, configurations) + for configurationLevel in fullNode.acceptedConfigurations: + for configuration in configurationLevel: + configurationLabels[configuration] = bin(configuration + self.bitMask+1)[3:] + + for levelIndex in range(1, len(fullNode.acceptedConfigurations) ): + for configuration in fullNode.acceptedConfigurations[levelIndex]: + for parentConfiguration in fullNode.acceptedConfigurations[levelIndex - 1]: + if hamming_distance(configuration, parentConfiguration) == 1: + edgeString = configurationLabels[parentConfiguration] + " -> " \ + + configurationLabels[configuration] + "\n" + dotEdges += edgeString + + fullNodes[self.inputSize] = fullNode + configurationLabelss[self.inputSize] = configurationLabels + dotEdgess[self.inputSize] = dotEdges + + + + + +def hamming_distance( x, y): + ''' + Calculates hamming distance between two unsigned int values. + Code was copied from http://en.wikipedia.org/wiki/Hamming_distance + ''' + dist = 0 + val = x ^ y + while( val != 0): + dist += 1 + val &= val -1 + + return dist \ No newline at end of file diff --git a/src/solamanDedekind/DedekindLattice/GeneratedDedekindLattices/preserve.txt b/src/solamanDedekind/DedekindLattice/GeneratedDedekindLattices/preserve.txt new file mode 100644 index 0000000..090e975 --- /dev/null +++ b/src/solamanDedekind/DedekindLattice/GeneratedDedekindLattices/preserve.txt @@ -0,0 +1,6 @@ +March 3rd, 2015: GeneratedDedekindLattices will not be included in a git commit +unless a file is included. This is that file. + +Note: We could have the application generate this folder as needed, but I believe +that because the folder's existence isn't tied to a run of the application +("dedekind world 5" would be) the user might expect the folder to already exist. \ No newline at end of file diff --git a/src/solamanDedekind/DedekindLattice/TransitionError.py b/src/solamanDedekind/DedekindLattice/TransitionError.py new file mode 100644 index 0000000..75be7a4 --- /dev/null +++ b/src/solamanDedekind/DedekindLattice/TransitionError.py @@ -0,0 +1,16 @@ +''' +Created on Mar 2, 2015 + +@author: Solaman +''' +class TransitionError(Exception): + ''' + Raised when an operation attempts a state transition that is + not allowed. + ''' + + def __init__(self, msg): + self.msg = msg + + def __str__(self): + return repr(self.value) \ No newline at end of file diff --git a/src/solamanDedekind/README.md b/src/solamanDedekind/README.md new file mode 100644 index 0000000..1fa279d --- /dev/null +++ b/src/solamanDedekind/README.md @@ -0,0 +1,10 @@ +# System-Diagnostic-Analysis +How to run +python DedekindLattice.py [input size] [output dot and pdf files = boolean] +TODO: +1. Testing for DedekindLattice \n +2. Refactoring \n +3. Save and load previously constructed lattices \n +4. On demand Node generation \n +5. Reduce storage space considerably (breaks on n = 5) \n +7. Dot file generation is very slow From fbc8cdb469964cbd4e5f926f920b04d7ca88023c Mon Sep 17 00:00:00 2001 From: solaman Date: Thu, 2 Apr 2015 19:56:29 -0500 Subject: [PATCH 2/4] Pushing latest version of my work, check the original repo for details. --- .../Dedekind/Controller/CommandOptions.py | 85 ++++++++++++++ .../Dedekind/Controller/Console.py | 33 ++++++ .../Controller}/__init__.py | 0 src/solamanDedekind/Dedekind/Dedekind.py | 33 ++++++ .../DedekindAnalysis.py | 4 +- .../GeneratedDedekindLattices/preserve.txt | 2 +- .../Model}/DedekindLattice.py | 55 ++++++--- .../Model}/DedekindNode.py | 4 +- .../Dedekind/Model/DedekindSetMapping.py | 59 ++++++++++ .../Dedekind/Model/__init__.py | 0 .../Test/ControllerTest/CommandOptionsTest.py | 66 +++++++++++ .../Dedekind/Test/ControllerTest/__init__.py | 0 .../Test/ModelTest}/DedekindLatticeTest.py | 2 +- .../Test/ModelTest}/DedekindNodeTest.py | 4 +- .../Test/ModelTest/DedekindSetMappingTest.py | 45 ++++++++ .../Dedekind/Test/ModelTest/__init__.py | 0 .../Test/ModelTest}/n_4.world_57344.dot | 104 +++++++++--------- .../Test/ModelTest}/n_4.world_57344.pdf | Bin .../Test/ModelTest}/writeToDotTest.dot | 0 src/solamanDedekind/Dedekind/Test/__init__.py | 0 .../TransitionError.py | 0 .../Dedekind/resources/__init__.py | 0 .../Dedekind/resources/setValues.csv | 1 + src/solamanDedekind/README.md | 20 ++-- 24 files changed, 431 insertions(+), 86 deletions(-) create mode 100644 src/solamanDedekind/Dedekind/Controller/CommandOptions.py create mode 100644 src/solamanDedekind/Dedekind/Controller/Console.py rename src/solamanDedekind/{DedekindLattice/DedekindLatticeTest => Dedekind/Controller}/__init__.py (100%) create mode 100644 src/solamanDedekind/Dedekind/Dedekind.py rename src/solamanDedekind/{DedekindLattice => Dedekind}/DedekindAnalysis.py (84%) rename src/solamanDedekind/{DedekindLattice => Dedekind}/GeneratedDedekindLattices/preserve.txt (72%) rename src/solamanDedekind/{DedekindLattice => Dedekind/Model}/DedekindLattice.py (77%) rename src/solamanDedekind/{DedekindLattice => Dedekind/Model}/DedekindNode.py (98%) create mode 100644 src/solamanDedekind/Dedekind/Model/DedekindSetMapping.py create mode 100644 src/solamanDedekind/Dedekind/Model/__init__.py create mode 100644 src/solamanDedekind/Dedekind/Test/ControllerTest/CommandOptionsTest.py create mode 100644 src/solamanDedekind/Dedekind/Test/ControllerTest/__init__.py rename src/solamanDedekind/{DedekindLattice/DedekindLatticeTest => Dedekind/Test/ModelTest}/DedekindLatticeTest.py (87%) rename src/solamanDedekind/{DedekindLattice/DedekindLatticeTest => Dedekind/Test/ModelTest}/DedekindNodeTest.py (97%) create mode 100644 src/solamanDedekind/Dedekind/Test/ModelTest/DedekindSetMappingTest.py create mode 100644 src/solamanDedekind/Dedekind/Test/ModelTest/__init__.py rename src/solamanDedekind/{DedekindLattice/DedekindLatticeTest => Dedekind/Test/ModelTest}/n_4.world_57344.dot (95%) rename src/solamanDedekind/{DedekindLattice/DedekindLatticeTest => Dedekind/Test/ModelTest}/n_4.world_57344.pdf (100%) rename src/solamanDedekind/{DedekindLattice/DedekindLatticeTest => Dedekind/Test/ModelTest}/writeToDotTest.dot (100%) create mode 100644 src/solamanDedekind/Dedekind/Test/__init__.py rename src/solamanDedekind/{DedekindLattice => Dedekind}/TransitionError.py (100%) create mode 100644 src/solamanDedekind/Dedekind/resources/__init__.py create mode 100644 src/solamanDedekind/Dedekind/resources/setValues.csv diff --git a/src/solamanDedekind/Dedekind/Controller/CommandOptions.py b/src/solamanDedekind/Dedekind/Controller/CommandOptions.py new file mode 100644 index 0000000..cc0b28f --- /dev/null +++ b/src/solamanDedekind/Dedekind/Controller/CommandOptions.py @@ -0,0 +1,85 @@ +''' +Created on Mar 31, 2015 + +@author: Solaman + +Used to store relevant information about command options. +''' +import sys + +class CommandOption(object): + ''' + class for a particular console option + ''' + + + def __init__(self, helpString, commandFunction): + ''' + Constructor + ''' + #Should describe what the option does + self.helpString = helpString + + #Function used to execute this option + self.commandFunction = commandFunction + + +class CommandOptions(object): + ''' + Used to store and order console options + ''' + + def __init__(self): + #If the user wants to request a command by its command string, they can do so with this + self._commands = {} + + self._commands["help"] = CommandOption("displays all available commands"\ + + " along with a description for each", self.printCommandOptions) + + def addCommand(self, commandString, helpString, commandFunction): + ''' + Adds a command option. This option then can be requested by calling + 'selectOption'. + ''' + if commandString == "help": + exceptMessage = "attempted to overwrite \"help\" option in ConsoleOptions. Don't do that plz." + raise Exception(exceptMessage) + + if not callable(commandFunction): + exceptMessage = "Command function must be a function! (must be callable)" + raise Exception(exceptMessage) + + self._commands[commandString] = CommandOption(helpString, commandFunction) + + def printCommandOptions(self): + ''' + Prints all commands available along with the help string associated with each + respective command. + ''' + print "-----" + print "Available Commands" + print "-----" + for command in self._commands.iterkeys(): + print command, ": ", self._commands[command].helpString + print "-----" + + + def selectCommand(self, userInput): + ''' + Calls the function associated with a particular command. + ''' + if len(userInput) == 0: + sys.stderr.write( "Must enter a command, type \"help\" to list all commands\n" ) + return + + command = userInput[0] + inputParams = userInput[1:] + + if command not in self._commands: + sys.stderr.write("Command not recognized, type \"help\" to list all commands\n") + return + + if len(inputParams) == 0: + self._commands[command].commandFunction() + else: + self._commands[command].commandFunction(inputParams) \ No newline at end of file diff --git a/src/solamanDedekind/Dedekind/Controller/Console.py b/src/solamanDedekind/Dedekind/Controller/Console.py new file mode 100644 index 0000000..35e8764 --- /dev/null +++ b/src/solamanDedekind/Dedekind/Controller/Console.py @@ -0,0 +1,33 @@ +''' +Created on Mar 31, 2015 + +@author: Solaman +This is the main interface with the application if the user chooses to use +it as a console application. +''' +import os + +class Console(object): + + def __init__(self, commandOptions): + self.commandOptions = commandOptions + + self.commandOptions.addCommand("exit", "exits the console.", self.endConsole) + + self.shouldContinue = True + + + def selectCommand(self, userInput): + self.commandOptions.selectCommand(userInput) + + def endConsole(self): + print "Bye!" + self.shouldContinue = False + +def run(commandOptions): + console = Console(commandOptions) + print "hello! To list commands, type \"help\"" + while console.shouldContinue: + userInput = raw_input("enter command: ") + userInput = userInput.split(" ") + console.selectCommand(userInput) \ No newline at end of file diff --git a/src/solamanDedekind/DedekindLattice/DedekindLatticeTest/__init__.py b/src/solamanDedekind/Dedekind/Controller/__init__.py similarity index 100% rename from src/solamanDedekind/DedekindLattice/DedekindLatticeTest/__init__.py rename to src/solamanDedekind/Dedekind/Controller/__init__.py diff --git a/src/solamanDedekind/Dedekind/Dedekind.py b/src/solamanDedekind/Dedekind/Dedekind.py new file mode 100644 index 0000000..90f3810 --- /dev/null +++ b/src/solamanDedekind/Dedekind/Dedekind.py @@ -0,0 +1,33 @@ +''' +Created on Apr 1, 2015 + +@author: Solaman +''' +from Controller.CommandOptions import CommandOptions +from Controller import Console +from Model.DedekindLattice import getDedekindNumber, generateDotFiles + +import sys + +def runConsole(): + global standardCommands + + del standardCommands._commands["console"] + Console.run(standardCommands) + +standardCommands = CommandOptions() + +standardCommands.addCommand("getNumber", "Finds Dedekind Number for a given input size."\ + + "\n\tInput:" + " function input size", getDedekindNumber) + +standardCommands.addCommand("dotFiles", "Generates dot files of all monotone boolean functions"\ + + " for a given input size." + "\n\tInput:" + " function input size", generateDotFiles) + +standardCommands.addCommand("console", "Run the program as a console.", runConsole) + +def main(): + userInput = sys.argv[1:] + standardCommands.selectCommand(userInput) + +if __name__ == '__main__': + main() \ No newline at end of file diff --git a/src/solamanDedekind/DedekindLattice/DedekindAnalysis.py b/src/solamanDedekind/Dedekind/DedekindAnalysis.py similarity index 84% rename from src/solamanDedekind/DedekindLattice/DedekindAnalysis.py rename to src/solamanDedekind/Dedekind/DedekindAnalysis.py index 4eaa890..19de75c 100644 --- a/src/solamanDedekind/DedekindLattice/DedekindAnalysis.py +++ b/src/solamanDedekind/Dedekind/DedekindAnalysis.py @@ -4,8 +4,8 @@ @author: Solaman ''' import cProfile -from DedekindNode import DedekindNode -from DedekindLattice import DedekindLattice +from Model.DedekindNode import DedekindNode +from Model.DedekindLattice import DedekindLattice node = DedekindNode(4, [15]) lattice = DedekindLattice(5) diff --git a/src/solamanDedekind/DedekindLattice/GeneratedDedekindLattices/preserve.txt b/src/solamanDedekind/Dedekind/GeneratedDedekindLattices/preserve.txt similarity index 72% rename from src/solamanDedekind/DedekindLattice/GeneratedDedekindLattices/preserve.txt rename to src/solamanDedekind/Dedekind/GeneratedDedekindLattices/preserve.txt index 090e975..f6fad87 100644 --- a/src/solamanDedekind/DedekindLattice/GeneratedDedekindLattices/preserve.txt +++ b/src/solamanDedekind/Dedekind/GeneratedDedekindLattices/preserve.txt @@ -3,4 +3,4 @@ unless a file is included. This is that file. Note: We could have the application generate this folder as needed, but I believe that because the folder's existence isn't tied to a run of the application -("dedekind world 5" would be) the user might expect the folder to already exist. \ No newline at end of file +("dedekind world 5" would be a folder that is tied) the user might expect the folder to already exist. \ No newline at end of file diff --git a/src/solamanDedekind/DedekindLattice/DedekindLattice.py b/src/solamanDedekind/Dedekind/Model/DedekindLattice.py similarity index 77% rename from src/solamanDedekind/DedekindLattice/DedekindLattice.py rename to src/solamanDedekind/Dedekind/Model/DedekindLattice.py index b2a740e..851a71a 100644 --- a/src/solamanDedekind/DedekindLattice/DedekindLattice.py +++ b/src/solamanDedekind/Dedekind/Model/DedekindLattice.py @@ -3,7 +3,6 @@ @author: Solaman ''' -import TransitionError from DedekindNode import DedekindNode class DedekindLattice(object): @@ -16,15 +15,25 @@ class DedekindLattice(object): ''' - def __init__(self, inputSize): + def __init__(self, inputSize, inputSet = None ): ''' Constructor. For now, we will store each monotone boolean function as an object. Future implementations will store them as a single bit - for lean memory usage + for lean memory usage. + @param inputSize- Size of input for the monotone boolean functions (MBF). + @param inputSet- Set to use for each monotone boolean function. If + the caller wishes to use a python set for interacting with the MBF's can + provide one here. Defaults to values found in "resources/setValues.csv" ''' + if inputSize < 0: raise Exception("Input size must be greater than or equal to 0") - self.lattice = {} + + self.setMapping = {} + if inputSet == None: + setValues = open("resources/setValues.csv").readAll() + setValues = setValues.split(",") + #bit mask refers to the possible bit values #of a given configuration. E.G. boolean functions with 4 inputs @@ -32,6 +41,8 @@ def __init__(self, inputSize): self.bitMask = 2**(inputSize) - 1 self.inputSize = inputSize + self.lattice = {} + self.emptyFunction = DedekindNode(self.inputSize, []) self.lattice[ self.emptyFunction.getIndex()] = self.emptyFunction @@ -78,7 +89,7 @@ def findUniqueFunctions(self): if node.isVisited == True: if node.parent == None: functionCount += node.childrenCount - print functionCount + return functionCount continue else: node.parent.childrenCount += node.childrenCount @@ -113,6 +124,9 @@ def getKey(self, node): def generateDotFiles(self): + ''' + + ''' import os directoryName = os.path.join("GeneratedDedekindLattices", str(self.inputSize) + "_DedekindLattice") if not os.path.exists(directoryName): @@ -124,18 +138,25 @@ def generateDotFiles(self): generatedFiles += 1 if generatedFiles % updateTime == 0: print generatedFiles, " written so far" + print "Done" - -if __name__ == "__main__": - import sys - if len(sys.argv) < 2: - print "Error: must provide the input size of the Dedekind Lattice" - inputSize = sys.argv[1] - dedekind = DedekindLattice(int(inputSize)) - dedekind.fillLattice() +def getDedekindNumber(userInput): + ''' + Constructs the Dedekind Lattice for the given input size and + returns the dedekind number associated with that input. + Values that return within a minute are currently n <= 5. + ''' + inputSize = int(userInput[0]) + dedekindLattice = DedekindLattice(inputSize) + print dedekindLattice.findUniqueFunctions() - if len(sys.argv) > 2: - if sys.argv[2] == "true": - dedekind.generateDotFiles() - print dedekind.monotoneCount \ No newline at end of file +def generateDotFiles(userInput): + ''' + Constructs the Dedekind Lattice for the given input size and + generates the dot files for each monotone boolean function with the given input size. + ''' + inputSize = int(userInput[0]) + dedekindLattice = DedekindLattice(inputSize) + dedekindLattice.fillLattice() + dedekindLattice.generateDotFiles() \ No newline at end of file diff --git a/src/solamanDedekind/DedekindLattice/DedekindNode.py b/src/solamanDedekind/Dedekind/Model/DedekindNode.py similarity index 98% rename from src/solamanDedekind/DedekindLattice/DedekindNode.py rename to src/solamanDedekind/Dedekind/Model/DedekindNode.py index eeb35d4..f1e5e29 100644 --- a/src/solamanDedekind/DedekindLattice/DedekindNode.py +++ b/src/solamanDedekind/Dedekind/Model/DedekindNode.py @@ -204,8 +204,8 @@ def writeToDotFile(self, writeLocation): dotFile.close() - pngFileName = dotFileName[:-3]+"pdf" - call("dot -Tpdf " + os.getcwd()+"\\" + dotFileName + " -o " + os.getcwd() + "\\"+ pngFileName, shell=True) + # pngFileName = dotFileName[:-3]+"pdf" + # call("dot -Tpdf " + os.getcwd()+"\\" + dotFileName + " -o " + os.getcwd() + "\\"+ pngFileName, shell=True) def initDotVariables(self): global fullNodes, configurationLabelss, dotEdgess diff --git a/src/solamanDedekind/Dedekind/Model/DedekindSetMapping.py b/src/solamanDedekind/Dedekind/Model/DedekindSetMapping.py new file mode 100644 index 0000000..2884098 --- /dev/null +++ b/src/solamanDedekind/Dedekind/Model/DedekindSetMapping.py @@ -0,0 +1,59 @@ +''' +Created on Apr 2, 2015 + +@author: Solaman +''' +import resources + +class DedekindSetMapping(object): + ''' + Since the DedekindLattice stores accepted configurations as integers, users of the library + might find it difficult to interact with as opposed to Python Sets. This class encapsulates + the mapping from those integer values to Elements in a set for easier use. + ''' + + + def __init__(self, inputSize, inputSet = None): + ''' + Constructor + @inputSet - input set for monotone boolean functions. If none is provided, a set is + constructed from "resources/setValues.csv" + ''' + self.inputSize = inputSize + self.bitToElement = {} + self.elementToBit = {} + + if inputSet == None: + import os + inputSetFileName = os.path.join(os.path.dirname(resources.__file__), "setValues.csv") + inputSet = open(inputSetFileName).read().split(",") + + elif isinstance(inputSet, set): + inputSet = list(inputSet) + + for bitShift in range(0, self.inputSize): + self.bitToElement[1< 0111 -1111 -> 1011 -1111 -> 1101 -1111 -> 1110 -0111 -> 0011 -1011 -> 0011 -0111 -> 0101 -1101 -> 0101 -0111 -> 0110 -1110 -> 0110 -1011 -> 1001 -1101 -> 1001 -1011 -> 1010 -1110 -> 1010 -1101 -> 1100 -1110 -> 1100 -0011 -> 0001 -0101 -> 0001 -1001 -> 0001 -0011 -> 0010 -0110 -> 0010 -1010 -> 0010 -0101 -> 0100 -0110 -> 0100 -1100 -> 0100 -1001 -> 1000 -1010 -> 1000 -1100 -> 1000 -0001 -> 0000 -0010 -> 0000 -0100 -> 0000 -1000 -> 0000 +digraph{ + rankdir=BT + node[shape=circle, style=filled, label=""] + edge[dir=none] +1111 [ color = green, label = "1111"] +0111 [ color = red, label = "0111"] +1011 [ color = red, label = "1011"] +1101 [ color = green, label = "1101"] +1110 [ color = green, label = "1110"] +0011 [ color = red, label = "0011"] +0101 [ color = red, label = "0101"] +0110 [ color = red, label = "0110"] +1001 [ color = red, label = "1001"] +1010 [ color = red, label = "1010"] +1100 [ color = red, label = "1100"] +0001 [ color = red, label = "0001"] +0010 [ color = red, label = "0010"] +0100 [ color = red, label = "0100"] +1000 [ color = red, label = "1000"] +0000 [ color = red, label = "0000"] +1111 -> 0111 +1111 -> 1011 +1111 -> 1101 +1111 -> 1110 +0111 -> 0011 +1011 -> 0011 +0111 -> 0101 +1101 -> 0101 +0111 -> 0110 +1110 -> 0110 +1011 -> 1001 +1101 -> 1001 +1011 -> 1010 +1110 -> 1010 +1101 -> 1100 +1110 -> 1100 +0011 -> 0001 +0101 -> 0001 +1001 -> 0001 +0011 -> 0010 +0110 -> 0010 +1010 -> 0010 +0101 -> 0100 +0110 -> 0100 +1100 -> 0100 +1001 -> 1000 +1010 -> 1000 +1100 -> 1000 +0001 -> 0000 +0010 -> 0000 +0100 -> 0000 +1000 -> 0000 } \ No newline at end of file diff --git a/src/solamanDedekind/DedekindLattice/DedekindLatticeTest/n_4.world_57344.pdf b/src/solamanDedekind/Dedekind/Test/ModelTest/n_4.world_57344.pdf similarity index 100% rename from src/solamanDedekind/DedekindLattice/DedekindLatticeTest/n_4.world_57344.pdf rename to src/solamanDedekind/Dedekind/Test/ModelTest/n_4.world_57344.pdf diff --git a/src/solamanDedekind/DedekindLattice/DedekindLatticeTest/writeToDotTest.dot b/src/solamanDedekind/Dedekind/Test/ModelTest/writeToDotTest.dot similarity index 100% rename from src/solamanDedekind/DedekindLattice/DedekindLatticeTest/writeToDotTest.dot rename to src/solamanDedekind/Dedekind/Test/ModelTest/writeToDotTest.dot diff --git a/src/solamanDedekind/Dedekind/Test/__init__.py b/src/solamanDedekind/Dedekind/Test/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/src/solamanDedekind/DedekindLattice/TransitionError.py b/src/solamanDedekind/Dedekind/TransitionError.py similarity index 100% rename from src/solamanDedekind/DedekindLattice/TransitionError.py rename to src/solamanDedekind/Dedekind/TransitionError.py diff --git a/src/solamanDedekind/Dedekind/resources/__init__.py b/src/solamanDedekind/Dedekind/resources/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/src/solamanDedekind/Dedekind/resources/setValues.csv b/src/solamanDedekind/Dedekind/resources/setValues.csv new file mode 100644 index 0000000..1b91aad --- /dev/null +++ b/src/solamanDedekind/Dedekind/resources/setValues.csv @@ -0,0 +1 @@ +A,B,C,D,E,F,G,H,I,J,K,L,M,N,O,P,Q,R,S,T,U,V,W,X,Y,Z \ No newline at end of file diff --git a/src/solamanDedekind/README.md b/src/solamanDedekind/README.md index 1fa279d..9376c9a 100644 --- a/src/solamanDedekind/README.md +++ b/src/solamanDedekind/README.md @@ -1,10 +1,12 @@ # System-Diagnostic-Analysis -How to run -python DedekindLattice.py [input size] [output dot and pdf files = boolean] -TODO: -1. Testing for DedekindLattice \n -2. Refactoring \n -3. Save and load previously constructed lattices \n -4. On demand Node generation \n -5. Reduce storage space considerably (breaks on n = 5) \n -7. Dot file generation is very slow +How to run: python Dedekind.py
+type "-help" to see a list of all available commands
+This project is meant to be used either as a command line, console, or library.
+If you wish create the monotone boolean functions and .dot files for each, try +running "python Dedekind.py" and use the command line or console.
+Else, all of these functions are available through Model.DedekindLattice.py . Which also have useful +features for System Diagnostic Analysis:
+by instantiated an instance of DedekindLattice.py, you can call "getNextNode()" which will return
+a new monotone boolean function within the DedekindLattice. From here, you can then call "isConsistent", passing + in a configuration, to see if the function would accept it. + From 1b594e96e94edf126ddbff195da73da09f7d4402 Mon Sep 17 00:00:00 2001 From: solaman Date: Sat, 9 May 2015 15:57:34 -0500 Subject: [PATCH 3/4] Added most recent version of all files from my repo --- .project | 28 +- src/solamanDedekind/.gitignore | 54 ++-- .../Dedekind/Controller/CommandOptions.py | 168 +++++----- .../Dedekind/Controller/Console.py | 64 ++-- src/solamanDedekind/Dedekind/Dedekind.py | 64 ++-- .../Dedekind/Algorithms/BottomUp/BottomUp.py | 65 ++++ .../Dedekind/Algorithms/BottomUp/__init__.py | 0 .../Hitting_Set_Tree/HittingSetTree.py | 90 ++++++ .../Hitting_Set_Tree/LogarithmicExtraction.py | 60 ++++ .../Algorithms/Hitting_Set_Tree/__init__.py | 0 .../Dedekind/Algorithms/Random/Random.py | 81 +++++ .../Dedekind/Algorithms/Random/__init__.py | 0 .../Dedekind/Algorithms/TopDown/TopDown.py | 71 +++++ .../Dedekind/Algorithms/TopDown/__init__.py | 0 .../Dedekind/Dedekind/Algorithms/__init__.py | 0 .../Dedekind/Dedekind/Algorithms/analysis.py | 110 +++++++ .../Dedekind/Controller/CommandOptions.py | 85 +++++ .../Dedekind/Dedekind/Controller/Console.py | 33 ++ .../Dedekind/Dedekind/Controller/__init__.py | 0 .../Dedekind/Dedekind/Dedekind.py | 37 +++ .../Dedekind/Dedekind/DedekindAnalysis.py | 22 ++ .../GeneratedDedekindLattices/preserve.txt | 6 + .../Dedekind/Model/DedekindLattice.py | 203 ++++++++++++ .../Dedekind/Dedekind/Model/DedekindNode.py | 290 ++++++++++++++++++ .../Dedekind/Model/DedekindNodeIter.py | 34 ++ .../Dedekind/Model/DedekindNodeLean.py | 133 ++++++++ .../Dedekind/Model/DedekindNodeTools.py | 17 + .../Dedekind/Model/DedekindSetMapping.py | 73 +++++ .../Dedekind/Dedekind/Model/LevelPermutor.py | 72 +++++ .../Dedekind/Dedekind/Model/__init__.py | 0 .../Dedekind/Test/AlgorithmTest/AllTests.py | 94 ++++++ .../Dedekind/Test/AlgorithmTest/__init__.py | 0 .../Test/ControllerTest/CommandOptionsTest.py | 66 ++++ .../Dedekind/Test/ControllerTest/__init__.py | 0 .../Test/ModelTest/DedekindLatticeTest.py | 25 ++ .../Test/ModelTest/DedekindNodeTest.py | 124 ++++++++ .../Test/ModelTest/DedekindSetMappingTest.py | 35 +++ .../Test/ModelTest/LevelPermutorTest.py | 26 ++ .../Dedekind/Test/ModelTest/__init__.py | 0 .../Test/ModelTest/n_4.world_57344.dot | 53 ++++ .../Test/ModelTest/n_4.world_57344.pdf | Bin 0 -> 10360 bytes .../Test/ModelTest/writeToDotTest.dot | 53 ++++ .../Dedekind/Dedekind/Test/__init__.py | 0 .../Dedekind/Dedekind/TransitionError.py | 16 + .../Dedekind/Dedekind/resources/__init__.py | 0 .../Dedekind/Dedekind/resources/setValues.csv | 1 + .../GeneratedDedekindLattices/preserve.txt | 10 +- .../Dedekind/Model/DedekindSetMapping.py | 116 +++---- .../Test/ControllerTest/CommandOptionsTest.py | 130 ++++---- .../Test/ModelTest/DedekindSetMappingTest.py | 88 +++--- .../Test/ModelTest/n_4.world_57344.dot | 104 +++---- src/solamanDedekind/Explanation.pdf | Bin 0 -> 153346 bytes src/solamanDedekind/README.md | 7 +- 53 files changed, 2397 insertions(+), 411 deletions(-) create mode 100644 src/solamanDedekind/Dedekind/Dedekind/Algorithms/BottomUp/BottomUp.py create mode 100644 src/solamanDedekind/Dedekind/Dedekind/Algorithms/BottomUp/__init__.py create mode 100644 src/solamanDedekind/Dedekind/Dedekind/Algorithms/Hitting_Set_Tree/HittingSetTree.py create mode 100644 src/solamanDedekind/Dedekind/Dedekind/Algorithms/Hitting_Set_Tree/LogarithmicExtraction.py create mode 100644 src/solamanDedekind/Dedekind/Dedekind/Algorithms/Hitting_Set_Tree/__init__.py create mode 100644 src/solamanDedekind/Dedekind/Dedekind/Algorithms/Random/Random.py create mode 100644 src/solamanDedekind/Dedekind/Dedekind/Algorithms/Random/__init__.py create mode 100644 src/solamanDedekind/Dedekind/Dedekind/Algorithms/TopDown/TopDown.py create mode 100644 src/solamanDedekind/Dedekind/Dedekind/Algorithms/TopDown/__init__.py create mode 100644 src/solamanDedekind/Dedekind/Dedekind/Algorithms/__init__.py create mode 100644 src/solamanDedekind/Dedekind/Dedekind/Algorithms/analysis.py create mode 100644 src/solamanDedekind/Dedekind/Dedekind/Controller/CommandOptions.py create mode 100644 src/solamanDedekind/Dedekind/Dedekind/Controller/Console.py create mode 100644 src/solamanDedekind/Dedekind/Dedekind/Controller/__init__.py create mode 100644 src/solamanDedekind/Dedekind/Dedekind/Dedekind.py create mode 100644 src/solamanDedekind/Dedekind/Dedekind/DedekindAnalysis.py create mode 100644 src/solamanDedekind/Dedekind/Dedekind/GeneratedDedekindLattices/preserve.txt create mode 100644 src/solamanDedekind/Dedekind/Dedekind/Model/DedekindLattice.py create mode 100644 src/solamanDedekind/Dedekind/Dedekind/Model/DedekindNode.py create mode 100644 src/solamanDedekind/Dedekind/Dedekind/Model/DedekindNodeIter.py create mode 100644 src/solamanDedekind/Dedekind/Dedekind/Model/DedekindNodeLean.py create mode 100644 src/solamanDedekind/Dedekind/Dedekind/Model/DedekindNodeTools.py create mode 100644 src/solamanDedekind/Dedekind/Dedekind/Model/DedekindSetMapping.py create mode 100644 src/solamanDedekind/Dedekind/Dedekind/Model/LevelPermutor.py create mode 100644 src/solamanDedekind/Dedekind/Dedekind/Model/__init__.py create mode 100644 src/solamanDedekind/Dedekind/Dedekind/Test/AlgorithmTest/AllTests.py create mode 100644 src/solamanDedekind/Dedekind/Dedekind/Test/AlgorithmTest/__init__.py create mode 100644 src/solamanDedekind/Dedekind/Dedekind/Test/ControllerTest/CommandOptionsTest.py create mode 100644 src/solamanDedekind/Dedekind/Dedekind/Test/ControllerTest/__init__.py create mode 100644 src/solamanDedekind/Dedekind/Dedekind/Test/ModelTest/DedekindLatticeTest.py create mode 100644 src/solamanDedekind/Dedekind/Dedekind/Test/ModelTest/DedekindNodeTest.py create mode 100644 src/solamanDedekind/Dedekind/Dedekind/Test/ModelTest/DedekindSetMappingTest.py create mode 100644 src/solamanDedekind/Dedekind/Dedekind/Test/ModelTest/LevelPermutorTest.py create mode 100644 src/solamanDedekind/Dedekind/Dedekind/Test/ModelTest/__init__.py create mode 100644 src/solamanDedekind/Dedekind/Dedekind/Test/ModelTest/n_4.world_57344.dot create mode 100644 src/solamanDedekind/Dedekind/Dedekind/Test/ModelTest/n_4.world_57344.pdf create mode 100644 src/solamanDedekind/Dedekind/Dedekind/Test/ModelTest/writeToDotTest.dot create mode 100644 src/solamanDedekind/Dedekind/Dedekind/Test/__init__.py create mode 100644 src/solamanDedekind/Dedekind/Dedekind/TransitionError.py create mode 100644 src/solamanDedekind/Dedekind/Dedekind/resources/__init__.py create mode 100644 src/solamanDedekind/Dedekind/Dedekind/resources/setValues.csv create mode 100644 src/solamanDedekind/Explanation.pdf diff --git a/.project b/.project index 64dc59f..5ca30de 100644 --- a/.project +++ b/.project @@ -1,11 +1,17 @@ - - - DedekindNumber - - - - - - - - + + + DedekindNumber + + + + + + org.python.pydev.PyDevBuilder + + + + + + org.python.pydev.pythonNature + + diff --git a/src/solamanDedekind/.gitignore b/src/solamanDedekind/.gitignore index 002098f..14b9f47 100644 --- a/src/solamanDedekind/.gitignore +++ b/src/solamanDedekind/.gitignore @@ -1,28 +1,28 @@ -# Compiled source # -################### -*.com -*.class -*.dll -*.exe -*.o -*.so -*.pyc - -# OS generated files # -###################### -.DS_Store -.DS_Store? -._* -.Spotlight-V100 -.Trashes -ehthumbs.db -Thumbs.db - -# Output Files # -################ -DedekindLattice/GeneratedDedekindLattices/* -!DedekindLattice/GeneratedDedekindLattices/preserve.txt - -# Experimental Files # -###################### +# Compiled source # +################### +*.com +*.class +*.dll +*.exe +*.o +*.so +*.pyc + +# OS generated files # +###################### +.DS_Store +.DS_Store? +._* +.Spotlight-V100 +.Trashes +ehthumbs.db +Thumbs.db + +# Output Files # +################ +DedekindLattice/GeneratedDedekindLattices/* +!DedekindLattice/GeneratedDedekindLattices/preserve.txt + +# Experimental Files # +###################### **/.noworkflow/** \ No newline at end of file diff --git a/src/solamanDedekind/Dedekind/Controller/CommandOptions.py b/src/solamanDedekind/Dedekind/Controller/CommandOptions.py index cc0b28f..2ac4f94 100644 --- a/src/solamanDedekind/Dedekind/Controller/CommandOptions.py +++ b/src/solamanDedekind/Dedekind/Controller/CommandOptions.py @@ -1,85 +1,85 @@ -''' -Created on Mar 31, 2015 - -@author: Solaman - -Used to store relevant information about command options. -''' -import sys - -class CommandOption(object): - ''' - class for a particular console option - ''' - - - def __init__(self, helpString, commandFunction): - ''' - Constructor - ''' - #Should describe what the option does - self.helpString = helpString - - #Function used to execute this option - self.commandFunction = commandFunction - - -class CommandOptions(object): - ''' - Used to store and order console options - ''' - - def __init__(self): - #If the user wants to request a command by its command string, they can do so with this - self._commands = {} - - self._commands["help"] = CommandOption("displays all available commands"\ - + " along with a description for each", self.printCommandOptions) - - def addCommand(self, commandString, helpString, commandFunction): - ''' - Adds a command option. This option then can be requested by calling - 'selectOption'. - ''' - if commandString == "help": - exceptMessage = "attempted to overwrite \"help\" option in ConsoleOptions. Don't do that plz." - raise Exception(exceptMessage) - - if not callable(commandFunction): - exceptMessage = "Command function must be a function! (must be callable)" - raise Exception(exceptMessage) - - self._commands[commandString] = CommandOption(helpString, commandFunction) - - def printCommandOptions(self): - ''' - Prints all commands available along with the help string associated with each - respective command. - ''' - print "-----" - print "Available Commands" - print "-----" - for command in self._commands.iterkeys(): - print command, ": ", self._commands[command].helpString - print "-----" - - - def selectCommand(self, userInput): - ''' - Calls the function associated with a particular command. - ''' - if len(userInput) == 0: - sys.stderr.write( "Must enter a command, type \"help\" to list all commands\n" ) - return - - command = userInput[0] - inputParams = userInput[1:] - - if command not in self._commands: - sys.stderr.write("Command not recognized, type \"help\" to list all commands\n") - return - - if len(inputParams) == 0: - self._commands[command].commandFunction() - else: +''' +Created on Mar 31, 2015 + +@author: Solaman + +Used to store relevant information about command options. +''' +import sys + +class CommandOption(object): + ''' + class for a particular console option + ''' + + + def __init__(self, helpString, commandFunction): + ''' + Constructor + ''' + #Should describe what the option does + self.helpString = helpString + + #Function used to execute this option + self.commandFunction = commandFunction + + +class CommandOptions(object): + ''' + Used to store and order console options + ''' + + def __init__(self): + #If the user wants to request a command by its command string, they can do so with this + self._commands = {} + + self._commands["help"] = CommandOption("displays all available commands"\ + + " along with a description for each", self.printCommandOptions) + + def addCommand(self, commandString, helpString, commandFunction): + ''' + Adds a command option. This option then can be requested by calling + 'selectOption'. + ''' + if commandString == "help": + exceptMessage = "attempted to overwrite \"help\" option in ConsoleOptions. Don't do that plz." + raise Exception(exceptMessage) + + if not callable(commandFunction): + exceptMessage = "Command function must be a function! (must be callable)" + raise Exception(exceptMessage) + + self._commands[commandString] = CommandOption(helpString, commandFunction) + + def printCommandOptions(self): + ''' + Prints all commands available along with the help string associated with each + respective command. + ''' + print "-----" + print "Available Commands" + print "-----" + for command in self._commands.iterkeys(): + print command, ": ", self._commands[command].helpString + print "-----" + + + def selectCommand(self, userInput): + ''' + Calls the function associated with a particular command. + ''' + if len(userInput) == 0: + sys.stderr.write( "Must enter a command, type \"help\" to list all commands\n" ) + return + + command = userInput[0] + inputParams = userInput[1:] + + if command not in self._commands: + sys.stderr.write("Command not recognized, type \"help\" to list all commands\n") + return + + if len(inputParams) == 0: + self._commands[command].commandFunction() + else: self._commands[command].commandFunction(inputParams) \ No newline at end of file diff --git a/src/solamanDedekind/Dedekind/Controller/Console.py b/src/solamanDedekind/Dedekind/Controller/Console.py index 35e8764..8acf541 100644 --- a/src/solamanDedekind/Dedekind/Controller/Console.py +++ b/src/solamanDedekind/Dedekind/Controller/Console.py @@ -1,33 +1,33 @@ -''' -Created on Mar 31, 2015 - -@author: Solaman -This is the main interface with the application if the user chooses to use -it as a console application. -''' -import os - -class Console(object): - - def __init__(self, commandOptions): - self.commandOptions = commandOptions - - self.commandOptions.addCommand("exit", "exits the console.", self.endConsole) - - self.shouldContinue = True - - - def selectCommand(self, userInput): - self.commandOptions.selectCommand(userInput) - - def endConsole(self): - print "Bye!" - self.shouldContinue = False - -def run(commandOptions): - console = Console(commandOptions) - print "hello! To list commands, type \"help\"" - while console.shouldContinue: - userInput = raw_input("enter command: ") - userInput = userInput.split(" ") +''' +Created on Mar 31, 2015 + +@author: Solaman +This is the main interface with the application if the user chooses to use +it as a console application. +''' +import os + +class Console(object): + + def __init__(self, commandOptions): + self.commandOptions = commandOptions + + self.commandOptions.addCommand("exit", "exits the console.", self.endConsole) + + self.shouldContinue = True + + + def selectCommand(self, userInput): + self.commandOptions.selectCommand(userInput) + + def endConsole(self): + print "Bye!" + self.shouldContinue = False + +def run(commandOptions): + console = Console(commandOptions) + print "hello! To list commands, type \"help\"" + while console.shouldContinue: + userInput = raw_input("enter command: ") + userInput = userInput.split(" ") console.selectCommand(userInput) \ No newline at end of file diff --git a/src/solamanDedekind/Dedekind/Dedekind.py b/src/solamanDedekind/Dedekind/Dedekind.py index 90f3810..7ab1c04 100644 --- a/src/solamanDedekind/Dedekind/Dedekind.py +++ b/src/solamanDedekind/Dedekind/Dedekind.py @@ -1,33 +1,33 @@ -''' -Created on Apr 1, 2015 - -@author: Solaman -''' -from Controller.CommandOptions import CommandOptions -from Controller import Console -from Model.DedekindLattice import getDedekindNumber, generateDotFiles - -import sys - -def runConsole(): - global standardCommands - - del standardCommands._commands["console"] - Console.run(standardCommands) - -standardCommands = CommandOptions() - -standardCommands.addCommand("getNumber", "Finds Dedekind Number for a given input size."\ - + "\n\tInput:" + " function input size", getDedekindNumber) - -standardCommands.addCommand("dotFiles", "Generates dot files of all monotone boolean functions"\ - + " for a given input size." + "\n\tInput:" + " function input size", generateDotFiles) - -standardCommands.addCommand("console", "Run the program as a console.", runConsole) - -def main(): - userInput = sys.argv[1:] - standardCommands.selectCommand(userInput) - -if __name__ == '__main__': +''' +Created on Apr 1, 2015 + +@author: Solaman +''' +from Controller.CommandOptions import CommandOptions +from Controller import Console +from Model.DedekindLattice import getDedekindNumber, generateDotFiles + +import sys + +def runConsole(): + global standardCommands + + del standardCommands._commands["console"] + Console.run(standardCommands) + +standardCommands = CommandOptions() + +standardCommands.addCommand("getNumber", "Finds Dedekind Number for a given input size."\ + + "\n\tInput:" + " function input size", getDedekindNumber) + +standardCommands.addCommand("dotFiles", "Generates dot files of all monotone boolean functions"\ + + " for a given input size." + "\n\tInput:" + " function input size", generateDotFiles) + +standardCommands.addCommand("console", "Run the program as a console.", runConsole) + +def main(): + userInput = sys.argv[1:] + standardCommands.selectCommand(userInput) + +if __name__ == '__main__': main() \ No newline at end of file diff --git a/src/solamanDedekind/Dedekind/Dedekind/Algorithms/BottomUp/BottomUp.py b/src/solamanDedekind/Dedekind/Dedekind/Algorithms/BottomUp/BottomUp.py new file mode 100644 index 0000000..0e0fc61 --- /dev/null +++ b/src/solamanDedekind/Dedekind/Dedekind/Algorithms/BottomUp/BottomUp.py @@ -0,0 +1,65 @@ +''' +Created on May 4, 2015 + +@author: Solaman +''' +from sets import ImmutableSet + +def computeAllMIS(setDescription, constraints): + ''' + Finds the Minimum Inconsistent Subsets for the given constraints and set Description + using a bottom up approach (where bottom is empty set). We check the powerset of constraints, and rule out possible + MIS based on whether or not a given set is consistent. + NOTE: There are probably far more optimal implementations of such an algorithm. However, + for the sake of comparison, we are only interested in the number of times + 'isConsistent' is called and so we refrain from these optimizations. + @param setDescription- A set of rules linking several items together. + Think of this as boolean equation in Conjunctive Normal Form. + @param Constraints- a set of items we would like to include. + Think of this as a value assignment for the previous boolean equation. + ''' + setsToCheck = generatePowerset(constraints) + misSet = set() + while len(setsToCheck) > 0: + checkNextSet(setsToCheck, misSet, setDescription) + + return misSet + + +def checkNextSet(setsToCheck, misSet, setDescription): + ''' + Takes the smallest set of possible MIS and finds its parents. Then checks if it is consistent. + If it is, then it is an MIS and we should rule out all of its parents. + If it is not, then we rule it out as a possible MIS. + ''' + parentSets = set() + + chosenSet = setsToCheck.pop() + setsToCheck.add(chosenSet) + for aSet in setsToCheck: + if len(chosenSet) > len(aSet): + chosenSet = aSet + + for possibleParent in setsToCheck: + if possibleParent.issuperset(chosenSet) and possibleParent != chosenSet: + parentSets.add(possibleParent) + + if setDescription.isConsistent(chosenSet): + misSet.add(chosenSet) + for parentSet in parentSets: + setsToCheck.remove(parentSet) + + setsToCheck.remove(chosenSet) + + + +def generatePowerset(theSet): + ''' + Generates powerset of a given set. + Original code found at http://stackoverflow.com/questions/18826571/python-powerset-of-a-given-set-with-generators + ''' + powerSet = set() + from itertools import chain, combinations + for subset in chain.from_iterable(combinations(theSet, r) for r in range(len(theSet)+1)): + powerSet.add( ImmutableSet(subset)) + return powerSet \ No newline at end of file diff --git a/src/solamanDedekind/Dedekind/Dedekind/Algorithms/BottomUp/__init__.py b/src/solamanDedekind/Dedekind/Dedekind/Algorithms/BottomUp/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/src/solamanDedekind/Dedekind/Dedekind/Algorithms/Hitting_Set_Tree/HittingSetTree.py b/src/solamanDedekind/Dedekind/Dedekind/Algorithms/Hitting_Set_Tree/HittingSetTree.py new file mode 100644 index 0000000..a9e7fc8 --- /dev/null +++ b/src/solamanDedekind/Dedekind/Dedekind/Algorithms/Hitting_Set_Tree/HittingSetTree.py @@ -0,0 +1,90 @@ +''' +Created on Apr 23, 2015 + +@author: Solaman +''' +from LogarithmicExtraction import computeSingleMIS +import LogarithmicExtraction +from sets import ImmutableSet + +def computeAllMIS(setDescription, constraints): + ''' + Taken from 'A Hybrid Diagnosis Approach Combining Black-Box + and White-Box Reasoning'. This attempts to find all Minimal Subset of Constraints + that will be inconsistent for the given Set Description. + @param setDescription- A set of rules linking several items together. + Think of this as boolean equation in Conjunctive Normal Form. + @param Constraints- a set of items we would like to include. + Think of this as an assignment for the previous boolean equation. + ''' + misSet= set() + currPath = ImmutableSet() + paths = set() + LogarithmicExtraction.newRun = True + computeAllMISHelper(setDescription, constraints, + misSet, currPath, paths) + return misSet + +def computeAllMISHelper(setDescription, constraints, misSet, currPath, paths): + #paths holds all previously visited paths of the hitting set tree + #currPath is the current. If any previous path is a subset of this one + #the we have already computed all MIS that would be found in the current path's subtree. + for path in paths: + if path in currPath: + return + + #if the current set of constraints is consistent + #Then there cannot be anymore MIS in its subtree + #so we add the current path to the set of paths enumerated and return. + if not setDescription.isConsistent(constraints): + paths.add(currPath) + return + #In order to avoid redundant MIS computations + #We check the current set of MIS misSet + #If it is possible to find any of the already computed MIS in the current iteration + #(it does not share an element in the currPath) then we just use that MIS + #and continue down the tree + currentMIS = ImmutableSet() + for mis in misSet: + if len(mis.intersection(currPath)) == 0: + currentMIS = mis + break + #If not MIS matches the previous description, we will need to + #compute a new one. + if currentMIS == ImmutableSet(): + currentMIS = computeSingleMIS(setDescription, constraints) + misSet.add(currentMIS) + + #iterate through the children of the current path + for element in currentMIS: + childPath = currPath.union( set(element)) + computeAllMISHelper(setDescription, constraints - ImmutableSet(element), misSet, childPath, paths) + +import sets +def computeAllJust(setDescription, artSet, justSet, curpath, allpaths): + ''' + Implementation of Hitting Set Tree found directly from EulerX. + A few modifications are made to ensure that it is compatible with this library's + implementation of logarathmic Extraction, otherwise everything else is the same + ''' + for path in allpaths: + if path.issubset(curpath): + return + #must be 'not' to be consistent with this library's implementation. + #Without it, it does not compute the MIS properly + #i.e. it does not pass any of the algorithm tests. + if not setDescription.isConsistent(artSet): + allpaths.add(curpath) + return + j = sets.Set() + for s in justSet: + if len(s.intersection(curpath)) == 0: + j = s + if len(j) == 0: + j = computeSingleMIS(setDescription, artSet) + if len(j) != 0: + justSet.add(j) + for a in j: + tmpcur = curpath.union( set(a)) + tmpart = artSet - ImmutableSet(a) + computeAllJust(setDescription, tmpart, justSet, tmpcur, allpaths) diff --git a/src/solamanDedekind/Dedekind/Dedekind/Algorithms/Hitting_Set_Tree/LogarithmicExtraction.py b/src/solamanDedekind/Dedekind/Dedekind/Algorithms/Hitting_Set_Tree/LogarithmicExtraction.py new file mode 100644 index 0000000..cc78c04 --- /dev/null +++ b/src/solamanDedekind/Dedekind/Dedekind/Algorithms/Hitting_Set_Tree/LogarithmicExtraction.py @@ -0,0 +1,60 @@ +''' +Created on Apr 23, 2015 + +@author: Solaman +''' +import random +from sets import ImmutableSet + +#to avoid checking the empty set as an MIS more than once for +#a given set description, an algorithm can set this to "True" before calling. +#This way the Logarithmic Extraction will know that the check is not excessive +newRun = False + +def computeSingleMIS(setDescription, constraints): + ''' + Taken from 'A Hybrid Diagnosis Approach Combining Black-Box + and White-Box Reasoning'. This attempts to find the Minimal Subset of Constraints + that will be inconsistent for the given Set Description. + @param setDescription- A set of rules linking several items together. + Think of this as boolean equation in Conjunctive Normal Form. + @param Constraints- a set of items we would like to include. + Think of this as a value assignment for the previous boolean equation. + ''' + potentialMIS = computeSingleMISHelper(setDescription, ImmutableSet(), constraints) + + #The Euler Implentation does not correctly compute the MIS for a set description + #where everything is always inconsistent (an empty set is inconsistent) + #This makes sense, but this library also considers this set description, + #so we must check the empty configuration here. + global newRun + if newRun == True and len(potentialMIS) == 1 \ + and setDescription.isConsistent(ImmutableSet()): + newRun = False + return ImmutableSet() + else: + newRun = False + return potentialMIS + +def computeSingleMISHelper(setDescription, currentConstraints, constraintsToAdd): + if len(constraintsToAdd) <= 1: + return constraintsToAdd + + constraintsToAddLeft = ImmutableSet(random.sample(constraintsToAdd, len(constraintsToAdd)/2)) + constraintsToAddRight = constraintsToAdd - constraintsToAddLeft + + #If either subset unioned with the current constraints is inconsistent + #then an MIS exists in the subset of them + if setDescription.isConsistent( currentConstraints.union(constraintsToAddLeft) ): + return computeSingleMISHelper(setDescription, currentConstraints, constraintsToAddLeft) + if setDescription.isConsistent( currentConstraints.union(constraintsToAddRight)): + return computeSingleMISHelper(setDescription, currentConstraints, constraintsToAddRight) + + #If both subsets unioned with the current constraints is consistent + #Then an MIS of the current constraints must use elements from both subsets. + #This will find such an MIS + potentialSolutionLeft = computeSingleMISHelper(setDescription, + currentConstraints.union(constraintsToAddRight), constraintsToAddLeft) + potentialSolutionRight = computeSingleMISHelper(setDescription, + currentConstraints.union(potentialSolutionLeft), constraintsToAddRight) + return potentialSolutionLeft.union(potentialSolutionRight) \ No newline at end of file diff --git a/src/solamanDedekind/Dedekind/Dedekind/Algorithms/Hitting_Set_Tree/__init__.py b/src/solamanDedekind/Dedekind/Dedekind/Algorithms/Hitting_Set_Tree/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/src/solamanDedekind/Dedekind/Dedekind/Algorithms/Random/Random.py b/src/solamanDedekind/Dedekind/Dedekind/Algorithms/Random/Random.py new file mode 100644 index 0000000..fd0df3e --- /dev/null +++ b/src/solamanDedekind/Dedekind/Dedekind/Algorithms/Random/Random.py @@ -0,0 +1,81 @@ +''' +Created on May 4, 2015 + +@author: Solaman +''' +from sets import ImmutableSet +checkedMap = {} + +def computeAllMIS(setDescription, constraints): + ''' + Finds the Minimum Inconsistent Subsets for the given constraints and set Description + using a randomized approach. We check the powerset of constraints, and rule out possible + MIS based on whether or not a given set is consistent. + NOTE: There are probably far more optimal implementations of such a random algorithm. However, + for the sake of comparison, we are only interested in the number of times + 'isConsistent' is called and so we refrain from these optimizations. + @param setDescription- A set of rules linking several items together. + Think of this as boolean equation in Conjunctive Normal Form. + @param Constraints- a set of items we would like to include. + Think of this as a value assignment for the previous boolean equation. + ''' + global checkedMap + checkedMap = {} + setsToCheck = generatePowerset(constraints) + misSet = set() + while len(setsToCheck) > 0: + checkRandomSet(setsToCheck, misSet, setDescription) + + return misSet + + +def checkRandomSet(setsToCheck, misSet, setDescription): + ''' + Takes a random set from the possible sets available and first finds its parent and children sets. + If it has none, then we know it is an MIS and add it to our solution. + If it has some, then we check if it is consistent and rule out the respective sets. + ''' + global checkedMap + import random + chosenSet = random.sample(setsToCheck, 1)[0] + parentSets = set() + childrenSets = set() + + for aSet in setsToCheck: + if chosenSet == aSet: + continue + elif aSet.issubset(chosenSet): + childrenSets.add(aSet) + elif aSet.issuperset(chosenSet): + parentSets.add(aSet) + + if chosenSet in checkedMap: + if len(parentSets) + len(childrenSets) == 0: + setsToCheck.remove(chosenSet) + misSet.add(chosenSet) + return + else: + if setDescription.isConsistent(chosenSet): + #If consistent, then all parents are consistent and also not MIS + for aSet in parentSets: + setsToCheck.remove(aSet) + else: + #If inconsistent, then all children including this one are inconsistent and also not MIS + for aSet in childrenSets: + setsToCheck.remove(aSet) + setsToCheck.remove(chosenSet) + checkedMap[chosenSet] = 1 + + + + +def generatePowerset(theSet): + ''' + Generates powerset of a given set. + Original code found at http://stackoverflow.com/questions/18826571/python-powerset-of-a-given-set-with-generators + ''' + powerSet = set() + from itertools import chain, combinations + for subset in chain.from_iterable(combinations(theSet, r) for r in range(len(theSet)+1)): + powerSet.add( ImmutableSet(subset)) + return powerSet \ No newline at end of file diff --git a/src/solamanDedekind/Dedekind/Dedekind/Algorithms/Random/__init__.py b/src/solamanDedekind/Dedekind/Dedekind/Algorithms/Random/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/src/solamanDedekind/Dedekind/Dedekind/Algorithms/TopDown/TopDown.py b/src/solamanDedekind/Dedekind/Dedekind/Algorithms/TopDown/TopDown.py new file mode 100644 index 0000000..292362d --- /dev/null +++ b/src/solamanDedekind/Dedekind/Dedekind/Algorithms/TopDown/TopDown.py @@ -0,0 +1,71 @@ +''' +Created on May 4, 2015 + +@author: Solaman +''' +from sets import ImmutableSet + +def computeAllMIS(setDescription, constraints): + ''' + Finds the Minimum Inconsistent Subsets for the given constraints and set Description + using a top down approach (where top is full set). We check the powerset of constraints, and rule out possible + MIS based on whether or not a given set is consistent. + NOTE: There are probably far more optimal implementations of such an algorithm. However, + for the sake of comparison, we are only interested in the number of times + 'isConsistent' is called and so we refrain from these optimizations. + @param setDescription- A set of rules linking several items together. + Think of this as boolean equation in Conjunctive Normal Form. + @param Constraints- a set of items we would like to include. + Think of this as a value assignment for the previous boolean equation. + ''' + + setsToCheck = generatePowerset(constraints) + misSet = set() + findMISSets(setsToCheck, misSet, setDescription) + + return misSet + + +def findMISSets(setsToCheck, misSet, setDescription): + ''' + Takes the largest set from potential MIS and find its children. Check if it is consistent + If it is, and it has children, then rule it out as an MIS, else add it to the MIS + If it is not, then rule it and its children out as MIS. + ''' + setsToCheckList = list(setsToCheck) + setsToCheckList.sort(key = lambda self: len(self), reverse = True) + + for currentSet in setsToCheckList: + if currentSet not in setsToCheck: + continue + childrenSets = set() + parentSets = set() + for aSet in setsToCheck: + if aSet.issubset(currentSet) and aSet != currentSet: + childrenSets.add(aSet) + + if aSet.issuperset(currentSet) and aSet != currentSet: + parentSets.add(aSet) + + if not setDescription.isConsistent(currentSet): + for child in childrenSets: + setsToCheck.remove(child) + setsToCheck.remove(currentSet) + else: + for parent in parentSets: + setsToCheck.remove(parent) + + for currentSet in setsToCheckList: + if currentSet in setsToCheck: + misSet.add(currentSet) + +def generatePowerset(theSet): + ''' + Generates powerset of a given set. + Original code found at http://stackoverflow.com/questions/18826571/python-powerset-of-a-given-set-with-generators + ''' + powerSet = set() + from itertools import chain, combinations + for subset in chain.from_iterable(combinations(theSet, r) for r in range(len(theSet)+1)): + powerSet.add( ImmutableSet(subset)) + return powerSet \ No newline at end of file diff --git a/src/solamanDedekind/Dedekind/Dedekind/Algorithms/TopDown/__init__.py b/src/solamanDedekind/Dedekind/Dedekind/Algorithms/TopDown/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/src/solamanDedekind/Dedekind/Dedekind/Algorithms/__init__.py b/src/solamanDedekind/Dedekind/Dedekind/Algorithms/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/src/solamanDedekind/Dedekind/Dedekind/Algorithms/analysis.py b/src/solamanDedekind/Dedekind/Dedekind/Algorithms/analysis.py new file mode 100644 index 0000000..a01ce23 --- /dev/null +++ b/src/solamanDedekind/Dedekind/Dedekind/Algorithms/analysis.py @@ -0,0 +1,110 @@ +''' +Created on Apr 28, 2015 + +@author: Solaman + +Used to perform analysis on various +algorithms for computing Minimum Inconsistent Subsets. +''' +from Algorithms.Hitting_Set_Tree import HittingSetTree +from Algorithms.Random import Random +from Algorithms.BottomUp import BottomUp +from Algorithms.TopDown import TopDown + +from Model.DedekindLattice import DedekindLattice +from Model.DedekindSetMapping import getFullSet, getConfAsSet + +algorithms = {} +algorithms["hitting_set_tree"] = HittingSetTree.computeAllMIS +algorithms["random"] = Random.computeAllMIS +algorithms["bottom_up"] = BottomUp.computeAllMIS +algorithms["top_down"] = TopDown.computeAllMIS +isAcceptedOriginalFunction = None + + +def runAnalysis(args): + if len(args) != 2: + print "must provide an algorithm and input size!" + return + algorithmKey = args[0] + if algorithmKey not in algorithms: + print "sorry! That algorithm was not recognized."\ + , "\nHere are the options:" + for _algorithmKey in algorithms.keys(): + print "\n\t", _algorithmKey + return + algorithm = algorithms[algorithmKey] + + if int(args[1]) >5: + print "input larger than 5 is terribly slow, don't do it!" + return + inputSize = int(args[1]) + lattice = DedekindLattice(inputSize) + callCounts = {} + + + while True: + currentNode = lattice.getNextNode() + + if currentNode == None: + break + + modifyisAccepted(currentNode) + fullConstraints = getFullSet(inputSize) + algorithm(currentNode, fullConstraints) + + if currentNode.callCount not in callCounts: + callCounts[currentNode.callCount] = 1 + else: + callCounts[currentNode.callCount] += 1 + + + printStatistics(callCounts) + +def printStatistics(callCounts): + print "min: ", min ( callCounts.keys()) + print "max: ", max( callCounts.keys()) + + totalAlgorithmCalls = sum( callCounts.values()) + totalIsConsistentCalls = 0 + for key in callCounts.keys(): + totalIsConsistentCalls += key * callCounts[key] + + print "mean: ", totalIsConsistentCalls/ totalAlgorithmCalls + + mean = totalIsConsistentCalls/ totalAlgorithmCalls + variance = 0.0 + for key in callCounts: + variance += (callCounts[key] - mean)**2 + variance = float(variance)/float(totalAlgorithmCalls) + + print "standard deviation: ", variance **.5 + medianCount = totalAlgorithmCalls/2 + for key in callCounts.keys(): + medianCount -= callCounts[key] + if medianCount <= 0: + print "median: ", key + break + + print "mode: ", max( callCounts.keys(), key= lambda x: callCounts[x]) + +def modifyisAccepted(setDescription): + ''' + In order to properly analyze an algorithm, we need to check how + many times it has to call "isAccepted" on the given setDescription. + We make this modification here. + ''' + + setDescription.callCount = 0 + setDescription.tries = [] + import types + setDescription.isConsistent = types.MethodType(isConsistent, setDescription) + + + + +def isConsistent(self, configuration): + from Model.DedekindNode import isConsistent as isConsistentOld + self.callCount += 1 + return isConsistentOld(self, configuration) + diff --git a/src/solamanDedekind/Dedekind/Dedekind/Controller/CommandOptions.py b/src/solamanDedekind/Dedekind/Dedekind/Controller/CommandOptions.py new file mode 100644 index 0000000..2ac4f94 --- /dev/null +++ b/src/solamanDedekind/Dedekind/Dedekind/Controller/CommandOptions.py @@ -0,0 +1,85 @@ +''' +Created on Mar 31, 2015 + +@author: Solaman + +Used to store relevant information about command options. +''' +import sys + +class CommandOption(object): + ''' + class for a particular console option + ''' + + + def __init__(self, helpString, commandFunction): + ''' + Constructor + ''' + #Should describe what the option does + self.helpString = helpString + + #Function used to execute this option + self.commandFunction = commandFunction + + +class CommandOptions(object): + ''' + Used to store and order console options + ''' + + def __init__(self): + #If the user wants to request a command by its command string, they can do so with this + self._commands = {} + + self._commands["help"] = CommandOption("displays all available commands"\ + + " along with a description for each", self.printCommandOptions) + + def addCommand(self, commandString, helpString, commandFunction): + ''' + Adds a command option. This option then can be requested by calling + 'selectOption'. + ''' + if commandString == "help": + exceptMessage = "attempted to overwrite \"help\" option in ConsoleOptions. Don't do that plz." + raise Exception(exceptMessage) + + if not callable(commandFunction): + exceptMessage = "Command function must be a function! (must be callable)" + raise Exception(exceptMessage) + + self._commands[commandString] = CommandOption(helpString, commandFunction) + + def printCommandOptions(self): + ''' + Prints all commands available along with the help string associated with each + respective command. + ''' + print "-----" + print "Available Commands" + print "-----" + for command in self._commands.iterkeys(): + print command, ": ", self._commands[command].helpString + print "-----" + + + def selectCommand(self, userInput): + ''' + Calls the function associated with a particular command. + ''' + if len(userInput) == 0: + sys.stderr.write( "Must enter a command, type \"help\" to list all commands\n" ) + return + + command = userInput[0] + inputParams = userInput[1:] + + if command not in self._commands: + sys.stderr.write("Command not recognized, type \"help\" to list all commands\n") + return + + if len(inputParams) == 0: + self._commands[command].commandFunction() + else: + self._commands[command].commandFunction(inputParams) \ No newline at end of file diff --git a/src/solamanDedekind/Dedekind/Dedekind/Controller/Console.py b/src/solamanDedekind/Dedekind/Dedekind/Controller/Console.py new file mode 100644 index 0000000..8acf541 --- /dev/null +++ b/src/solamanDedekind/Dedekind/Dedekind/Controller/Console.py @@ -0,0 +1,33 @@ +''' +Created on Mar 31, 2015 + +@author: Solaman +This is the main interface with the application if the user chooses to use +it as a console application. +''' +import os + +class Console(object): + + def __init__(self, commandOptions): + self.commandOptions = commandOptions + + self.commandOptions.addCommand("exit", "exits the console.", self.endConsole) + + self.shouldContinue = True + + + def selectCommand(self, userInput): + self.commandOptions.selectCommand(userInput) + + def endConsole(self): + print "Bye!" + self.shouldContinue = False + +def run(commandOptions): + console = Console(commandOptions) + print "hello! To list commands, type \"help\"" + while console.shouldContinue: + userInput = raw_input("enter command: ") + userInput = userInput.split(" ") + console.selectCommand(userInput) \ No newline at end of file diff --git a/src/solamanDedekind/Dedekind/Dedekind/Controller/__init__.py b/src/solamanDedekind/Dedekind/Dedekind/Controller/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/src/solamanDedekind/Dedekind/Dedekind/Dedekind.py b/src/solamanDedekind/Dedekind/Dedekind/Dedekind.py new file mode 100644 index 0000000..b42fdbd --- /dev/null +++ b/src/solamanDedekind/Dedekind/Dedekind/Dedekind.py @@ -0,0 +1,37 @@ +''' +Created on Apr 1, 2015 + +@author: Solaman +''' +from Controller.CommandOptions import CommandOptions +from Controller import Console +from Model.DedekindLattice import getDedekindNumber, generateDotFiles + +import sys +from Algorithms.analysis import runAnalysis + +def runConsole(): + global standardCommands + + del standardCommands._commands["console"] + Console.run(standardCommands) + +standardCommands = CommandOptions() + +standardCommands.addCommand("getNumber", "Finds Dedekind Number for a given input size."\ + + "\n\tInput:" + " function input size", getDedekindNumber) + +standardCommands.addCommand("dotFiles", "Generates dot files of all monotone boolean functions"\ + + " for a given input size." + "\n\tInput:" + " function input size", generateDotFiles) + +standardCommands.addCommand("analyze", "Runs a System-Diagnostic algorithm"\ + + " and perform analysis on varying MBF's" +"\n\tInput: algorithm name, input size", runAnalysis ) +standardCommands.addCommand("console", "Run the program as a console.", runConsole) + + +def main(): + userInput = sys.argv[1:] + standardCommands.selectCommand(userInput) + +if __name__ == '__main__': + main() \ No newline at end of file diff --git a/src/solamanDedekind/Dedekind/Dedekind/DedekindAnalysis.py b/src/solamanDedekind/Dedekind/Dedekind/DedekindAnalysis.py new file mode 100644 index 0000000..3baa304 --- /dev/null +++ b/src/solamanDedekind/Dedekind/Dedekind/DedekindAnalysis.py @@ -0,0 +1,22 @@ +''' +Created on Mar 13, 2015 + +@author: Solaman +''' +import cProfile +from Model.DedekindNode import DedekindNode +from Model.DedekindLattice import DedekindLattice + +node = DedekindNode(4, [15]) +lattice = DedekindLattice(5, lean = True) + +def analyzeDedekindNode(): + cProfile.run("node.generatePossibleConfigurations()") + +def analyzeFindUniqueFunctions(): + cProfile.run("lattice.getDedekindNumber()") + +if __name__ == '__main__': + #analyzeDedekindNode() + #analyzeDedekindLattice() + analyzeFindUniqueFunctions() \ No newline at end of file diff --git a/src/solamanDedekind/Dedekind/Dedekind/GeneratedDedekindLattices/preserve.txt b/src/solamanDedekind/Dedekind/Dedekind/GeneratedDedekindLattices/preserve.txt new file mode 100644 index 0000000..13497a6 --- /dev/null +++ b/src/solamanDedekind/Dedekind/Dedekind/GeneratedDedekindLattices/preserve.txt @@ -0,0 +1,6 @@ +March 3rd, 2015: GeneratedDedekindLattices will not be included in a git commit +unless a file is included. This is that file. + +Note: We could have the application generate this folder as needed, but I believe +that because the folder's existence isn't tied to a run of the application +("dedekind world 5" would be a folder that is tied) the user might expect the folder to already exist. \ No newline at end of file diff --git a/src/solamanDedekind/Dedekind/Dedekind/Model/DedekindLattice.py b/src/solamanDedekind/Dedekind/Dedekind/Model/DedekindLattice.py new file mode 100644 index 0000000..3d53f7c --- /dev/null +++ b/src/solamanDedekind/Dedekind/Dedekind/Model/DedekindLattice.py @@ -0,0 +1,203 @@ +''' +Created on Feb 26, 2015 + +@author: Solaman +''' +from DedekindNode import DedekindNode +from Model.LevelPermutor import LevelPermutor +from DedekindNode import getIndex + +class LatticeFiller(object): + ''' + Constructed when the Lattice is constructed if the user wants to fill in the + lattice with each Monotone Boolean Function individually. + This class provides a level of abstraction from the inner workings of the module. + ''' + def __init__(self, lattice): + self.lattice = lattice + self.nodeList = [] + self.nodeList.append(lattice.emptyFunction) + self.nodeList.append(lattice.baseFunction) + self.wasFilled = False + + def getNextNode(self): + ''' + Returns the most recently added node to the queue + if it is not empty. + ''' + if self.nodeList == []: + return None + node = self.nodeList.pop() + children = node.generateChildren() + for child in children: + self.nodeList.append(child) + self.lattice.lattice[getIndex(child)] = child + return node + + def fillLattice(self): + while self.getNextNode() != None: + continue + + def getDedekindNumber(self): + if self.wasFilled == False: + self.fillLattice() + self.wasFilled = True + return len(self.lattice.lattice.values()) + +class LatticeFillerUnique(object): + ''' + Constructed when the Lattice is constructed if the user would like to fill in + the lattice with Monotone Boolean Functions that are equivalent by level. + This class provides a level of abstraction from the inner workings of the module. + ''' + def __init__(self, lattice, lean= False): + from Model.DedekindNodeLean import DedekindNodeLean + if lean == True: + lattice.baseFunction = DedekindNodeLean(lattice.baseFunction.inputSize,\ + lattice.baseFunction.acceptedConfigurations[-1]) + + self.lattice = lattice + self.nodeList = [] + lattice.emptyFunction.isVisited = False + lattice.emptyFunction.parent = None + self.nodeList.append(lattice.emptyFunction) + lattice.baseFunction.isVisited = False + lattice.baseFunction.parent = None + self.nodeList.append(lattice.baseFunction) + self.levelPermutor = LevelPermutor(lattice.inputSize) + self.mileMarker = 10000 + + self.wasFilled = False + + def getNextNode(self): + #We don't need to compute functions that are isomorphisms of each other. + #We store each function by there level, and then counts by the number of possible children for the function + #It is proven that functions by level that have the same number of possible children are isomorphisms + #{"level" : {"isomorphismCount": count, "children" : children } } + if self.nodeList == []: + return None + node = self.nodeList.pop() + if node.isVisited == True: + if node.parent == None: + return node + else: + node.parent.childrenCount += node.childrenCount + if node.parent.childrenCount >= self.mileMarker: + print "marker: ", self.mileMarker + self.mileMarker = node.parent.childrenCount * 2 + return self.getNextNode() + + if self.getKey(node) in self.levelPermutor: + node.parent.childrenCount += self.levelPermutor[self.getKey(node)].childrenCount + return self.getNextNode() + else: + self.lattice.lattice[ getIndex(node.getAcceptedConfigurationsAsList()) ] = node + node.childrenCount = 1 + self.levelPermutor[node.getLastLevel()] = node + children = node.generateChildren() + node.isVisited = True + self.nodeList.append(node) + for child in children: + child.parent = node + child.isVisited = False + self.nodeList.append(child) + return node + + def getKey(self, node): + if hasattr(node, "key"): + return node.key + else: + node.key = getIndex(node.getLastLevel()) + return node.key + + def fillLattice(self): + while self.getNextNode() != None: + continue + + def getDedekindNumber(self): + if self.wasFilled == False: + self.fillLattice() + self.wasFilled = True + return self.lattice.baseFunction.childrenCount + 1 + +class DedekindLattice(object): + ''' + We aim to generate the Dedekind Lattices using this class. Namely, + We want to generate all monotone boolean functions given an n input size. + Currently, we will aim to only generate the lattices in working memory. + Future implementations will hopefully be able to dynamically + Generate a node of a given Lattice. + ''' + + def __init__(self, inputSize, generateUnique = True, lean = False): + ''' + Constructor. For now, we will store each monotone boolean function + as an object. Future implementations will store them as a single bit + for lean memory usage + ''' + if inputSize < 0: + raise Exception("Input size must be greater than or equal to 0") + self.lattice = {} + + #bit mask refers to the possible bit values + #of a given configuration. E.G. boolean functions with 4 inputs + #Will have a bit mask of 0xF + self.bitMask = 2**(inputSize) - 1 + self.inputSize = inputSize + + self.emptyFunction = DedekindNode(self.inputSize, []) + self.lattice[ getIndex(self.emptyFunction)] = self.emptyFunction + + self.baseFunction = DedekindNode(self.inputSize, [self.bitMask]) + self.lattice[ getIndex(self.baseFunction)] = self.baseFunction + + if generateUnique: + self.latticeFiller = LatticeFillerUnique(self, lean) + + else: + self.latticeFiller = LatticeFiller(self) + + def getDedekindNumber(self): + return self.latticeFiller.getDedekindNumber() + + def getNextNode(self): + return self.latticeFiller.getNextNode() + + + def generateDotFiles(self): + ''' + + ''' + import os + directoryName = os.path.join("GeneratedDedekindLattices", str(self.inputSize) + "_DedekindLattice") + if not os.path.exists(directoryName): + os.mkdir(directoryName) + updateTime = self.monotoneCount/10 + generatedFiles = 0 + for function in self.lattice.itervalues(): + function.writeToDotFile(directoryName) + generatedFiles += 1 + if generatedFiles % updateTime == 0: + print generatedFiles, " written so far" + print "Done" + + +def getDedekindNumber(userInput): + ''' + Constructs the Dedekind Lattice for the given input size and + returns the dedekind number associated with that input. + Values that return within a minute are currently n <= 5. + ''' + inputSize = int(userInput[0]) + dedekindLattice = DedekindLattice(inputSize, lean =True) + print dedekindLattice.getDedekindNumber() + +def generateDotFiles(userInput): + ''' + Constructs the Dedekind Lattice for the given input size and + generates the dot files for each monotone boolean function with the given input size. + ''' + inputSize = int(userInput[0]) + dedekindLattice = DedekindLattice(inputSize) + dedekindLattice.fillLattice() + dedekindLattice.generateDotFiles() \ No newline at end of file diff --git a/src/solamanDedekind/Dedekind/Dedekind/Model/DedekindNode.py b/src/solamanDedekind/Dedekind/Dedekind/Model/DedekindNode.py new file mode 100644 index 0000000..261734d --- /dev/null +++ b/src/solamanDedekind/Dedekind/Dedekind/Model/DedekindNode.py @@ -0,0 +1,290 @@ +''' +Created on Mar 3, 2015 + +@author: Solaman +''' +from itertools import combinations as genCombinations +import os +from collections import Iterable +from DedekindNodeIter import DedekindNodeIter +from DedekindSetMapping import getConfAsInt + +#Used for Dot file generation +#Once a node is written to a dot file +#A full Node is created to help +fullNodes = {} + +#Used for Dot file generation +#Once a node is written to a dot file +#labels for each configuration are made +configurationLabelss = {} + +#Used for Dot file Generation +#Once a node is written to a dot file +#edges between configurations are written as dot edges. +dotEdgess = {} + +#To avoid cost of calculating configuration levels continuously, +#We will calculate the levels from the very beginning. +configurationLevelss = {} + +class DedekindNode(Iterable): + ''' + A boolean function. It is up to the user to ensure that it is monotone. + ''' + + + def __init__(self, inputSize, acceptedConfigurations, nodeToCopy = None): + ''' + Each function has a set of accepted configurations. To aid in the book keeping of this algorithm, + each accepted configuration is stored by "level", namely, how many of the given inputs must be "off" + in the configuration. so if the configuration 0b0001 is accepted and the bitmask is 0b1111, + then the configuration would be stored at the 3rd level. + For rudimentary checking, the accepted configurations are added by descending level. + ''' + self.childrenSize = 0 + self.inputSize = inputSize + self.bitMask = self.bitMask = 2**(inputSize) - 1 + + + if nodeToCopy != None: + temp = nodeToCopy.getAcceptedConfigurationsAsList() + temp.extend(acceptedConfigurations) + acceptedConfigurations = temp + self.acceptedConfigurations = [] + + for acceptedConfiguration in acceptedConfigurations: + level = getConfigurationLevel(self.inputSize, acceptedConfiguration) + if level > len( self.acceptedConfigurations): + exceptionMessage = "Cannot add configurations beyond the highest level + 1" \ + + "\n attempted level: " + str(level) \ + +"\n level limit: " + str(len(self.acceptedConfigurations)) + raise Exception( exceptionMessage) + + if level == len(self.acceptedConfigurations): + self.acceptedConfigurations.append( [acceptedConfiguration]) + else: + self.acceptedConfigurations[level].append( acceptedConfiguration) + + self.index = -1 + + + def getAcceptedConfigurationsAsList(self): + acceptedConfigurations = [] + for level in self.acceptedConfigurations: + acceptedConfigurations.extend(level) + + return acceptedConfigurations + + def isConsistent(self, configuration): + ''' + Checks if a configuration would be accepted or not. + ''' + return isConsistent(self, configuration) + + def _generatePossibleConfigurations(self): + ''' + Generates possible configurations to add to the function such that the new functions would be monotone + (given that this function is monotone). We observe that this can be done by level combinations. + E.G. if the input size is 4, and the last level of the function is 1->[0b1011, 0b0111], then the children + of the node in the lattice can have the level 2 configuration [0b0011]. + (if any other, then it would not be monotone). + ''' + possibleConfigurations = [] + newMaxLevel = len(self.acceptedConfigurations) + + #current max configuration level is [self.bitMask] + if newMaxLevel == 1: + possibleConfigurations = getLevelOneConfigurations(self.inputSize) + + #Entire Dedekind Node Lattice is filled, can add [0] as an accepted configuration + elif newMaxLevel == self.inputSize \ + and len(self.acceptedConfigurations[-1]) == self.inputSize: + possibleConfigurations = [0] + #current max configuration level is [] (none are accepted) + elif newMaxLevel == 0: + return [] + elif newMaxLevel< self.inputSize: + combinations = genCombinations(self.acceptedConfigurations[-1], newMaxLevel) + possibleConfigurations = [] + for combination in combinations: + possibleConfiguration = self.bitMask + for configuration in combination: + possibleConfiguration &= configuration + if getConfigurationLevel(self.inputSize, possibleConfiguration) == newMaxLevel: + possibleConfigurations.append(possibleConfiguration) + return possibleConfigurations + + return possibleConfigurations + + + def generateChildren(self): + ''' + Generates all Dedekind Nodes that would be considered the children of this + DedekindNode + ''' + children = [] + possibleConfigurations = self._generatePossibleConfigurations() + + for numberOfConfigurations in range(1, len(possibleConfigurations) + 1): + combinations = genCombinations(possibleConfigurations, numberOfConfigurations) + for combination in combinations: + children.append( DedekindNode(self.inputSize, combination, self)) + + return children + + def __iter__(self): + ''' + Implemented this with good design in mind, however + If you want something fast, it is better to use + getAcceptedConfigurationsAsList + ''' + return DedekindNodeIter(self) + + def getLastLevel(self): + if len(self.acceptedConfigurations) > 0: + return self.acceptedConfigurations[-1] + else: + return [] + + def writeToDotFile(self, writeLocation): + global fullNodes, configurationLabelss, dotEdgess + + dotFileName = os.path.join(writeLocation, "n_" + str(self.inputSize)\ + + "." + "world_" + str(getIndex(self.getAcceptedConfigurationsAsList()))\ + + ".dot") + dotFile = open( dotFileName, "w") + dotFile.write("""digraph{ + rankdir=BT + node[shape=circle, style=filled, label=""] + edge[dir=none]\n""") + + initDotVariables(self.inputSize) + fullNode = fullNodes[self.inputSize] + configurationLabels = configurationLabelss[self.inputSize] + dotEdges = dotEdgess[self.inputSize] + + #configurationList = self.getAcceptedConfigurationsAsList() + for configuration in fullNode: + if configuration in self: + dotFile.write( configurationLabels[configuration] +" [ color = green, "\ + + "label = \""+ configurationLabels[configuration] + "\"]\n") + else: + dotFile.write( configurationLabels[configuration] +" [ color = red, "\ + + "label = \""+ configurationLabels[configuration] + "\"]\n") + + dotFile.write(dotEdges) + dotFile.write("}") + + dotFile.close() + +def isConsistent(node, configuration): + ''' + Checks if a configuration would be deemed "inconsistent". + This is confusing! The DedekindNode represents a faulty system, and the + "accepted configurations" represent sets such that, if you deemed the given + components (represented by bits) as "faulty" and all others as safe, you would explain + erroneous output. + ''' + from sets import ImmutableSet + if isinstance(configuration, ImmutableSet): + configuration = getConfAsInt(configuration, node.inputSize) + if (getIndex(node.getAcceptedConfigurationsAsList()) & 1 << configuration ) == 0: + return False + else: + return True + +def getFullNode(inputSize): + global fullNodes + if inputSize not in fullNodes: + bitMask = (1< " \ + + configurationLabels[configuration] + "\n" + dotEdges += edgeString + + fullNodes[inputSize] = fullNode + configurationLabelss[inputSize] = configurationLabels + dotEdgess[inputSize] = dotEdges + +def getIndex(configurationList): + ''' + If we treat each configuration as its own integer value, we can combine each value into an integer + of size 2**inputSize bits. E.G. if the input size is 4, then each configuration has a value between 0-15. + So an integer of 16 bits, where each bit is for each configuration, will represent the function + and its accepted configurations. Since this value is unique, we can also use it as an index for the function + ''' + index = 0 + for configuration in configurationList: + index |= (1 << configuration) + + return index + +def getConfigurationLevel(inputSize, configuration): + global configurationLevelss + if inputSize not in configurationLevelss: + bitMask = (1<= len(self.dedekindNode.acceptedConfigurations): + raise StopIteration + + result = self.dedekindNode.acceptedConfigurations[self.levelIndex][self.configIndex] + self.configIndex += 1 + + if self.configIndex >= len(self.dedekindNode.acceptedConfigurations[self.levelIndex]): + self.configIndex = 0 + self.levelIndex += 1 + + return result + \ No newline at end of file diff --git a/src/solamanDedekind/Dedekind/Dedekind/Model/DedekindNodeLean.py b/src/solamanDedekind/Dedekind/Dedekind/Model/DedekindNodeLean.py new file mode 100644 index 0000000..4f756a9 --- /dev/null +++ b/src/solamanDedekind/Dedekind/Dedekind/Model/DedekindNodeLean.py @@ -0,0 +1,133 @@ +''' +Created on Apr 30, 2015 + +@author: Solaman +''' +from itertools import combinations as genCombinations +from DedekindSetMapping import getConfAsInt + +#To avoid cost of calculating configuration levels continuously, +#We will calculate the levels from the very beginning. +configurationLevelss = {} + +class DedekindNodeLean(object): + ''' + Attempt to speed up generation of nodes. For computing the Dedekind number, + we only need the "highest" level of configurations, so we attempt + to reduce overhead by keeping only this level. + ''' + + def __init__(self, inputSize, acceptedConfigurations): + ''' + Each function has a set of accepted configurations. To aid in the book keeping of this algorithm, + each accepted configuration is stored by "level", namely, how many of the given inputs must be "off" + in the configuration. so if the configuration 0b0001 is accepted and the bitmask is 0b1111, + then the configuration would be stored at the 3rd level. + For rudimentary checking, the accepted configurations are added by descending level. + ''' + self.childrenSize = 0 + self.inputSize = inputSize + self.bitMask = self.bitMask = 2**(inputSize) - 1 + self.acceptedConfigurations = acceptedConfigurations + self.level = -1 + if acceptedConfigurations != []: + self.level = getConfigurationLevel(inputSize, acceptedConfigurations[0]) + + self.index = -1 + + def _generatePossibleConfigurations(self): + ''' + Generates possible configurations to add to the function such that the new functions would be monotone + (given that this function is monotone). We observe that this can be done by level combinations. + E.G. if the input size is 4, and the last level of the function is 1->[0b1011, 0b0111], then the children + of the node in the lattice can have the level 2 configuration [0b0011]. + (if any other, then it would not be monotone). + ''' + possibleConfigurations = [] + + #current max configuration level is [self.bitMask] + if self.level == 0: + possibleConfigurations = getLevelOneConfigurations(self.inputSize) + + #Entire Dedekind Node Lattice is filled, can add [0] as an accepted configuration + elif self.level == self.inputSize - 1 \ + and len(self.acceptedConfigurations) == self.inputSize: + possibleConfigurations = [0] + #current max configuration level is [] (none are accepted) + elif self.level == -1: + return [] + elif self.level< self.inputSize - 1: + combinations = genCombinations(self.acceptedConfigurations, self.level + 1) + possibleConfigurations = [] + for combination in combinations: + possibleConfiguration = self.bitMask + for configuration in combination: + possibleConfiguration &= configuration + if getConfigurationLevel(self.inputSize, possibleConfiguration) == self.level + 1: + possibleConfigurations.append(possibleConfiguration) + return possibleConfigurations + + return possibleConfigurations + + + def generateChildren(self): + ''' + Generates all Dedekind Nodes that would be considered the children of this + DedekindNode + ''' + children = [] + possibleConfigurations = self._generatePossibleConfigurations() + + for numberOfConfigurations in range(1, len(possibleConfigurations) + 1): + combinations = genCombinations(possibleConfigurations, numberOfConfigurations) + for combination in combinations: + children.append( DedekindNodeLean(self.inputSize, combination)) + + return children + + def getLastLevel(self): + return self.acceptedConfigurations + + def getAcceptedConfigurationsAsList(self): + return self.acceptedConfigurations + +def getConfigurationLevel(inputSize, configuration): + global configurationLevelss + if inputSize not in configurationLevelss: + bitMask = (1< 0111 +1111 -> 1011 +1111 -> 1101 +1111 -> 1110 +0111 -> 0011 +1011 -> 0011 +0111 -> 0101 +1101 -> 0101 +0111 -> 0110 +1110 -> 0110 +1011 -> 1001 +1101 -> 1001 +1011 -> 1010 +1110 -> 1010 +1101 -> 1100 +1110 -> 1100 +0011 -> 0001 +0101 -> 0001 +1001 -> 0001 +0011 -> 0010 +0110 -> 0010 +1010 -> 0010 +0101 -> 0100 +0110 -> 0100 +1100 -> 0100 +1001 -> 1000 +1010 -> 1000 +1100 -> 1000 +0001 -> 0000 +0010 -> 0000 +0100 -> 0000 +1000 -> 0000 +} \ No newline at end of file diff --git a/src/solamanDedekind/Dedekind/Dedekind/Test/ModelTest/n_4.world_57344.pdf b/src/solamanDedekind/Dedekind/Dedekind/Test/ModelTest/n_4.world_57344.pdf new file mode 100644 index 0000000000000000000000000000000000000000..2b580d694a8ca210e4a7295da1ce314160c34e55 GIT binary patch literal 10360 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 V_Kv?I&dtpUJ7`EHt{{Q>{{Um}a~c2u literal 0 HcmV?d00001 diff --git a/src/solamanDedekind/Dedekind/Dedekind/Test/ModelTest/writeToDotTest.dot b/src/solamanDedekind/Dedekind/Dedekind/Test/ModelTest/writeToDotTest.dot new file mode 100644 index 0000000..b828616 --- /dev/null +++ b/src/solamanDedekind/Dedekind/Dedekind/Test/ModelTest/writeToDotTest.dot @@ -0,0 +1,53 @@ +digraph{ + rankdir=BT + node[shape=circle, style=filled, label=""] + edge[dir=none] +1111 [ color = green, label = "1111"] +0111 [ color = red, label = "0111"] +1011 [ color = red, label = "1011"] +1101 [ color = green, label = "1101"] +1110 [ color = green, label = "1110"] +0011 [ color = red, label = "0011"] +0101 [ color = red, label = "0101"] +0110 [ color = red, label = "0110"] +1001 [ color = red, label = "1001"] +1010 [ color = red, label = "1010"] +1100 [ color = red, label = "1100"] +0001 [ color = red, label = "0001"] +0010 [ color = red, label = "0010"] +0100 [ color = red, label = "0100"] +1000 [ color = red, label = "1000"] +0000 [ color = red, label = "0000"] +1111 -> 0111 +1111 -> 1011 +1111 -> 1101 +1111 -> 1110 +0111 -> 0011 +1011 -> 0011 +0111 -> 0101 +1101 -> 0101 +0111 -> 0110 +1110 -> 0110 +1011 -> 1001 +1101 -> 1001 +1011 -> 1010 +1110 -> 1010 +1101 -> 1100 +1110 -> 1100 +0011 -> 0001 +0101 -> 0001 +1001 -> 0001 +0011 -> 0010 +0110 -> 0010 +1010 -> 0010 +0101 -> 0100 +0110 -> 0100 +1100 -> 0100 +1001 -> 1000 +1010 -> 1000 +1100 -> 1000 +0001 -> 0000 +0010 -> 0000 +0100 -> 0000 +1000 -> 0000 +} \ No newline at end of file diff --git a/src/solamanDedekind/Dedekind/Dedekind/Test/__init__.py b/src/solamanDedekind/Dedekind/Dedekind/Test/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/src/solamanDedekind/Dedekind/Dedekind/TransitionError.py b/src/solamanDedekind/Dedekind/Dedekind/TransitionError.py new file mode 100644 index 0000000..75be7a4 --- /dev/null +++ b/src/solamanDedekind/Dedekind/Dedekind/TransitionError.py @@ -0,0 +1,16 @@ +''' +Created on Mar 2, 2015 + +@author: Solaman +''' +class TransitionError(Exception): + ''' + Raised when an operation attempts a state transition that is + not allowed. + ''' + + def __init__(self, msg): + self.msg = msg + + def __str__(self): + return repr(self.value) \ No newline at end of file diff --git a/src/solamanDedekind/Dedekind/Dedekind/resources/__init__.py b/src/solamanDedekind/Dedekind/Dedekind/resources/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/src/solamanDedekind/Dedekind/Dedekind/resources/setValues.csv b/src/solamanDedekind/Dedekind/Dedekind/resources/setValues.csv new file mode 100644 index 0000000..1b91aad --- /dev/null +++ b/src/solamanDedekind/Dedekind/Dedekind/resources/setValues.csv @@ -0,0 +1 @@ +A,B,C,D,E,F,G,H,I,J,K,L,M,N,O,P,Q,R,S,T,U,V,W,X,Y,Z \ No newline at end of file diff --git a/src/solamanDedekind/Dedekind/GeneratedDedekindLattices/preserve.txt b/src/solamanDedekind/Dedekind/GeneratedDedekindLattices/preserve.txt index f6fad87..13497a6 100644 --- a/src/solamanDedekind/Dedekind/GeneratedDedekindLattices/preserve.txt +++ b/src/solamanDedekind/Dedekind/GeneratedDedekindLattices/preserve.txt @@ -1,6 +1,6 @@ -March 3rd, 2015: GeneratedDedekindLattices will not be included in a git commit -unless a file is included. This is that file. - -Note: We could have the application generate this folder as needed, but I believe -that because the folder's existence isn't tied to a run of the application +March 3rd, 2015: GeneratedDedekindLattices will not be included in a git commit +unless a file is included. This is that file. + +Note: We could have the application generate this folder as needed, but I believe +that because the folder's existence isn't tied to a run of the application ("dedekind world 5" would be a folder that is tied) the user might expect the folder to already exist. \ No newline at end of file diff --git a/src/solamanDedekind/Dedekind/Model/DedekindSetMapping.py b/src/solamanDedekind/Dedekind/Model/DedekindSetMapping.py index 2884098..29a9a42 100644 --- a/src/solamanDedekind/Dedekind/Model/DedekindSetMapping.py +++ b/src/solamanDedekind/Dedekind/Model/DedekindSetMapping.py @@ -1,59 +1,59 @@ -''' -Created on Apr 2, 2015 - -@author: Solaman -''' -import resources - -class DedekindSetMapping(object): - ''' - Since the DedekindLattice stores accepted configurations as integers, users of the library - might find it difficult to interact with as opposed to Python Sets. This class encapsulates - the mapping from those integer values to Elements in a set for easier use. - ''' - - - def __init__(self, inputSize, inputSet = None): - ''' - Constructor - @inputSet - input set for monotone boolean functions. If none is provided, a set is - constructed from "resources/setValues.csv" - ''' - self.inputSize = inputSize - self.bitToElement = {} - self.elementToBit = {} - - if inputSet == None: - import os - inputSetFileName = os.path.join(os.path.dirname(resources.__file__), "setValues.csv") - inputSet = open(inputSetFileName).read().split(",") - - elif isinstance(inputSet, set): - inputSet = list(inputSet) - - for bitShift in range(0, self.inputSize): - self.bitToElement[1< 0111 -1111 -> 1011 -1111 -> 1101 -1111 -> 1110 -0111 -> 0011 -1011 -> 0011 -0111 -> 0101 -1101 -> 0101 -0111 -> 0110 -1110 -> 0110 -1011 -> 1001 -1101 -> 1001 -1011 -> 1010 -1110 -> 1010 -1101 -> 1100 -1110 -> 1100 -0011 -> 0001 -0101 -> 0001 -1001 -> 0001 -0011 -> 0010 -0110 -> 0010 -1010 -> 0010 -0101 -> 0100 -0110 -> 0100 -1100 -> 0100 -1001 -> 1000 -1010 -> 1000 -1100 -> 1000 -0001 -> 0000 -0010 -> 0000 -0100 -> 0000 -1000 -> 0000 +digraph{ + rankdir=BT + node[shape=circle, style=filled, label=""] + edge[dir=none] +1111 [ color = green, label = "1111"] +0111 [ color = red, label = "0111"] +1011 [ color = red, label = "1011"] +1101 [ color = green, label = "1101"] +1110 [ color = green, label = "1110"] +0011 [ color = red, label = "0011"] +0101 [ color = red, label = "0101"] +0110 [ color = red, label = "0110"] +1001 [ color = red, label = "1001"] +1010 [ color = red, label = "1010"] +1100 [ color = red, label = "1100"] +0001 [ color = red, label = "0001"] +0010 [ color = red, label = "0010"] +0100 [ color = red, label = "0100"] +1000 [ color = red, label = "1000"] +0000 [ color = red, label = "0000"] +1111 -> 0111 +1111 -> 1011 +1111 -> 1101 +1111 -> 1110 +0111 -> 0011 +1011 -> 0011 +0111 -> 0101 +1101 -> 0101 +0111 -> 0110 +1110 -> 0110 +1011 -> 1001 +1101 -> 1001 +1011 -> 1010 +1110 -> 1010 +1101 -> 1100 +1110 -> 1100 +0011 -> 0001 +0101 -> 0001 +1001 -> 0001 +0011 -> 0010 +0110 -> 0010 +1010 -> 0010 +0101 -> 0100 +0110 -> 0100 +1100 -> 0100 +1001 -> 1000 +1010 -> 1000 +1100 -> 1000 +0001 -> 0000 +0010 -> 0000 +0100 -> 0000 +1000 -> 0000 } \ No newline at end of file diff --git a/src/solamanDedekind/Explanation.pdf b/src/solamanDedekind/Explanation.pdf new file mode 100644 index 0000000000000000000000000000000000000000..afc2edf9e7855698cb0f269b88a20d427dad8870 GIT binary patch literal 153346 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%0D8ADB4N38d%ZG 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 E03T;kD*ylh literal 0 HcmV?d00001 diff --git a/src/solamanDedekind/README.md b/src/solamanDedekind/README.md index 9376c9a..7a6c8d8 100644 --- a/src/solamanDedekind/README.md +++ b/src/solamanDedekind/README.md @@ -8,5 +8,10 @@ Else, all of these functions are available through Model.DedekindLattice.py . Wh features for System Diagnostic Analysis:
by instantiated an instance of DedekindLattice.py, you can call "getNextNode()" which will return
a new monotone boolean function within the DedekindLattice. From here, you can then call "isConsistent", passing - in a configuration, to see if the function would accept it. + in a configuration, to see if the function would accept it.
+ DO NOT BE CONFUSED BY computeALLMIS and the "isConsistent" function of DedekindNodes!
+ In System-Diagnostic-Analysis, when we ask for a minimal inconsistent subset, we ask for + the smallest set which explains the inconsistency of an input to an answer.
+ When we ask if a set "isConsistent" however, we are asking if the set explains the inConsistency + of the input to an answer (would assuming the set is erroneous make the answer consistent with the input?). From 7d4dce6a99be73d66c636deeac6d1e1fa9c6ab98 Mon Sep 17 00:00:00 2001 From: solaman Date: Sat, 9 May 2015 16:01:00 -0500 Subject: [PATCH 4/4] Error when commiting, attempting to fix. --- .../Algorithms/BottomUp/BottomUp.py | 0 .../Algorithms/BottomUp/__init__.py | 0 .../Hitting_Set_Tree/HittingSetTree.py | 0 .../Hitting_Set_Tree/LogarithmicExtraction.py | 0 .../Algorithms/Hitting_Set_Tree/__init__.py | 0 .../Algorithms/Random/Random.py | 0 .../Algorithms/Random/__init__.py | 0 .../Algorithms/TopDown/TopDown.py | 0 .../Algorithms/TopDown/__init__.py | 0 .../{Dedekind => }/Algorithms/__init__.py | 0 .../{Dedekind => }/Algorithms/analysis.py | 0 src/solamanDedekind/Dedekind/Dedekind.py | 4 + .../Dedekind/Controller/CommandOptions.py | 85 ----- .../Dedekind/Dedekind/Controller/Console.py | 33 -- .../Dedekind/Dedekind/Dedekind.py | 37 --- .../Dedekind/Dedekind/DedekindAnalysis.py | 22 -- .../GeneratedDedekindLattices/preserve.txt | 6 - .../Dedekind/Model/DedekindLattice.py | 203 ------------ .../Dedekind/Dedekind/Model/DedekindNode.py | 290 ------------------ .../Dedekind/Model/DedekindSetMapping.py | 73 ----- .../Dedekind/Dedekind/Model/__init__.py | 0 .../Dedekind/Test/AlgorithmTest/__init__.py | 0 .../Test/ControllerTest/CommandOptionsTest.py | 66 ---- .../Dedekind/Test/ControllerTest/__init__.py | 0 .../Test/ModelTest/DedekindLatticeTest.py | 25 -- .../Test/ModelTest/DedekindNodeTest.py | 124 -------- .../Test/ModelTest/DedekindSetMappingTest.py | 35 --- .../Dedekind/Test/ModelTest/__init__.py | 0 .../Test/ModelTest/n_4.world_57344.dot | 53 ---- .../Test/ModelTest/n_4.world_57344.pdf | Bin 10360 -> 0 bytes .../Test/ModelTest/writeToDotTest.dot | 53 ---- .../Dedekind/Dedekind/Test/__init__.py | 0 .../Dedekind/Dedekind/TransitionError.py | 16 - .../Dedekind/Dedekind/resources/__init__.py | 0 .../Dedekind/Dedekind/resources/setValues.csv | 1 - .../Dedekind/DedekindAnalysis.py | 7 +- .../Dedekind/Model/DedekindLattice.py | 223 ++++++++------ .../Dedekind/Model/DedekindNode.py | 234 ++++++++------ .../{Dedekind => }/Model/DedekindNodeIter.py | 0 .../{Dedekind => }/Model/DedekindNodeLean.py | 0 .../{Dedekind => }/Model/DedekindNodeTools.py | 0 .../Dedekind/Model/DedekindSetMapping.py | 22 +- .../{Dedekind => }/Model/LevelPermutor.py | 0 .../Test/AlgorithmTest/AllTests.py | 0 .../AlgorithmTest}/__init__.py | 0 .../Test/ModelTest/DedekindNodeTest.py | 14 +- .../Test/ModelTest/DedekindSetMappingTest.py | 10 - .../Test/ModelTest/LevelPermutorTest.py | 0 48 files changed, 297 insertions(+), 1339 deletions(-) rename src/solamanDedekind/Dedekind/{Dedekind => }/Algorithms/BottomUp/BottomUp.py (100%) rename src/solamanDedekind/Dedekind/{Dedekind => }/Algorithms/BottomUp/__init__.py (100%) rename src/solamanDedekind/Dedekind/{Dedekind => }/Algorithms/Hitting_Set_Tree/HittingSetTree.py (100%) rename src/solamanDedekind/Dedekind/{Dedekind => }/Algorithms/Hitting_Set_Tree/LogarithmicExtraction.py (100%) rename src/solamanDedekind/Dedekind/{Dedekind => }/Algorithms/Hitting_Set_Tree/__init__.py (100%) rename src/solamanDedekind/Dedekind/{Dedekind => }/Algorithms/Random/Random.py (100%) rename src/solamanDedekind/Dedekind/{Dedekind => }/Algorithms/Random/__init__.py (100%) rename src/solamanDedekind/Dedekind/{Dedekind => }/Algorithms/TopDown/TopDown.py (100%) rename src/solamanDedekind/Dedekind/{Dedekind => }/Algorithms/TopDown/__init__.py (100%) rename src/solamanDedekind/Dedekind/{Dedekind => }/Algorithms/__init__.py (100%) rename src/solamanDedekind/Dedekind/{Dedekind => }/Algorithms/analysis.py (100%) delete mode 100644 src/solamanDedekind/Dedekind/Dedekind/Controller/CommandOptions.py delete mode 100644 src/solamanDedekind/Dedekind/Dedekind/Controller/Console.py delete mode 100644 src/solamanDedekind/Dedekind/Dedekind/Dedekind.py delete mode 100644 src/solamanDedekind/Dedekind/Dedekind/DedekindAnalysis.py delete mode 100644 src/solamanDedekind/Dedekind/Dedekind/GeneratedDedekindLattices/preserve.txt delete mode 100644 src/solamanDedekind/Dedekind/Dedekind/Model/DedekindLattice.py delete mode 100644 src/solamanDedekind/Dedekind/Dedekind/Model/DedekindNode.py delete mode 100644 src/solamanDedekind/Dedekind/Dedekind/Model/DedekindSetMapping.py delete mode 100644 src/solamanDedekind/Dedekind/Dedekind/Model/__init__.py delete mode 100644 src/solamanDedekind/Dedekind/Dedekind/Test/AlgorithmTest/__init__.py delete mode 100644 src/solamanDedekind/Dedekind/Dedekind/Test/ControllerTest/CommandOptionsTest.py delete mode 100644 src/solamanDedekind/Dedekind/Dedekind/Test/ControllerTest/__init__.py delete mode 100644 src/solamanDedekind/Dedekind/Dedekind/Test/ModelTest/DedekindLatticeTest.py delete mode 100644 src/solamanDedekind/Dedekind/Dedekind/Test/ModelTest/DedekindNodeTest.py delete mode 100644 src/solamanDedekind/Dedekind/Dedekind/Test/ModelTest/DedekindSetMappingTest.py delete mode 100644 src/solamanDedekind/Dedekind/Dedekind/Test/ModelTest/__init__.py delete mode 100644 src/solamanDedekind/Dedekind/Dedekind/Test/ModelTest/n_4.world_57344.dot delete mode 100644 src/solamanDedekind/Dedekind/Dedekind/Test/ModelTest/n_4.world_57344.pdf delete mode 100644 src/solamanDedekind/Dedekind/Dedekind/Test/ModelTest/writeToDotTest.dot delete mode 100644 src/solamanDedekind/Dedekind/Dedekind/Test/__init__.py delete mode 100644 src/solamanDedekind/Dedekind/Dedekind/TransitionError.py delete mode 100644 src/solamanDedekind/Dedekind/Dedekind/resources/__init__.py delete mode 100644 src/solamanDedekind/Dedekind/Dedekind/resources/setValues.csv rename src/solamanDedekind/Dedekind/{Dedekind => }/Model/DedekindNodeIter.py (100%) rename src/solamanDedekind/Dedekind/{Dedekind => }/Model/DedekindNodeLean.py (100%) rename src/solamanDedekind/Dedekind/{Dedekind => }/Model/DedekindNodeTools.py (100%) rename src/solamanDedekind/Dedekind/{Dedekind => }/Model/LevelPermutor.py (100%) rename src/solamanDedekind/Dedekind/{Dedekind => }/Test/AlgorithmTest/AllTests.py (100%) rename src/solamanDedekind/Dedekind/{Dedekind/Controller => Test/AlgorithmTest}/__init__.py (100%) rename src/solamanDedekind/Dedekind/{Dedekind => }/Test/ModelTest/LevelPermutorTest.py (100%) diff --git a/src/solamanDedekind/Dedekind/Dedekind/Algorithms/BottomUp/BottomUp.py b/src/solamanDedekind/Dedekind/Algorithms/BottomUp/BottomUp.py similarity index 100% rename from src/solamanDedekind/Dedekind/Dedekind/Algorithms/BottomUp/BottomUp.py rename to src/solamanDedekind/Dedekind/Algorithms/BottomUp/BottomUp.py diff --git a/src/solamanDedekind/Dedekind/Dedekind/Algorithms/BottomUp/__init__.py b/src/solamanDedekind/Dedekind/Algorithms/BottomUp/__init__.py similarity index 100% rename from src/solamanDedekind/Dedekind/Dedekind/Algorithms/BottomUp/__init__.py rename to src/solamanDedekind/Dedekind/Algorithms/BottomUp/__init__.py diff --git a/src/solamanDedekind/Dedekind/Dedekind/Algorithms/Hitting_Set_Tree/HittingSetTree.py b/src/solamanDedekind/Dedekind/Algorithms/Hitting_Set_Tree/HittingSetTree.py similarity index 100% rename from src/solamanDedekind/Dedekind/Dedekind/Algorithms/Hitting_Set_Tree/HittingSetTree.py rename to src/solamanDedekind/Dedekind/Algorithms/Hitting_Set_Tree/HittingSetTree.py diff --git a/src/solamanDedekind/Dedekind/Dedekind/Algorithms/Hitting_Set_Tree/LogarithmicExtraction.py b/src/solamanDedekind/Dedekind/Algorithms/Hitting_Set_Tree/LogarithmicExtraction.py similarity index 100% rename from src/solamanDedekind/Dedekind/Dedekind/Algorithms/Hitting_Set_Tree/LogarithmicExtraction.py rename to src/solamanDedekind/Dedekind/Algorithms/Hitting_Set_Tree/LogarithmicExtraction.py diff --git a/src/solamanDedekind/Dedekind/Dedekind/Algorithms/Hitting_Set_Tree/__init__.py b/src/solamanDedekind/Dedekind/Algorithms/Hitting_Set_Tree/__init__.py similarity index 100% rename from src/solamanDedekind/Dedekind/Dedekind/Algorithms/Hitting_Set_Tree/__init__.py rename to src/solamanDedekind/Dedekind/Algorithms/Hitting_Set_Tree/__init__.py diff --git a/src/solamanDedekind/Dedekind/Dedekind/Algorithms/Random/Random.py b/src/solamanDedekind/Dedekind/Algorithms/Random/Random.py similarity index 100% rename from src/solamanDedekind/Dedekind/Dedekind/Algorithms/Random/Random.py rename to src/solamanDedekind/Dedekind/Algorithms/Random/Random.py diff --git a/src/solamanDedekind/Dedekind/Dedekind/Algorithms/Random/__init__.py b/src/solamanDedekind/Dedekind/Algorithms/Random/__init__.py similarity index 100% rename from src/solamanDedekind/Dedekind/Dedekind/Algorithms/Random/__init__.py rename to src/solamanDedekind/Dedekind/Algorithms/Random/__init__.py diff --git a/src/solamanDedekind/Dedekind/Dedekind/Algorithms/TopDown/TopDown.py b/src/solamanDedekind/Dedekind/Algorithms/TopDown/TopDown.py similarity index 100% rename from src/solamanDedekind/Dedekind/Dedekind/Algorithms/TopDown/TopDown.py rename to src/solamanDedekind/Dedekind/Algorithms/TopDown/TopDown.py diff --git a/src/solamanDedekind/Dedekind/Dedekind/Algorithms/TopDown/__init__.py b/src/solamanDedekind/Dedekind/Algorithms/TopDown/__init__.py similarity index 100% rename from src/solamanDedekind/Dedekind/Dedekind/Algorithms/TopDown/__init__.py rename to src/solamanDedekind/Dedekind/Algorithms/TopDown/__init__.py diff --git a/src/solamanDedekind/Dedekind/Dedekind/Algorithms/__init__.py b/src/solamanDedekind/Dedekind/Algorithms/__init__.py similarity index 100% rename from src/solamanDedekind/Dedekind/Dedekind/Algorithms/__init__.py rename to src/solamanDedekind/Dedekind/Algorithms/__init__.py diff --git a/src/solamanDedekind/Dedekind/Dedekind/Algorithms/analysis.py b/src/solamanDedekind/Dedekind/Algorithms/analysis.py similarity index 100% rename from src/solamanDedekind/Dedekind/Dedekind/Algorithms/analysis.py rename to src/solamanDedekind/Dedekind/Algorithms/analysis.py diff --git a/src/solamanDedekind/Dedekind/Dedekind.py b/src/solamanDedekind/Dedekind/Dedekind.py index 7ab1c04..b42fdbd 100644 --- a/src/solamanDedekind/Dedekind/Dedekind.py +++ b/src/solamanDedekind/Dedekind/Dedekind.py @@ -8,6 +8,7 @@ from Model.DedekindLattice import getDedekindNumber, generateDotFiles import sys +from Algorithms.analysis import runAnalysis def runConsole(): global standardCommands @@ -23,7 +24,10 @@ def runConsole(): standardCommands.addCommand("dotFiles", "Generates dot files of all monotone boolean functions"\ + " for a given input size." + "\n\tInput:" + " function input size", generateDotFiles) +standardCommands.addCommand("analyze", "Runs a System-Diagnostic algorithm"\ + + " and perform analysis on varying MBF's" +"\n\tInput: algorithm name, input size", runAnalysis ) standardCommands.addCommand("console", "Run the program as a console.", runConsole) + def main(): userInput = sys.argv[1:] diff --git a/src/solamanDedekind/Dedekind/Dedekind/Controller/CommandOptions.py b/src/solamanDedekind/Dedekind/Dedekind/Controller/CommandOptions.py deleted file mode 100644 index 2ac4f94..0000000 --- a/src/solamanDedekind/Dedekind/Dedekind/Controller/CommandOptions.py +++ /dev/null @@ -1,85 +0,0 @@ -''' -Created on Mar 31, 2015 - -@author: Solaman - -Used to store relevant information about command options. -''' -import sys - -class CommandOption(object): - ''' - class for a particular console option - ''' - - - def __init__(self, helpString, commandFunction): - ''' - Constructor - ''' - #Should describe what the option does - self.helpString = helpString - - #Function used to execute this option - self.commandFunction = commandFunction - - -class CommandOptions(object): - ''' - Used to store and order console options - ''' - - def __init__(self): - #If the user wants to request a command by its command string, they can do so with this - self._commands = {} - - self._commands["help"] = CommandOption("displays all available commands"\ - + " along with a description for each", self.printCommandOptions) - - def addCommand(self, commandString, helpString, commandFunction): - ''' - Adds a command option. This option then can be requested by calling - 'selectOption'. - ''' - if commandString == "help": - exceptMessage = "attempted to overwrite \"help\" option in ConsoleOptions. Don't do that plz." - raise Exception(exceptMessage) - - if not callable(commandFunction): - exceptMessage = "Command function must be a function! (must be callable)" - raise Exception(exceptMessage) - - self._commands[commandString] = CommandOption(helpString, commandFunction) - - def printCommandOptions(self): - ''' - Prints all commands available along with the help string associated with each - respective command. - ''' - print "-----" - print "Available Commands" - print "-----" - for command in self._commands.iterkeys(): - print command, ": ", self._commands[command].helpString - print "-----" - - - def selectCommand(self, userInput): - ''' - Calls the function associated with a particular command. - ''' - if len(userInput) == 0: - sys.stderr.write( "Must enter a command, type \"help\" to list all commands\n" ) - return - - command = userInput[0] - inputParams = userInput[1:] - - if command not in self._commands: - sys.stderr.write("Command not recognized, type \"help\" to list all commands\n") - return - - if len(inputParams) == 0: - self._commands[command].commandFunction() - else: - self._commands[command].commandFunction(inputParams) \ No newline at end of file diff --git a/src/solamanDedekind/Dedekind/Dedekind/Controller/Console.py b/src/solamanDedekind/Dedekind/Dedekind/Controller/Console.py deleted file mode 100644 index 8acf541..0000000 --- a/src/solamanDedekind/Dedekind/Dedekind/Controller/Console.py +++ /dev/null @@ -1,33 +0,0 @@ -''' -Created on Mar 31, 2015 - -@author: Solaman -This is the main interface with the application if the user chooses to use -it as a console application. -''' -import os - -class Console(object): - - def __init__(self, commandOptions): - self.commandOptions = commandOptions - - self.commandOptions.addCommand("exit", "exits the console.", self.endConsole) - - self.shouldContinue = True - - - def selectCommand(self, userInput): - self.commandOptions.selectCommand(userInput) - - def endConsole(self): - print "Bye!" - self.shouldContinue = False - -def run(commandOptions): - console = Console(commandOptions) - print "hello! To list commands, type \"help\"" - while console.shouldContinue: - userInput = raw_input("enter command: ") - userInput = userInput.split(" ") - console.selectCommand(userInput) \ No newline at end of file diff --git a/src/solamanDedekind/Dedekind/Dedekind/Dedekind.py b/src/solamanDedekind/Dedekind/Dedekind/Dedekind.py deleted file mode 100644 index b42fdbd..0000000 --- a/src/solamanDedekind/Dedekind/Dedekind/Dedekind.py +++ /dev/null @@ -1,37 +0,0 @@ -''' -Created on Apr 1, 2015 - -@author: Solaman -''' -from Controller.CommandOptions import CommandOptions -from Controller import Console -from Model.DedekindLattice import getDedekindNumber, generateDotFiles - -import sys -from Algorithms.analysis import runAnalysis - -def runConsole(): - global standardCommands - - del standardCommands._commands["console"] - Console.run(standardCommands) - -standardCommands = CommandOptions() - -standardCommands.addCommand("getNumber", "Finds Dedekind Number for a given input size."\ - + "\n\tInput:" + " function input size", getDedekindNumber) - -standardCommands.addCommand("dotFiles", "Generates dot files of all monotone boolean functions"\ - + " for a given input size." + "\n\tInput:" + " function input size", generateDotFiles) - -standardCommands.addCommand("analyze", "Runs a System-Diagnostic algorithm"\ - + " and perform analysis on varying MBF's" +"\n\tInput: algorithm name, input size", runAnalysis ) -standardCommands.addCommand("console", "Run the program as a console.", runConsole) - - -def main(): - userInput = sys.argv[1:] - standardCommands.selectCommand(userInput) - -if __name__ == '__main__': - main() \ No newline at end of file diff --git a/src/solamanDedekind/Dedekind/Dedekind/DedekindAnalysis.py b/src/solamanDedekind/Dedekind/Dedekind/DedekindAnalysis.py deleted file mode 100644 index 3baa304..0000000 --- a/src/solamanDedekind/Dedekind/Dedekind/DedekindAnalysis.py +++ /dev/null @@ -1,22 +0,0 @@ -''' -Created on Mar 13, 2015 - -@author: Solaman -''' -import cProfile -from Model.DedekindNode import DedekindNode -from Model.DedekindLattice import DedekindLattice - -node = DedekindNode(4, [15]) -lattice = DedekindLattice(5, lean = True) - -def analyzeDedekindNode(): - cProfile.run("node.generatePossibleConfigurations()") - -def analyzeFindUniqueFunctions(): - cProfile.run("lattice.getDedekindNumber()") - -if __name__ == '__main__': - #analyzeDedekindNode() - #analyzeDedekindLattice() - analyzeFindUniqueFunctions() \ No newline at end of file diff --git a/src/solamanDedekind/Dedekind/Dedekind/GeneratedDedekindLattices/preserve.txt b/src/solamanDedekind/Dedekind/Dedekind/GeneratedDedekindLattices/preserve.txt deleted file mode 100644 index 13497a6..0000000 --- a/src/solamanDedekind/Dedekind/Dedekind/GeneratedDedekindLattices/preserve.txt +++ /dev/null @@ -1,6 +0,0 @@ -March 3rd, 2015: GeneratedDedekindLattices will not be included in a git commit -unless a file is included. This is that file. - -Note: We could have the application generate this folder as needed, but I believe -that because the folder's existence isn't tied to a run of the application -("dedekind world 5" would be a folder that is tied) the user might expect the folder to already exist. \ No newline at end of file diff --git a/src/solamanDedekind/Dedekind/Dedekind/Model/DedekindLattice.py b/src/solamanDedekind/Dedekind/Dedekind/Model/DedekindLattice.py deleted file mode 100644 index 3d53f7c..0000000 --- a/src/solamanDedekind/Dedekind/Dedekind/Model/DedekindLattice.py +++ /dev/null @@ -1,203 +0,0 @@ -''' -Created on Feb 26, 2015 - -@author: Solaman -''' -from DedekindNode import DedekindNode -from Model.LevelPermutor import LevelPermutor -from DedekindNode import getIndex - -class LatticeFiller(object): - ''' - Constructed when the Lattice is constructed if the user wants to fill in the - lattice with each Monotone Boolean Function individually. - This class provides a level of abstraction from the inner workings of the module. - ''' - def __init__(self, lattice): - self.lattice = lattice - self.nodeList = [] - self.nodeList.append(lattice.emptyFunction) - self.nodeList.append(lattice.baseFunction) - self.wasFilled = False - - def getNextNode(self): - ''' - Returns the most recently added node to the queue - if it is not empty. - ''' - if self.nodeList == []: - return None - node = self.nodeList.pop() - children = node.generateChildren() - for child in children: - self.nodeList.append(child) - self.lattice.lattice[getIndex(child)] = child - return node - - def fillLattice(self): - while self.getNextNode() != None: - continue - - def getDedekindNumber(self): - if self.wasFilled == False: - self.fillLattice() - self.wasFilled = True - return len(self.lattice.lattice.values()) - -class LatticeFillerUnique(object): - ''' - Constructed when the Lattice is constructed if the user would like to fill in - the lattice with Monotone Boolean Functions that are equivalent by level. - This class provides a level of abstraction from the inner workings of the module. - ''' - def __init__(self, lattice, lean= False): - from Model.DedekindNodeLean import DedekindNodeLean - if lean == True: - lattice.baseFunction = DedekindNodeLean(lattice.baseFunction.inputSize,\ - lattice.baseFunction.acceptedConfigurations[-1]) - - self.lattice = lattice - self.nodeList = [] - lattice.emptyFunction.isVisited = False - lattice.emptyFunction.parent = None - self.nodeList.append(lattice.emptyFunction) - lattice.baseFunction.isVisited = False - lattice.baseFunction.parent = None - self.nodeList.append(lattice.baseFunction) - self.levelPermutor = LevelPermutor(lattice.inputSize) - self.mileMarker = 10000 - - self.wasFilled = False - - def getNextNode(self): - #We don't need to compute functions that are isomorphisms of each other. - #We store each function by there level, and then counts by the number of possible children for the function - #It is proven that functions by level that have the same number of possible children are isomorphisms - #{"level" : {"isomorphismCount": count, "children" : children } } - if self.nodeList == []: - return None - node = self.nodeList.pop() - if node.isVisited == True: - if node.parent == None: - return node - else: - node.parent.childrenCount += node.childrenCount - if node.parent.childrenCount >= self.mileMarker: - print "marker: ", self.mileMarker - self.mileMarker = node.parent.childrenCount * 2 - return self.getNextNode() - - if self.getKey(node) in self.levelPermutor: - node.parent.childrenCount += self.levelPermutor[self.getKey(node)].childrenCount - return self.getNextNode() - else: - self.lattice.lattice[ getIndex(node.getAcceptedConfigurationsAsList()) ] = node - node.childrenCount = 1 - self.levelPermutor[node.getLastLevel()] = node - children = node.generateChildren() - node.isVisited = True - self.nodeList.append(node) - for child in children: - child.parent = node - child.isVisited = False - self.nodeList.append(child) - return node - - def getKey(self, node): - if hasattr(node, "key"): - return node.key - else: - node.key = getIndex(node.getLastLevel()) - return node.key - - def fillLattice(self): - while self.getNextNode() != None: - continue - - def getDedekindNumber(self): - if self.wasFilled == False: - self.fillLattice() - self.wasFilled = True - return self.lattice.baseFunction.childrenCount + 1 - -class DedekindLattice(object): - ''' - We aim to generate the Dedekind Lattices using this class. Namely, - We want to generate all monotone boolean functions given an n input size. - Currently, we will aim to only generate the lattices in working memory. - Future implementations will hopefully be able to dynamically - Generate a node of a given Lattice. - ''' - - def __init__(self, inputSize, generateUnique = True, lean = False): - ''' - Constructor. For now, we will store each monotone boolean function - as an object. Future implementations will store them as a single bit - for lean memory usage - ''' - if inputSize < 0: - raise Exception("Input size must be greater than or equal to 0") - self.lattice = {} - - #bit mask refers to the possible bit values - #of a given configuration. E.G. boolean functions with 4 inputs - #Will have a bit mask of 0xF - self.bitMask = 2**(inputSize) - 1 - self.inputSize = inputSize - - self.emptyFunction = DedekindNode(self.inputSize, []) - self.lattice[ getIndex(self.emptyFunction)] = self.emptyFunction - - self.baseFunction = DedekindNode(self.inputSize, [self.bitMask]) - self.lattice[ getIndex(self.baseFunction)] = self.baseFunction - - if generateUnique: - self.latticeFiller = LatticeFillerUnique(self, lean) - - else: - self.latticeFiller = LatticeFiller(self) - - def getDedekindNumber(self): - return self.latticeFiller.getDedekindNumber() - - def getNextNode(self): - return self.latticeFiller.getNextNode() - - - def generateDotFiles(self): - ''' - - ''' - import os - directoryName = os.path.join("GeneratedDedekindLattices", str(self.inputSize) + "_DedekindLattice") - if not os.path.exists(directoryName): - os.mkdir(directoryName) - updateTime = self.monotoneCount/10 - generatedFiles = 0 - for function in self.lattice.itervalues(): - function.writeToDotFile(directoryName) - generatedFiles += 1 - if generatedFiles % updateTime == 0: - print generatedFiles, " written so far" - print "Done" - - -def getDedekindNumber(userInput): - ''' - Constructs the Dedekind Lattice for the given input size and - returns the dedekind number associated with that input. - Values that return within a minute are currently n <= 5. - ''' - inputSize = int(userInput[0]) - dedekindLattice = DedekindLattice(inputSize, lean =True) - print dedekindLattice.getDedekindNumber() - -def generateDotFiles(userInput): - ''' - Constructs the Dedekind Lattice for the given input size and - generates the dot files for each monotone boolean function with the given input size. - ''' - inputSize = int(userInput[0]) - dedekindLattice = DedekindLattice(inputSize) - dedekindLattice.fillLattice() - dedekindLattice.generateDotFiles() \ No newline at end of file diff --git a/src/solamanDedekind/Dedekind/Dedekind/Model/DedekindNode.py b/src/solamanDedekind/Dedekind/Dedekind/Model/DedekindNode.py deleted file mode 100644 index 261734d..0000000 --- a/src/solamanDedekind/Dedekind/Dedekind/Model/DedekindNode.py +++ /dev/null @@ -1,290 +0,0 @@ -''' -Created on Mar 3, 2015 - -@author: Solaman -''' -from itertools import combinations as genCombinations -import os -from collections import Iterable -from DedekindNodeIter import DedekindNodeIter -from DedekindSetMapping import getConfAsInt - -#Used for Dot file generation -#Once a node is written to a dot file -#A full Node is created to help -fullNodes = {} - -#Used for Dot file generation -#Once a node is written to a dot file -#labels for each configuration are made -configurationLabelss = {} - -#Used for Dot file Generation -#Once a node is written to a dot file -#edges between configurations are written as dot edges. -dotEdgess = {} - -#To avoid cost of calculating configuration levels continuously, -#We will calculate the levels from the very beginning. -configurationLevelss = {} - -class DedekindNode(Iterable): - ''' - A boolean function. It is up to the user to ensure that it is monotone. - ''' - - - def __init__(self, inputSize, acceptedConfigurations, nodeToCopy = None): - ''' - Each function has a set of accepted configurations. To aid in the book keeping of this algorithm, - each accepted configuration is stored by "level", namely, how many of the given inputs must be "off" - in the configuration. so if the configuration 0b0001 is accepted and the bitmask is 0b1111, - then the configuration would be stored at the 3rd level. - For rudimentary checking, the accepted configurations are added by descending level. - ''' - self.childrenSize = 0 - self.inputSize = inputSize - self.bitMask = self.bitMask = 2**(inputSize) - 1 - - - if nodeToCopy != None: - temp = nodeToCopy.getAcceptedConfigurationsAsList() - temp.extend(acceptedConfigurations) - acceptedConfigurations = temp - self.acceptedConfigurations = [] - - for acceptedConfiguration in acceptedConfigurations: - level = getConfigurationLevel(self.inputSize, acceptedConfiguration) - if level > len( self.acceptedConfigurations): - exceptionMessage = "Cannot add configurations beyond the highest level + 1" \ - + "\n attempted level: " + str(level) \ - +"\n level limit: " + str(len(self.acceptedConfigurations)) - raise Exception( exceptionMessage) - - if level == len(self.acceptedConfigurations): - self.acceptedConfigurations.append( [acceptedConfiguration]) - else: - self.acceptedConfigurations[level].append( acceptedConfiguration) - - self.index = -1 - - - def getAcceptedConfigurationsAsList(self): - acceptedConfigurations = [] - for level in self.acceptedConfigurations: - acceptedConfigurations.extend(level) - - return acceptedConfigurations - - def isConsistent(self, configuration): - ''' - Checks if a configuration would be accepted or not. - ''' - return isConsistent(self, configuration) - - def _generatePossibleConfigurations(self): - ''' - Generates possible configurations to add to the function such that the new functions would be monotone - (given that this function is monotone). We observe that this can be done by level combinations. - E.G. if the input size is 4, and the last level of the function is 1->[0b1011, 0b0111], then the children - of the node in the lattice can have the level 2 configuration [0b0011]. - (if any other, then it would not be monotone). - ''' - possibleConfigurations = [] - newMaxLevel = len(self.acceptedConfigurations) - - #current max configuration level is [self.bitMask] - if newMaxLevel == 1: - possibleConfigurations = getLevelOneConfigurations(self.inputSize) - - #Entire Dedekind Node Lattice is filled, can add [0] as an accepted configuration - elif newMaxLevel == self.inputSize \ - and len(self.acceptedConfigurations[-1]) == self.inputSize: - possibleConfigurations = [0] - #current max configuration level is [] (none are accepted) - elif newMaxLevel == 0: - return [] - elif newMaxLevel< self.inputSize: - combinations = genCombinations(self.acceptedConfigurations[-1], newMaxLevel) - possibleConfigurations = [] - for combination in combinations: - possibleConfiguration = self.bitMask - for configuration in combination: - possibleConfiguration &= configuration - if getConfigurationLevel(self.inputSize, possibleConfiguration) == newMaxLevel: - possibleConfigurations.append(possibleConfiguration) - return possibleConfigurations - - return possibleConfigurations - - - def generateChildren(self): - ''' - Generates all Dedekind Nodes that would be considered the children of this - DedekindNode - ''' - children = [] - possibleConfigurations = self._generatePossibleConfigurations() - - for numberOfConfigurations in range(1, len(possibleConfigurations) + 1): - combinations = genCombinations(possibleConfigurations, numberOfConfigurations) - for combination in combinations: - children.append( DedekindNode(self.inputSize, combination, self)) - - return children - - def __iter__(self): - ''' - Implemented this with good design in mind, however - If you want something fast, it is better to use - getAcceptedConfigurationsAsList - ''' - return DedekindNodeIter(self) - - def getLastLevel(self): - if len(self.acceptedConfigurations) > 0: - return self.acceptedConfigurations[-1] - else: - return [] - - def writeToDotFile(self, writeLocation): - global fullNodes, configurationLabelss, dotEdgess - - dotFileName = os.path.join(writeLocation, "n_" + str(self.inputSize)\ - + "." + "world_" + str(getIndex(self.getAcceptedConfigurationsAsList()))\ - + ".dot") - dotFile = open( dotFileName, "w") - dotFile.write("""digraph{ - rankdir=BT - node[shape=circle, style=filled, label=""] - edge[dir=none]\n""") - - initDotVariables(self.inputSize) - fullNode = fullNodes[self.inputSize] - configurationLabels = configurationLabelss[self.inputSize] - dotEdges = dotEdgess[self.inputSize] - - #configurationList = self.getAcceptedConfigurationsAsList() - for configuration in fullNode: - if configuration in self: - dotFile.write( configurationLabels[configuration] +" [ color = green, "\ - + "label = \""+ configurationLabels[configuration] + "\"]\n") - else: - dotFile.write( configurationLabels[configuration] +" [ color = red, "\ - + "label = \""+ configurationLabels[configuration] + "\"]\n") - - dotFile.write(dotEdges) - dotFile.write("}") - - dotFile.close() - -def isConsistent(node, configuration): - ''' - Checks if a configuration would be deemed "inconsistent". - This is confusing! The DedekindNode represents a faulty system, and the - "accepted configurations" represent sets such that, if you deemed the given - components (represented by bits) as "faulty" and all others as safe, you would explain - erroneous output. - ''' - from sets import ImmutableSet - if isinstance(configuration, ImmutableSet): - configuration = getConfAsInt(configuration, node.inputSize) - if (getIndex(node.getAcceptedConfigurationsAsList()) & 1 << configuration ) == 0: - return False - else: - return True - -def getFullNode(inputSize): - global fullNodes - if inputSize not in fullNodes: - bitMask = (1< " \ - + configurationLabels[configuration] + "\n" - dotEdges += edgeString - - fullNodes[inputSize] = fullNode - configurationLabelss[inputSize] = configurationLabels - dotEdgess[inputSize] = dotEdges - -def getIndex(configurationList): - ''' - If we treat each configuration as its own integer value, we can combine each value into an integer - of size 2**inputSize bits. E.G. if the input size is 4, then each configuration has a value between 0-15. - So an integer of 16 bits, where each bit is for each configuration, will represent the function - and its accepted configurations. Since this value is unique, we can also use it as an index for the function - ''' - index = 0 - for configuration in configurationList: - index |= (1 << configuration) - - return index - -def getConfigurationLevel(inputSize, configuration): - global configurationLevelss - if inputSize not in configurationLevelss: - bitMask = (1< 0111 -1111 -> 1011 -1111 -> 1101 -1111 -> 1110 -0111 -> 0011 -1011 -> 0011 -0111 -> 0101 -1101 -> 0101 -0111 -> 0110 -1110 -> 0110 -1011 -> 1001 -1101 -> 1001 -1011 -> 1010 -1110 -> 1010 -1101 -> 1100 -1110 -> 1100 -0011 -> 0001 -0101 -> 0001 -1001 -> 0001 -0011 -> 0010 -0110 -> 0010 -1010 -> 0010 -0101 -> 0100 -0110 -> 0100 -1100 -> 0100 -1001 -> 1000 -1010 -> 1000 -1100 -> 1000 -0001 -> 0000 -0010 -> 0000 -0100 -> 0000 -1000 -> 0000 -} \ No newline at end of file diff --git a/src/solamanDedekind/Dedekind/Dedekind/Test/ModelTest/n_4.world_57344.pdf b/src/solamanDedekind/Dedekind/Dedekind/Test/ModelTest/n_4.world_57344.pdf deleted file mode 100644 index 2b580d694a8ca210e4a7295da1ce314160c34e55..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 10360 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 V_Kv?I&dtpUJ7`EHt{{Q>{{Um}a~c2u diff --git a/src/solamanDedekind/Dedekind/Dedekind/Test/ModelTest/writeToDotTest.dot b/src/solamanDedekind/Dedekind/Dedekind/Test/ModelTest/writeToDotTest.dot deleted file mode 100644 index b828616..0000000 --- a/src/solamanDedekind/Dedekind/Dedekind/Test/ModelTest/writeToDotTest.dot +++ /dev/null @@ -1,53 +0,0 @@ -digraph{ - rankdir=BT - node[shape=circle, style=filled, label=""] - edge[dir=none] -1111 [ color = green, label = "1111"] -0111 [ color = red, label = "0111"] -1011 [ color = red, label = "1011"] -1101 [ color = green, label = "1101"] -1110 [ color = green, label = "1110"] -0011 [ color = red, label = "0011"] -0101 [ color = red, label = "0101"] -0110 [ color = red, label = "0110"] -1001 [ color = red, label = "1001"] -1010 [ color = red, label = "1010"] -1100 [ color = red, label = "1100"] -0001 [ color = red, label = "0001"] -0010 [ color = red, label = "0010"] -0100 [ color = red, label = "0100"] -1000 [ color = red, label = "1000"] -0000 [ color = red, label = "0000"] -1111 -> 0111 -1111 -> 1011 -1111 -> 1101 -1111 -> 1110 -0111 -> 0011 -1011 -> 0011 -0111 -> 0101 -1101 -> 0101 -0111 -> 0110 -1110 -> 0110 -1011 -> 1001 -1101 -> 1001 -1011 -> 1010 -1110 -> 1010 -1101 -> 1100 -1110 -> 1100 -0011 -> 0001 -0101 -> 0001 -1001 -> 0001 -0011 -> 0010 -0110 -> 0010 -1010 -> 0010 -0101 -> 0100 -0110 -> 0100 -1100 -> 0100 -1001 -> 1000 -1010 -> 1000 -1100 -> 1000 -0001 -> 0000 -0010 -> 0000 -0100 -> 0000 -1000 -> 0000 -} \ No newline at end of file diff --git a/src/solamanDedekind/Dedekind/Dedekind/Test/__init__.py b/src/solamanDedekind/Dedekind/Dedekind/Test/__init__.py deleted file mode 100644 index e69de29..0000000 diff --git a/src/solamanDedekind/Dedekind/Dedekind/TransitionError.py b/src/solamanDedekind/Dedekind/Dedekind/TransitionError.py deleted file mode 100644 index 75be7a4..0000000 --- a/src/solamanDedekind/Dedekind/Dedekind/TransitionError.py +++ /dev/null @@ -1,16 +0,0 @@ -''' -Created on Mar 2, 2015 - -@author: Solaman -''' -class TransitionError(Exception): - ''' - Raised when an operation attempts a state transition that is - not allowed. - ''' - - def __init__(self, msg): - self.msg = msg - - def __str__(self): - return repr(self.value) \ No newline at end of file diff --git a/src/solamanDedekind/Dedekind/Dedekind/resources/__init__.py b/src/solamanDedekind/Dedekind/Dedekind/resources/__init__.py deleted file mode 100644 index e69de29..0000000 diff --git a/src/solamanDedekind/Dedekind/Dedekind/resources/setValues.csv b/src/solamanDedekind/Dedekind/Dedekind/resources/setValues.csv deleted file mode 100644 index 1b91aad..0000000 --- a/src/solamanDedekind/Dedekind/Dedekind/resources/setValues.csv +++ /dev/null @@ -1 +0,0 @@ -A,B,C,D,E,F,G,H,I,J,K,L,M,N,O,P,Q,R,S,T,U,V,W,X,Y,Z \ No newline at end of file diff --git a/src/solamanDedekind/Dedekind/DedekindAnalysis.py b/src/solamanDedekind/Dedekind/DedekindAnalysis.py index 19de75c..3baa304 100644 --- a/src/solamanDedekind/Dedekind/DedekindAnalysis.py +++ b/src/solamanDedekind/Dedekind/DedekindAnalysis.py @@ -8,16 +8,13 @@ from Model.DedekindLattice import DedekindLattice node = DedekindNode(4, [15]) -lattice = DedekindLattice(5) +lattice = DedekindLattice(5, lean = True) def analyzeDedekindNode(): cProfile.run("node.generatePossibleConfigurations()") - -def analyzeDedekindLattice(): - cProfile.run("lattice.fillLattice()") def analyzeFindUniqueFunctions(): - cProfile.run("lattice.findUniqueFunctions()") + cProfile.run("lattice.getDedekindNumber()") if __name__ == '__main__': #analyzeDedekindNode() diff --git a/src/solamanDedekind/Dedekind/Model/DedekindLattice.py b/src/solamanDedekind/Dedekind/Model/DedekindLattice.py index 851a71a..3d53f7c 100644 --- a/src/solamanDedekind/Dedekind/Model/DedekindLattice.py +++ b/src/solamanDedekind/Dedekind/Model/DedekindLattice.py @@ -4,7 +4,122 @@ @author: Solaman ''' from DedekindNode import DedekindNode +from Model.LevelPermutor import LevelPermutor +from DedekindNode import getIndex +class LatticeFiller(object): + ''' + Constructed when the Lattice is constructed if the user wants to fill in the + lattice with each Monotone Boolean Function individually. + This class provides a level of abstraction from the inner workings of the module. + ''' + def __init__(self, lattice): + self.lattice = lattice + self.nodeList = [] + self.nodeList.append(lattice.emptyFunction) + self.nodeList.append(lattice.baseFunction) + self.wasFilled = False + + def getNextNode(self): + ''' + Returns the most recently added node to the queue + if it is not empty. + ''' + if self.nodeList == []: + return None + node = self.nodeList.pop() + children = node.generateChildren() + for child in children: + self.nodeList.append(child) + self.lattice.lattice[getIndex(child)] = child + return node + + def fillLattice(self): + while self.getNextNode() != None: + continue + + def getDedekindNumber(self): + if self.wasFilled == False: + self.fillLattice() + self.wasFilled = True + return len(self.lattice.lattice.values()) + +class LatticeFillerUnique(object): + ''' + Constructed when the Lattice is constructed if the user would like to fill in + the lattice with Monotone Boolean Functions that are equivalent by level. + This class provides a level of abstraction from the inner workings of the module. + ''' + def __init__(self, lattice, lean= False): + from Model.DedekindNodeLean import DedekindNodeLean + if lean == True: + lattice.baseFunction = DedekindNodeLean(lattice.baseFunction.inputSize,\ + lattice.baseFunction.acceptedConfigurations[-1]) + + self.lattice = lattice + self.nodeList = [] + lattice.emptyFunction.isVisited = False + lattice.emptyFunction.parent = None + self.nodeList.append(lattice.emptyFunction) + lattice.baseFunction.isVisited = False + lattice.baseFunction.parent = None + self.nodeList.append(lattice.baseFunction) + self.levelPermutor = LevelPermutor(lattice.inputSize) + self.mileMarker = 10000 + + self.wasFilled = False + + def getNextNode(self): + #We don't need to compute functions that are isomorphisms of each other. + #We store each function by there level, and then counts by the number of possible children for the function + #It is proven that functions by level that have the same number of possible children are isomorphisms + #{"level" : {"isomorphismCount": count, "children" : children } } + if self.nodeList == []: + return None + node = self.nodeList.pop() + if node.isVisited == True: + if node.parent == None: + return node + else: + node.parent.childrenCount += node.childrenCount + if node.parent.childrenCount >= self.mileMarker: + print "marker: ", self.mileMarker + self.mileMarker = node.parent.childrenCount * 2 + return self.getNextNode() + + if self.getKey(node) in self.levelPermutor: + node.parent.childrenCount += self.levelPermutor[self.getKey(node)].childrenCount + return self.getNextNode() + else: + self.lattice.lattice[ getIndex(node.getAcceptedConfigurationsAsList()) ] = node + node.childrenCount = 1 + self.levelPermutor[node.getLastLevel()] = node + children = node.generateChildren() + node.isVisited = True + self.nodeList.append(node) + for child in children: + child.parent = node + child.isVisited = False + self.nodeList.append(child) + return node + + def getKey(self, node): + if hasattr(node, "key"): + return node.key + else: + node.key = getIndex(node.getLastLevel()) + return node.key + + def fillLattice(self): + while self.getNextNode() != None: + continue + + def getDedekindNumber(self): + if self.wasFilled == False: + self.fillLattice() + self.wasFilled = True + return self.lattice.baseFunction.childrenCount + 1 + class DedekindLattice(object): ''' We aim to generate the Dedekind Lattices using this class. Namely, @@ -14,26 +129,15 @@ class DedekindLattice(object): Generate a node of a given Lattice. ''' - - def __init__(self, inputSize, inputSet = None ): + def __init__(self, inputSize, generateUnique = True, lean = False): ''' Constructor. For now, we will store each monotone boolean function as an object. Future implementations will store them as a single bit - for lean memory usage. - @param inputSize- Size of input for the monotone boolean functions (MBF). - @param inputSet- Set to use for each monotone boolean function. If - the caller wishes to use a python set for interacting with the MBF's can - provide one here. Defaults to values found in "resources/setValues.csv" + for lean memory usage ''' - if inputSize < 0: raise Exception("Input size must be greater than or equal to 0") - - self.setMapping = {} - if inputSet == None: - setValues = open("resources/setValues.csv").readAll() - setValues = setValues.split(",") - + self.lattice = {} #bit mask refers to the possible bit values #of a given configuration. E.G. boolean functions with 4 inputs @@ -41,88 +145,25 @@ def __init__(self, inputSize, inputSet = None ): self.bitMask = 2**(inputSize) - 1 self.inputSize = inputSize - self.lattice = {} - self.emptyFunction = DedekindNode(self.inputSize, []) - self.lattice[ self.emptyFunction.getIndex()] = self.emptyFunction + self.lattice[ getIndex(self.emptyFunction)] = self.emptyFunction self.baseFunction = DedekindNode(self.inputSize, [self.bitMask]) - self.lattice[ self.baseFunction.getIndex()] = self.baseFunction + self.lattice[ getIndex(self.baseFunction)] = self.baseFunction - def getNextNode(self): - ''' - Returns the most recently added node to the queue - if it is not empty. - ''' - if self.nodeList == []: - return None - node = self.nodeList.pop() - children = node.generateChildren() - for child in children: - self.nodeList.append(child) - self.lattice[child.getIndex()] = child - return node - - - def fillLattice(self): - self.nodeList = [] - self.nodeList.append(self.baseFunction) - while self.getNextNode() != None: - x = 1 - self.monotoneCount= len( self.lattice.values()) + if generateUnique: + self.latticeFiller = LatticeFillerUnique(self, lean) - def findUniqueFunctions(self): - #We don't need to compute functions that are isomorphisms of each other. - #We store each function by there level, and then counts by the number of possible children for the function - #It is proven that functions by level that have the same number of possible children are isomorphisms - #{"level" : {"isomorphismCount": count, "children" : children } } - self.nodeList = [] - functionCount = 1 - self.childrenCounts = {} - - self.baseFunction.isVisited = False - self.baseFunction.parent = None - self.nodeList.append(self.baseFunction) - - while self.nodeList != []: - node = self.nodeList.pop() - if node.isVisited == True: - if node.parent == None: - functionCount += node.childrenCount - return functionCount - continue - else: - node.parent.childrenCount += node.childrenCount - key = self.getKey(node) - self.childrenCounts[ node.level][key] = node.childrenCount - continue + else: + self.latticeFiller = LatticeFiller(self) - node.level = len(node.acceptedConfigurations) - 1 - if node.level not in self.childrenCounts: - self.childrenCounts [ node.level] = {} - - node.possibleConfigurations = node._generatePossibleConfigurations() - node.possibleConfigurationSize = len(node.possibleConfigurations) - key = self.getKey(node) - if key in self.childrenCounts[node.level]: - node.parent.childrenCount += self.childrenCounts[node.level][ key] - else: - children = node.generateChildren() - node.childrenCount = 1 - node.isVisited = True - self.nodeList.append(node) - for child in children: - child.parent = node - child.isVisited = False - self.nodeList.append(child) - - def getKey(self, node): - return str(len(node.acceptedConfigurations[node.level])) + "-" + str(node.possibleConfigurationSize) + def getDedekindNumber(self): + return self.latticeFiller.getDedekindNumber() + + def getNextNode(self): + return self.latticeFiller.getNextNode() - - - - + def generateDotFiles(self): ''' @@ -148,8 +189,8 @@ def getDedekindNumber(userInput): Values that return within a minute are currently n <= 5. ''' inputSize = int(userInput[0]) - dedekindLattice = DedekindLattice(inputSize) - print dedekindLattice.findUniqueFunctions() + dedekindLattice = DedekindLattice(inputSize, lean =True) + print dedekindLattice.getDedekindNumber() def generateDotFiles(userInput): ''' diff --git a/src/solamanDedekind/Dedekind/Model/DedekindNode.py b/src/solamanDedekind/Dedekind/Model/DedekindNode.py index f1e5e29..261734d 100644 --- a/src/solamanDedekind/Dedekind/Model/DedekindNode.py +++ b/src/solamanDedekind/Dedekind/Model/DedekindNode.py @@ -4,29 +4,31 @@ @author: Solaman ''' from itertools import combinations as genCombinations -from subprocess import call import os +from collections import Iterable +from DedekindNodeIter import DedekindNodeIter +from DedekindSetMapping import getConfAsInt #Used for Dot file generation - #Once a node is written to a dot file - #A full Node is created to help +#Once a node is written to a dot file +#A full Node is created to help fullNodes = {} - #Used for Dot file generation - #Once a node is written to a dot file - #labels for each configuration are made +#Used for Dot file generation +#Once a node is written to a dot file +#labels for each configuration are made configurationLabelss = {} - #Used for Dot file Generation - #Once a node is written to a dot file - #edges between configurations are written as dot edges. +#Used for Dot file Generation +#Once a node is written to a dot file +#edges between configurations are written as dot edges. dotEdgess = {} #To avoid cost of calculating configuration levels continuously, #We will calculate the levels from the very beginning. configurationLevelss = {} -class DedekindNode(object): +class DedekindNode(Iterable): ''' A boolean function. It is up to the user to ensure that it is monotone. ''' @@ -44,14 +46,15 @@ def __init__(self, inputSize, acceptedConfigurations, nodeToCopy = None): self.inputSize = inputSize self.bitMask = self.bitMask = 2**(inputSize) - 1 + if nodeToCopy != None: - temp = nodeToCopy.acceptedConfigurationsAsList() + temp = nodeToCopy.getAcceptedConfigurationsAsList() temp.extend(acceptedConfigurations) acceptedConfigurations = temp self.acceptedConfigurations = [] for acceptedConfiguration in acceptedConfigurations: - level = self.getConfigurationLevel( acceptedConfiguration) + level = getConfigurationLevel(self.inputSize, acceptedConfiguration) if level > len( self.acceptedConfigurations): exceptionMessage = "Cannot add configurations beyond the highest level + 1" \ + "\n attempted level: " + str(level) \ @@ -64,24 +67,21 @@ def __init__(self, inputSize, acceptedConfigurations, nodeToCopy = None): self.acceptedConfigurations[level].append( acceptedConfiguration) self.index = -1 + - def isConsistent(self, configuration): - ''' - With regards to System Diagnostics, this function tests if the given configuration - is "consistent" with the node/function. I.e. is the configuration an accepted one? - ''' - if ( self.getIndex() & 1 << configuration ) == 0: - return False - else: - return True - - def acceptedConfigurationsAsList(self): + def getAcceptedConfigurationsAsList(self): acceptedConfigurations = [] for level in self.acceptedConfigurations: acceptedConfigurations.extend(level) return acceptedConfigurations + def isConsistent(self, configuration): + ''' + Checks if a configuration would be accepted or not. + ''' + return isConsistent(self, configuration) + def _generatePossibleConfigurations(self): ''' Generates possible configurations to add to the function such that the new functions would be monotone @@ -95,7 +95,7 @@ def _generatePossibleConfigurations(self): #current max configuration level is [self.bitMask] if newMaxLevel == 1: - possibleConfigurations = self.getLevelOneConfigurations() + possibleConfigurations = getLevelOneConfigurations(self.inputSize) #Entire Dedekind Node Lattice is filled, can add [0] as an accepted configuration elif newMaxLevel == self.inputSize \ @@ -111,24 +111,11 @@ def _generatePossibleConfigurations(self): possibleConfiguration = self.bitMask for configuration in combination: possibleConfiguration &= configuration - if self.getConfigurationLevel(possibleConfiguration) == newMaxLevel: + if getConfigurationLevel(self.inputSize, possibleConfiguration) == newMaxLevel: possibleConfigurations.append(possibleConfiguration) return possibleConfigurations return possibleConfigurations - - def getLevelOneConfigurations(self): - ''' - Possible level One configurations are generated uniquely from all others. Given that the current - function only accepts the state where all inputs are "on", possible level one configurations are any - input such that one input is "off". - E.G. if input size is 4, the children of {0b1111} can potentially have any of {0b0111, 0b1011, 0b1101, 0b1110} - ''' - levelOneConfigurations = [] - for inputIndex in range(0, self.inputSize): - levelOneConfigurations.append( (1 << inputIndex) ^ self.bitMask ) - - return levelOneConfigurations def generateChildren(self): @@ -146,38 +133,25 @@ def generateChildren(self): return children - def getIndex(self): + def __iter__(self): ''' - If we treat each configuration as its own integer value, we can combine each value into an integer - of size 2**inputSize bits. E.G. if the input size is 4, then each configuration has a value between 0-15. - So an integer of 16 bits, where each bit is for each configuration, will represent the function - and its accepted configurations. Since this value is unique, we can also use it as an index for the function + Implemented this with good design in mind, however + If you want something fast, it is better to use + getAcceptedConfigurationsAsList ''' - if self.index != -1: - return self.index - index = 0 - for configurationLevel in self.acceptedConfigurations: - for configuration in configurationLevel: - index |= (1 << configuration) - - self.index = index - return index + return DedekindNodeIter(self) - def getConfigurationLevel(self, configuration): - global configurationLevelss - if self.inputSize not in configurationLevelss: - configurationLevels = [0] *(self.bitMask + 1) - for index in range (0, self.bitMask + 1): - configurationLevels[index] = hamming_distance(self.bitMask, index) - - configurationLevelss[self.inputSize] = configurationLevels - return configurationLevelss[self.inputSize][configuration] + def getLastLevel(self): + if len(self.acceptedConfigurations) > 0: + return self.acceptedConfigurations[-1] + else: + return [] def writeToDotFile(self, writeLocation): global fullNodes, configurationLabelss, dotEdgess dotFileName = os.path.join(writeLocation, "n_" + str(self.inputSize)\ - + "." + "world_" + str(self.getIndex())\ + + "." + "world_" + str(getIndex(self.getAcceptedConfigurationsAsList()))\ + ".dot") dotFile = open( dotFileName, "w") dotFile.write("""digraph{ @@ -185,14 +159,14 @@ def writeToDotFile(self, writeLocation): node[shape=circle, style=filled, label=""] edge[dir=none]\n""") - self.initDotVariables() + initDotVariables(self.inputSize) fullNode = fullNodes[self.inputSize] configurationLabels = configurationLabelss[self.inputSize] dotEdges = dotEdgess[self.inputSize] - configurationList = self.acceptedConfigurationsAsList() - for configuration in fullNode.acceptedConfigurationsAsList(): - if configuration in configurationList: + #configurationList = self.getAcceptedConfigurationsAsList() + for configuration in fullNode: + if configuration in self: dotFile.write( configurationLabels[configuration] +" [ color = green, "\ + "label = \""+ configurationLabels[configuration] + "\"]\n") else: @@ -203,42 +177,102 @@ def writeToDotFile(self, writeLocation): dotFile.write("}") dotFile.close() + +def isConsistent(node, configuration): + ''' + Checks if a configuration would be deemed "inconsistent". + This is confusing! The DedekindNode represents a faulty system, and the + "accepted configurations" represent sets such that, if you deemed the given + components (represented by bits) as "faulty" and all others as safe, you would explain + erroneous output. + ''' + from sets import ImmutableSet + if isinstance(configuration, ImmutableSet): + configuration = getConfAsInt(configuration, node.inputSize) + if (getIndex(node.getAcceptedConfigurationsAsList()) & 1 << configuration ) == 0: + return False + else: + return True + +def getFullNode(inputSize): + global fullNodes + if inputSize not in fullNodes: + bitMask = (1< " \ - + configurationLabels[configuration] + "\n" - dotEdges += edgeString - - fullNodes[self.inputSize] = fullNode - configurationLabelss[self.inputSize] = configurationLabels - dotEdgess[self.inputSize] = dotEdges + return fullNodes[inputSize] + +def initDotVariables(inputSize): + global fullNodes, configurationLabelss, dotEdgess + ''' + Helper function used to set up variables used for writing + a Dedekind Node to a dot file. + ''' + bitMask = (1< " \ + + configurationLabels[configuration] + "\n" + dotEdges += edgeString + + fullNodes[inputSize] = fullNode + configurationLabelss[inputSize] = configurationLabels + dotEdgess[inputSize] = dotEdges + +def getIndex(configurationList): + ''' + If we treat each configuration as its own integer value, we can combine each value into an integer + of size 2**inputSize bits. E.G. if the input size is 4, then each configuration has a value between 0-15. + So an integer of 16 bits, where each bit is for each configuration, will represent the function + and its accepted configurations. Since this value is unique, we can also use it as an index for the function + ''' + index = 0 + for configuration in configurationList: + index |= (1 << configuration) + + return index + +def getConfigurationLevel(inputSize, configuration): + global configurationLevelss + if inputSize not in configurationLevelss: + bitMask = (1<