{ "cells": [ { "cell_type": "markdown", "id": "6d4d5c7b", "metadata": {}, "source": [ "[Home](Home.ipynb)\n", "\n", "# Tractors in a Field\n", "\n", "\"The\n", "\n", "Looking down from above, we see a tractor plowing a field. The field is our ASCII art canvas in some stories. However today we have Unicode. \n", "\n", "In other stories, our Field is an Argand Plane. The tractor is on a track which puts it at every point in the field. We model both Field and Tractor as Python classes.\n", "\n", "In still other stories, we're literally simulating the agricultural activities of plowing and planting. Perhaps we're using programs to remotely control our robot devices.\n", "\n", "In practice, we have many reasons to consider this canonical pattern, of a raster beam painting a picture. We may contrast the sequential visiting of cells to parallel processes that evaluate the cells more or less simultaneously.\n", "\n", "In the Python world, we have numpy for performing \"vectorized\" operations on entire fields in one operation, no looping required. \n", "\n", "The tractor-based approach implemented here is more strictly sequential, with a controllable \"visit order\" in some types of Tractor. That means the route taken through a field, visiting all the cells in turn, may not be fixed." ] }, { "cell_type": "markdown", "id": "549020f5", "metadata": {}, "source": [ "If you eyeball the source code for [tractor_1.py](tractor_1.py), you will see that it uses a dict as its base data structure. \n", "\n", "Often times, including in other Tractor implementations, we think of a rectangular matrix as more like a list of lists. \n", "\n", "However, given dictionary keys might serve as lookup coordinates, the dict-based version may be functionally the same, and end up saving memory." ] }, { "cell_type": "code", "execution_count": 2, "id": "09afd355", "metadata": {}, "outputs": [ { "name": "stdout", "output_type": "stream", "text": [ "\n", "\n", "...........\n", "...........\n", "...........\n", "...........\n", "...........\n", "...JUST USE\n", " IT........\n", "...........\n", "...........\n", "...........\n", "...........\n" ] } ], "source": [ "! python tractor_1.py" ] }, { "cell_type": "markdown", "id": "78592b15", "metadata": {}, "source": [ "The [farmworld.py](farmworld.py) code is rather similar. One interesting aspect of the design pattern is we have access to the tractors in the field through the field. However we also have access to the field through the tractors. \n", "\n", "It's as if each tractor contains a memorized version of the field, which would be true of either a human or robot driven machine." ] }, { "cell_type": "code", "execution_count": 2, "id": "6023186e", "metadata": {}, "outputs": [], "source": [ "import farmworld" ] }, { "cell_type": "code", "execution_count": 3, "id": "f70472ed", "metadata": {}, "outputs": [ { "name": "stdout", "output_type": "stream", "text": [ "make a movie\n", "Empty field, all is peaceful\n", "\n", "********************\n", "********************\n", "********************\n", "********************\n", "********************\n", "********************\n", "********************\n", "********************\n", "********************\n", "********************\n", "********************\n", "********************\n", "********************\n", "********************\n", "********************\n", "********************\n", "********************\n", "********************\n", "********************\n", "********************\n", "\n", "Showing the tractors in a list: \n", "Tractor(pos=[10, 10], facing=E, marker=O, fuel=100)\n", "Tractor(pos=[10, 11], facing=W, marker=X, fuel=100)\n", "===\n", "A busy day begins...\n", "\n", "********************\n", "********************\n", "********************\n", "********************\n", "********************\n", "********************\n", "********************\n", "********************\n", "********************\n", "********************\n", "**********OX********\n", "********************\n", "********************\n", "********************\n", "********************\n", "********************\n", "********************\n", "********************\n", "********************\n", "********************\n", "\n", "Advance the film: Frame 0\n", "Advance the film: Frame 1\n", "Advance the film: Frame 2\n", "After 3 ticks of the clock, the tractors have moved...\n", "\n", "********************\n", "********************\n", "********************\n", "********************\n", "********************\n", "********************\n", "********************\n", "********************\n", "********************\n", "********************\n", "********X****O******\n", "********************\n", "********************\n", "********************\n", "********************\n", "********************\n", "********************\n", "********************\n", "********************\n", "********************\n", "\n" ] } ], "source": [ "farmworld._test()" ] }, { "cell_type": "code", "execution_count": 6, "id": "31104c67", "metadata": {}, "outputs": [ { "name": "stdout", "output_type": "stream", "text": [ "['In', 'Out', '_', '_5', '__', '___', '__builtin__', '__builtins__', '__doc__', '__loader__', '__name__', '__package__', '__spec__', '_dh', '_exit_code', '_i', '_i1', '_i2', '_i3', '_i4', '_i5', '_i6', '_ih', '_ii', '_iii', '_oh', 'exit', 'farmworld', 'get_ipython', 'quit', 'tractor_1']\n" ] } ], "source": [ "print(dir())" ] }, { "cell_type": "code", "execution_count": 11, "id": "a3a9f6d5", "metadata": {}, "outputs": [ { "data": { "text/plain": [ "Farm(20,20) @ 4457466256" ] }, "execution_count": 11, "metadata": {}, "output_type": "execute_result" } ], "source": [ "thefarm = farmworld.Farm(20,20)\n", "thefarm # fires __repr__" ] }, { "cell_type": "code", "execution_count": 13, "id": "a8afc609", "metadata": {}, "outputs": [ { "name": "stdout", "output_type": "stream", "text": [ "********************\n", "********************\n", "********************\n", "********************\n", "********************\n", "********************\n", "********************\n", "********************\n", "********************\n", "********************\n", "********************\n", "********************\n", "********************\n", "********************\n", "********************\n", "********************\n", "********************\n", "********************\n", "********************\n", "********************\n", "\n" ] } ], "source": [ "print(str(thefarm)) # str triggers __str__ which calls Farm.render(self)" ] }, { "cell_type": "code", "execution_count": 16, "id": "6a616a90", "metadata": {}, "outputs": [ { "name": "stdout", "output_type": "stream", "text": [ "********************\n", "********************\n", "********************\n", "********************\n", "********************\n", "********************\n", "********************\n", "********************\n", "********************\n", "********************\n", "**********$*********\n", "********************\n", "********************\n", "********************\n", "********************\n", "********************\n", "********************\n", "********************\n", "********************\n", "********************\n", "\n" ] } ], "source": [ "t1 = farmworld.Tractor(thefarm, pos=[10,10], marker=\"$\", facing=\"N\")\n", "t1.plow()\n", "print(str(thefarm))" ] }, { "cell_type": "markdown", "id": "468c78ee", "metadata": {}, "source": [ "Let's look at the Tractor type initializer, the ```__init__```:\n", "\n", "```python\n", " \n", " class Tractor:\n", "\n", " def __init__(self, farm , pos = [0,0], \n", " facing=\"N\", marker=\"*\" , fuel=100):\n", " self.thefarm = farm\n", " self.pos = pos\n", " self.facing = facing\n", " self.marker = marker\n", " self.thefarm.add(self)\n", " self.fuel = fuel\n", "```\n", "\n", "The default position is ```[0,0]```, in the middle of the farm. However no marker (character) gets planted in the farm's field, until we actually plow the ground, by activating the ```plow``` method inside of ```Tractor```. \n", "\n", "We might separate ```plant``` and ```plow``` in a different design. The ```plow``` method might be a synonym for the ```__next__``` method for example.\n", "\n", "```python\n", "\n", " class Tractor:\n", " \n", " ...\n", " \n", " def plow(self, marker=None):\n", " if marker:\n", " self.marker = marker\n", " y,x = self.pos\n", " self.thefarm.field[y][x] = self.marker\n", "\n", "```\n", "\n", "Here's where the rubber meets the road, or the tractor meets the dirt, as through the list of lists ```self.thefarm.field``` is any tractor's way to address (plow in) said field.\n", "\n", "Notice also that our Tractor is defined as a generator, meaning it implements the ```__next__``` method, such that by triggering ```__next__``` we cause the tractor to move in whatever direction it's facing, until reaching an edge, it which point it's not smart enough to do anything put sit there, as time passes.\n", "\n", "\n", "```python\n", "\n", " class Tractor:\n", " \n", " ...\n", " \n", " \n", " def __next__(self):\n", " \"\"\"\n", " Makes me an iterator\n", " \"\"\"\n", " y,x = self.pos\n", "\n", " if self.fuel > 0:\n", "\n", " if self.facing == \"N\":\n", " if y > 0:\n", " y -= 1\n", " else:\n", " raise StopIteration\n", "\n", " elif self.facing == \"S\":\n", " if y < self.thefarm.h - 1:\n", " y += 1\n", " else:\n", " raise StopIteration\n", "\n", " elif self.facing == \"W\":\n", " if x > 0:\n", " x -= 1\n", " else:\n", " raise StopIteration\n", "\n", " elif self.facing == \"E\":\n", " if x < self.thefarm.w - 1:\n", " x += 1\n", " else:\n", " raise StopIteration\n", "\n", " self.fuel -= 1\n", " self.pos = (y,x)\n", "\n", " else: # outta gas\n", " raise StopIteration\n", "\n", " return self.thefarm.field[y][x]\n", " \n", "\n", "```\n", "\n", "\n", "By hooking all the tractors (we may have many) to the farm's tiktok method, we cause them all to advance.\n", "\n", "```python\n", "\n", " class Farm:\n", " \n", " ...\n", " \n", " def ticktock(self): # controller\n", " \"\"\"tick tock o' the clock\n", " time marches on!\n", " Advance each tractor in the direction it's facing,\n", " ignoring stuck tractors already at a fence (some\n", " types of Tractor are smarter than others about fences).\n", " \"\"\"\n", " for tractor in self.tractors:\n", " try:\n", " next(tractor) # state changer\n", " except StopIteration:\n", " pass # harmless stuck tractor signal\n", "\n", " self.framenumber += 1\n", " return self.framenumber \n", "``` " ] }, { "cell_type": "code", "execution_count": 17, "id": "f6857997", "metadata": {}, "outputs": [ { "name": "stdout", "output_type": "stream", "text": [ "********************\n", "********************\n", "********************\n", "********************\n", "********************\n", "********************\n", "**********$*********\n", "********************\n", "********************\n", "********************\n", "**********$*********\n", "********************\n", "********************\n", "********************\n", "********************\n", "********************\n", "********************\n", "********************\n", "********************\n", "********************\n", "\n" ] } ], "source": [ "next(t1)\n", "next(t1)\n", "next(t1)\n", "next(t1)\n", "t1.plow()\n", "print(str(thefarm))" ] }, { "cell_type": "markdown", "id": "bdfe030f-6433-42f7-b95b-b9305ab95745", "metadata": {}, "source": [ "![](http://news.bbcimg.co.uk/media/images/49538000/jpg/_49538127_cropcircle_spl.jpg)" ] }, { "cell_type": "code", "execution_count": 5, "id": "7898bf68", "metadata": {}, "outputs": [], "source": [ "# %load tractor_2.py\n", "\"\"\"\n", "CropCircleTractor\n", "\n", "Inherits from Tractor with same __next__ based raster pattern,\n", "however in this subclass, planting a @ occurs when the underlying\n", "complex number in the corresponding plane does not diverge after\n", "10 iterations of z = z * z + c. Creates a file of ASCII art best\n", "viewed fixed width font, small font size.\n", "\n", "EXAMPLE OUTPUT: https://flic.kr/p/xyNXhN\n", "\n", "(cl) MIT License 2015 by 4dsolutions.net\n", "\"\"\"\n", "\n", "from tractor_1 import Tractor, Field\n", "\n", "class CropCircleTractor(Tractor):\n", "\n", " def config(self, x_scale, y_scale, x_offset, y_offset):\n", " self.x_scale, self.y_scale = x_scale, y_scale\n", " self.x_offset, self.y_offset = x_offset, y_offset\n", "\n", " def __next__(self):\n", " super().__next__() # updates pos\n", " c = complex((self.col + self.y_offset) * self.y_scale, \n", " (self.row + self.x_offset) * self.x_scale)\n", " z = complex(0,0)\n", " # here is where we could add more iterations and also\n", " # start to add nuance, in terms of \"shady characters\"\n", " for _ in range(15):\n", " z = z*z + c\n", " if abs(z) <= 2:\n", " self.plant(\"🎃\")\n", " elif abs(z) <= 100:\n", " self.plant(\"🐍\")\n", " elif abs(z) <= 10000:\n", " self.plant(\"👀\")\n", " return z\n", " \n", " def __iter__(self):\n", " return self\n", "\n", "if __name__ == \"__main__\":\n", " the_field = Field(100, 250)\n", " the_field.add_tractor(CropCircleTractor) # initialized as added\n", " the_tractor = the_field.Ts[0] # grab reference to instance\n", " the_tractor.marker = \" \"\n", " the_tractor.config(.025, .01, -50, -200)\n", " the_tractor.fuel_level = 100 * 250\n", " for z in the_tractor:\n", " if the_tractor.pos == (99, 249):\n", " break\n", " with open(\"mandelbrot.txt\", \"w\") as fractal:\n", " print(the_field, file = fractal)\n", "\n" ] }, { "cell_type": "code", "execution_count": 38, "id": "b6d24985", "metadata": {}, "outputs": [], "source": [ "! python tractor_2.py" ] }, { "cell_type": "markdown", "id": "2768449b", "metadata": {}, "source": [ "\"mandelbrot_emoji\"" ] }, { "cell_type": "code", "execution_count": 37, "id": "b60c7d80", "metadata": {}, "outputs": [], "source": [ "# %load mandelbrot.txt" ] }, { "cell_type": "markdown", "id": "5d55d01d", "metadata": {}, "source": [ "Use thefarm.py to print successive frames, like of a movie, to investigate the path taken by your tractors, once you've set them in the field and initialized them.\n", "\n", "The Farm or Field type keeps track of what tractors are on them, and may nudge each one to a next position, suggesting the passage of time.\n", "\n", "For its part, the Tractor type has an internalized Field instance, much as a tractor driver would have an internalized mental model of the terrain, plus a plan of action." ] }, { "cell_type": "code", "execution_count": 33, "id": "ceab78ae", "metadata": {}, "outputs": [], "source": [ "# ! python thefarm.py" ] }, { "cell_type": "code", "execution_count": 27, "id": "220c8036", "metadata": {}, "outputs": [ { "data": { "text/plain": [ "'********************'" ] }, "execution_count": 27, "metadata": {}, "output_type": "execute_result" } ], "source": [ "\"*\" * 20" ] }, { "cell_type": "code", "execution_count": 29, "id": "27adb9d8", "metadata": {}, "outputs": [ { "name": "stdout", "output_type": "stream", "text": [ "\n", "💀💀💀💀💀💀💀💀💀💀\n", "💀💀💀💀💀💀💀💀💀💀\n", "💀💀💀💀💀💀💀💀💀💀\n", "💀💀💀💀💀💀💀💀💀💀\n", "💀💀💀💀👻👻👻👻👻💀\n", "💀💀💀💀💀💀💀💀💀💀\n", "💀💀💀💀💀💀💀💀💀💀\n", "💀💀💀💀💀💀💀💀💀💀\n", "💀💀💀💀💀💀💀💀💀💀\n", "💀💀💀💀💀💀💀💀💀💀\n", "\n", "🐙🐙🐙🐙🐙🐙🐙🐙🐙🐙\n", "🐙🐙🐙🐙🐙🐙🐙🐙🐙🐙\n", "🐙🐙🐙🐙🐙🐙🐙🐙🐙🐙\n", "🐙🐙🐙🐙🐙🐙🐙🐙🐙🐙\n", "🐙🐙🐙🐙🐅🐅🐅🐅🐅🐙\n", "🐙🐙🐙🐙🐙🐙🐙🐙🐙🐙\n", "🐙🐙🐙🐙🐙🐙🐙🐙🐙🐙\n", "🐙🐙🐙🐙🐙🐙🐙🐙🐙🐙\n", "🐙🐙🐙🐙🐙🐙🐙🐙🐙🐙\n", "🐙🐙🐙🐙🐙🐙🐙🐙🐙🐙\n" ] } ], "source": [ "import emo_tractor" ] }, { "cell_type": "code", "execution_count": 1, "id": "9bff15ac-8ba9-421f-9165-c3cc7e4f60d3", "metadata": {}, "outputs": [ { "name": "stdout", "output_type": "stream", "text": [ "\n", "\n", "...........\n", "...........\n", "...........\n", "...........\n", "...........\n", "...JUST USE\n", " IT........\n", "...........\n", "...........\n", "...........\n", "...........\n" ] } ], "source": [ "\"\"\"\n", "Field & Tractor\n", "\n", "Tractor behaves as an iterator wrapping around a\n", "field in a spiral by default, returning from lower\n", "right (n-1, m-1) to upper left (0, 0) where n, m is\n", "number of rows and columns respectively.\n", "\n", "The TractorWriter takes a string and starts planting\n", "it sequentially at a preset position.\n", "\n", "(cl) MIT License 2015 by 4dsolutions.net \n", "\"\"\"\n", "\n", "\n", "class Field(dict):\n", " \"\"\"\n", " Field is a mapping, subclass of dict, with keys (x, y)\n", " \"\"\"\n", "\n", " def __init__(self, rows, columns, *args, **kwargs):\n", " super().__init__(*args, **kwargs)\n", " self.dims = (rows, columns)\n", " self.marker = \".\"\n", " self.Ts = [ ] # add tractors to this list\n", "\n", " @property\n", " def rows(self):\n", " return self.dims[0]\n", "\n", " @property\n", " def columns(self):\n", " return self.dims[1]\n", "\n", "\n", " def __str__(self):\n", " \"\"\"\n", " output the field as a string\n", " \"\"\"\n", " s = \"\"\n", " for x in range(self.rows):\n", " s += \"\\n\"\n", " for y in range(self.columns):\n", " s += self[x,y] if (x,y) in self else self.marker\n", " return s\n", "\n", " def __repr__(self):\n", " return \"Field({}, {})\".format(self.rows, self.columns)\n", "\n", " def add_tractor(self, T):\n", " \"\"\"\n", " A tractor gains a reference to this very field when added thereto\n", " \"\"\"\n", " the_gen = T(self)\n", " self.Ts.append( the_gen )\n", "\n", "\n", "class Tractor:\n", " \"\"\"\n", " An iterator, spirals through Field and rasters to top again by default\n", " \"\"\"\n", "\n", " def __init__(self, my_field):\n", " self._myfield = my_field # shows up when added to a field\n", " self.pos = (0, 0) # changing internal state\n", " self.fuel_level = 150 # might run out of gas,\n", "\n", " def plant(self, the_crop):\n", " self._myfield[self.pos] = the_crop\n", "\n", " def __iter__(self):\n", " return self\n", "\n", " def __next__(self):\n", " \"\"\"\n", " Spiralling algorithm. If more columns to go, stay in\n", " current row. If end of column, start a next row, which\n", " may be top left if this was last row. Decrement fuel \n", " with each increment.\n", " \"\"\"\n", " self.fuel_level -= 1 # decrement fuel\n", " if self.col + 1 < self._myfield.columns:\n", " self.pos = (self.row, self.col + 1)\n", " else:\n", " if self.row + 1 < self._myfield.rows:\n", " self.pos = (self.row + 1, 0)\n", " else:\n", " self.pos = (0, 0)\n", " return self.pos\n", "\n", " @property\n", " def row(self):\n", " return self.pos[0]\n", "\n", " @property\n", " def col(self):\n", " return self.pos[1]\n", "\n", " def __repr__(self):\n", " return \"\".format(self.pos, self.fuel_level)\n", "\n", "class TractorWriter(Tractor):\n", "\n", " def write(self, what, where):\n", " self.what = what\n", " self.start_point = where\n", " self.cnt = 0\n", " self.writing = False\n", "\n", " def __next__(self):\n", " \"\"\"\n", " Plant what is to be written once the start position\n", " is reached.\n", " \"\"\"\n", " if self.pos == self.start_point:\n", " self.writing = True\n", " if self.writing:\n", " if self.cnt == len(self.what):\n", " self.writing = False\n", " self.cnt = 0\n", " else:\n", " self.plant(self.what[self.cnt])\n", " self.cnt += 1\n", " super().__next__() # updates pos\n", " \n", " def __repr__(self):\n", " return \"\".format(self.pos, self.fuel_level, self.what)\n", "\n", "if __name__ == \"__main__\":\n", " the_field = Field(11, 11)\n", " the_field.add_tractor(TractorWriter)\n", " the_tractor = the_field.Ts[0]\n", " the_tractor.write(\"JUST USE IT\", (5,3))\n", " for _ in range(121):\n", " next(the_tractor)\n", " print(the_tractor)\n", " print(the_field)\n" ] }, { "cell_type": "code", "execution_count": null, "id": "3778483c-608d-4a13-98aa-02bdb48c920e", "metadata": {}, "outputs": [], "source": [] } ], "metadata": { "kernelspec": { "display_name": "Python 3 (ipykernel)", "language": "python", "name": "python3" }, "language_info": { "codemirror_mode": { "name": "ipython", "version": 3 }, "file_extension": ".py", "mimetype": "text/x-python", "name": "python", "nbconvert_exporter": "python", "pygments_lexer": "ipython3", "version": "3.9.12" } }, "nbformat": 4, "nbformat_minor": 5 }