diff --git a/copycat/codeletMethods.py b/copycat/codeletMethods.py index a497410..3b9ce42 100644 --- a/copycat/codeletMethods.py +++ b/copycat/codeletMethods.py @@ -22,7 +22,6 @@ def codelet(name): return f return wrap - # some methods common to the codelets def __showWhichStringObjectIsFrom(structure): if not structure: diff --git a/copycat/copycat.py b/copycat/copycat.py index 51166d2..92f1a54 100644 --- a/copycat/copycat.py +++ b/copycat/copycat.py @@ -3,6 +3,7 @@ from .randomness import Randomness from .slipnet import Slipnet from .temperature import Temperature from .workspace import Workspace +from .gui import GUI class Reporter(object): """Do-nothing base class for defining new reporter types""" @@ -23,30 +24,56 @@ class Reporter(object): class Copycat(object): - def __init__(self, rng_seed=None, reporter=None): + def __init__(self, rng_seed=None, reporter=None, showgui=True): self.coderack = Coderack(self) self.random = Randomness(rng_seed) self.slipnet = Slipnet() self.temperature = Temperature() # TODO: use entropy self.workspace = Workspace(self) self.reporter = reporter or Reporter() + self.showgui = showgui + self.gui = GUI('Copycat') + self.lastUpdate = float('-inf') - def mainLoop(self, lastUpdate): + def step(self): + if (not self.showgui) or (self.showgui and (not self.gui.app.primary.control.paused or self.gui.app.primary.control.has_step())): + self.coderack.chooseAndRunCodelet() + self.reporter.report_coderack(self.coderack) + self.reporter.report_temperature(self.temperature) + self.reporter.report_workspace(self.workspace) + if (self.showgui): + self.gui.update(self) + + def update_workspace(self, currentTime): + self.workspace.updateEverything() + self.coderack.updateCodelets() + self.slipnet.update(self.random) + self.temperature.update(self.workspace.getUpdatedTemperature()) + self.lastUpdate = currentTime + self.reporter.report_slipnet(self.slipnet) + + def check_reset(self): + if self.gui.app.primary.control.go: + initial, modified, target = self.gui.app.primary.control.get_vars() + self.reset_with_strings(initial, modified, target) + return True + else: + return False + + def reset_with_strings(self, initial, modified, target): + self.workspace.resetWithStrings(initial, modified, target) + self.gui.app.reset_with_strings(initial, modified, target) + + def mainLoop(self): currentTime = self.coderack.codeletsRun 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()) # TODO: use entropy - lastUpdate = currentTime - self.reporter.report_slipnet(self.slipnet) - self.coderack.chooseAndRunCodelet() - self.reporter.report_coderack(self.coderack) - self.reporter.report_temperature(self.temperature) - self.reporter.report_workspace(self.workspace) - return lastUpdate + if currentTime >= self.lastUpdate + 15: + self.update_workspace(currentTime) + self.step() + + if self.showgui: + self.gui.refresh() def runTrial(self): """Run a trial of the copycat algorithm""" @@ -54,9 +81,8 @@ class Copycat(object): self.slipnet.reset() self.temperature.reset() # TODO: use entropy self.workspace.reset() - lastUpdate = float('-inf') while self.workspace.finalAnswer is None: - lastUpdate = self.mainLoop(lastUpdate) + self.mainLoop() answer = { 'answer': self.workspace.finalAnswer, 'temp': self.temperature.last_unclamped_value, # TODO: use entropy @@ -65,22 +91,43 @@ class Copycat(object): self.reporter.report_answer(answer) return answer - def run(self, initial, modified, target, iterations): - self.workspace.resetWithStrings(initial, modified, target) + def runGUI(self): + while not self.check_reset(): + self.gui.update(self) + self.gui.refresh() + answers = {} + while True: + if self.check_reset(): + answers = {} + answer = self.runTrial() + if self.showgui: + self.gui.app.log('Answered: {}'.format(answer['answer'])) + d = answers.setdefault(answer['answer'], { + 'count': 0, + 'sumtemp': 0, + 'sumtime': 0 + }) + d['count'] += 1 + d['sumtemp'] += answer['temp'] + d['sumtime'] += answer['time'] + if self.showgui: + self.gui.add_answers(answers) - 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') + for answer, d in answers.items(): + d['avgtemp'] = d.pop('sumtemp') / d['count'] + d['avgtime'] = d.pop('sumtime') / d['count'] + + def run(self, initial, modified, target, iterations): + self.reset_with_strings(initial, modified, target) self.temperature.useAdj('best') answers = {} for i in range(iterations): + if self.check_reset(): + answers = {} answer = self.runTrial() + if self.showgui: + self.gui.app.log('Answered: {}'.format(answer['answer'])) d = answers.setdefault(answer['answer'], { 'count': 0, 'sumtemp': 0, # TODO: use entropy @@ -89,6 +136,8 @@ class Copycat(object): d['count'] += 1 d['sumtemp'] += answer['temp'] # TODO: use entropy d['sumtime'] += answer['time'] + if self.showgui: + self.gui.add_answers(answers) for answer, d in answers.items(): d['avgtemp'] = d.pop('sumtemp') / d['count'] @@ -96,6 +145,7 @@ class Copycat(object): return answers def run_forever(self, initial, modified, target): - self.workspace.resetWithStrings(initial, modified, target) + self.reset_with_strings(initial, modified, target) while True: + self.check_reset() self.runTrial() diff --git a/copycat/gui/__init__.py b/copycat/gui/__init__.py new file mode 100644 index 0000000..80f3ae3 --- /dev/null +++ b/copycat/gui/__init__.py @@ -0,0 +1 @@ +from .gui import GUI diff --git a/copycat/gui/control.py b/copycat/gui/control.py new file mode 100644 index 0000000..04dc43d --- /dev/null +++ b/copycat/gui/control.py @@ -0,0 +1,56 @@ +import tkinter as tk +import tkinter.ttk as ttk + +from .gridframe import GridFrame +from .entry import Entry + +class Control(GridFrame): + def __init__(self, parent, *args, **kwargs): + GridFrame.__init__(self, parent, *args, **kwargs) + + self.paused = True + self.steps = 0 + self.go = False + + self.playbutton = ttk.Button(self, text='Play', command=lambda : self.toggle()) + self.add(self.playbutton, 0, 0) + + self.stepbutton = ttk.Button(self, text='Step', command=lambda : self.step()) + self.add(self.stepbutton, 1, 0) + + self.entry = Entry(self) + self.add(self.entry, 0, 1, xspan=2) + + self.gobutton = ttk.Button(self, text='Go', command=lambda : self.set_go()) + self.add(self.gobutton, 0, 2, xspan=2) + + def play(self): + self.paused = False + self.playbutton['text'] = 'Pause' + + def pause(self): + self.paused = True + self.playbutton['text'] = 'Play' + + def toggle(self): + if self.paused: + self.play() + else: + self.pause() + + def step(self): + self.steps += 1 + + def has_step(self): + if self.steps > 0: + self.steps -= 1 + return True + else: + return False + + def set_go(self): + self.go = True + self.play() + + def get_vars(self): + return self.entry.a.get(), self.entry.b.get(), self.entry.c.get() diff --git a/copycat/gui/entry.py b/copycat/gui/entry.py new file mode 100644 index 0000000..e4112a8 --- /dev/null +++ b/copycat/gui/entry.py @@ -0,0 +1,27 @@ + +import tkinter as tk +import tkinter.ttk as ttk + +from .gridframe import GridFrame + +class Entry(GridFrame): + def __init__(self, parent, *args, **kwargs): + GridFrame.__init__(self, parent, *args, **kwargs) + self.aLabel = ttk.Label(self, text='Initial:') + self.a = ttk.Entry(self, style='EntryStyle.TEntry') + + self.add(self.aLabel, 0, 0) + self.add(self.a, 0, 1) + + self.bLabel = ttk.Label(self, text='Final:') + self.b = ttk.Entry(self, style='EntryStyle.TEntry') + + self.add(self.bLabel, 1, 0) + self.add(self.b, 1, 1) + + self.cLabel = ttk.Label(self, text='Next:') + self.c = ttk.Entry(self, style='EntryStyle.TEntry') + + self.add(self.cLabel, 2, 0) + self.add(self.c, 2, 1) + GridFrame.configure(self) diff --git a/copycat/gui/gridframe.py b/copycat/gui/gridframe.py new file mode 100644 index 0000000..e083b42 --- /dev/null +++ b/copycat/gui/gridframe.py @@ -0,0 +1,11 @@ +import tkinter as tk +import tkinter.ttk as ttk + +class GridFrame(tk.Frame): + def __init__(self, parent, *args, **kwargs): + ttk.Frame.__init__(self, parent, *args, **kwargs) + + def add(self, element, x, y, xspan=1, yspan=1): + element.grid(column=x, row=y, columnspan=xspan, rowspan=yspan, sticky=tk.N+tk.E+tk.S+tk.W) + tk.Grid.rowconfigure(self, x, weight=1) + tk.Grid.columnconfigure(self, y, weight=1) diff --git a/copycat/gui/gui.py b/copycat/gui/gui.py new file mode 100755 index 0000000..7b599bf --- /dev/null +++ b/copycat/gui/gui.py @@ -0,0 +1,98 @@ +import sys +import time + +import tkinter as tk +import tkinter.ttk as ttk + +from tkinter import scrolledtext +from tkinter import filedialog + +import matplotlib.pyplot as plt + +from .status import Status, StatusFrame +from .status import Plot +from .gridframe import GridFrame +from .primary import Primary +from .list import List +from .style import configure_style + +from .plot import plot_imbedded + +plt.style.use('dark_background') + +class MainApplication(GridFrame): + + def __init__(self, parent, *args, **kwargs): + GridFrame.__init__(self, parent, *args, **kwargs) + + self.parent = parent + self.primary = Primary(self, *args, **kwargs) + self.add(self.primary, 0, 0, xspan=2) + self.create_widgets() + GridFrame.configure(self) + + #self.messages = [] + + #def log(self, message): + # self.messages.append(message) + + def create_widgets(self): + self.slipList = List(self, 10) + self.add(self.slipList, 0, 1) + + self.codeletList = List(self, 10) + self.add(self.codeletList, 1, 1) + + self.objectList = List(self, 10) + self.add(self.objectList, 2, 1) + + #self.logBox = List(self, 10) + #self.add(self.logBox, 1, 0) + + self.graph2 = Plot(self, 'Answer Distribution') + self.add(self.graph2, 2, 0) + + def update(self, copycat): + self.primary.update(copycat) + + slipnodes = copycat.slipnet.slipnodes + codelets = copycat.coderack.codelets + objects = copycat.workspace.objects + + self.slipList.update(slipnodes, key=lambda s:s.activation, + formatter=lambda s : '{}: {}'.format(s.name, round(s.activation, 2))) + self.codeletList.update(codelets, key=lambda c:c.urgency, formatter= lambda s : '{}: {}'.format(s.name, round(s.urgency, 2))) + self.objectList.update(objects, formatter=lambda s : '{}'.format(str(s.descriptions))) + #self.logBox.update(list(reversed(self.messages))[:10]) + + def reset_with_strings(self, initial, modified, target): + self.primary.reset_with_strings(initial, modified, target) + +class GUI(object): + def __init__(self, title, updateInterval=.1): + self.root = tk.Tk() + self.root.title(title) + tk.Grid.rowconfigure(self.root, 0, weight=1) + tk.Grid.columnconfigure(self.root, 0, weight=1) + self.app = MainApplication(self.root) + self.app.grid(row=0, column=0, sticky=tk.N+tk.S+tk.E+tk.W) + + configure_style(ttk.Style()) + + self.lastUpdated = time.time() + self.updateInterval = updateInterval + + def add_answers(self, answers): + def modifier(status): + with plt.style.context(('dark_background')): + plot_imbedded(answers, status) + self.app.graph2.status.modifier = modifier + + def refresh(self): + self.root.update_idletasks() + self.root.update() + + def update(self, copycat): + current = time.time() + self.app.update(copycat) + self.lastUpdated = current diff --git a/copycat/gui/list.py b/copycat/gui/list.py new file mode 100644 index 0000000..7768efd --- /dev/null +++ b/copycat/gui/list.py @@ -0,0 +1,26 @@ +import tkinter as tk +import tkinter.ttk as ttk + +import time + +from .gridframe import GridFrame + +class List(GridFrame): + + def __init__(self, parent, columns, updateInterval=.1): + GridFrame.__init__(self, parent) + self.text = ttk.Label(self, anchor='w', justify=tk.LEFT, width=30) + self.add(self.text, 0, 0) + + self.columns = columns + + self.lastUpdated = time.time() + self.updateInterval = updateInterval + + def update(self, l, key=None, reverse=False, formatter=lambda s : str(s)): + current = time.time() + if current - self.lastUpdated > self.updateInterval: + l = l[:self.columns] + if key is not None: + l = sorted(l, key=key, reverse=False) + self.text['text'] = '\n'.join(map(formatter, l)) diff --git a/copycat/gui/plot.py b/copycat/gui/plot.py new file mode 100644 index 0000000..8583ff7 --- /dev/null +++ b/copycat/gui/plot.py @@ -0,0 +1,17 @@ +import matplotlib.pyplot as plt; plt.rcdefaults() +import numpy as np +import matplotlib.pyplot as plt + +def plot_imbedded(answers, status): + answers = sorted(answers.items(), key=lambda kv : kv[1]['count']) + objects = [t[0] for t in answers] + yvalues = [t[1]['count'] for t in answers] + + y_pos = np.arange(len(objects)) + + status.subplot.clear() + status.subplot.bar(y_pos, yvalues, align='center', alpha=0.5) + status.subplot.set_xticks(y_pos) + status.subplot.set_xticklabels(tuple(objects)) + status.subplot.set_ylabel('Count') + status.subplot.set_title('Answers') diff --git a/copycat/gui/primary.py b/copycat/gui/primary.py new file mode 100644 index 0000000..2e8d094 --- /dev/null +++ b/copycat/gui/primary.py @@ -0,0 +1,72 @@ +import tkinter as tk +import tkinter.ttk as ttk + +from tkinter import scrolledtext +from tkinter import filedialog + +from .control import Control +from .gridframe import GridFrame + +font1Size = 32 +font2Size = 16 +font1 = ('Helvetica', str(font1Size)) +font2 = ('Helvetica', str(font2Size)) + +style = dict(background='black', + foreground='white', + font=font2) + +def create_main_canvas(root, initial, final, new, guess): + padding = 100 + + canvas = tk.Canvas(root, background='black') + + def add_sequences(sequences, x, y): + for sequence in sequences: + x += padding + if sequence is None: + sequence = '' + for char in sequence: + canvas.create_text(x, y, text=char, anchor=tk.NW, font=font1, fill='white') + x += font1Size + return x, y + + x = 0 + y = padding + + add_sequences([initial, final], x, y) + + x = 0 + y += padding + + add_sequences([new, guess], x, y) + + #canvas['height'] = str(int(canvas['height']) + padding) + #canvas['width'] = str(int(canvas['width']) + padding) + + return canvas + +class Primary(GridFrame): + + def __init__(self, parent, *args, **kwargs): + GridFrame.__init__(self, parent, *args, **kwargs) + + self.initial = '' + self.modified = '' + self.target = '' + + self.canvas = create_main_canvas(self, self.initial, self.modified, self.target, '') + self.add(self.canvas, 0, 0, xspan=2) + self.control = Control(self) + self.add(self.control, 0, 2) + GridFrame.configure(self) + + def update(self, copycat): + answer = '' if copycat.workspace.rule is None else copycat.workspace.rule.buildTranslatedRule() + #self.canvas = create_main_canvas(self, self.initial, self.modified, self.target, answer) + #self.add(self.canvas, 0, 0, xspan=2) + + def reset_with_strings(self, initial, modified, target): + self.initial = initial + self.modified = modified + self.target = target diff --git a/copycat/gui/status.py b/copycat/gui/status.py new file mode 100644 index 0000000..e9863a1 --- /dev/null +++ b/copycat/gui/status.py @@ -0,0 +1,66 @@ +import matplotlib +matplotlib.use("TkAgg") +from matplotlib.backends.backend_tkagg import FigureCanvasTkAgg, NavigationToolbar2TkAgg +from matplotlib.figure import Figure + +import tkinter as tk +import tkinter.ttk as ttk + +import time +import matplotlib.animation as animation + +import matplotlib.pyplot as plt + +plt.style.use('dark_background') + +from .gridframe import GridFrame + +class Plot(GridFrame): + def __init__(self, parent, title): + GridFrame.__init__(self, parent) + self.status = Status() + self.sframe = StatusFrame(self, self.status, title) + self.add(self.sframe, 0, 0, xspan=2) + + self.savebutton = ttk.Button(self, text='Save to path:', command=lambda : self.save()) + self.add(self.savebutton, 0, 1) + + self.pathentry = ttk.Entry(self, style='EntryStyle.TEntry', textvariable='output/dist.png') + self.add(self.pathentry, 1, 1) + + def save(self): + path = self.pathentry.get() + if len(path) > 0: + try: + self.status.figure.savefig(path) + except Exception as e: + print(e) + +class StatusFrame(ttk.Frame): + def __init__(self, parent, status, title): + ttk.Frame.__init__(self, parent) + self.status = status + + self.canvas = FigureCanvasTkAgg(status.figure, self) + self.canvas.show() + self.canvas.get_tk_widget().pack(side=tk.BOTTOM, fill=tk.BOTH, expand=True) + + self.animation = animation.FuncAnimation(status.figure, lambda i : status.update_plots(i), interval=1000) + +class Status(object): + def __init__(self): + self.figure = Figure(figsize=(5,5), dpi=100) + self.subplot = self.figure.add_subplot(111) + self.x = [] + self.y = [] + + def modifier(status): + with plt.style.context(('dark_background')): + status.subplot.plot(status.x, status.y) + + self.modifier = modifier + self.update_plots(0) + + def update_plots(self, i): + self.subplot.clear() + self.modifier(self) diff --git a/copycat/gui/style.py b/copycat/gui/style.py new file mode 100644 index 0000000..a8bca07 --- /dev/null +++ b/copycat/gui/style.py @@ -0,0 +1,33 @@ +style_dict = dict(foreground='white', + background='black') + +map_options = dict( + foreground=[('disabled', 'black'), + ('pressed', 'white'), + ('active', 'white')], + background=[('disabled', 'black'), + ('pressed', '!focus', 'black'), + ('active', 'black')], + highlightcolor=[('focus', 'black'), + ('!focus', 'black')]) + +def configure_style(style): + style.configure('TButton', **style_dict) + style.map('TButton', **map_options) + style.configure('TLabel', **style_dict) + #style.configure('TEntry', **style_dict) + #style.map('TEntry', **map_options) + + # A hack to change entry style + style.element_create("plain.field", "from", "clam") + style.layout("EntryStyle.TEntry", + [('Entry.plain.field', {'children': [( + 'Entry.background', {'children': [( + 'Entry.padding', {'children': [( + 'Entry.textarea', {'sticky': 'nswe'})], + 'sticky': 'nswe'})], 'sticky': 'nswe'})], + 'border':'2', 'sticky': 'nswe'})]) + style.configure("EntryStyle.TEntry", + background="black", + foreground="white", + fieldbackground="black") diff --git a/copycat/sampleText.txt b/copycat/sampleText.txt new file mode 100644 index 0000000..7cb721a --- /dev/null +++ b/copycat/sampleText.txt @@ -0,0 +1,4 @@ +1,2 +3,4 +7,7 +100,100 diff --git a/copycat/workspace.py b/copycat/workspace.py index 1c1ace7..c9af5d9 100644 --- a/copycat/workspace.py +++ b/copycat/workspace.py @@ -23,6 +23,16 @@ class Workspace(object): self.intraStringUnhappiness = 0.0 self.interStringUnhappiness = 0.0 + # LSaldyt: default initializations for GUI entry + self.targetString = '' + self.initialString = '' + self.modifiedString = '' + self.finalAnswer = None + self.changedObject = None + self.objects = [] + self.structures = [] + self.rule = None + def __repr__(self): return '' % ( self.initialString, self.modifiedString, self.targetString, diff --git a/gui.py b/gui.py new file mode 100755 index 0000000..af8292d --- /dev/null +++ b/gui.py @@ -0,0 +1,24 @@ +#!/usr/bin/env python3 +import argparse +import logging + +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'], + )) + +def main(): + 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.') + options = parser.parse_args() + + copycat = Copycat(reporter=SimpleReporter(), rng_seed=options.seed) + copycat.runGUI() + +if __name__ == '__main__': + main()