├── .travis.yml ├── LICENSE ├── MANIFEST.in ├── Makefile ├── README.md ├── calysto ├── __init__.py ├── ai │ ├── Numeric.py │ ├── __init__.py │ └── conx.py ├── chart │ ├── __init__.py │ └── googlechart.py ├── display.py ├── graphics.py ├── images │ ├── logo-32x32.png │ └── logo-64x64.png ├── magics │ ├── __init__.py │ └── simulation_magic.py ├── simulation.py ├── simulations │ ├── Bug1.py │ ├── Ladybug1.py │ └── __init__.py ├── util │ ├── __init__.py │ ├── code2notebook.py │ ├── ottobib.py │ └── password.py ├── widget │ ├── __init__.py │ └── camera.py └── zgraphics.py ├── setup.cfg └── setup.py /.travis.yml: -------------------------------------------------------------------------------- 1 | # After changing this file, check it on: 2 | # http://lint.travis-ci.org/ 3 | 4 | 5 | language: python 6 | python: 7 | - 3.4 8 | - 3.5 9 | - 3.6 10 | 11 | 12 | install: 13 | # install IPython according to their travis file: 14 | # workaround for https://github.com/travis-ci/travis-cookbooks/issues/155 15 | - sudo rm -rf /dev/shm && sudo ln -s /run/shm /dev/shm 16 | # Pierre Carrier's PPA for PhantomJS and CasperJS 17 | - travis_retry sudo add-apt-repository -y ppa:pcarrier/ppa 18 | - time sudo apt-get update 19 | - travis_retry sudo apt-get install pandoc libzmq3-dev 20 | - travis_retry pip install -f https://nipy.bic.berkeley.edu/wheelhouse/travis jinja2 sphinx pygments tornado requests mock pyzmq jsonschema jsonpointer mistune svgwrite Pillow cairosvg ipython notebook ipywidgets 21 | 22 | - travis_retry git clone --depth=50 --recursive https://github.com/calysto/metakernel 23 | - cd metakernel 24 | - python setup.py install 25 | - cd .. 26 | 27 | - travis_retry pip install coveralls jedi 28 | - travis_retry python setup.py install 29 | 30 | script: 31 | - nosetests --exe -v --with-doctest calysto 32 | 33 | script: 34 | - nosetests --exe -v --with-doctest --with-cov --cover-package calysto; 35 | 36 | after_success: 37 | - coveralls 38 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Copyright (c) 2014, Calysto 2 | All rights reserved. 3 | 4 | Redistribution and use in source and binary forms, with or without 5 | modification, are permitted provided that the following conditions are met: 6 | 7 | * Redistributions of source code must retain the above copyright notice, this 8 | list of conditions and the following disclaimer. 9 | 10 | * Redistributions in binary form must reproduce the above copyright notice, 11 | this list of conditions and the following disclaimer in the documentation 12 | and/or other materials provided with the distribution. 13 | 14 | THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" 15 | AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE 16 | IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE 17 | DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE 18 | FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL 19 | DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR 20 | SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER 21 | CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, 22 | OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE 23 | OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. 24 | 25 | -------------------------------------------------------------------------------- /MANIFEST.in: -------------------------------------------------------------------------------- 1 | include *.txt 2 | include *.rst 3 | include calysto/images/*.png 4 | include LICENSE 5 | recursive-include docs *.rst 6 | recursive-include docs *.png 7 | recursive-include docs *.py 8 | prune .git 9 | prune docs/build 10 | prune dist 11 | prune build 12 | -------------------------------------------------------------------------------- /Makefile: -------------------------------------------------------------------------------- 1 | export VERSION=`python setup.py --version 2>/dev/null` 2 | 3 | tag: 4 | git commit -a -m "Release $(VERSION)"; true 5 | git tag v$(VERSION) 6 | git push origin --all 7 | git push origin --tags 8 | twine upload dist/* 9 | 10 | all: 11 | rm -rf dist 12 | pip install wheel -U 13 | python3 setup.py register 14 | python3 setup.py bdist_wheel 15 | python3 setup.py sdist --formats=zip 16 | twine upload dist/* 17 | 18 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | calysto 2 | ======= 3 | 4 | [![Gitter](https://badges.gitter.im/Calysto/calysto.svg)](https://gitter.im/Calysto/calysto?utm_source=badge&utm_medium=badge&utm_campaign=pr-badge) [![Travis](https://travis-ci.org/Calysto/calysto.svg?branch=master)](https://travis-ci.org/Calysto/calysto/) [![PyPI version](https://badge.fury.io/py/calysto.svg)](https://badge.fury.io/py/calysto) 5 | 6 | Libraries and Languages for Python and Jupyter 7 | 8 | Homepage: http://calysto.github.io/ 9 | 10 | ```shell 11 | $ pip install calysto 12 | 13 | OR 14 | 15 | $ pip3 install calysto 16 | ``` 17 | 18 | You may also need to install: 19 | 20 | * libcairo2 21 | * libXrender 22 | * libfontconfig 23 | 24 | On MacOS for graphics, you may also need: 25 | 26 | * `brew install cairo` 27 | -------------------------------------------------------------------------------- /calysto/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Calysto/calysto/a9a6e2a402d8d1fc2c0a9bb8f3981da8a3a9e55f/calysto/__init__.py -------------------------------------------------------------------------------- /calysto/ai/Numeric.py: -------------------------------------------------------------------------------- 1 | ## Pure-Python Numeric replacement 2 | ## (c) 2013, Doug Blank 3 | ## GPL, version 3.0 4 | 5 | import math, operator, copy 6 | import array as python_array 7 | import random 8 | from functools import reduce 9 | 10 | #class python_array(parray.array): 11 | # def __new__(cls, *args, **kwargs): 12 | # return parray.array.__new__(cls, *args, **kwargs) 13 | 14 | def ndim(n, *args, **kwargs): 15 | """ 16 | Makes a multi-dimensional array of random floats. (Replaces 17 | RandomArray). 18 | """ 19 | thunk = kwargs.get("thunk", lambda: random.random()) 20 | if not args: 21 | return [thunk() for i in range(n)] 22 | A = [] 23 | for i in range(n): 24 | A.append( ndim(*args, thunk=thunk) ) 25 | return A 26 | 27 | class array: 28 | """ 29 | Replaces Numeric's ndarray. 30 | """ 31 | def __init__(self, data, typecode='f'): 32 | ## array([1, 2]) 33 | ## array([[1, 2], [3, 4]]) 34 | if type(data) == array: 35 | self.array = data[:] 36 | elif type(data[0]) in [int, float, int, bool]: 37 | # array.array of floats 38 | self.array = python_array.array('d', data) 39 | else: 40 | # list of Arrays 41 | self.array = list(map(array, data)) 42 | 43 | def __getitem__(self, item): 44 | return self.array[item] 45 | 46 | def __setitem__(self, item, value): 47 | self.array[item] = value 48 | 49 | def __len__(self): 50 | return len(self.array) 51 | 52 | def copy(self): 53 | if type(self.array) is list: 54 | return copy.deepcopy(self.array) 55 | else: 56 | return self.array[:] # vector only! 57 | 58 | def __repr__(self): 59 | if type(self.array) is list: 60 | return str([str(v) for v in self.array]) 61 | else: 62 | return str(self.array.tolist()) 63 | 64 | def __lt__(self, other): 65 | if type(self.array) is list: 66 | return array([v < other for v in self.array]) 67 | return array([f < other for f in self.array]) 68 | 69 | def __gt__(self, other): 70 | if type(self.array) is list: 71 | return array([v > other for v in self.array]) 72 | return array([f > other for f in self.array]) 73 | 74 | def __mul__(self, other): 75 | if type(other) in [int, float, int]: 76 | return array([v * other for v in self.array]) 77 | else: # array * [0, 1] 78 | return array(list(map(lambda a,b: a * b, self.array, other))) 79 | 80 | def __div__(self, other): 81 | if type(other) in [int, float, int]: 82 | return array([v / other for v in self.array]) 83 | else: 84 | raise Exception("not implemented yet") 85 | #else: # array * [0, 1] 86 | # return array(map(lambda a,b: a / b, self.array, other)) 87 | 88 | def __sub__(self, other): 89 | if type(other) in [int, float, int]: 90 | return array([v - other for v in self.array]) 91 | else: # array - [0, 1] 92 | ##print("-", self.array, other) 93 | return array(list(map(lambda a,b: a - b, self.array, other))) 94 | 95 | def __rsub__(self, other): 96 | if type(other) in [int, float, int]: 97 | return array([other - v for v in self.array]) 98 | else: # array - [0, 1] 99 | return array(list(map(lambda a,b: b - a, self.array, other))) 100 | 101 | def __add__(self, other): 102 | if type(other) in [int, float, int]: 103 | return array([v + other for v in self.array]) 104 | else: # array + [0, 1] 105 | #print "add a", self.array 106 | #print "add b", other 107 | return array(list(map(lambda a,b: a + b, self.array, other))) 108 | 109 | def __pow__(self, other): 110 | if type(other) in [int, float, int]: 111 | return array([v ** other for v in self.array]) 112 | else: # array ** [0, 1] 113 | return array(list(map(lambda a,b: a ** b, self.array, other))) 114 | 115 | def __abs__(self): 116 | return array([abs(v) for v in self.array]) 117 | 118 | def getShape(self): 119 | if type(self.array) == list: 120 | return (len(self.array), len(self.array[0])) 121 | else: 122 | return (len(self.array), ) 123 | 124 | shape = property(getShape) 125 | __rmul__ = __mul__ 126 | __rgt__ = __gt__ 127 | __rlt__ = __lt__ 128 | __radd__ = __add__ 129 | 130 | fabs = abs 131 | exp = math.exp 132 | argmin = lambda vector: vector.index(min(vector)) 133 | 134 | def put(toArray, arange, fromArray): 135 | for i in arange: 136 | if type(fromArray) in [int, float, int]: 137 | toArray[i] = fromArray 138 | else: 139 | toArray[i] = fromArray[i] 140 | 141 | def arange(size): 142 | return list(range(size)) 143 | 144 | def zeros(dims, typecode='f'): 145 | if type(dims) == type(1): 146 | dims = (dims,) 147 | return array(ndim(*dims, thunk=lambda: 0.0)) 148 | 149 | def ones(dims, typecode='f'): 150 | if type(dims) == type(1): 151 | dims = (dims,) 152 | return array(ndim(*dims, thunk=lambda: 1.0)) 153 | 154 | class add: 155 | @staticmethod 156 | def reduce(vector): 157 | """ 158 | Can be a vector or matrix. If data are bool, sum Trues. 159 | """ 160 | if type(vector) is list: # matrix 161 | return array(list(map(add.reduce, vector))) 162 | else: 163 | return sum(vector) # Numeric_array, return scalar 164 | 165 | class multiply: 166 | @staticmethod 167 | def reduce(vector): 168 | """ 169 | Can be a vector or matrix. If data are bool, sum Trues. 170 | """ 171 | if type(vector) is list: # matrix 172 | return array(list(map(multiply.reduce, vector))) 173 | else: 174 | return reduce(operator.mul, vector) # Numeric.array, return scalar 175 | 176 | def outerproduct(a, b): 177 | """Numeric.outerproduct([0.46474895, 0.46348238, 0.53923529, 0.46428344, 0.50223047], 178 | [-0.16049719, 0.17086812, 0.1692107 , 0.17433657, 0.1738235 , 179 | 0.17292975, 0.17553493, 0.17222987, -0.17038313, 0.17725782, 180 | 0.18428386]) => 181 | [[-0.0745909, 0.07941078, 0.07864049, 0.08102274, 0.08078429, 0.08036892, 182 | 0.08157967, 0.08004365, -0.07918538, 0.08238038, 0.08564573] 183 | [-0.07438762, 0.07919436, 0.07842618, 0.08080193, 0.08056413, 0.08014989, 184 | 0.08135735, 0.07982551, -0.07896958, 0.08215587, 0.08541232] 185 | [-0.08654575, 0.09213812, 0.09124438, 0.09400843, 0.09373177, 0.09324982, 186 | 0.09465463, 0.09287243, -0.09187659, 0.09558367, 0.09937236] 187 | [-0.07451619, 0.07933124, 0.07856172, 0.08094158, 0.08070337, 0.08028842, 188 | 0.08149796, 0.07996348, -0.07910606, 0.08229787, 0.08555994] 189 | [-0.08060658, 0.08581518, 0.08498277, 0.08755714, 0.08729946, 0.08685059, 190 | 0.08815899, 0.08649909, -0.0855716, 0.08902428, 0.09255297]])""" 191 | result = zeros((len(a), len(b))) 192 | for i in range(len(a)): 193 | for j in range(len(b)): 194 | result[i][j] = a[i] * b[j] 195 | return result 196 | 197 | def matrixmultiply(a, b): 198 | #print "x a:", a 199 | #print "x b:", b 200 | # a = [[0, 1], [2, 3], [4, 5]], b = [0, 1] 201 | if type(b[0]) in [float, int, int]: 202 | retval = zeros(len(a)) 203 | for i in range(len(a)): 204 | for j in range(len(a[0])): 205 | retval[i] = retval[i] + (a[i][j] * b[j]) 206 | else: 207 | retval = zeros(len(b[0])) 208 | for i in range(len(a)): 209 | retval = retval + (a[i] * b[i]) 210 | return retval 211 | 212 | ###==================================================== 213 | 214 | def test(): 215 | count = 0 216 | ocount = 0 217 | rcount = 0 218 | 219 | def compare(answer, solution, question): 220 | correct = True 221 | try: 222 | if type(solution[0]) == list: 223 | for i in range(len(solution)): 224 | for j in range(len(solution[0])): 225 | if abs(answer[i][j] - solution[i][j]) > .00001: 226 | correct = False 227 | else: 228 | for i in range(len(solution)): 229 | if abs(answer[i] - solution[i]) > .00001: 230 | correct = False 231 | except: 232 | traceback.print_exc() 233 | correct = False 234 | return int(correct) 235 | 236 | def testexpr(expr, solution, count): 237 | print(("Test %s: %s" % (count, expr))) 238 | # Testing original Numeric versus this version 239 | # Define one, or both of these: 240 | #orig = expr.replace("Numeric.", "MyNumeric.") 241 | #repl = expr.replace("Numeric.", "Numeric.") 242 | try: 243 | a = eval(orig) 244 | except: 245 | traceback.print_exc() 246 | a = "ERROR" 247 | print((" Numeric:", a)) 248 | try: 249 | b = eval(repl) 250 | except: 251 | traceback.print_exc() 252 | b = "ERROR" 253 | print((" Replace:", b)) 254 | return compare(a, solution, expr), compare(b, solution, expr) 255 | 256 | for expr, result in [ 257 | ("Numeric.array([1, 2, 3])", [1, 2, 3]), 258 | ("Numeric.array([1, 2, 3]) * .1", [.1, .2, .3]), 259 | ("Numeric.array([1, 2, 3]) + .1", [1.1, 2.1, 3.1]), 260 | ("Numeric.array([1, 2, 3]) - .1", [0.9, 1.9, 2.9]), 261 | ("1 - Numeric.array([1, 2, 3])", [0, -1, -2]), 262 | ("Numeric.array([1, 2, 3]) * (1 - Numeric.array([1, 2, 3]))", [0, -2, -6]), 263 | ("Numeric.array([1, 2, 3]) + Numeric.array([1, 2, 3])", [2, 4, 6]), 264 | ("Numeric.array([1, 2, 3]) - Numeric.array([1, 2, 3])", [0, 0, 0]), 265 | ("Numeric.array([1, 2, 3]) * Numeric.array([1, 2, 3])", [1, 4, 9]), 266 | ("Numeric.matrixmultiply([0, 1], Numeric.array([[1, 2, 3], [4, 5, 6]]))", [4, 5, 6]), 267 | ("""Numeric.matrixmultiply(Numeric.array([ 0.46474895, 0.46348238, 0.53923529, 0.46428344, 0.50223047]), 268 | Numeric.array([[ 0.04412408, -0.075404 , -0.06693672, -0.02464404, 0.02612747, 269 | 0.02212752, 0.07636238, 0.072556 , -0.06134244, 0.02546186, 270 | -0.01727653], 271 | [-0.08557264, -0.00232236, 0.07388691, -0.00192655, 0.01600296, 272 | 0.06193477, -0.02097884, 0.04044046, -0.05679244, -0.09011306, 273 | -0.0977003 ], 274 | [-0.06398591, 0.08277569, -0.09862752, -0.06175452, -0.09487095, 275 | 0.0585492 , 0.00494566, 0.0130769 , 0.02689676, -0.05318661, 276 | 0.00723755], 277 | [-0.02247093, 0.08601486, 0.09500633, -0.01985373, -0.06269768, 278 | -0.01716621, -0.0814789 , -0.09739735, -0.03084963, 0.09573449, 279 | 0.0864492 ], 280 | [-0.06170642, 0.06903436, 0.05886129, 0.04076389, -0.02530141, 281 | -0.0604482 , -0.02426675, -0.013736 , 0.00506414, 0.02097294, 282 | -0.05462886]]))""", [-0.09508197, 0.0831217, 0.02362488, -0.03439132, -0.07341459, 0.03223229, 283 | -0.02158392, 0.00739668, -0.05210706, -0.00363136, -0.03670823]), 284 | ("""Numeric.matrixmultiply(Numeric.array([[0, 1], [2, 3], [4, 5]]), Numeric.array([0, 1]))""", [1, 3, 5]), 285 | ("""Numeric.outerproduct([0.46474895, 0.46348238, 0.53923529, 0.46428344, 0.50223047], 286 | [-0.16049719, 0.17086812, 0.1692107 , 0.17433657, 0.1738235 , 287 | 0.17292975, 0.17553493, 0.17222987, -0.17038313, 0.17725782, 288 | 0.18428386])""", 289 | [[-0.0745909, 0.07941078, 0.07864049, 0.08102274, 0.08078429, 0.08036892, 290 | 0.08157967, 0.08004365, -0.07918538, 0.08238038, 0.08564573], 291 | [-0.07438762, 0.07919436, 0.07842618, 0.08080193, 0.08056413, 0.08014989, 292 | 0.08135735, 0.07982551, -0.07896958, 0.08215587, 0.08541232], 293 | [-0.08654575, 0.09213812, 0.09124438, 0.09400843, 0.09373177, 0.09324982, 294 | 0.09465463, 0.09287243, -0.09187659, 0.09558367, 0.09937236], 295 | [-0.07451619, 0.07933124, 0.07856172, 0.08094158, 0.08070337, 0.08028842, 296 | 0.08149796, 0.07996348, -0.07910606, 0.08229787, 0.08555994], 297 | [-0.08060658, 0.08581518, 0.08498277, 0.08755714, 0.08729946, 0.08685059, 298 | 0.08815899, 0.08649909, -0.0855716, 0.08902428, 0.09255297]]), 299 | ]: 300 | count += 1 301 | o, r = testexpr(expr, result, count) 302 | print(("-" * 60)) 303 | print((count, o, r)) 304 | ocount += o 305 | rcount += r 306 | print(("%s expressions tested: %s originals passed; %s replacements passed" % (count, ocount, rcount))) 307 | -------------------------------------------------------------------------------- /calysto/ai/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Calysto/calysto/a9a6e2a402d8d1fc2c0a9bb8f3981da8a3a9e55f/calysto/ai/__init__.py -------------------------------------------------------------------------------- /calysto/chart/__init__.py: -------------------------------------------------------------------------------- 1 | 2 | from .googlechart import GoogleChart 3 | -------------------------------------------------------------------------------- /calysto/chart/googlechart.py: -------------------------------------------------------------------------------- 1 | from __future__ import print_function 2 | 3 | from IPython.display import Javascript, HTML 4 | from calysto.display import display 5 | 6 | class GoogleChart(object): 7 | USE_PNG = False 8 | SET_VAR = False 9 | 10 | id = 0 11 | def __init__(self, gtype, keys=[], data=[], **options): 12 | self.gtype = gtype 13 | self.keys = keys 14 | self.data = data 15 | self.use_png = GoogleChart.USE_PNG 16 | self.set_var = GoogleChart.SET_VAR 17 | if "use_png" in options: 18 | self.use_png = options["use_png"] 19 | del options["use_png"] 20 | if "set_var" in options: 21 | self.set_var = options["set_var"] 22 | del options["set_var"] 23 | self.options = options 24 | GoogleChart.id += 1 25 | 26 | def _toList(self, row): 27 | return [item for item in row] 28 | 29 | def _arrayToDataTable(self): 30 | nCols = 0 31 | if len(self.data) > 0: 32 | try: 33 | nCols = len(self.data[0]) 34 | except: 35 | nCols = 1 36 | 37 | if len(self.keys) != 0: 38 | table = [self._toList(self.keys)] 39 | elif nCols == 1: 40 | table = [[''] * (nCols + 1)] 41 | else: 42 | table = [[''] * (nCols)] 43 | 44 | t = 0 45 | for row in self.data: 46 | if nCols == 1: 47 | if self.gtype == "History": 48 | table.append([str(t)] + [row]) 49 | else: 50 | table.append([t] + [row]) 51 | else: 52 | table.append(self._toList(row)) 53 | t += 1 54 | return table 55 | 56 | def set_png_variable(self, variable=None): 57 | if variable is None: 58 | variable = self.get_chart_name() 59 | display(Javascript("IPython.notebook.kernel.execute('%%set %(variable)s \"' + document.charts['chart_div_%(id)s'].getImageURI() + '\"');" 60 | % {"id": self.id, 61 | "variable": variable})) 62 | print("Set '%s' to png" % variable) 63 | 64 | def show_png(self): 65 | display(HTML(""" 66 | 67 | 70 | """ % {"id": self.id})) 71 | 72 | def get_chart_name(self): 73 | return "chart_div_%(id)s" % {"id": self.id} 74 | 75 | def _repr_html_(self): 76 | return """ 77 |
78 | 79 | """ % {"gtype": self.gtype, 106 | "data": self._arrayToDataTable(), 107 | "options": self.options, 108 | "use_png": "true" if self.use_png else "false", 109 | "set_var": "true" if self.set_var else "false", 110 | "id": self.id} 111 | -------------------------------------------------------------------------------- /calysto/display.py: -------------------------------------------------------------------------------- 1 | 2 | try: 3 | from metakernel.display import * 4 | except: 5 | from IPython.display import * 6 | -------------------------------------------------------------------------------- /calysto/graphics.py: -------------------------------------------------------------------------------- 1 | """ 2 | Parts based on John Zelle's graphics.py 3 | http://mcsp.wartburg.edu/zelle/python/ppics2/index.html 4 | 5 | LICENSE: This is open-source software released under the terms of the 6 | GPL (http://www.gnu.org/licenses/gpl.html). 7 | """ 8 | 9 | __all__ = [ 10 | # The container: 11 | 'Canvas', 12 | # Shapes: 13 | 'Shape', 'Line', 'Circle', 'Text', 'Rectangle', 14 | 'Ellipse', 'Polyline', 'Polygon', 'Picture', 'Arc', 15 | 'BarChart', 'Point', 'Turtle', 16 | # Pixel-based items: 17 | 'Pixel', 'Color', 18 | # Units: 19 | 'cm', 'em', 'ex', 'mm', 'pc', 'pt', 'px' 20 | ] 21 | 22 | import svgwrite 23 | from svgwrite import cm, em, ex, mm, pc, pt, px 24 | import cairosvg 25 | import numpy 26 | import math 27 | import copy 28 | import io 29 | from cairosvg.parser import Tree 30 | 31 | def rotate(x, y, length, radians): 32 | return (x + length * math.cos(-radians), y - length * math.sin(-radians)) 33 | 34 | def distance(p1, p2): 35 | return math.sqrt((p1[0] - p2[0]) ** 2 + (p1[1] - p2[1]) ** 2) 36 | 37 | class Transform: 38 | """ 39 | Internal class for 2-D coordinate transformations 40 | """ 41 | def __init__(self, w, h, xlow, ylow, xhigh, yhigh): 42 | # w, h are width and height of window 43 | # (xlow,ylow) coordinates of lower-left [raw (0,h-1)] 44 | # (xhigh,yhigh) coordinates of upper-right [raw (w-1,0)] 45 | xspan = (xhigh-xlow) 46 | yspan = (yhigh-ylow) 47 | self.xbase = xlow 48 | self.ybase = yhigh 49 | self.xscale = xspan/float(w-1) 50 | self.yscale = yspan/float(h-1) 51 | 52 | def screen(self,x,y): 53 | # Returns x,y in screen (actually window) coordinates 54 | xs = (x-self.xbase) / self.xscale 55 | ys = (self.ybase-y) / self.yscale 56 | return int(xs+0.5),int(ys+0.5) 57 | 58 | def world(self,xs,ys): 59 | # Returns xs,ys in world coordinates 60 | x = xs*self.xscale + self.xbase 61 | y = self.ybase - ys*self.yscale 62 | return x,y 63 | 64 | class Canvas(object): 65 | def __init__(self, size=(300, 300), filename="noname.svg", **extras): 66 | if "debug" not in extras: 67 | extras["debug"] = False 68 | self.filename = filename 69 | self.size = size 70 | self.extras = extras 71 | self.shapes = [] 72 | self._viewbox = None 73 | self.matrix = [] 74 | self.fill_color = Color(128, 0, 128) 75 | self.stroke_color = Color(0, 0, 0) 76 | self.stroke_width_width = 1 77 | self.fill_opacity = None 78 | self.stroke_opacity = None 79 | self.trans = None 80 | 81 | def __repr__(self): 82 | return "" % str(self.size) 83 | 84 | def _render(self, **attribs): 85 | drawing = svgwrite.Drawing(self.filename, self.size, **self.extras) 86 | if self._viewbox: 87 | drawing.viewbox(*self._viewbox) 88 | for key in attribs: 89 | drawing.attribs[key] = attribs[key] 90 | for shape in self.shapes: 91 | shape._add(drawing) 92 | return drawing 93 | 94 | def setCoords(self, x1, y1, x2, y2): 95 | """Set coordinates of window to run from (x1,y1) in the 96 | lower-left corner to (x2,y2) in the upper-right corner.""" 97 | self.trans = Transform(self.size[0], self.size[1], x1, y1, x2, y2) 98 | 99 | def toScreen(self, x, y): 100 | if self.trans: 101 | return self.trans.screen(x,y) 102 | else: 103 | return x,y 104 | 105 | def toWorld(self, x, y): 106 | if self.trans: 107 | return self.trans.world(x,y) 108 | else: 109 | return x,y 110 | 111 | def toScaleX(self, v): 112 | if self.trans: 113 | return v/self.trans.xscale 114 | else: 115 | return v 116 | 117 | def toScaleY(self, v): 118 | if self.trans: 119 | return v/self.trans.yscale 120 | else: 121 | return v 122 | 123 | def fill(self, color): 124 | self.fill_color = color 125 | if isinstance(color, Color): 126 | self.fill_opacity = color.alpha/255 127 | else: 128 | self.fill_opacity = None 129 | 130 | def setFill(self, color): 131 | self.fill(color) 132 | 133 | def setOutline(self, color): 134 | self.stroke(color) 135 | 136 | def setWidth(self, pixels): 137 | self.stroke_width(pixels) 138 | 139 | def noFill(self): 140 | self.fill_opacity = 0.0 141 | 142 | def stroke(self, color): 143 | self.stroke_color = color 144 | if isinstance(color, Color): 145 | self.stroke_opacity = color.alpha/255 146 | else: 147 | self.stroke_opacity = None 148 | 149 | def noStroke(self): 150 | self.stroke_opacity = 0.0 151 | 152 | def stroke_width(self, width): 153 | self.stroke_width_width = width 154 | 155 | def pushMatrix(self): 156 | self.matrix.append([]) 157 | 158 | def translate(self, x, y): 159 | self.matrix[-1].append(("translate", x, y)) 160 | 161 | def rotate(self, radians): 162 | self.matrix[-1].append(("rotate", radians * 180/math.pi)) 163 | 164 | def scale(self, x, y): 165 | self.matrix[-1].append(("scale", x, y)) 166 | 167 | def popMatrix(self): 168 | self.matrix.pop() 169 | 170 | def viewbox(self, xmin, ymin, width, height): 171 | self._viewbox = (xmin, ymin, width, height) 172 | 173 | def save(self, filename=None, **attribs): 174 | format = "svg" 175 | if filename and "." in filename: 176 | format = filename.rsplit(".", 1)[1] 177 | if format == "svg": 178 | drawing = self._render(**attribs) 179 | if filename: 180 | drawing.saveas(filename) 181 | else: 182 | drawing.save() 183 | else: 184 | im = self.toPIL(**attribs) 185 | im.save(filename, format=format) 186 | 187 | def draw(self, shape): 188 | shape.canvas = self 189 | shape.t = shape.canvas.toScreen 190 | shape.tx = shape.canvas.toScaleX 191 | shape.ty = shape.canvas.toScaleY 192 | shape.matrix = copy.copy(self.matrix) 193 | if "fill" not in shape.extras: 194 | shape.extras["fill"] = self.fill_color 195 | if "stroke" not in shape.extras: 196 | shape.extras["stroke"] = self.stroke_color 197 | if "stroke-width" not in shape.extras: 198 | shape.extras["stroke-width"] = self.stroke_width_width 199 | if "stroke-opacity" not in shape.extras: 200 | if self.stroke_opacity is not None: 201 | shape.extras["stroke-opacity"] = self.stroke_opacity 202 | if "fill-opacity" not in shape.extras: 203 | if self.fill_opacity is not None: 204 | shape.extras["fill-opacity"] = self.fill_opacity 205 | self.shapes.append(shape) 206 | return self 207 | 208 | def undraw(self, shape): 209 | shape.canvas = None 210 | del self.shapes[self.shapes.index(shape)] 211 | return self 212 | 213 | def get_html(self, **attribs): 214 | if "onClick" in attribs: 215 | onClick = attribs["onClick"] 216 | del attribs["onClick"] 217 | return self._repr_svg_(**attribs) 218 | 219 | def _repr_svg_(self, **attribs): 220 | drawing = self._render(**attribs) 221 | return drawing.tostring() 222 | 223 | def __str__(self): 224 | return self._repr_svg_() 225 | 226 | def convert(self, format="png", **kwargs): 227 | """ 228 | png, ps, pdf, gif, jpg, svg 229 | returns image in format as bytes 230 | """ 231 | if format.upper() in cairosvg.SURFACES: 232 | surface = cairosvg.SURFACES[format.upper()] 233 | else: 234 | raise Exception("'%s' image format unavailable: use one of %s" % 235 | (format.upper(), list(cairosvg.SURFACES.keys()))) 236 | return surface.convert(bytestring=str(self), **kwargs) 237 | 238 | def toPIL(self, **attribs): 239 | """ 240 | Convert canvas to a PIL image 241 | """ 242 | import PIL.Image 243 | bytes = self.convert("png") 244 | sfile = io.BytesIO(bytes) 245 | pil = PIL.Image.open(sfile) 246 | return pil 247 | 248 | def toGIF(self, **attribs): 249 | """ 250 | Convert canvas to GIF bytes 251 | """ 252 | im = self.toPIL(**attribs) 253 | sfile = io.BytesIO() 254 | im.save(sfile, format="gif") 255 | return sfile.getvalue() 256 | 257 | def toArray(self): 258 | """ 259 | Converts canvas to a numpy array. 260 | """ 261 | im = self.toPIL(**attribs) 262 | return im.toarray() 263 | 264 | def start_movie(self): 265 | self.frames = [] 266 | 267 | def frame(self): 268 | self.frames.append(self.toGIF()) 269 | 270 | def save_movie(self): 271 | pass 272 | 273 | def getPixels(self): 274 | """ 275 | Return a stream of pixels from current Canvas. 276 | """ 277 | array = self.toArray() 278 | (width, height, depth) = array.size 279 | for x in range(width): 280 | for y in range(height): 281 | yield Pixel(array, x, y) 282 | 283 | def sortByZ(self): 284 | self.shapes.sort(key=lambda shape: shape.z) 285 | 286 | def clear(self): 287 | """ 288 | Clear all of the shapes. 289 | """ 290 | self.shapes.clear() 291 | 292 | class Pixel(object): 293 | """ 294 | Wrapper interface to numpy array. 295 | """ 296 | def __init__(self, array, x, y): 297 | self.array = array 298 | self.x = x 299 | self.y = y 300 | 301 | def getX(self): 302 | return self.x 303 | 304 | def getY(self): 305 | return self.y 306 | 307 | def getColor(self, x, y): 308 | return Color(*self.array[x, y]) 309 | 310 | def setColor(self, x, y, color): 311 | self.array[x, y][0] = color.red 312 | self.array[x, y][1] = color.green 313 | self.array[x, y][2] = color.blue 314 | self.array[x, y][3] = color.alpha 315 | 316 | def getRGBA(self, x, y): 317 | return self.array[x, y] 318 | 319 | class Color(object): 320 | def __init__(self, r, g=None, b=None, a=255): 321 | self.red = r 322 | if g is not None: 323 | self.green = g 324 | else: 325 | self.green = r 326 | if b is not None: 327 | self.blue = b 328 | else: 329 | self.blue = r 330 | self.alpha = a 331 | 332 | def __str__(self): 333 | def h(num): 334 | return ("0x%02x" % num)[-2:] 335 | if self.alpha != 255: 336 | return "rgba(%s,%s,%s,%s)" % (self.red, self.green, self.blue, self.alpha/255) 337 | else: 338 | return "#" + h(self.red) + h(self.green) + h(self.blue) 339 | 340 | def __getitem__(self, pos): 341 | return [self.red, self.green, self.blue][pos] 342 | 343 | class Shape(object): 344 | def __init__(self, center=(0,0), **extras): 345 | if isinstance(center, tuple): 346 | self.center = list(center) 347 | else: 348 | self.center = center # use directly, no copy 349 | self.extras = extras 350 | self.canvas = None 351 | # Transforms: 352 | self.t = self.tx = self.ty = None 353 | self.direction = 0 354 | self.pen = False 355 | self.z = 0 356 | 357 | def __repr__(self): 358 | return "" % self.center 359 | 360 | def draw(self, canvas): 361 | canvas.draw(self) 362 | return canvas 363 | 364 | def clone(self): 365 | """ 366 | """ 367 | pass 368 | #return clone, not drawn 369 | 370 | def _add(self, drawing): 371 | # Shape._add 372 | pass 373 | 374 | def _apply_matrices(self, shape, matrices): 375 | for matrix in matrices: 376 | for transform in matrix: 377 | self._apply_transform(shape, *transform) 378 | 379 | def _apply_transform(self, shape, transform, *args): 380 | if transform == "rotate": 381 | shape.rotate(*args) 382 | elif transform == "translate": 383 | shape.translate(*args) 384 | else: 385 | raise Exception("invalid transform: " + transform) 386 | 387 | def undraw(self): 388 | self.canvas.undraw(self) 389 | self.canvas = None 390 | self.t = self.tx = self.ty = None 391 | 392 | def forward(self, distance): 393 | start = self.center[:] 394 | self.center[0] += distance * math.cos(self.direction) 395 | self.center[1] += distance * math.sin(self.direction) 396 | if self.pen and self.canvas: 397 | Line(start, self.center, **self.extras).draw(self.canvas) 398 | return self.canvas 399 | 400 | def turn(self, degrees): 401 | """ 402 | Turn the shape direction by these degrees. 403 | """ 404 | self.direction -= (math.pi / 180.0) * degrees 405 | 406 | def fill(self, color): 407 | self.extras["fill"] = str(color) 408 | if isinstance(color, Color): 409 | self.extras["fill-opacity"] = color.alpha/255 410 | else: 411 | self.extras["fill-opacity"] = 1.0 412 | 413 | def setFill(self, color): 414 | self.fill(color) 415 | 416 | def setOutline(self, color): 417 | self.stroke(color) 418 | 419 | def setWidth(self, pixels): 420 | self.stroke_width(pixels) 421 | 422 | def noFill(self): 423 | self.extras["fill-opacity"] = 0.0 424 | 425 | def noStroke(self): 426 | self.extras["stroke-opacity"] = 0.0 427 | 428 | def stroke(self, color): 429 | self.extras["stroke"] = str(color) 430 | if isinstance(color, Color): 431 | self.extras["stroke-opacity"] = color.alpha/255 432 | else: 433 | self.extras["stroke-opacity"] = 1.0 434 | 435 | def stroke_width(self, width): 436 | self.extras["stroke-width"] = width 437 | 438 | def moveToTop(self): 439 | self.canvas.shapes.remove(self) 440 | self.canvas.shapes.append(self) 441 | return self.canvas 442 | 443 | def moveToBottom(self): 444 | self.canvas.shapes.remove(self) 445 | self.canvas.shapes.insert(0, self) 446 | return self.canvas 447 | 448 | class Circle(Shape): 449 | def __init__(self, center=(0,0), radius=1, **extras): 450 | super(Circle, self).__init__(center) 451 | self.radius = radius 452 | self.extras = extras 453 | 454 | def getP1(self): 455 | """ 456 | Left, upper point 457 | """ 458 | return Point(self.center[0] - self.radius, 459 | self.center[1] - self.radius) 460 | 461 | def getP2(self): 462 | """ 463 | Right, lower point 464 | """ 465 | return Point(self.center[0] + self.radius, 466 | self.center[1] + self.radius) 467 | 468 | def __repr__(self): 469 | return "" % (self.center, self.radius) 470 | 471 | def moveTo(self, center): 472 | self.center[:] = center # use directly, no copy 473 | return self.canvas 474 | 475 | def move(self, delta, delta_y=None): 476 | if delta_y is None: 477 | self.center[:] = [self.center[0] + delta[0], self.center[1] + delta[1]] 478 | else: 479 | self.center[:] = [self.center[0] + delta, self.center[1] + delta_y] 480 | return self.canvas 481 | 482 | def _add(self, drawing): 483 | if self.canvas.trans: 484 | shape = drawing.ellipse(center=self.t(*self.center), 485 | r=(self.tx(self.radius), 486 | self.ty(self.radius)), 487 | **self.extras) 488 | else: 489 | shape = drawing.circle(center=self.center, r=self.radius, **self.extras) 490 | self._apply_matrices(shape, self.matrix) 491 | drawing.add(shape) 492 | 493 | class Point(Circle): 494 | def __init__(self, x, y, **extras): 495 | super(Point, self).__init__((x, y), 1) 496 | 497 | def __repr__(self): 498 | return "" % (self.center[0], self.center[1]) 499 | 500 | def _add(self, drawing): 501 | if self.canvas.trans: 502 | shape = drawing.circle(center=self.t(*self.center), r=self.radius, **self.extras) 503 | else: 504 | shape = drawing.circle(center=self.center, r=self.radius, **self.extras) 505 | self._apply_matrices(shape, self.matrix) 506 | drawing.add(shape) 507 | 508 | def __getitem__(self, pos): 509 | return self.center[pos] 510 | 511 | def __iter__(self): 512 | yield self.center[0] 513 | yield self.center[1] 514 | 515 | def getX(self): 516 | return self.center[0] 517 | 518 | def getY(self): 519 | return self.center[1] 520 | 521 | class Arc(Shape): 522 | def __init__(self, center=(0,0), radius=1, start=0, stop=0, **extras): 523 | super(Arc, self).__init__(center) 524 | self.radius = radius 525 | self.start = start 526 | self.stop = stop 527 | self.extras = extras 528 | 529 | def getP1(self): 530 | """ 531 | Left, upper point 532 | """ 533 | return Point(self.center[0] - self.radius, 534 | self.center[1] - self.radius) 535 | 536 | def getP2(self): 537 | """ 538 | Right, lower point 539 | """ 540 | return Point(self.center[0] + self.radius, 541 | self.center[1] + self.radius) 542 | 543 | def __repr__(self): 544 | return "" % (self.center, self.radius) 545 | 546 | def moveTo(self, center): 547 | self.center[:] = center # use directly, no copy 548 | return self.canvas 549 | 550 | def move(self, delta, delta_y=None): 551 | if delta_y is None: 552 | self.center[:] = [self.center[0] + delta[0], self.center[1] + delta[1]] 553 | else: 554 | self.center[:] = [self.center[0] + delta, self.center[1] + delta_y] 555 | return self.canvas 556 | 557 | def _add(self, drawing): 558 | current = self.start 559 | if self.canvas.trans: 560 | points = [self.t(*self.center)] 561 | else: 562 | points = [(self.center[0], self.center[1])] 563 | while current < self.stop: 564 | if self.canvas.trans: 565 | c = self.t(*self.center) 566 | # FIXME: allow scale in x and y dimensions: 567 | points.append(rotate(c[0], c[1], self.tx(self.radius), current)) 568 | else: 569 | points.append(rotate(self.center[0], self.center[1], self.radius, current)) 570 | current += math.pi/180 * 5.0 # every five degrees 571 | extras = copy.copy(self.extras) 572 | extras["stroke-opacity"] = 0.0 573 | shape = drawing.polygon(points=points, **extras) 574 | self._apply_matrices(shape, self.matrix) 575 | drawing.add(shape) 576 | extras = copy.copy(self.extras) 577 | extras["fill-opacity"] = 0.0 578 | shape = drawing.polyline(points=points[1:], **extras) 579 | self._apply_matrices(shape, self.matrix) 580 | drawing.add(shape) 581 | 582 | class Line(Shape): 583 | def __init__(self, start=(0,0), end=(0,0), **extras): 584 | super(Line, self).__init__() 585 | if "stroke" not in extras: 586 | extras["stroke"] = Color(0, 0, 0) 587 | if "stroke-width" not in extras: 588 | extras["stroke-width"] = 1 589 | if isinstance(start, tuple): 590 | self.start = list(start) 591 | else: 592 | self.start = start # use directly, no copy 593 | if isinstance(end, tuple): 594 | self.end = list(end) 595 | else: 596 | self.end = end # use directly, no copy 597 | self.extras = extras 598 | 599 | def __repr__(self): 600 | return "" % (self.start, self.end) 601 | 602 | def moveTo(self, start): 603 | diff_x = start[0] - self.start[0] 604 | diff_y = start[1] - self.start[1] 605 | self.start[:] = start 606 | self.end[:] = self.end[0] + diff_x, self.end[1] + diff_y 607 | return self.canvas 608 | 609 | def move(self, delta, delta_y=None): 610 | if delta_y is None: 611 | self.start[:] = self.start[0] + delta[0], self.start[1] + delta[1] 612 | self.end[:] = self.end[0] + delta[0], self.end[1] + delta[1] 613 | else: 614 | self.start[:] = self.start[0] + delta, self.start[1] + delta_y 615 | self.end[:] = self.end[0] + delta, self.end[1] + delta_y 616 | return self.canvas 617 | 618 | def _add(self, drawing): 619 | if self.canvas.trans: 620 | shape = drawing.line(start=self.t(*self.start), 621 | end=self.t(*self.end), **self.extras) 622 | else: 623 | shape = drawing.line(start=self.start, end=self.end, **self.extras) 624 | self._apply_matrices(shape, self.matrix) 625 | drawing.add(shape) 626 | 627 | class Turtle(object): 628 | def __init__(self, canvas, center, angle=0): 629 | self.canvas = canvas 630 | self.x = center[0] 631 | self.y = center[1] 632 | self.angle = angle * math.pi/180.0 633 | self.angle_units = "degrees" 634 | self.pen = "down" 635 | self.fill_color = Color(128, 0, 128) 636 | self.stroke = Color(0, 0, 0) 637 | self.stroke_width = 1 638 | self.fill_opacity = None 639 | self.stroke_opacity = None 640 | self.arrow = None 641 | self.draw_arrow() 642 | 643 | 644 | def draw_arrow(self): 645 | if self.arrow: 646 | self.arrow.undraw() 647 | self.canvas.pushMatrix() 648 | self.arrow = Polygon([(-10, 5), 649 | ( 0, 0), 650 | (-10, -5), 651 | ( -5, 0)]) 652 | self.canvas.translate(self.x, self.y) 653 | self.canvas.rotate(self.angle) 654 | self.arrow.draw(self.canvas) 655 | self.canvas.popMatrix() 656 | 657 | def forward(self, distance): 658 | new_x, new_y = rotate(self.x, self.y, distance, self.angle) 659 | if self.pen == "down": 660 | line = Line((self.x, self.y), (new_x, new_y), 661 | **{"stroke": self.stroke, 662 | "stroke-width": self.stroke_width}) 663 | line.draw(self.canvas) 664 | self.x, self.y = new_x, new_y 665 | self.draw_arrow() 666 | return self.canvas 667 | 668 | def backward(self, distance): 669 | return self.forward(-distance) 670 | 671 | def left(self, angle): 672 | return self.right(-angle) 673 | 674 | def right(self, angle): 675 | if self.angle_units == "degrees": 676 | angle = angle * math.pi/180 677 | self.angle += angle 678 | self.angle = self.angle % (math.pi * 2) 679 | self.draw_arrow() 680 | return self.canvas 681 | 682 | def goto(self, new_x, new_y): 683 | if self.pen == "down": 684 | line = Line((self.x, self.y), (new_x, new_y), 685 | **{"stroke": self.stroke, 686 | "stroke-width": self.stroke_width}) 687 | line.draw(self.canvas) 688 | self.x, self.y = new_x, new_y 689 | self.draw_arrow() 690 | return self.canvas 691 | 692 | def penup(self): 693 | self.pen = "up" 694 | 695 | def pendown(self): 696 | self.pen = "down" 697 | 698 | class Text(Shape): 699 | def __init__(self, text="", start=(0,0), **extras): 700 | super(Text, self).__init__() 701 | self.text = text 702 | if isinstance(start, tuple): 703 | self.start = list(start) 704 | else: 705 | self.start = start # use directly, no copy 706 | self.extras = extras 707 | 708 | def __repr__(self): 709 | return "" % self.start 710 | 711 | def moveTo(self, start): 712 | self.start[:] = start 713 | return self.canvas 714 | 715 | def move(self, delta, delta_y=None): 716 | if delta_y is None: 717 | self.start[:] = self.start[0] + delta[0], self.start[1] + delta[1] 718 | else: 719 | self.start[:] = self.start[0] + delta, self.start[1] + delta_y 720 | return self.canvas 721 | 722 | def _add(self, drawing): 723 | if self.canvas.trans: 724 | shape = drawing.text(text=self.text, insert=self.t(*self.start), **self.extras) 725 | else: 726 | shape = drawing.text(text=self.text, insert=self.start, **self.extras) 727 | self._apply_matrices(shape, self.matrix) 728 | drawing.add(shape) 729 | 730 | class Rectangle(Shape): 731 | def __init__(self, start=(0,0), size=(1,1), rx=None, ry=None, **extras): 732 | super(Rectangle, self).__init__() 733 | if isinstance(start, tuple): 734 | self.start = list(start) 735 | else: 736 | self.start = start # use directly, no copy 737 | if isinstance(size, tuple): 738 | self.size = list(size) 739 | else: 740 | self.size = size # use directly, no copy 741 | self.rx = rx 742 | self.ry = ry 743 | self.extras = extras 744 | 745 | def __repr__(self): 746 | return "" % (self.start, self.size) 747 | 748 | def moveTo(self, start): 749 | self.start[:] = start 750 | return self.canvas 751 | 752 | def move(self, delta, delta_y=None): 753 | if delta_y is None: 754 | self.start[:] = self.start[0] + delta[0], self.start[1] + delta[1] 755 | else: 756 | self.start[:] = self.start[0] + delta, self.start[1] + delta_y 757 | return self.canvas 758 | 759 | def _add(self, drawing): 760 | if self.canvas.trans: 761 | shape = drawing.rect(insert=self.t(*self.start), 762 | size=(self.tx(self.size[0]), self.ty(self.size[1])), 763 | rx=self.rx, 764 | ry=self.ry, 765 | **self.extras) 766 | else: 767 | shape = drawing.rect(insert=self.start, size=self.size, rx=self.rx, ry=self.ry, **self.extras) 768 | self._apply_matrices(shape, self.matrix) 769 | drawing.add(shape) 770 | 771 | class Ellipse(Shape): 772 | def __init__(self, center=(0,0), radii=(1,1), **extras): 773 | super(Ellipse, self).__init__(center) 774 | if isinstance(radii, tuple): 775 | self.radii = list(radii) 776 | else: 777 | self.radii = radii 778 | self.extras = extras 779 | 780 | def __repr__(self): 781 | return "" % str(self.radii) 782 | 783 | def moveTo(self, center): 784 | self.center[:] = center # use directly, no copy 785 | return self.canvas 786 | 787 | def move(self, delta, delta_y=None): 788 | if delta_y is None: 789 | self.center[:] = [self.center[0] + delta[0], self.center[1] + delta[1]] 790 | else: 791 | self.center[:] = [self.center[0] + delta, self.center[1] + delta_y] 792 | return self.canvas 793 | 794 | def _add(self, drawing): 795 | if self.canvas.trans: 796 | shape = drawing.ellipse(center=self.t(*self.center), 797 | r=(self.tx(self.radii[0]), 798 | self.ty(self.radii[1])), 799 | **self.extras) 800 | else: 801 | shape = drawing.ellipse(center=self.center, r=self.radii, **self.extras) 802 | self._apply_matrices(shape, self.matrix) 803 | drawing.add(shape) 804 | 805 | class Polyline(Shape): 806 | def __init__(self, points=[], **extras): 807 | super(Polyline, self).__init__() 808 | self.points = points # not a copy FIXME 809 | self.extras = extras 810 | 811 | def __repr__(self): 812 | return "" % str(self.points) 813 | 814 | def moveTo(self, start): 815 | diff_x = start[0] - self.points[0][0] 816 | diff_y = start[1] - self.points[0][1] 817 | for i in range(len(self.points)): 818 | self.points[i] = self.points[i][0] + diff_x, self.points[i][1] + diff_y 819 | return self.canvas 820 | 821 | def move(self, delta, delta_y=None): 822 | for i in range(len(self.points)): 823 | if delta_y is None: 824 | self.points[i] = self.points[i][0] + delta[0], self.points[i][1] + delta[1] 825 | else: 826 | self.points[i] = self.points[i][0] + delta, self.points[i][1] + delta_y 827 | return self.canvas 828 | 829 | def _add(self, drawing): 830 | if self.canvas.trans: 831 | shape = drawing.polyline(points=map(lambda p: self.t(*p), self.points), **self.extras) 832 | else: 833 | shape = drawing.polyline(points=self.points, **self.extras) 834 | self._apply_matrices(shape, self.matrix) 835 | drawing.add(shape) 836 | 837 | class Polygon(Shape): 838 | def __init__(self, points=[], **extras): 839 | super(Polygon, self).__init__() 840 | self.points = points # not a copy FIXME 841 | self.extras = extras 842 | 843 | def __repr__(self): 844 | return "" % str(self.points) 845 | 846 | def moveTo(self, start): 847 | diff_x = start[0] - self.points[0][0] 848 | diff_y = start[1] - self.points[0][1] 849 | for i in range(len(self.points)): 850 | self.points[i] = self.points[i][0] + diff_x, self.points[i][1] + diff_y 851 | return self.canvas 852 | 853 | def move(self, delta, delta_y=None): 854 | for i in range(len(self.points)): 855 | if delta_y is None: 856 | self.points[i] = self.points[i][0] + delta[0], self.points[i][1] + delta[1] 857 | else: 858 | self.points[i] = self.points[i][0] + delta, self.points[i][1] + delta_y 859 | return self.canvas 860 | 861 | def _add(self, drawing): 862 | if self.canvas.trans: 863 | shape = drawing.polygon(points=map(lambda p: self.t(*p), self.points), **self.extras) 864 | else: 865 | shape = drawing.polygon(points=self.points, **self.extras) 866 | self._apply_matrices(shape, self.matrix) 867 | drawing.add(shape) 868 | 869 | class Picture(Shape): 870 | def __init__(self, href, start=None, size=None, **extras): 871 | super(Picture, self).__init__() 872 | self.href = href 873 | self.start = start 874 | self.size = size 875 | self.extras = extras 876 | 877 | def __repr__(self): 878 | return "" % (self.start, self.size) 879 | 880 | def moveTo(self, start): 881 | self.start[:] = start 882 | return self.canvas 883 | 884 | def move(self, delta, delta_y=None): 885 | if delta_y is None: 886 | self.start[:] = self.start[0] + delta[0], self.start[1] + delta[1] 887 | else: 888 | self.start[:] = self.start[0] + delta, self.start[1] + delta_y 889 | return self.canvas 890 | 891 | def _add(self, drawing): 892 | if self.canvas.trans: 893 | shape = drawing.image(self.href, 894 | insert=self.t(*self.start), 895 | size=self.t(*self.size), **self.extras) 896 | else: 897 | shape = drawing.image(self.href, insert=self.start, size=self.size, **self.extras) 898 | self._apply_matrices(shape, self.matrix) 899 | drawing.add(shape) 900 | 901 | class Plot(Canvas): 902 | """ 903 | """ 904 | def __init__(self, *args, **kwargs): 905 | super(Plot, self).__init__(*args, **kwargs) 906 | ## Borders: 907 | self.frame_size = 20 908 | self.x_offset = 20 909 | ## (upper left, width, height) 910 | self.plot_boundaries = [self.frame_size + self.x_offset, 911 | self.frame_size, 912 | self.size[0] - 2 * self.frame_size - self.x_offset, 913 | self.size[1] - 2 * self.frame_size] 914 | self.plot_width = self.plot_boundaries[2] - self.plot_boundaries[0] 915 | self.plot_height = self.plot_boundaries[3] - self.plot_boundaries[1] 916 | self.plot_origin = (self.plot_boundaries[0], self.size[1] - self.frame_size * 2) 917 | self.background = Rectangle((self.plot_boundaries[0], 918 | self.plot_boundaries[1]), 919 | (self.plot_width, self.plot_height)) 920 | self.background.noFill() 921 | self.background.stroke(Color(0, 0, 0)) 922 | self.draw(self.background) 923 | 924 | class BarChart(Plot): 925 | """ 926 | """ 927 | def __init__(self, *args, **kwargs): 928 | super(BarChart, self).__init__(*args, **kwargs) 929 | self.data = kwargs.get("data", []) 930 | self.bar_width = self.plot_width / len(self.data) 931 | max_count = max(self.data) 932 | start_x = self.plot_origin[0] + self.bar_width * 0.125 933 | start_y = self.plot_origin[1] 934 | count = 0 935 | for item in self.data: 936 | self.draw( Rectangle((start_x + self.bar_width * count, 937 | start_y - item/max_count * self.plot_height), 938 | (self.bar_width * 0.75, 939 | item/max_count * self.plot_height))) 940 | count += 1 941 | # X legend: 942 | self.labels = kwargs.get("labels", []) 943 | count = 0 944 | for item in self.labels: 945 | self.draw( Text(item, (start_x + self.bar_width * count + self.bar_width/3, 946 | start_y + self.frame_size))) 947 | count += 1 948 | # Y legend: 949 | for count in range(4 + 1): 950 | self.draw( Text(str(max_count * count/4)[0:5], 951 | (0, 1.2 * self.frame_size + self.plot_height - count/4 * self.plot_height))) 952 | 953 | #g(**extras) # Group 954 | #symbol(**extras) 955 | #svg(insert=None, size=None, **extras) 956 | #use(href, insert=None, size=None, **extras) 957 | #a(href, target='_blank', **extras) # Link 958 | #marker(insert=None, size=None, orient=None, **extras) 959 | #script(href=None, content='', **extras) 960 | #style(content='', **extras) 961 | #linearGradient(start=None, end=None, inherit=None, **extras) 962 | #radialGradient(center=None, r=None, focal=None, inherit=None, **extras) 963 | #mask(start=None, size=None, **extras) 964 | #clipPath(**extras) 965 | #set(element=None, **extras) 966 | 967 | #tspan(text, insert=None, x=[], y=[], dx=[], dy=[], rotate=[], **extras) # TextSpan 968 | #tref(element, **extras) 969 | #textPath(path, text, startOffset=None, method='align', spacing='exact', **extras) 970 | #textArea(text=None, insert=None, size=None, **extras) 971 | #path(d=None, **extras) 972 | 973 | #animate(element=None, **extras) 974 | #animateColor(element=None, **extras) 975 | #animateMotion(element=None, **extras) 976 | #animateTransform(transform, element=None, **extras) 977 | #filter(start=None, size=None, resolution=None, inherit=None, **extras) 978 | -------------------------------------------------------------------------------- /calysto/images/logo-32x32.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Calysto/calysto/a9a6e2a402d8d1fc2c0a9bb8f3981da8a3a9e55f/calysto/images/logo-32x32.png -------------------------------------------------------------------------------- /calysto/images/logo-64x64.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Calysto/calysto/a9a6e2a402d8d1fc2c0a9bb8f3981da8a3a9e55f/calysto/images/logo-64x64.png -------------------------------------------------------------------------------- /calysto/magics/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Calysto/calysto/a9a6e2a402d8d1fc2c0a9bb8f3981da8a3a9e55f/calysto/magics/__init__.py -------------------------------------------------------------------------------- /calysto/magics/simulation_magic.py: -------------------------------------------------------------------------------- 1 | """ 2 | ================ 3 | simulation magic 4 | ================ 5 | 6 | """ 7 | 8 | 9 | from metakernel import Magic, option 10 | from calysto.display import display 11 | from calysto.simulation import * 12 | 13 | 14 | class SimulationMagic(Magic): 15 | 16 | def cell_simulation(self): 17 | """ 18 | """ 19 | if "def brain" in self.code: 20 | env = {} 21 | exec(self.code, env) 22 | brain = env["brain"] 23 | from calysto.display import display 24 | from calysto.simulation import DiscreteView, get_robot 25 | sim = DiscreteView("Ladybug1") 26 | robot = get_robot() 27 | 28 | def update(robot): 29 | ox, oy = robot.x, robot.y 30 | brain(robot) 31 | if (ox, oy) == (robot.x, robot.y): 32 | robot.set_energy(robot.energy - 0.75) # cost of being alive 33 | 34 | robot.update = lambda: update(robot) 35 | display(sim.sim_widgets) 36 | self.evaluate = False 37 | else: 38 | from calysto.display import display 39 | from calysto.simulation import DiscreteView, get_robot 40 | vsim = DiscreteView("Ladybug1") 41 | robot = get_robot() 42 | robot.rules = self.code 43 | display(vsim.sim_widgets) 44 | self.evaluate = False 45 | 46 | 47 | def register_magics(kernel): 48 | kernel.register_magics(SimulationMagic) 49 | 50 | def register_ipython_magics(): 51 | from metakernel import IPythonKernel 52 | from IPython.core.magic import register_cell_magic 53 | kernel = IPythonKernel() 54 | magic = SimulationMagic(kernel) 55 | 56 | @register_cell_magic 57 | def simulation(line, cell): 58 | magic.code = cell 59 | magic.cell_simulation() 60 | -------------------------------------------------------------------------------- /calysto/simulation.py: -------------------------------------------------------------------------------- 1 | from calysto.graphics import (Canvas, Polygon, Rectangle, Circle, 2 | Color, Line, Ellipse, Arc, Text) 3 | import numpy as np 4 | import traceback 5 | import threading 6 | import random 7 | import math 8 | import time 9 | import sys 10 | import os 11 | 12 | SIMULATION = None 13 | 14 | def rotateAround(x1, y1, length, angle): 15 | return Point(x1 + length * math.cos(-angle), y1 - length * math.sin(-angle)) 16 | 17 | def distance(x1, y1, x2, y2): 18 | return math.sqrt((x1 - x2) * (x1 - x2) + (y1 - y2) * (y1 - y2)) 19 | 20 | def pdistance(x1, y1, x2, y2, patches_size): 21 | pw = patches_size[0] 22 | ph = patches_size[1] 23 | min_x_diff = min(abs((x1 + pw) - x2), abs(x1 - x2), abs(x1 - (x2 + pw))) 24 | min_y_diff = min(abs((y1 + ph) - y2), abs(y1 - y2), abs(y1 - (y2 + ph))) 25 | return math.sqrt(min_x_diff ** 2 + min_y_diff ** 2) 26 | 27 | class Point(object): 28 | def __init__(self, x, y, z=0): 29 | self.x = x 30 | self.y = y 31 | self.z = z 32 | 33 | def __len__(self): 34 | return 2 35 | 36 | def __iter__(self): 37 | yield self.x 38 | yield self.y 39 | 40 | def __getitem__(self, pos): 41 | if pos == 0: 42 | return self.x 43 | elif pos == 1: 44 | return self.y 45 | elif pos == 2: 46 | return self.z 47 | 48 | class Drawable(object): 49 | def __init__(self, points, color): 50 | self.points = points 51 | self.color = color 52 | 53 | def draw(self, canvas): 54 | polygon = Polygon([(p.x, p.y) for p in self.points]) 55 | polygon.fill(self.color) 56 | polygon.noStroke() 57 | polygon.draw(canvas) 58 | 59 | class Wall(Drawable): 60 | """ 61 | Wall object in the simulated world. 62 | """ 63 | 64 | class Simulation(object): 65 | def __init__(self, w, h, *robots, **kwargs): 66 | global SIMULATION 67 | background_color = kwargs.get("background_color", None) 68 | draw_walls = kwargs.get("draw_walls", True) 69 | self.w = w 70 | self.h = h 71 | if background_color: 72 | self.background_color = background_color 73 | else: 74 | self.background_color = Color(0, 128, 0) 75 | self.scale = 250 76 | self.at_x = 0 77 | self.at_y = 0 78 | self.robots = [] 79 | self.walls = [] 80 | self.shapes = [] 81 | if draw_walls: 82 | self.makeWall(0, 0, self.w, 10, Color(128, 0, 128)) 83 | self.makeWall(0, 0, 10, self.h, Color(128, 0, 128)) 84 | self.makeWall(0, self.h - 10.0, self.w, self.h, Color(128, 0, 128)) 85 | self.makeWall(self.w - 10.0, 0.0, 10, self.h, Color(128, 0, 128)) 86 | self.need_to_stop = threading.Event() 87 | self.is_running = threading.Event() 88 | self.brain_running = threading.Event() 89 | self.paused = threading.Event() 90 | self.clock = 0.0 91 | self.sim_time = .1 # every update advances this much time 92 | self.gui_time = .25 # update screen this often; used in watch 93 | self.gui_update = 5 # used in widget-based view 94 | for robot in robots: 95 | self.addRobot(robot) 96 | self.error = None 97 | SIMULATION = self 98 | 99 | def reset(self): 100 | self.clock = 0.0 101 | for robot in self.robots: 102 | robot.stop() 103 | robot.x = robot.ox 104 | robot.y = robot.oy 105 | robot.direction = robot.odirection 106 | if robot.brain: 107 | self.runBrain(robot.brain) 108 | 109 | def start_sim(self, gui=True, set_values={}, error=None): 110 | """ 111 | Run the simulation in the background, showing the GUI by default. 112 | """ 113 | self.error = error 114 | if not self.is_running.is_set(): 115 | def loop(): 116 | self.need_to_stop.clear() 117 | self.is_running.set() 118 | for robot in self.robots: 119 | if robot.brain: 120 | self.runBrain(robot.brain) 121 | count = 0 122 | while not self.need_to_stop.isSet(): 123 | if not self.paused.is_set(): 124 | self.clock += self.sim_time 125 | for robot in self.robots: 126 | try: 127 | robot.update() 128 | except Exception as exc: 129 | self.need_to_stop.set() 130 | if error: 131 | error.value = "Error: %s. Now stopping simulation." % str(exc) 132 | else: 133 | raise 134 | if gui: 135 | self.draw() 136 | if count % self.gui_update == 0: 137 | if "canvas" in set_values: 138 | set_values["canvas"].value = str(self.render()) 139 | if "energy" in set_values: 140 | if len(self.robots) > 0: 141 | set_values["energy"].value = str(self.robots[0].energy) 142 | count += 1 143 | self.realsleep(self.sim_time) 144 | if self.robots[0].energy <= 0: 145 | self.need_to_stop.set() 146 | self.is_running.clear() 147 | for robot in self.robots: 148 | robot.stop() 149 | threading.Thread(target=loop).start() 150 | 151 | def render(self): 152 | canvas = Canvas(size=(self.w, self.h)) 153 | rect = Rectangle((self.at_x, self.at_y), (self.w, self.h)) 154 | rect.fill(self.background_color) 155 | rect.noStroke() 156 | rect.draw(canvas) 157 | for wall in self.walls: 158 | wall.draw(canvas) 159 | for shape in self.shapes: 160 | shape.draw(canvas) 161 | for robot in self.robots: 162 | robot.draw(canvas) 163 | if self.brain_running.is_set(): 164 | if not self.paused.is_set(): 165 | state = "Brain Running..." 166 | else: 167 | state = "Brain paused!" 168 | else: 169 | if not self.paused.is_set(): 170 | state = "Idle" 171 | else: 172 | state = "Paused" 173 | clock = Text("%.1f %s" % (self.clock, state), (15, self.h - 15)) 174 | clock.fill(Color(255, 255, 255)) 175 | clock.stroke(Color(255, 255, 255)) 176 | clock.stroke_width(1) 177 | clock.draw(canvas) 178 | return canvas 179 | 180 | def draw(self): 181 | """ 182 | Render and draw the world and robots. 183 | """ 184 | from calysto.display import display, clear_output 185 | canvas = self.render() 186 | clear_output(wait=True) 187 | display(canvas) 188 | 189 | def watch(self): 190 | """ 191 | Watch a running simulation. 192 | """ 193 | while True: 194 | self.draw() 195 | self.realsleep(self.gui_time) 196 | 197 | def stop_sim(self): 198 | self.need_to_stop.set() 199 | time.sleep(.250) 200 | for robot in self.robots: 201 | robot.stop() 202 | 203 | def realsleep(self, seconds): 204 | """ 205 | Realtime sleep, to not overwhelm the system. 206 | """ 207 | self.need_to_stop.wait(seconds) 208 | 209 | def sleep(self, seconds): 210 | """ 211 | Sleep in simulated time. 212 | """ 213 | start = self.time() 214 | while (self.time() - start < seconds and 215 | not self.need_to_stop.is_set()): 216 | self.need_to_stop.wait(self.sim_time) 217 | 218 | def time(self): 219 | return self.clock 220 | 221 | def runBrain(self, f): 222 | """ 223 | Run a brain program in the background. 224 | """ 225 | if self.error: 226 | self.error.value = "" 227 | def wrapper(): 228 | self.brain_running.set() 229 | try: 230 | f() 231 | except KeyboardInterrupt: 232 | # Just stop 233 | pass 234 | except Exception as e: 235 | if self.error: 236 | self.error.value = "
" + traceback.format_exc() + "
" 237 | else: 238 | raise 239 | finally: 240 | self.brain_running.clear() 241 | # Otherwise, will show error 242 | threading.Thread(target=wrapper).start() 243 | 244 | def makeWall(self, x, y, w, h, color): 245 | wall = Wall([Point(x, y), 246 | Point(x + w, y), 247 | Point(x + w, y + h), 248 | Point(x, y + h)], 249 | color) 250 | self.walls.append(wall) 251 | 252 | def set_gui_update(self, value): 253 | self.gui_update = value 254 | 255 | def setScale(self, s): 256 | ## scale the world... > 1 make it bigger 257 | self.scale = s * 250 258 | 259 | def addRobot(self, robot): 260 | self.robots.append(robot) 261 | robot.setSimulation(self) 262 | 263 | class DiscreteSimulation(Simulation): 264 | def __init__(self, *args, **kwargs): 265 | super(DiscreteSimulation, self).__init__(*args, **kwargs) 266 | self.pwidth = kwargs.get("pwidth", 10) 267 | self.pheight = kwargs.get("pheight", 10) 268 | self.psize = (int(self.w/self.pwidth), int(self.h/self.pheight)) 269 | self.items = {} 270 | self.items["f"] = self.drawFood 271 | self.initialize() 272 | self.gui_update = 1 273 | 274 | def initialize(self): 275 | self.patches = [[None for h in range(self.psize[1])] for w in range(self.psize[0])] 276 | 277 | def reset(self): 278 | for robot in self.robots: 279 | robot.stop() 280 | robot.x = robot.ox 281 | robot.y = robot.oy 282 | robot.direction = robot.odirection 283 | robot.energy = robot.oenergy 284 | robot.history = [robot.energy] 285 | 286 | def addCluster(self, cx, cy, item, count, lam_percent=.25): 287 | """ 288 | Add a Poisson cluster of count items around (x,y). 289 | """ 290 | dx, dy = map(lambda v: v * lam_percent, self.psize) 291 | total = 0 292 | while total < count: 293 | points = np.random.poisson(lam=(dx, dy), size=(count, 2)) 294 | for x, y in points: 295 | px, py = (int(x - dx + cx), int(y - dy + cy)) 296 | if self.getPatch(px, py) is None: 297 | self.setPatch(px, py, item) 298 | total += 1 299 | if total == count: 300 | break 301 | 302 | def setPatch(self, px, py, item): 303 | self.patches[int(px) % self.psize[0]][int(py) % self.psize[1]] = item 304 | 305 | def getPatch(self, px, py): 306 | return self.patches[px % self.psize[0]][py % self.psize[1]] 307 | 308 | def getPatchLocation(self, px, py): 309 | return (px % self.psize[0], py % self.psize[1]) 310 | 311 | def render(self): 312 | self.shapes.clear() 313 | for x in range(0, int(self.w/self.pwidth)): 314 | for y in range(0, int(self.h/self.pheight)): 315 | if self.patches[x][y] and self.patches[x][y] in self.items: 316 | self.items[self.patches[x][y]](x, y) 317 | return super(DiscreteSimulation, self).render() 318 | 319 | def drawFood(self, px, py): 320 | center = (px * self.pwidth + self.pwidth/2, 321 | py * self.pheight + self.pheight/2) 322 | food = Circle(center, 5) 323 | food.fill("yellow") 324 | self.shapes.append(food) 325 | 326 | def drawWall(self, px, py): 327 | center = (px * self.pwidth + self.pwidth/2, 328 | py * self.pheight + self.pheight/2) 329 | food = Circle(center, 5) 330 | food.fill("purple") 331 | self.shapes.append(food) 332 | 333 | class Hit(object): 334 | def __init__(self, x, y, distance, color, start_x, start_y): 335 | self.x = x 336 | self.y = y 337 | self.distance = distance 338 | self.color = color 339 | self.start_x = start_x 340 | self.start_y = start_y 341 | 342 | class Robot(object): 343 | def __init__(self, x, y, direction): 344 | """ 345 | direction is in radians 346 | """ 347 | self.simulation = None 348 | self.brain = None 349 | self.x = self.ox = x 350 | self.y = self.oy= y 351 | self.direction = self.odirection = direction 352 | self.debug = False 353 | self.vx = 0.0 ## velocity in x direction 354 | self.vy = 0.0 ## velocity in y direction 355 | self.va = 0.0 ## turn velocity 356 | ## sensors 357 | self.stalled = False 358 | self.bounding_box = [Point(0, 0)] * 4 359 | self.color = Color(255, 0, 0) 360 | self.ir_sensors = [None] * 2 # Hits 361 | self.max_ir = 1/5 # ratio of robot 362 | self.camera = [None] * 256 # Hits 363 | self.take_picture = threading.Event() 364 | self.picture_ready = threading.Event() 365 | self.body_points = [] 366 | 367 | def __enter__(self): 368 | return self 369 | 370 | def __exit__(self, exc_type, exc_val, exc_tb): 371 | self.stop() 372 | 373 | def setSimulation(self, simulation): 374 | self.simulation = simulation 375 | sx = [0.05, 0.05, 0.07, 0.07, 0.09, 0.09, 0.07, 376 | 0.07, 0.05, 0.05, -0.05, -0.05, -0.07, 377 | -0.08, -0.09, -0.09, -0.08, -0.07, -0.05, 378 | -0.05] 379 | sy = [0.06, 0.08, 0.07, 0.06, 0.06, -0.06, -0.06, 380 | -0.07, -0.08, -0.06, -0.06, -0.08, -0.07, 381 | -0.06, -0.05, 0.05, 0.06, 0.07, 0.08, 0.06] 382 | self.body_points = [] 383 | for i in range(len(sx)): 384 | self.body_points.append(Point(sx[i] * self.simulation.scale, sy[i] * self.simulation.scale)) 385 | 386 | ### Continuous Movements: 387 | 388 | def sleep(self, seconds): 389 | self.simulation.sleep(seconds) 390 | if self.simulation.need_to_stop.is_set(): 391 | raise KeyboardInterrupt() 392 | 393 | def forward(self, seconds, vx=5): 394 | """ 395 | Move continuously in simulator for seconds and velocity vx. 396 | """ 397 | self.vx = vx 398 | self.sleep(seconds) 399 | self.vx = 0 400 | 401 | def backward(self, seconds, vx=5): 402 | self.vx = -vx 403 | self.sleep(seconds) 404 | self.vx = 0 405 | 406 | def turnLeft(self, seconds, va=math.pi/180): 407 | self.va = va * 4 408 | self.sleep(seconds) 409 | self.va = 0 410 | 411 | def turnRight(self, seconds, va=math.pi/180): 412 | self.va = -va * 4 413 | self.sleep(seconds) 414 | self.va = 0 415 | 416 | def getIR(self, pos=None): 417 | ## 0 is on right, front 418 | ## 1 is on left, front 419 | if pos is None: 420 | return [self.getIR(0), self.getIR(1)] 421 | else: 422 | hit = self.ir_sensors[pos] 423 | if (hit is not None): 424 | return hit.distance / (self.max_ir * self.simulation.scale) 425 | else: 426 | return 1.0 427 | 428 | def takePicture(self): 429 | self.picture_ready.clear() 430 | self.take_picture.set() 431 | pic = None 432 | if self.picture_ready.wait(1.5): 433 | from PIL import Image 434 | pic = Image.new("RGB", (256, 128)) 435 | size = max(self.simulation.w, self.simulation.h) 436 | for i in range(len(self.camera)): 437 | hit = self.camera[i] 438 | if (hit != None): 439 | s = max(min(1.0 - hit.distance/size, 1.0), 0.0) 440 | if isinstance(hit.color, Color): 441 | r = hit.color.red 442 | g = hit.color.green 443 | b = hit.color.blue 444 | else: 445 | try: 446 | import webcolors 447 | r, g, b = webcolors.name_to_rgb(hit.color) 448 | except: 449 | r, g, b = (128, 128, 128) 450 | hcolor = (int(r * s), int(g * s), int(b * s)) 451 | high = (1.0 - s) * 128 452 | ##pg.line(i, 0 + high/2, i, 128 - high/2) 453 | else: 454 | high = 0 455 | hcolor = None 456 | for j in range(128): 457 | if (j < high/2): ##256 - high/2.0): ## sky 458 | pic.putpixel((i, j), (0, 0, 128)) 459 | elif (j < 128 - high/2): ##256 - high and hcolor != None): ## hit 460 | if (hcolor != None): 461 | pic.putpixel((i, j), hcolor) 462 | else: ## ground 463 | pic.putpixel((i, j), (0, 128, 0)) 464 | if self.simulation.need_to_stop.is_set(): 465 | raise KeyboardInterrupt() 466 | self.take_picture.clear() 467 | return pic 468 | 469 | def stop(self): 470 | self.vx = 0.0 471 | self.vy = 0.0 472 | self.va = 0.0 473 | 474 | def ccw(self, ax, ay, bx, by, cx, cy): 475 | ## counter clockwise 476 | return (((cy - ay) * (bx - ax)) > ((by - ay) * (cx - ax))) 477 | 478 | def intersect(self, ax, ay, bx, by, cx, cy, dx, dy): 479 | ## Return True if line segments AB and CD intersect 480 | return (self.ccw(ax, ay, cx, cy, dx, dy) != self.ccw(bx, by, cx, cy, dx, dy) and 481 | self.ccw(ax, ay, bx, by, cx, cy) != self.ccw(ax, ay, bx, by, dx, dy)) 482 | 483 | def coefs(self, p1x, p1y, p2x, p2y): 484 | A = (p1y - p2y) 485 | B = (p2x - p1x) 486 | C = (p1x * p2y - p2x * p1y) 487 | return [A, B, -C] 488 | 489 | def intersect_coefs(self, L1_0, L1_1, L1_2, L2_0, L2_1, L2_2): 490 | D = L1_0 * L2_1 - L1_1 * L2_0 491 | Dx = L1_2 * L2_1 - L1_1 * L2_2 492 | Dy = L1_0 * L2_2 - L1_2 * L2_0 493 | if (D != 0): 494 | x1 = Dx / D 495 | y1 = Dy / D 496 | return [x1, y1] 497 | else: 498 | return None 499 | 500 | def intersect_hit(self, p1x, p1y, p2x, p2y, p3x, p3y, p4x, p4y): 501 | ## http:##stackoverflow.com/questions/20677795/find-the-point-of-intersecting-lines 502 | L1 = self.coefs(p1x, p1y, p2x, p2y) 503 | L2 = self.coefs(p3x, p3y, p4x, p4y) 504 | xy = self.intersect_coefs(L1[0], L1[1], L1[2], L2[0], L2[1], L2[2]) 505 | ## now check to see on both segments: 506 | if (xy != None): 507 | lowx = min(p1x, p2x) - .1 508 | highx = max(p1x, p2x) + .1 509 | lowy = min(p1y, p2y) - .1 510 | highy = max(p1y, p2y) + .1 511 | if (lowx <= xy[0] and xy[0] <= highx and 512 | lowy <= xy[1] and xy[1] <= highy): 513 | lowx = min(p3x, p4x) - .1 514 | highx = max(p3x, p4x) + .1 515 | lowy = min(p3y, p4y) - .1 516 | highy = max(p3y, p4y) + .1 517 | if (lowx <= xy[0] and xy[0] <= highx and 518 | lowy <= xy[1] and xy[1] <= highy): 519 | return xy 520 | return None 521 | 522 | def castRay(self, x1, y1, a, maxRange): 523 | hits = [] 524 | x2 = math.sin(a) * maxRange + x1 525 | y2 = math.cos(a) * maxRange + y1 526 | for wall in self.simulation.walls: 527 | ## if intersection, can't move 528 | v1, v2, v3, v4 = wall.points 529 | pos = self.intersect_hit(x1, y1, x2, y2, v1.x, v1.y, v2.x, v2.y) 530 | if (pos != None): 531 | dist = distance(pos[0], pos[1], x1, y1) 532 | hits.append(Hit(pos[0], pos[1], dist, wall.color, x1, y1)) 533 | 534 | pos = self.intersect_hit(x1, y1, x2, y2, v2.x, v2.y, v3.x, v3.y) 535 | if (pos != None): 536 | dist = distance(pos[0], pos[1], x1, y1) 537 | hits.append(Hit(pos[0], pos[1], dist, wall.color, x1, y1)) 538 | 539 | pos = self.intersect_hit(x1, y1, x2, y2, v3.x, v3.y, v4.x, v4.y) 540 | if (pos != None): 541 | dist = distance(pos[0], pos[1], x1, y1) 542 | hits.append(Hit(pos[0], pos[1], dist, wall.color, x1, y1)) 543 | 544 | pos = self.intersect_hit(x1, y1, x2, y2, v4.x, v4.y, v1.x, v1.y) 545 | if (pos != None): 546 | dist = distance(pos[0], pos[1], x1, y1) 547 | hits.append(Hit(pos[0], pos[1], dist, wall.color, x1, y1)) 548 | 549 | for robot in self.simulation.robots: 550 | if robot is self: 551 | continue 552 | v1, v2, v3, v4 = robot.bounding_box 553 | pos = self.intersect_hit(x1, y1, x2, y2, v1.x, v1.y, v2.x, v2.y) 554 | if (pos != None): 555 | dist = distance(pos[0], pos[1], x1, y1) 556 | hits.append(Hit(pos[0], pos[1], dist, robot.color, x1, y1)) 557 | 558 | pos = self.intersect_hit(x1, y1, x2, y2, v2.x, v2.y, v3.x, v3.y) 559 | if (pos != None): 560 | dist = distance(pos[0], pos[1], x1, y1) 561 | hits.append(Hit(pos[0], pos[1], dist, robot.color, x1, y1)) 562 | 563 | pos = self.intersect_hit(x1, y1, x2, y2, v3.x, v3.y, v4.x, v4.y) 564 | if (pos != None): 565 | dist = distance(pos[0], pos[1], x1, y1) 566 | hits.append(Hit(pos[0], pos[1], dist, robot.color, x1, y1)) 567 | 568 | pos = self.intersect_hit(x1, y1, x2, y2, v4.x, v4.y, v1.x, v1.y) 569 | if (pos != None): 570 | dist = distance(pos[0], pos[1], x1, y1) 571 | hits.append(Hit(pos[0], pos[1], dist, robot.color, x1, y1)) 572 | 573 | if len(hits) == 0: 574 | return None 575 | else: 576 | return self.min_hits(hits) 577 | 578 | def min_hits(self, hits): 579 | minimum = hits[0] 580 | for hit in hits: 581 | if (hit.distance < minimum.distance): 582 | minimum = hit 583 | return minimum 584 | 585 | def check_for_stall(self, px, py, pdirection): 586 | scale = self.simulation.scale 587 | p1 = rotateAround(px, py, 30/250.0 * scale, pdirection + math.pi/4 + 0 * math.pi/2) 588 | p2 = rotateAround(px, py, 30/250.0 * scale, pdirection + math.pi/4 + 1 * math.pi/2) 589 | p3 = rotateAround(px, py, 30/250.0 * scale, pdirection + math.pi/4 + 2 * math.pi/2) 590 | p4 = rotateAround(px, py, 30/250.0 * scale, pdirection + math.pi/4 + 3 * math.pi/2) 591 | for wall in self.simulation.walls: 592 | ## if intersection, can't move 593 | v1 = wall.points[0] 594 | v2 = wall.points[1] 595 | v3 = wall.points[2] 596 | v4 = wall.points[3] 597 | ## p1 to p2 598 | if (self.intersect(p1[0], p1[1], p2[0], p2[1], 599 | v1.x, v1.y, v2.x, v2.y) or 600 | self.intersect(p1[0], p1[1], p2[0], p2[1], 601 | v2.x, v2.y, v3.x, v3.y) or 602 | self.intersect(p1[0], p1[1], p2[0], p2[1], 603 | v3.x, v3.y, v4.x, v4.y) or 604 | self.intersect(p1[0], p1[1], p2[0], p2[1], 605 | v4.x, v4.y, v1.x, v1.y) or 606 | ## p2 to p3 607 | self.intersect(p2[0], p2[1], p3[0], p3[1], 608 | v1.x, v1.y, v2.x, v2.y) or 609 | self.intersect(p2[0], p2[1], p3[0], p3[1], 610 | v2.x, v2.y, v3.x, v3.y) or 611 | self.intersect(p2[0], p2[1], p3[0], p3[1], 612 | v3.x, v3.y, v4.x, v4.y) or 613 | self.intersect(p2[0], p2[1], p3[0], p3[1], 614 | v4.x, v4.y, v1.x, v1.y) or 615 | ## p3 to p4 616 | self.intersect(p3[0], p3[1], p4[0], p4[1], 617 | v1.x, v1.y, v2.x, v2.y) or 618 | self.intersect(p3[0], p3[1], p4[0], p4[1], 619 | v2.x, v2.y, v3.x, v3.y) or 620 | self.intersect(p3[0], p3[1], p4[0], p4[1], 621 | v3.x, v3.y, v4.x, v4.y) or 622 | self.intersect(p3[0], p3[1], p4[0], p4[1], 623 | v4.x, v4.y, v1.x, v1.y) or 624 | ## p4 to p1 625 | self.intersect(p4[0], p4[1], p1[0], p1[1], 626 | v1.x, v1.y, v2.x, v2.y) or 627 | self.intersect(p4[0], p4[1], p1[0], p1[1], 628 | v2.x, v2.y, v3.x, v3.y) or 629 | self.intersect(p4[0], p4[1], p1[0], p1[1], 630 | v3.x, v3.y, v4.x, v4.y) or 631 | self.intersect(p4[0], p4[1], p1[0], p1[1], 632 | v4.x, v4.y, v1.x, v1.y)): 633 | self.stalled = True 634 | break 635 | 636 | if not self.stalled: 637 | # keep checking for obstacles: 638 | for robot in self.simulation.robots: 639 | if robot is self: 640 | continue 641 | v1, v2, v3, v4 = robot.bounding_box 642 | ## p1 to p2 643 | if (self.intersect(p1[0], p1[1], p2[0], p2[1], 644 | v1.x, v1.y, v2.x, v2.y) or 645 | self.intersect(p1[0], p1[1], p2[0], p2[1], 646 | v2.x, v2.y, v3.x, v3.y) or 647 | self.intersect(p1[0], p1[1], p2[0], p2[1], 648 | v3.x, v3.y, v4.x, v4.y) or 649 | self.intersect(p1[0], p1[1], p2[0], p2[1], 650 | v4.x, v4.y, v1.x, v1.y) or 651 | ## p2 to p3 652 | self.intersect(p2[0], p2[1], p3[0], p3[1], 653 | v1.x, v1.y, v2.x, v2.y) or 654 | self.intersect(p2[0], p2[1], p3[0], p3[1], 655 | v2.x, v2.y, v3.x, v3.y) or 656 | self.intersect(p2[0], p2[1], p3[0], p3[1], 657 | v3.x, v3.y, v4.x, v4.y) or 658 | self.intersect(p2[0], p2[1], p3[0], p3[1], 659 | v4.x, v4.y, v1.x, v1.y) or 660 | ## p3 to p4 661 | self.intersect(p3[0], p3[1], p4[0], p4[1], 662 | v1.x, v1.y, v2.x, v2.y) or 663 | self.intersect(p3[0], p3[1], p4[0], p4[1], 664 | v2.x, v2.y, v3.x, v3.y) or 665 | self.intersect(p3[0], p3[1], p4[0], p4[1], 666 | v3.x, v3.y, v4.x, v4.y) or 667 | self.intersect(p3[0], p3[1], p4[0], p4[1], 668 | v4.x, v4.y, v1.x, v1.y) or 669 | ## p4 to p1 670 | self.intersect(p4[0], p4[1], p1[0], p1[1], 671 | v1.x, v1.y, v2.x, v2.y) or 672 | self.intersect(p4[0], p4[1], p1[0], p1[1], 673 | v2.x, v2.y, v3.x, v3.y) or 674 | self.intersect(p4[0], p4[1], p1[0], p1[1], 675 | v3.x, v3.y, v4.x, v4.y) or 676 | self.intersect(p4[0], p4[1], p1[0], p1[1], 677 | v4.x, v4.y, v1.x, v1.y)): 678 | self.stalled = True 679 | break 680 | return (p1, p2, p3, p4) 681 | 682 | def bump_variability(self): 683 | return (random.random() * .2) - .1 684 | 685 | def update(self): 686 | scale = self.simulation.scale 687 | tvx = self.vx * math.sin(-self.direction + math.pi/2) + self.vy * math.cos(-self.direction + math.pi/2) 688 | tvy = self.vx * math.cos(-self.direction + math.pi/2) - self.vy * math.sin(-self.direction + math.pi/2) 689 | ## proposed positions: 690 | self.stalled = False 691 | if (self.vx != 0 or self.vy != 0 or self.va != 0): 692 | px = self.x + tvx/250.0 * scale 693 | py = self.y + tvy/250.0 * scale 694 | pdirection = self.direction - self.va 695 | pbox = self.check_for_stall(px, py, pdirection) 696 | if (not self.stalled): 697 | ## if no intersection, make move 698 | self.x = px 699 | self.y = py 700 | self.direction = pdirection 701 | self.bounding_box = pbox 702 | else: 703 | self.direction += self.bump_variability() 704 | 705 | ## update sensors, camera: 706 | ## on right: 707 | p = rotateAround(self.x, self.y, 25/250.0 * scale, self.direction + math.pi/8) 708 | hit = self.castRay(p[0], p[1], -self.direction + math.pi/2.0, self.max_ir * scale) 709 | self.ir_sensors[0] = hit 710 | 711 | p = rotateAround(self.x, self.y, 25/250.0 * scale, self.direction - math.pi/8) 712 | hit = self.castRay(p[0], p[1], -self.direction + math.pi/2, self.max_ir * scale) 713 | self.ir_sensors[1] = hit 714 | 715 | ## camera: 716 | if self.take_picture.is_set(): 717 | for i in range(256): 718 | angle = i/256.0 * 60 - 30 719 | self.camera[i] = self.castRay(self.x, self.y, -self.direction + math.pi/2.0 - angle*math.pi/180.0, 1000) 720 | self.picture_ready.set() 721 | 722 | def draw(self, canvas): 723 | scale = self.simulation.scale 724 | if self.debug: 725 | ## bounding box: 726 | p1, p2, p3, p4 = self.bounding_box 727 | for line in [Line((p1[0], p1[1]), (p2[0], p2[1])), 728 | Line((p2[0], p2[1]), (p3[0], p3[1])), 729 | Line((p3[0], p3[1]), (p4[0], p4[1])), 730 | Line((p4[0], p4[1]), (p1[0], p1[1]))]: 731 | line.stroke(Color(255, 255, 255)) 732 | line.draw(canvas) 733 | 734 | for hit, offset in zip(self.ir_sensors, [math.pi/8, -math.pi/8]): 735 | if hit: 736 | # FIXME: offset should be part ofsensor: 737 | p = rotateAround(self.x, self.y, 25/250.0 * scale, self.direction + offset) 738 | # Draw hit: 739 | ellipse = Ellipse((p[0], p[1]), (5, 5)) 740 | ellipse.fill(Color(0, 255, 0)) 741 | ellipse.draw(canvas) 742 | ellipse = Ellipse((hit.x, hit.y), (5, 5)) 743 | ellipse.fill(Color(0, 255, 0)) 744 | ellipse.draw(canvas) 745 | 746 | self.draw_body(canvas) 747 | self.draw_sensors(canvas) 748 | 749 | def draw_body(self, canvas): 750 | scale = self.simulation.scale 751 | canvas.pushMatrix() 752 | canvas.translate(self.x, self.y) 753 | canvas.rotate(self.direction) 754 | ## body: 755 | if (self.stalled): 756 | canvas.fill(Color(128, 128, 128)) 757 | canvas.stroke(Color(255, 255, 255)) 758 | else: 759 | canvas.fill(self.color) 760 | canvas.noStroke() 761 | canvas.noStroke() 762 | polygon = Polygon(self.body_points) 763 | polygon.draw(canvas) 764 | ## Draw wheels: 765 | canvas.fill(Color(0)) 766 | rect = Rectangle((-10/250.0 * scale, -23/250.0 * scale), (19/250.0 * scale, 5/250.0 * scale)) 767 | rect.draw(canvas) 768 | rect = Rectangle((-10/250.0 * scale, 18/250.0 * scale), (19/250.0 * scale, 5/250.0 * scale)) 769 | rect.draw(canvas) 770 | ## hole: 771 | canvas.fill(Color(0, 64, 0)) 772 | ellipse = Ellipse((0, 0), (7/250.0 * scale, 7/250.0 * scale)) 773 | ellipse.draw(canvas) 774 | ## fluke 775 | canvas.fill(Color(0, 64, 0)) 776 | rect = Rectangle((15/250.0 * scale, -10/250.0 * scale), (4/250.0 * scale, 19/250.0 * scale)) 777 | rect.draw(canvas) 778 | canvas.popMatrix() 779 | 780 | def draw_sensors(self, canvas): 781 | scale = self.simulation.scale 782 | ## draw sensors 783 | ## right front IR 784 | ## position of start of sensor: 785 | p1 = rotateAround(self.x, self.y, 25/250.0 * scale, self.direction + math.pi/8) 786 | ## angle of sensor: 787 | p2 = rotateAround(p1[0], p1[1], self.getIR(0) * self.max_ir * scale, self.direction) 788 | dist = distance(p1[0], p1[1], p2[0], p2[1]) 789 | if (self.getIR(0) < 1.0): 790 | canvas.stroke(Color(255)) 791 | canvas.fill(Color(128, 0, 128, 64)) 792 | arc = Arc((p1[0], p1[1]), dist, self.direction - .5, self.direction + .5) 793 | arc.draw(canvas) 794 | ## left front IR 795 | p1 = rotateAround(self.x, self.y, 25/250.0 * scale, self.direction - math.pi/8) 796 | ## angle of sensor: 797 | p2 = rotateAround(p1[0], p1[1], self.getIR(1) * self.max_ir * scale, self.direction) 798 | dist = distance(p1[0], p1[1], p2[0], p2[1]) 799 | if (self.getIR(1) < 1.0): 800 | canvas.stroke(Color(255)) 801 | canvas.noStroke() 802 | canvas.fill(Color(128, 0, 128, 64)) 803 | arc = Arc((p1[0], p1[1]), dist, self.direction - .5, self.direction + .5) 804 | arc.draw(canvas) 805 | 806 | class DiscreteLadybug(Robot): 807 | def __init__(self, *args, **kwargs): 808 | super(DiscreteLadybug, self).__init__(*args, **kwargs) 809 | self.energy = kwargs.get("energy", 100) 810 | self.oenergy = self.energy 811 | self.history = [self.energy] 812 | self.block_types = ["b", "w"] ## bugs and walls, block movement 813 | self.edible = {"f": 20} 814 | self.state = "0" 815 | self.rules = None 816 | 817 | def draw_body(self, canvas): 818 | px, py = self.x, self.y 819 | center = (px * self.simulation.pwidth + self.simulation.pwidth/2, 820 | py * self.simulation.pheight + self.simulation.pheight/2) 821 | ladybug = Circle(center, self.simulation.pwidth) 822 | ladybug.fill("red") 823 | ladybug.draw(canvas) 824 | head = Arc(center, self.simulation.pwidth, self.direction - math.pi/2, self.direction + math.pi/2) 825 | head.fill("black") 826 | head.draw(canvas) 827 | 828 | def forward(self, distance): 829 | self.move(distance, 0) 830 | 831 | def backward(self, distance): 832 | self.move(-distance, 0) 833 | 834 | def set_energy(self, energy): 835 | self.energy = energy 836 | self.history.append(self.energy) 837 | 838 | def turnLeft(self, angle): 839 | self.direction -= angle * math.pi/180 840 | self.set_energy(self.energy - angle/360 * 4.0) 841 | self.direction = self.direction % (math.pi * 2.0) 842 | 843 | def stop(self): 844 | #self.energy -= 0.75 845 | pass 846 | 847 | def turnRight(self, angle): 848 | self.direction += angle * math.pi/180 849 | # 90 degree == 1 unit 850 | self.set_energy(self.energy - angle/360 * 4.0) 851 | self.direction = self.direction % (math.pi * 2.0) 852 | 853 | def sign(self, value): 854 | if value == 0: 855 | return 0 856 | elif value < 0: 857 | return -1 858 | elif value > 0: 859 | return 1 860 | 861 | def move(self, tx, ty): 862 | for step in range(int(max(abs(tx), abs(ty)))): 863 | dx = self.sign(tx) 864 | dy = self.sign(ty) 865 | x = dx * math.sin(-self.direction + math.pi/2) + dy * math.cos(-self.direction + math.pi/2) 866 | y = dx * math.cos(-self.direction + math.pi/2) - dy * math.sin(-self.direction + math.pi/2) 867 | # check to see if move is possible: 868 | px, py = self.simulation.getPatchLocation(int(self.x + x), int(self.y + y)) 869 | # update energy (even if agent was unable to move): 870 | # distance of 1 is 1 unit of energy: 871 | self.set_energy(self.energy - pdistance(self.x, self.y, px, py, self.simulation.psize)) 872 | spot = self.simulation.patches[px][py] 873 | # if can move, make move: 874 | if spot is None or spot not in self.block_types: 875 | # if food, eat it: 876 | if spot in self.edible: 877 | self.set_energy(self.energy + self.edible[spot]) 878 | # move into: 879 | self.simulation.patches[px][py] = 'b' 880 | # Move out of: 881 | self.simulation.setPatch(int(self.x), int(self.y), None) 882 | # Update location: 883 | self.x, self.y = px, py 884 | 885 | def parseRule(self, srule): 886 | parts = [] 887 | args = [] 888 | current = "" 889 | state = "begin" 890 | for s in srule: 891 | if state == "begin": 892 | if s == "#" and current == "" and len(parts) == 0: 893 | return None 894 | if s in [" ", "\n", "\t"]: # whitespace 895 | if current: 896 | parts.append(current) 897 | current = "" 898 | elif s == "(": # args 899 | state = "args" 900 | if current: 901 | parts.append(current) 902 | current = "" 903 | else: 904 | current += s 905 | elif state == "args": 906 | if s in [" ", "\n", "\t"]: # whitespace 907 | if current: 908 | args.append(current) 909 | current = "" 910 | elif s == ")": # args 911 | state = "begin" 912 | if current: 913 | args.append(current) 914 | current = "" 915 | elif s == ",": # args 916 | if current: 917 | args.append(current) 918 | current = "" 919 | else: 920 | current += s 921 | if current: 922 | parts.append(current) 923 | parts.insert(-1, args) 924 | # state, match, "->", action, args, state 925 | if len(parts) == 1 and len(args) == 0: 926 | return None 927 | elif len(parts) != 6: 928 | raise Exception("Invalid length of rule in '%s'" % srule) 929 | elif parts[2] != "->": 930 | raise Exception("Item #3 should be => in '%s'" % srule) 931 | return parts 932 | 933 | def applyAction(self, command, args): 934 | if command == "turnLeft": 935 | if args[0] not in ["90", "180"]: 936 | raise Exception("Invalid angle: must be 90 or 180") 937 | self.turnLeft(float(args[0])) 938 | elif command == "turnRight": 939 | if args[0] not in ["90", "180"]: 940 | raise Exception("Invalid angle: must be 90 or 180") 941 | self.turnRight(float(args[0])) 942 | elif command == "forward": 943 | if not (1 <= float(args[0]) <= 9): 944 | raise Exception("Invalid distance: must be >= 1 or <= 9") 945 | self.forward(float(args[0])) 946 | elif command == "backward": 947 | if not (1 <= float(args[0]) <= 9): 948 | raise Exception("Invalid distance: must be >= 1 or <= 9") 949 | self.backward(float(args[0])) 950 | elif command == "stop": 951 | self.stop() 952 | 953 | def update(self): 954 | if self.rules is None: 955 | self.set_energy(self.energy - 0.75) # cost of being alive 956 | return 957 | firedRule = False 958 | rules = self.rules.strip() 959 | rules = rules.split("\n") 960 | if len(rules) == 0: 961 | raise Exception("Need at least one rule") 962 | ox, oy = self.x, self.y 963 | for rule in rules: 964 | # state, match, "->", action, args, state 965 | # match fff, *f*, f**, **f, no rule match, no movement 966 | parts = self.parseRule(rule) 967 | if parts: 968 | state, match, arrow, action, args, next_state = parts 969 | sense = self.getSenses() 970 | if self.state == state and self.match(match, sense): 971 | self.applyAction(action, args) 972 | self.state = next_state 973 | firedRule = True 974 | break 975 | if (ox, oy) == (self.x, self.y): 976 | self.set_energy(self.energy - 0.75) # cost of being alive 977 | if not firedRule: 978 | raise Exception("No rule matched") 979 | 980 | def getSenses(self): 981 | senses = [] 982 | #self.positions = [] 983 | # dx: forward/backward; dy: left/right 984 | 985 | 986 | for dx,dy in [[0, 1], [1, 1], [1, 0], [1, -1], [0, -1]]: 987 | #for dx,dy in [[0, -1], [1, -1], [1, 0], [1, 1], [0, 1]]: 988 | x = dx * math.sin(-self.direction + math.pi/2) + dy * math.cos(-self.direction + math.pi/2) 989 | y = dx * math.cos(-self.direction + math.pi/2) - dy * math.sin(-self.direction + math.pi/2) 990 | item = self.simulation.getPatch(round(self.x + x), round(self.y + y)) 991 | senses.append(item) 992 | #self.positions.append((round(self.x + x), round(self.y + y))) 993 | return senses 994 | 995 | def match(self, rule, world): 996 | if len(rule) != len(world): 997 | raise Exception("Matching part requires 5 characters: '%s'" % rule) 998 | for w,r in zip(world, rule): 999 | if w != r and r != "*": 1000 | return False 1001 | return True 1002 | 1003 | class LadyBug(Robot): 1004 | def draw_body(self, canvas): 1005 | scale = self.simulation.scale 1006 | canvas.pushMatrix() 1007 | canvas.translate(self.x, self.y) 1008 | canvas.rotate(self.direction) 1009 | # Draw with front to right 1010 | width = 0.15 * scale 1011 | length = 0.2 * scale 1012 | for x in [-length/3, 0, length/3]: 1013 | for side in [-1, 1]: 1014 | end = x + (random.random() * length/5) - length/10 1015 | leg = Line((x, width/3 * side), (end, (width/3 + width/4) * side)) 1016 | leg.stroke_width(3) 1017 | leg.draw(canvas) 1018 | leg = Line((end, (width/3 + width/4) * side), (end - length/5, (width/3 + width/4 + length/5) * side)) 1019 | leg.stroke_width(3) 1020 | leg.draw(canvas) 1021 | body = Ellipse((0,0), (length/2, width/2)) 1022 | if not self.stalled: 1023 | body.fill(self.color) 1024 | else: 1025 | body.fill(Color(128, 128, 128)) 1026 | body.draw(canvas) 1027 | head = Arc((width/2, 0), width/3, -math.pi/2, math.pi/2) 1028 | head.fill(Color(0, 0, 0)) 1029 | head.draw(canvas) 1030 | line = Line((width/1.5, 0), (-width/1.5, 0)) 1031 | line.draw(canvas) 1032 | for x,y in [(length/5, width/5), (0, width/10), 1033 | (-length/5, -width/3), (length/5, -width/5), 1034 | (-length/4, width/4)]: 1035 | spot = Ellipse((x, y), (length/20, length/20)) 1036 | spot.fill(Color(0, 0, 0)) 1037 | spot.draw(canvas) 1038 | eye = Ellipse((length/2, width/5), (.01 * scale, .01 * scale)) 1039 | eye.fill(Color(255, 255, 255)) 1040 | eye.draw(canvas) 1041 | eye = Ellipse((length/2, -width/5), (.01 * scale, .01 * scale)) 1042 | eye.fill(Color(255, 255, 255)) 1043 | eye.draw(canvas) 1044 | canvas.popMatrix() 1045 | 1046 | class Spider(Robot): 1047 | 1048 | def __init__(self, *args, **kwargs): 1049 | super().__init__(*args, **kwargs) 1050 | self.max_ir = 1/2 # ratio of robot 1051 | 1052 | def bump_variability(self): 1053 | return 0.0 1054 | 1055 | def draw_body(self, canvas): 1056 | scale = self.simulation.scale 1057 | canvas.pushMatrix() 1058 | canvas.translate(self.x, self.y) 1059 | canvas.rotate(self.direction) 1060 | # Draw with front to right 1061 | width = 0.2 * scale 1062 | length = 0.2 * scale 1063 | for x in [-length/8 * 2.5, -length/8, length/8, length/8 * 2.5]: 1064 | for side in [-1, 1]: 1065 | end = x + (random.random() * length/5) - length/10 1066 | leg = Line((x + length/7, 0), (end, (width/4 + width/4) * side)) 1067 | leg.stroke_width(5) 1068 | leg.stroke(Color(0, 0, 0)) 1069 | leg.draw(canvas) 1070 | leg = Line((end, (width/4 + width/4) * side), (end - length/5, (width/4 + width/4 + length/5) * side)) 1071 | leg.stroke_width(3) 1072 | leg.stroke(Color(0, 0, 0)) 1073 | leg.draw(canvas) 1074 | body = Ellipse((0,0), (length/3, width/3)) 1075 | head = Circle((width/2, 0), width/5) 1076 | if not self.stalled: 1077 | body.fill(Color(0, 0, 0)) 1078 | head.fill(Color(0, 0, 0)) 1079 | else: 1080 | body.fill(Color(128, 128, 128)) 1081 | head.fill(Color(128, 128, 128)) 1082 | body.draw(canvas) 1083 | head.draw(canvas) 1084 | eye = Ellipse((length/2, width/5), (.01 * scale, .01 * scale)) 1085 | eye.fill(Color(255, 255, 255)) 1086 | eye.draw(canvas) 1087 | eye = Ellipse((length/2, -width/5), (.01 * scale, .01 * scale)) 1088 | eye.fill(Color(255, 255, 255)) 1089 | eye.draw(canvas) 1090 | canvas.popMatrix() 1091 | 1092 | ### ------------------------------------ 1093 | 1094 | def get_sim(): 1095 | return SIMULATION 1096 | 1097 | def get_robot(index=0): 1098 | return SIMULATION.robots[index] 1099 | 1100 | def loadSimulation(sim_filename): 1101 | sim_folder, filename = os.path.split(__file__) 1102 | sim_folder = os.path.join(sim_folder, "simulations") 1103 | if sim_folder not in sys.path: 1104 | sys.path.append(sim_folder) 1105 | mod = __import__(sim_filename) 1106 | return mod.makeSimulation() 1107 | 1108 | class DiscreteView(object): 1109 | def __init__(self, sim_filename): 1110 | from ipywidgets import widgets 1111 | self.sim_filename = sim_filename 1112 | self.canvas = widgets.HTML() 1113 | self.go_button = widgets.Button(description="Go") 1114 | self.stop_button = widgets.Button(description="Stop") 1115 | self.step_button = widgets.Button(description="Step") 1116 | self.reset_button = widgets.Button(description="Restart") 1117 | self.error = widgets.HTML("") 1118 | self.simulation = loadSimulation(self.sim_filename) 1119 | self.simulation.sim_time = 0.2 1120 | self.canvas.value = str(self.simulation.render()) 1121 | self.energy_widget = widgets.Text(str(self.simulation.robots[0].energy)) 1122 | #self.energy_widget.disabled = True 1123 | self.sim_widgets = widgets.VBox([self.canvas, 1124 | widgets.HBox([self.go_button, 1125 | self.stop_button, 1126 | self.step_button, 1127 | self.reset_button, 1128 | self.energy_widget, 1129 | ]), 1130 | self.error]) 1131 | self.go_button.on_click(self.go) 1132 | self.stop_button.on_click(self.stop) 1133 | self.step_button.on_click(self.step) 1134 | self.reset_button.on_click(self.reset) 1135 | 1136 | def go(self, obj): 1137 | self.simulation.start_sim(gui=False, set_values={"canvas": self.canvas, 1138 | "energy": self.energy_widget}, 1139 | error=self.error) 1140 | 1141 | def stop(self, obj=None): 1142 | self.simulation.stop_sim() 1143 | 1144 | def step(self, obj=None): 1145 | self.simulation.clock += self.simulation.sim_time 1146 | for robot in self.simulation.robots: 1147 | robot.update() 1148 | self.canvas.value = str(self.simulation.render()) 1149 | self.energy_widget.value = str(self.simulation.robots[0].energy) 1150 | 1151 | def reset(self, obj=None): 1152 | self.simulation.clock = 0.0 1153 | self.simulation.stop_sim() 1154 | self.simulation.initialize() 1155 | self.canvas.value = str(self.simulation.render()) 1156 | self.energy_widget.value = str(self.simulation.robots[0].energy) 1157 | 1158 | def setRobot(self, pos, robot): 1159 | self.simulation.robots[pos] = robot 1160 | 1161 | def addCluster(self, x, y, item, count): 1162 | self.simulation.addCluster(x, y, item, count) 1163 | 1164 | def render(self): 1165 | return self.simulation.render() 1166 | 1167 | clock = property(lambda self: self.simulation.clock) 1168 | 1169 | def View(sim_filename): 1170 | try: 1171 | from ipywidgets import widgets 1172 | except: 1173 | from IPython.html import widgets 1174 | 1175 | def stop_sim(obj): 1176 | simulation.stop_sim() 1177 | pause_button.visible = False 1178 | 1179 | def restart(x, y, direction): 1180 | simulation.stop_sim() 1181 | time.sleep(.250) 1182 | simulation.reset() 1183 | canvas.value = str(simulation.render()) 1184 | simulation.start_sim(gui=False, set_values={"canvas": canvas}, error=error) 1185 | pause_button.visible = True 1186 | 1187 | def stop_and_start(obj): 1188 | simulation.stop_sim() 1189 | time.sleep(.250) 1190 | simulation.start_sim(gui=False, set_values={"canvas": canvas}, error=error) 1191 | pause_button.visible = True 1192 | 1193 | def toggle_pause(obj): 1194 | if simulation.paused.is_set(): 1195 | simulation.paused.clear() 1196 | pause_button.description = "Pause Simulation" 1197 | else: 1198 | simulation.paused.set() 1199 | pause_button.description = "Resume Simulation" 1200 | 1201 | canvas = widgets.HTML() 1202 | #stop_button = widgets.Button(description="Stop Brain") 1203 | stop_sim_button = widgets.Button(description="Stop Simulation") 1204 | restart_button = widgets.Button(description="Restart Simulation") 1205 | pause_button = widgets.Button(description="Pause Simulation") 1206 | gui_button = widgets.IntSlider(description="GUI Update Interval", min=1, max=10, value=5) 1207 | error = widgets.HTML("") 1208 | 1209 | simulation = loadSimulation(sim_filename) 1210 | simulation.start_sim(gui=False, set_values={"canvas": canvas}, error=error) 1211 | #simulation.stop_sim() 1212 | 1213 | canvas.value = str(simulation.render()) 1214 | 1215 | sim_widgets = widgets.VBox([canvas, 1216 | gui_button, 1217 | widgets.HBox([stop_sim_button, 1218 | #stop_button, 1219 | restart_button, 1220 | pause_button, 1221 | ]), 1222 | error]) 1223 | 1224 | 1225 | #stop_button.on_click(stop_and_start) 1226 | stop_sim_button.on_click(stop_sim) 1227 | restart_button.on_click(lambda obj: restart(550, 350, -math.pi/2)) 1228 | pause_button.on_click(toggle_pause) 1229 | gui_button.on_trait_change(lambda *args: simulation.set_gui_update(gui_button.value), "value") 1230 | 1231 | return sim_widgets 1232 | 1233 | class DNARobot(object): 1234 | def __init__(self, robot, dna=None): 1235 | from calysto.ai import conx 1236 | self.clen = 3 1237 | self.net = conx.SRN(verbosity=-1) 1238 | self.net.addSRNLayers(5, 3, 1) 1239 | self.dna_length = len(self.net.arrayify() * self.clen) 1240 | self.robot = robot 1241 | if dna is None: 1242 | self.dna = self.make_dna(self.dna_length) 1243 | else: 1244 | self.dna = dna 1245 | self.net.unArrayify(self.make_array_from_dna(self.dna)) 1246 | self.net["context"].setActivations([.25, .25, .25]) 1247 | 1248 | def codon2weight(self, codon): 1249 | """ 1250 | Turn a codon of "000" to "999" to a number between 1251 | -5.0 and 5.0. 1252 | """ 1253 | length = len(codon) 1254 | retval = int(codon) 1255 | return retval/(10 ** (length - 1)) - 5.0 1256 | 1257 | def weight2codon(self, weight, length=None): 1258 | """ 1259 | Given a weight between -5 and 5, turn it into 1260 | a codon, eg "000" to "999" 1261 | """ 1262 | if length is None: 1263 | length = self.clen 1264 | retval = 0 1265 | weight = min(max(weight + 5.0, 0), 10.0) * (10 ** (length - 1)) 1266 | for i in range(length): 1267 | if i == length - 1: # last one 1268 | d = int(round(weight / (10 ** (length - i - 1)))) 1269 | else: 1270 | d = int(weight / (10 ** (length - i - 1))) 1271 | weight = weight % (10 ** (length - i - 1)) 1272 | retval += d * (10 ** (length - i - 1)) 1273 | return ("%0" + str(length) + "d") % retval 1274 | 1275 | def make_dna(self, length=None): 1276 | import random 1277 | if length is None: 1278 | length = self.dna_length 1279 | return "".join([random.choice("0123456789") for i in range(length)]) 1280 | 1281 | def make_array_from_dna(self, dna): 1282 | array = [] 1283 | for i in range(0, len(dna), self.clen): 1284 | codon = dna[i:i+self.clen] 1285 | array.append(self.codon2weight(codon)) 1286 | return array 1287 | 1288 | def draw(self, canvas): 1289 | self.robot.draw(canvas) 1290 | 1291 | def stop(self): 1292 | self.robot.stop() 1293 | 1294 | def update(self): 1295 | def sense2num(s): 1296 | if s == 'w': 1297 | return 0.75 1298 | elif s == None: 1299 | return 0.5 1300 | else: 1301 | return 0.25 1302 | senses = self.robot.getSenses() 1303 | fsenses = list(map(sense2num, senses)) 1304 | v = self.net.propagate(input=fsenses) 1305 | self.net.postBackprop() 1306 | if v[0] < .33: 1307 | self.robot.turnLeft(90) 1308 | elif v[0] < .66: 1309 | self.robot.forward(1) 1310 | else: 1311 | self.robot.turnRight(90) 1312 | 1313 | x = property(lambda self: self.robot.x, 1314 | lambda self, value: setattr(self.robot, "x", value)) 1315 | y = property(lambda self: self.robot.y, 1316 | lambda self, value: setattr(self.robot, "y", value)) 1317 | direction = property(lambda self: self.robot.direction, 1318 | lambda self, value: setattr(self.robot, "direction", value)) 1319 | energy = property(lambda self: self.robot.energy, 1320 | lambda self, value: setattr(self.robot, "energy", value)) 1321 | ox = property(lambda self: self.robot.ox) 1322 | oy = property(lambda self: self.robot.oy) 1323 | odirection = property(lambda self: self.robot.odirection) 1324 | oenergy = property(lambda self: self.robot.oenergy) 1325 | history = property(lambda self: self.robot.history, 1326 | lambda self, value: setattr(self.robot, "history", value)) 1327 | -------------------------------------------------------------------------------- /calysto/simulations/Bug1.py: -------------------------------------------------------------------------------- 1 | from calysto.simulation import LadyBug, Spider, Simulation, Color 2 | import math 3 | 4 | def makeSimulation(): 5 | ladybug = LadyBug(550, 350, -math.pi/2) 6 | spider = Spider(50, 50, 0) 7 | def spiderBrain(): 8 | direction = 1 9 | while simulation.is_running.is_set(): 10 | if spider.stalled: 11 | direction = direction * -1 12 | if direction == 1: 13 | spider.forward(1) 14 | else: 15 | spider.backward(1) 16 | spider.stop() 17 | spider.brain = spiderBrain 18 | simulation = Simulation(600, 400, ladybug, spider) 19 | simulation.makeWall(500, 100, 10, 200, Color(255, 255, 0)) 20 | simulation.makeWall(10, 100, 190, 10, Color(255, 255, 0)) 21 | simulation.makeWall(300, 100, 200, 10, Color(255, 255, 0)) 22 | simulation.makeWall(100, 300, 410, 10, Color(255, 255, 0)) 23 | simulation.makeWall(10, 200, 390, 10, Color(255, 255, 0)) 24 | return simulation 25 | -------------------------------------------------------------------------------- /calysto/simulations/Ladybug1.py: -------------------------------------------------------------------------------- 1 | from calysto.simulation import * 2 | import numpy 3 | import random 4 | import math 5 | 6 | class LadybugSimulation(DiscreteSimulation): 7 | def initialize(self): 8 | random.seed(42) 9 | numpy.random.seed(42) 10 | super(LadybugSimulation, self).initialize() 11 | self.reset() 12 | for robot in self.robots: 13 | robot.state = "0" 14 | self.items["w"] = self.drawWall 15 | for i in range(self.psize[0]): 16 | self.setPatch(i, 0, "w") 17 | self.setPatch(i, self.psize[1] - 1, "w") 18 | for i in range(self.psize[1]): 19 | self.setPatch(0, i, "w") 20 | self.setPatch(self.psize[0] - 1, i, "w") 21 | self.addCluster(random.random() * 15, 22 | random.random() * 10, 23 | 'f', 20) 24 | self.addCluster(random.random() * 15 + 30, 25 | random.random() * 10 + 20, 26 | 'f', 20) 27 | 28 | def makeSimulation(): 29 | sim = LadybugSimulation(600, 400, 30 | draw_walls=False, 31 | background_color="green") 32 | ladybug = DiscreteLadybug(30, 20, -math.pi/2) 33 | sim.addRobot(ladybug) 34 | return sim 35 | -------------------------------------------------------------------------------- /calysto/simulations/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Calysto/calysto/a9a6e2a402d8d1fc2c0a9bb8f3981da8a3a9e55f/calysto/simulations/__init__.py -------------------------------------------------------------------------------- /calysto/util/__init__.py: -------------------------------------------------------------------------------- 1 | 2 | from . import code2notebook, ottobib, password 3 | 4 | __all__ = ["code2notebook", "ottobib", "password"] 5 | -------------------------------------------------------------------------------- /calysto/util/code2notebook.py: -------------------------------------------------------------------------------- 1 | #----------------------------------------------------------------------------- 2 | # Copyright (C) 2014, Doug Blank 3 | # 4 | # Distributed under the terms of the BSD License. The full license is in 5 | # the file COPYING, distributed as part of this software. 6 | #----------------------------------------------------------------------------- 7 | 8 | # based on code in IPython 9 | 10 | import os 11 | import json 12 | import uuid 13 | import hmac 14 | import hashlib 15 | import sys 16 | 17 | PY3 = (sys.version_info[0] >= 3) 18 | 19 | def read_secret_key(): 20 | try: 21 | filename = os.path.expanduser("~/.ipython/profile_calico/security/notebook_secret") 22 | with open(filename, 'rb') as f: 23 | return f.read() 24 | except: 25 | return "NOSECRET-FIXME" 26 | 27 | encoder = json.encoder.JSONEncoder(sort_keys=True, indent=1) 28 | key = read_secret_key() 29 | if PY3: 30 | SECRET = key 31 | unicode_type = str 32 | else: 33 | SECRET = unicode(key).encode("ascii") 34 | unicode_type = unicode 35 | 36 | def convert(py_file): 37 | py_full_path = os.path.abspath(py_file) 38 | base_path, base_name = os.path.split(py_full_path) 39 | base, ext = os.path.splitext(base_name) 40 | code_list = open(py_full_path).readlines() 41 | nb_full_path = os.path.join(base_path, base + ".ipynb") 42 | ## --------------------- 43 | notebook = make_notebook(code_list) 44 | sign(notebook) 45 | save(notebook, nb_full_path) 46 | 47 | def sign(notebook): 48 | notebook["metadata"]["signature"] = sign_notebook(notebook) 49 | 50 | def save(notebook, filename): 51 | fp = open(filename, "w") 52 | fp.write(encoder.encode(notebook)) 53 | fp.close() 54 | 55 | def cast_bytes(s, encoding=None): 56 | if not isinstance(s, bytes): 57 | return encode(s, encoding) 58 | return s 59 | 60 | def yield_everything(obj): 61 | if isinstance(obj, dict): 62 | for key in sorted(obj): 63 | value = obj[key] 64 | yield cast_bytes(key) 65 | for b in yield_everything(value): 66 | yield b 67 | elif isinstance(obj, (list, tuple)): 68 | for element in obj: 69 | for b in yield_everything(element): 70 | yield b 71 | elif isinstance(obj, unicode_type): 72 | yield obj.encode('utf8') 73 | else: 74 | yield unicode_type(obj).encode('utf8') 75 | 76 | def sign_notebook(notebook, digestmod=hashlib.sha256): 77 | h = hmac.HMAC(SECRET, digestmod=digestmod) 78 | for b in yield_everything(notebook): 79 | h.update(b) 80 | return "sha256:" + h.hexdigest() 81 | 82 | def make_notebook(code_list=None): 83 | notebook = {} 84 | notebook["metadata"] = { 85 | "name": "", 86 | "signature": "", ## to be signed later 87 | } 88 | notebook["nbformat"] = 3 89 | notebook["nbformat_minor"] = 0 90 | notebook["worksheets"] = [make_worksheet(code_list)] 91 | return notebook 92 | 93 | def make_worksheet(code_list=None, cell_type="code", language="python"): 94 | worksheet = {} 95 | if code_list: 96 | worksheet["cells"] = [make_cell(code_list, cell_type="code", language="python")] 97 | else: 98 | worksheet["cells"] = [] 99 | worksheet["metadata"] = {} 100 | return worksheet 101 | 102 | def add_cell(notebook, cell): 103 | notebook["worksheets"][0]["cells"].append(cell) 104 | 105 | def make_cell(code_list=None, cell_type="code", language="python"): 106 | cell = { 107 | "cell_type": cell_type, # markdown, code, 108 | "collapsed": False, 109 | "metadata": {}, 110 | } 111 | if cell_type == "code": 112 | cell["input"] = code_list 113 | cell["language"] = language 114 | cell["outputs"] = [] 115 | elif cell_type == "markdown": 116 | cell["source"] = code_list 117 | return cell 118 | 119 | if __name__ == '__main__': 120 | import sys 121 | for arg in sys.argv[1:]: 122 | convert(arg) 123 | -------------------------------------------------------------------------------- /calysto/util/ottobib.py: -------------------------------------------------------------------------------- 1 | import urllib 2 | import re 3 | 4 | def ottobib(isbn): 5 | fp = urllib.request.urlopen("https://www.ottobib.com/isbn/%s/bibtex" % isbn) 6 | html = fp.read() 7 | match = re.findall("", html, re.DOTALL) 8 | if len(match) > 0: 9 | return match[0] 10 | raise Exception("ISBN not found") 11 | -------------------------------------------------------------------------------- /calysto/util/password.py: -------------------------------------------------------------------------------- 1 | from __future__ import print_function 2 | 3 | try: 4 | import pexpect 5 | import getpass 6 | except: 7 | print("No password support") 8 | 9 | try: 10 | raw_input 11 | except NameError: 12 | raw_input = input 13 | 14 | COMMAND_PROMPT = '[$#] ' 15 | TERMINAL_PROMPT = r'Terminal type\?' 16 | TERMINAL_TYPE = 'vt100' 17 | SSH_NEWKEY = r'Are you sure you want to continue connecting \(yes/no\)\?' 18 | 19 | def login(host, user, password): 20 | 21 | child = pexpect.spawn('ssh -l %s %s'%(user, host)) 22 | #fout = file ("LOG.TXT","wb") 23 | #child.setlog (fout) 24 | 25 | i = child.expect([pexpect.TIMEOUT, SSH_NEWKEY, '[Pp]assword: ']) 26 | if i == 0: # Timeout 27 | print('ERROR!') 28 | print('SSH could not login. Here is what SSH said:') 29 | print(child.before, child.after) 30 | return 31 | if i == 1: # SSH does not have the public key. Just accept it. 32 | child.sendline ('yes') 33 | child.expect ('[Pp]assword: ') 34 | child.sendline(password) 35 | # Now we are either at the command prompt or 36 | # the login process is asking for our terminal type. 37 | i = child.expect (['Permission denied', TERMINAL_PROMPT, COMMAND_PROMPT]) 38 | if i == 0: 39 | print('Permission denied on host:', host) 40 | return 41 | if i == 1: 42 | child.sendline (TERMINAL_TYPE) 43 | child.expect (COMMAND_PROMPT) 44 | return child 45 | 46 | # (current) UNIX password: 47 | def change_password_remote(child, user, oldpassword, newpassword): 48 | 49 | child.sendline('passwd') 50 | i = child.expect(['[Oo]ld [Pp]assword', '.current.*password', '[Nn]ew [Pp]assword']) 51 | # Root does not require old password, so it gets to bypass the next step. 52 | if i == 0 or i == 1: 53 | child.sendline(oldpassword) 54 | child.expect('[Nn]ew .*[Pp]assword') 55 | child.sendline(newpassword) 56 | i = child.expect(['[Nn]ew .*[Pp]assword', '[Rr]etype', '[Rr]e-enter']) 57 | if i == 0: 58 | print('Host did not like new password. Here is what it said...') 59 | print(child.before) 60 | child.send (chr(3)) # Ctrl-C 61 | child.sendline('') # This should tell remote passwd command to quit. 62 | return 63 | child.sendline(newpassword) 64 | 65 | def change_password(argv=["localhost"]): 66 | user = raw_input('Username: ') 67 | password = getpass.getpass('Current Password: ') 68 | newpassword = getpass.getpass('New Password: ') 69 | newpasswordconfirm = getpass.getpass('Confirm New Password: ') 70 | if newpassword != newpasswordconfirm: 71 | print('New Passwords do not match.') 72 | return 1 73 | 74 | for host in argv: 75 | child = login(host, user, password) 76 | if child == None: 77 | print('Could not login to host:', host) 78 | continue 79 | print('Changing password on host:', host) 80 | change_password_remote(child, user, password, newpassword) 81 | child.expect(COMMAND_PROMPT) 82 | child.sendline('exit') 83 | print('done!') 84 | 85 | -------------------------------------------------------------------------------- /calysto/widget/__init__.py: -------------------------------------------------------------------------------- 1 | from io import BytesIO 2 | 3 | from PIL import Image 4 | 5 | from calysto.chart import GoogleChart 6 | 7 | __all__ = ['GoogleChart'] 8 | 9 | def display_pil_image(im): 10 | """Displayhook function for PIL Images, rendered as PNG.""" 11 | from IPython.core import display 12 | b = BytesIO() 13 | im.save(b, format='png') 14 | data = b.getvalue() 15 | 16 | ip_img = display.Image(data=data, format='png', embed=True) 17 | return ip_img._repr_png_() 18 | 19 | 20 | # register display func with PNG formatter: 21 | try: 22 | png_formatter = get_ipython().display_formatter.formatters['image/png'] 23 | dpi = png_formatter.for_type(Image.Image, display_pil_image) 24 | except: 25 | pass # not in an IPython client 26 | -------------------------------------------------------------------------------- /calysto/widget/camera.py: -------------------------------------------------------------------------------- 1 | # calysto - general support utilities for Jupyter 2 | # 3 | # Copyright (c) Douglas S. Blank 4 | # 5 | # This program is free software; you can redistribute it and/or modify 6 | # it under the terms of the GNU General Public License as published by 7 | # the Free Software Foundation; either version 3 of the License, or 8 | # (at your option) any later version. 9 | # 10 | # This program is distributed in the hope that it will be useful, 11 | # but WITHOUT ANY WARRANTY; without even the implied warranty of 12 | # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 13 | # GNU General Public License for more details. 14 | # 15 | # You should have received a copy of the GNU General Public License 16 | # along with this program; if not, write to the Free Software 17 | # Foundation, Inc., 51 Franklin Street, Fifth Floor, 18 | # Boston, MA 02110-1301 USA 19 | 20 | import numpy as np 21 | import PIL 22 | import base64 23 | import io 24 | 25 | from IPython.display import Javascript, display 26 | import ipywidgets 27 | from ipywidgets import Widget, register, widget_serialization, DOMWidget 28 | from traitlets import Bool, Dict, Int, Float, Unicode, List, Instance 29 | 30 | __version__ = "1.0.3" 31 | 32 | #------------------------------------------------------------------------ 33 | # utility functions 34 | 35 | def uri_to_image(image_str, width=320, height=240): 36 | header, image_b64 = image_str.split(",") 37 | image_binary = base64.b64decode(image_b64) 38 | image = PIL.Image.open(io.BytesIO(image_binary)).resize((width, height)) 39 | return image 40 | 41 | @register("CameraWidget") 42 | class CameraWidget(DOMWidget): 43 | """Represents a media source.""" 44 | _view_module = Unicode('camera').tag(sync=True) 45 | _view_name = Unicode('CameraView').tag(sync=True) 46 | _model_module = Unicode('camera').tag(sync=True) 47 | _model_name = Unicode('CameraModel').tag(sync=True) 48 | _view_module_version = Unicode(__version__).tag(sync=True) 49 | # Specify audio constraint and video constraint as a boolean or dict. 50 | audio = Bool(False).tag(sync=True) 51 | video = Bool(True).tag(sync=True) 52 | image = Unicode('').tag(sync=True) 53 | image_count = Int(0).tag(sync=True) 54 | 55 | def __init__(self, *args, **kwargs): 56 | display(Javascript(get_camera_javascript())) 57 | super().__init__(*args, **kwargs) 58 | 59 | def get_image(self): 60 | if self.image: 61 | return uri_to_image(self.image) 62 | 63 | def get_data(self): 64 | if self.image: 65 | image = uri_to_image(self.image) 66 | ## trim from 4 to 3 dimensions: (remove alpha) 67 | # remove the 3 index of dimension index 2 (the A of RGBA color) 68 | image = np.delete(image, np.s_[3], 2) 69 | return (np.array(image).astype("float32") / 255.0) 70 | 71 | def get_camera_javascript(width=320, height=240): 72 | if ipywidgets._version.version_info < (7,): 73 | jupyter_widgets = "jupyter-js-widgets" 74 | else: 75 | jupyter_widgets = "@jupyter-widgets/base" 76 | camera_javascript = """ 77 | require.undef('camera'); 78 | 79 | define('camera', ["%(jupyter_widgets)s"], function(widgets) { 80 | var CameraView = widgets.DOMWidgetView.extend({ 81 | defaults: _.extend({}, widgets.DOMWidgetView.prototype.defaults, { 82 | _view_name: 'CameraView', 83 | _view_module: 'camera', 84 | audio: false, 85 | video: true, 86 | }), 87 | 88 | initialize: function() { 89 | 90 | var div = document.createElement('div'); 91 | var el = document.createElement('video'); 92 | el.setAttribute('id', "video_widget"); 93 | el.setAttribute('width', %(width)s); 94 | el.setAttribute('height', %(height)s); 95 | div.appendChild(el); 96 | var canvas = document.createElement('canvas'); 97 | canvas.setAttribute('id', "video_canvas"); 98 | canvas.setAttribute('width', %(width)s); 99 | canvas.setAttribute('height', %(height)s); 100 | div.appendChild(canvas); 101 | div.appendChild(document.createElement('br')); 102 | var button = document.createElement('button'); 103 | button.innerHTML = "Take a Picture"; 104 | var that = this; 105 | button.onclick = function(b) { 106 | var video = document.querySelector("#video_widget"); 107 | var canvas = document.querySelector("#video_canvas"); 108 | if (video) { 109 | canvas.getContext('2d').drawImage(video, 0, 0, canvas.width, canvas.height); 110 | var url = canvas.toDataURL('image/png'); 111 | if (that.model) { 112 | that.model.set('image', url); 113 | that.model.save_changes(); 114 | } 115 | } 116 | }; 117 | div.appendChild(button); 118 | this.setElement(div); 119 | CameraView.__super__.initialize.apply(this, arguments); 120 | }, 121 | 122 | render: function() { 123 | var that = this; 124 | that.model.stream.then(function(stream) { 125 | that.el.children[0].src = window.URL.createObjectURL(stream); 126 | that.el.children[0].play(); 127 | }); 128 | } 129 | }); 130 | 131 | var CameraModel = widgets.DOMWidgetModel.extend({ 132 | defaults: _.extend({}, widgets.DOMWidgetModel.prototype.defaults, { 133 | _model_name: 'CameraModel', 134 | _model_module: 'camera', 135 | audio: false, 136 | video: true 137 | }), 138 | 139 | initialize: function() { 140 | CameraModel.__super__.initialize.apply(this, arguments); 141 | // Get the camera permissions 142 | this.stream = navigator.mediaDevices.getUserMedia({audio: false, video: true}); 143 | } 144 | }); 145 | return { 146 | CameraModel: CameraModel, 147 | CameraView: CameraView 148 | } 149 | }); 150 | """ % {"width": width, "height": height, "jupyter_widgets": jupyter_widgets} 151 | return camera_javascript 152 | -------------------------------------------------------------------------------- /calysto/zgraphics.py: -------------------------------------------------------------------------------- 1 | from calysto.graphics import (Canvas, Color, Pixel, Point) 2 | 3 | from ipywidgets import widgets 4 | import time 5 | 6 | from calysto.graphics import (Text as _CText, 7 | Rectangle as _CRectangle, 8 | Ellipse as _CEllipse, 9 | Line as _CLine, 10 | Polyline as _CPolyline, 11 | Polygon as _CPolygon, 12 | Picture as _CPicture, 13 | Arc as _CArc, 14 | Circle as _CCircle) 15 | 16 | class GraphWin(Canvas): 17 | ID = 1 18 | def __init__(self, title="Graphics Window", width=320, height=240): 19 | super(GraphWin, self).__init__(size=(width, height)) 20 | from calysto.display import display, Javascript 21 | self.background_color = None 22 | self.title = widgets.HTML("%s" % title) 23 | self.mouse_x = widgets.IntText() 24 | self.mouse_y = widgets.IntText() 25 | self.svg_canvas = widgets.HTML( 26 | self.get_html(onClick="window.clicked(evt, '%s', '%s')" % (self.mouse_x.model_id, self.mouse_y.model_id))) 27 | self.window = widgets.HBox([self.title, 28 | self.svg_canvas]) 29 | display(Javascript(""" 30 | window.clicked = function (evt, x_model, y_model) { 31 | var e = evt.srcElement.farthestViewportElement || evt.target; 32 | var dim = e.getBoundingClientRect(); 33 | var x = evt.clientX - dim.left; 34 | var y = evt.clientY - dim.top; 35 | var manager = IPython.WidgetManager._managers[0]; 36 | var model_prom = manager.get_model(x_model); 37 | model_prom.then(function(model) { 38 | model.set('value', Math.round(x)); 39 | model.save_changes(); 40 | }); 41 | model_prom = manager.get_model(y_model); 42 | model_prom.then(function(model) { 43 | model.set('value', Math.round(y)); 44 | model.save_changes(); 45 | }); 46 | }; 47 | """)) 48 | display(self.window) 49 | 50 | def close(self): 51 | self.window.close() 52 | 53 | def setBackground(self, color): 54 | self.background_color = color 55 | 56 | def draw(self, shape): 57 | super(GraphWin, self).draw(shape) 58 | self.svg_canvas.value = self.get_html( 59 | onClick="window.clicked(evt, '%s', '%s')" % (self.mouse_x.model_id, self.mouse_y.model_id)) 60 | 61 | def _render(self): 62 | drawing = super(GraphWin, self)._render() 63 | if self.background_color: 64 | rect = _CRectangle((0,0), self.size) 65 | rect.canvas = self 66 | rect.matrix = [] 67 | rect.fill(self.background_color) 68 | rect._add(drawing) 69 | return drawing 70 | 71 | def plot(self, x, y, color="black"): 72 | """ 73 | Uses coordinant system. 74 | """ 75 | p = Point(x, y) 76 | p.fill(color) 77 | p.draw(self) 78 | 79 | def plotPixel(self, x, y, color="black"): 80 | """ 81 | Doesn't use coordinant system. 82 | """ 83 | p = Point(x, y) 84 | p.fill(color) 85 | p.draw(self) 86 | p.t = lambda v: v 87 | p.tx = lambda v: v 88 | p.ty = lambda v: v 89 | 90 | def getMouse(self): 91 | """ 92 | Waits for a mouse click. 93 | """ 94 | # FIXME: this isn't working during an executing cell 95 | self.mouse_x.value = -1 96 | self.mouse_y.value = -1 97 | while self.mouse_x.value == -1 and self.mouse_y.value == -1: 98 | time.sleep(.1) 99 | return (self.mouse_x.value, self.mouse_y.value) 100 | 101 | def checkMouse(self): 102 | """ 103 | Gets last click, or none. 104 | """ 105 | return (self.mouse_x.value, self.mouse_y.value) 106 | 107 | class Text(_CText): 108 | def __init__(self, center, text, **extras): 109 | super(Text, self).__init__(text, 110 | (center[0] - len(text) * 3.5, 111 | center[1] + 10), **extras) 112 | 113 | class Rectangle(_CRectangle): 114 | def __init__(self, start, stop, **extras): 115 | super(Rectangle, self).__init__( 116 | (start[0], start[1]), 117 | (stop[0] - start[0], stop[1] - start[1]), 118 | **extras) 119 | self.noFill() 120 | 121 | class Oval(_CEllipse): 122 | """ 123 | """ 124 | def __init__(self, start, stop, **extras): 125 | super(Oval, self).__init__( 126 | (start[0] + (stop[0] + start[0])/2, 127 | start[1] + (stop[1] - start[1])/2), 128 | ((stop[0] - start[0])/2, 129 | (stop[1] - start[1])/2), 130 | **extras) 131 | self.noFill() 132 | 133 | class Circle(_CCircle): 134 | """ 135 | """ 136 | def __init__(self, center, radius, **kwargs): 137 | super(Circle, self).__init__((center[0], center[1]), 138 | radius=radius, **kwargs) 139 | self.noFill() 140 | 141 | class Line(_CLine): 142 | """ 143 | """ 144 | def __init__(self, *args, **kwargs): 145 | super(Line, self).__init__(*args, **kwargs) 146 | self.noFill() 147 | -------------------------------------------------------------------------------- /setup.cfg: -------------------------------------------------------------------------------- 1 | [metadata] 2 | license_file = LICENSE 3 | -------------------------------------------------------------------------------- /setup.py: -------------------------------------------------------------------------------- 1 | try: 2 | from setuptools.core import setup 3 | except ImportError: 4 | from distutils.core import setup 5 | import sys 6 | 7 | svem_flag = '--single-version-externally-managed' 8 | if svem_flag in sys.argv: 9 | # Die, setuptools, die. 10 | sys.argv.remove(svem_flag) 11 | 12 | setup(name='calysto', 13 | version='1.0.6', 14 | description='Libraries and Languages for Python and IPython', 15 | long_description="Libraries and Languages for IPython and Python", 16 | author='Douglas Blank', 17 | author_email='doug.blank@gmail.com', 18 | url="https://github.com/Calysto/calysto", 19 | install_requires=['IPython', 'metakernel', 'svgwrite', 'cairosvg', 20 | "ipywidgets"], 21 | packages=['calysto', 22 | 'calysto.util', 23 | 'calysto.widget', 24 | 'calysto.chart', 25 | 'calysto.simulations', 26 | 'calysto.magics', 27 | 'calysto.ai'], 28 | data_files = [("calysto/images", ["calysto/images/logo-64x64.png", 29 | "calysto/images/logo-32x32.png"])], 30 | classifiers = [ 31 | 'Framework :: IPython', 32 | 'License :: OSI Approved :: BSD License', 33 | 'Programming Language :: Python :: 3', 34 | 'Programming Language :: Python :: 2', 35 | 'Programming Language :: Scheme', 36 | ] 37 | ) 38 | --------------------------------------------------------------------------------