Merge branch 'develop'

This commit is contained in:
LSaldyt
2017-10-16 13:43:30 -07:00
28 changed files with 552 additions and 254 deletions

4
.gitignore vendored
View File

@ -19,6 +19,7 @@ pip-log.txt
# Unit test / coverage reports # Unit test / coverage reports
.coverage .coverage
.tox .tox
.log
# Other filesystems # Other filesystems
.svn .svn
@ -26,3 +27,6 @@ pip-log.txt
# Editors # Editors
.*.swp .*.swp
# Output
output/*

View File

@ -0,0 +1,6 @@
{
"cells": [],
"metadata": {},
"nbformat": 4,
"nbformat_minor": 2
}

12
.travis.yml Normal file
View File

@ -0,0 +1,12 @@
language: python
branches:
only:
- "develop"
- "master"
python:
- "3.6"
install:
- pip3 install -r requirements.txt
script:
- python3 tests.py

81
Copycat.ipynb Normal file
View File

@ -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
}

View File

@ -1,8 +1,15 @@
co.py.cat 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. 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). 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). 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. 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?

View File

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

View File

@ -1 +1,3 @@
from .copycat import Copycat, Reporter # noqa from .copycat import Copycat, Reporter # noqa
from .plot import plot_answers
from .io import save_answers

View File

@ -72,11 +72,14 @@ def __structureVsStructure(structure1, weight1, structure2, weight2):
"""Return true if the first structure comes out stronger than the second.""" """Return true if the first structure comes out stronger than the second."""
ctx = structure1.ctx ctx = structure1.ctx
random = ctx.random random = ctx.random
# TODO: use entropy
temperature = ctx.temperature temperature = ctx.temperature
structure1.updateStrength() structure1.updateStrength()
structure2.updateStrength() structure2.updateStrength()
# TODO: use entropy
weightedStrength1 = temperature.getAdjustedValue( weightedStrength1 = temperature.getAdjustedValue(
structure1.totalStrength * weight1) structure1.totalStrength * weight1)
# TODO: use entropy
weightedStrength2 = temperature.getAdjustedValue( weightedStrength2 = temperature.getAdjustedValue(
structure2.totalStrength * weight2) structure2.totalStrength * weight2)
return random.weighted_greater_than(weightedStrength1, weightedStrength2) return random.weighted_greater_than(weightedStrength1, weightedStrength2)
@ -120,6 +123,15 @@ def __slippability(ctx, conceptMappings):
@codelet('breaker') @codelet('breaker')
def breaker(ctx, codelet): 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 random = ctx.random
temperature = ctx.temperature temperature = ctx.temperature
workspace = ctx.workspace workspace = ctx.workspace
@ -138,6 +150,8 @@ def breaker(ctx, codelet):
if structure.source.group == structure.destination.group: if structure.source.group == structure.destination.group:
breakObjects += [structure.source.group] breakObjects += [structure.source.group]
# Break all the objects or none of them; this matches the Java # 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: for structure in breakObjects:
breakProbability = temperature.getAdjustedProbability( breakProbability = temperature.getAdjustedProbability(
structure.totalStrength / 100.0) structure.totalStrength / 100.0)
@ -150,17 +164,18 @@ def breaker(ctx, codelet):
def chooseRelevantDescriptionByActivation(ctx, workspaceObject): def chooseRelevantDescriptionByActivation(ctx, workspaceObject):
random = ctx.random random = ctx.random
descriptions = workspaceObject.relevantDescriptions() descriptions = workspaceObject.relevantDescriptions()
weights = [description.descriptor.activation weights = [description.descriptor.activation for description in descriptions]
for description in descriptions]
return random.weighted_choice(descriptions, weights) return random.weighted_choice(descriptions, weights)
def similarPropertyLinks(ctx, slip_node): def similarPropertyLinks(ctx, slip_node):
random = ctx.random random = ctx.random
# TODO: use entropy
temperature = ctx.temperature temperature = ctx.temperature
result = [] result = []
for slip_link in slip_node.propertyLinks: for slip_link in slip_node.propertyLinks:
association = slip_link.degreeOfAssociation() / 100.0 association = slip_link.degreeOfAssociation() / 100.0
# TODO:use entropy
probability = temperature.getAdjustedProbability(association) probability = temperature.getAdjustedProbability(association)
if random.coinFlip(probability): if random.coinFlip(probability):
result += [slip_link] result += [slip_link]
@ -183,7 +198,7 @@ def bottom_up_description_scout(ctx, codelet):
sliplinks = similarPropertyLinks(ctx, description.descriptor) sliplinks = similarPropertyLinks(ctx, description.descriptor)
assert sliplinks assert sliplinks
weights = [sliplink.degreeOfAssociation() * sliplink.destination.activation weights = [sliplink.degreeOfAssociation() * sliplink.destination.activation
for sliplink in sliplinks] for sliplink in sliplinks]
chosen = random.weighted_choice(sliplinks, weights) chosen = random.weighted_choice(sliplinks, weights)
chosenProperty = chosen.destination chosenProperty = chosen.destination
coderack.proposeDescription(chosenObject, chosenProperty.category(), coderack.proposeDescription(chosenObject, chosenProperty.category(),
@ -211,11 +226,13 @@ def top_down_description_scout(ctx, codelet):
def description_strength_tester(ctx, codelet): def description_strength_tester(ctx, codelet):
coderack = ctx.coderack coderack = ctx.coderack
random = ctx.random random = ctx.random
# TODO: use entropy
temperature = ctx.temperature temperature = ctx.temperature
description = codelet.arguments[0] description = codelet.arguments[0]
description.descriptor.buffer = 100.0 description.descriptor.buffer = 100.0
description.updateStrength() description.updateStrength()
strength = description.totalStrength strength = description.totalStrength
# TODO: use entropy
probability = temperature.getAdjustedProbability(strength / 100.0) probability = temperature.getAdjustedProbability(strength / 100.0)
assert random.coinFlip(probability) assert random.coinFlip(probability)
coderack.newCodelet('description-builder', strength, [description]) coderack.newCodelet('description-builder', strength, [description])
@ -295,11 +312,12 @@ def rule_scout(ctx, codelet):
coderack = ctx.coderack coderack = ctx.coderack
random = ctx.random random = ctx.random
slipnet = ctx.slipnet slipnet = ctx.slipnet
# TODO: use entropy
temperature = ctx.temperature temperature = ctx.temperature
workspace = ctx.workspace workspace = ctx.workspace
assert workspace.numberOfUnreplacedObjects() == 0 assert workspace.numberOfUnreplacedObjects() == 0
changedObjects = [o for o in workspace.initial.objects if o.changed] 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 there are no changed objects, propose a rule with no changes
if not changedObjects: if not changedObjects:
return coderack.proposeRule(None, None, None, None) return coderack.proposeRule(None, None, None, None)
@ -329,10 +347,11 @@ def rule_scout(ctx, codelet):
if targetObject.described(node): if targetObject.described(node):
if targetObject.distinguishingDescriptor(node): if targetObject.distinguishingDescriptor(node):
newList += [node] newList += [node]
objectList = newList # surely this should be += objectList = newList # surely this should be +=
# "union of this and distinguishing descriptors" # "union of this and distinguishing descriptors"
assert objectList assert objectList
# use conceptual depth to choose a description # use conceptual depth to choose a description
# TODO: use entropy
weights = [ weights = [
temperature.getAdjustedValue(node.conceptualDepth) temperature.getAdjustedValue(node.conceptualDepth)
for node in objectList for node in objectList
@ -344,6 +363,7 @@ def rule_scout(ctx, codelet):
objectList += [changed.replacement.relation] objectList += [changed.replacement.relation]
objectList += [changed.replacement.objectFromModified.getDescriptor( objectList += [changed.replacement.objectFromModified.getDescriptor(
slipnet.letterCategory)] slipnet.letterCategory)]
# TODO: use entropy
# use conceptual depth to choose a relation # use conceptual depth to choose a relation
weights = [ weights = [
temperature.getAdjustedValue(node.conceptualDepth) temperature.getAdjustedValue(node.conceptualDepth)
@ -358,9 +378,11 @@ def rule_scout(ctx, codelet):
def rule_strength_tester(ctx, codelet): def rule_strength_tester(ctx, codelet):
coderack = ctx.coderack coderack = ctx.coderack
random = ctx.random random = ctx.random
# TODO: use entropy
temperature = ctx.temperature temperature = ctx.temperature
rule = codelet.arguments[0] rule = codelet.arguments[0]
rule.updateStrength() rule.updateStrength()
# TODO: use entropy
probability = temperature.getAdjustedProbability(rule.totalStrength / 100.0) probability = temperature.getAdjustedProbability(rule.totalStrength / 100.0)
if random.coinFlip(probability): if random.coinFlip(probability):
coderack.newCodelet('rule-builder', rule.totalStrength, [rule]) coderack.newCodelet('rule-builder', rule.totalStrength, [rule])
@ -393,8 +415,8 @@ def replacement_finder(ctx, codelet):
relation = relations[diff] relation = relations[diff]
else: else:
relation = None relation = None
letterOfInitialString.replacement = Replacement(ctx, letterOfInitialString.replacement = Replacement(ctx, letterOfInitialString,
letterOfInitialString, letterOfModifiedString, relation) letterOfModifiedString, relation)
if relation != slipnet.sameness: if relation != slipnet.sameness:
letterOfInitialString.changed = True letterOfInitialString.changed = True
workspace.changedObject = letterOfInitialString workspace.changedObject = letterOfInitialString
@ -437,8 +459,8 @@ def top_down_bond_scout__direction(ctx, codelet):
coderack = ctx.coderack coderack = ctx.coderack
slipnet = ctx.slipnet slipnet = ctx.slipnet
direction = codelet.arguments[0] direction = codelet.arguments[0]
source = __getScoutSource(ctx, source = __getScoutSource(ctx, direction, formulas.localDirectionCategoryRelevance,
direction, formulas.localDirectionCategoryRelevance, 'bond') 'bond')
destination = chooseDirectedNeighbor(ctx, source, direction) destination = chooseDirectedNeighbor(ctx, source, direction)
assert destination assert destination
logging.info('to object: %s', 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): def bond_strength_tester(ctx, codelet):
coderack = ctx.coderack coderack = ctx.coderack
random = ctx.random random = ctx.random
# TODO: use entropy
temperature = ctx.temperature temperature = ctx.temperature
bond = codelet.arguments[0] bond = codelet.arguments[0]
__showWhichStringObjectIsFrom(bond) __showWhichStringObjectIsFrom(bond)
bond.updateStrength() bond.updateStrength()
strength = bond.totalStrength strength = bond.totalStrength
# TODO: use entropy
probability = temperature.getAdjustedProbability(strength / 100.0) probability = temperature.getAdjustedProbability(strength / 100.0)
logging.info('bond strength = %d for %s', strength, bond) logging.info('bond strength = %d for %s', strength, bond)
assert random.coinFlip(probability) assert random.coinFlip(probability)
@ -503,7 +527,7 @@ def bond_builder(ctx, codelet):
if incompatibleCorrespondences: if incompatibleCorrespondences:
logging.info("trying to break incompatible correspondences") logging.info("trying to break incompatible correspondences")
assert __fight(bond, 2.0, incompatibleCorrespondences, 3.0) assert __fight(bond, 2.0, incompatibleCorrespondences, 3.0)
#assert __fightIncompatibles(incompatibleCorrespondences, # assert __fightIncompatibles(incompatibleCorrespondences,
# bond, 'correspondences', 2.0, 3.0) # bond, 'correspondences', 2.0, 3.0)
for incompatible in incompatibleBonds: for incompatible in incompatibleBonds:
incompatible.break_the_structure() incompatible.break_the_structure()
@ -693,7 +717,7 @@ def top_down_group_scout__direction(ctx, codelet):
direction, bondFacet) direction, bondFacet)
#noinspection PyStringFormat # noinspection PyStringFormat
@codelet('group-scout--whole-string') @codelet('group-scout--whole-string')
def group_scout__whole_string(ctx, codelet): def group_scout__whole_string(ctx, codelet):
coderack = ctx.coderack coderack = ctx.coderack
@ -739,12 +763,14 @@ def group_strength_tester(ctx, codelet):
coderack = ctx.coderack coderack = ctx.coderack
random = ctx.random random = ctx.random
slipnet = ctx.slipnet slipnet = ctx.slipnet
# TODO: use entropy
temperature = ctx.temperature temperature = ctx.temperature
# update strength value of the group # update strength value of the group
group = codelet.arguments[0] group = codelet.arguments[0]
__showWhichStringObjectIsFrom(group) __showWhichStringObjectIsFrom(group)
group.updateStrength() group.updateStrength()
strength = group.totalStrength strength = group.totalStrength
# TODO: use entropy
probability = temperature.getAdjustedProbability(strength / 100.0) probability = temperature.getAdjustedProbability(strength / 100.0)
if random.coinFlip(probability): if random.coinFlip(probability):
# it is strong enough - post builder & activate nodes # it is strong enough - post builder & activate nodes
@ -860,6 +886,7 @@ def __getCutoffWeights(bondDensity):
def rule_translator(ctx, codelet): def rule_translator(ctx, codelet):
coderack = ctx.coderack coderack = ctx.coderack
random = ctx.random random = ctx.random
# TODO: use entropy
temperature = ctx.temperature temperature = ctx.temperature
workspace = ctx.workspace workspace = ctx.workspace
assert workspace.rule assert workspace.rule
@ -872,6 +899,7 @@ def rule_translator(ctx, codelet):
bondDensity = min(bondDensity, 1.0) bondDensity = min(bondDensity, 1.0)
weights = __getCutoffWeights(bondDensity) weights = __getCutoffWeights(bondDensity)
cutoff = 10.0 * random.weighted_choice(list(range(1, 11)), weights) cutoff = 10.0 * random.weighted_choice(list(range(1, 11)), weights)
# TODO: use entropy
if cutoff >= temperature.actual_value: if cutoff >= temperature.actual_value:
result = workspace.rule.buildTranslatedRule() result = workspace.rule.buildTranslatedRule()
if result is not None: if result is not None:
@ -908,11 +936,11 @@ def bottom_up_correspondence_scout(ctx, codelet):
and m.initialDescriptionType != slipnet.bondFacet] and m.initialDescriptionType != slipnet.bondFacet]
initialDescriptionTypes = [m.initialDescriptionType for m in opposites] initialDescriptionTypes = [m.initialDescriptionType for m in opposites]
flipTargetObject = False flipTargetObject = False
if (objectFromInitial.spansString() and if (objectFromInitial.spansString() and
objectFromTarget.spansString() and objectFromTarget.spansString() and
slipnet.directionCategory in initialDescriptionTypes slipnet.directionCategory in initialDescriptionTypes
and all(m.label == slipnet.opposite for m in opposites) # unreached? and all(m.label == slipnet.opposite for m in opposites) # unreached?
and slipnet.opposite.activation != 100.0): and slipnet.opposite.activation != 100.0):
objectFromTarget = objectFromTarget.flippedVersion() objectFromTarget = objectFromTarget.flippedVersion()
conceptMappings = formulas.getMappings( conceptMappings = formulas.getMappings(
objectFromInitial, objectFromTarget, objectFromInitial, objectFromTarget,
@ -928,6 +956,7 @@ def important_object_correspondence_scout(ctx, codelet):
coderack = ctx.coderack coderack = ctx.coderack
random = ctx.random random = ctx.random
slipnet = ctx.slipnet slipnet = ctx.slipnet
# TODO: use entropy
temperature = ctx.temperature temperature = ctx.temperature
workspace = ctx.workspace workspace = ctx.workspace
objectFromInitial = chooseUnmodifiedObject(ctx, 'relativeImportance', objectFromInitial = chooseUnmodifiedObject(ctx, 'relativeImportance',
@ -935,6 +964,7 @@ def important_object_correspondence_scout(ctx, codelet):
assert objectFromInitial is not None assert objectFromInitial is not None
descriptors = objectFromInitial.relevantDistinguishingDescriptors() descriptors = objectFromInitial.relevantDistinguishingDescriptors()
# choose descriptor by conceptual depth # choose descriptor by conceptual depth
# TODO: use entropy
weights = [temperature.getAdjustedValue(n.conceptualDepth) for n in descriptors] weights = [temperature.getAdjustedValue(n.conceptualDepth) for n in descriptors]
slipnode = random.weighted_choice(descriptors, weights) slipnode = random.weighted_choice(descriptors, weights)
assert slipnode assert slipnode
@ -967,11 +997,11 @@ def important_object_correspondence_scout(ctx, codelet):
and m.initialDescriptionType != slipnet.bondFacet] and m.initialDescriptionType != slipnet.bondFacet]
initialDescriptionTypes = [m.initialDescriptionType for m in opposites] initialDescriptionTypes = [m.initialDescriptionType for m in opposites]
flipTargetObject = False flipTargetObject = False
if (objectFromInitial.spansString() if (objectFromInitial.spansString()
and objectFromTarget.spansString() and objectFromTarget.spansString()
and slipnet.directionCategory in initialDescriptionTypes and slipnet.directionCategory in initialDescriptionTypes
and all(m.label == slipnet.opposite for m in opposites) # unreached? and all(m.label == slipnet.opposite for m in opposites) # unreached?
and slipnet.opposite.activation != 100.0): and slipnet.opposite.activation != 100.0):
objectFromTarget = objectFromTarget.flippedVersion() objectFromTarget = objectFromTarget.flippedVersion()
conceptMappings = formulas.getMappings( conceptMappings = formulas.getMappings(
objectFromInitial, objectFromTarget, objectFromInitial, objectFromTarget,
@ -986,6 +1016,7 @@ def important_object_correspondence_scout(ctx, codelet):
def correspondence_strength_tester(ctx, codelet): def correspondence_strength_tester(ctx, codelet):
coderack = ctx.coderack coderack = ctx.coderack
random = ctx.random random = ctx.random
# TODO: use entropy
temperature = ctx.temperature temperature = ctx.temperature
workspace = ctx.workspace workspace = ctx.workspace
correspondence = codelet.arguments[0] correspondence = codelet.arguments[0]
@ -998,6 +1029,7 @@ def correspondence_strength_tester(ctx, codelet):
objectFromTarget.flipped_version()))) objectFromTarget.flipped_version())))
correspondence.updateStrength() correspondence.updateStrength()
strength = correspondence.totalStrength strength = correspondence.totalStrength
# TODO: use entropy
probability = temperature.getAdjustedProbability(strength / 100.0) probability = temperature.getAdjustedProbability(strength / 100.0)
if random.coinFlip(probability): if random.coinFlip(probability):
# activate some concepts # activate some concepts
@ -1051,8 +1083,8 @@ def correspondence_builder(ctx, codelet):
# if there is an incompatible bond then fight against it # if there is an incompatible bond then fight against it
initial = correspondence.objectFromInitial initial = correspondence.objectFromInitial
target = correspondence.objectFromTarget target = correspondence.objectFromTarget
if (initial.leftmost or initial.rightmost and if (initial.leftmost or initial.rightmost and
target.leftmost or target.rightmost): target.leftmost or target.rightmost):
# search for the incompatible bond # search for the incompatible bond
incompatibleBond = correspondence.getIncompatibleBond() incompatibleBond = correspondence.getIncompatibleBond()
if incompatibleBond: if incompatibleBond:

View File

@ -68,6 +68,7 @@ class Coderack(object):
self.postBottomUpCodelets() self.postBottomUpCodelets()
def probabilityOfPosting(self, codeletName): def probabilityOfPosting(self, codeletName):
# TODO: use entropy
temperature = self.ctx.temperature temperature = self.ctx.temperature
workspace = self.ctx.workspace workspace = self.ctx.workspace
if codeletName == 'breaker': if codeletName == 'breaker':
@ -83,6 +84,7 @@ class Coderack(object):
if 'correspondence' in codeletName: if 'correspondence' in codeletName:
return workspace.interStringUnhappiness / 100.0 return workspace.interStringUnhappiness / 100.0
if 'description' in codeletName: if 'description' in codeletName:
# TODO: use entropy
return (temperature.value() / 100.0) ** 2 return (temperature.value() / 100.0) ** 2
return workspace.intraStringUnhappiness / 100.0 return workspace.intraStringUnhappiness / 100.0
@ -155,12 +157,15 @@ class Coderack(object):
def __postBottomUpCodelets(self, codeletName): def __postBottomUpCodelets(self, codeletName):
random = self.ctx.random random = self.ctx.random
# TODO: use entropy
temperature = self.ctx.temperature temperature = self.ctx.temperature
probability = self.probabilityOfPosting(codeletName) probability = self.probabilityOfPosting(codeletName)
howMany = self.howManyToPost(codeletName) howMany = self.howManyToPost(codeletName)
urgency = 3 urgency = 3
if codeletName == 'breaker': if codeletName == 'breaker':
urgency = 1 urgency = 1
# TODO: use entropy
if temperature.value() < 25.0 and 'translator' in codeletName: if temperature.value() < 25.0 and 'translator' in codeletName:
urgency = 5 urgency = 5
for _ in range(howMany): for _ in range(howMany):
@ -285,8 +290,11 @@ class Coderack(object):
def chooseCodeletToRun(self): def chooseCodeletToRun(self):
random = self.ctx.random random = self.ctx.random
# TODO: use entropy
temperature = self.ctx.temperature temperature = self.ctx.temperature
assert self.codelets assert self.codelets
# TODO: use entropy
scale = (100.0 - temperature.value() + 10.0) / 15.0 scale = (100.0 - temperature.value() + 10.0) / 15.0
chosen = random.weighted_choice(self.codelets, [codelet.urgency ** scale for codelet in self.codelets]) chosen = random.weighted_choice(self.codelets, [codelet.urgency ** scale for codelet in self.codelets])
self.removeCodelet(chosen) self.removeCodelet(chosen)

View File

@ -4,7 +4,6 @@ from .slipnet import Slipnet
from .temperature import Temperature from .temperature import Temperature
from .workspace import Workspace from .workspace import Workspace
class Reporter(object): class Reporter(object):
"""Do-nothing base class for defining new reporter types""" """Do-nothing base class for defining new reporter types"""
def report_answer(self, answer): def report_answer(self, answer):
@ -16,7 +15,7 @@ class Reporter(object):
def report_slipnet(self, slipnet): def report_slipnet(self, slipnet):
pass pass
def report_temperature(self, temperature): def report_temperature(self, temperature): #TODO: use entropy
pass pass
def report_workspace(self, workspace): def report_workspace(self, workspace):
@ -28,19 +27,19 @@ class Copycat(object):
self.coderack = Coderack(self) self.coderack = Coderack(self)
self.random = Randomness(rng_seed) self.random = Randomness(rng_seed)
self.slipnet = Slipnet() self.slipnet = Slipnet()
self.temperature = Temperature() self.temperature = Temperature() # TODO: use entropy
self.workspace = Workspace(self) self.workspace = Workspace(self)
self.reporter = reporter or Reporter() self.reporter = reporter or Reporter()
def mainLoop(self, lastUpdate): def mainLoop(self, lastUpdate):
currentTime = self.coderack.codeletsRun currentTime = self.coderack.codeletsRun
self.temperature.tryUnclamp(currentTime) self.temperature.tryUnclamp(currentTime) # TODO: use entropy
# Every 15 codelets, we update the workspace. # Every 15 codelets, we update the workspace.
if currentTime >= lastUpdate + 15: if currentTime >= lastUpdate + 15:
self.workspace.updateEverything() self.workspace.updateEverything()
self.coderack.updateCodelets() self.coderack.updateCodelets()
self.slipnet.update(self.random) self.slipnet.update(self.random)
self.temperature.update(self.workspace.getUpdatedTemperature()) self.temperature.update(self.workspace.getUpdatedTemperature()) # TODO: use entropy
lastUpdate = currentTime lastUpdate = currentTime
self.reporter.report_slipnet(self.slipnet) self.reporter.report_slipnet(self.slipnet)
self.coderack.chooseAndRunCodelet() self.coderack.chooseAndRunCodelet()
@ -53,14 +52,14 @@ class Copycat(object):
"""Run a trial of the copycat algorithm""" """Run a trial of the copycat algorithm"""
self.coderack.reset() self.coderack.reset()
self.slipnet.reset() self.slipnet.reset()
self.temperature.reset() self.temperature.reset() # TODO: use entropy
self.workspace.reset() self.workspace.reset()
lastUpdate = float('-inf') lastUpdate = float('-inf')
while self.workspace.finalAnswer is None: while self.workspace.finalAnswer is None:
lastUpdate = self.mainLoop(lastUpdate) lastUpdate = self.mainLoop(lastUpdate)
answer = { answer = {
'answer': self.workspace.finalAnswer, 'answer': self.workspace.finalAnswer,
'temp': self.temperature.last_unclamped_value, 'temp': self.temperature.last_unclamped_value, # TODO: use entropy
'time': self.coderack.codeletsRun, 'time': self.coderack.codeletsRun,
} }
self.reporter.report_answer(answer) self.reporter.report_answer(answer)
@ -68,16 +67,27 @@ class Copycat(object):
def run(self, initial, modified, target, iterations): def run(self, initial, modified, target, iterations):
self.workspace.resetWithStrings(initial, modified, target) 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 = {} answers = {}
for i in range(iterations): for i in range(iterations):
answer = self.runTrial() answer = self.runTrial()
d = answers.setdefault(answer['answer'], { d = answers.setdefault(answer['answer'], {
'count': 0, 'count': 0,
'sumtemp': 0, 'sumtemp': 0, # TODO: use entropy
'sumtime': 0 'sumtime': 0
}) })
d['count'] += 1 d['count'] += 1
d['sumtemp'] += answer['temp'] d['sumtemp'] += answer['temp'] # TODO: use entropy
d['sumtime'] += answer['time'] d['sumtime'] += answer['time']
for answer, d in answers.items(): for answer, d in answers.items():

View File

@ -63,7 +63,7 @@ class CursesReporter(Reporter):
coderackHeight = height - upperHeight - answersHeight coderackHeight = height - upperHeight - answersHeight
self.focusOnSlipnet = focus_on_slipnet self.focusOnSlipnet = focus_on_slipnet
self.fpsGoal = fps_goal 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.upperWindow = SafeSubwindow(window, upperHeight, width-5, 0, 5)
self.coderackWindow = SafeSubwindow(window, coderackHeight, width-5, upperHeight, 5) self.coderackWindow = SafeSubwindow(window, coderackHeight, width-5, upperHeight, 5)
self.answersWindow = SafeSubwindow(window, answersHeight, width-5, upperHeight + coderackHeight, 5) self.answersWindow = SafeSubwindow(window, answersHeight, width-5, upperHeight + coderackHeight, 5)
@ -239,6 +239,7 @@ class CursesReporter(Reporter):
w.border() w.border()
w.refresh() w.refresh()
#TODO: use entropy
def report_temperature(self, temperature): def report_temperature(self, temperature):
self.do_keyboard_shortcuts() self.do_keyboard_shortcuts()
w = self.temperatureWindow w = self.temperatureWindow

View File

@ -96,6 +96,7 @@ class Group(WorkspaceObject):
support = self.localSupport() / 100.0 support = self.localSupport() / 100.0
activation = slipnet.length.activation / 100.0 activation = slipnet.length.activation / 100.0
supportedActivation = (support * activation) ** exp supportedActivation = (support * activation) ** exp
#TODO: use entropy
return temperature.getAdjustedProbability(supportedActivation) return temperature.getAdjustedProbability(supportedActivation)
def flippedVersion(self): def flippedVersion(self):
@ -130,6 +131,7 @@ class Group(WorkspaceObject):
cubedlength = length ** 3 cubedlength = length ** 3
fred = cubedlength * (100.0 - slipnet.length.activation) / 100.0 fred = cubedlength * (100.0 - slipnet.length.activation) / 100.0
probability = 0.5 ** fred probability = 0.5 ** fred
#TODO: use entropy
value = temperature.getAdjustedProbability(probability) value = temperature.getAdjustedProbability(probability)
if value < 0.06: if value < 0.06:
value = 0.0 value = 0.0

9
copycat/io.py Normal file
View File

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

20
copycat/plot.py Normal file
View File

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

View File

@ -1,9 +1,81 @@
import math 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): class Temperature(object):
def __init__(self): def __init__(self):
self.reset() 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): def reset(self):
self.actual_value = 100.0 self.actual_value = 100.0
@ -34,12 +106,13 @@ class Temperature(object):
return value ** (((100.0 - self.value()) / 30.0) + 0.5) return value ** (((100.0 - self.value()) / 30.0) + 0.5)
def getAdjustedProbability(self, value): def getAdjustedProbability(self, value):
if value == 0 or value == 0.5 or self.value() == 0: temp = self.value()
return value prob = value
if value < 0.5: return self._adjustmentFormulas[self.adjustmentType](temp, prob)
return 1.0 - self.getAdjustedProbability(1.0 - value)
coldness = 100.0 - self.value() def useAdj(self, adj):
a = math.sqrt(coldness) print('Changing to adjustment formula {}'.format(adj))
c = (10 - a) / 100 self.adjustmentType = adj
f = (c + 1) * value
return max(f, 0.5) def adj_formulas(self):
return self._adjustmentFormulas.keys()

View File

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

View File

@ -1,3 +1,6 @@
"""Workspace module."""
from . import formulas from . import formulas
from .bond import Bond from .bond import Bond
from .correspondence import Correspondence from .correspondence import Correspondence
@ -14,6 +17,7 @@ def __adjustUnhappiness(values):
class Workspace(object): class Workspace(object):
def __init__(self, ctx): def __init__(self, ctx):
"""To initialize the workspace."""
self.ctx = ctx self.ctx = ctx
self.totalUnhappiness = 0.0 self.totalUnhappiness = 0.0
self.intraStringUnhappiness = 0.0 self.intraStringUnhappiness = 0.0
@ -35,11 +39,21 @@ class Workspace(object):
self.changedObject = None self.changedObject = None
self.objects = [] self.objects = []
self.structures = [] self.structures = []
self.rule = None self.rule = None # Only one rule? : LSaldyt
self.initial = WorkspaceString(self.ctx, self.initialString) self.initial = WorkspaceString(self.ctx, self.initialString)
self.modified = WorkspaceString(self.ctx, self.modifiedString) self.modified = WorkspaceString(self.ctx, self.modifiedString)
self.target = WorkspaceString(self.ctx, self.targetString) 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): def assessUnhappiness(self):
self.intraStringUnhappiness = __adjustUnhappiness( self.intraStringUnhappiness = __adjustUnhappiness(
o.relativeImportance * o.intraStringUnhappiness o.relativeImportance * o.intraStringUnhappiness
@ -51,6 +65,7 @@ class Workspace(object):
o.relativeImportance * o.totalUnhappiness o.relativeImportance * o.totalUnhappiness
for o in self.objects) for o in self.objects)
# TODO: these 3 methods seem to be the same... are they? If so, Extract method.
def calculateIntraStringUnhappiness(self): def calculateIntraStringUnhappiness(self):
value = sum( value = sum(
o.relativeImportance * o.intraStringUnhappiness o.relativeImportance * o.intraStringUnhappiness
@ -82,7 +97,13 @@ class Workspace(object):
self.initial.updateIntraStringUnhappiness() self.initial.updateIntraStringUnhappiness()
self.target.updateIntraStringUnhappiness() self.target.updateIntraStringUnhappiness()
# TODO: use entropy
def getUpdatedTemperature(self): def getUpdatedTemperature(self):
'''
Calculation of global tolerance towards irrelevance
temp = weightedAverage(totalUnhappiness(.8), ruleWeakness(.2))
'''
self.calculateIntraStringUnhappiness() self.calculateIntraStringUnhappiness()
self.calculateInterStringUnhappiness() self.calculateInterStringUnhappiness()
self.calculateTotalUnhappiness() self.calculateTotalUnhappiness()
@ -97,7 +118,7 @@ class Workspace(object):
)) ))
def numberOfUnrelatedObjects(self): 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 objects = [o for o in self.objects
if o.string == self.initial or o.string == self.target] if o.string == self.initial or o.string == self.target]
objects = [o for o in objects if not o.spansString()] objects = [o for o in objects if not o.spansString()]
@ -115,21 +136,21 @@ class Workspace(object):
return len(objects) return len(objects)
def numberOfUnreplacedObjects(self): 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 objects = [o for o in self.objects
if o.string == self.initial and isinstance(o, Letter)] if o.string == self.initial and isinstance(o, Letter)]
objects = [o for o in objects if not o.replacement] objects = [o for o in objects if not o.replacement]
return len(objects) return len(objects)
def numberOfUncorrespondingObjects(self): 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 objects = [o for o in self.objects
if o.string == self.initial or o.string == self.target] if o.string == self.initial or o.string == self.target]
objects = [o for o in objects if not o.correspondence] objects = [o for o in objects if not o.correspondence]
return len(objects) return len(objects)
def numberOfBonds(self): 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)) return sum(1 for o in self.structures if isinstance(o, Bond))
def correspondences(self): def correspondences(self):

View File

@ -1,5 +1,6 @@
def __chooseObjectFromList(ctx, objects, attribute): def __chooseObjectFromList(ctx, objects, attribute):
# TODO: use entropy
random = ctx.random random = ctx.random
temperature = ctx.temperature temperature = ctx.temperature
weights = [ weights = [

View File

@ -2,7 +2,6 @@ from .description import Description
from .formulas import weightedAverage from .formulas import weightedAverage
from .workspaceStructure import WorkspaceStructure from .workspaceStructure import WorkspaceStructure
class WorkspaceObject(WorkspaceStructure): class WorkspaceObject(WorkspaceStructure):
# pylint: disable=too-many-instance-attributes # pylint: disable=too-many-instance-attributes
def __init__(self, workspaceString): def __init__(self, workspaceString):

View File

@ -38,7 +38,7 @@ class WorkspaceString(object):
return self.string[i] return self.string[i]
def updateRelativeImportance(self): 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) total = sum(o.rawImportance for o in self.objects)
if not total: if not total:
for o in self.objects: for o in self.objects:

0
input/.placeholder Normal file
View File

9
input/problems.csv Normal file
View File

@ -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
1 abc abd ijk
2 aabc aabd ijkk
3 abc abd kji
4 abc abd mrrjjj
5 abc abd rssttt
6 abc abd xyz
7 abc abd ijjkkk
8 rst rsu xyz
9 abc abd xyyzzz

View File

@ -0,0 +1,4 @@
abc,abd,ijk
aabc,aabd,ijkk
abc,abd,xyz
abc,abd,ijjkkk
1 abc abd ijk
2 aabc aabd ijkk
3 abc abd xyz
4 abc abd ijjkkk

49
main.py
View File

@ -1,21 +1,61 @@
#!/usr/bin/env python3 #!/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 argparse
import logging import logging
from copycat import Copycat, Reporter from copycat import Copycat, Reporter, plot_answers, save_answers
class SimpleReporter(Reporter): class SimpleReporter(Reporter):
"""Reports results from a single run."""
def report_answer(self, answer): def report_answer(self, answer):
"""Self-explanatory code."""
print('Answered %s (time %d, final temperature %.1f)' % ( print('Answered %s (time %d, final temperature %.1f)' % (
answer['answer'], answer['time'], answer['temp'], answer['answer'], answer['time'], answer['temp'],
)) ))
def main(): 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 = argparse.ArgumentParser()
parser.add_argument('--seed', type=int, default=None, help='Provide a deterministic seed for the RNG.') 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('--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('initial', type=str, help='A...')
parser.add_argument('modified', type=str, help='...is to B...') parser.add_argument('modified', type=str, help='...is to B...')
parser.add_argument('target', type=str, help='...as C is to... what?') 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']): 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'])) 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__': if __name__ == '__main__':
main() main()

0
output/.placeholder Normal file
View File

2
requirements.txt Normal file
View File

@ -0,0 +1,2 @@
matplotlib
numpy

View File

@ -1,5 +1,5 @@
#!/usr/bin/env python #!/usr/bin/env python
"""Self-explanatory."""
from setuptools import setup from setuptools import setup
setup( setup(

143
tests.py Normal file
View File

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