├── project ├── build.properties └── plugins.sbt ├── demos ├── digit-prediction │ ├── model.h5 │ └── digits_prediction.nlogox ├── Flocking Clusters.nlogox └── Traffic Basic - Reinforcement.nlogox ├── .gitignore ├── src ├── test │ └── Tests.scala ├── pyext.py └── main │ └── PythonExtension.scala ├── BUILDING.md ├── .github └── workflows │ └── main.yml ├── USING.md ├── tests.txt ├── documentation.conf └── README.md /project/build.properties: -------------------------------------------------------------------------------- 1 | sbt.version=1.7.2 2 | -------------------------------------------------------------------------------- /demos/digit-prediction/model.h5: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/NetLogo/Python-Extension/HEAD/demos/digit-prediction/model.h5 -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | .bsp/ 2 | .bundledFiles 3 | target/ 4 | extensions/ 5 | /*.jar 6 | /*.zip 7 | 8 | py/ 9 | /pyext.py 10 | -------------------------------------------------------------------------------- /project/plugins.sbt: -------------------------------------------------------------------------------- 1 | resolvers ++= Seq( 2 | "netlogo-extension-plugin" at "https://dl.cloudsmith.io/public/netlogo/netlogo-extension-plugin/maven/" 3 | , "netlogo-extension-documentation" at "https://dl.cloudsmith.io/public/netlogo/netlogo-extension-documentation/maven/" 4 | ) 5 | 6 | addSbtPlugin("org.nlogo" % "netlogo-extension-plugin" % "7.0.2") 7 | addSbtPlugin("org.nlogo" % "netlogo-extension-documentation" % "0.8.3") 8 | -------------------------------------------------------------------------------- /src/test/Tests.scala: -------------------------------------------------------------------------------- 1 | // (C) Uri Wilensky. https://github.com/NetLogo/Python-Extension 2 | 3 | package org.nlogo.extensions.py 4 | 5 | import java.io.File 6 | 7 | import org.nlogo.headless.TestLanguage 8 | 9 | object Tests { 10 | val testFileNames = Seq("tests.txt") 11 | val testFiles = testFileNames.map( (f) => (new File(f)).getCanonicalFile ) 12 | } 13 | 14 | class Tests extends TestLanguage(Tests.testFiles) 15 | -------------------------------------------------------------------------------- /BUILDING.md: -------------------------------------------------------------------------------- 1 | ## Building 2 | 3 | Make sure your sbt is at least at version 0.13.6 4 | 5 | Run `sbt package`. 6 | 7 | If compilation succeeds, `py.jar` will be created and the required dependencies will be copied to the root of the repository. Copy all the `jar` files and `pyext.py` from the repository root to a `py` directory inside your NetLogo `extensions` directory. 8 | 9 | Run `sbt test` to run the NetLogo language test code from `tests.txt`. 10 | -------------------------------------------------------------------------------- /.github/workflows/main.yml: -------------------------------------------------------------------------------- 1 | name: build-and-test 2 | 3 | on: 4 | push: 5 | pull_request: 6 | workflow_dispatch: 7 | 8 | env: 9 | LIBERICA_URL: https://download.bell-sw.com/java/17.0.5+8/bellsoft-jdk17.0.5+8-linux-amd64-full.tar.gz 10 | 11 | jobs: 12 | build-and-test: 13 | runs-on: ubuntu-latest 14 | steps: 15 | - uses: actions/checkout@v3 16 | 17 | - uses: olafurpg/setup-scala@v13 18 | with: 19 | java-version: liberica@17=tgz+${{ env.LIBERICA_URL }} 20 | 21 | - uses: actions/setup-python@v4 22 | with: 23 | python-version: '3.x' 24 | 25 | - name: Install Python 3 libraries 26 | run: pip3 install numpy scikit-learn 27 | 28 | - run: sbt -v update compile 29 | - run: sbt -v test 30 | -------------------------------------------------------------------------------- /USING.md: -------------------------------------------------------------------------------- 1 | ## Using 2 | 3 | As with all NetLogo extensions, you must declare that you're using this extension in your NetLogo code with: 4 | 5 | ```netlogo 6 | extensions [ 7 | py 8 | ; ... your other extensions 9 | ] 10 | ``` 11 | 12 | The general workflow of this extension is to run `py:setup py:python` to initialize the Python session that NetLogo will talk to, and then use `py:run`, `py:runresult`, and `py:set` to interact with that Python session. 13 | By default, `py:python` will report the latest version of Python that the extension finds on your system. 14 | You can also use `py:python3` or `py:python2` to use Python 3 or 2 specifically. 15 | See the [Configuring](#configuring) section below to specify exactly which Python installations to use. 16 | 17 | Here's an example to get you started: 18 | 19 | ```netlogo 20 | observer> py:setup py:python 21 | observer> show py:runresult "1 + 1" 22 | observer: 2 23 | observer> py:run "print('hi')" 24 | hi 25 | observer> py:run "import math" 26 | observer> show py:runresult "[math.factorial(i) for i in range(10)]" 27 | observer: [1 1 2 6 24 120 720 5040 40320 362880] 28 | observer> py:set "patch_xs" [ pxcor ] of patches 29 | observer> show py:runresult "max(patch_xs)" 30 | observer: 16 31 | observer> py:run "print(min(patch_xs))" 32 | -16 33 | ``` 34 | 35 | See the documentation for each of the particular primitives for details on, for instance, how to multi-line statements and how object type conversions work. 36 | See the demo models included in the `demo` folder for some examples of using libraries such as `numpy` and `tensorflow`. 37 | 38 | See the documentation on `py:set` to learn how to have the extension serialize entire agents and agentsets into Python dictionaries. 39 | 40 | There is also a separate interactive Python console that can be found under Python > Interactive Python Console. 41 | This console is connected to the same Python session as all the Python NetLogo primitives, so you can define a variable in your model and access it in the interactive Python console window. 42 | 43 | ### Error handling 44 | 45 | Python errors will be reported in NetLogo as "Extension exceptions". For instance, this code: 46 | 47 | ```netlogo 48 | py:run "raise Exception('hi')" 49 | ``` 50 | 51 | will result in the NetLogo error "Extension exception: hi". 52 | To see the Python stack trace of the exception, click "Show internal details". 53 | If you then scroll down, you will find the Python stack trace in the middle of the Java stack trace. 54 | 55 | ## Configuring 56 | 57 | By default, the `py:python2`, `py:python3`, and `py:python` commands will attempt to find a Python executable of the appropriate version. 58 | If you'd like to change which Python executable they use, or they can't find a Python executable, you should configure which Python executables to use. 59 | You can do this by either: 60 | 61 | - Using the configuration menu under the Python toolbar menu that appears when you use a model that uses the Python extension. 62 | - Editing the `python.properties` file that appears in the Python extension installation folder as follows: 63 | 64 | ``` 65 | python3=/path/to/python3 66 | python2=/path/to/python2 67 | ``` 68 | -------------------------------------------------------------------------------- /src/pyext.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | 3 | import os 4 | import socket 5 | import sys 6 | import numbers 7 | import json 8 | 9 | if sys.version_info[0] > 2 and sys.version_info[1] > 3: 10 | from collections.abc import Mapping 11 | else: 12 | from collections import Mapping 13 | 14 | import traceback 15 | 16 | # In 17 | QUIT_MSG = -1 18 | STMT_MSG = 0 19 | EXPR_MSG = 1 20 | ASSN_MSG = 2 21 | EXPR_STRINGIFIED_MSG = 3 22 | HEARTBEAT_REQUEST = 4 23 | 24 | # Out 25 | SUCC_MSG = 0 26 | ERR_MSG = 1 27 | HEARTBEAT_RESPONSE = 4 28 | 29 | def start_server(): 30 | sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM) 31 | try: 32 | conn = make_connection(sock) 33 | try: 34 | encoder = FlexibleEncoder() 35 | env_globals = {} 36 | for line in conn.makefile(): 37 | try: 38 | decoded = json.loads(line) 39 | type = decoded["type"] 40 | 41 | if type == QUIT_MSG: 42 | conn.sendall(json.dumps({"type": SUCC_MSG}).encode('utf-8') + b"\n") 43 | break 44 | elif type == STMT_MSG: 45 | handle_statement(conn, decoded["body"], env_globals, encoder) 46 | elif type == EXPR_MSG: 47 | handle_expression(conn, decoded["body"], env_globals, encoder) 48 | elif type == ASSN_MSG: 49 | handle_assignment(conn, decoded["body"], env_globals, encoder) 50 | elif type == EXPR_STRINGIFIED_MSG: 51 | handle_expression_stringified(conn, decoded["body"], env_globals, encoder) 52 | elif type == HEARTBEAT_REQUEST: 53 | handle_heartbeat(conn) 54 | except Exception as e: 55 | handle_exception(conn, e, encoder) 56 | finally: 57 | flush() 58 | finally: 59 | conn.close() 60 | finally: 61 | sock.close() 62 | 63 | def make_connection(sock): 64 | sock.bind(('localhost', 0)) 65 | sock.listen(0) 66 | _, port = sock.getsockname() 67 | sys.stdout.write("{}\n".format(port)) 68 | sys.stdout.flush() 69 | conn, addr = sock.accept() 70 | return conn 71 | 72 | def handle_heartbeat(conn): 73 | conn.sendall(json.dumps({"type": HEARTBEAT_RESPONSE}).encode('utf-8') + b"\n") 74 | 75 | def handle_statement(conn, body, env_globals, encoder): 76 | exec(body, env_globals) 77 | conn.sendall(json.dumps({"type": SUCC_MSG, "body": ""}).encode('utf-8') + b"\n") 78 | 79 | def handle_expression(conn, body, env_globals, encoder): 80 | evaluated = eval(body, env_globals) 81 | encoded = encoder.encode({"type": SUCC_MSG, "body": evaluated}) 82 | conn.sendall(encoded.encode('utf-8') + b"\n") 83 | 84 | 85 | def handle_assignment(conn, body, env_globals, encoder): 86 | varName = body["varName"] 87 | value = body["value"] 88 | env_globals[varName] = value 89 | conn.sendall(json.dumps({"type": SUCC_MSG, "body": ""}).encode('utf-8') + b"\n") 90 | 91 | 92 | def handle_expression_stringified(conn, body, env_globals, encoder): 93 | representation = "" 94 | if len(body.strip()) > 0: 95 | ## Ask python if the given string can be evaluated as an expression. If so, evaluate it and return it, if not, 96 | ## Then try running it as code that doesn't evaluate to anything and return nothing. 97 | try: 98 | compiled = compile(body, "", 'eval') 99 | evaluated = eval(compiled, env_globals) 100 | if evaluated is not None: 101 | if isinstance(evaluated, str): 102 | representation = evaluated 103 | else: 104 | representation = repr(evaluated) 105 | 106 | except SyntaxError as e: 107 | exec(body, env_globals) 108 | 109 | encoded = encoder.encode({"type": SUCC_MSG, "body": representation}) 110 | conn.sendall(encoded.encode('utf-8') + b"\n") 111 | 112 | 113 | def handle_exception(conn, e, encoder): 114 | err_data = {"type": ERR_MSG, "body": {"message": str(e), "longMessage": traceback.format_exc()}} 115 | conn.sendall(encoder.encode(err_data).encode('utf-8') + b"\n") 116 | 117 | 118 | def flush(): 119 | sys.stdout.flush() 120 | sys.stderr.flush() 121 | 122 | 123 | class FlexibleEncoder(json.JSONEncoder): 124 | def default(self, o): 125 | if isinstance(o, numbers.Integral): 126 | return int(o) 127 | if isinstance(o, numbers.Number): 128 | return float(o) 129 | if isinstance(o, Mapping): 130 | return dict(o) 131 | elif hasattr(o, '__len__') and hasattr(o, '__iter__'): 132 | return list(o) 133 | else: 134 | return json.JSONEncoder.default(self, o) # let it error 135 | 136 | 137 | if __name__ == '__main__': 138 | sys.path.insert(0, os.getcwd()) 139 | start_server() 140 | -------------------------------------------------------------------------------- /tests.txt: -------------------------------------------------------------------------------- 1 | # Don't know how to test multiline error output... the error here is obsolete 2 | #friendly-error-with-no-setup 3 | # extensions [ py ] 4 | # O> py:run "foo" => ERROR Extension exception: Python process has not been started. Please run PY:SETUP before any other python #extension primitive. 5 | #nonexistent-python 6 | # extensions [ py ] 7 | # O> py:setup "foobarbaz" => ERROR Extension exception: Couldn't find Python executable: foobarbaz 8 | 9 | # open-connection-python2 10 | # extensions [ py ] 11 | # O> py:setup py:python2 12 | # O> py:run "print('python hi1!')" 13 | # O> py:run "print('python hi2!')" 14 | # 15 | # python2-gets-python-2 16 | # extensions [ py ] 17 | # O> py:setup py:python2 18 | # O> py:run "import sys" 19 | # py:runresult "sys.version_info[0]" => 2 20 | # 21 | # basic-python-types-convert-python2 22 | # extensions [ py ] 23 | # O> py:setup py:python2 24 | # py:runresult "1" => 1 25 | # py:runresult "'hi'" => "hi" 26 | # py:runresult "True" => true 27 | # py:runresult "False" => false 28 | # py:runresult "[1, 'hi', [2, 'bye']]" => [1 "hi" [2 "bye"]] 29 | # 30 | # basic-netlogo-types-convert-python2 31 | # extensions [ py ] 32 | # O> py:setup py:python2 33 | # O> py:set "num" 1 34 | # O> py:set "string" "hi" 35 | # O> py:set "lst" [1 "hi" [2 "bye"]] 36 | # O> py:set "b" true 37 | # py:runresult "num" => 1 38 | # py:runresult "string" => "hi" 39 | # py:runresult "lst" => [1 "hi" [2 "bye"]] 40 | # py:runresult "b" => true 41 | # 42 | # error-handling-python2 43 | # extensions [ py ] 44 | # O> py:setup py:python2 45 | # O> py:run "raise Exception('hi')" => ERROR Extension exception: hi 46 | # py:runresult "1 / 0" => ERROR Extension exception: integer division or modulo by zero 47 | # 48 | # dicts-convert-to-lists-python2 49 | # extensions [ py ] 50 | # O> py:setup py:python2 51 | # py:runresult "{'a': 1}" => [["a" 1]] 52 | # 53 | # sets-convert-to-lists-python2 54 | # extensions [ py ] 55 | # O> py:setup py:python2 56 | # py:runresult "{1, 1}" => [1] 57 | # 58 | # np-array-convert-to-lists-python2 59 | # extensions [ py ] 60 | # O> py:setup py:python2 61 | # O> py:run "import numpy as np" 62 | # py:runresult "np.array([1,2,3])" => [1 2 3] 63 | # 64 | # supports-utf8-python2 65 | # extensions [ py ] 66 | # O> py:setup py:python2 67 | # py:runresult "'😃'" => "😃" 68 | # 69 | # errors-on-nan-python2 70 | # extensions [ py ] 71 | # O> py:setup py:python2 72 | # py:runresult "float('nan')" => ERROR Extension exception: Python reported a non-numeric value from a mathematical operation. 73 | # 74 | # errors-on-infinity-python2 75 | # extensions [ py ] 76 | # O> py:setup py:python2 77 | # py:runresult "float('inf')" => ERROR Extension exception: Python reported a number too large for NetLogo. 78 | 79 | open-connection-python3 80 | extensions [ py ] 81 | O> py:setup py:python3 82 | 83 | python3-gets-python-3 84 | extensions [ py ] 85 | O> py:setup py:python3 86 | O> py:run "import sys" 87 | py:runresult "sys.version_info[0]" => 3 88 | 89 | basic-python-types-convert-python3 90 | extensions [ py ] 91 | O> py:setup py:python3 92 | py:runresult "1" => 1 93 | py:runresult "'hi'" => "hi" 94 | py:runresult "True" => true 95 | py:runresult "False" => false 96 | py:runresult "[1, 'hi', [2, 'bye']]" => [1 "hi" [2 "bye"]] 97 | 98 | basic-netlogo-types-convert-python3 99 | extensions [ py ] 100 | O> py:setup py:python3 101 | O> py:set "num" 1 102 | O> py:set "string" "hi" 103 | O> py:set "lst" [1 "hi" [2 "bye"]] 104 | O> py:set "b" true 105 | py:runresult "num" => 1 106 | py:runresult "string" => "hi" 107 | py:runresult "lst" => [1 "hi" [2 "bye"]] 108 | py:runresult "b" => true 109 | 110 | error-handling-python3 111 | extensions [ py ] 112 | O> py:setup py:python3 113 | O> py:run "raise Exception('hi')" => ERROR Extension exception: hi 114 | py:runresult "1 / 0" => ERROR Extension exception: division by zero 115 | 116 | multiline-python3 117 | extensions [ py ] 118 | O> py:setup py:python3 119 | O> (py:run "x = 0" "for i in range(11):" " x += i" "x += 45") 120 | py:runresult "x" => 100 121 | 122 | # We were running into https://stackoverflow.com/questions/45132645/list-comprehension-in-exec-with-empty-locals-nameerror 123 | comprehensions-see-variables-python3 124 | extensions [ py ] 125 | O> py:setup py:python3 126 | O> (py:run "x = 2" "y = [i ** x for i in range(5)]") 127 | py:runresult "y" => [0 1 4 9 16] 128 | 129 | dicts-convert-to-lists-python3 130 | extensions [ py ] 131 | O> py:setup py:python3 132 | py:runresult "{'a': 1}" => [["a" 1]] 133 | 134 | sets-convert-to-lists-python3 135 | extensions [ py ] 136 | O> py:setup py:python3 137 | py:runresult "{1, 1}" => [1] 138 | 139 | np-array-convert-to-lists-python3 140 | extensions [ py ] 141 | O> py:setup py:python3 142 | O> py:run "import numpy as np" 143 | py:runresult "np.array([1,2,3])" => [1 2 3] 144 | 145 | send-receive-big-python3 146 | extensions [ py ] 147 | O> py:setup py:python3 148 | O> py:set "foo" range 100000 149 | length py:runresult "foo" => 100000 150 | 151 | setup-takes-arguments-python3 152 | extensions [ py ] 153 | O> (py:setup py:python3 "-O") 154 | py:runresult "0" => 0 155 | 156 | supports-utf8-python3 157 | extensions [ py ] 158 | O> py:setup py:python3 159 | py:runresult "'😃'" => "😃" 160 | 161 | python-prefers-3 162 | extensions [ py ] 163 | O> py:setup py:python 164 | O> py:run "import sys" 165 | py:runresult "sys.version_info[0]" => 3 166 | 167 | errors-on-nan-python3 168 | extensions [ py ] 169 | O> py:setup py:python3 170 | py:runresult "float('nan')" => ERROR Extension exception: Python reported a non-numeric value from a mathematical operation. 171 | 172 | errors-on-infinity-python3 173 | extensions [ py ] 174 | O> py:setup py:python3 175 | py:runresult "float('inf')" => ERROR Extension exception: Python reported a number too large for NetLogo. 176 | 177 | turtle-serialization 178 | extensions [ py ] 179 | breed [goats goat] 180 | goats-own [energy agent-var agentset-var ] 181 | O> py:setup py:python3 182 | O> create-goats 1 [ set heading 0 set color 75 ] 183 | O> ask goat 0 [ set energy 42 ] 184 | O> py:set "goat" goat 0 185 | py:runresult "str(goat)" => "{'WHO': 0, 'COLOR': 75, 'HEADING': 0, 'XCOR': 0, 'YCOR': 0, 'SHAPE': 'default', 'LABEL': '', 'LABEL-COLOR': 9.9, 'BREED': 'GOATS', 'HIDDEN?': False, 'SIZE': 1, 'PEN-SIZE': 1, 'PEN-MODE': 'up', 'ENERGY': 42, 'AGENT-VAR': 0, 'AGENTSET-VAR': 0}" 186 | 187 | turtle-re-serialization 188 | extensions [ py ] 189 | breed [goats goat] 190 | goats-own [energy agent-var agentset-var ] 191 | O> py:setup py:python3 192 | O> create-goats 1 [ set heading 0 set color 75 ] 193 | O> ask goat 0 [ set energy 42 ] 194 | O> py:set "goat" goat 0 195 | py:runresult "goat" => [["WHO" 0] ["COLOR" 75] ["HEADING" 0] ["XCOR" 0] ["YCOR" 0] ["SHAPE" "default"] ["LABEL" ""] ["LABEL-COLOR" 9.9] ["BREED" "GOATS"] ["HIDDEN?" false] ["SIZE" 1] ["PEN-SIZE" 1] ["PEN-MODE" "up"] ["ENERGY" 42] ["AGENT-VAR" 0] ["AGENTSET-VAR" 0]] 196 | 197 | turtle-re-serialization-agent-and-agentset-variables 198 | extensions [ py ] 199 | breed [goats goat] 200 | goats-own [energy agent-var agentset-var ] 201 | O> py:setup py:python3 202 | O> create-goats 2 [ set heading 0 set color 75 ] 203 | O> ask goat 0 [ set agent-var goat 1 ] 204 | O> ask goat 0 [ set agentset-var goats ] 205 | O> py:set "goat" goat 0 206 | py:runresult "goat" => [["WHO" 0] ["COLOR" 75] ["HEADING" 0] ["XCOR" 0] ["YCOR" 0] ["SHAPE" "default"] ["LABEL" ""] ["LABEL-COLOR" 9.9] ["BREED" "GOATS"] ["HIDDEN?" false] ["SIZE" 1] ["PEN-SIZE" 1] ["PEN-MODE" "up"] ["ENERGY" 0] ["AGENT-VAR" "goat 1"] ["AGENTSET-VAR" "GOATS"]] 207 | -------------------------------------------------------------------------------- /documentation.conf: -------------------------------------------------------------------------------- 1 | extensionName = "py" 2 | markdownTemplate = """ 3 | # NetLogo Python extension 4 | 5 | This NetLogo extension allows you to run Python code from NetLogo. It works with both Python 2 and 3, and should work with almost all Python libraries. 6 | 7 | {{> BUILDING.md }} 8 | 9 | {{> USING.md }} 10 | 11 | 12 | ## Primitives 13 | 14 | {{#primitives}} 15 | {{> primTemplate}} 16 | {{/primitives}} 17 | """ 18 | 19 | primTemplate = """ 20 | ### `{{name}}` 21 | 22 | ```NetLogo 23 | {{#examples}} 24 | {{primitive.fullName}}{{#args}} {{name}}{{/args}} 25 | {{/examples}} 26 | ``` 27 | 28 | {{{description}}} 29 | """ 30 | 31 | filesToIncludeInManual = [ "USING.md", "primitives" ] 32 | 33 | primitives = [ 34 | { 35 | name: setup, 36 | type: command, 37 | arguments: [ { name: python-executable, type: string } ], 38 | description: """ 39 | Create the Python session that this extension will use to execute code. The session will be started with the given Python executable. This command *must* be run before running any other Python extension primitive. Running this command again will shutdown the current Python environment and start a new one. 40 | 41 | The executable may be specified as a relative path, absolute path, or just the executable name if it is on your PATH. 42 | Furthermore, this extension offers a few helper primitives for getting particular versions of Python in system 43 | independent ways. 44 | 45 | In general, unless working with a virtual environment or a specific system setup, you should do: 46 | 47 | ```NetLogo 48 | py:setup py:python ; if your code works with either Python 2 or 3 49 | py:setup py:python3 ; for Python 3 50 | py:setup py:python2 ; for Python 2 51 | ``` 52 | 53 | `py:setup` may be invoked by directly referring to different Pythons as well. For instance: 54 | 55 | ```NetLogo 56 | py:setup "python3" ; if `python3` is on your PATH 57 | py:setup "python" ; if `python` is on your PATH 58 | ``` 59 | 60 | If you use virtualenv or Conda, simply specify the path of the Python executable in the environment you wish to use: 61 | 62 | ```NetLogo 63 | py:setup "/path/to/myenv/bin/python" 64 | ``` 65 | 66 | The path may be relative or absolute. So, if you have a virtual environment in the same folder as your model, you can do: 67 | 68 | ```NetLogo 69 | py:setup "myenv/bin/python" 70 | ``` 71 | """ 72 | }, 73 | { 74 | name: python, 75 | type: reporter, 76 | returns: string, 77 | description: """ 78 | Reports either the path to the latest version of Python configured in the `python.properties` file or, if that is blank, looks for a Python executable on your system's PATH. 79 | For Windows, there is an installation option for including Python on your PATH. 80 | For MacOS and Linux, it will likely already be on your PATH. 81 | The output of this reporter is meant to be used with `py:setup`, but you may also use it to see which Python installation this extension will use by default. 82 | 83 | For example, on MacOS with Homebrew installed Python 3: 84 | ```NetLogo 85 | observer> show py:python 86 | observer: "/usr/local/bin/python3" 87 | ``` 88 | """ 89 | }, 90 | { 91 | name: python2, 92 | type: reporter, 93 | returns: string, 94 | description: """ 95 | Reports either the path to Python 2 configured in the `python.properties` file or, if that is blank, looks for a Python 2 executable on your system's PATH. 96 | For Windows, there is an installation option for including Python on your PATH. 97 | For MacOS and Linux, it will likely already be on your PATH. 98 | The output of this reporter is meant to be used with `py:setup`, but you may also use it to see which Python 2 installation this extension will use by default. 99 | 100 | For example, on MacOS with Homebrew installed Python 2: 101 | ```NetLogo 102 | observer> show py:python2 103 | observer: "/usr/local/bin/python2" 104 | ``` 105 | """ 106 | }, 107 | { 108 | name: python3, 109 | type: reporter, 110 | returns: string, 111 | description: """ 112 | Reports either the path to Python 3 configured in the `python.properties` file or, if that is blank, looks for a Python 3 executable on your system's PATH. 113 | For Windows, there is an installation option for including Python on your PATH. 114 | For MacOS and Linux, it will likely already be on your PATH. 115 | The output of this reporter is meant to be used with `py:setup`, but you may also use it to see which Python 3 installation this extension will use by default. 116 | 117 | For example, on MacOS with Homebrew installed Python 3: 118 | ```NetLogo 119 | observer> show py:python3 120 | observer: "/usr/local/bin/python3" 121 | ``` 122 | """ 123 | }, 124 | { 125 | name: run, 126 | type: command, 127 | arguments: [ {name: python-statement, type: "repeatable string"}], 128 | description: """ 129 | Runs the given Python statements in the current Python session. To make multi-line Python code easier to run, this command will take multiple strings, each of which will be interpreted as a separate line of Python code. For instance: 130 | 131 | ```NetLogo 132 | (py:run 133 | "import matplotlib" 134 | "matplotlib.use('TkAgg')" 135 | "import numpy as np" 136 | "import matplotlib.pyplot as plt" 137 | "for i in range(10):" 138 | " plt.plot([ x ** i for x in np.arange(-1, 1, 0.1) ])" 139 | "plt.show()" 140 | ) 141 | ``` 142 | 143 | `py:run` will wait for the statements to finish running before continuing. Thus, if you have long running Python code, NetLogo will pause while it runs. 144 | """ 145 | }, 146 | { 147 | name: runresult, 148 | type: reporter, 149 | arguments: [ {name: "python-expression", type: "repeatable string"} ], 150 | returns: anything, 151 | 152 | description: """ 153 | Evaluates the given Python expression and reports the result. 154 | `py:runresult` attempts to convert from Python data types to NetLogo data types. 155 | Numbers, strings, and booleans convert as you would expect. 156 | Any list-like object in Python (that is, anything with a length that you can iterate through) will be converted to a NetLogo list. 157 | For instance, Python lists and NumPy arrays will convert to NetLogo lists. 158 | Python dicts (and dict-like objects) will convert to a NetLogo list of key-value pairs (where each pair is represented as a list). 159 | `None` will be converted to `nobody`. 160 | Other objects will simply be converted to a string representation. 161 | 162 | Note that due a [current issue](https://github.com/qiemem/PythonExtension/issues/6), dict keys will always be reported as strings. 163 | If you need to report non-string keys, report the `.items()` of the dict instead of the dict itself. 164 | """ 165 | }, 166 | { 167 | name: set, 168 | type: command, 169 | arguments: [ {name: "variable-name", type: "string"}, {name: "value", type: "anything"} ], 170 | description: """ 171 | Sets a variable in the Python session with the given name to the given NetLogo value. NetLogo objects will be converted to Python objects as expected. 172 | 173 | All vanilla NetLogo objects are supported, but objects from other extensions, even other bundled extensions, are not supported. 174 | 175 | ```NetLogo 176 | py:set "x" [1 2 3] 177 | show py:runresult "x" ;; Shows [1 2 3] 178 | ``` 179 | 180 | Agents are converted into dictionaries with elements for each agent variable. Agentsets are converted into lists of agent dictionaries. 181 | 182 | ```NetLogo 183 | breed [goats goat] 184 | goats-own [energy ] 185 | create-goats 1 [ set heading 0 set color 75 ] 186 | ask goat 0 [ set energy 42 ] 187 | py:set "goat" goat 0 188 | py:runresult "str(goat)" ;; Should output: "{'WHO': 0, 'COLOR': 75, 'HEADING': 0, 'XCOR': 0, 'YCOR': 0, 'SHAPE': 'default', 'LABEL': '', 'LABEL-COLOR': 9.9, 'BREED': 'GOATS', 'HIDDEN?': False, 'SIZE': 1, 'PEN-SIZE': 1, 'PEN-MODE': 'up', 'ENERGY': 42}" 189 | ``` 190 | 191 | Agents with variables containing references to agentsets will have those variables converted into the string representation of that agentset. 192 | 193 | """ 194 | } 195 | ] 196 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | 2 | # NetLogo Python extension 3 | 4 | This NetLogo extension allows you to run Python code from NetLogo. It works with both Python 2 and 3, and should work with almost all Python libraries. 5 | 6 | ## Building 7 | 8 | Make sure your sbt is at least at version 0.13.6 9 | 10 | Run `sbt package`. 11 | 12 | If compilation succeeds, `py.jar` will be created and the required dependencies will be copied to the root of the repository. Copy all the `jar` files and `pyext.py` from the repository root to a `py` directory inside your NetLogo `extensions` directory. 13 | 14 | Run `sbt test` to run the NetLogo language test code from `tests.txt`. 15 | 16 | ## Using 17 | 18 | As with all NetLogo extensions, you must declare that you're using this extension in your NetLogo code with: 19 | 20 | ```netlogo 21 | extensions [ 22 | py 23 | ; ... your other extensions 24 | ] 25 | ``` 26 | 27 | The general workflow of this extension is to run `py:setup py:python` to initialize the Python session that NetLogo will talk to, and then use `py:run`, `py:runresult`, and `py:set` to interact with that Python session. 28 | By default, `py:python` will report the latest version of Python that the extension finds on your system. 29 | You can also use `py:python3` or `py:python2` to use Python 3 or 2 specifically. 30 | See the [Configuring](#configuring) section below to specify exactly which Python installations to use. 31 | 32 | Here's an example to get you started: 33 | 34 | ```netlogo 35 | observer> py:setup py:python 36 | observer> show py:runresult "1 + 1" 37 | observer: 2 38 | observer> py:run "print('hi')" 39 | hi 40 | observer> py:run "import math" 41 | observer> show py:runresult "[math.factorial(i) for i in range(10)]" 42 | observer: [1 1 2 6 24 120 720 5040 40320 362880] 43 | observer> py:set "patch_xs" [ pxcor ] of patches 44 | observer> show py:runresult "max(patch_xs)" 45 | observer: 16 46 | observer> py:run "print(min(patch_xs))" 47 | -16 48 | ``` 49 | 50 | See the documentation for each of the particular primitives for details on, for instance, how to multi-line statements and how object type conversions work. 51 | See the demo models included in the `demo` folder for some examples of using libraries such as `numpy` and `tensorflow`. 52 | 53 | ### Error handling 54 | 55 | Python errors will be reported in NetLogo as "Extension exceptions". For instance, this code: 56 | 57 | ```netlogo 58 | py:run "raise Exception('hi')" 59 | ``` 60 | 61 | will result in the NetLogo error "Extension exception: hi". 62 | To see the Python stack trace of the exception, click "Show internal details". 63 | If you then scroll down, you will find the Python stack trace in the middle of the Java stack trace. 64 | 65 | ## Configuring 66 | 67 | By default, the `py:python2`, `py:python3`, and `py:python` commands will attempt to find a Python executable of the appropriate version. 68 | If you'd like to change which Python executable they use, or they can't find a Python executable, you should configure which Python executables to use. 69 | You can do this by either: 70 | 71 | - Using the configuration menu under the Python toolbar menu that appears when you use a model that uses the Python extension. 72 | - Editing the `python.properties` file that appears in the Python extension installation folder as follows: 73 | 74 | ``` 75 | python3=/path/to/python3 76 | python2=/path/to/python2 77 | ``` 78 | 79 | 80 | ## Primitives 81 | 82 | 83 | ### `py:setup` 84 | 85 | ```NetLogo 86 | py:setup python-executable 87 | ``` 88 | 89 | 90 | Create the Python session that this extension will use to execute code. The session will be started with the given Python executable. This command *must* be run before running any other Python extension primitive. Running this command again will shutdown the current Python environment and start a new one. 91 | 92 | The executable may be specified as a relative path, absolute path, or just the executable name if it is on your PATH. 93 | Furthermore, this extension offers a few helper primitives for getting particular versions of Python in system 94 | independent ways. 95 | 96 | In general, unless working with a virtual environment or a specific system setup, you should do: 97 | 98 | ```NetLogo 99 | py:setup py:python ; if your code works with either Python 2 or 3 100 | py:setup py:python3 ; for Python 3 101 | py:setup py:python2 ; for Python 2 102 | ``` 103 | 104 | `py:setup` may be invoked by directly referring to different Pythons as well. For instance: 105 | 106 | ```NetLogo 107 | py:setup "python3" ; if `python3` is on your PATH 108 | py:setup "python" ; if `python` is on your PATH 109 | ``` 110 | 111 | If you use virtualenv or Conda, simply specify the path of the Python executable in the environment you wish to use: 112 | 113 | ```NetLogo 114 | py:setup "/path/to/myenv/bin/python" 115 | ``` 116 | 117 | The path may be relative or absolute. So, if you have a virtual environment in the same folder as your model, you can do: 118 | 119 | ```NetLogo 120 | py:setup "myenv/bin/python" 121 | ``` 122 | 123 | 124 | 125 | ### `py:python` 126 | 127 | ```NetLogo 128 | py:python 129 | ``` 130 | 131 | 132 | Reports either the path to the latest version of Python configured in the `python.properties` file or, if that is blank, looks for a Python executable on your system's PATH. 133 | For Windows, there is an installation option for including Python on your PATH. 134 | For MacOS and Linux, it will likely already be on your PATH. 135 | The output of this reporter is meant to be used with `py:setup`, but you may also use it to see which Python installation this extension will use by default. 136 | 137 | For example, on MacOS with Homebrew installed Python 3: 138 | ```NetLogo 139 | observer> show py:python 140 | observer: "/usr/local/bin/python3" 141 | ``` 142 | 143 | 144 | 145 | ### `py:python2` 146 | 147 | ```NetLogo 148 | py:python2 149 | ``` 150 | 151 | 152 | Reports either the path to Python 2 configured in the `python.properties` file or, if that is blank, looks for a Python 2 executable on your system's PATH. 153 | For Windows, there is an installation option for including Python on your PATH. 154 | For MacOS and Linux, it will likely already be on your PATH. 155 | The output of this reporter is meant to be used with `py:setup`, but you may also use it to see which Python 2 installation this extension will use by default. 156 | 157 | For example, on MacOS with Homebrew installed Python 2: 158 | ```NetLogo 159 | observer> show py:python2 160 | observer: "/usr/local/bin/python2" 161 | ``` 162 | 163 | 164 | 165 | ### `py:python3` 166 | 167 | ```NetLogo 168 | py:python3 169 | ``` 170 | 171 | 172 | Reports either the path to Python 3 configured in the `python.properties` file or, if that is blank, looks for a Python 3 executable on your system's PATH. 173 | For Windows, there is an installation option for including Python on your PATH. 174 | For MacOS and Linux, it will likely already be on your PATH. 175 | The output of this reporter is meant to be used with `py:setup`, but you may also use it to see which Python 3 installation this extension will use by default. 176 | 177 | For example, on MacOS with Homebrew installed Python 3: 178 | ```NetLogo 179 | observer> show py:python3 180 | observer: "/usr/local/bin/python3" 181 | ``` 182 | 183 | 184 | 185 | ### `py:run` 186 | 187 | ```NetLogo 188 | py:run python-statement 189 | ``` 190 | 191 | 192 | Runs the given Python statements in the current Python session. To make multi-line Python code easier to run, this command will take multiple strings, each of which will be interpreted as a separate line of Python code. For instance: 193 | 194 | ```NetLogo 195 | (py:run 196 | "import matplotlib" 197 | "matplotlib.use('TkAgg')" 198 | "import numpy as np" 199 | "import matplotlib.pyplot as plt" 200 | "for i in range(10):" 201 | " plt.plot([ x ** i for x in np.arange(-1, 1, 0.1) ])" 202 | "plt.show()" 203 | ) 204 | ``` 205 | 206 | `py:run` will wait for the statements to finish running before continuing. Thus, if you have long running Python code, NetLogo will pause while it runs. 207 | 208 | 209 | 210 | ### `py:runresult` 211 | 212 | ```NetLogo 213 | py:runresult python-expression 214 | ``` 215 | 216 | 217 | Evaluates the given Python expression and reports the result. 218 | `py:runresult` attempts to convert from Python data types to NetLogo data types. 219 | Numbers, strings, and booleans convert as you would expect. 220 | Any list-like object in Python (that is, anything with a length that you can iterate through) will be converted to a NetLogo list. 221 | For instance, Python lists and NumPy arrays will convert to NetLogo lists. 222 | Python dicts (and dict-like objects) will convert to a NetLogo list of key-value pairs (where each pair is represented as a list). 223 | `None` will be converted to `nobody`. 224 | Other objects will simply be converted to a string representation. 225 | 226 | Note that due a [current issue](https://github.com/qiemem/PythonExtension/issues/6), dict keys will always be reported as strings. 227 | If you need to report non-string keys, report the `.items()` of the dict instead of the dict itself. 228 | 229 | 230 | 231 | ### `py:set` 232 | 233 | ```NetLogo 234 | py:set variable-name value 235 | ``` 236 | 237 | 238 | Sets a variable in the Python session with the given name to the given NetLogo value. NetLogo objects will be converted to Python objects as expected. 239 | 240 | ```NetLogo 241 | py:set "x" [1 2 3] 242 | show py:runresult "x" ;; Shows [1 2 3] 243 | ``` 244 | 245 | Agents are converted into dictionaries with elements for each agent variable. Agentsets are converted into lists of agent dictionaries. 246 | 247 | ```NetLogo 248 | breed [goats goat] 249 | goats-own [energy ] 250 | create-goats 1 [ set heading 0 set color 75 ] 251 | ask goat 0 [ set energy 42 ] 252 | py:set "goat" goat 0 253 | py:runresult "str(goat)" ;; Should output: "{'WHO': 0, 'COLOR': 75, 'HEADING': 0, 'XCOR': 0, 'YCOR': 0, 'SHAPE': 'default', 'LABEL': '', 'LABEL-COLOR': 9.9, 'BREED': 'GOATS', 'HIDDEN?': False, 'SIZE': 1, 'PEN-SIZE': 1, 'PEN-MODE': 'up', 'ENERGY': 42}" 254 | ``` 255 | 256 | Agents with variables containing references to agentsets will have those variables converted into the string representation of that agentset. 257 | 258 | 259 | 260 | -------------------------------------------------------------------------------- /src/main/PythonExtension.scala: -------------------------------------------------------------------------------- 1 | package org.nlogo.extensions.py 2 | 3 | import com.fasterxml.jackson.core.json.JsonReadFeature 4 | import com.fasterxml.jackson.databind.json.JsonMapper 5 | import com.fasterxml.jackson.databind.ObjectMapper 6 | 7 | import java.awt.GraphicsEnvironment 8 | import java.io.{ BufferedReader, Closeable, File, IOException, InputStreamReader } 9 | import java.lang.ProcessBuilder.Redirect 10 | import java.nio.file.Paths 11 | 12 | import org.json4s.jackson.{ JsonMethods, Json4sScalaModule } 13 | 14 | import org.nlogo.languagelibrary.Subprocess.path 15 | import org.nlogo.languagelibrary.Subprocess 16 | import org.nlogo.languagelibrary.config.{ Config, ConfigProperty, FileProperty, Menu } 17 | 18 | import org.nlogo.api 19 | import org.nlogo.api._ 20 | import org.nlogo.core.{ LogoList, Syntax } 21 | import org.nlogo.workspace.AbstractWorkspace 22 | 23 | import scala.collection.immutable.ArraySeq 24 | 25 | object PythonExtension { 26 | val codeName = "py" 27 | val longName = "Python" 28 | val extLangBin = if (System.getProperty("os.name").toLowerCase.startsWith("win")) { "python" } else { "python3" } 29 | 30 | private var _pythonProcess: Option[Subprocess] = None 31 | 32 | var menu: Option[Menu] = None 33 | val config: Config = Config.createForPropertyFile(classOf[PythonExtension], PythonExtension.codeName) 34 | 35 | var isHeadless = true 36 | 37 | def pythonProcess: Subprocess = { 38 | _pythonProcess.getOrElse(throw new ExtensionException( 39 | "Python process has not been started. Please run PY:SETUP before any other python extension primitive.")) 40 | } 41 | 42 | def pythonProcess_=(proc: Subprocess): Unit = { 43 | _pythonProcess.foreach(_.close()) 44 | _pythonProcess = Some(proc) 45 | } 46 | 47 | def killPython(): Unit = { 48 | _pythonProcess.foreach(_.close()) 49 | _pythonProcess = None 50 | } 51 | 52 | } 53 | 54 | class PythonExtension extends api.DefaultClassManager { 55 | override def load(manager: api.PrimitiveManager): Unit = { 56 | manager.addPrimitive("setup", SetupPython) 57 | manager.addPrimitive("run", Run) 58 | manager.addPrimitive("runresult", RunResult) 59 | manager.addPrimitive("set", Set) 60 | manager.addPrimitive("python2", 61 | FindPython(() => PythonSubprocess.python2) 62 | ) 63 | manager.addPrimitive("python3", 64 | FindPython(() => PythonSubprocess.python3) 65 | ) 66 | manager.addPrimitive("python", 67 | FindPython(() => PythonSubprocess.anyPython) 68 | ) 69 | manager.addPrimitive("__path", Path) 70 | } 71 | 72 | override def runOnce(em: ExtensionManager): Unit = { 73 | super.runOnce(em) 74 | 75 | PythonExtension.isHeadless = !em.workspaceContext.workspaceGUI 76 | 77 | if (!PythonExtension.isHeadless) { 78 | val py2Message = s"It is recommended to use Python 3 if possible and enter its path above. If you must use Python 2, enter the path to its executable folder below." 79 | val py2Property = new FileProperty("python2", "python2", PythonExtension.config.get("python2").getOrElse(""), py2Message) 80 | PythonExtension.menu = Menu.create(em, PythonExtension.longName, PythonExtension.extLangBin, PythonExtension.config, Seq(py2Property)) 81 | } 82 | } 83 | 84 | override def unload(em: ExtensionManager): Unit = { 85 | super.unload(em) 86 | PythonExtension.killPython() 87 | 88 | if (!PythonExtension.isHeadless) 89 | PythonExtension.menu.foreach(_.unload()) 90 | } 91 | 92 | } 93 | 94 | object Using { 95 | def apply[A <: Closeable, B](resource: A)(fn: A => B): B = 96 | apply(resource, (x: A) => x.close())(fn) 97 | 98 | def apply[A, B](resource: A, cleanup: A => Unit)(fn: A => B): B = 99 | try fn(resource) finally if (resource != null) cleanup(resource) 100 | } 101 | 102 | object PythonSubprocess { 103 | def python2: Option[File] = { 104 | val maybePy2File = PythonExtension.config.get("python2").map( (dir) => { 105 | val bin = if (System.getProperty("os.name").toLowerCase.startsWith("win")) { "python.exe" } else { "python2" } 106 | val path = Paths.get(dir, bin) 107 | new File(path.toString) 108 | }) 109 | maybePy2File.orElse(pythons.find(_.version._1 == 2).map(_.file)) 110 | } 111 | 112 | def python3: Option[File] = { 113 | val maybePythonRuntimeFile = Config.getRuntimePath( 114 | PythonExtension.extLangBin 115 | , PythonExtension.config.runtimePath.getOrElse("") 116 | , "--version" 117 | ).map(new File(_)) 118 | maybePythonRuntimeFile.orElse(pythons.find(_.version._1 == 3).map(_.file)) 119 | } 120 | 121 | def anyPython: Option[File] = python3 orElse python2 122 | 123 | def pythons: LazyList[PythonBinary] = 124 | path.to(LazyList) 125 | .flatMap(_.listFiles((_, name) => name.toLowerCase.matches(raw"python[\d\.]*(?:\.exe)??"))) 126 | .flatMap(PythonBinary.fromFile) 127 | } 128 | 129 | object SetupPython extends api.Command { 130 | override def getSyntax: Syntax = Syntax.commandSyntax( 131 | right = List(Syntax.StringType | Syntax.RepeatableType) 132 | ) 133 | 134 | override def perform(args: Array[Argument], context: Context): Unit = { 135 | val pyExtensionDirectory = Config.getExtensionRuntimeDirectory(classOf[PythonExtension], PythonExtension.codeName) 136 | val pythonCmd = ArraySeq.unsafeWrapArray(args.map(_.getString)) 137 | val maybePyFile = new File(pyExtensionDirectory, "pyext.py") 138 | val pyFile = if (maybePyFile.exists) { maybePyFile } else { (new File("pyext.py")).getCanonicalFile } 139 | val pyScript: String = pyFile.toString 140 | try { 141 | val mapper = new JsonMethods { 142 | override def mapper: ObjectMapper = 143 | JsonMapper.builder.addModule(new Json4sScalaModule).enable(JsonReadFeature.ALLOW_NON_NUMERIC_NUMBERS).build() 144 | } 145 | PythonExtension.pythonProcess = Subprocess.start(context.workspace, pythonCmd, Seq(pyScript), 146 | PythonExtension.codeName, PythonExtension.longName, 147 | customMapper = Option(mapper)) 148 | 149 | if (!PythonExtension.isHeadless) 150 | PythonExtension.menu.foreach(_.setup(PythonExtension.pythonProcess.evalStringified)) 151 | } catch { 152 | case e: Exception => 153 | // Different errors can manifest in different operating systems. Thus, rather than dispatching in the specific 154 | // exception position, we catch all problems with Python bootup, look for common problems, and then offer advice 155 | // accordingly. 156 | val prefix = "Python failed to start." 157 | val wrongPathTip = "Check to make sure the correct path was entered in the Python configuration" + 158 | " menu or supply the correct path as an argument to PY:SETUP." 159 | val details = s"Details:\n\n${e.getLocalizedMessage}" 160 | val suffix = s"$wrongPathTip\n\n$details" 161 | 162 | val pythonFile = new File(pythonCmd.head) 163 | pythonFile.length() 164 | if (!pythonFile.exists) { 165 | throw new ExtensionException( 166 | s"$prefix Expected path to Python executable but '$pythonFile' does not exist. $suffix", e 167 | ) 168 | } else if (pythonFile.isDirectory) { 169 | throw new ExtensionException( 170 | s"$prefix Expected path to Python executable but '$pythonFile' is a directory. $suffix", e 171 | ) 172 | } else if (!pythonFile.canExecute) { 173 | throw new ExtensionException(s"$prefix NetLogo does not have permission to run '$pythonFile'. $suffix", e) 174 | } else { 175 | throw new ExtensionException(s"$prefix Encountered an error while running '$pythonFile'. $suffix", e) 176 | } 177 | } 178 | } 179 | } 180 | 181 | object Run extends api.Command { 182 | override def getSyntax: Syntax = Syntax.commandSyntax( 183 | right = List(Syntax.StringType | Syntax.RepeatableType) 184 | ) 185 | 186 | override def perform(args: Array[Argument], context: Context): Unit = 187 | PythonExtension.pythonProcess.exec(args.map(_.getString).mkString("\n")) 188 | } 189 | 190 | object RunResult extends api.Reporter { 191 | override def getSyntax: Syntax = Syntax.reporterSyntax( 192 | right = List(Syntax.StringType), 193 | ret = Syntax.WildcardType 194 | ) 195 | 196 | override def report(args: Array[Argument], context: Context): AnyRef = 197 | PythonExtension.pythonProcess.eval(args.map(_.getString).mkString("\n")) 198 | } 199 | 200 | object Set extends api.Command { 201 | override def getSyntax: Syntax = Syntax.commandSyntax( 202 | right = List(Syntax.StringType, Subprocess.convertibleTypesSyntax)) 203 | 204 | override def perform(args: Array[Argument], context: Context): Unit = 205 | PythonExtension.pythonProcess.assign(args(0).getString, args(1).get) 206 | } 207 | 208 | case class FindPython(pyFinder: () => Option[File]) extends api.Reporter { 209 | 210 | override def report(args: Array[Argument], context: Context): String = 211 | pyFinder().map(_.toString).getOrElse( 212 | throw new ExtensionException("Couldn't find an appropriate version of Python. Please set the path to your Python executable in the Python > Configure menu.\n") 213 | ) 214 | 215 | override def getSyntax: Syntax = Syntax.reporterSyntax(ret = Syntax.StringType) 216 | } 217 | 218 | object Path extends api.Reporter { 219 | override def report(args: Array[Argument], context: Context): LogoList = 220 | LogoList.fromVector(Subprocess.path.flatMap(_.listFiles.filter(_.getName.toLowerCase.matches(raw"python[\d\.]*(?:\.exe)??"))).map(_.getAbsolutePath).toVector) 221 | 222 | override def getSyntax: Syntax = Syntax.reporterSyntax(ret = Syntax.ListType) 223 | } 224 | 225 | case class PythonBinary(file: File, version: (Int, Int, Int)) 226 | 227 | object PythonBinary { 228 | def fromPath(s: String): Option[PythonBinary] = fromFile(new File(s)) 229 | 230 | def fromFile(f: File): Option[PythonBinary] = { 231 | try { 232 | val proc = new ProcessBuilder(f.getAbsolutePath, "-V") 233 | .redirectError(Redirect.PIPE) 234 | .redirectInput(Redirect.PIPE) 235 | .start() 236 | Option(new BufferedReader(new InputStreamReader(proc.getInputStream)).readLine()).orElse( 237 | Option(new BufferedReader(new InputStreamReader(proc.getErrorStream)).readLine()) 238 | ).flatMap { verString => 239 | parsePythonVersion(verString).map({ case (version) => PythonBinary(f, version) }) 240 | } 241 | } catch { 242 | case _: IOException => None 243 | case _: IllegalStateException => None 244 | case _: SecurityException => None 245 | } 246 | } 247 | 248 | def parsePythonVersion(v: String): Option[(Int, Int, Int)] = { 249 | val m = """Python (\d+)\.(\d+)\.(\d+)""".r.findAllIn(v) 250 | if (m.groupCount == 3) { 251 | Some((m.group(1).toInt, m.group(2).toInt, m.group(3).toInt)) 252 | } else { 253 | None 254 | } 255 | } 256 | } 257 | -------------------------------------------------------------------------------- /demos/digit-prediction/digits_prediction.nlogox: -------------------------------------------------------------------------------- 1 | 2 | 3 | 43 | map [ x -> 44 | [ pcolor / 10 ] of patch x y 45 | ] coords-x 46 | ] coords-y 47 | py:run "array = np.array(images).reshape(1, 28, 28, 1)" 48 | py:run "y_pred = model.predict(array)[0]" 49 | set digit py:runresult "np.argmax(y_pred)" 50 | 51 | ;; return probabilities per digit (softmax layer) 52 | set probabilities py:runresult "y_pred" 53 | end 54 | 55 | to clear 56 | clear-patches 57 | predict 58 | end]]> 59 | 60 | 61 | 62 | 63 | 64 | 65 | digit 66 | 100 * item 0 probabilities 67 | 100 * item 1 probabilities 68 | 100 * item 2 probabilities 69 | 100 * item 3 probabilities 70 | 100 * item 4 probabilities 71 | 100 * item 5 probabilities 72 | 100 * item 6 probabilities 73 | 100 * item 7 probabilities 74 | 100 * item 8 probabilities 75 | 100 * item 9 probabilities 76 | 77 | ## WHAT IS IT? 78 | 79 | The model predict digits written on the patch. The model was pretrained on MNIST dataset using convolutional neural network (CNN) based on [Kaggle implementation](https://www.kaggle.com/yassineghouzam/introduction-to-cnn-keras-0-997-top-6). It can recognised digits from 0 to 9 (multilabel classification). 80 | 81 | ## HOW IT WORKS 82 | 83 | The input to neural network is array created from the colors of the patch. Next the neural network compute its weights and return the number from 0 to 9. 84 | 85 | ## HOW TO USE IT 86 | 87 | 1. Click _setup-py_ to import all necessary libraries and load model (it can take a while) 88 | 2. Click _draw_ to start drawing on the board 89 | 3. While the mouse is not pressed, on the monitor widget it should be displayed predicted digit 90 | 4. On multiple monitors are showing probabilities from softmax neural network layer 91 | 5. Click _clear_ button to set all patches to black (reset) 92 | 6. Turn on _erase?_ to set pen to black color 93 | 94 | ## THINGS TO NOTICE 95 | 96 | The model is not 100% accurate and sometimes show ridiculous result. 97 | 98 | Notice the need for the `Python` packages: 99 | - **keras** (also **tensorflow** as a backend) 100 | - **numpy** 101 | 102 | ## EXTENDING THE MODEL 103 | 104 | Create new model in `Python` e.g for letters recognition. 105 | 106 | ## NETLOGO FEATURES 107 | 108 | Usage of `map` in `map` to get pcolor of the patches (_predict_ function). 109 | 110 | ## COPYRIGHT 111 | 112 | Copyright 2018 Robert Jankowski. 113 | 114 | 115 | 116 | 117 | 118 | 119 | 120 | 121 | 122 | 123 | 124 | 125 | 126 | 127 | 128 | 129 | 130 | 131 | 132 | 133 | 134 | 135 | 136 | 137 | 138 | 139 | 140 | 141 | 142 | 143 | 144 | 145 | 146 | 147 | 148 | 149 | 150 | 151 | 152 | 153 | 154 | 155 | 156 | 157 | 158 | 159 | 160 | 161 | 162 | 163 | 164 | 165 | 166 | 167 | 168 | 169 | 170 | 171 | 172 | 173 | 174 | 175 | 176 | 177 | 178 | 179 | 180 | 181 | 182 | 183 | 184 | 185 | 186 | 187 | 188 | 189 | 190 | 191 | 192 | 193 | 194 | 195 | 196 | 197 | 198 | 199 | 200 | 201 | 202 | 203 | 204 | 205 | 206 | 207 | 208 | 209 | 210 | 211 | 212 | 213 | 214 | 215 | 216 | 217 | 218 | 219 | 220 | 221 | 222 | 223 | 224 | 225 | 226 | 227 | 228 | 229 | 230 | 231 | 232 | 233 | 234 | 235 | 236 | 237 | 238 | 239 | 240 | 241 | 242 | 243 | 244 | 245 | 246 | 247 | 248 | 249 | 250 | 251 | 252 | 253 | 254 | 255 | 256 | 257 | 258 | 259 | 260 | 261 | 262 | 263 | 264 | 265 | 266 | 267 | 268 | 269 | 270 | 271 | 272 | 273 | 274 | 275 | 276 | 277 | 278 | 279 | 280 | 281 | 282 | 283 | 284 | 285 | 286 | 287 | 288 | 289 | 290 | 291 | 292 | 293 | 294 | 295 | 296 | 297 | 298 | 299 | 300 | 301 | 302 | 303 | 304 | 305 | 306 | 307 | 308 | 309 | 310 | 311 | 312 | 313 | 314 | 315 | 316 | 317 | 318 | 319 | 320 | 321 | 322 | 323 | 324 | 325 | 326 | 327 | 328 | 329 | 330 | 331 | 332 | 333 | 334 | 335 | 336 | 337 | 338 | 339 | 340 | 341 | 342 | 343 | 344 | 345 | 346 | 347 | 348 | 349 | 350 | 351 | 352 | 353 | 354 | 355 | 356 | 357 | 358 | 359 | 360 | 361 | 362 | 363 | 364 | 365 | 366 | 367 | 368 | 369 | 370 | 371 | 372 | 373 | 374 | 375 | 376 | 377 | 378 | 379 | 380 | 381 | 382 | 383 | 384 | 385 | 386 | 387 | 388 | 389 | 390 | 391 | 392 | 393 | 394 | 395 | 396 | 397 | 398 | 399 | 400 | 401 | 402 | 403 | 404 | 405 | 406 | 407 | 408 | 409 | 410 | 411 | 412 | 413 | 414 | 415 | 416 | 417 | 418 | 419 | 420 | 421 | 422 | 423 | 424 | 425 | 426 | 427 | 428 | 429 | 430 | 431 | 432 | 433 | 434 | 435 | 436 | 437 | 438 | 439 | 440 | 441 | 442 | 443 | 444 | 445 | 446 | 447 | 448 | 449 | 450 | 451 | 452 | 453 | 454 | 455 | 456 | 457 | 458 | 459 | 460 | 461 | 462 | 463 | 464 | 465 | 466 | 467 | 468 | 469 | 470 | 471 | 472 | 473 | 474 | 475 | 476 | 477 | 478 | 479 | 480 | 481 | 482 | 483 | 484 | 485 | 486 | 487 | 488 | 489 | 490 | 491 | 492 | 493 | 494 | 495 | 496 | 497 | 498 | 499 | 500 | 501 | 502 | 503 | 504 | 505 | 506 | 507 | 508 | 509 | 510 | 511 | 512 | 513 | 514 | 515 | 516 | 517 | 518 | 519 | 520 | 521 | 522 | 523 | 524 | 525 | 526 | 527 | 528 | 529 | 530 | 531 | 532 | 533 | 534 | 535 | 536 | 537 | 538 | 539 | 540 | 541 | 542 | 543 | 544 | 545 | 546 | 547 | 548 | 549 | 550 | 551 | 552 | 553 | 554 | 555 | 556 | 557 | 558 | 559 | 560 | 561 | 562 | 563 | 564 | 565 | 566 | 567 | 568 | 569 | 570 | 571 | 572 | 573 | 574 | 575 | 576 | 577 | 578 | 579 | 580 | 581 | 582 | 583 | 584 | 585 | 586 | 587 | 588 | 589 | 590 | 591 | 592 | 593 | 594 | 595 | 596 | 597 | 598 | 599 | 600 | 601 | 602 | 603 | 604 | 605 | 606 | 607 | 608 | 609 | 610 | 611 | 612 | 613 | 614 | 615 | 616 | 617 | 618 | 619 | 620 | 621 | 622 | 623 | 624 | 625 | 626 | 627 | 628 | 629 | 630 | 631 | 632 | 633 | 634 | 635 | 636 | 637 | 638 | 639 | 640 | 641 | 642 | 643 | 644 | 645 | 646 | 647 | 648 | 649 | 650 | 651 | 652 | 653 | 654 | 655 | 656 | 657 | 658 | 659 | 660 | 661 | 662 | 663 | 664 | 665 | 666 | 667 | 668 | 669 | 670 | 671 | 672 | 673 | 674 | 675 | 676 | 677 | 678 | 679 | 680 | 681 | 682 | 683 | 684 | 685 | 686 | 687 | 688 | 689 | 690 | 691 | 692 | 693 | 694 | 695 | 696 | 697 | 698 | 699 | 700 | 701 | 702 | 703 | 704 | 705 | 706 | 707 | 708 | 709 | 710 | 711 | 712 | 713 | 714 | 715 | 716 | 717 | 718 | 719 | 720 | 721 | 722 | 723 | 724 | 725 | 726 | 727 | 728 | 729 | 730 | 731 | 732 | 733 | 734 | 735 | 736 | 737 | 738 | 739 | 740 | 741 | 742 | 743 | 744 | 745 | 746 | 747 | 748 | 749 | 750 | 751 | 752 | 753 | 754 | 755 | 756 | 757 | 758 | 759 | 760 | 761 | 762 | 763 | 764 | 765 | 766 | 767 | 768 | 769 | 770 | 771 | 772 | 773 | 774 | 775 | 776 | 777 | 778 | 779 | 780 | 781 | 782 | 783 | 784 | 785 | 786 | 787 | 788 | 789 | 790 | 791 | 792 | 793 | 794 | 795 | 796 | 797 | 798 | 799 | 800 | 801 | 802 | 803 | 804 | 805 | 806 | 807 | 808 | 809 | 810 | 811 | 812 | 813 | 814 | 815 | 816 | 817 | 818 | 819 | 820 | 821 | 822 | 823 | 824 | 825 | 826 | 827 | 828 | 829 | 830 | 831 | 832 | 833 | 834 | 835 | 836 | 837 | 838 | 839 | 840 | 841 | 842 | 843 | 844 | 845 | 846 | 847 | 848 | 849 | 850 | 851 | 852 | 853 | 854 | 855 | 856 | 857 | 858 | 859 | 860 | 861 | 862 | 863 | 864 | 865 | 866 | 867 | 868 | 869 | 870 | 871 | 872 | 873 | 874 | 875 | 876 | 877 | 878 | 879 | 880 | 881 | 882 | 883 | 884 | 885 | 886 | 887 | 888 | 889 | 890 | 891 | 892 | 893 | 894 | 895 | 896 | 897 | 898 | 899 | 900 | 901 | 902 | 903 | 904 | 905 | 906 | 907 | 908 | 909 | 910 | 911 | 912 | 913 | 914 | 915 | 916 | 917 | 918 | 919 | 920 | 921 | 922 | 923 | 924 | 925 | 926 | 927 | 928 | 929 | 930 | 931 | 932 | 933 | 934 | 935 | 936 | 937 | 938 | 939 | 940 | 941 | 942 | 943 | 944 | 945 | 946 | setup repeat 75 [ go ] 947 | 948 | -------------------------------------------------------------------------------- /demos/Flocking Clusters.nlogox: -------------------------------------------------------------------------------- 1 | 2 | 3 | [ (list xcor ycor dx dy) ] of t ] sort turtles 35 | py:set "eps" vision 36 | 37 | let clusters py:runresult "cl.dbscan(np.array(coords), eps)" 38 | (foreach (sort turtles) (item 1 clusters) [ [t c] -> 39 | ask t [ 40 | ifelse c = -1 [ 41 | set color grey 42 | ][ 43 | set color item ((c mod 13) + 1) base-colors 44 | ] 45 | ] 46 | ]) 47 | tick 48 | end 49 | 50 | to flock ;; turtle procedure 51 | find-flockmates 52 | if any? flockmates 53 | [ find-nearest-neighbor 54 | ifelse distance nearest-neighbor < minimum-separation 55 | [ separate ] 56 | [ align 57 | cohere ] ] 58 | end 59 | 60 | to find-flockmates ;; turtle procedure 61 | set flockmates other turtles in-radius vision 62 | end 63 | 64 | to find-nearest-neighbor ;; turtle procedure 65 | set nearest-neighbor min-one-of flockmates [distance myself] 66 | end 67 | 68 | ;;; SEPARATE 69 | 70 | to separate ;; turtle procedure 71 | turn-away ([heading] of nearest-neighbor) max-separate-turn 72 | end 73 | 74 | ;;; ALIGN 75 | 76 | to align ;; turtle procedure 77 | turn-towards average-flockmate-heading max-align-turn 78 | end 79 | 80 | to-report average-flockmate-heading ;; turtle procedure 81 | ;; We can't just average the heading variables here. 82 | ;; For example, the average of 1 and 359 should be 0, 83 | ;; not 180. So we have to use trigonometry. 84 | let x-component sum [dx] of flockmates 85 | let y-component sum [dy] of flockmates 86 | ifelse x-component = 0 and y-component = 0 87 | [ report heading ] 88 | [ report atan x-component y-component ] 89 | end 90 | 91 | ;;; COHERE 92 | 93 | to cohere ;; turtle procedure 94 | turn-towards average-heading-towards-flockmates max-cohere-turn 95 | end 96 | 97 | to-report average-heading-towards-flockmates ;; turtle procedure 98 | ;; "towards myself" gives us the heading from the other turtle 99 | ;; to me, but we want the heading from me to the other turtle, 100 | ;; so we add 180 101 | let x-component mean [sin (towards myself + 180)] of flockmates 102 | let y-component mean [cos (towards myself + 180)] of flockmates 103 | ifelse x-component = 0 and y-component = 0 104 | [ report heading ] 105 | [ report atan x-component y-component ] 106 | end 107 | 108 | ;;; HELPER PROCEDURES 109 | 110 | to turn-towards [new-heading max-turn] ;; turtle procedure 111 | turn-at-most (subtract-headings new-heading heading) max-turn 112 | end 113 | 114 | to turn-away [new-heading max-turn] ;; turtle procedure 115 | turn-at-most (subtract-headings heading new-heading) max-turn 116 | end 117 | 118 | ;; turn right by "turn" degrees (or left if "turn" is negative), 119 | ;; but never turn more than "max-turn" degrees 120 | to turn-at-most [turn max-turn] ;; turtle procedure 121 | ifelse abs turn > max-turn 122 | [ ifelse turn > 0 123 | [ rt max-turn ] 124 | [ lt max-turn ] ] 125 | [ rt turn ] 126 | end 127 | 128 | 129 | ; Copyright 1998 Uri Wilensky. 130 | ; See Info tab for full copyright and license.]]> 131 | 132 | 133 | 134 | 135 | 136 | 137 | 138 | 139 | 140 | 141 | 142 | ]]> 245 | 246 | 247 | 248 | 249 | 250 | 251 | 252 | 253 | 254 | 255 | 256 | 257 | 258 | 259 | 260 | 261 | 262 | 263 | 264 | 265 | 266 | 267 | 268 | 269 | 270 | 271 | 272 | 273 | 274 | 275 | 276 | 277 | 278 | 279 | 280 | 281 | 282 | 283 | 284 | 285 | 286 | 287 | 288 | 289 | 290 | 291 | 292 | 293 | 294 | 295 | 296 | 297 | 298 | 299 | 300 | 301 | 302 | 303 | 304 | 305 | 306 | 307 | 308 | 309 | 310 | 311 | 312 | 313 | 314 | 315 | 316 | 317 | 318 | 319 | 320 | 321 | 322 | 323 | 324 | 325 | 326 | 327 | 328 | 329 | 330 | 331 | 332 | 333 | 334 | 335 | 336 | 337 | 338 | 339 | 340 | 341 | 342 | 343 | 344 | 345 | 346 | 347 | 348 | 349 | 350 | 351 | 352 | 353 | 354 | 355 | 356 | 357 | 358 | 359 | 360 | 361 | 362 | 363 | 364 | 365 | 366 | 367 | 368 | 369 | 370 | 371 | 372 | 373 | 374 | 375 | 376 | 377 | 378 | 379 | 380 | 381 | 382 | 383 | 384 | 385 | 386 | 387 | 388 | 389 | 390 | 391 | 392 | 393 | 394 | 395 | 396 | 397 | 398 | 399 | 400 | 401 | 402 | 403 | 404 | 405 | 406 | 407 | 408 | 409 | 410 | 411 | 412 | 413 | 414 | 415 | 416 | 417 | 418 | 419 | 420 | 421 | 422 | 423 | 424 | 425 | 426 | 427 | 428 | 429 | 430 | 431 | 432 | 433 | 434 | 435 | 436 | 437 | 438 | 439 | 440 | 441 | 442 | 443 | 444 | 445 | 446 | 447 | 448 | 449 | 450 | 451 | 452 | 453 | 454 | 455 | 456 | 457 | 458 | 459 | 460 | 461 | 462 | 463 | 464 | 465 | 466 | 467 | 468 | 469 | 470 | 471 | 472 | 473 | 474 | 475 | 476 | 477 | 478 | 479 | 480 | 481 | 482 | 483 | 484 | 485 | 486 | 487 | 488 | 489 | 490 | 491 | 492 | 493 | 494 | 495 | 496 | 497 | 498 | 499 | 500 | 501 | 502 | 503 | 504 | 505 | 506 | 507 | 508 | 509 | 510 | 511 | 512 | 513 | 514 | 515 | 516 | 517 | 518 | 519 | 520 | 521 | 522 | 523 | 524 | 525 | 526 | 527 | 528 | 529 | 530 | 531 | 532 | 533 | 534 | 535 | 536 | 537 | 538 | 539 | 540 | 541 | 542 | 543 | 544 | 545 | 546 | 547 | 548 | 549 | 550 | 551 | 552 | 553 | 554 | 555 | 556 | 557 | 558 | 559 | 560 | 561 | 562 | 563 | 564 | 565 | 566 | 567 | 568 | 569 | 570 | 571 | 572 | 573 | 574 | 575 | 576 | 577 | 578 | 579 | 580 | 581 | 582 | 583 | 584 | 585 | 586 | 587 | 588 | 589 | 590 | 591 | 592 | 593 | 594 | 595 | 596 | 597 | 598 | 599 | 600 | 601 | 602 | 603 | 604 | 605 | 606 | 607 | 608 | 609 | 610 | 611 | 612 | 613 | 614 | 615 | 616 | 617 | 618 | 619 | 620 | 621 | 622 | 623 | 624 | 625 | 626 | 627 | 628 | 629 | 630 | 631 | 632 | 633 | 634 | 635 | 636 | 637 | 638 | 639 | 640 | 641 | 642 | 643 | 644 | 645 | 646 | 647 | 648 | 649 | 650 | 651 | 652 | 653 | 654 | 655 | 656 | 657 | 658 | 659 | 660 | 661 | 662 | 663 | 664 | 665 | 666 | 667 | 668 | 669 | 670 | 671 | 672 | 673 | 674 | 675 | 676 | 677 | 678 | 679 | 680 | 681 | 682 | 683 | 684 | 685 | 686 | 687 | 688 | 689 | 690 | 691 | 692 | 693 | 694 | 695 | 696 | 697 | 698 | 699 | 700 | 701 | 702 | 703 | 704 | 705 | 706 | 707 | 708 | 709 | 710 | 711 | 712 | 713 | 714 | 715 | 716 | 717 | 718 | 719 | 720 | 721 | 722 | 723 | 724 | 725 | 726 | 727 | 728 | 729 | 730 | 731 | 732 | 733 | 734 | 735 | 736 | 737 | 738 | 739 | 740 | 741 | 742 | 743 | 744 | 745 | 746 | 747 | 748 | 749 | 750 | 751 | 752 | 753 | 754 | 755 | 756 | 757 | 758 | 759 | 760 | 761 | 762 | 763 | 764 | 765 | 766 | 767 | 768 | 769 | 770 | 771 | 772 | 773 | 774 | 775 | 776 | 777 | 778 | 779 | 780 | 781 | 782 | 783 | 784 | 785 | 786 | 787 | 788 | 789 | 790 | 791 | 792 | 793 | 794 | 795 | 796 | 797 | 798 | 799 | 800 | 801 | 802 | 803 | 804 | 805 | 806 | 807 | 808 | 809 | 810 | 811 | 812 | 813 | 814 | 815 | 816 | 817 | 818 | 819 | 820 | 821 | 822 | 823 | 824 | 825 | 826 | 827 | 828 | 829 | 830 | 831 | 832 | 833 | 834 | 835 | 836 | 837 | 838 | 839 | 840 | 841 | 842 | 843 | 844 | 845 | 846 | 847 | 848 | 849 | 850 | 851 | 852 | 853 | 854 | 855 | 856 | 857 | 858 | 859 | 860 | 861 | 862 | 863 | 864 | 865 | 866 | 867 | 868 | 869 | 870 | 871 | 872 | 873 | 874 | 875 | 876 | 877 | 878 | 879 | 880 | 881 | 882 | 883 | 884 | 885 | 886 | 887 | 888 | 889 | 890 | 891 | 892 | 893 | 894 | 895 | 896 | 897 | 898 | 899 | 900 | 901 | 902 | 903 | 904 | 905 | 906 | 907 | 908 | 909 | 910 | 911 | 912 | 913 | 914 | 915 | 916 | 917 | 918 | 919 | 920 | 921 | 922 | 923 | 924 | 925 | 926 | 927 | 928 | 929 | 930 | 931 | 932 | 933 | 934 | 935 | 936 | 937 | 938 | 939 | 940 | 941 | 942 | 943 | 944 | 945 | 946 | 947 | 948 | 949 | 950 | 951 | 952 | 953 | 954 | set population 200 955 | setup 956 | repeat 200 [ go ] 957 | 958 | -------------------------------------------------------------------------------- /demos/Traffic Basic - Reinforcement.nlogox: -------------------------------------------------------------------------------- 1 | 2 | 3 | speed] 37 | [-> distance next-car] 38 | [-> [speed] of next-car] 39 | ) 40 | 41 | if fp-exp? [ 42 | set inputs lput [-> exp-rate ] inputs 43 | ] 44 | if fp-ticks? [ 45 | set inputs lput [-> ticks] inputs 46 | ] 47 | 48 | py:set "state_dims" length inputs 49 | py:set "hl_size" 36 50 | py:set "num_actions" 3 51 | py:set "memory_size" memory-size 52 | py:set "batch_size" batch-size 53 | py:set "lr" learning-rate 54 | 55 | (py:run 56 | "model = Sequential()" 57 | "model.add(Dense(hl_size, input_shape=(state_dims,), activation='relu'))" 58 | "model.add(Dense(hl_size, activation='relu'))" 59 | "model.add(Dense(hl_size, activation='relu'))" 60 | "model.add(Dense(num_actions))" 61 | "optimizer = optimizers.adam(lr=lr)" 62 | "model.compile(optimizer, 'mse')" 63 | "model.summary()" 64 | "memory = []") 65 | 66 | ask patches [ setup-road ] 67 | set speed-limit 1 68 | set speed-min 0 69 | setup-cars 70 | reset-ticks 71 | end 72 | 73 | to setup-road ;; patch procedure 74 | if pycor < 2 and pycor > -2 [ set pcolor white ] 75 | end 76 | 77 | to setup-cars 78 | if number-of-cars > world-width [ 79 | user-message (word 80 | "There are too many cars for the amount of road. " 81 | "Please decrease the NUMBER-OF-CARS slider to below " 82 | (world-width + 1) " and press the SETUP button again. " 83 | "The setup has stopped.") 84 | stop 85 | ] 86 | set-default-shape turtles "car" 87 | create-turtles number-of-cars [ 88 | set color blue 89 | set xcor random-xcor 90 | set heading 90 91 | ;; set initial speed to be in range 0.1 to 1.0 92 | set speed 0.1 + random-float 0.9 93 | separate-cars 94 | ] 95 | set sample-car one-of turtles 96 | ask sample-car [ set color red ] 97 | end 98 | 99 | ; this procedure is needed so when we click "Setup" we 100 | ; don't end up with any two cars on the same patch 101 | to separate-cars ;; turtle procedure 102 | if any? other turtles-here [ 103 | fd 1 104 | separate-cars 105 | ] 106 | end 107 | 108 | to go 109 | py:set "discount" discount 110 | ;; if there is a car right ahead of you, match its speed then slow down 111 | select-actions 112 | ask turtles [ 113 | if action = 0 [ decelerate set color red ] 114 | if action = 1 [ set color yellow ] 115 | if action = 2 [ accelerate set color green] 116 | ;; don't slow down below speed minimum or speed up beyond speed limit 117 | if distance next-car < 1 + speed [ slow-down-car next-car ] 118 | if speed < speed-min [ set speed speed-min ] 119 | if speed > speed-limit [ set speed speed-limit ] 120 | fd speed 121 | set reward (log (speed + 1e-8) 2) 122 | ;set reward speed 123 | ] 124 | if train? [ 125 | remember 126 | train 127 | ] 128 | tick 129 | end 130 | 131 | to select-actions 132 | ask turtles [ set state map runresult inputs ] 133 | let turtle-list sort turtles 134 | py:set "states" map [ t -> [ state ] of t ] turtle-list 135 | let actions py:runresult "np.argmax(model.predict(np.array(states)), axis = 1)" 136 | 137 | (foreach turtle-list actions [ [t a] -> 138 | ask t [ 139 | ifelse random-float 1 < exp-rate [ 140 | set action random 3 141 | ] [ 142 | set action a 143 | ] 144 | ] 145 | ]) 146 | end 147 | 148 | to-report exp-rate 149 | report exploration-rate / (1 + exploration-decay-rate * ticks) 150 | end 151 | 152 | to remember 153 | ask turtles [ set next-state map runresult inputs ] 154 | let data [ (list state action reward next-state) ] of turtles 155 | py:set "new_exp" data 156 | (py:run 157 | "memory.extend(new_exp)" 158 | "if len(memory) > memory_size:" 159 | " memory = memory[-memory_size:]") 160 | end 161 | 162 | to train 163 | (py:run 164 | "sample_ix = np.random.randint(len(memory), size = batch_size)" 165 | "inputs = np.array([memory[i][0] for i in sample_ix])" 166 | "actions = np.array([memory[i][1] for i in sample_ix])" 167 | "rewards = np.array([memory[i][2] for i in sample_ix])" 168 | "next_states = np.array([memory[i][3] for i in sample_ix])" 169 | "targets = model.predict(inputs)" 170 | "next_state_rewards = model.predict(next_states)" 171 | "next_state_qs = np.max(next_state_rewards, axis = 1)" 172 | "targets[np.arange(targets.shape[0]), actions] = rewards + discount * next_state_qs" 173 | "model.train_on_batch(inputs, targets)" 174 | ) 175 | 176 | end 177 | 178 | to-report next-car 179 | let here min-one-of turtles-here with [ xcor > [ xcor ] of myself ] [ distance myself ] 180 | if here != nobody [ report here ] 181 | let i 1 182 | while [ not any? turtles-on patch-ahead i ] [ 183 | set i i + 1 184 | ] 185 | report min-one-of turtles-on patch-ahead i [ distance myself ] 186 | end 187 | 188 | 189 | to slow-down-car [ car-ahead ] ;; turtle procedure 190 | ;; slow down so you are driving more slowly than the car ahead of you 191 | set speed [ speed ] of car-ahead - stop-penalty 192 | end 193 | 194 | to accelerate 195 | set speed speed + acceleration 196 | end 197 | 198 | to decelerate 199 | set speed speed - deceleration 200 | end 201 | 202 | ; Copyright 1997 Uri Wilensky. 203 | ; See Info tab for full copyright and license.]]> 204 | 205 | 206 | 207 | 208 | 209 | 210 | 211 | 212 | 213 | 214 | 215 | 216 | plot [speed] of sample-car 217 | 218 | 219 | 220 | plot min [speed] of turtles 221 | 222 | 223 | 224 | plot max [speed] of turtles 225 | 226 | 227 | 228 | 229 | 230 | 231 | set-plot-y-range 0 number-of-cars 232 | 233 | 234 | 235 | plot count turtles with [ action = 0 ] 236 | 237 | 238 | 239 | plot count turtles with [ action = 1 ] 240 | 241 | 242 | 243 | plot count turtles with [ action = 2 ] 244 | 245 | 246 | 247 | 248 | 249 | 250 | 251 | 252 | exp-rate 253 | 254 | 255 | 256 | ]]> 371 | 372 | 373 | 374 | 375 | 376 | 377 | 378 | 379 | 380 | 381 | 382 | 383 | 384 | 385 | 386 | 387 | 388 | 389 | 390 | 391 | 392 | 393 | 394 | 395 | 396 | 397 | 398 | 399 | 400 | 401 | 402 | 403 | 404 | 405 | 406 | 407 | 408 | 409 | 410 | 411 | 412 | 413 | 414 | 415 | 416 | 417 | 418 | 419 | 420 | 421 | 422 | 423 | 424 | 425 | 426 | 427 | 428 | 429 | 430 | 431 | 432 | 433 | 434 | 435 | 436 | 437 | 438 | 439 | 440 | 441 | 442 | 443 | 444 | 445 | 446 | 447 | 448 | 449 | 450 | 451 | 452 | 453 | 454 | 455 | 456 | 457 | 458 | 459 | 460 | 461 | 462 | 463 | 464 | 465 | 466 | 467 | 468 | 469 | 470 | 471 | 472 | 473 | 474 | 475 | 476 | 477 | 478 | 479 | 480 | 481 | 482 | 483 | 484 | 485 | 486 | 487 | 488 | 489 | 490 | 491 | 492 | 493 | 494 | 495 | 496 | 497 | 498 | 499 | 500 | 501 | 502 | 503 | 504 | 505 | 506 | 507 | 508 | 509 | 510 | 511 | 512 | 513 | 514 | 515 | 516 | 517 | 518 | 519 | 520 | 521 | 522 | 523 | 524 | 525 | 526 | 527 | 528 | 529 | 530 | 531 | 532 | 533 | 534 | 535 | 536 | 537 | 538 | 539 | 540 | 541 | 542 | 543 | 544 | 545 | 546 | 547 | 548 | 549 | 550 | 551 | 552 | 553 | 554 | 555 | 556 | 557 | 558 | 559 | 560 | 561 | 562 | 563 | 564 | 565 | 566 | 567 | 568 | 569 | 570 | 571 | 572 | 573 | 574 | 575 | 576 | 577 | 578 | 579 | 580 | 581 | 582 | 583 | 584 | 585 | 586 | 587 | 588 | 589 | 590 | 591 | 592 | 593 | 594 | 595 | 596 | 597 | 598 | 599 | 600 | 601 | 602 | 603 | 604 | 605 | 606 | 607 | 608 | 609 | 610 | 611 | 612 | 613 | 614 | 615 | 616 | 617 | 618 | 619 | 620 | 621 | 622 | 623 | 624 | 625 | 626 | 627 | 628 | 629 | 630 | 631 | 632 | 633 | 634 | 635 | 636 | 637 | 638 | 639 | 640 | 641 | 642 | 643 | 644 | 645 | 646 | 647 | 648 | 649 | 650 | 651 | 652 | 653 | 654 | 655 | 656 | 657 | 658 | 659 | 660 | 661 | 662 | 663 | 664 | 665 | 666 | 667 | 668 | 669 | 670 | 671 | 672 | 673 | 674 | 675 | 676 | 677 | 678 | 679 | 680 | 681 | 682 | 683 | 684 | 685 | 686 | 687 | 688 | 689 | 690 | 691 | 692 | 693 | 694 | 695 | 696 | 697 | 698 | 699 | 700 | 701 | 702 | 703 | 704 | 705 | 706 | 707 | 708 | 709 | 710 | 711 | 712 | 713 | 714 | 715 | 716 | 717 | 718 | 719 | 720 | 721 | 722 | 723 | 724 | 725 | 726 | 727 | 728 | 729 | 730 | 731 | 732 | 733 | 734 | 735 | 736 | 737 | 738 | 739 | 740 | 741 | 742 | 743 | 744 | 745 | 746 | 747 | 748 | 749 | 750 | 751 | 752 | 753 | 754 | 755 | 756 | 757 | 758 | 759 | 760 | 761 | 762 | 763 | 764 | 765 | 766 | 767 | 768 | 769 | 770 | 771 | 772 | 773 | 774 | 775 | 776 | 777 | 778 | 779 | 780 | 781 | 782 | 783 | 784 | 785 | 786 | 787 | 788 | 789 | 790 | 791 | 792 | 793 | 794 | 795 | 796 | 797 | 798 | 799 | 800 | 801 | 802 | 803 | 804 | 805 | 806 | 807 | 808 | 809 | 810 | 811 | 812 | 813 | 814 | 815 | 816 | 817 | 818 | 819 | 820 | 821 | 822 | 823 | 824 | 825 | 826 | 827 | 828 | 829 | 830 | 831 | 832 | 833 | 834 | 835 | 836 | 837 | 838 | 839 | 840 | 841 | 842 | 843 | 844 | 845 | 846 | 847 | 848 | 849 | 850 | 851 | 852 | 853 | 854 | 855 | 856 | 857 | 858 | 859 | 860 | 861 | 862 | 863 | 864 | 865 | 866 | 867 | 868 | 869 | 870 | 871 | 872 | 873 | 874 | 875 | 876 | 877 | 878 | 879 | 880 | 881 | 882 | 883 | 884 | 885 | 886 | 887 | 888 | 889 | 890 | 891 | 892 | 893 | 894 | 895 | 896 | 897 | 898 | 899 | 900 | 901 | 902 | 903 | 904 | 905 | 906 | 907 | 908 | 909 | 910 | 911 | 912 | 913 | 914 | 915 | 916 | 917 | 918 | 919 | 920 | 921 | 922 | 923 | 924 | 925 | 926 | 927 | 928 | 929 | 930 | 931 | 932 | 933 | 934 | 935 | 936 | 937 | 938 | 939 | 940 | 941 | 942 | 943 | 944 | 945 | 946 | 947 | 948 | 949 | 950 | 951 | 952 | 953 | 954 | 955 | 956 | 957 | 958 | 959 | 960 | 961 | 962 | 963 | 964 | 965 | 966 | 967 | 968 | 969 | 970 | 971 | 972 | 973 | 974 | 975 | 976 | 977 | 978 | 979 | 980 | 981 | 982 | 983 | 984 | 985 | 986 | 987 | 988 | 989 | 990 | 991 | 992 | 993 | 994 | 995 | 996 | 997 | 998 | 999 | 1000 | 1001 | 1002 | 1003 | 1004 | 1005 | 1006 | 1007 | 1008 | 1009 | 1010 | 1011 | 1012 | 1013 | 1014 | 1015 | 1016 | 1017 | 1018 | 1019 | 1020 | 1021 | 1022 | 1023 | 1024 | 1025 | 1026 | 1027 | 1028 | 1029 | 1030 | 1031 | 1032 | 1033 | 1034 | 1035 | 1036 | 1037 | 1038 | 1039 | 1040 | 1041 | 1042 | 1043 | 1044 | 1045 | 1046 | 1047 | 1048 | 1049 | 1050 | 1051 | 1052 | 1053 | 1054 | 1055 | 1056 | 1057 | 1058 | 1059 | 1060 | 1061 | 1062 | 1063 | 1064 | 1065 | 1066 | 1067 | 1068 | 1069 | 1070 | 1071 | 1072 | 1073 | 1074 | 1075 | 1076 | 1077 | 1078 | 1079 | 1080 | setup 1081 | repeat 180 [ go ] 1082 | 1083 | --------------------------------------------------------------------------------