diff --git a/.gitignore b/.gitignore index c56418d..c6ecab7 100644 --- a/.gitignore +++ b/.gitignore @@ -19,6 +19,7 @@ pip-log.txt # Unit test / coverage reports .coverage .tox +.log # Other filesystems .svn @@ -26,3 +27,6 @@ pip-log.txt # Editors .*.swp + +# Output +output/* diff --git a/.ipynb_checkpoints/Copycat-checkpoint.ipynb b/.ipynb_checkpoints/Copycat-checkpoint.ipynb new file mode 100644 index 0000000..2fd6442 --- /dev/null +++ b/.ipynb_checkpoints/Copycat-checkpoint.ipynb @@ -0,0 +1,6 @@ +{ + "cells": [], + "metadata": {}, + "nbformat": 4, + "nbformat_minor": 2 +} diff --git a/.travis.yml b/.travis.yml new file mode 100644 index 0000000..45fbf65 --- /dev/null +++ b/.travis.yml @@ -0,0 +1,12 @@ +language: python +branches: + only: + - "develop" + - "master" +python: + - "3.6" +install: + - pip3 install -r requirements.txt +script: + - python3 tests.py + diff --git a/Copycat.ipynb b/Copycat.ipynb new file mode 100644 index 0000000..2225b63 --- /dev/null +++ b/Copycat.ipynb @@ -0,0 +1,81 @@ +{ + "cells": [ + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "# Copycat \n", + "\n", + "Just type your copycat example, and the number of iterations." + ] + }, + { + "cell_type": "code", + "execution_count": 9, + "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "Answered iijjkl (time 1374, final temperature 13.5)\n", + "Answered iijjll (time 665, final temperature 19.6)\n", + "Answered iijjll (time 406, final temperature 16.6)\n", + "Answered iijjkl (time 379, final temperature 47.9)\n", + "Answered iijjll (time 556, final temperature 19.2)\n", + "Answered iijjkl (time 813, final temperature 42.8)\n", + "Answered iijjll (time 934, final temperature 15.5)\n", + "Answered iijjkl (time 1050, final temperature 49.5)\n", + "Answered iijjkl (time 700, final temperature 44.0)\n", + "Answered iijjkl (time 510, final temperature 34.8)\n", + "Answered iijjkl (time 673, final temperature 18.1)\n", + "Answered iijjkl (time 1128, final temperature 19.8)\n", + "Answered iijjll (time 961, final temperature 19.9)\n", + "Answered iijjll (time 780, final temperature 16.5)\n", + "Answered iijjll (time 607, final temperature 17.8)\n", + "Answered iijjll (time 594, final temperature 39.7)\n", + "Answered iijjll (time 736, final temperature 18.4)\n", + "Answered iijjll (time 903, final temperature 18.6)\n", + "Answered iijjll (time 601, final temperature 20.6)\n", + "Answered iijjll (time 949, final temperature 42.4)\n", + "iijjll: 12 (avg time 724.3, avg temp 22.1)\n", + "iijjkl: 8 (avg time 828.4, avg temp 33.8)\n" + ] + } + ], + "source": [ + "%run main.py abc abd iijjkk --iterations 20" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "collapsed": true + }, + "outputs": [], + "source": [] + } + ], + "metadata": { + "kernelspec": { + "display_name": "Python 3", + "language": "python", + "name": "python3" + }, + "language_info": { + "codemirror_mode": { + "name": "ipython", + "version": 3 + }, + "file_extension": ".py", + "mimetype": "text/x-python", + "name": "python", + "nbconvert_exporter": "python", + "pygments_lexer": "ipython3", + "version": "3.6.1" + } + }, + "nbformat": 4, + "nbformat_minor": 2 +} diff --git a/README.md b/README.md index 0a55f18..48287a9 100644 --- a/README.md +++ b/README.md @@ -1,8 +1,15 @@ co.py.cat ========= +I am planning to use this codebase, or Joseph A. Hager's, to implement a variation of Copycat that uses *Entropy* instead of *Temperature*, while still preserving the parallel terraced scan in full form. If the change is viable, I plan to write a paper on that (if anyone is interested in co-authoring, let me know). For the general idea, please see pages 41 and 42 of the [*Information Sciences*](https://github.com/Alex-Linhares/FARGlexandria/blob/master/Literature/Chess-Capyblanca-2014-Linhares-Information%20Sciences.pdf) paper on [Capyblanca](https://github.com/Alex-Linhares/FARGlexandria). + +**If you would like to help research and publish a paper, please let me know.** + +Please see also [FARGlexandria](https://github.com/Alex-Linhares/FARGlexandria), a repository with all FARG projects (and help if you have some of the missing info there, especially about Letter Spirit and George!) + +------------------------------- An implementation of [Douglas Hofstadter](http://prelectur.stanford.edu/lecturers/hofstadter/)'s Copycat algorithm. -The Copycat algorithm is explained [on Wikipedia](https://en.wikipedia.org/wiki/Copycat_%28software%29), and that page has many links for deeper reading. +The Copycat algorithm is explained [on Wikipedia](https://en.wikipedia.org/wiki/Copycat_%28software%29), and that page has many links for deeper reading. See also [Farglexandria](https://github.com/Alex-Linhares/Farglexandria). This implementation is a copycat of Scott Boland's [Java implementation](https://archive.org/details/JavaCopycat). The original Java-to-Python translation work was done by J Alan Brogan (@jalanb on GitHub). @@ -73,3 +80,10 @@ $ python ``` The result of `run` is a dict containing the same information as was printed by `main.py` above. + + + +Questions +--------- + +1. Why are codelets **NOT** implemented through lambda? diff --git a/copycat.log b/copycat.log deleted file mode 100644 index 945ec05..0000000 --- a/copycat.log +++ /dev/null @@ -1,63 +0,0 @@ -object chosen = j from target string -destination: k -chosen bond facet: letterCategory -Source: j, destination: k -source descriptor: J -destination descriptor: K -proposing successor bond -urgency: 100.0, number: 1, bin: 7 -object chosen = j from target string -destination: i -chosen bond facet: letterCategory -Source: j, destination: i -source descriptor: J -destination descriptor: I -proposing predecessor bond -object chosen = successor bond between j and k from other string -bond strength = 48 for successor bond between j and k -object chosen = a from initial string -destination: b -chosen bond facet: letterCategory -Source: a, destination: b -source descriptor: A -destination descriptor: B -proposing successor bond -object chosen = successor bond between a and b from other string -bond strength = 48 for successor bond between a and b -succeeded: posting bond-builder -object chosen = successor bond between a and b from other string -number of incompatibleBonds: 0 -no incompatible bonds -no incompatible groups -building bond successor bond between a and b -object chosen = a from initial string -destination: b -chosen bond facet: letterCategory -Source: a, destination: b -source descriptor: A -destination descriptor: B -proposing successor bond -object chosen = a from initial string -destination: b -chosen bond facet: letterCategory -Source: a, destination: b -source descriptor: A -destination descriptor: B -proposing successor bond -Post top down: top-down-description-scout, with urgency: 5 -posting bottom up codelets -object chosen = a from initial string -object chosen = j from target string -destination: i -chosen bond facet: letterCategory -Source: j, destination: i -source descriptor: J -destination descriptor: I -proposing predecessor bond -object chosen = a from initial string -destination: b -chosen bond facet: letterCategory -Source: a, destination: b -source descriptor: A -destination descriptor: B -proposing successor bond diff --git a/copycat/__init__.py b/copycat/__init__.py index 67e5cc9..4e8bc55 100644 --- a/copycat/__init__.py +++ b/copycat/__init__.py @@ -1 +1,3 @@ from .copycat import Copycat, Reporter # noqa +from .plot import plot_answers +from .io import save_answers diff --git a/copycat/codeletMethods.py b/copycat/codeletMethods.py index dad3924..a497410 100644 --- a/copycat/codeletMethods.py +++ b/copycat/codeletMethods.py @@ -72,11 +72,14 @@ def __structureVsStructure(structure1, weight1, structure2, weight2): """Return true if the first structure comes out stronger than the second.""" ctx = structure1.ctx random = ctx.random + # TODO: use entropy temperature = ctx.temperature structure1.updateStrength() structure2.updateStrength() + # TODO: use entropy weightedStrength1 = temperature.getAdjustedValue( structure1.totalStrength * weight1) + # TODO: use entropy weightedStrength2 = temperature.getAdjustedValue( structure2.totalStrength * weight2) return random.weighted_greater_than(weightedStrength1, weightedStrength2) @@ -120,6 +123,15 @@ def __slippability(ctx, conceptMappings): @codelet('breaker') def breaker(ctx, codelet): + # From the original LISP: + ''' + First decides probabilistically whether or not to fizzle, based on + temperature. Chooses a structure and random and decides probabilistically + whether or not to break it as a function of its total weakness. + + If the structure is a bond in a group, have to break the group in + order to break the bond. + ''' random = ctx.random temperature = ctx.temperature workspace = ctx.workspace @@ -138,6 +150,8 @@ def breaker(ctx, codelet): if structure.source.group == structure.destination.group: breakObjects += [structure.source.group] # Break all the objects or none of them; this matches the Java + # "all objects" means a bond and its group, if it has one. + for structure in breakObjects: breakProbability = temperature.getAdjustedProbability( structure.totalStrength / 100.0) @@ -150,17 +164,18 @@ def breaker(ctx, codelet): def chooseRelevantDescriptionByActivation(ctx, workspaceObject): random = ctx.random descriptions = workspaceObject.relevantDescriptions() - weights = [description.descriptor.activation - for description in descriptions] + weights = [description.descriptor.activation for description in descriptions] return random.weighted_choice(descriptions, weights) def similarPropertyLinks(ctx, slip_node): random = ctx.random + # TODO: use entropy temperature = ctx.temperature result = [] for slip_link in slip_node.propertyLinks: association = slip_link.degreeOfAssociation() / 100.0 + # TODO:use entropy probability = temperature.getAdjustedProbability(association) if random.coinFlip(probability): result += [slip_link] @@ -183,7 +198,7 @@ def bottom_up_description_scout(ctx, codelet): sliplinks = similarPropertyLinks(ctx, description.descriptor) assert sliplinks weights = [sliplink.degreeOfAssociation() * sliplink.destination.activation - for sliplink in sliplinks] + for sliplink in sliplinks] chosen = random.weighted_choice(sliplinks, weights) chosenProperty = chosen.destination coderack.proposeDescription(chosenObject, chosenProperty.category(), @@ -211,11 +226,13 @@ def top_down_description_scout(ctx, codelet): def description_strength_tester(ctx, codelet): coderack = ctx.coderack random = ctx.random + # TODO: use entropy temperature = ctx.temperature description = codelet.arguments[0] description.descriptor.buffer = 100.0 description.updateStrength() strength = description.totalStrength + # TODO: use entropy probability = temperature.getAdjustedProbability(strength / 100.0) assert random.coinFlip(probability) coderack.newCodelet('description-builder', strength, [description]) @@ -295,11 +312,12 @@ def rule_scout(ctx, codelet): coderack = ctx.coderack random = ctx.random slipnet = ctx.slipnet + # TODO: use entropy temperature = ctx.temperature workspace = ctx.workspace assert workspace.numberOfUnreplacedObjects() == 0 changedObjects = [o for o in workspace.initial.objects if o.changed] - #assert len(changedObjects) < 2 + # assert len(changedObjects) < 2 # if there are no changed objects, propose a rule with no changes if not changedObjects: return coderack.proposeRule(None, None, None, None) @@ -329,10 +347,11 @@ def rule_scout(ctx, codelet): if targetObject.described(node): if targetObject.distinguishingDescriptor(node): newList += [node] - objectList = newList # surely this should be += - # "union of this and distinguishing descriptors" + objectList = newList # surely this should be += + # "union of this and distinguishing descriptors" assert objectList # use conceptual depth to choose a description + # TODO: use entropy weights = [ temperature.getAdjustedValue(node.conceptualDepth) for node in objectList @@ -344,6 +363,7 @@ def rule_scout(ctx, codelet): objectList += [changed.replacement.relation] objectList += [changed.replacement.objectFromModified.getDescriptor( slipnet.letterCategory)] + # TODO: use entropy # use conceptual depth to choose a relation weights = [ temperature.getAdjustedValue(node.conceptualDepth) @@ -358,9 +378,11 @@ def rule_scout(ctx, codelet): def rule_strength_tester(ctx, codelet): coderack = ctx.coderack random = ctx.random + # TODO: use entropy temperature = ctx.temperature rule = codelet.arguments[0] rule.updateStrength() + # TODO: use entropy probability = temperature.getAdjustedProbability(rule.totalStrength / 100.0) if random.coinFlip(probability): coderack.newCodelet('rule-builder', rule.totalStrength, [rule]) @@ -393,8 +415,8 @@ def replacement_finder(ctx, codelet): relation = relations[diff] else: relation = None - letterOfInitialString.replacement = Replacement(ctx, - letterOfInitialString, letterOfModifiedString, relation) + letterOfInitialString.replacement = Replacement(ctx, letterOfInitialString, + letterOfModifiedString, relation) if relation != slipnet.sameness: letterOfInitialString.changed = True workspace.changedObject = letterOfInitialString @@ -437,8 +459,8 @@ def top_down_bond_scout__direction(ctx, codelet): coderack = ctx.coderack slipnet = ctx.slipnet direction = codelet.arguments[0] - source = __getScoutSource(ctx, - direction, formulas.localDirectionCategoryRelevance, 'bond') + source = __getScoutSource(ctx, direction, formulas.localDirectionCategoryRelevance, + 'bond') destination = chooseDirectedNeighbor(ctx, source, direction) assert destination logging.info('to object: %s', destination) @@ -458,11 +480,13 @@ def top_down_bond_scout__direction(ctx, codelet): def bond_strength_tester(ctx, codelet): coderack = ctx.coderack random = ctx.random + # TODO: use entropy temperature = ctx.temperature bond = codelet.arguments[0] __showWhichStringObjectIsFrom(bond) bond.updateStrength() strength = bond.totalStrength + # TODO: use entropy probability = temperature.getAdjustedProbability(strength / 100.0) logging.info('bond strength = %d for %s', strength, bond) assert random.coinFlip(probability) @@ -503,7 +527,7 @@ def bond_builder(ctx, codelet): if incompatibleCorrespondences: logging.info("trying to break incompatible correspondences") assert __fight(bond, 2.0, incompatibleCorrespondences, 3.0) - #assert __fightIncompatibles(incompatibleCorrespondences, + # assert __fightIncompatibles(incompatibleCorrespondences, # bond, 'correspondences', 2.0, 3.0) for incompatible in incompatibleBonds: incompatible.break_the_structure() @@ -693,7 +717,7 @@ def top_down_group_scout__direction(ctx, codelet): direction, bondFacet) -#noinspection PyStringFormat +# noinspection PyStringFormat @codelet('group-scout--whole-string') def group_scout__whole_string(ctx, codelet): coderack = ctx.coderack @@ -739,12 +763,14 @@ def group_strength_tester(ctx, codelet): coderack = ctx.coderack random = ctx.random slipnet = ctx.slipnet + # TODO: use entropy temperature = ctx.temperature # update strength value of the group group = codelet.arguments[0] __showWhichStringObjectIsFrom(group) group.updateStrength() strength = group.totalStrength + # TODO: use entropy probability = temperature.getAdjustedProbability(strength / 100.0) if random.coinFlip(probability): # it is strong enough - post builder & activate nodes @@ -860,6 +886,7 @@ def __getCutoffWeights(bondDensity): def rule_translator(ctx, codelet): coderack = ctx.coderack random = ctx.random + # TODO: use entropy temperature = ctx.temperature workspace = ctx.workspace assert workspace.rule @@ -872,6 +899,7 @@ def rule_translator(ctx, codelet): bondDensity = min(bondDensity, 1.0) weights = __getCutoffWeights(bondDensity) cutoff = 10.0 * random.weighted_choice(list(range(1, 11)), weights) + # TODO: use entropy if cutoff >= temperature.actual_value: result = workspace.rule.buildTranslatedRule() if result is not None: @@ -908,11 +936,11 @@ def bottom_up_correspondence_scout(ctx, codelet): and m.initialDescriptionType != slipnet.bondFacet] initialDescriptionTypes = [m.initialDescriptionType for m in opposites] flipTargetObject = False - if (objectFromInitial.spansString() and - objectFromTarget.spansString() and - slipnet.directionCategory in initialDescriptionTypes - and all(m.label == slipnet.opposite for m in opposites) # unreached? - and slipnet.opposite.activation != 100.0): + if (objectFromInitial.spansString() and + objectFromTarget.spansString() and + slipnet.directionCategory in initialDescriptionTypes + and all(m.label == slipnet.opposite for m in opposites) # unreached? + and slipnet.opposite.activation != 100.0): objectFromTarget = objectFromTarget.flippedVersion() conceptMappings = formulas.getMappings( objectFromInitial, objectFromTarget, @@ -928,6 +956,7 @@ def important_object_correspondence_scout(ctx, codelet): coderack = ctx.coderack random = ctx.random slipnet = ctx.slipnet + # TODO: use entropy temperature = ctx.temperature workspace = ctx.workspace objectFromInitial = chooseUnmodifiedObject(ctx, 'relativeImportance', @@ -935,6 +964,7 @@ def important_object_correspondence_scout(ctx, codelet): assert objectFromInitial is not None descriptors = objectFromInitial.relevantDistinguishingDescriptors() # choose descriptor by conceptual depth + # TODO: use entropy weights = [temperature.getAdjustedValue(n.conceptualDepth) for n in descriptors] slipnode = random.weighted_choice(descriptors, weights) assert slipnode @@ -967,11 +997,11 @@ def important_object_correspondence_scout(ctx, codelet): and m.initialDescriptionType != slipnet.bondFacet] initialDescriptionTypes = [m.initialDescriptionType for m in opposites] flipTargetObject = False - if (objectFromInitial.spansString() - and objectFromTarget.spansString() - and slipnet.directionCategory in initialDescriptionTypes - and all(m.label == slipnet.opposite for m in opposites) # unreached? - and slipnet.opposite.activation != 100.0): + if (objectFromInitial.spansString() + and objectFromTarget.spansString() + and slipnet.directionCategory in initialDescriptionTypes + and all(m.label == slipnet.opposite for m in opposites) # unreached? + and slipnet.opposite.activation != 100.0): objectFromTarget = objectFromTarget.flippedVersion() conceptMappings = formulas.getMappings( objectFromInitial, objectFromTarget, @@ -986,6 +1016,7 @@ def important_object_correspondence_scout(ctx, codelet): def correspondence_strength_tester(ctx, codelet): coderack = ctx.coderack random = ctx.random + # TODO: use entropy temperature = ctx.temperature workspace = ctx.workspace correspondence = codelet.arguments[0] @@ -998,6 +1029,7 @@ def correspondence_strength_tester(ctx, codelet): objectFromTarget.flipped_version()))) correspondence.updateStrength() strength = correspondence.totalStrength + # TODO: use entropy probability = temperature.getAdjustedProbability(strength / 100.0) if random.coinFlip(probability): # activate some concepts @@ -1051,8 +1083,8 @@ def correspondence_builder(ctx, codelet): # if there is an incompatible bond then fight against it initial = correspondence.objectFromInitial target = correspondence.objectFromTarget - if (initial.leftmost or initial.rightmost and - target.leftmost or target.rightmost): + if (initial.leftmost or initial.rightmost and + target.leftmost or target.rightmost): # search for the incompatible bond incompatibleBond = correspondence.getIncompatibleBond() if incompatibleBond: diff --git a/copycat/coderack.py b/copycat/coderack.py index fb58e2a..33fef58 100644 --- a/copycat/coderack.py +++ b/copycat/coderack.py @@ -68,6 +68,7 @@ class Coderack(object): self.postBottomUpCodelets() def probabilityOfPosting(self, codeletName): + # TODO: use entropy temperature = self.ctx.temperature workspace = self.ctx.workspace if codeletName == 'breaker': @@ -83,6 +84,7 @@ class Coderack(object): if 'correspondence' in codeletName: return workspace.interStringUnhappiness / 100.0 if 'description' in codeletName: + # TODO: use entropy return (temperature.value() / 100.0) ** 2 return workspace.intraStringUnhappiness / 100.0 @@ -155,12 +157,15 @@ class Coderack(object): def __postBottomUpCodelets(self, codeletName): random = self.ctx.random + # TODO: use entropy temperature = self.ctx.temperature probability = self.probabilityOfPosting(codeletName) howMany = self.howManyToPost(codeletName) urgency = 3 if codeletName == 'breaker': urgency = 1 + + # TODO: use entropy if temperature.value() < 25.0 and 'translator' in codeletName: urgency = 5 for _ in range(howMany): @@ -285,8 +290,11 @@ class Coderack(object): def chooseCodeletToRun(self): random = self.ctx.random + # TODO: use entropy temperature = self.ctx.temperature assert self.codelets + + # TODO: use entropy scale = (100.0 - temperature.value() + 10.0) / 15.0 chosen = random.weighted_choice(self.codelets, [codelet.urgency ** scale for codelet in self.codelets]) self.removeCodelet(chosen) diff --git a/copycat/copycat.py b/copycat/copycat.py index fc400b8..51166d2 100644 --- a/copycat/copycat.py +++ b/copycat/copycat.py @@ -4,7 +4,6 @@ from .slipnet import Slipnet from .temperature import Temperature from .workspace import Workspace - class Reporter(object): """Do-nothing base class for defining new reporter types""" def report_answer(self, answer): @@ -16,7 +15,7 @@ class Reporter(object): def report_slipnet(self, slipnet): pass - def report_temperature(self, temperature): + def report_temperature(self, temperature): #TODO: use entropy pass def report_workspace(self, workspace): @@ -28,19 +27,19 @@ class Copycat(object): self.coderack = Coderack(self) self.random = Randomness(rng_seed) self.slipnet = Slipnet() - self.temperature = Temperature() + self.temperature = Temperature() # TODO: use entropy self.workspace = Workspace(self) self.reporter = reporter or Reporter() def mainLoop(self, lastUpdate): currentTime = self.coderack.codeletsRun - self.temperature.tryUnclamp(currentTime) + self.temperature.tryUnclamp(currentTime) # TODO: use entropy # Every 15 codelets, we update the workspace. if currentTime >= lastUpdate + 15: self.workspace.updateEverything() self.coderack.updateCodelets() self.slipnet.update(self.random) - self.temperature.update(self.workspace.getUpdatedTemperature()) + self.temperature.update(self.workspace.getUpdatedTemperature()) # TODO: use entropy lastUpdate = currentTime self.reporter.report_slipnet(self.slipnet) self.coderack.chooseAndRunCodelet() @@ -53,14 +52,14 @@ class Copycat(object): """Run a trial of the copycat algorithm""" self.coderack.reset() self.slipnet.reset() - self.temperature.reset() + self.temperature.reset() # TODO: use entropy self.workspace.reset() lastUpdate = float('-inf') while self.workspace.finalAnswer is None: lastUpdate = self.mainLoop(lastUpdate) answer = { 'answer': self.workspace.finalAnswer, - 'temp': self.temperature.last_unclamped_value, + 'temp': self.temperature.last_unclamped_value, # TODO: use entropy 'time': self.coderack.codeletsRun, } self.reporter.report_answer(answer) @@ -68,16 +67,27 @@ class Copycat(object): def run(self, initial, modified, target, iterations): self.workspace.resetWithStrings(initial, modified, target) + + self.temperature.useAdj('original') + #self.temperature.useAdj('entropy') + #self.temperature.useAdj('inverse') # 100 weight + #self.temperature.useAdj('fifty_converge') + #self.temperature.useAdj('soft') + #self.temperature.useAdj('weighted_soft') + #self.temperature.useAdj('alt_fifty') + #self.temperature.useAdj('average_alt') + self.temperature.useAdj('best') + answers = {} for i in range(iterations): answer = self.runTrial() d = answers.setdefault(answer['answer'], { 'count': 0, - 'sumtemp': 0, + 'sumtemp': 0, # TODO: use entropy 'sumtime': 0 }) d['count'] += 1 - d['sumtemp'] += answer['temp'] + d['sumtemp'] += answer['temp'] # TODO: use entropy d['sumtime'] += answer['time'] for answer, d in answers.items(): diff --git a/copycat/curses_reporter.py b/copycat/curses_reporter.py index 08f24db..faa8548 100644 --- a/copycat/curses_reporter.py +++ b/copycat/curses_reporter.py @@ -63,7 +63,7 @@ class CursesReporter(Reporter): coderackHeight = height - upperHeight - answersHeight self.focusOnSlipnet = focus_on_slipnet self.fpsGoal = fps_goal - self.temperatureWindow = SafeSubwindow(window, height, 5, 0, 0) + self.temperatureWindow = SafeSubwindow(window, height, 5, 0, 0) # TODO: use entropy (entropyWindow) self.upperWindow = SafeSubwindow(window, upperHeight, width-5, 0, 5) self.coderackWindow = SafeSubwindow(window, coderackHeight, width-5, upperHeight, 5) self.answersWindow = SafeSubwindow(window, answersHeight, width-5, upperHeight + coderackHeight, 5) @@ -239,6 +239,7 @@ class CursesReporter(Reporter): w.border() w.refresh() + #TODO: use entropy def report_temperature(self, temperature): self.do_keyboard_shortcuts() w = self.temperatureWindow diff --git a/copycat/group.py b/copycat/group.py index 015dae3..0d3abb4 100644 --- a/copycat/group.py +++ b/copycat/group.py @@ -96,6 +96,7 @@ class Group(WorkspaceObject): support = self.localSupport() / 100.0 activation = slipnet.length.activation / 100.0 supportedActivation = (support * activation) ** exp + #TODO: use entropy return temperature.getAdjustedProbability(supportedActivation) def flippedVersion(self): @@ -130,6 +131,7 @@ class Group(WorkspaceObject): cubedlength = length ** 3 fred = cubedlength * (100.0 - slipnet.length.activation) / 100.0 probability = 0.5 ** fred + #TODO: use entropy value = temperature.getAdjustedProbability(probability) if value < 0.06: value = 0.0 diff --git a/copycat/io.py b/copycat/io.py new file mode 100644 index 0000000..ae1185b --- /dev/null +++ b/copycat/io.py @@ -0,0 +1,9 @@ + +def save_answers(answers, filename): + answers = sorted(answers.items(), key=lambda kv : kv[1]['count']) + keys = [k for k, v in answers] + counts = [str(v['count']) for k, v in answers] + with open(filename, 'w') as outfile: + outfile.write(','.join(keys)) + outfile.write('\n') + outfile.write(','.join(counts)) diff --git a/copycat/plot.py b/copycat/plot.py new file mode 100644 index 0000000..772d0f1 --- /dev/null +++ b/copycat/plot.py @@ -0,0 +1,20 @@ +import matplotlib.pyplot as plt; plt.rcdefaults() +import numpy as np +import matplotlib.pyplot as plt + + +def plot_answers(answers, show=True, save=True, filename='distribution.png'): + answers = sorted(answers.items(), key=lambda kv : kv[1]['count']) + objects = [t[0] + ' (temp:{})'.format(round(t[1]['avgtemp'], 2)) for t in answers] + yvalues = [t[1]['count'] for t in answers] + + y_pos = np.arange(len(objects)) + + plt.bar(y_pos, yvalues, align='center', alpha=0.5) + plt.xticks(y_pos, objects) + plt.ylabel('Count') + plt.title('Answers') + if show: + plt.show() + if save: + plt.savefig('output/{}'.format(filename)) diff --git a/copycat/temperature.py b/copycat/temperature.py index f00e479..e62f0a8 100644 --- a/copycat/temperature.py +++ b/copycat/temperature.py @@ -1,9 +1,81 @@ import math +# Alternate formulas for getAdjustedProbability + +def _original(temp, prob): + if prob == 0 or prob == 0.5 or temp == 0: + return prob + if prob < 0.5: + return 1.0 - _original(temp, 1.0 - prob) + coldness = 100.0 - temp + a = math.sqrt(coldness) + c = (10 - a) / 100 + f = (c + 1) * prob + return max(f, 0.5) + +def _entropy(temp, prob): + if prob == 0 or prob == 0.5 or temp == 0: + return prob + if prob < 0.5: + return 1.0 - _original(temp, 1.0 - prob) + coldness = 100.0 - temp + a = math.sqrt(coldness) + c = (10 - a) / 100 + f = (c + 1) * prob + return -f * math.log2(f) + +def _weighted(temp, prob, s, u): + weighted = (temp / 100) * s + ((100 - temp) / 100) * u + return weighted + +def _weighted_inverse(temp, prob): + iprob = 1 - prob + return _weighted(temp, prob, iprob, prob) + +def _fifty_converge(temp, prob): # Uses .5 instead of 1-prob + return _weighted(temp, prob, .5, prob) + +def _soft_curve(temp, prob): # Curves to the average of the (1-p) and .5 + return min(1, _weighted(temp, prob, (1.5-prob)/2, prob)) + +def _weighted_soft_curve(temp, prob): # Curves to the weighted average of the (1-p) and .5 + weight = 100 + gamma = .5 # convergance value + alpha = 1 # gamma weight + beta = 3 # iprob weight + curved = min(1, (temp / weight) * ((alpha * gamma + beta * (1 - prob)) / (alpha + beta)) + ((weight - temp) / weight) * prob) + return curved + +def _alt_fifty(temp, prob): + s = .5 + u = prob ** 2 if prob < .5 else math.sqrt(prob) + return _weighted(temp, prob, s, u) + +def _averaged_alt(temp, prob): + s = (1.5 - prob)/2 + u = prob ** 2 if prob < .5 else math.sqrt(prob) + return _weighted(temp, prob, s, u) + +def _working_best(temp, prob): + s = .5 # convergence + r = 1.05 # power + u = prob ** r if prob < .5 else prob ** (1/r) + return _weighted(temp, prob, s, u) class Temperature(object): def __init__(self): self.reset() + self.adjustmentType = 'inverse' + self._adjustmentFormulas = { + 'original' : _original, + 'entropy' : _entropy, + 'inverse' : _weighted_inverse, + 'fifty_converge' : _fifty_converge, + 'soft' : _soft_curve, + 'weighted_soft' : _weighted_soft_curve, + 'alt_fifty' : _alt_fifty, + 'average_alt' : _averaged_alt, + 'best' : _working_best} def reset(self): self.actual_value = 100.0 @@ -34,12 +106,13 @@ class Temperature(object): return value ** (((100.0 - self.value()) / 30.0) + 0.5) def getAdjustedProbability(self, value): - if value == 0 or value == 0.5 or self.value() == 0: - return value - if value < 0.5: - return 1.0 - self.getAdjustedProbability(1.0 - value) - coldness = 100.0 - self.value() - a = math.sqrt(coldness) - c = (10 - a) / 100 - f = (c + 1) * value - return max(f, 0.5) + temp = self.value() + prob = value + return self._adjustmentFormulas[self.adjustmentType](temp, prob) + + def useAdj(self, adj): + print('Changing to adjustment formula {}'.format(adj)) + self.adjustmentType = adj + + def adj_formulas(self): + return self._adjustmentFormulas.keys() diff --git a/copycat/tests.py b/copycat/tests.py deleted file mode 100644 index 7556f4a..0000000 --- a/copycat/tests.py +++ /dev/null @@ -1,137 +0,0 @@ -import unittest - -from .copycat import Copycat - - -def pnormaldist(p): - table = { - 0.80: 1.2815, - 0.90: 1.6448, - 0.95: 1.9599, - 0.98: 2.3263, - 0.99: 2.5758, - 0.995: 2.8070, - 0.998: 3.0902, - 0.999: 3.2905, - 0.9999: 3.8905, - 0.99999: 4.4171, - 0.999999: 4.8916, - 0.9999999: 5.3267, - 0.99999999: 5.7307, - 0.999999999: 6.1094, - } - return max(v for k, v in table.items() if k <= p) - - -def lower_bound_on_probability(hits, attempts, confidence=0.95): - if attempts == 0: - return 0 - z = pnormaldist(confidence) - zsqr = z*z - phat = 1.0 * hits / attempts - under_sqrt = (phat * (1 - phat) + zsqr / (4*attempts)) / attempts - denominator = (1 + zsqr / attempts) - return (phat + zsqr / (2*attempts) - z * (under_sqrt ** 0.5)) / denominator - - -def upper_bound_on_probability(hits, attempts, confidence=0.95): - misses = attempts - hits - return 1.0 - lower_bound_on_probability(misses, attempts, confidence) - - -class TestCopycat(unittest.TestCase): - def setUp(self): - self.longMessage = True # new in Python 2.7 - - def assertProbabilitiesLookRoughlyLike(self, actual, expected): - actual_count = 0.0 + sum(d['count'] for d in list(actual.values())) - expected_count = 0.0 + sum(d['count'] for d in list(expected.values())) - self.assertGreater(actual_count, 1) - self.assertGreater(expected_count, 1) - for k in set(list(actual.keys()) + list(expected.keys())): - if k not in expected: - self.fail('Key %s was produced but not expected! %r != %r' % (k, actual, expected)) - expected_probability = expected[k]['count'] / expected_count - if k in actual: - actual_lo = lower_bound_on_probability(actual[k]['count'], actual_count) - actual_hi = upper_bound_on_probability(actual[k]['count'], actual_count) - if not (actual_lo <= expected_probability <= actual_hi): - print('Failed (%s <= %s <= %s)' % (actual_lo, expected_probability, actual_hi)) - self.fail('Count ("obviousness" metric) seems way off! %r != %r' % (actual, expected)) - if abs(actual[k]['avgtemp'] - expected[k]['avgtemp']) >= 10.0 + (10.0 / actual[k]['count']): - print('Failed (%s - %s >= %s)' % (actual[k]['avgtemp'], expected[k]['avgtemp'], 10.0 + (10.0 / actual[k]['count']))) - self.fail('Temperature ("elegance" metric) seems way off! %r != %r' % (actual, expected)) - else: - actual_hi = upper_bound_on_probability(0, actual_count) - if not (0 <= expected_probability <= actual_hi): - self.fail('No instances of expected key %s were produced! %r != %r' % (k, actual, expected)) - - def run_testcase(self, initial, modified, target, iterations, expected): - actual = Copycat().run(initial, modified, target, iterations) - self.assertEqual(sum(a['count'] for a in list(actual.values())), iterations) - self.assertProbabilitiesLookRoughlyLike(actual, expected) - - def test_simple_cases(self): - self.run_testcase('abc', 'abd', 'efg', 50, { - 'efd': {'count': 1, 'avgtemp': 16}, - 'efh': {'count': 99, 'avgtemp': 19}, - }) - self.run_testcase('abc', 'abd', 'ijk', 50, { - 'ijd': {'count': 4, 'avgtemp': 24}, - 'ijl': {'count': 96, 'avgtemp': 20}, - }) - - def test_abc_xyz(self): - self.run_testcase('abc', 'abd', 'xyz', 20, { - 'xyd': {'count': 100, 'avgtemp': 19}, - }) - - def test_ambiguous_case(self): - self.run_testcase('abc', 'abd', 'ijkk', 50, { - 'ijkkk': {'count': 7, 'avgtemp': 21}, - 'ijll': {'count': 47, 'avgtemp': 28}, - 'ijkl': {'count': 44, 'avgtemp': 32}, - 'ijkd': {'count': 2, 'avgtemp': 65}, - }) - - def test_mrrjjj(self): - self.run_testcase('abc', 'abd', 'mrrjjj', 50, { - 'mrrjjjj': {'count': 4, 'avgtemp': 16}, - 'mrrkkk': {'count': 31, 'avgtemp': 47}, - 'mrrjjk': {'count': 64, 'avgtemp': 51}, - 'mrrjkk': {'count': 1, 'avgtemp': 52}, - 'mrrjjd': {'count': 1, 'avgtemp': 54}, - }) - - def test_elongation(self): - # This isn't remotely what a human would say. - self.run_testcase('abc', 'aabbcc', 'milk', 50, { - 'milj': {'count': 85, 'avgtemp': 55}, - 'mikj': {'count': 10, 'avgtemp': 56}, - 'milk': {'count': 1, 'avgtemp': 56}, - 'lilk': {'count': 1, 'avgtemp': 57}, - 'milb': {'count': 3, 'avgtemp': 57}, - }) - - def test_repairing_successor_sequence(self): - # This isn't remotely what a human would say. - self.run_testcase('aba', 'abc', 'xyx', 50, { - 'xc': {'count': 9, 'avgtemp': 57}, - 'xyc': {'count': 82, 'avgtemp': 59}, - 'cyx': {'count': 7, 'avgtemp': 68}, - 'xyx': {'count': 2, 'avgtemp': 69}, - }) - - def test_nonsense(self): - self.run_testcase('cat', 'dog', 'cake', 10, { - 'cakg': {'count': 99, 'avgtemp': 70}, - 'gake': {'count': 1, 'avgtemp': 59}, - }) - self.run_testcase('cat', 'dog', 'kitten', 10, { - 'kitteg': {'count': 96, 'avgtemp': 66}, - 'kitten': {'count': 4, 'avgtemp': 68}, - }) - - -if __name__ == '__main__': - unittest.main() diff --git a/copycat/workspace.py b/copycat/workspace.py index 3e5228a..1c1ace7 100644 --- a/copycat/workspace.py +++ b/copycat/workspace.py @@ -1,3 +1,6 @@ +"""Workspace module.""" + + from . import formulas from .bond import Bond from .correspondence import Correspondence @@ -14,6 +17,7 @@ def __adjustUnhappiness(values): class Workspace(object): def __init__(self, ctx): + """To initialize the workspace.""" self.ctx = ctx self.totalUnhappiness = 0.0 self.intraStringUnhappiness = 0.0 @@ -35,11 +39,21 @@ class Workspace(object): self.changedObject = None self.objects = [] self.structures = [] - self.rule = None + self.rule = None # Only one rule? : LSaldyt self.initial = WorkspaceString(self.ctx, self.initialString) self.modified = WorkspaceString(self.ctx, self.modifiedString) self.target = WorkspaceString(self.ctx, self.targetString) + ''' + # TODO: Initial part of refactoring in this method + def getAssessedUnhappiness(self, unhappiness): + o.Unhappiness = __adjustUnhappiness( + o.relativeImportance * o.Unhappiness + for o in self.objects) + pass + ''' + + # TODO: Extract method? def assessUnhappiness(self): self.intraStringUnhappiness = __adjustUnhappiness( o.relativeImportance * o.intraStringUnhappiness @@ -51,6 +65,7 @@ class Workspace(object): o.relativeImportance * o.totalUnhappiness for o in self.objects) + # TODO: these 3 methods seem to be the same... are they? If so, Extract method. def calculateIntraStringUnhappiness(self): value = sum( o.relativeImportance * o.intraStringUnhappiness @@ -82,7 +97,13 @@ class Workspace(object): self.initial.updateIntraStringUnhappiness() self.target.updateIntraStringUnhappiness() + # TODO: use entropy def getUpdatedTemperature(self): + ''' + Calculation of global tolerance towards irrelevance + + temp = weightedAverage(totalUnhappiness(.8), ruleWeakness(.2)) + ''' self.calculateIntraStringUnhappiness() self.calculateInterStringUnhappiness() self.calculateTotalUnhappiness() @@ -97,7 +118,7 @@ class Workspace(object): )) def numberOfUnrelatedObjects(self): - """A list of all objects in the workspace with >= 1 open bond slots""" + """Computes the number of all objects in the workspace with >= 1 open bond slots.""" objects = [o for o in self.objects if o.string == self.initial or o.string == self.target] objects = [o for o in objects if not o.spansString()] @@ -115,21 +136,21 @@ class Workspace(object): return len(objects) def numberOfUnreplacedObjects(self): - """A list of all unreplaced objects in the initial string""" + """A list of all unreplaced objects in the initial string.""" objects = [o for o in self.objects if o.string == self.initial and isinstance(o, Letter)] objects = [o for o in objects if not o.replacement] return len(objects) def numberOfUncorrespondingObjects(self): - """A list of all uncorresponded objects in the initial string""" + """A list of all uncorresponded objects in the initial string.""" objects = [o for o in self.objects if o.string == self.initial or o.string == self.target] objects = [o for o in objects if not o.correspondence] return len(objects) def numberOfBonds(self): - """The number of bonds in the workspace""" + """The number of bonds in the workspace.""" return sum(1 for o in self.structures if isinstance(o, Bond)) def correspondences(self): diff --git a/copycat/workspaceFormulas.py b/copycat/workspaceFormulas.py index 40d991d..8f97cb3 100644 --- a/copycat/workspaceFormulas.py +++ b/copycat/workspaceFormulas.py @@ -1,5 +1,6 @@ def __chooseObjectFromList(ctx, objects, attribute): + # TODO: use entropy random = ctx.random temperature = ctx.temperature weights = [ diff --git a/copycat/workspaceObject.py b/copycat/workspaceObject.py index e641cea..9096861 100644 --- a/copycat/workspaceObject.py +++ b/copycat/workspaceObject.py @@ -2,7 +2,6 @@ from .description import Description from .formulas import weightedAverage from .workspaceStructure import WorkspaceStructure - class WorkspaceObject(WorkspaceStructure): # pylint: disable=too-many-instance-attributes def __init__(self, workspaceString): diff --git a/copycat/workspaceString.py b/copycat/workspaceString.py index 2d7a149..a57c218 100644 --- a/copycat/workspaceString.py +++ b/copycat/workspaceString.py @@ -38,7 +38,7 @@ class WorkspaceString(object): return self.string[i] def updateRelativeImportance(self): - """Update the normalised importance of all objects in the string""" + """Update the normalised importance of all objects in the string.""" total = sum(o.rawImportance for o in self.objects) if not total: for o in self.objects: diff --git a/input/.placeholder b/input/.placeholder new file mode 100644 index 0000000..e69de29 diff --git a/input/problems.csv b/input/problems.csv new file mode 100644 index 0000000..3e941fa --- /dev/null +++ b/input/problems.csv @@ -0,0 +1,9 @@ +abc,abd,ijk +aabc,aabd,ijkk +abc,abd,kji +abc,abd,mrrjjj +abc,abd,rssttt +abc,abd,xyz +abc,abd,ijjkkk +rst,rsu,xyz +abc,abd,xyyzzz diff --git a/input/reduced_problems.csv b/input/reduced_problems.csv new file mode 100644 index 0000000..f5fe027 --- /dev/null +++ b/input/reduced_problems.csv @@ -0,0 +1,4 @@ +abc,abd,ijk +aabc,aabd,ijkk +abc,abd,xyz +abc,abd,ijjkkk diff --git a/main.py b/main.py index 8673d0d..23b1033 100755 --- a/main.py +++ b/main.py @@ -1,21 +1,61 @@ #!/usr/bin/env python3 +""" +Main Copycat program. + +To run it, type at the terminal: + + > python main.py abc abd ppqqrr --interations 10 + +The script takes three to five arguments. The first two are a pair of strings +with some change, for example "abc" and "abd". The third is a string which the +script should try to change analogously. The fourth (which defaults to "1") is +a number of iterations. One can also specify a defined seed value for the +random number generator. + +This instruction produces output such as: + + iiijjjlll: 670 (avg time 1108.5, avg temp 23.6) + iiijjjd: 2 (avg time 1156.0, avg temp 35.0) + iiijjjkkl: 315 (avg time 1194.4, avg temp 35.5) + iiijjjkll: 8 (avg time 2096.8, avg temp 44.1) + iiijjjkkd: 5 (avg time 837.2, avg temp 48.0) + + wyz: 5 (avg time 2275.2, avg temp 14.9) + xyd: 982 (avg time 2794.4, avg temp 17.5) + yyz: 7 (avg time 2731.9, avg temp 25.1) + dyz: 2 (avg time 3320.0, avg temp 27.1) + xyy: 2 (avg time 4084.5, avg temp 31.1) + xyz: 2 (avg time 1873.5, avg temp 52.1) + +The first number indicates how many times Copycat chose that string as its +answer; higher means "more obvious". The last number indicates the average +final temperature of the workspace; lower means "more elegant". +""" + import argparse import logging -from copycat import Copycat, Reporter +from copycat import Copycat, Reporter, plot_answers, save_answers class SimpleReporter(Reporter): + """Reports results from a single run.""" + def report_answer(self, answer): + """Self-explanatory code.""" print('Answered %s (time %d, final temperature %.1f)' % ( answer['answer'], answer['time'], answer['temp'], )) + def main(): - logging.basicConfig(level=logging.INFO, format='%(message)s', filename='./copycat.log', filemode='w') + """Program's main entrance point. Self-explanatory code.""" + logging.basicConfig(level=logging.INFO, format='%(message)s', filename='./output/copycat.log', filemode='w') parser = argparse.ArgumentParser() parser.add_argument('--seed', type=int, default=None, help='Provide a deterministic seed for the RNG.') parser.add_argument('--iterations', type=int, default=1, help='Run the given case this many times.') + parser.add_argument('--plot', action='store_true', help='Plot a bar graph of answer distribution') + parser.add_argument('--noshow', action='store_true', help='Don\'t display bar graph at end of run') parser.add_argument('initial', type=str, help='A...') parser.add_argument('modified', type=str, help='...is to B...') parser.add_argument('target', type=str, help='...as C is to... what?') @@ -27,5 +67,10 @@ def main(): for answer, d in sorted(iter(answers.items()), key=lambda kv: kv[1]['avgtemp']): print('%s: %d (avg time %.1f, avg temp %.1f)' % (answer, d['count'], d['avgtime'], d['avgtemp'])) + if options.plot: + plot_answers(answers, show=not options.noshow) + save_answers(answers, 'output/answers.csv') + + if __name__ == '__main__': main() diff --git a/output/.placeholder b/output/.placeholder new file mode 100644 index 0000000..e69de29 diff --git a/requirements.txt b/requirements.txt new file mode 100644 index 0000000..db5d81e --- /dev/null +++ b/requirements.txt @@ -0,0 +1,2 @@ +matplotlib +numpy diff --git a/setup.py b/setup.py index 8119fd9..ac987e5 100644 --- a/setup.py +++ b/setup.py @@ -1,5 +1,5 @@ #!/usr/bin/env python - +"""Self-explanatory.""" from setuptools import setup setup( diff --git a/tests.py b/tests.py new file mode 100644 index 0000000..04f0418 --- /dev/null +++ b/tests.py @@ -0,0 +1,143 @@ +import unittest +from pprint import pprint + +from copycat import Copycat + +# TODO: update test cases to use entropy + +# CHI2 values for n degrees freedom +_chiSquared_table = { + 1:3.841, + 2:5.991, + 3:7.815, + 4:9.488, + 5:11.071, + 6:12.592, + 7:14.067, + 8:15.507, + 9:16.919, + 10:18.307 + } + +class TestCopycat(unittest.TestCase): + + def setUp(self): + self.longMessage = True # new in Python 2.7 + + def assertProbabilitiesLookRoughlyLike(self, actual, expected, iterations): + + answerKeys = set(list(actual.keys()) + list(expected.keys())) + degreesFreedom = len(answerKeys) + chiSquared = 0 + + get_count = lambda k, d : d[k]['count'] if k in d else 0 + + for k in answerKeys: + E = get_count(k, expected) + O = get_count(k, actual) + if E == 0: + print('Warning! Expected 0 counts of {}, but got {}'.format(k, O)) + else: + chiSquared += (O - E) ** 2 / E + + if chiSquared >= _chiSquared_table[degreesFreedom]: + self.fail('Significant different between expected and actual answer distributions: \n' + + 'Chi2 value: {} with {} degrees of freedom'.format(chiSquared, degreesFreedom)) + + def run_testcase(self, initial, modified, target, iterations, expected): + print('expected:') + pprint(expected) + actual = Copycat().run(initial, modified, target, iterations) + print('actual:') + pprint(actual) + self.assertEqual(sum(a['count'] for a in list(actual.values())), iterations) + self.assertProbabilitiesLookRoughlyLike(actual, expected, iterations) + + def test_simple_cases(self): + self.run_testcase('abc', 'abd', 'efg', 30, + {'dfg': {'avgtemp': 72.37092377767368, 'avgtime': 475.0, 'count': 1}, + 'efd': {'avgtemp': 49.421147725239024, 'avgtime': 410.5, 'count': 2}, + 'efh': {'avgtemp': 19.381658717913258, + 'avgtime': 757.1851851851852, + 'count': 27}}) + self.run_testcase('abc', 'abd', 'ijk', 30, + {'ijd': {'avgtemp': 14.691978036611559, 'avgtime': 453.0, 'count': 1}, + 'ijl': {'avgtemp': 22.344023091153964, + 'avgtime': 742.1428571428571, + 'count': 28}, + 'jjk': {'avgtemp': 11.233344554288019, 'avgtime': 595.0, 'count': 1}}) + + + def test_abc_xyz(self): + self.run_testcase('abc', 'abd', 'xyz', 100, + {'dyz': {'avgtemp': 16.78130739435325, 'avgtime': 393.0, 'count': 1}, + 'wyz': {'avgtemp': 26.100450643627426, 'avgtime': 4040.0, 'count': 2}, + 'xyd': {'avgtemp': 21.310415433987586, + 'avgtime': 5592.277777777777, + 'count': 90}, + 'xyz': {'avgtemp': 23.798124933747882, 'avgtime': 3992.0, 'count': 1}, + 'yyz': {'avgtemp': 27.137975077133788, 'avgtime': 4018.5, 'count': 6}}) + + def test_ambiguous_case(self): + self.run_testcase('abc', 'abd', 'ijkk', 100, + {'ijd': {'avgtemp': 55.6767488926397, 'avgtime': 948.0, 'count': 1}, + 'ijkd': {'avgtemp': 78.09357723857647, 'avgtime': 424.5, 'count': 2}, + 'ijkk': {'avgtemp': 68.54252699118226, 'avgtime': 905.5, 'count': 2}, + 'ijkkk': {'avgtemp': 21.75444235750483, + 'avgtime': 2250.3333333333335, + 'count': 3}, + 'ijkl': {'avgtemp': 38.079858245918466, + 'avgtime': 1410.2391304347825, + 'count': 46}, + 'ijll': {'avgtemp': 27.53845719945872, + 'avgtime': 1711.8863636363637, + 'count': 44}, + 'jjkk': {'avgtemp': 75.76606718990365, 'avgtime': 925.0, 'count': 2}}) + + def test_mrrjjj(self): + self.run_testcase('abc', 'abd', 'mrrjjj', 30, + {'mrrjjd': {'avgtemp': 44.46354725386579, 'avgtime': 1262.0, 'count': 1}, + 'mrrjjjj': {'avgtemp': 17.50702440140412, 'avgtime': 1038.375, 'count': 8}, + 'mrrjjk': {'avgtemp': 55.189156978290264, + 'avgtime': 1170.6363636363637, + 'count': 11}, + 'mrrkkk': {'avgtemp': 43.709349775080746, 'avgtime': 1376.2, 'count': 10}}) + + ''' + Below are examples of improvements that could be made to copycat. + + def test_elongation(self): + # This isn't remotely what a human would say. + self.run_testcase('abc', 'aabbcc', 'milk', 30, + {'lilk': {'avgtemp': 68.18128407669258, + 'avgtime': 1200.6666666666667, + 'count': 3}, + 'mikj': {'avgtemp': 57.96973195905564, + 'avgtime': 1236.888888888889, + 'count': 9}, + 'milb': {'avgtemp': 79.98413990245763, 'avgtime': 255.0, 'count': 1}, + 'milj': {'avgtemp': 64.95289549955349, 'avgtime': 1192.4, 'count': 15}, + 'milk': {'avgtemp': 66.11387816293755, 'avgtime': 1891.5, 'count': 2}}) + def test_repairing_successor_sequence(self): + # This isn't remotely what a human would say. + self.run_testcase('aba', 'abc', 'xyx', 30, + {'cyx': {'avgtemp': 82.10555880340601, 'avgtime': 2637.0, 'count': 2}, + 'xc': {'avgtemp': 73.98845045179358, 'avgtime': 5459.5, 'count': 2}, + 'xyc': {'avgtemp': 77.1384941639991, + 'avgtime': 4617.434782608696, + 'count': 23}, + 'xyx': {'avgtemp': 74.39287653046891, 'avgtime': 3420.0, 'count': 3}}) + def test_nonsense(self): + self.run_testcase('cat', 'dog', 'cake', 10, { + 'cakg': {'count': 99, 'avgtemp': 70}, + 'gake': {'count': 1, 'avgtemp': 59}, + }) + self.run_testcase('cat', 'dog', 'kitten', 10, { + 'kitteg': {'count': 96, 'avgtemp': 66}, + 'kitten': {'count': 4, 'avgtemp': 68}, + }) + ''' + + +if __name__ == '__main__': + unittest.main()