Add a curses front-end. This is looking good now!
And clean up some logic in `rule.py`. This is the place where the "brains" of Copycat really live, it seems; Copycat can only succeed at solving a puzzle if it can take the `Rule` it deduced and apply it to the target string to produce a new string. And it can only do that if the necessary *actions* have been programmed into `rule.py`. Right now, it explicitly can't deal with "rules" that involve more than one local change; that involve reversal; or more importantly, IIUC, rules that involve "ascending runs", because the idea of a successor-group is(?) known to the Slipnet but not to `rule.py`; the latter deals only in "strings", not in "workspace objects". This seems like a major flaw in the system... but maybe I'm missing something.
This commit is contained in:
23
README.md
23
README.md
@ -14,8 +14,8 @@ J Alan Brogan writes:
|
|||||||
> from [Melanie Mitchell](https://en.wikipedia.org/wiki/Melanie_Mitchell)'s book
|
> from [Melanie Mitchell](https://en.wikipedia.org/wiki/Melanie_Mitchell)'s book
|
||||||
> "[Analogy-Making as Perception](http://www.amazon.com/Analogy-Making-Perception-Computer-Melanie-Mitchell/dp/0262132893/ref=tmm_hrd_title_0?ie=UTF8&qid=1351269085&sr=1-3)".
|
> "[Analogy-Making as Perception](http://www.amazon.com/Analogy-Making-Perception-Computer-Melanie-Mitchell/dp/0262132893/ref=tmm_hrd_title_0?ie=UTF8&qid=1351269085&sr=1-3)".
|
||||||
|
|
||||||
Cloning the repo
|
Running the command-line program
|
||||||
----------------
|
--------------------------------
|
||||||
|
|
||||||
To clone the repo locally, run these commands:
|
To clone the repo locally, run these commands:
|
||||||
|
|
||||||
@ -40,6 +40,25 @@ ppqqrs: 4 (avg time 439.0, avg temp 37.3)
|
|||||||
The first number indicates how many times Copycat chose that string as its answer; higher means "more obvious".
|
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".
|
The last number indicates the average final temperature of the workspace; lower means "more elegant".
|
||||||
|
|
||||||
|
|
||||||
|
Running the `curses` interface
|
||||||
|
------------------------------
|
||||||
|
|
||||||
|
Follow the instructions to clone the repo as above, but then run `curses_main` instead of `main`:
|
||||||
|
|
||||||
|
```
|
||||||
|
$ git clone https://github.com/Quuxplusone/co.py.cat.git
|
||||||
|
$ cd co.py.cat/copycat
|
||||||
|
$ python curses_main.py abc abd ppqqrr
|
||||||
|
```
|
||||||
|
|
||||||
|
This script takes only three 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 number of iterations is always implicitly "infinite".
|
||||||
|
To kill the program, hit Ctrl+C.
|
||||||
|
|
||||||
|
|
||||||
Installing the module
|
Installing the module
|
||||||
---------------------
|
---------------------
|
||||||
|
|
||||||
|
|||||||
@ -854,8 +854,9 @@ def rule_translator(ctx, codelet):
|
|||||||
weights = __getCutoffWeights(bondDensity)
|
weights = __getCutoffWeights(bondDensity)
|
||||||
cutoff = 10.0 * random.weighted_choice(range(1, 11), weights)
|
cutoff = 10.0 * random.weighted_choice(range(1, 11), weights)
|
||||||
if cutoff >= temperature.actual_value:
|
if cutoff >= temperature.actual_value:
|
||||||
if workspace.rule.buildTranslatedRule():
|
result = workspace.rule.buildTranslatedRule()
|
||||||
workspace.foundAnswer = True
|
if result is not None:
|
||||||
|
workspace.finalAnswer = result
|
||||||
else:
|
else:
|
||||||
temperature.clampUntil(coderack.codeletsRun + 100)
|
temperature.clampUntil(coderack.codeletsRun + 100)
|
||||||
|
|
||||||
|
|||||||
@ -47,42 +47,43 @@ class Copycat(object):
|
|||||||
self.reporter.report_temperature(self.temperature)
|
self.reporter.report_temperature(self.temperature)
|
||||||
return lastUpdate
|
return lastUpdate
|
||||||
|
|
||||||
def runTrial(self, answers):
|
def runTrial(self):
|
||||||
"""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()
|
||||||
self.workspace.reset()
|
self.workspace.reset()
|
||||||
lastUpdate = float('-inf')
|
lastUpdate = float('-inf')
|
||||||
while not self.workspace.foundAnswer:
|
while self.workspace.finalAnswer is None:
|
||||||
lastUpdate = self.mainLoop(lastUpdate)
|
lastUpdate = self.mainLoop(lastUpdate)
|
||||||
if self.workspace.rule:
|
answer = {
|
||||||
answer = self.workspace.rule.finalAnswer
|
'answer': self.workspace.finalAnswer,
|
||||||
else:
|
'temp': self.temperature.last_unclamped_value,
|
||||||
answer = None
|
'time': self.coderack.codeletsRun,
|
||||||
finalTemperature = self.temperature.last_unclamped_value
|
}
|
||||||
finalTime = self.coderack.codeletsRun
|
self.reporter.report_answer(answer)
|
||||||
logging.info('Answered %s (time %d, final temperature %.1f)' % (answer, finalTime, finalTemperature))
|
return answer
|
||||||
self.reporter.report_answer({
|
|
||||||
'answer': answer,
|
|
||||||
'temp': finalTemperature,
|
|
||||||
'time': finalTime,
|
|
||||||
})
|
|
||||||
d = answers.setdefault(answer, {
|
|
||||||
'count': 0,
|
|
||||||
'sumtemp': 0,
|
|
||||||
'sumtime': 0
|
|
||||||
})
|
|
||||||
d['count'] += 1
|
|
||||||
d['sumtemp'] += finalTemperature
|
|
||||||
d['sumtime'] += finalTime
|
|
||||||
|
|
||||||
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)
|
||||||
answers = {}
|
answers = {}
|
||||||
for i in xrange(iterations):
|
for i in xrange(iterations):
|
||||||
self.runTrial(answers)
|
answer = self.runTrial()
|
||||||
|
d = answers.setdefault(answer['answer'], {
|
||||||
|
'count': 0,
|
||||||
|
'sumtemp': 0,
|
||||||
|
'sumtime': 0
|
||||||
|
})
|
||||||
|
d['count'] += 1
|
||||||
|
d['sumtemp'] += answer['temp']
|
||||||
|
d['sumtime'] += answer['time']
|
||||||
|
|
||||||
for answer, d in answers.iteritems():
|
for answer, d in answers.iteritems():
|
||||||
d['avgtemp'] = d.pop('sumtemp') / d['count']
|
d['avgtemp'] = d.pop('sumtemp') / d['count']
|
||||||
d['avgtime'] = d.pop('sumtime') / d['count']
|
d['avgtime'] = d.pop('sumtime') / d['count']
|
||||||
return answers
|
return answers
|
||||||
|
|
||||||
|
def run_forever(self, initial, modified, target):
|
||||||
|
self.workspace.resetWithStrings(initial, modified, target)
|
||||||
|
while True:
|
||||||
|
self.runTrial()
|
||||||
|
|||||||
26
copycat/curses_main.py
Normal file
26
copycat/curses_main.py
Normal file
@ -0,0 +1,26 @@
|
|||||||
|
import curses
|
||||||
|
import logging
|
||||||
|
import sys
|
||||||
|
|
||||||
|
from copycat import Copycat
|
||||||
|
from curses_reporter import CursesReporter
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == '__main__':
|
||||||
|
logging.basicConfig(level=logging.INFO, format='%(message)s', filename='./copycat.log', filemode='w')
|
||||||
|
|
||||||
|
try:
|
||||||
|
args = sys.argv[1:]
|
||||||
|
initial, modified, target = args
|
||||||
|
except ValueError:
|
||||||
|
print >>sys.stderr, 'Usage: %s initial modified target [iterations]' % sys.argv[0]
|
||||||
|
sys.exit(1)
|
||||||
|
|
||||||
|
try:
|
||||||
|
window = curses.initscr()
|
||||||
|
copycat = Copycat(reporter=CursesReporter(window))
|
||||||
|
copycat.run_forever(initial, modified, target)
|
||||||
|
except KeyboardInterrupt:
|
||||||
|
pass
|
||||||
|
finally:
|
||||||
|
curses.endwin()
|
||||||
111
copycat/curses_reporter.py
Normal file
111
copycat/curses_reporter.py
Normal file
@ -0,0 +1,111 @@
|
|||||||
|
import curses
|
||||||
|
from copycat import Reporter
|
||||||
|
|
||||||
|
class CursesReporter(Reporter):
|
||||||
|
def __init__(self, window):
|
||||||
|
curses.curs_set(0) # hide the cursor
|
||||||
|
height, width = window.getmaxyx()
|
||||||
|
slipnetHeight = 10
|
||||||
|
coderackHeight = height - 15
|
||||||
|
answersHeight = 5
|
||||||
|
self.temperatureWindow = window.derwin(height, 5, 0, 0)
|
||||||
|
self.slipnetWindow = curses.newwin(slipnetHeight, width-5, 0, 5)
|
||||||
|
self.coderackWindow = curses.newwin(coderackHeight, width-5, slipnetHeight, 5)
|
||||||
|
self.answersWindow = curses.newwin(answersHeight, width-5, slipnetHeight + coderackHeight, 5)
|
||||||
|
for w in [self.temperatureWindow, self.slipnetWindow, self.answersWindow]:
|
||||||
|
w.clear()
|
||||||
|
w.border()
|
||||||
|
w.refresh()
|
||||||
|
self.answers = {}
|
||||||
|
|
||||||
|
def report_answer(self, answer):
|
||||||
|
d = self.answers.setdefault(answer['answer'], {
|
||||||
|
'answer': answer['answer'],
|
||||||
|
'count': 0,
|
||||||
|
'sumtime': 0,
|
||||||
|
'sumtemp': 0,
|
||||||
|
})
|
||||||
|
d['count'] += 1
|
||||||
|
d['sumtemp'] += answer['temp']
|
||||||
|
d['sumtime'] += answer['time']
|
||||||
|
d['avgtemp'] = d['sumtemp'] / d['count']
|
||||||
|
d['avgtime'] = d['sumtime'] / d['count']
|
||||||
|
|
||||||
|
def fitness(d):
|
||||||
|
return 3 * d['count'] - d['avgtemp']
|
||||||
|
|
||||||
|
def represent(d):
|
||||||
|
return '%s: %d (avg time %.1f, avg temp %.1f)' % (
|
||||||
|
d['answer'], d['count'], d['avgtime'], d['avgtemp'],
|
||||||
|
)
|
||||||
|
|
||||||
|
answersToPrint = sorted(self.answers.itervalues(), key=fitness, reverse=True)
|
||||||
|
|
||||||
|
w = self.answersWindow
|
||||||
|
pageWidth = w.getmaxyx()[1]
|
||||||
|
if pageWidth >= 96:
|
||||||
|
columnWidth = (pageWidth - 6) / 2
|
||||||
|
for i, d in enumerate(answersToPrint[:3]):
|
||||||
|
w.addnstr(i+1, 2, represent(d), columnWidth)
|
||||||
|
for i, d in enumerate(answersToPrint[3:6]):
|
||||||
|
w.addnstr(i+1, pageWidth - columnWidth - 2, represent(d), columnWidth)
|
||||||
|
else:
|
||||||
|
columnWidth = pageWidth - 4
|
||||||
|
for i, d in enumerate(answersToPrint[:3]):
|
||||||
|
w.addnstr(i+1, 2, represent(d), columnWidth)
|
||||||
|
w.refresh()
|
||||||
|
|
||||||
|
def report_coderack(self, coderack):
|
||||||
|
NUMBER_OF_BINS = 7
|
||||||
|
|
||||||
|
# Combine duplicate codelets for printing.
|
||||||
|
counts = {}
|
||||||
|
for c in coderack.codelets:
|
||||||
|
assert 1 <= c.urgency <= NUMBER_OF_BINS
|
||||||
|
key = (c.urgency, c.name)
|
||||||
|
counts[key] = counts.get(key, 0) + 1
|
||||||
|
|
||||||
|
# Sort the most common and highest-urgency codelets to the top.
|
||||||
|
entries = sorted(
|
||||||
|
(count, key[0], key[1])
|
||||||
|
for key, count in counts.iteritems()
|
||||||
|
)
|
||||||
|
|
||||||
|
# Figure out how we'd like to render each codelet's name.
|
||||||
|
printable_entries = [
|
||||||
|
(urgency, '%s (%d)' % (name, count))
|
||||||
|
for count, urgency, name in entries
|
||||||
|
]
|
||||||
|
|
||||||
|
# Render each codelet in the appropriate column,
|
||||||
|
# as close to the top of the page as physically possible.
|
||||||
|
w = self.coderackWindow
|
||||||
|
pageHeight, pageWidth = w.getmaxyx()
|
||||||
|
columnWidth = (pageWidth - len('important-object-correspondence-scout (n)')) / (NUMBER_OF_BINS - 1)
|
||||||
|
|
||||||
|
def is_vacant(y, x):
|
||||||
|
return (w.inch(y, x) & 0xFF) == 0x20
|
||||||
|
|
||||||
|
w.erase()
|
||||||
|
for u, string in printable_entries:
|
||||||
|
# Find the highest point on the page where we could place this entry.
|
||||||
|
start_column = (u - 1) * columnWidth
|
||||||
|
end_column = start_column + len(string)
|
||||||
|
for r in range(pageHeight):
|
||||||
|
if all(is_vacant(r, c) for c in xrange(start_column, end_column+20)):
|
||||||
|
w.addstr(r, start_column, string)
|
||||||
|
break
|
||||||
|
w.refresh()
|
||||||
|
|
||||||
|
def report_slipnet(self, slipnet):
|
||||||
|
pass
|
||||||
|
|
||||||
|
def report_temperature(self, temperature):
|
||||||
|
height = self.temperatureWindow.getmaxyx()[0]
|
||||||
|
max_mercury = height - 4
|
||||||
|
mercury = max_mercury * temperature.value() / 100.0
|
||||||
|
for i in range(max_mercury):
|
||||||
|
ch = ' ,o%8'[int(4 * min(max(0, mercury - i), 1))]
|
||||||
|
self.temperatureWindow.addstr(max_mercury - i, 1, 3*ch)
|
||||||
|
self.temperatureWindow.addnstr(height - 2, 1, '%3d' % temperature.actual_value, 3)
|
||||||
|
self.temperatureWindow.refresh()
|
||||||
@ -16,7 +16,7 @@ class Description(WorkspaceStructure):
|
|||||||
def __str__(self):
|
def __str__(self):
|
||||||
s = 'description(%s) of %s' % (self.descriptor.get_name(), self.object)
|
s = 'description(%s) of %s' % (self.descriptor.get_name(), self.object)
|
||||||
workspace = self.ctx.workspace
|
workspace = self.ctx.workspace
|
||||||
if self.object.string == workspace.initial:
|
if self.object.string == getattr(workspace, 'initial', None):
|
||||||
s += ' in initial string'
|
s += ' in initial string'
|
||||||
else:
|
else:
|
||||||
s += ' in target string'
|
s += ' in target string'
|
||||||
|
|||||||
@ -1,11 +1,18 @@
|
|||||||
import logging
|
import logging
|
||||||
import sys
|
import sys
|
||||||
|
|
||||||
from copycat import Copycat
|
from copycat import Copycat, Reporter
|
||||||
|
|
||||||
|
|
||||||
|
class SimpleReporter(Reporter):
|
||||||
|
def report_answer(self, answer):
|
||||||
|
print 'Answered %s (time %d, final temperature %.1f)' % (
|
||||||
|
answer['answer'], answer['time'], answer['temp'],
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
if __name__ == '__main__':
|
if __name__ == '__main__':
|
||||||
logging.basicConfig(level=logging.WARN, format='%(message)s', filename='./copycat.log', filemode='w')
|
logging.basicConfig(level=logging.INFO, format='%(message)s', filename='./copycat.log', filemode='w')
|
||||||
|
|
||||||
try:
|
try:
|
||||||
args = sys.argv[1:]
|
args = sys.argv[1:]
|
||||||
@ -19,7 +26,7 @@ if __name__ == '__main__':
|
|||||||
print >>sys.stderr, 'Usage: %s initial modified target [iterations]' % sys.argv[0]
|
print >>sys.stderr, 'Usage: %s initial modified target [iterations]' % sys.argv[0]
|
||||||
sys.exit(1)
|
sys.exit(1)
|
||||||
|
|
||||||
copycat = Copycat()
|
copycat = Copycat(reporter=SimpleReporter())
|
||||||
answers = copycat.run(initial, modified, target, iterations)
|
answers = copycat.run(initial, modified, target, iterations)
|
||||||
|
|
||||||
for answer, d in sorted(answers.iteritems(), key=lambda kv: kv[1]['avgtemp']):
|
for answer, d in sorted(answers.iteritems(), key=lambda kv: kv[1]['avgtemp']):
|
||||||
|
|||||||
@ -99,9 +99,13 @@ class Rule(WorkspaceStructure):
|
|||||||
# applies the changes to self string ie. successor
|
# applies the changes to self string ie. successor
|
||||||
if self.facet == slipnet.length:
|
if self.facet == slipnet.length:
|
||||||
if self.relation == slipnet.predecessor:
|
if self.relation == slipnet.predecessor:
|
||||||
return string[0:-1]
|
return string[:-1]
|
||||||
if self.relation == slipnet.successor:
|
elif self.relation == slipnet.successor:
|
||||||
return string + string[0:1]
|
# This seems to be happening at the wrong level of abstraction.
|
||||||
|
# "Lengthening" is not an operation that makes sense on strings;
|
||||||
|
# it makes sense only on *groups*, and here we've lost the
|
||||||
|
# "groupiness" of this string. What gives?
|
||||||
|
return string + string[0]
|
||||||
return string
|
return string
|
||||||
# apply character changes
|
# apply character changes
|
||||||
if self.relation == slipnet.predecessor:
|
if self.relation == slipnet.predecessor:
|
||||||
@ -123,24 +127,21 @@ class Rule(WorkspaceStructure):
|
|||||||
self.descriptor = self.descriptor.applySlippages(slippages)
|
self.descriptor = self.descriptor.applySlippages(slippages)
|
||||||
self.relation = self.relation.applySlippages(slippages)
|
self.relation = self.relation.applySlippages(slippages)
|
||||||
# generate the final string
|
# generate the final string
|
||||||
self.finalAnswer = workspace.targetString
|
|
||||||
changeds = [o for o in workspace.target.objects if
|
changeds = [o for o in workspace.target.objects if
|
||||||
o.described(self.descriptor) and
|
o.described(self.descriptor) and
|
||||||
o.described(self.category)]
|
o.described(self.category)]
|
||||||
changed = changeds and changeds[0] or None
|
if len(changeds) == 0:
|
||||||
logging.debug('changed object = %s', changed)
|
return workspace.targetString
|
||||||
if changed:
|
elif len(changeds) > 1:
|
||||||
left = changed.leftIndex
|
logging.info("More than one letter changed. Sorry, I can't solve problems like this right now.")
|
||||||
startString = ''
|
return None
|
||||||
if left > 1:
|
else:
|
||||||
startString = self.finalAnswer[0: left - 1]
|
changed = changeds[0]
|
||||||
|
logging.debug('changed object = %s', changed)
|
||||||
|
left = changed.leftIndex - 1
|
||||||
right = changed.rightIndex
|
right = changed.rightIndex
|
||||||
middleString = self.__changeString(
|
s = workspace.targetString
|
||||||
self.finalAnswer[left - 1: right])
|
changed_middle = self.__changeString(s[left:right])
|
||||||
if not middleString:
|
if changed_middle is None:
|
||||||
return False
|
return None
|
||||||
endString = ''
|
return s[:left] + changed_middle + s[right:]
|
||||||
if right < len(self.finalAnswer):
|
|
||||||
endString = self.finalAnswer[right:]
|
|
||||||
self.finalAnswer = startString + middleString + endString
|
|
||||||
return True
|
|
||||||
|
|||||||
@ -32,7 +32,7 @@ class Workspace(object):
|
|||||||
self.reset()
|
self.reset()
|
||||||
|
|
||||||
def reset(self):
|
def reset(self):
|
||||||
self.foundAnswer = False
|
self.finalAnswer = None
|
||||||
self.changedObject = None
|
self.changedObject = None
|
||||||
self.objects = []
|
self.objects = []
|
||||||
self.structures = []
|
self.structures = []
|
||||||
|
|||||||
Reference in New Issue
Block a user