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
.coverage
.tox
.log
# Other filesystems
.svn
@ -26,3 +27,6 @@ pip-log.txt
# Editors
.*.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
=========
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?

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 .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."""
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:

View File

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

View File

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

View File

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

View File

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

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

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 .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):

View File

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

View File

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

View File

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

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

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
"""Self-explanatory."""
from setuptools import 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()