├── .gitignore ├── LICENSE.MIT ├── Makefile ├── README.md ├── TODO.md ├── help.css ├── help.html ├── http_server_with_upload.py ├── index.html ├── media ├── glider-73.webm ├── hrz-spinner.gif └── neighbors.png ├── notes.txt ├── other ├── hyptess-src.tgz └── hyptess.tgz ├── publish.sh ├── src ├── configure.coffee ├── core │ ├── acosh_polyfill.coffee │ ├── cellular_automata.coffee │ ├── chain_map.coffee │ ├── decompose_to_translations.coffee │ ├── field.coffee │ ├── fminsearch.coffee │ ├── fminsearch_performance_optimize.coffee │ ├── knuth_bendix.coffee │ ├── matrix3.coffee │ ├── poincare_view.coffee │ ├── regular_tiling.coffee │ ├── rule.coffee │ ├── sample_vd_rewriter.coffee │ ├── triangle_group_representation.coffee │ ├── utils.coffee │ ├── vondyck.coffee │ ├── vondyck_chain.coffee │ └── vondyck_rewriter.coffee ├── ext │ ├── canvas2svg.js │ ├── lzw.coffee │ └── polyfills.js └── ui │ ├── animator.coffee │ ├── application.coffee │ ├── canvas_util.coffee │ ├── context_delegate.coffee │ ├── dom_builder.coffee │ ├── ghost_click_detector.coffee │ ├── htmlutil.coffee │ ├── indexeddb.coffee │ ├── mousetool.coffee │ ├── navigator.coffee │ ├── observer.coffee │ ├── observer_remote.coffee │ ├── parseuri.coffee │ └── render_worker.coffee ├── styles.css ├── tests ├── perftest_simulation.coffee ├── test_cellular_automaton.coffee ├── test_chain_map.coffee ├── test_decompose_to_translations.coffee ├── test_field.coffee ├── test_fminsearch.coffee ├── test_hyperbolic_tessellation.coffee ├── test_knuth_bemdix.coffee ├── test_lzw.coffee ├── test_matrix3.coffee ├── test_new_group.coffee ├── test_parseuri.coffee ├── test_poincare_view.coffee ├── test_triangle_group_representation.coffee ├── test_utils.coffee ├── test_vondyck_chain.coffee └── test_vondyck_rewriter.coffee └── uploads └── README.md /.gitignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | node_modules 3 | *.js 4 | uploads/*.png 5 | uploads/*.webm 6 | nw-selenium 7 | reports 8 | 9 | -------------------------------------------------------------------------------- /LICENSE.MIT: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: 6 | 7 | The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. 8 | 9 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. -------------------------------------------------------------------------------- /Makefile: -------------------------------------------------------------------------------- 1 | .PHONY = test test_app start startwin publish 2 | 3 | application: 4 | browserify -t coffeeify src/ui/application.coffee > application.js 5 | # browserify -t coffeeify src/ui/render_worker.coffee > render_worker.js 6 | 7 | test: 8 | mocha tests/test*.coffee --compilers coffee:coffee-script/register 9 | 10 | start: 11 | python http_server_with_upload.py & 12 | xdg-open http://localhost:8000/index.html 13 | 14 | startwin: 15 | /c/Python33/python.exe http_server_with_upload.py & 16 | start http://localhost:8000/index.html 17 | 18 | clean: 19 | rm application.js render_worker.js 20 | 21 | 22 | publish: test application 23 | git checkout master 24 | sh publish.sh 25 | cd ../homepage-sources && sh ./publish.sh 26 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | Cellular automata on hyperbolic fields 2 | ====================================== 3 | 4 | Simulator of cellular automata on regular hyperbolic plane tilings, working in browser. 5 | 6 | [See it online](http://dmishin.github.io/hyperbolic-ca-simulator/index.html) 7 | 8 | For usage details, see [help page](http://dmishin.github.io/hyperbolic-ca-simulator/help.html) 9 | 10 | 11 | Key features are: 12 | 13 | * Support of arbitrary regular tilings. 14 | * Unlimited world size 15 | 16 | Building 17 | ======== 18 | Build requirements are: Node.JS, NPM, GNU Make. 19 | Install NPM modules: coffee-script, browserify, coffeeify. 20 | 21 | Testing 22 | ======= 23 | 24 | Running tests additionally requires the following NPM modules: mocha 25 | 26 | ```bash 27 | $ make test 28 | ``` 29 | 30 | Requirements 31 | ============ 32 | Works in any contemporary browser: Firefox, Chromium. Probably, works in the latest IE. 33 | 34 | Upload animation feature works only if the page is open from the local server. To do it, Python 3 is additionally required. Change to the project direcory, build it (alternatively, download [index.html](http://dmishin.github.io/hyperbolic-ca-simulator/index.html) and [application.js](http://dmishin.github.io/hyperbolic-ca-simulator/application.js) from the demo site), then run: 35 | 36 | ```bash 37 | $ python http_server_with_upload.py 38 | ``` 39 | 40 | After this, open http://localhost:8000/index.html and upload feature should work. 41 | 42 | Known bugs 43 | ========== 44 | 45 | ### Change to generic rule, then change grid, then change back to binary 46 | Workaround: set rule manually again. 47 | 48 | Licence 49 | ======= 50 | 51 | MIT 52 | -------------------------------------------------------------------------------- /TODO.md: -------------------------------------------------------------------------------- 1 | TODO List 2 | --------- 3 | Things left to implement 4 | 5 | ### GUI 6 | * [ ] Select manually, export selection (remove export visible) 7 | * [ ] Notifications 8 | * [x] Save and load state to Indexed database 9 | * [x] Import data from URL 10 | * [x] Write short help 11 | * [x] Display generation 12 | * [x] Support Day/Night rules 13 | * [x] Random fill fills fixed number of cells, not radius 14 | * [ ] Advanced settings: fill percent, size; critical population; 15 | * [x] Pan / Edit button 16 | * [x] Home pointer 17 | * [x] Manually setting image size 18 | * [x] Export to SVG 19 | * [x] Upload frames of smooth animations 20 | * [ ] Enable upload interface by button, not only on localhost. 21 | * [ ] Animator: handle large distances 22 | * [ ] Animator: remember initial position 23 | 24 | ### Internal code structure 25 | * [ ] Improve performance of eliminateFinalA, by trying only rewrites that change something. (Is it really different? Check performance.) 26 | * [ ] Performance tests 27 | * [x] Re-group modules: core, ui. Target: make core modules easily usable in a separate project 28 | * [x] Reorganize code, to make appendRewrite, eliminateFinalA, group a parts of a single entity. 29 | * [x] Split application.coffee into modules. It is too big. 30 | * [x] Create application class. 31 | 32 | ### Major rewrites 33 | * [ ] Use web worker for calculations. 34 | 35 | -------------------------------------------------------------------------------- /help.css: -------------------------------------------------------------------------------- 1 | /* ****************************** 2 | * Main window layout 3 | */ 4 | body{ 5 | background-color: #fff; 6 | color: #222; 7 | margin: 0; 8 | padding: 0; 9 | } 10 | 11 | article p{ 12 | margin-left: 2em; 13 | } 14 | figure { 15 | text-align: center; 16 | } 17 | figcaption { 18 | font-size: 80%; 19 | } 20 | 21 | h2 { 22 | border-bottom: solid 1px silver; 23 | margin-top: 1em; 24 | } 25 | h3 { 26 | color: #005; 27 | } 28 | h4 { 29 | color: #055; 30 | } 31 | 32 | pre { 33 | border: solid 1px silver; 34 | padding: 1em; 35 | margin: 1em 3em 1em 3em; 36 | background-color: #ffe; 37 | } 38 | 39 | 40 | span.button-text{ 41 | display: inline-block; 42 | 43 | margin: 0.2em 0; 44 | padding:0; 45 | 46 | display: inline-block; 47 | border: none; 48 | color: #000000; 49 | border-radius: 0.2em; 50 | -webkit-border-radius: 0.2em; 51 | -moz-border-radius: 0.2em; 52 | /*font-family: Verdana;*/ 53 | width: auto; 54 | height: auto; 55 | font-size: 1em; 56 | padding: 0.5em; 57 | -text-shadow: 0 1px 0 #FFFFFF; 58 | background-color: #E0E0E0; 59 | } -------------------------------------------------------------------------------- /help.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | Hyperbolic Cellular Automata Simulator 5 | 6 | 7 | 8 | 9 | 10 | 12 | 13 |
14 |

Hyperbolic Cellular Automata Simulator

15 |
16 | 20 |
Period 10 spaceship on the {3;9} tiling, binary rule [B 3 S 2 7]. See it in 21 | 22 | the simulator. 23 |
24 |
25 | 26 |

27 | This software is a simulator of cellular automata, acting on an regular tiling (grids) of the hyperbolic plane. 28 |

29 |

30 | It supports arbitrary large configurations of cells, limited only by available memory. 31 |

32 |

33 | The simulator is written in JavaScript (CoffeeScript actually) and works directly in the browser. It is an open software, sources are available on Github. 34 |

35 | 36 |

Features

37 |

38 | Key features are: 39 |

    40 |
  • Support of arbitrary large configurations.
    41 | The field is unlimited in all directions. However, large number of cells slows down computations and requires large amount of memory. In practice, configurations of 50'000 cells are near to the upper limit. 42 |
  • 43 |
  • 44 | Support of arbitrary regular hyperbolic tilings.
    45 | Hyperbolic plane can be tiled by a regular N-gons, with M N-gons around every vertex, if and only if
    46 | 47 | $$ \frac1N+\frac1M < \frac12 $$ 48 | Simply put, any tiling that is impossible on plane or on sphere, is possible on hyperbolic plane. 49 |
  • 50 |
  • 51 | Search
    52 | Observable area of the hyperbolic plane is limited. Search feature allows to find all cell groups in the field. 53 |
  • 54 | 55 |
56 |

57 |

The simulator also supports: 58 |

66 |

67 |

Tilings and neighbors

68 |

Only Moore neighborhood scheme is supported. In the {N;M} tiling (M regular N-gons are touching every vertex), total number of neighbors of a cell is \(N(M-2)\). 69 |

70 | 71 |
Cell neighbors in the {7;3}, {5;5}, {3;7} tilings.
72 |
73 |

74 | 75 |

Rules

76 |

For 2-state automata rules are specified in the slightly modified "BS" notation (used by Golly). The format is:
77 | $$B\ n_1\ n_2\ ...\ S\ m_1\ m_2\ ...$$
78 | where \(n_{1,2,...}\) are number of neighbors, required for cell to "born" (change from state 0 to state 1), and \(m_{1,2,...}\) are numbers of neighbors, required to "survive" (stay in state 1). Numbers must be separated by spaces because in hyperbolic tilings number of neighbors often exceeds 10. 79 |

80 |

To set rule, edit the "Rule" field on a side pane, then press enter or click button Set. 81 |

82 |

Custom rules

83 |

Custom rules are written in JavaScript. They support arbitrary number of states, and custom formulas for calculation of neighbors sum (default is summation). 84 |

85 |

To change to the custom rule mode, click button Generic... near the rule entry. Current rule would be converted to the custom rules code. 86 |

87 |

Custom rules format

88 |

89 | Custom rules must be a valid JS code, evaluated in expression context to an object with the following required fields: 90 |

    91 |
  • 'states' :: Integer
    92 | Number of states, must be 2 or more.
  • 93 |
  • 'next' :: Function(State, SumStates) → State
    94 | Takes current state and sum of neighbors, and returns new state.
  • 95 |
96 | 97 | By default, sum of neighbors is calculated by summation of state values. This can be overridden by the following optional fields: 98 |
    99 |
  • 'plus' :: Function(SumStates, State) → SumStates
    100 | Summation function that takes incomplete sum of states and add new state to it. Default is 101 |
    function(s,x){ return s+x; }
    102 | 
    103 |
  • 104 |
  • 'plusInitial' :: SumStates
    105 | Initial sum of states for sum calculation. Default is 0. 106 |
  • 107 |
108 |

109 |

Editing and navigation

110 |

111 | By default (when Pan button above view is active), left mouse button is used to pan view, middle button or left + shift are used to toggle state of a cell. This behavior is inverted by activating Edit button on the top of the view. 112 |

113 |

When panning view, dragging near view center translates view, and dragging near view edge rotates it. 114 |

115 |

Button Straighten view puts central cell exactly to the center of the view and rotates view to make the cell aligned vertically or horizontally. (Useful for making images and animations). 116 |

117 |

Memory

118 |

There is no undo (yet?). Instead, simple "memory" functionality with MS (set), MR (restore), MC (clear) buttons is implemented. It works exactly like in calculators. 119 |

120 | 121 |

Import and export

122 |

It is possible to export view state into a textual format. Either whole world or only visible area can be exported. Sample export result: 123 |

7$3$(A2(B|1))(A3(B|1))(b(A3(B|1)(b|1)))(a3(B|1))(B|1(a(B|1)))(a2(B|1))(A(B(a(B|1))))(a(B|1))
124 | 
125 | Here, 7 and 3 are tiling parameters, the rest is the description of cell states. 126 |

127 |

Cell position is described by a Von Dyck group \(D(N,M,2)\) with generators a, b. Generator "a" represents rotation around the N-gon center, generator "b" represents rotation around vertex. Coordinate of a cell is a chain of powers of "a" and "b". 128 |

129 |

In the export format, letters "a" and "b" designate generators, "A" and "B" are their inverse elements. Optional number after the letter is generator power. Number after vertical bar | is state of the cell. Braces are required; they allow to group multiple chains with common initial elements. 130 |

131 | 132 |

Save and load in Indexed Database

133 |

It is possible to store whole world state in the Indexed Database on the local machine. Saves are organized by grid and by rule; names are not required to be unique. They are stored locally and not uploaded anywhere. 134 |

135 |

By default, only saves matching current tiling and rule are shown. Use buttons to show all tilings and all rules.

136 | 137 |

Making animations

138 |

Uploading animations is only supported, when the page is run locally. To do it: 139 |

    140 |
  1. Download sources 141 |
    $get clone https://github.com/dmishin/hyperbolic-ca-simulator.git
    142 |
  2. 143 |
  3. Either build them: 144 |
    $make
    145 | Or download the compiled version from the demo site: 146 |
    $wget http://dmishin.github.io/hyperbolic-ca-simulator/index.html
    147 | $wget http://dmishin.github.io/hyperbolic-ca-simulator/application.js
    148 |
  4. 149 |
  5. Use python 3 to start local server 150 |
    $python http_server_with_upload.py
    151 |
  6. 152 |
  7. Open the local site: http://localhost:8000/index.html
  8. 153 |
154 |

155 |

156 | Animator GUI allows to set start and end points of animation. Use memory buttons (MS) to save state before adjusting positions. 157 |

158 |

Derotate button tries to adjusts start and end position by equal amount so that they would be connected by a pure translation (without rotation). In euclidean geometry, it is meaningless but in hyperbolic geometry it is useful. It allows to make seamless animations of gliders (top animation is done using it). 159 |

160 |

Steps to make seamless animation of a spaceship

161 |

162 |

    163 |
  1. Locate a spaceship (use Search and find far configurations)
  2. 164 |
  3. Adjust view to display it better, then use "Straighten view". (hotkey: Alt+S)
  4. 165 |
  5. Remember world state using MS (hotkey: M).
  6. 166 |
  7. Set animation start
  8. 167 |
  9. Simulate for several steps until spaceship returns into the original configuration (hotkey: N).
  10. 168 |
  11. Adjust view and use "Straighten view" to put spaceship exactly in the same position as in step #2.
  12. 169 |
  13. Set animation end
  14. 170 |
  15. Click Derotate to make path between start and end straight. (Should be possible for a real glider). You can ensure that positions are changed by clicking View button.
  16. 171 | 172 |
  17. Set other animation parameters (size, frames, generations, name), restore works state (MR, hotkey U), and run animation. 173 |
174 | Frames of animation are uploaded to the "uploads" folder. 175 |

176 |

Sources and license

177 |

Sources are available on Github under the permissive ">MIT license.

178 |

The code uses following third party libraries: 179 |

182 |

183 |

Contacts

184 |

185 | Don't hesitate to contact me on G+ or Reddit. 186 |

187 |
188 | 189 | 190 | -------------------------------------------------------------------------------- /http_server_with_upload.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python3 2 | 3 | """Simple HTTP Server With Upload. 4 | 5 | This module builds on BaseHTTPServer by implementing the standard GET 6 | and HEAD requests in a fairly straightforward manner. 7 | 8 | see: https://gist.github.com/UniIsland/3346170 9 | """ 10 | 11 | 12 | __version__ = "0.1" 13 | __all__ = ["SimpleHTTPRequestHandler"] 14 | __author__ = "bones7456" 15 | __home_page__ = "http://li2z.cn/" 16 | 17 | import os 18 | import posixpath 19 | import http.server 20 | import urllib.request, urllib.parse, urllib.error 21 | import cgi 22 | import shutil 23 | import mimetypes 24 | import re 25 | from io import BytesIO 26 | 27 | 28 | class SimpleHTTPRequestHandler(http.server.BaseHTTPRequestHandler): 29 | 30 | """Simple HTTP request handler with GET/HEAD/POST commands. 31 | 32 | This serves files from the current directory and any of its 33 | subdirectories. The MIME type for files is determined by 34 | calling the .guess_type() method. And can reveive file uploaded 35 | by client. 36 | 37 | The GET/HEAD/POST requests are identical except that the HEAD 38 | request omits the actual contents of the file. 39 | 40 | """ 41 | 42 | server_version = "SimpleHTTPWithUpload/" + __version__ 43 | 44 | def do_GET(self): 45 | """Serve a GET request.""" 46 | f = self.send_head() 47 | if f: 48 | self.copyfile(f, self.wfile) 49 | f.close() 50 | 51 | def do_HEAD(self): 52 | """Serve a HEAD request.""" 53 | f = self.send_head() 54 | if f: 55 | f.close() 56 | 57 | def do_POST(self): 58 | """Serve a POST request.""" 59 | r, info = self.deal_post_data() 60 | print((r, info, "by: ", self.client_address)) 61 | f = BytesIO() 62 | f.write(b'') 63 | f.write(b"\nUpload Result Page\n") 64 | f.write(b"\n

Upload Result Page

\n") 65 | f.write(b"
\n") 66 | if r: 67 | f.write(b"Success:") 68 | else: 69 | f.write(b"Failed:") 70 | f.write(info.encode()) 71 | f.write(("
back" % self.headers['referer']).encode()) 72 | f.write(b"
Powerd By: bones7456, check new version at ") 73 | f.write(b"") 74 | f.write(b"here.\n\n") 75 | length = f.tell() 76 | f.seek(0) 77 | self.send_response(200) 78 | self.send_header("Content-type", "text/html") 79 | self.send_header("Content-Length", str(length)) 80 | self.end_headers() 81 | if f: 82 | self.copyfile(f, self.wfile) 83 | f.close() 84 | 85 | def deal_post_data(self): 86 | content_type = self.headers['content-type'] 87 | if not content_type: 88 | return (False, "Content-Type header doesn't contain boundary") 89 | 90 | print("Content-type is:", content_type) 91 | if content_type.startswith('application/x-www-form-urlencoded'): 92 | print("URLENCODED content") 93 | print (self.path) 94 | length = int(self.headers.get('content-length')) 95 | 96 | postvars = cgi.parse_qs(self.rfile.read(length), keep_blank_values=1) 97 | print (postvars) 98 | 99 | return (False, "Can't process this data yet") 100 | elif content_type.startswith('multipart/form-data'): 101 | boundary = content_type.split("=")[1].encode() 102 | remainbytes = int(self.headers['content-length']) 103 | line = self.rfile.readline() 104 | remainbytes -= len(line) 105 | if not boundary in line: 106 | return (False, "Content NOT begin with boundary") 107 | line = self.rfile.readline() 108 | remainbytes -= len(line) 109 | fn = re.findall(r'Content-Disposition.*name="file"; filename="(.*)"', line.decode()) 110 | if not fn: 111 | return (False, "Can't find out file name...") 112 | path = self.translate_path(self.path) 113 | fn = os.path.join(path, fn[0]) 114 | line = self.rfile.readline() 115 | remainbytes -= len(line) 116 | line = self.rfile.readline() 117 | remainbytes -= len(line) 118 | print ("###", path, fn, fn[0]) 119 | try: 120 | out = open(fn, 'wb') 121 | except IOError: 122 | return (False, "Can't create file to write, do you have permission to write?") 123 | 124 | preline = self.rfile.readline() 125 | remainbytes -= len(preline) 126 | while remainbytes > 0: 127 | line = self.rfile.readline() 128 | remainbytes -= len(line) 129 | if boundary in line: 130 | preline = preline[0:-1] 131 | if preline.endswith(b'\r'): 132 | preline = preline[0:-1] 133 | out.write(preline) 134 | out.close() 135 | return (True, "File '%s' upload success!" % fn) 136 | else: 137 | out.write(preline) 138 | preline = line 139 | return (False, "Unexpect Ends of data.") 140 | 141 | def send_head(self): 142 | """Common code for GET and HEAD commands. 143 | 144 | This sends the response code and MIME headers. 145 | 146 | Return value is either a file object (which has to be copied 147 | to the outputfile by the caller unless the command was HEAD, 148 | and must be closed by the caller under all circumstances), or 149 | None, in which case the caller has nothing further to do. 150 | 151 | """ 152 | path = self.translate_path(self.path) 153 | f = None 154 | if os.path.isdir(path): 155 | if not self.path.endswith('/'): 156 | # redirect browser - doing basically what apache does 157 | self.send_response(301) 158 | self.send_header("Location", self.path + "/") 159 | self.end_headers() 160 | return None 161 | for index in "index.html", "index.htm": 162 | index = os.path.join(path, index) 163 | if os.path.exists(index): 164 | path = index 165 | break 166 | else: 167 | return self.list_directory(path) 168 | ctype = self.guess_type(path) 169 | try: 170 | # Always read in binary mode. Opening files in text mode may cause 171 | # newline translations, making the actual size of the content 172 | # transmitted *less* than the content-length! 173 | f = open(path, 'rb') 174 | except IOError: 175 | self.send_error(404, "File not found") 176 | return None 177 | self.send_response(200) 178 | self.send_header("Content-type", ctype) 179 | fs = os.fstat(f.fileno()) 180 | self.send_header("Content-Length", str(fs[6])) 181 | self.send_header("Last-Modified", self.date_time_string(fs.st_mtime)) 182 | self.end_headers() 183 | return f 184 | 185 | def list_directory(self, path): 186 | """Helper to produce a directory listing (absent index.html). 187 | 188 | Return value is either a file object, or None (indicating an 189 | error). In either case, the headers are sent, making the 190 | interface the same as for send_head(). 191 | 192 | """ 193 | try: 194 | list = os.listdir(path) 195 | except os.error: 196 | self.send_error(404, "No permission to list directory") 197 | return None 198 | list.sort(key=lambda a: a.lower()) 199 | f = BytesIO() 200 | displaypath = cgi.escape(urllib.parse.unquote(self.path)) 201 | f.write(b'') 202 | f.write(("\nDirectory listing for %s\n" % displaypath).encode()) 203 | f.write(("\n

Directory listing for %s

\n" % displaypath).encode()) 204 | f.write(b"
\n") 205 | #f.write(b"
") 206 | f.write(b"") 207 | f.write(b"") 208 | f.write(b"
\n") 209 | f.write(b"
\n\n
\n\n\n") 223 | length = f.tell() 224 | f.seek(0) 225 | self.send_response(200) 226 | self.send_header("Content-type", "text/html") 227 | self.send_header("Content-Length", str(length)) 228 | self.end_headers() 229 | return f 230 | 231 | def translate_path(self, path): 232 | """Translate a /-separated PATH to the local filename syntax. 233 | 234 | Components that mean special things to the local file system 235 | (e.g. drive or directory names) are ignored. (XXX They should 236 | probably be diagnosed.) 237 | 238 | """ 239 | # abandon query parameters 240 | path = path.split('?',1)[0] 241 | path = path.split('#',1)[0] 242 | path = posixpath.normpath(urllib.parse.unquote(path)) 243 | words = path.split('/') 244 | words = [_f for _f in words if _f] 245 | path = os.getcwd() 246 | for word in words: 247 | drive, word = os.path.splitdrive(word) 248 | head, word = os.path.split(word) 249 | if word in (os.curdir, os.pardir): continue 250 | path = os.path.join(path, word) 251 | return path 252 | 253 | def copyfile(self, source, outputfile): 254 | """Copy all data between two file objects. 255 | 256 | The SOURCE argument is a file object open for reading 257 | (or anything with a read() method) and the DESTINATION 258 | argument is a file object open for writing (or 259 | anything with a write() method). 260 | 261 | The only reason for overriding this would be to change 262 | the block size or perhaps to replace newlines by CRLF 263 | -- note however that this the default server uses this 264 | to copy binary data as well. 265 | 266 | """ 267 | shutil.copyfileobj(source, outputfile) 268 | 269 | def guess_type(self, path): 270 | """Guess the type of a file. 271 | 272 | Argument is a PATH (a filename). 273 | 274 | Return value is a string of the form type/subtype, 275 | usable for a MIME Content-type header. 276 | 277 | The default implementation looks the file's extension 278 | up in the table self.extensions_map, using application/octet-stream 279 | as a default; however it would be permissible (if 280 | slow) to look inside the data to make a better guess. 281 | 282 | """ 283 | 284 | base, ext = posixpath.splitext(path) 285 | if ext in self.extensions_map: 286 | return self.extensions_map[ext] 287 | ext = ext.lower() 288 | if ext in self.extensions_map: 289 | return self.extensions_map[ext] 290 | else: 291 | return self.extensions_map[''] 292 | 293 | if not mimetypes.inited: 294 | mimetypes.init() # try to read system mime.types 295 | extensions_map = mimetypes.types_map.copy() 296 | extensions_map.update({ 297 | '': 'application/octet-stream', # Default 298 | '.py': 'text/plain', 299 | '.c': 'text/plain', 300 | '.h': 'text/plain', 301 | }) 302 | 303 | 304 | #def test(HandlerClass = SimpleHTTPRequestHandler, 305 | # ServerClass = http.server.HTTPServer): 306 | # http.server.test(HandlerClass, ServerClass) 307 | 308 | if __name__ == '__main__': 309 | PORT = 8000 310 | import http.server 311 | import socketserver 312 | httpd = socketserver.TCPServer(("", PORT), SimpleHTTPRequestHandler) 313 | print("serving at port", PORT) 314 | httpd.serve_forever() 315 | 316 | -------------------------------------------------------------------------------- /index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | Hyperbolic Cellular Automata Simulator 4 | 5 | 6 | 7 | 8 |
9 | Help
10 | 11 | 12 |
13 |
14 | 15 | 16 | 115 | 116 | 117 |
118 |
119 | 120 | 121 | 122 | 123 | 124 | 125 | 126 | 127 | 128 | 129 | 130 | 131 |
132 | 135 | 136 | 137 |
138 | 139 |
140 | 141 |
142 |
143 | 144 |
145 |
146 |
147 | 148 |
149 |
150 | 151 | 157 | 158 | 159 |
160 |
161 | 162 | 163 | 177 | 178 | 189 | 200 | 201 | 216 | 217 | 218 | 234 | 235 | 236 | 248 | 249 | 250 | 251 | 252 | -------------------------------------------------------------------------------- /media/glider-73.webm: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/dmishin/hyperbolic-ca-simulator/01142a9139fac5508e3476759c3a52570402ee0c/media/glider-73.webm -------------------------------------------------------------------------------- /media/hrz-spinner.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/dmishin/hyperbolic-ca-simulator/01142a9139fac5508e3476759c3a52570402ee0c/media/hrz-spinner.gif -------------------------------------------------------------------------------- /media/neighbors.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/dmishin/hyperbolic-ca-simulator/01142a9139fac5508e3476759c3a52570402ee0c/media/neighbors.png -------------------------------------------------------------------------------- /notes.txt: -------------------------------------------------------------------------------- 1 | 2 | Interesting findings 3 | 4 | 5 | Grid 8/3 6 | Rule B 2 S 3 4 5 7 | 8 | 9 | Big oscillator (most often, except trivial) 10 | |1(A2(B|1(a2(B|1)))(b(A3(B|1))))(A3(B|1(a2(B|1))))(A(b(A3(B|1))))(a2(B|1))(a(B|1)) 11 | 12 | hollow circle 13 | (a(B(A3(B|1))(a2(B|1))(A4(B(a(B|1))))))(A3(B|1))(A(b(A3(B|1)))(B(a(B|1))))(a3(B|1))(A2(B(a2(B|1))))(B|1) 14 | 15 | 4-flipflop 16 | (A2(B|1))(b|1)(A4(B|1))(a(B|1))(a2(B(A3(B|1)))) 17 | 18 | 19 | 20 | 21 | grid 7/3 22 | Rule B 2 S 3 4 23 | 24 | Glider 25 | |1(a(B|1(A3(B|1))(a3(B|1))))(A2(B|1))(B(A3(B|1)))(A3(B|1))(b(A3(b|1)))(a2(B|1(A3(B|1)))) 26 | 27 | 28 | big glider 29 | |1(b|1(A3(B|1(a(B(a3(B|1))(A3(B|1))(a2(B|1))))))(A2(b(A3(B|1)))))(A2(b(A3(B(a(B|1)))))(B(a(B|1))))(A3(b(A3(B|1(a(B|1))))(A2(b(A3(B|1(a(B|1)))))))(B|1(a(B(a2(B|1(a3(B|1))(a2(B|1))))))(a2(B|1(a2(B|1))))))(a2(B|1(A3(B|1))))(A(B(a(B(A2(b(A3(B|1))))(a2(B|1))(A3(B(a(B|1)))))))) 30 | 31 | 32 | 33 | rule B 3 S 2 6 34 | oscillator 35 | 3$8$(b2|1)(A(b2|1)(B2|1))(a(B|1))(B2|1)(B|1) 36 | 37 | 38 | 39 | 40 | ##Encoding video 41 | 42 | #Result fine, but not great, lines are a bit shaky 43 | ffmpeg -f image2 -i glider-73-%04d.png -c:v libvpx -crf 12 -b:v 500K glider.webm 44 | 45 | crf 12 size 258 kb 46 | crf 6 same size 256 k 47 | 48 | bv 900 much better quality. 49 | size - 400k 50 | 51 | 52 | 53 | ### Droppning DB in firefox 54 | indexedDB.deleteDatabase('SavedFields').onsuccess=(function(e){console.log("Delete OK");}) 55 | 56 | 57 | 58 | ### fminsearch optimal parameters search 59 | Found best parameters: 60 | { reached: true, 61 | x: [ 2.1607253086419744, 0.5344135802469137, 0.21604938271604934 ], 62 | f: 131.8574, 63 | steps: 19, 64 | evaluations: 31 } 65 | 66 | ### after algorithm update 67 | { reached: true, 68 | x: [ 1.8967535436671243, 0.5075917352537719, 0.5819280121170549 ], 69 | f: 305.88, 70 | steps: 21, 71 | evaluations: 28 } 72 | 73 | { reached: true, 74 | x: [ 1.8227494855967077, 0.5406973379629628, 0.5832312564300409 ], 75 | f: 305.0143, 76 | steps: 21, 77 | evaluations: 31 } 78 | 79 | 80 | { reached: true, 81 | x: [ 2.0775121228806572, 0.5012562320493826, 0.5680960251028805 ], 82 | f: 322.9677, 83 | steps: 19, 84 | evaluations: 28 } 85 | 86 | 87 | { reached: true, 88 | x: [ 1.9662103813788327, 0.5229066549139674, 0.582174635024036 ], 89 | f: 310.8792, 90 | steps: 18, 91 | evaluations: 27 } 92 | 93 | #with shrink working on xh, not xr 94 | # Results are much better, success ratio equals to 1 almost always. 95 | 96 | Found best parameters: 97 | 98 | 99 | # no swap 100 | #seems even slightly better? 101 | { reached: true, 102 | x: [ 2.106298093785322, 0.43061503577329086, 0.4304678123345441 ], 103 | f: 152.5352, 104 | steps: 21, 105 | evaluations: 48 } 106 | 107 | 108 | no swap, simpler function 109 | { reached: true, 110 | x: [ 2.1333213855274655, 0.309652483460801, 0.564626752758327 ], 111 | f: 76.8806, 112 | steps: 20, 113 | evaluations: 49 } 114 | { reached: true, 115 | x: [ 2.051473781940811, 0.30889151199872644, 0.5790011071522101 ], 116 | f: 76.8845, 117 | steps: 18, 118 | evaluations: 44 } 119 | 120 | 121 | # swap and recalc center 122 | even worser. test also not passing. 123 | 124 | { reached: true, 125 | x: [ 2.0666666666666664, 0.4, 0.5666666666666667 ], 126 | f: 78.9804, 127 | steps: 20, 128 | evaluations: 30 } 129 | { reached: true, 130 | x: [ 2.1298820674557466, 0.4107337018199039, 0.44920417845409 ], 131 | f: 79.1192, 132 | steps: 18, 133 | evaluations: 24 } 134 | -------------------------------------------------------------------------------- /other/hyptess-src.tgz: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/dmishin/hyperbolic-ca-simulator/01142a9139fac5508e3476759c3a52570402ee0c/other/hyptess-src.tgz -------------------------------------------------------------------------------- /other/hyptess.tgz: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/dmishin/hyperbolic-ca-simulator/01142a9139fac5508e3476759c3a52570402ee0c/other/hyptess.tgz -------------------------------------------------------------------------------- /publish.sh: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | set -e 3 | 4 | echo Publishing data on dmishin home page 5 | ODIR=../homepage-sources/res/hyperbolic-ca-simulator 6 | 7 | cp -r *.html *.js media *.css README.md $ODIR 8 | 9 | cd $ODIR 10 | 11 | git add * 12 | git status 13 | 14 | git commit -m "Publishing updated hyperbolic-ca-simulator" 15 | 16 | git push 17 | -------------------------------------------------------------------------------- /src/configure.coffee: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env coffee 2 | fs = require "fs" 3 | path = require "path" 4 | os = require "os" 5 | mkdirp = require "mkdirp" 6 | 7 | isWin = !!os.platform().match(/^win/); 8 | 9 | walkDir = (dir, cb) -> 10 | for fname in fs.readdirSync dir 11 | subPath = path.join dir, fname 12 | if fs.lstatSync(subPath).isDirectory() 13 | walkDir subPath, cb 14 | else 15 | cb dir, fname 16 | 17 | 18 | js_sources = [] 19 | coffee_sources = [] 20 | js_outputs = [] 21 | coffee_outputs = [] 22 | output_dirs = {} 23 | 24 | makeCoffeeSource = (dir, fname)-> 25 | ipath = path.join(dir, fname) 26 | odir = dir.replace(/^scripts-src/, "scripts") 27 | output_dirs[odir] = true 28 | opath = path.join(odir, fname.replace(/.coffee$/,".js")) 29 | fs.writeSync makefile, "#{opath}: #{ipath}\n" 30 | fs.writeSync makefile, "\t$(COFFEE) $(COFFEE_FLAGS) -c -o #{odir} #{ipath}\n" 31 | coffee_sources.push ipath 32 | coffee_outputs.push opath 33 | 34 | makeJsSource = (dir, fname)-> 35 | ipath = path.join(dir, fname) 36 | odir = dir.replace(/^scripts-src/, "scripts") 37 | opath = path.join(odir, fname) 38 | 39 | fs.writeSync makefile, "#{opath}: #{ipath}\n" 40 | fs.writeSync makefile, 41 | if isWin 42 | "\tcopy /Y #{ipath} #{opath}\n" 43 | else 44 | "\tln -f #{ipath} #{opath}\n" 45 | 46 | js_sources.push ipath 47 | js_outputs.push opath 48 | 49 | 50 | 51 | makefile = fs.openSync "Makefile", "w" 52 | 53 | makefile_template = fs.readFileSync "Makefile.in" 54 | fs.writeSync makefile, ""+makefile_template 55 | 56 | walkDir "scripts-src", (d, f)-> 57 | if /.coffee$/.test f 58 | makeCoffeeSource d, f 59 | 60 | else if /.js$/.test f 61 | makeJsSource d, f 62 | 63 | joinStrings = (strs) -> strs.join " " 64 | 65 | fs.writeSync makefile, "JS_SOURCES = #{joinStrings js_sources}\n" 66 | fs.writeSync makefile, "COFFEE_SOURCES = #{joinStrings coffee_sources}\n" 67 | 68 | fs.writeSync makefile, "JS_OUTPUTS = #{joinStrings js_outputs}\n" 69 | fs.writeSync makefile, "COFFEE_OUTPUTS = #{joinStrings coffee_outputs}\n" 70 | fs.writeSync makefile, "ALL_OUTPUTS = $(JS_OUTPUTS) $(COFFEE_OUTPUTS)\n" 71 | 72 | fs.writeSync makefile, "everything: $(ALL_OUTPUTS)\n" 73 | 74 | fs.closeSync makefile 75 | 76 | #Create the complete output directory structure. 77 | for odir of output_dirs 78 | mkdirp odir 79 | -------------------------------------------------------------------------------- /src/core/acosh_polyfill.coffee: -------------------------------------------------------------------------------- 1 | 2 | Math.acosh = Math.acosh || (x) -> Math.log(x + Math.sqrt(x * x - 1)) 3 | -------------------------------------------------------------------------------- /src/core/cellular_automata.coffee: -------------------------------------------------------------------------------- 1 | {ChainMap} = require "./chain_map.coffee" 2 | 3 | exports.neighborsSum = neighborsSum = (cells, tiling, plus=((x,y)->x+y), plusInitial=0)-> 4 | sums = new ChainMap 5 | cells.forItems (cell, value)-> 6 | for neighbor in tiling.moore cell 7 | sums.putAccumulate neighbor, value, plus, plusInitial 8 | #don't forget the cell itself! It must also present, with zero (initial) neighbor sum 9 | if sums.get(cell) is null 10 | sums.put(cell, plusInitial) 11 | return sums 12 | 13 | exports.evaluateTotalisticAutomaton = evaluateTotalisticAutomaton = (cells, tiling, nextStateFunc, plus, plusInitial)-> 14 | newCells = new ChainMap 15 | sums = neighborsSum cells, tiling, plus, plusInitial 16 | sums.forItems (cell, neighSum)-> 17 | cellState = cells.get(cell) ? 0 18 | nextState = nextStateFunc cellState, neighSum 19 | if nextState isnt 0 20 | newCells.put cell, nextState 21 | return newCells 22 | 23 | 24 | -------------------------------------------------------------------------------- /src/core/chain_map.coffee: -------------------------------------------------------------------------------- 1 | #Hash map that uses chain as key 2 | exports.ChainMap = class ChainMap 3 | constructor: (initialSize = 16) -> 4 | #table size MUST be power of 2! Or else write your own implementation of % that works with negative hashes. 5 | if initialSize & (initialSize-1) isnt 0 #this trick works! 6 | throw new Error "size must be power of 2" 7 | @table = ([] for i in [0...initialSize] by 1) 8 | @count = 0 9 | @maxFillRatio = 0.7 10 | 11 | @sizeMask = initialSize - 1 12 | 13 | _index: (chain) -> chain.hash() & @sizeMask 14 | 15 | putAccumulate: (chain, value, accumulateFunc, accumulateInitial)-> 16 | cell = @table[@_index chain] 17 | 18 | for key_value in cell 19 | if key_value[0].equals chain 20 | #Update existing value 21 | key_value[1] = accumulateFunc key_value[1], value 22 | return 23 | 24 | cell.push [chain, accumulateFunc(accumulateInitial, value)] 25 | @count += 1 26 | if @count > @maxFillRatio*@table.length 27 | @_growTable() 28 | return this 29 | 30 | put: (chain, value) -> @putAccumulate chain, value, (x,y)->y 31 | 32 | get: (chain) -> 33 | # console.log "geting for #{chain}" 34 | for key_value in @table[@_index chain] 35 | if key_value[0].equals chain 36 | #console.log " found something" 37 | return key_value[1] 38 | #console.log " not found" 39 | return null 40 | 41 | remove: (chain) -> 42 | tableCell = @table[@_index chain] 43 | for key_value, index in tableCell 44 | if key_value[0].equals chain 45 | tableCell.splice index, 1 46 | @count -= 1 47 | return true 48 | return false 49 | 50 | _growTable: -> 51 | newTable = new ChainMap (@table.length * 2) 52 | #console.log "Growing table to #{newTable.table.length}" 53 | for cell in @table 54 | for [key, value] in cell 55 | newTable.put key, value 56 | @table = newTable.table 57 | @sizeMask = newTable.sizeMask 58 | return 59 | 60 | forItems: (callback) -> 61 | for cell in @table 62 | for [key, value] in cell 63 | callback key, value 64 | return 65 | 66 | copy: -> 67 | copied = new ChainMap 1 #minimal size 68 | 69 | copied.count = @count 70 | copied.maxFillRatio = @maxFillRatio 71 | copied.sizeMask = @sizeMask 72 | 73 | copied.table = for cell in @table 74 | for key_value in cell 75 | key_value[..] 76 | 77 | return copied 78 | -------------------------------------------------------------------------------- /src/core/decompose_to_translations.coffee: -------------------------------------------------------------------------------- 1 | M = require "./matrix3.coffee" 2 | {fminsearch} = require "./fminsearch.coffee" 3 | #{hyperbolicRealJordan}= require "./hyperbolic_jordan" 4 | 5 | 6 | 7 | exports.decomposeToTranslations2 = decomposeToTranslations2 = (m) -> 8 | 9 | # M = V D V^-1 10 | # 11 | # D is diag [1, exp(d), exp(-d)] 12 | # In the same time 13 | # 14 | # M = T^-1 Tau T 15 | # where T, Tau are pure translations 16 | # Therefore, 17 | # Tau = R Tau0 R^-1 18 | # where R is pure rotation, Tau0 - translation along x. 19 | # 20 | # Therefore 21 | # Tau0 = V0 D V0^-1 22 | # 23 | # where V0 [ 0,1,1; 1,0,0; 0,1,-1] 24 | # 25 | # Combining this 26 | # 27 | # V D V^-1 = T^-1 R V0 S D S^-1 V0^-1 R^-1 T 28 | # 29 | # V = T^-1 R V0 S 30 | # 31 | # where S - arbitrary nonzero diagonal matrix 32 | # T^-1 R = V S^-1 V0^-1 33 | 34 | exports.decomposeToTranslations = decomposeToTranslations = (m, eps=1e-5) -> 35 | 36 | #Another idea, reducing number of parameters 37 | # 38 | # Approximate paramters of matrix T1, fitness is rotation amount of the T2 39 | 40 | shiftAndDecompose = ([t1x, t1y]) -> 41 | T1 = M.translationMatrix t1x, t1y 42 | iT1 = M.hyperbolicInv T1 43 | Tau = M.mul T1, M.mul m, iT1 44 | 45 | #decompose Tau to rotation and translation part 46 | return M.hyperbolicDecompose Tau 47 | 48 | #fitness is absolute value of angle 49 | fitness = (t1xy) -> 50 | Math.abs(shiftAndDecompose(t1xy)[0]) 51 | 52 | res = fminsearch fitness, [0.0, 0.0], 0.1, eps 53 | 54 | if res.reached and res.f < eps*10 55 | [t1x, t1y] = res.x 56 | [angle, t2x, t2y] = shiftAndDecompose [t1x, t1y] 57 | [M.translationMatrix(t1x, t1y), M.translationMatrix(t2x, t2y)] 58 | else 59 | [null, null] 60 | 61 | exports.decomposeToTranslationsFmin = decomposeToTranslationsFmin = (m, eps=1e-5) -> 62 | #Decompose hyperbolic matrix to 3 translations: M = T1^-1 T2 T1 63 | #not always possible. 64 | fitness = ([dx1,dy1,dx2,dy2]) -> 65 | t1 = M.translationMatrix dx1, dy1 66 | t2 = M.translationMatrix dx2, dy2 67 | 68 | #calculate difference 69 | d = M.mul M.hyperbolicInv(t1), M.mul t2, t1 70 | M.addScaledInplace d, m, -1 71 | 72 | M.amplitude d 73 | 74 | #detect transllation 75 | x = M.mulv m, [0,0,1] 76 | 77 | res = fminsearch fitness, [0.0, 0.0, x[0], x[1]], 0.1, eps 78 | if res.reached and res.f < eps*10 79 | [dx1,dy1,dx2,dy2] = res.x 80 | t1 = M.translationMatrix dx1, dy1 81 | t2 = M.translationMatrix dx2, dy2 82 | [t1,t2] 83 | else 84 | [null, null] 85 | 86 | 87 | exports.decomposeToTranslationsAggresively = (m, eps=1e-5, attempts = 1000) -> 88 | 89 | #detect range 90 | x = M.mulv m, [0,0,1] 91 | d = Math.sqrt(x[0]**2+x[1]**2) 92 | 93 | decomposeTranslated = (t0, eps) -> 94 | mPrime = M.mul M.hyperbolicInv(t0), M.mul m, t0 95 | [t1,t2] = decomposeToTranslationsFmin mPrime, eps 96 | #t0^-1 m t0 = t1^-1 t2 t1 97 | # m = t0 t1^-1 t2 t1 t0^-1 98 | # 99 | if t1 isnt null 100 | return [M.mul(t1, M.hyperbolicInv t0), t2] 101 | else 102 | return [null, null] 103 | 104 | #attempts with radom pre-translation 105 | for attempt in [0... attempts] 106 | d = Math.random()*d*3 107 | angle = Math.random()*Math.PI*2 108 | t0 = M.translationMatrix(d*Math.cos(angle),d*Math.sin(angle)) 109 | [t1,t2] = decomposeTranslated t0, 1e-2 110 | if t1 isnt null 111 | #fine optiomization 112 | console.log "fine optimization" 113 | return decomposeTranslated t1 114 | 115 | console.log "All attempts failed" 116 | return [null, null] 117 | -------------------------------------------------------------------------------- /src/core/field.coffee: -------------------------------------------------------------------------------- 1 | {unity, newNode} = require "./vondyck_chain.coffee" 2 | {ChainMap} = require "./chain_map.coffee" 3 | 4 | #High-level utils for working with hyperbolic cellular fields 5 | 6 | exports.extractClusterAt = extractClusterAt = (cells, tiling, chain) -> 7 | #use cycle instead of recursion in order to avoid possible stack overflow. 8 | #Clusters may be big. 9 | stack = [chain] 10 | cluster = [] 11 | while stack.length > 0 12 | c = stack.pop() 13 | continue if cells.get(c) is null 14 | 15 | cells.remove c 16 | cluster.push c 17 | 18 | for neighbor in tiling.moore c 19 | if cells.get(neighbor) isnt null 20 | stack.push neighbor 21 | return cluster 22 | 23 | exports.allClusters = (cells, tiling) -> 24 | cellsCopy = cells.copy() 25 | clusters = [] 26 | 27 | cells.forItems (chain, value) -> 28 | if cellsCopy.get(chain) isnt null 29 | clusters.push extractClusterAt(cellsCopy, tiling, chain) 30 | 31 | return clusters 32 | 33 | 34 | #Generate JS object from this field. 35 | # object tries to efectively store states of the field cells in the tree. 36 | # Position of echa cell is represented by chain. 37 | # Chains can be long; for nearby chains, their tails are the same. 38 | # Storing chains in list would cause duplication of repeating tails. 39 | # 40 | # Object structure: 41 | # { 42 | # g: 'a' or 'b', name of the group generator. Not present in root! 43 | # p: integer, power of the generator. Not present in root! 44 | # [v:] value of the cell. Optional. 45 | # [cs]: [children] array of child trees 46 | # } 47 | exports.exportField = (cells) -> 48 | root = { 49 | } 50 | chain2treeNode = new ChainMap 51 | chain2treeNode.put unity, root 52 | 53 | putChain = (chain) -> #returns tree node for that chain 54 | node = chain2treeNode.get chain 55 | if node is null 56 | parentNode = putChain chain.t 57 | node = {} 58 | node[chain.letter] = chain.p 59 | if parentNode.cs? 60 | parentNode.cs.push node 61 | else 62 | parentNode.cs = [node] 63 | chain2treeNode.put chain, node 64 | return node 65 | cells.forItems (chain, value) -> 66 | putChain(chain).v = value 67 | 68 | return root 69 | 70 | 71 | exports.importFieldTo = importFieldTo = (fieldData, callback) -> 72 | putNode = (rootChain, rootNode)-> 73 | if rootNode.v? 74 | #node is a cell that stores some value? 75 | callback rootChain, rootNode.v 76 | if rootNode.cs? 77 | for childNode in rootNode.cs 78 | if childNode.a? 79 | putNode(newNode('a', childNode.a, rootChain), childNode) 80 | else if childNode.b? 81 | putNode(newNode('b', childNode.b, rootChain), childNode) 82 | else 83 | throw new Error "Node has neither A nor B generator" 84 | return 85 | putNode unity, fieldData 86 | 87 | exports.importField = (fieldData, cells = new ChainMap, preprocess)-> 88 | importFieldTo fieldData, (chain, value) -> 89 | if preprocess? then chain = preprocess(chain) 90 | cells.put chain, value 91 | return cells 92 | 93 | #Generate random value in range from 1 to nStates-1 94 | exports.randomStateGenerator = (nStates) -> -> 95 | (Math.floor(Math.random()*(nStates-1))|0) + 1 96 | 97 | exports.randomFill = (field, density, center, r, tiling, randomState ) -> 98 | if density < 0 or density > 1.0 99 | throw new Error "Density must be in [0;1]" 100 | #by default, fill with ones. 101 | randomState = randomState ? -> 1 102 | 103 | for cell in tiling.farNeighborhood center, r 104 | if Math.random() < density 105 | field.put cell, randomState() 106 | return 107 | 108 | #Fill randomly, visiting numCells cells around the given center. 109 | exports.randomFillFixedNum = (field, density, center, numCells, tiling, randomState ) -> 110 | if density < 0 or density > 1.0 111 | throw new Error "Density must be in [0;1]" 112 | #by default, fill with ones. 113 | randomState = randomState ? -> 1 114 | visited = 0 115 | tiling.forFarNeighborhood center, (cell, _)-> 116 | #Time to stop iteration? 117 | return false if visited >= numCells 118 | if Math.random() < density 119 | field.put cell, randomState() 120 | visited+=1 121 | #Continue 122 | return true 123 | return 124 | 125 | 126 | 127 | exports.stringifyFieldData = (data) -> 128 | parts = [] 129 | doStringify = (data)-> 130 | if data.v? 131 | parts.push "|"+data.v 132 | if data.cs? 133 | for child in data.cs 134 | parts.push '(' 135 | if child.a? 136 | gen = 'a' 137 | pow = child.a 138 | else if child.b? 139 | gen = 'b' 140 | pow = child.b 141 | #parts.push "b#{child.b}" 142 | else throw new Error "bad data, neither a nor b" 143 | if pow < 0 144 | gen = gen.toUpperCase() 145 | pow = -pow 146 | parts.push gen 147 | parts.push "#{pow}" if pow isnt 1 148 | 149 | doStringify child 150 | parts.push ')' 151 | doStringify(data) 152 | return parts.join "" 153 | 154 | #Parse what stringifyFieldData returns. 155 | # Produce object, suitable for importField 156 | exports.parseFieldData = (text) -> 157 | integer = (text, pos) -> 158 | #console.log "parsing from #{pos}: '#{text}'" 159 | sign = 1 160 | value = '' 161 | getResult = -> 162 | if value is '' 163 | return null 164 | else 165 | v = sign * parseInt(value, 10) 166 | #console.log "parsed int: #{v}" 167 | return [v, pos] 168 | 169 | while true 170 | if pos >= text.length 171 | return getResult() 172 | c = text[pos] 173 | if c is '-' 174 | sign = -sign 175 | else if c >= '0' and c <= '9' 176 | value += c 177 | else 178 | return getResult() 179 | pos += 1 180 | return 181 | 182 | skipSpaces = (text, pos) -> 183 | while pos < text.length and text[pos] in [' ','\t','\r','\n'] 184 | pos += 1 185 | return pos 186 | 187 | awaitChar = (char, text, pos) -> 188 | pos = skipSpaces text, pos 189 | return null if pos >= text.length 190 | c = text[pos] 191 | pos += 1 192 | return null if c isnt char 193 | return pos 194 | 195 | parseChildSpec = (text, pos) -> 196 | 197 | #parse 198 | pos = awaitChar '(', text, pos 199 | return null if pos is null 200 | 201 | #parse generator name... 202 | pos = skipSpaces text, pos 203 | return null if pos >= text.length 204 | gen = text[pos] 205 | pos += 1 206 | return null unless gen in ['a','b','A','B'] 207 | genLower = gen.toLowerCase() 208 | powerSign = if genLower is gen then 1 else -1 209 | gen = genLower 210 | 211 | #parse generaotr power 212 | pos = skipSpaces text, pos 213 | powerRes = integer text, pos 214 | if powerRes is null 215 | power = 1 216 | else 217 | [power, pos] = powerRes 218 | power *= powerSign 219 | 220 | #parse cell state and children 221 | pos = skipSpaces text, pos 222 | valueRes = parseValueSpec text, pos 223 | return null if valueRes is null 224 | [value, pos] = valueRes 225 | 226 | #store previously parsed generator and power 227 | value[gen] = power 228 | #console.log "Value updated with generator data, waiting for ) from #{pos}, '#{text.substring(pos)}'" 229 | 230 | 231 | pos = skipSpaces text, pos 232 | pos = awaitChar ')', text, pos 233 | return null if pos is null 234 | 235 | #ok, parsed child fine! 236 | #console.log "parsed child OK" 237 | return [value, pos] 238 | 239 | 240 | parseValueSpec = (text, pos) -> 241 | value = {} 242 | pos = skipSpaces text, pos 243 | 244 | pos1 = awaitChar '|', text, pos 245 | if pos1 isnt null 246 | #has value 247 | pos = pos1 248 | intResult = integer(text, pos) 249 | if intResult isnt null 250 | [value.v, pos] = intResult 251 | #parse children 252 | children = [] 253 | #console.log "parsing children from from #{pos}, '#{text.substring(pos)}'" 254 | while true 255 | childRes = parseChildSpec text, pos 256 | if childRes is null 257 | #console.log "no more children..." 258 | break 259 | children.push childRes[0] 260 | pos = childRes[1] 261 | #console.log "parsed #{children.length} children" 262 | if children.length > 0 263 | value.cs = children 264 | return [value, pos] 265 | #finally, parse all 266 | allRes = parseValueSpec text, 0 267 | if allRes is null 268 | throw new Error "Faield to parse!" 269 | pos = allRes[1] 270 | pos = skipSpaces text, pos 271 | if pos isnt text.length 272 | throw new Error "garbage after end" 273 | return allRes[0] 274 | 275 | ### 276 | """ 277 | exports.parseFieldData1 = (data) -> 278 | #data format (separators not included) is: 279 | # 280 | # text ::= value_spec 281 | # value_spec ::= [value]? ( '(' child_spec ')' )* 282 | # value ::= integer 283 | # child_spec ::= generator power value_spec 284 | # generator ::= a | b 285 | # power ::= integer 286 | # 287 | # 288 | 289 | #parser returns either null or pair: 290 | # (parse result, next position) 291 | # 292 | # optional combinator 293 | # parse result is value of the inner parser or null 294 | # always succeeds 295 | # 296 | optional = (parser) -> (text, start) -> 297 | parsed = parser(text, start) 298 | if parsed is null 299 | [null, start] 300 | else 301 | parsed 302 | 303 | 304 | literal = (lit) -> (text, pos) -> 305 | for lit_i, i in lit 306 | if pos+i >= text.length 307 | return null 308 | if text[pos+i] isnt lit_i 309 | return null 310 | return [lit, pos+lit.length] 311 | 312 | oneOf = (parsers...) -> (text, pos) -> 313 | for p in parsers 314 | res = p(text,pos) 315 | return res if res isnt null 316 | return null 317 | 318 | word = (allowedChars) -> 319 | charSet = {} 320 | for c in allowedChars 321 | charSet[c] = true 322 | return (text, start) -> 323 | parseResult = "" 324 | pos = start 325 | while pos < text.length 326 | c = text[pos] 327 | if charSet.hasOwnProperty c 328 | parseResult += c 329 | pos += 1 330 | else 331 | break 332 | if parseResult is "" 333 | null 334 | else 335 | 336 | seq = (parsers) -> (text, pos) -> 337 | results = [] 338 | for p in parsers 339 | r = p(text, pos) 340 | if r isnt null 341 | results.push r 342 | pos = r[1] 343 | else 344 | return null 345 | return [results, pos] 346 | 347 | map = (parser, func) -> (text, pos) -> 348 | r = parser(text, pos) 349 | return null if r is null 350 | return [func(r[0]), r[1]] 351 | 352 | integer = seq( optional(literal('-')), word('123456789') 353 | integer = map( parseInteger, [sign, digits]-> 354 | parseInt((sign or '')+digits, 10) ) 355 | 356 | 357 | 358 | parseInteger = (text, start) -> 359 | hasSign = false 360 | """ 361 | ### 362 | -------------------------------------------------------------------------------- /src/core/fminsearch.coffee: -------------------------------------------------------------------------------- 1 | 2 | combine2= (v1, k1, v2, k2) -> 3 | (v1[i]*k1+v2[i]*k2 for i in [0...v1.length] by 1) 4 | 5 | scaleInplace= (v,k)-> 6 | for i in [0...v.length] by 1 7 | v[i]*=k 8 | return v 9 | 10 | addInplace= (v1,v2)-> 11 | for v2i, i in v2 12 | v1[i] += v2[i] 13 | return v1 14 | 15 | amplitude = (x)-> Math.max (Math.abs(xi) for xi in x)... 16 | 17 | #optimal parameters for Rozenbrock optimiation 18 | exports.alpha = 2.05 19 | exports.beta = 0.46 20 | exports.gamma = 0.49 21 | 22 | exports.fminsearch = (func, x0, step, tol=1e-6, maxiter=10000)-> 23 | alpha = exports.alpha 24 | beta = exports.beta 25 | gamma = exports.gamma 26 | 27 | n = x0.length 28 | 29 | #generate initial polygon 30 | poly = (x0[..] for i in [0..n] by 1) 31 | for i in [1..n] by 1 32 | poly[i][i-1] += step 33 | 34 | evaluations = n+1 35 | 36 | findCenter = -> 37 | xc = withValue[0][0][..] 38 | for i in [1..(n-1)] by 1 39 | addInplace xc, withValue[i][0] 40 | scaleInplace xc, 1.0/n 41 | return xc 42 | 43 | polySize = -> 44 | minima = withValue[0][0][..] 45 | maxima = withValue[0][0][..] 46 | for i in [1...withValue.length] by 1 47 | xi = withValue[i][0] 48 | for xij, j in xi 49 | if xij < minima[j] 50 | minima[j] = xij 51 | if xij > maxima[j] 52 | maxima[j] = xij 53 | Math.max (maxima[i]-minima[i] for i in [0...n] by 1)... 54 | 55 | 56 | makeAnswerOK = -> 57 | rval = 58 | reached:true 59 | x: withValue[0][0] 60 | f: withValue[0][1] 61 | steps: iter 62 | evaluations: evaluations 63 | 64 | withValue = ( [x, func(x)] for x in poly ) 65 | 66 | #sort by function value 67 | sortPoints = -> withValue.sort (a,b) -> a[1] - b[1] 68 | 69 | iter = 0 70 | while iter < maxiter 71 | iter += 1 72 | 73 | sortPoints() 74 | #worst is last 75 | 76 | #find center of all points except the last (worst) one. 77 | xc = findCenter() 78 | 79 | #Best, worst and first-before-worst values. 80 | f0 = withValue[0][1] 81 | fg = withValue[n-1][1] 82 | 83 | [xh, fh] = withValue[n] 84 | #console.log "I=#{iter}\tf0=#{f0}\tfg=#{fg}\tfh=#{fh}" 85 | 86 | #reflect 87 | #xr = xc-(xh-xc) = 2xc - xh 88 | xr = combine2 xc, 2.0, xh, -1.0 89 | fr = func xr 90 | evaluations += 1 91 | 92 | if fr < f0 93 | #extend 94 | # xe = xc+ (xr-xc)*alpha = xr*alpha + xc*(1-alpha) 95 | xe = combine2 xr, alpha, xc, (1-alpha) 96 | fe = func xe 97 | evaluations += 1 98 | if fe < fr 99 | #use fe 100 | withValue[n] = [xe, fe] 101 | else 102 | #use fr 103 | withValue[n] = [xr, fr] 104 | else if fr < fg 105 | #use xr 106 | withValue[n] = [xr, fr] 107 | else 108 | # This is present in the original decription of the method, but it makes result even slightly worser! 109 | #if fr < fh 110 | # #swap xr, xg 111 | # [[xr, fr], withValue[n-1]] = [withValue[n-1], [xr, fr]] 112 | # # my own invertion - makes worser. 113 | # #xc = findCenter() 114 | 115 | #now fr >= fh 116 | #shrink 117 | #xs = xc+ (xr-xc)*beta 118 | xs = combine2 xh, beta, xc, (1-beta) 119 | fs = func xs 120 | evaluations += 1 121 | 122 | if fs < fh 123 | #use shrink 124 | withValue[n] = [xs, fs] 125 | if polySize() < tol then return makeAnswerOK() 126 | else 127 | #global shrink 128 | x0 = withValue[0][0] 129 | #check exit 130 | if polySize() < tol then return makeAnswerOK() 131 | #global shrink 132 | for i in [1..n] 133 | xi = combine2 withValue[i][0], gamma, x0, 1-gamma 134 | fi = func xi 135 | withValue[i] = [xi,fi] 136 | evaluations += n 137 | 138 | sortPoints() 139 | rval = 140 | reached: false 141 | x: withValue[0][0] 142 | f: withValue[0][1] 143 | steps: iter 144 | evaluations: evaluations 145 | -------------------------------------------------------------------------------- /src/core/fminsearch_performance_optimize.coffee: -------------------------------------------------------------------------------- 1 | 2 | fminsearch = require "./fminsearch.coffee" 3 | fminsearch1 = require "./Fminsearch.coffee" 4 | 5 | near = (x, y, eps=1e-5) -> Math.abs(x-y) 8 | 9 | sampleSize = 10000 10 | 11 | #use rozenbrock for test 12 | func = ([x,y]) -> (1-x)**2 + 100*(y-x**2)**2 13 | #func = ([x,y]) -> (1-x)**2 + 2*(1-y)**2 14 | 15 | 16 | randRange = (a,b) -> Math.random()*(b-a)+a 17 | 18 | makeInitialPoint = -> [randRange(-5,5), randRange(-5,5)] 19 | 20 | initialSamples = (makeInitialPoint() for _ in [0...sampleSize] by 1) 21 | 22 | 23 | penalty = 10000 24 | step = 1.0 25 | eps = 1e-5 26 | maxiter = 1000 27 | 28 | measurePerformance = ([alpha, beta,gamma])-> 29 | 30 | fminsearch.alpha = alpha 31 | fminsearch.beta = beta 32 | fminsearch.gamma = gamma 33 | 34 | price = 0 35 | success = 0 36 | for x0 in initialSamples 37 | res = fminsearch.fminsearch func, x0, step, eps, maxiter 38 | price += res.evaluations 39 | 40 | unless res.reached 41 | price += penalty 42 | continue 43 | unless near(res.x[0], 1.0, eps*10) and near(res.x[1], 1.0, eps*10) 44 | price += penalty 45 | continue 46 | success += 1 47 | price /= sampleSize 48 | console.log "ABG: #{JSON.stringify [alpha, beta,gamma]} price: #{price} success ratio: #{success / initialSamples.length}" 49 | return price 50 | 51 | #for abg in [[2.0,0.5,0.5], [30.0,0.5,0.5], [2.0,0.3,0.5], [2.0,0.5,0.2]] 52 | # console.log "===testing ABG:" 53 | # console.dir abg 54 | # console.log "price: #{measurePerformance abg}" 55 | 56 | fminsearch.alpha = 1000 57 | if fminsearch1.alpha is 1000 58 | throw new Error "modules not separate" 59 | console.log "Trying to find an optimal performace" 60 | res = fminsearch1.fminsearch measurePerformance, [2.0,0.5,0.5], 0.1, 0.01 61 | console.log "Found best parameters:" 62 | console.dir(res) 63 | 64 | runtest() 65 | -------------------------------------------------------------------------------- /src/core/knuth_bendix.coffee: -------------------------------------------------------------------------------- 1 | #Based on http://www.math.rwth-aachen.de/~Gerhard.Hiss/Students/DiplomarbeitPfeiffer.pdf 2 | #algorithm 3, 4 3 | #import itertools 4 | 5 | 6 | #values are encoded as simple strings. 7 | # User is responsible 8 | 9 | print = (s ... ) -> console.log( s.join(" ") ) 10 | 11 | #COnvert "less or equal" function to the JS-compatible comparator function 12 | le2cmp = ( leFunc ) -> 13 | (a,b) -> 14 | if a is b 15 | 0 16 | else if leFunc(a,b) 17 | -1 18 | else 19 | 1 20 | 21 | exports.RewriteRuleset = class RewriteRuleset 22 | constructor: (rules)-> 23 | @rules = rules 24 | 25 | pprint: ()-> 26 | print ("{") 27 | for [v, w] in @_sortedItems() 28 | print " #{v} -> #{w}" 29 | print "}" 30 | 31 | copy: ()-> 32 | new RewriteRuleset(JSON.parse JSON.stringify @rules) 33 | 34 | _sortedItems: ()-> 35 | items = @items() 36 | items.sort le2cmp(shortLex) 37 | return items 38 | 39 | suffices: -> (k for k of @rules) 40 | 41 | size: -> @suffices().length 42 | items: -> ( [k, v] for k, v of @rules ) 43 | 44 | 45 | __equalOneSided: (other) -> 46 | for k, v of @rules 47 | if other.rules[k] isnt v 48 | return false 49 | return true 50 | 51 | equals: ( other)-> this.__equalOneSided(other) and other.__equalOneSided(this) 52 | 53 | #__hash__: ()-> return hash(@rules) 54 | 55 | add: ( v, w)-> 56 | @rules[v] = w 57 | 58 | remove: ( v)-> 59 | delete @rules[v] 60 | 61 | normalize: ( lessOrEq )-> 62 | SS = {} 63 | for v, w of @rules 64 | [v, w] = sortPairReverse(v, w, lessOrEq) 65 | #v is biggest now 66 | if not SS[v]? 67 | SS[v] = w 68 | else 69 | #resolve conflict by chosing the lowest of 2. 70 | SS[v] = sortPairReverse(w, SS[v], lessOrEq)[1] 71 | return new RewriteRuleset(SS) 72 | 73 | __ruleLengths: ()-> 74 | lens = {} 75 | for k of @rules 76 | lens[k.length] = null 77 | lenslist = (parseInt(k, 10) for k of lens) 78 | lenslist.sort() 79 | return lenslist 80 | 81 | appendRewrite: ( s, xs_)-> 82 | #"""Append elements of the string xs_ to the string s, running all rewrite rules""" 83 | rules = @rules 84 | return s if xs_.length is 0 85 | 86 | xs = xs_.split("") 87 | xs.reverse() 88 | 89 | lengths = @__ruleLengths() 90 | 91 | while xs.length > 0 92 | s = s + xs.pop() 93 | 94 | for suffixLen in lengths 95 | suffix = s.substring(s.length-suffixLen) 96 | #console.log "suf: #{suffix}, len: #{suffixLen}" 97 | rewriteAs = rules[suffix] 98 | if rewriteAs? 99 | #Rewrite found! 100 | #console.log " Rewrite found: #{suffix}, #{rewriteAs}" 101 | s = s.substring(0, s.length - suffixLen) 102 | for i in [rewriteAs.length-1 .. 0] by -1 103 | xs.push rewriteAs[i] 104 | continue 105 | return s 106 | has: (key) -> @rules.hasOwnProperty key 107 | rewrite: ( s )-> @appendRewrite( "", s ) 108 | 109 | 110 | exports.shortLex = shortLex = (s1, s2)-> 111 | #"""Shortlex less or equal comparator""" 112 | if s1.length > s2.length 113 | return false 114 | if s1.length < s2.length 115 | return true 116 | return s1 <= s2 117 | 118 | exports.overlap = overlap = (s1, s2)-> 119 | #"""Two strings: s1, s2. 120 | #Reutnrs x,y,z such as: 121 | #s1 = xy 122 | #s2 = yz 123 | #""" 124 | 125 | if s2.length is 0 126 | return [s1, "", s2] 127 | 128 | [i1, i2] = [0, 0] 129 | #i1, i2: indices in s1, s2 130 | s2_0 = s2[0] 131 | istart = Math.max( 0, s1.length - s2.length ) 132 | for i in [istart ... s1.length] 133 | s1_i = s1[i] 134 | if s1_i is s2_0 135 | #console.log "Comparing #{s1.substring(i+1)} and #{s2.substring(1, s1.length-i)}" 136 | if s1.substring(i+1) is s2.substring(1, s1.length-i) 137 | return [s1.substring(0,i), s1.substring(i), s2.substring(s1.length-i)] 138 | return [s1, "", s2] 139 | 140 | exports.splitBy = splitBy = (s1, s2)-> 141 | #"""Split sequence s1 by sequence s2. 142 | #Returns True and prefix + postfix, or just False and None None 143 | #""" 144 | if s2.length == 0 145 | [true, s1, ""] 146 | 147 | for i in [0...s1.length - s2.length+1] 148 | if s1.substring(i, i+s2.length) is s2 149 | return [true, s1.substring(0,i), s1.substring(i+s2.length)] 150 | return [false, null, null] 151 | 152 | sortPairReverse = ( a, b, lessOrEq )-> 153 | #"""return a1, b1 such that a1 >= b1""" 154 | if lessOrEq(a,b) 155 | [b, a] 156 | else [a,b] 157 | 158 | findOverlap = ( v1, w1, v2, w2 )-> 159 | #"""Find a sequence that is can be rewritten in 2 ways using given rules""" 160 | # if v1=xy and v2=yz 161 | [x, y, z] = overlap(v1, v2) 162 | if y #if there is nonempty overlap 163 | return [true, x+w2, w1+z] 164 | 165 | [hasSplit, x, z] = splitBy(v1, v2) 166 | if hasSplit# and x.length>0 and z.length>0 167 | return [true, w1, x+w2+z] 168 | 169 | return [false, null, null] 170 | 171 | knuthBendixCompletion = (S, lessOrEqual)-> 172 | #"""S :: dict of rewrite rules: (original, rewrite) 173 | #lessorequal :: (x, y) -> boolean 174 | #""" 175 | 176 | SS = S.copy() 177 | # 178 | for [v1, w1] in S.items() 179 | for [v2, w2] in S.items() 180 | # if v1=xy and v2=yz 181 | #[x, y, z] = overlap(v1, v2) 182 | [hasOverlap, s1, s2] = findOverlap(v1,w1, v2,w2) 183 | if hasOverlap 184 | t1 = S.rewrite s1 185 | t2 = S.rewrite s2 186 | if t1 isnt t2 187 | #dprint ("Conflict found", v1, w1, v2, w2) 188 | [t1, t2] = sortPairReverse(t1, t2, lessOrEqual) 189 | #dprint(" add rule:", (t1,t2) ) 190 | SS.add(t1, t2) 191 | return SS 192 | 193 | simplifyRules = (S_, lessOrEqual)-> 194 | S = S_.copy() 195 | Slist = S_.items() #used to iterate 196 | 197 | while Slist.length > 0 198 | [v,w] = vw = Slist.pop() 199 | S.remove(v) 200 | 201 | vv = S.rewrite vw[0] 202 | ww = S.rewrite vw[1] 203 | 204 | addBack = true 205 | if vv is ww 206 | #dprint("Redundant rewrite", v, w) 207 | addBack = false 208 | else 209 | vw1 = sortPairReverse(vv,ww, lessOrEqual) 210 | if vw1[0] isnt vw[0] and vw1[1] isnt vw[1] 211 | #dprint ("Simplify rule:", vw, "->", vw1 ) 212 | S.add( vw1... ) 213 | Slist.push(vw1) 214 | addBack = false 215 | if addBack 216 | S.add(v,w) 217 | return S 218 | 219 | exports.knuthBendix = (S0, lessOrEqual=shortLex, maxIters = 1000, maxRulesetSize = 1000, onIteration=null)-> 220 | #"""Main funciton of the Knuth-Bendix completion algorithm. 221 | #arguments: 222 | #S - original rewrite table 223 | #lessOrEqual - comparator for strings. shortLex is the default one. 224 | #maxIters - maximal number of iterations. If reached, exception is raised. 225 | #maxRulesetSize - maximal number of ruleset. If reached, exception is raised. 226 | #onIteration - callback, called each iteration of the method. It receives iteration number and current table. 227 | #""" 228 | S = S0.normalize(lessOrEqual) 229 | for i in [0...maxIters] 230 | if S.size() > maxRulesetSize 231 | throw new Error("Ruleset grew too big") 232 | SS = simplifyRules(S, lessOrEqual) 233 | SSS = knuthBendixCompletion(SS, lessOrEqual) 234 | if SSS.equals S 235 | #Convergence achieved! 236 | return SSS 237 | if onIteration? 238 | onIteration( i, S ) 239 | S = SSS 240 | throw new Error("Iterations exceeded") 241 | 242 | -------------------------------------------------------------------------------- /src/core/matrix3.coffee: -------------------------------------------------------------------------------- 1 | #Operations on 3x3 matrices 2 | # Matrices stored as arrays, row by row 3 | 4 | exports.eye = eye = -> [1.0, 0.0, 0.0, 0.0, 1.0, 0.0, 0.0, 0.0, 1.0] 5 | 6 | exports.zero = zero = -> [0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0] 7 | exports.set = set = (m,i,j,v) -> 8 | m[i*3+j]=v 9 | return m 10 | 11 | exports.rot = rot = (i,j,angle) -> 12 | m = eye() 13 | s = Math.sin angle 14 | c = Math.cos angle 15 | set m, i, i, c 16 | set m, i, j, -s 17 | set m, j, i, s 18 | set m, j, j, c 19 | return m 20 | 21 | exports.hrot = hrot = (i, j, sinhD) -> 22 | m = eye() 23 | s = sinhD 24 | c = Math.sqrt( sinhD*sinhD + 1 ) 25 | set m, i, i, c 26 | set m, i, j, s 27 | set m, j, i, s 28 | set m, j, j, c 29 | return m 30 | 31 | 32 | exports.mul = mul = (m1, m2) -> 33 | m = zero() 34 | for i in [0...3] 35 | for j in [0...3] 36 | s = 0.0 37 | for k in [0...3] 38 | s += m1[i*3+k] * m2[k*3+j] 39 | m[i*3+j] = s 40 | return m 41 | 42 | 43 | exports.approxEq = approxEq = (m1, m2, eps=1e-6)-> 44 | d = 0.0 45 | for i in [0...9] 46 | d += Math.abs(m1[i] - m2[i]) 47 | return d < eps 48 | 49 | exports.copy = copy = (m) -> m[..] 50 | 51 | exports.mulv = mulv = (m, v) -> 52 | [m[0]*v[0] + m[1]*v[1] + m[2]*v[2], 53 | m[3]*v[0] + m[4]*v[1] + m[5]*v[2], 54 | m[6]*v[0] + m[7]*v[1] + m[8]*v[2]] 55 | 56 | exports.approxEqv = approxEqv = (v1, v2, eps = 1e-6) -> 57 | d = 0.0 58 | for i in [0...3] 59 | d += Math.abs(v1[i] - v2[i]) 60 | return d < eps 61 | 62 | ### 63 | # m: matrix( [m0, m1, m2], [m3,m4,m5], [m6,m7,m8] ); 64 | # ratsimp(invert(m)*determinant(m)); 65 | # determinant( 66 | ### 67 | exports.inv = inv = (m) -> 68 | #Calculated with maxima 69 | iD = 1.0 / (m[0]*(m[4]*m[8]-m[5]*m[7])-m[1]*(m[3]*m[8]-m[5]*m[6])+m[2]*(m[3]*m[7]-m[4]*m[6])) 70 | 71 | [(m[4]*m[8]-m[5]*m[7])*iD,(m[2]*m[7]-m[1]*m[8])*iD,(m[1]*m[5]-m[2]*m[4])*iD,(m[5]*m[6]-m[3]*m[8])*iD,(m[0]*m[8]-m[2]*m[6])*iD,(m[2]*m[3]-m[0]*m[5])*iD,(m[3]*m[7]-m[4]*m[6])*iD,(m[1]*m[6]-m[0]*m[7])*iD,(m[0]*m[4]-m[1]*m[3])*iD] 72 | 73 | exports.smul = smul = (k, m) -> (mi*k for mi in m) 74 | exports.add = add = (m1, m2) -> (m1[i]+m2[i] for i in [0...9]) 75 | exports.addScaledInplace = addScaledInplace = (m, m1, k) -> 76 | for i in [0...m.length] 77 | m[i] += m1[i]*k 78 | return m 79 | exports.transpose = transpose = (m)-> 80 | [m[0], m[3], m[6], 81 | m[1], m[4], m[7], 82 | m[2], m[5], m[8]] 83 | exports.hyperbolicInv = hyperbolicInv = (m) -> 84 | #x' S x = 1, S = diag (-1, -1, 1) 85 | #x' M' S M x = 1 86 | #M' S M = S 87 | #M^-1 = SM'S 88 | [ m[0], m[3], -m[6], 89 | m[1], m[4], -m[7], 90 | -m[2], -m[5], m[8]] 91 | 92 | exports.cleanupHyperbolicMoveMatrix = cleanupHyperbolicMoveMatrix = (m)-> 93 | smul 0.5, add(m, inv hyperbolicInv m) 94 | 95 | exports.translationMatrix = translationMatrix = (dx, dy) -> 96 | #Formulae obtained with Maxima, 97 | # as combination of (inverse rotate) * (shift by x) * (rotate) 98 | # distance is acosh( dx^2 + dy^2 + 1 ) 99 | r2 = dx*dx+dy*dy 100 | dt = Math.sqrt(r2+1) 101 | k = if r2 < 1e-6 then 0.5 else (dt-1)/r2 102 | 103 | xxk = dx*dx*k 104 | xyk = dx*dy*k 105 | yyk = dy*dy*k 106 | 107 | [xxk+1, xyk, dx, 108 | xyk, yyk+1, dy, 109 | dx, dy, dt] 110 | 111 | exports.rotationMatrix = rotationMatrix = (angle) -> 112 | s = Math.sin angle 113 | c = Math.cos angle 114 | [c, s, 0.0, 115 | -s, c, 0.0, 116 | 0.0, 0.0, 1.0] 117 | 118 | exports.amplitude = amplitude = (m) -> Math.max (Math.abs(mi) for mi in m) ... 119 | 120 | 121 | #Decompose hyperbolic matrix to translation and rotation 122 | # returns 3 values: rotation angle, dx, dy 123 | # dx and dy are parameters of the translationMatrix 124 | exports.hyperbolicDecompose = (m)-> 125 | #first, detect translation, look, how far it translates origin 126 | [dx, dy, t] = mulv m, [0,0,1] 127 | 128 | #multiply out the translation 129 | T = translationMatrix -dx, -dy 130 | R = mul T, m 131 | 132 | #now R shoulw be purely rotation matrix 133 | #TODO validate this? 134 | 135 | cos = (R[0]+R[4])*0.5 136 | sin = (R[1]-R[3])*0.5 137 | 138 | [Math.atan2(sin, cos), dx, dy] 139 | 140 | ### array of matrix powers, from 0th to (n-1)th 141 | ### 142 | exports.powers = (matrix, n) -> 143 | #current power 144 | m_n= eye() 145 | 146 | pows = [m_n] 147 | for i in [1...n] 148 | m_n = mul matrix, m_n 149 | pows.push m_n 150 | return pows 151 | -------------------------------------------------------------------------------- /src/core/poincare_view.coffee: -------------------------------------------------------------------------------- 1 | M = require "./matrix3.coffee" 2 | {unity} = require "./vondyck_chain.coffee" 3 | {ChainMap} = require "./chain_map.coffee" 4 | 5 | len2 = (x,y) -> x*x + y*y 6 | 7 | #determine cordinates of the cell, containing given point 8 | exports.makeXYT2path = (tiling, maxSteps=100) -> 9 | cell2point = (cell) -> M.mulv tiling.repr(cell), [0.0,0.0,1.0] 10 | vectorDist = ([x1,y1,t1], [x2,y2,t2]) -> 11 | #actually, this is the correct way: 12 | # Math.acosh t1*t2 - x1*x2 - y1*y2 13 | #however, acosh is costy, and we need only comparisions... 14 | t1*t2 - x1*x2 - y1*y2 - 1 15 | 16 | nearestNeighbor = (cell, xyt) -> 17 | dBest = null 18 | neiBest = null 19 | for nei in tiling.moore cell 20 | dNei = vectorDist cell2point(nei), xyt 21 | if (dBest is null) or (dNei < dBest) 22 | dBest = dNei 23 | neiBest = nei 24 | return [neiBest, dBest] 25 | 26 | return (xyt) -> 27 | #FInally, search 28 | cell = unity #start search at origin 29 | cellDist = vectorDist cell2point(cell), xyt 30 | #Just in case, avoid infinite iteration 31 | step = 0 32 | while step < maxSteps 33 | step += 1 34 | [nextNei, nextNeiDist] = nearestNeighbor cell, xyt 35 | if nextNeiDist > cellDist 36 | break 37 | else 38 | cell = nextNei 39 | cellDist = nextNeiDist 40 | return cell 41 | 42 | #Convert poincare circle coordinates to hyperbolic (x,y,t) representation 43 | exports.poincare2hyperblic = (x,y) -> 44 | # direct conversion: 45 | # x = xh / (th + 1) 46 | # y = yh / (th + 1) 47 | # 48 | # when th^2 - xh^2 - yh^2 == 1 49 | # 50 | # r2 = x^2 + y^2 = (xh^2+yh^2)/(th+1)^2 = (th^2-1)/(th+1)^2 = (th-1)/(th+1) 51 | # 52 | # r2 th + r2 = th - 1 53 | # th (r2-1) = -1-r2 54 | # th = (r2+1)/(1-r2) 55 | r2 = x*x+y*y 56 | if r2 >= 1.0 57 | return null 58 | 59 | th = (r2+1)/(1-r2) 60 | # th + 1 = (r2+1)/(1-r2)+1 = (r2+1+1-r2)/(1-r2) = 2/(1-r2) 61 | return [x * (th+1), y*(th+1), th ] 62 | 63 | 64 | 65 | # Create list of cells, that in Poincare projection are big enough. 66 | exports.visibleNeighborhood = (tiling, minCellSize) -> 67 | #Visible size of the polygon far away 68 | cells = new ChainMap 69 | walk = (cell) -> 70 | return if cells.get(cell) isnt null 71 | cellSize = visiblePolygonSize tiling, tiling.repr cell 72 | cells.put cell, cellSize 73 | if cellSize > minCellSize 74 | for nei in tiling.moore cell 75 | walk nei 76 | return 77 | walk unity 78 | visibleCells = [] 79 | cells.forItems (cell, size)-> 80 | if size >= minCellSize 81 | visibleCells.push cell 82 | return visibleCells 83 | 84 | exports.makeCellShapePoincare = (tiling, cellTransformMatrix, context) -> 85 | pPrev = null 86 | for vertex, i in tiling.cellShape 87 | [x0, y0, t0] = pPrev ? M.mulv(cellTransformMatrix, vertex) 88 | [x1, y1, t1] = pPrev = M.mulv(cellTransformMatrix, tiling.cellShape[(i+1) % tiling.cellShape.length]) 89 | 90 | #poincare coordinates 91 | inv_t0 = 1.0/(t0+1) 92 | xx0 = x0*inv_t0 93 | yy0 = y0*inv_t0 94 | 95 | inv_t1 = 1.0/(t1+1) 96 | xx1 = x1*inv_t1 97 | yy1 = y1*inv_t1 98 | 99 | if i is 0 100 | context.moveTo xx0, yy0 101 | drawPoincareCircleTo context, xx0, yy0, xx1, yy1 102 | #context.closePath() 103 | return 104 | 105 | exports.drawPoincareCircleTo = drawPoincareCircleTo = (context, x0, y0, x1, y1) -> 106 | #Calculate radius of the circular arc. 107 | 108 | sq_l0 = len2( x0, y0 ) 109 | # (x0^2+y0^2)*inv_t0^2 = (t0^2-1)/(t0+1)^2 = (t0-1)/(t0+1) = 1-2/(t0+1) 110 | sq_l1 = len2( x1, y1 ) # = 1-2/(t1+1) 111 | 112 | k0 = (1+1/sq_l0) * 0.5 # = (1+(t0+1)/(t0-1)) * 0.5 = t0/(t0-1) 113 | k1 = (1+1/sq_l1) * 0.5 # = t1/(t1-1) 114 | 115 | delta2 = len2( x0*k0 - x1*k1, y0*k0 - y1*k1 ) 116 | #x0*k0 = x0/(t0+1)*t0/(t0-1) = (x0t0)/(t0^2-1) 117 | 118 | #k_ is not needed anymore 119 | 120 | if delta2 < 1e-4 # 0.01^2 lenght of a path too small, create straight line instead to make it faster. 121 | context.lineTo( x1, y1 ) 122 | return 123 | 124 | cross = (x0*y1 - x1*y0) 125 | r2 = ( sq_l0 * sq_l1 * delta2 ) / (cross*cross) - 1 126 | 127 | r = - Math.sqrt( r2 ) 128 | if cross < 0 129 | r = -r 130 | 131 | if Math.abs(r) < 100 132 | drawBezierApproxArcTo context, x0, y0, x1, y1, r, r<0 133 | else 134 | context.lineTo x1, y1 135 | 136 | exports.drawBezierApproxArcTo = drawBezierApproxArcTo = (context, x0, y0, x1, y1, r, reverse) -> 137 | d2 = len2(x0-x1, y0-y1) 138 | d = Math.sqrt( d2 ) 139 | 140 | ct = Math.sqrt(r*r - d2*0.25) 141 | if reverse 142 | ct = -ct 143 | 144 | #Main formulas for calculating bezier points. Math was used to get them. 145 | r_ct = r-ct 146 | kx = (4.0/3.0)*r_ct/d 147 | ky = -(8.0/3.0)*r*r_ct/d2 + 1.0/6.0 148 | 149 | #Get the bezier control point positions 150 | #vx is a perpendicular vector, vy is parallel 151 | vy_x = x1-x0 152 | vy_y = y1-y0 153 | vx_x = vy_y 154 | vx_y = -vy_x # #rotated by Pi/2 155 | 156 | xc = (x0+x1)*0.5 157 | yc = (y0+y1)*0.5 158 | 159 | p11x = xc + vx_x * kx + vy_x * ky 160 | p11y = yc + vx_y * kx + vy_y * ky 161 | 162 | #p12x = xc + vx_x * kx - vy_x * ky 163 | #p12y = yc + vx_y * kx - vy_y * ky 164 | p12x = xc + vy_y * kx - vy_x * ky 165 | p12y = yc + -vy_x * kx - vy_y * ky 166 | 167 | context.bezierCurveTo p11x, p11y, p12x, p12y, x1, y1 168 | 169 | 170 | exports.hyperbolic2poincare = ([x,y,t], dist) -> 171 | #poincare coordinates 172 | # t**2 - x**2 - y**2 = 1 173 | # 174 | # if scaled, 175 | # s = sqrt(t**2 - x**2 - y**2) 176 | # 177 | # xx = x/s, yy=y/s, tt=t/s, tt+1 = (t+s)/s 178 | # 179 | # xxx = xx/(tt+1) = x/s/(t+s)*s = x/(t+s) 180 | # yyy = y/(t+s) 181 | r2 = x**2+y**2 182 | s2 = t**2-r2 183 | if s2 <=0 184 | its = 1.0/Math.sqrt(r2) 185 | else 186 | its = 1.0/(t+Math.sqrt(s2)) 187 | 188 | if dist 189 | # Length of a vector, might be denormalized 190 | # s2 = t**2 - x**2 - y**2 191 | # s = sqrt(s2) 192 | # 193 | # xx = x/s 194 | # yy = y/s 195 | # tt = t/s 196 | # 197 | # d = acosh tt = acosh t/sqrt(t**2 - x**2 - y**2) 198 | # = log( t/sqrt(t**2 - x**2 - y**2) + sqrt(t**2/(t**2 - x**2 - y**2) - 1) ) = 199 | # = log( (t + sqrt(x**2 + y**2)) / sqrt(t**2 - x**2 - y**2) ) = 200 | # 201 | # = log(t + sqrt(x**2 + y**2)) - 0.5*log(t**2 - x**2 - y**2) 202 | d = if s2 <= 0 203 | Infinity 204 | else 205 | Math.acosh(t/Math.sqrt(s2)) 206 | [x*its, y*its, d] 207 | else 208 | [x*its, y*its] 209 | 210 | exports.visiblePolygonSize = visiblePolygonSize = (tiling, cellTransformMatrix) -> 211 | xmin = xmax = ymin = ymax = 0.0 212 | 213 | for vertex, i in tiling.cellShape 214 | [x, y, t] = M.mulv cellTransformMatrix, vertex 215 | xx = x/t 216 | yy = y/t 217 | if i is 0 218 | xmin = xmax = xx 219 | ymin = ymax = yy 220 | else 221 | xmin = Math.min xmin, xx 222 | xmax = Math.max xmax, xx 223 | 224 | ymin = Math.min ymin, yy 225 | ymax = Math.max ymax, yy 226 | 227 | return Math.max( xmax - xmin, ymax - ymin ) 228 | 229 | -------------------------------------------------------------------------------- /src/core/regular_tiling.coffee: -------------------------------------------------------------------------------- 1 | "use strict" 2 | {VonDyck} = require "./vondyck.coffee" 3 | {ChainMap} = require "./chain_map.coffee" 4 | {unity, reverseShortlexLess} = require "./vondyck_chain.coffee" 5 | M = require "./matrix3.coffee" 6 | 7 | exports.RegularTiling = class RegularTiling extends VonDyck 8 | constructor: (n,m) -> 9 | super(n,m,2) 10 | @solve() 11 | if @representation? 12 | @cellShape = @_generateNGon() 13 | 14 | toString: -> "VonDyck(#{@n}, #{@m}, #{@k})" 15 | 16 | #Convert path to an unique cell identifier by taking a shortest version of all rotated variants 17 | toCell: (chain)-> 18 | eliminateFinalA chain, @appendRewrite, @n 19 | 20 | #Return moore neighbors of a cell 21 | moore: (chain)-> 22 | #reutrns Moore (vertex) neighborhood of the cell. 23 | # it contains N cells of von Neumann neighborhood 24 | # and N*(M-3) cells, sharing single vertex. 25 | # In total, N*(M-2) cells. 26 | neighbors = new Array(@n*(@m-2)) 27 | i = 0 28 | for powerA in [0...@n] by 1 29 | for powerB in [1...@m-1] by 1 30 | #adding truncateA to eliminate final rotation of the chain. 31 | nStep = if powerA 32 | [['b', powerB], ['a', powerA]] 33 | else 34 | [['b', powerB]] 35 | neighbors[i] = @toCell @appendRewrite chain, nStep 36 | i += 1 37 | return neighbors 38 | 39 | #calls a callback fucntion for each cell in the far neighborhood of the original. 40 | # starts from the original cell, and then calls the callback for more and more far cells, encircling it. 41 | # stops when callback returns false. 42 | forFarNeighborhood: (center, callback) -> 43 | cells = new ChainMap 44 | cells.put center, true 45 | #list of cells of the latest complete layer 46 | thisLayer = [center] 47 | #list of cells in the previous complete layer 48 | prevLayer = [] 49 | #Radius of the latest complete layer 50 | radius = 0 51 | 52 | return if not callback center, radius 53 | 54 | while true 55 | #now for each cell in the latest layer, find neighbors, that are not marked yet. 56 | # They would form a new layer. 57 | radius += 1 58 | newLayer = [] 59 | for cell in thisLayer 60 | for neighCell in @moore cell 61 | if not cells.get neighCell 62 | #Detected new unvisited cell - register it and call a callback 63 | newLayer.push neighCell 64 | cells.put neighCell, true 65 | return if not callback neighCell, radius 66 | #new layer complete at this point. 67 | # Now move to the next layer. 68 | # memory optimization: remove from the visited map cells of the prevLayer, since they are not neeed anymore. 69 | # actually, this is quite minor optimization, since cell counts grow exponentially, but I would like to do it. 70 | for cell in prevLayer 71 | if not cells.remove cell 72 | throw new Error("Assertion failed: cell not present") 73 | #rename layers 74 | prevLayer = thisLayer 75 | thisLayer = newLayer 76 | #And loop! 77 | #The loop is only finished by 'return'. 78 | 79 | 80 | # r - radius 81 | # appendRewrite: rewriter for chains. 82 | # n,m - parameters of the tessellation 83 | # Return value: 84 | # list of chains to append 85 | farNeighborhood:(center, r) -> 86 | #map of visited cells 87 | cells = new ChainMap 88 | cells.put center, true 89 | getCellList = (cells) -> 90 | cellList = [] 91 | cells.forItems (cell, state) -> 92 | cellList.push cell 93 | return cellList 94 | 95 | for i in [0...r] by 1 96 | for cell in getCellList cells 97 | for nei in @moore cell 98 | cells.put nei, true 99 | 100 | getCellList cells 101 | 102 | #produces shape (array of 3-vectors) 103 | _generateNGon: -> 104 | #Take center of generator B and rotate it by action of A 105 | 106 | if @k is 2 107 | for i in [0...@n] 108 | M.mulv @representation.aPower(i), @representation.centerB 109 | else 110 | #dead code actually, but interesting for experiments 111 | for i2 in [0...@n*2] 112 | i = (i2/2) | 0 113 | if (i2 % 2) is 0 114 | M.mulv @representation.aPower(i), @representation.centerB 115 | else 116 | M.mulv @representation.aPower(i), @representation.centerAB 117 | 118 | 119 | #Remove last element of a chain, if it is A. 120 | takeLastA = (chain) -> 121 | if (chain is unity) or (chain.letter isnt 'a') 122 | chain 123 | else 124 | chain.t 125 | 126 | # Add all possible rotations powers of A generator) to the end of the chain, 127 | # and choose minimal of all chains (by some ordering). 128 | eliminateFinalA = (chain, appendRewrite, orderA) -> 129 | chain = takeLastA chain 130 | #zero chain is always shortest, return it. 131 | if chain is unity 132 | return chain 133 | #now chain ends with B power, for sure. 134 | #if chain.letter isnt 'b' then throw new Error "two A's in the chain!" 135 | 136 | #bPower = chain.p 137 | 138 | #TODO: only try to append A powers that cause rewriting. 139 | 140 | bestChain = chain 141 | for i in [1...orderA] 142 | chain_i = appendRewrite chain, [['a', i]] 143 | if reverseShortlexLess chain_i, bestChain 144 | bestChain = chain_i 145 | #console.log "EliminateA: got #{chain}, produced #{bestChain}" 146 | return bestChain 147 | -------------------------------------------------------------------------------- /src/core/rule.coffee: -------------------------------------------------------------------------------- 1 | {parseIntChecked} = require "./utils.coffee" 2 | 3 | 4 | class BaseFunc 5 | plus: (x,y) -> x+y 6 | plusInitial: 0 7 | setGeneration: (g)-> 8 | getType: -> throw new Error "Function type undefined" 9 | toGeneric: -> throw new Error "Function type undefined" 10 | evaluate: -> throw new Error "Function type undefined" 11 | changeGrid: (n,m)-> this 12 | 13 | # Generic TF is given by its code. 14 | # Code is a JS object with 3 fields: 15 | # states: N #integer 16 | # sum: (r, x) -> r' #default is (x,y) -> x+y 17 | # sumInitial: value r0 #default is 0 18 | # next: (sum, value) -> value 19 | exports.GenericTransitionFunc = class GenericTransitionFunc extends BaseFunc 20 | constructor: ( @code ) -> 21 | @generation = 0 22 | @_parse() 23 | toString: -> @code 24 | isStable: -> @evaluate(0,0) is 0 25 | setGeneration: (g) -> @generation = g 26 | getType: -> "custom" 27 | _parse: -> 28 | tfObject = eval '('+@code+')' 29 | throw new Error("Numer of states not specified") unless tfObject.states? 30 | throw new Error("Transition function not specified") unless tfObject.next? 31 | 32 | @numStates = tfObject.states 33 | @plus = (tfObject.sum ? ((x,y)->x+y)) 34 | @plusInitial = (tfObject.sumInitial ? 0) 35 | @evaluate = tfObject.next 36 | 37 | throw new Error "Number of states must be 2 or more" if @numStates <= 1 38 | toGeneric: -> this 39 | 40 | #DayNight functions are those, who transform empty field to filled and back. 41 | # They can be effectively simulated as a pair of 2 rules, applying one rule for even generations and another for odd. 42 | 43 | isDayNightRule = (binaryFunc)-> 44 | binaryFunc.evaluate(0,0) == 1 and binaryFunc.evaluate(1, binaryFunc.numNeighbors) == 0 45 | 46 | exports.DayNightTransitionFunc = class DayNightTransitionFunc extends BaseFunc 47 | constructor: (@base) -> 48 | throw new Error("base function is not flashing") if not isDayNightRule @base 49 | @phase = 0 50 | 51 | toString: -> @base.toString() 52 | numStates: 2 53 | getType: -> "binary" 54 | 55 | setGeneration: (g)-> 56 | @phase = g & 1 57 | 58 | isStable: -> 59 | @base.evaluate(0,0) is 1 and @base.evaluate(1,@base.numNeighbors) is 0 60 | 61 | evaluate: (x, s) -> 62 | if @phase 63 | 1 - @base.evaluate(x,s) 64 | else 65 | @base.evaluate(1-x, @base.numNeighbors-s) 66 | toGeneric: -> new GenericTransitionFunc dayNightBinaryTransitionFunc2GenericCode this 67 | changeGrid: (n,m)-> new DayNightTransitionFunc @base.changeGrid n, m 68 | 69 | exports.BinaryTransitionFunc = class BinaryTransitionFunc extends BaseFunc 70 | constructor: ( @n, @m, bornAt, stayAt ) -> 71 | @numNeighbors = @n*(@m-2) 72 | @table = for arr in [bornAt, stayAt] 73 | for s in [0 .. @numNeighbors] by 1 74 | if s in arr then 1 else 0 75 | 76 | isStable: -> table[0][0] is 0 77 | 78 | plus: (x,y) -> x+y 79 | plusInitial: 0 80 | numStates: 2 81 | getType: -> "binary" 82 | 83 | evaluate: (state, sum) -> 84 | throw new Error "Bad state: #{state}" unless state in [0,1] 85 | throw new Error "Bad sum: #{sum}" if sum < 0 or sum > @numNeighbors 86 | @table[state][sum] 87 | 88 | toString: -> 89 | "B " + @_nonzeroIndices(@table[0]).join(" ") + " S " + @_nonzeroIndices(@table[1]).join(" ") 90 | 91 | _nonzeroIndices: (arr)-> (i for x, i in arr when x isnt 0) 92 | toGeneric: -> return new GenericTransitionFunc binaryTransitionFunc2GenericCode this 93 | changeGrid: (n,m)-> 94 | #OK, that's dirty but easy 95 | parseTransitionFunction @toString(), n, m, false 96 | 97 | 98 | # BxxxSxxx 99 | exports.parseTransitionFunction = parseTransitionFunction = (str, n, m, allowDayNight=true) -> 100 | match = str.match /^\s*B([\d\s]+)S([\d\s]+)$/ 101 | throw new Error("Bad function string: #{str}") unless match? 102 | 103 | strings2array = (s)-> 104 | for part in s.split ' ' when part 105 | parseIntChecked part 106 | 107 | bArray = strings2array match[1] 108 | sArray = strings2array match[2] 109 | func = new BinaryTransitionFunc n, m, bArray, sArray 110 | 111 | #If allowed, convert function to day/night rule 112 | if allowDayNight and isDayNightRule func 113 | new DayNightTransitionFunc func 114 | else 115 | func 116 | 117 | 118 | exports.binaryTransitionFunc2GenericCode = binaryTransitionFunc2GenericCode = (binTf) -> 119 | row2condition = (row) -> ("s===#{sum}" for nextValue, sum in row when nextValue).join(" || ") 120 | 121 | conditionBorn = row2condition binTf.table[0] 122 | conditionStay = row2condition binTf.table[1] 123 | 124 | code = ["""//Automatically generated code for binary rule #{binTf} 125 | { 126 | //number of states 127 | 'states': 2, 128 | 129 | //Neighbors sum calculation is default. Code for reference. 130 | //'plus': function(s,x){ return s+x; }, 131 | //'plusInitial': 0, 132 | 133 | //Transition function. Takes current state and sum, returns new state. 134 | //this.generation stores current generation number 135 | 'next': function(x, s){ 136 | if (x===1 && (#{conditionStay})) return 1; 137 | if (x===0 && (#{conditionBorn})) return 1; 138 | return 0; 139 | } 140 | }"""] 141 | 142 | 143 | exports.dayNightBinaryTransitionFunc2GenericCode = dayNightBinaryTransitionFunc2GenericCode = (binTf) -> 144 | row2condition = (row) -> ("s===#{sum}" for nextValue, sum in row when nextValue).join(" || ") 145 | row2conditionInv = (row) -> ("s===#{binTf.base.numNeighbors-sum}" for nextValue, sum in row when nextValue).join(" || ") 146 | 147 | conditionBorn = row2condition binTf.base.table[0] 148 | conditionStay = row2condition binTf.base.table[1] 149 | conditionBornInv = row2conditionInv binTf.base.table[0] 150 | conditionStayInv = row2conditionInv binTf.base.table[1] 151 | 152 | code = ["""//Automatically generated code for population-inverting binary rule #{binTf} 153 | { 154 | //number of states 155 | 'states': 2, 156 | 157 | //Neighbors sum calculation is default. Code for reference. 158 | //'plus': function(s,x){ return s+x; }, 159 | //'plusInitial': 0, 160 | 161 | //Transition function. Takes current state and sum, returns new state. 162 | 'next': function(x, s){ 163 | var phase = this.generation & 1; 164 | 165 | //The original rule #{binTf} inverts state of an empty field 166 | //To calculate it efficiently, we instead invert each odd generation, so that population never goes to infinity. 167 | 168 | 169 | if (phase === 0){ 170 | //On even generations, invert output 171 | if (x===1 && (#{conditionStay})) return 0; 172 | if (x===0 && (#{conditionBorn})) return 0; 173 | return 1 174 | } else { 175 | //On odd generations, invert input state and nighbors sum 176 | if (x===0 && (#{conditionStayInv})) return 1; 177 | if (x===1 && (#{conditionBornInv})) return 1; 178 | return 0; 179 | } 180 | } 181 | }"""] 182 | 183 | -------------------------------------------------------------------------------- /src/core/sample_vd_rewriter.coffee: -------------------------------------------------------------------------------- 1 | # Generates JS code that effectively rewrites 2 | {RewriteRuleset}= require "./knuth_bendix.coffee" 3 | {unity} = require "./vondyck_chain.coffee" 4 | {makeAppendRewrite, groupPowersVd} = require "./vondyck_rewriter.coffee" 5 | 6 | testRewriter = (appendRewrite, sSource, sExpected)-> 7 | gSource = groupPowersVd sSource 8 | gExpected = groupPowersVd sExpected 9 | 10 | reversed = (s)-> 11 | rs = s[..] 12 | rs.reverse() 13 | return rs 14 | 15 | result = appendRewrite unity, reversed(gSource) 16 | expected = unity.appendStack reversed gExpected 17 | 18 | if result.equals expected 19 | console.log("Test #{sSource}->#{sExpected} passed") 20 | else 21 | console.log("Test #{sSource}->#{sExpected} failed") 22 | console.log(" expected result:"+(expected)) 23 | console.log(" received result:"+(result)) 24 | 25 | 26 | 27 | main = -> 28 | table = {'ba': 'AB', 'bB': '', 'BAB': 'a', 'BBB': 'b', 'Bb': '', 'aBB': 'BAb', 'ABA': 'b', 'AAA': 'a', 'Aa': '', 'bAA': 'ABa', 'ab': 'BA', 'aBA': 'AAb', 'bAB': 'BBa', 'bb': 'BB', 'aA': '', 'aa': 'AA'} 29 | s = new RewriteRuleset(table) 30 | 31 | appendRewrite = makeAppendRewrite s 32 | 33 | for [s, t] in s.items() 34 | testRewriter(appendRewrite, s,t) 35 | return 36 | 37 | main() 38 | -------------------------------------------------------------------------------- /src/core/triangle_group_representation.coffee: -------------------------------------------------------------------------------- 1 | M = require "./matrix3.coffee" 2 | {mod} = require "./utils.coffee" 3 | 4 | # exports.TriangleGroup = class TriangleGroup 5 | # constructor: (p,q,r) -> 6 | # [sp,sq,sr] = (Math.cos(Math.PI/n) for n in [p,q,r]) 7 | # @pqr = [p,q,r] 8 | 9 | # m = [-1.0,sp,sr, \ 10 | # sp,-1.0,sq, \ 11 | # sr,sq,-1.0] 12 | # @m = m 13 | 14 | # im = M.add M.smul(2, m), M.eye() 15 | 16 | # sigma = (k) -> 17 | # s = M.zero() 18 | # e = M.eye() 19 | # for i in [0...3] 20 | # for j in [0...3] 21 | # M.set s, i, j, (if i is k then im else e)[i*3+j] 22 | # return s 23 | # @m_pqr = (sigma(i) for i in [0...3]) 24 | # toString = -> 25 | # "Trg(#{@pqr[0]},#{@pqr[1]},#{@pqr[2]})"%self.pqr 26 | 27 | 28 | 29 | ### 30 | # Impoementation of VD groups of order (n, m, 2) 31 | # with 2 generators: a, b 32 | # and rules: a^n = b^m = abab = e 33 | # 34 | # such that `a` has fixed point (0,0,1) 35 | ### 36 | exports.CenteredVonDyck = class CenteredVonDyck 37 | constructor: (n, m, k=2) -> 38 | #a^n = b^m = (abab) = e 39 | {cos, sin, sqrt, PI} = Math 40 | alpha = PI / n 41 | beta = PI / m 42 | gamma = PI / k 43 | 44 | @n = n 45 | @m = m 46 | @k = k 47 | 48 | #Representation of generator A: rotation of the 2N-gon 49 | @a = M.rot 0, 1, (2*alpha) 50 | 51 | #Hyp.cosine of the distance from the center of ne 2N-gon to the order-K vertex. (when K=2, it is the center of the edge of N-gon) 52 | @cosh_x = (cos(beta)+cos(alpha)*cos(gamma))/(sin(alpha)*sin(gamma)) 53 | 54 | #Hyp.cosine of the distance from the center of ne 2N-gon to the order-N vertex. 55 | @cosh_r = (cos(gamma)+cos(alpha)*cos(beta))/(sin(alpha)*sin(beta)) 56 | 57 | if @cosh_r < 1.0 + 1e-10 #treshold 58 | throw new Error("von Dyck group {#{n},#{m},#{k}} is not hyperbolic, representation not supported.") 59 | 60 | @sinh_r = sqrt( @cosh_r**2 - 1 ) 61 | @sinh_x = sqrt( @cosh_x**2 - 1 ) 62 | 63 | #REpresentation of generator B: rotation of the 2N-gon around the vertex of order M. 64 | @b = M.mul( M.mul(M.hrot(0, 2, @sinh_r), M.rot(0, 1, 2*beta)), M.hrot(0, 2, -@sinh_r) ) 65 | 66 | @aPowers = M.powers @a, n 67 | @bPowers = M.powers @b, m 68 | 69 | #Points, that are invariant under generator action. Rotation centers. 70 | @centerA = [0.0,0.0,1.0] 71 | @centerB = [@sinh_r, 0.0, @cosh_r] 72 | @centerAB = [@sinh_x*cos(alpha), @sinh_x*sin(alpha), @cosh_x ] 73 | 74 | aPower: (i) -> @aPowers[ mod i, @n ] 75 | bPower: (i) -> @bPowers[ mod i, @m ] 76 | generatorPower: (g, i)-> 77 | if g is 'a' 78 | @aPower i 79 | else if g is 'b' 80 | @bPower i 81 | else throw new Error "Unknown generator: #{g}" 82 | toString: -> "CenteredVonDyck(#{@n},#{@m},#{@k})" 83 | -------------------------------------------------------------------------------- /src/core/utils.coffee: -------------------------------------------------------------------------------- 1 | "use strict" 2 | exports.formatString = (s, args)-> 3 | s.replace /{(\d+)}/g, (match, number) -> args[number] ? match 4 | 5 | exports.pad = (num, size) -> 6 | s = num+""; 7 | while s.length < size 8 | s = "0" + s 9 | return s 10 | 11 | exports.parseIntChecked = (s)-> 12 | v = parseInt s, 10 13 | throw new Error("Bad number: #{s}") if Number.isNaN v 14 | return v 15 | 16 | exports.parseFloatChecked = (s)-> 17 | v = parseFloat s 18 | throw new Error("Bad number: #{s}") if Number.isNaN v 19 | return v 20 | 21 | #mathematical modulo 22 | exports.mod = (i,n) -> ((i%n)+n)%n 23 | 24 | -------------------------------------------------------------------------------- /src/core/vondyck.coffee: -------------------------------------------------------------------------------- 1 | {makeAppendRewrite, vdRule} = require "./vondyck_rewriter.coffee" 2 | {parseChain, unity} = require "./vondyck_chain.coffee" 3 | {RewriteRuleset, knuthBendix} = require "../core/knuth_bendix.coffee" 4 | {CenteredVonDyck} = require "./triangle_group_representation.coffee" 5 | 6 | #Top-level interface for vonDyck groups. 7 | exports.VonDyck = class VonDyck 8 | constructor: (@n, @m, @k=2)-> 9 | throw new Error "bad N" if @n <= 0 10 | throw new Error "bad M" if @m <= 0 11 | throw new Error "bad K" if @k <= 0 12 | 13 | @unity = unity 14 | 15 | #Matrix representation is only supported for hyperbolic groups at the moment. 16 | @representation = switch @type() 17 | when "hyperbolic" 18 | new CenteredVonDyck @n, @m, @k 19 | when "euclidean" 20 | null 21 | when "spheric" 22 | null 23 | 24 | #Return group type. One of "hyperbolic", "euclidean" or "spheric" 25 | type: -> 26 | #1/n+1/m+1/k ? 1 27 | # 28 | # (nm+nk+mk) ? nmk 29 | num = @n*@m + @n*@k + @m*@k 30 | den = @n*@m*@k 31 | 32 | if num < den 33 | "hyperbolic" 34 | else if num is den 35 | "euclidean" 36 | else 37 | "spheric" 38 | 39 | toString: -> "VonDyck(#{@n}, #{@m}, #{@k})" 40 | 41 | parse: (s) -> parseChain s 42 | 43 | solve: -> 44 | rewriteRuleset = knuthBendix vdRule @n, @m, @k 45 | @appendRewrite = makeAppendRewrite rewriteRuleset 46 | #console.log "Solved group #{@} OK" 47 | 48 | appendRewrite: (chain, stack)-> 49 | throw new Error "Group not solved" 50 | 51 | rewrite: (chain)-> 52 | @appendRewrite @unity, chain.asStack() 53 | 54 | repr: (chain) -> chain.repr @representation 55 | 56 | inverse: (chain) -> @appendInverse unity, chain 57 | 58 | # appends c^-1 to a 59 | appendInverse: (a, c) -> 60 | elementsWithPowers = c.asStack() 61 | elementsWithPowers.reverse() 62 | for e_p in elementsWithPowers 63 | e_p[1] *= -1 64 | @appendRewrite a, elementsWithPowers 65 | 66 | append: (c1, c2) -> 67 | @appendRewrite c1, c2.asStack() 68 | -------------------------------------------------------------------------------- /src/core/vondyck_chain.coffee: -------------------------------------------------------------------------------- 1 | ### Implementation of values of von Dyck groups. 2 | # Each value is a chain of powers of 2 generators: A and B 3 | # 4 | # Example: 5 | # x = a*b*a-1*b^2*a*b-3 6 | # 7 | # vD groups have additional relations for generators: 8 | # a^n === b^m === (ab)^k, 9 | # however this implementation is agnostic about these details. 10 | # They are implemented by the js_rewriter module. 11 | # 12 | # (To this module actually implements free group of 2 generators...) 13 | # 14 | # To facilitate chain appending/truncating, theyt are implemented as a functional data structure. 15 | # Root element is `unity`, it represens identity element of the group. 16 | ### 17 | M = require "./matrix3.coffee" 18 | 19 | exports.Node = class Node 20 | hash: -> 21 | if (h = @h) isnt null 22 | h 23 | else 24 | #seen here: http://werxltd.com/wp/2010/05/13/javascript-implementation-of-javas-string-hashcode-method/ 25 | h = @t.hash() 26 | @h = (((h<<5)-h) + (@letterCode<<7) + @p) | 0 27 | repr: (generatorMatrices) -> 28 | if (m = @mtx) isnt null 29 | m 30 | else 31 | @mtx = M.mul @t.repr(generatorMatrices), generatorMatrices.generatorPower(@letter, @p) 32 | len: -> @l 33 | equals: (c) -> chainEquals(this, c) 34 | a: (pow) -> new NodeA pow, this 35 | b: (pow) -> new NodeB pow, this 36 | toString: -> showChain this 37 | ### Convert chain to array of pairs: [letter, power], where letter is "a" or "b" and power is integer. 38 | # Top element of the chain becomes first element of the array 39 | ### 40 | asStack: -> 41 | result = [] 42 | node = this 43 | while node isnt unity 44 | result.push [node.letter, node.p] 45 | node = node.t 46 | return result 47 | 48 | #Append elements from the array to the chain. 49 | # First element of the array becomes top element of the chain; 50 | # stack itself becomes empty 51 | appendStack: (stack)-> 52 | chain = this 53 | while stack.length > 0 54 | [e, p] = stack.pop() 55 | chain = newNode e, p, chain 56 | return chain 57 | 58 | exports.unity = unity = new Node 59 | unity.l = 0 60 | unity.h = 0 61 | unity.mtx = M.eye() 62 | unity.repr = (g) -> @mtx #jsut overload with a faster code. 63 | 64 | exports.NodeA = class NodeA extends Node 65 | letter: 'a' 66 | letterCode: 0 67 | constructor: (@p, @t)-> 68 | @l = if @t is unity then 1 else @t.l+1 69 | @h = null 70 | @mtx = null #support for calculating matrix representations 71 | 72 | exports.NodeB = class NodeB extends Node 73 | letter: 'b' 74 | letterCode: 1 75 | constructor: (@p, @t)-> 76 | @l = if @t is unity then 1 else @t.l+1 77 | @h = null 78 | @mtx = null 79 | 80 | chainEquals = (a, b) -> 81 | while true 82 | return true if a is b 83 | if a is unity or b is unity 84 | return false #a is E and b is E, but not both 85 | 86 | if (a.letter isnt b.letter) or (a.p isnt b.p) 87 | return false 88 | a = a.t 89 | b = b.t 90 | 91 | 92 | showChain = (node) -> 93 | if node is unity 94 | return 'e' 95 | parts = [] 96 | while node isnt unity 97 | letter = node.letter 98 | power = node.p 99 | if power < 0 100 | letter = letter.toUpperCase() 101 | power = - power 102 | #Adding in reverse order! 103 | if power isnt 1 104 | parts.push "^#{power}" 105 | parts.push letter 106 | node = node.t 107 | return parts.reverse().join '' 108 | 109 | #reverse of showChain 110 | exports.parseChain = (s) -> 111 | return unity if s is '' or s is 'e' 112 | prepend = (tail) -> tail 113 | 114 | updPrepender = (prepender, letter, power) -> (tail) -> 115 | newNode letter, power, prepender tail 116 | 117 | while s 118 | match = s.match /([aAbB])(?:\^(\d+))?/ 119 | throw new Error("Bad syntax: #{s}") unless match 120 | s = s.substr match[0].length 121 | letter = match[1] 122 | power = parseInt (match[2] ? '1'), 10 123 | letterLow = letter.toLowerCase() 124 | if letter isnt letterLow 125 | power = -power 126 | prepend = updPrepender prepend, letterLow, power 127 | prepend unity 128 | 129 | exports.truncateA = truncateA = (chain)-> 130 | while (chain isnt unity) and (chain.letter is "a") 131 | chain = chain.t 132 | return chain 133 | 134 | exports.truncateB = truncateB = (chain)-> 135 | while (chain isnt unity) and (chain.letter is "b") 136 | chain = chain.t 137 | return chain 138 | 139 | 140 | exports.nodeConstructors = nodeConstructors = 141 | a: NodeA 142 | b: NodeB 143 | 144 | exports.newNode = newNode = (letter, power, parent) -> 145 | new nodeConstructors[letter](power, parent) 146 | 147 | ### 148 | # Reverse compare 2 chains by shortlex algorithm 149 | ### 150 | exports.reverseShortlexLess = reverseShortlexLess = (c1, c2) -> 151 | if c1 is unity 152 | return c2 isnt unity 153 | else 154 | #c1 not unity 155 | if c2 is unity 156 | return false 157 | else 158 | #neither is unity 159 | if c1.l isnt c2.l 160 | return c1.l < c2.l 161 | #both are equal length 162 | while c1 isnt unity 163 | if c1.letter isnt c2.letter 164 | return c1.letter < c2.letter 165 | if c1.p isnt c2.p 166 | return c1.p < c2.p 167 | #go upper 168 | c1 = c1.t 169 | c2 = c2.t 170 | #exactly equal 171 | return false 172 | 173 | -------------------------------------------------------------------------------- /src/ext/lzw.coffee: -------------------------------------------------------------------------------- 1 | # LZW-compress a string 2 | exports.lzw_encode = (s) -> 3 | return "" if s is "" 4 | dict = {} 5 | data = s#(s + "").split("") 6 | out = [] 7 | currChar = undefined 8 | phrase = data[0] 9 | code = 256 10 | i = 1 11 | 12 | while i < data.length 13 | currChar = data[i] 14 | if dict[phrase + currChar]? 15 | phrase += currChar 16 | else 17 | out.push String.fromCharCode (if phrase.length > 1 then dict[phrase] else phrase.charCodeAt(0)) 18 | dict[phrase + currChar] = code 19 | code++ 20 | phrase = currChar 21 | i++ 22 | out.push String.fromCharCode (if phrase.length > 1 then dict[phrase] else phrase.charCodeAt(0)) 23 | i = 0 24 | 25 | out.join "" 26 | 27 | # Decompress an LZW-encoded string 28 | exports.lzw_decode = (s) -> 29 | return "" if s is "" 30 | dict = {} 31 | data = s #(s + "").split("") 32 | currChar = data[0] 33 | oldPhrase = currChar 34 | out = [ currChar ] 35 | code = 256 36 | phrase = undefined 37 | i = 1 38 | 39 | while i < data.length 40 | currCode = data[i].charCodeAt(0) 41 | if currCode < 256 42 | phrase = data[i] 43 | else 44 | phrase = (if dict[currCode] then dict[currCode] else (oldPhrase + currChar)) 45 | out.push phrase 46 | currChar = phrase.charAt(0) 47 | dict[code] = oldPhrase + currChar 48 | code++ 49 | oldPhrase = phrase 50 | i++ 51 | out.join "" 52 | -------------------------------------------------------------------------------- /src/ext/polyfills.js: -------------------------------------------------------------------------------- 1 | //source: https://gist.github.com/paulirish/1579671 2 | (function() { 3 | var lastTime = 0; 4 | var vendors = ['ms', 'moz', 'webkit', 'o']; 5 | for(var x = 0; x < vendors.length && !window.requestAnimationFrame; ++x) { 6 | window.requestAnimationFrame = window[vendors[x]+'RequestAnimationFrame']; 7 | window.cancelAnimationFrame = window[vendors[x]+'CancelAnimationFrame'] 8 | || window[vendors[x]+'CancelRequestAnimationFrame']; 9 | } 10 | 11 | if (!window.requestAnimationFrame) 12 | window.requestAnimationFrame = function(callback, element) { 13 | var currTime = new Date().getTime(); 14 | var timeToCall = Math.max(0, 16 - (currTime - lastTime)); 15 | var id = window.setTimeout(function() { callback(currTime + timeToCall); }, 16 | timeToCall); 17 | lastTime = currTime + timeToCall; 18 | return id; 19 | }; 20 | 21 | if (!window.cancelAnimationFrame) 22 | window.cancelAnimationFrame = function(id) { 23 | clearTimeout(id); 24 | }; 25 | }()); 26 | -------------------------------------------------------------------------------- /src/ui/animator.coffee: -------------------------------------------------------------------------------- 1 | "use strict" 2 | #Hyperbolic computations core 3 | M = require "../core/matrix3.coffee" 4 | {decomposeToTranslations} = require "../core/decompose_to_translations.coffee" 5 | 6 | #Misc utilities 7 | {E,flipSetTimeout} = require "./htmlutil.coffee" 8 | {formatString, pad, parseIntChecked} = require "../core/utils.coffee" 9 | 10 | interpolateHyperbolic = (T) -> 11 | [Trot, Tdx, Tdy] = M.hyperbolicDecompose T 12 | #Real distance translated is acosh( sqrt(1+dx^2+dy^2)) 13 | Tr2 = Tdx**2 + Tdy**2 14 | Tdist = Math.acosh Math.sqrt(Tr2+1.0) 15 | Tr = Math.sqrt Tr2 16 | if Tr < 1e-6 17 | dirX = 0.0 18 | dirY = 0.0 19 | else 20 | dirX = Tdx / Tr 21 | dirY = Tdy / Tr 22 | 23 | return (p) -> 24 | rot = Trot * p 25 | dist = Tdist * p 26 | r = Math.sqrt(Math.cosh(dist)**2-1.0) 27 | dx = r*dirX 28 | dy = r*dirY 29 | 30 | M.mul M.translationMatrix(dx, dy), M.rotationMatrix(rot) 31 | exports.Animator = class Animator 32 | constructor: (@application)-> 33 | @oldSize = null 34 | @uploadWorker = null 35 | @busy = false 36 | @reset() 37 | 38 | assertNotBusy: -> 39 | if @busy 40 | throw new Error "Animator is busy" 41 | 42 | reset: -> 43 | @cancelWork() if @busy 44 | @startChain = null 45 | @startOffset = null 46 | @endChain = null 47 | @endOffset = null 48 | @_updateButtons() 49 | 50 | _updateButtons: -> 51 | E('animate-view-start').disabled = @startChain is null 52 | E('animate-view-end').disabled = @endChain is null 53 | E('btn-upload-animation').disabled = (@startChain is null) or (@endChain is null) 54 | E('btn-animate-cancel').style.display = if @busy then '' else 'none' 55 | E('btn-upload-animation').style.display = unless @busy then '' else 'none' 56 | E('btn-animate-derotate').disabled = not (@startChain? and @endChain?) 57 | 58 | 59 | setStart: (observer) -> 60 | @assertNotBusy() 61 | @startChain = observer.getViewCenter() 62 | @startOffset = observer.getViewOffsetMatrix() 63 | @_updateButtons() 64 | 65 | setEnd: (observer) -> 66 | @assertNotBusy() 67 | @endChain = observer.getViewCenter() 68 | @endOffset = observer.getViewOffsetMatrix() 69 | @_updateButtons() 70 | viewStart: (observer) -> 71 | @assertNotBusy() 72 | observer.navigateTo @startChain, @startOffset 73 | viewEnd: (observer) -> 74 | @assertNotBusy() 75 | observer.navigateTo @endChain, @endOffset 76 | 77 | derotate: -> 78 | console.log "offset matrix:" 79 | console.dir @offsetMatrix() 80 | [t1, t2] = decomposeToTranslations @offsetMatrix() 81 | if t1 is null 82 | alert "Derotation not possible" 83 | return 84 | #@endOffset * Mdelta * @startOffset^-1 = t1^-1 * t2 * t1 85 | @endOffset = M.mul t1, @endOffset 86 | @startOffset = M.mul t1, @startOffset 87 | 88 | #and now apply similarity rotation to both of the start and end points so that t2 is strictly vertical 89 | [dx,dy,_] = M.mulv t2, [0,0,1] 90 | r = Math.sqrt(dx**2+dy**2) 91 | 92 | if r > 1e-6 93 | s = dy/r 94 | c = dx/r 95 | R = [c, s, 0.0, 96 | -s, c, 0.0, 97 | 0.0, 0.0, 1.0] 98 | R = M.mul R, M.rotationMatrix(-Math.PI/2) 99 | @endOffset = M.mul R, @endOffset 100 | @startOffset = M.mul R, @startOffset 101 | 102 | alert "Start and end point adjusted." 103 | 104 | _setCanvasSize: -> 105 | size = parseIntChecked E('animate-size').value 106 | if size <=0 or size >= 65536 107 | throw new Error("Size #{size} is inappropriate") 108 | 109 | @application.setCanvasResize true 110 | canvas = @application.getCanvas() 111 | @oldSize = [canvas.width, canvas.height] 112 | canvas.width = canvas.height = size 113 | 114 | _restoreCanvasSize: -> 115 | throw new Error("restore withou set") unless @oldSize 116 | canvas = @application.getCanvas() 117 | [canvas.width, canvas.height] = @oldSize 118 | @oldSize = null 119 | @application.setCanvasResize false 120 | @application.redraw() 121 | 122 | _beginWork: -> 123 | @busy = true 124 | @_setCanvasSize() 125 | @_updateButtons() 126 | console.log "Started animation" 127 | 128 | _endWork: -> 129 | @_restoreCanvasSize() 130 | console.log "End animation" 131 | @busy = false 132 | @_updateButtons() 133 | 134 | cancelWork: -> 135 | return unless @busy 136 | clearTimeout @uploadWorker if @uploadWorker 137 | @uploadWorker = null 138 | @_endWork() 139 | 140 | #matrix between first and last points 141 | offsetMatrix: -> 142 | #global (surreally big) view matrix is: 143 | # 144 | # Moffset * M(chain) 145 | # 146 | # where Moffset is view offset, and M(chain) is transformation matrix of the chain. 147 | # We need to find matrix T such that 148 | # 149 | # T * MoffsetStart * M(chainStart) = MoffsetEnd * M(chainEnd) 150 | # 151 | # Solvign this, get: 152 | # T = MoffsetEnd * (M(chainEnd) * M(chainStart)^-1) * MoffsetStart^-1 153 | # 154 | # T = MoffsetEnd * M(chainEnd + invChain(chainStart) * MoffsetStart^-1 155 | tiling = @application.tiling 156 | 157 | #Not very sure but lets try 158 | #Mdelta = tiling.repr tiling.appendInverse(@endChain, @startChain) 159 | inv = (c) -> tiling.inverse c 160 | app = (c1, c2) -> tiling.append c1,c2 161 | 162 | # e, S bad 163 | # S, e bad 164 | # 165 | # E, s good? Seems to be good, but power calculation is wrong. 166 | Mdelta = tiling.repr app(inv(@endChain), @startChain) 167 | 168 | T = M.mul(M.mul(@endOffset, Mdelta), M.hyperbolicInv(@startOffset)) 169 | return T 170 | 171 | animate: (observer, stepsPerGen, generations, callback)-> 172 | return unless @startChain? and @endChain? 173 | @assertNotBusy() 174 | 175 | T = @offsetMatrix() 176 | 177 | #Make interpolator for this matrix 178 | Tinterp = interpolateHyperbolic M.hyperbolicInv T 179 | 180 | index = 0 181 | totalSteps = generations * stepsPerGen 182 | framesBeforeGeneration = stepsPerGen 183 | 184 | imageNameTemplate = E('upload-name').value 185 | @_beginWork() 186 | uploadStep = => 187 | @uploadWorker = null 188 | #If we were cancelled - return quickly 189 | return unless @busy 190 | @application.getObserver().navigateTo @startChain, @startOffset 191 | p = index / totalSteps 192 | @application.getObserver().modifyView M.hyperbolicInv Tinterp(p) 193 | @application.drawEverything() 194 | 195 | imageName = formatString imageNameTemplate, [pad(index,4)] 196 | @application.uploadToServer imageName, (ajax)=> 197 | #if we were cancelled, return quickly 198 | return unless @busy 199 | if ajax.readyState is XMLHttpRequest.DONE and ajax.status is 200 200 | console.log "Upload success" 201 | index +=1 202 | framesBeforeGeneration -= 1 203 | if framesBeforeGeneration is 0 204 | @application.doStep() 205 | framesBeforeGeneration = stepsPerGen 206 | 207 | if index <= totalSteps 208 | console.log "request next frame" 209 | @uploadWorker = flipSetTimeout 50, uploadStep 210 | else 211 | @_endWork() 212 | else 213 | console.log "Upload failure, cancel" 214 | console.log ajax.responseText 215 | @_endWork() 216 | 217 | uploadStep() 218 | -------------------------------------------------------------------------------- /src/ui/canvas_util.coffee: -------------------------------------------------------------------------------- 1 | #taken from http://www.html5canvastutorials.com/advanced/html5-canvas-mouse-coordinates/ 2 | exports.getCanvasCursorPosition = (e, canvas) -> 3 | if e.type is "touchmove" or e.type is "touchstart" or e.type is "touchend" 4 | e=e.touches[0] 5 | if e.clientX? 6 | rect = canvas.getBoundingClientRect() 7 | return [e.clientX - rect.left, e.clientY - rect.top] 8 | -------------------------------------------------------------------------------- /src/ui/context_delegate.coffee: -------------------------------------------------------------------------------- 1 | exports.ContextDelegate = class ContextDelegate 2 | constructor: -> 3 | @commands = [] 4 | 5 | moveTo: (x,y) -> @commands.push 1, x, y 6 | lineTo: (x,y) -> @commands.push 2, x, y 7 | bezierCurveTo: (x1,y1,x2,y2,x3,y3) -> @commands.push 3, x1,y1,x2,y2,x3,y3 8 | closePath: -> @commands.push 4 9 | reset: -> @commands = [] 10 | take: -> 11 | c = @commands 12 | @commands = [] 13 | return c 14 | 15 | exports.runCommands = runCommands = (context, cs) -> 16 | i = 0 17 | n = cs.length 18 | while i < n 19 | switch cs[i] 20 | when 1 21 | context.moveTo cs[i+1], cs[i+2] 22 | i += 3 23 | when 2 24 | context.lineTo cs[i+1], cs[i+2] 25 | i += 3 26 | when 3 27 | context.bezierCurveTo cs[i+1], cs[i+2], cs[i+3], cs[i+4], cs[i+5], cs[i+6] 28 | i += 7 29 | when 4 30 | context.closePath() 31 | i += 1 32 | else 33 | throw new Error "Unnown drawing command #{cs[i]}" 34 | return 35 | -------------------------------------------------------------------------------- /src/ui/dom_builder.coffee: -------------------------------------------------------------------------------- 1 | ######### 2 | # Let's make a bicycle! 3 | # 4 | exports.DomBuilder = class DomBuilder 5 | constructor: ( tag=null ) -> 6 | @root = root = 7 | if tag is null 8 | document.createDocumentFragment() 9 | else 10 | document.createElement tag 11 | @current = @root 12 | @vars = {} 13 | 14 | tag: (name) -> 15 | @current.appendChild e=document.createElement name 16 | @current = e 17 | this 18 | store: (varname) -> 19 | @vars[varname] = @current 20 | this 21 | rtag: (var_name, name) -> 22 | @tag name 23 | @store var_name 24 | end: -> 25 | @current = cur = @current.parentNode 26 | throw new Error "Too many end()'s" if cur is null 27 | this 28 | 29 | text: (txt) -> 30 | @current.appendChild document.createTextNode txt 31 | this 32 | 33 | a: (name, value) -> 34 | @current.setAttribute name, value 35 | this 36 | 37 | append: (elementReference) -> 38 | @current.appendChild elementReference 39 | this 40 | 41 | DIV: -> @tag "div" 42 | A: -> @tag "a" 43 | SPAN: -> @tag "span" 44 | 45 | ID: (id) -> @a "id", id 46 | CLASS: (cls) -> @a "class", cls 47 | 48 | finalize: -> 49 | r = @root 50 | @root = @current = @vars = null 51 | r 52 | #Usage: 53 | # dom = new DimBuilder 54 | # dom.tag("div").a("id", "my-div").a("class","toolbar").end() 55 | # 56 | -------------------------------------------------------------------------------- /src/ui/ghost_click_detector.coffee: -------------------------------------------------------------------------------- 1 | 2 | ### 3 | # In some mobile browsers, ghost clicks can not be prevented. So here easy solution: every mouse event, 4 | # coming after some interval after a touch event is ghost 5 | ### 6 | exports.GhostClickDetector = class GhostClickDetector 7 | constructor: -> 8 | @isGhost = false 9 | @timerHandle = null 10 | @ghostInterval = 1000 #ms 11 | #Bound functions 12 | @_onTimer = => 13 | @isGhost=false 14 | @timerHandle=null 15 | @_onTouch = => 16 | @onTouch() 17 | onTouch: -> 18 | @stopTimer() 19 | @isGhost = true 20 | @timerHandle = window.setTimeout @_onTimer, @ghostInterval 21 | 22 | stopTimer: -> 23 | if (handle = @timerHandle) 24 | window.clearTimeout handle 25 | @timerHandle = null 26 | addListeners: (element)-> 27 | for evtName in ["touchstart", "touchend"] 28 | element.addEventListener evtName, @_onTouch, false 29 | 30 | -------------------------------------------------------------------------------- /src/ui/htmlutil.coffee: -------------------------------------------------------------------------------- 1 | #I am learning JS and want to implement this functionality by hand 2 | 3 | exports.flipSetTimeout = (t, cb) -> setTimeout cb, t 4 | 5 | exports.E = E = (id) -> document.getElementById id 6 | 7 | # Remove class from the element 8 | exports.removeClass = removeClass = (e, c) -> 9 | e.className = (ci for ci in e.className.split " " when c isnt ci).join " " 10 | 11 | exports.addClass = addClass = (e, c) -> 12 | e.className = 13 | if (classes = e.className) is "" 14 | c 15 | else 16 | classes + " " + c 17 | 18 | idOrNull = (elem)-> 19 | if elem is null 20 | null 21 | else 22 | elem.getAttribute "id" 23 | 24 | exports.ButtonGroup = class ButtonGroup 25 | constructor: (containerElem, tag, selectedId=null, @selectedClass="btn-selected")-> 26 | if selectedId isnt null 27 | addClass (@selected = E selectedId), @selectedClass 28 | else 29 | @selected = null 30 | 31 | @handlers = change: [] 32 | for btn in containerElem.getElementsByTagName tag 33 | btn.addEventListener "click", @_btnClickListener btn 34 | return 35 | 36 | _changeActiveButton: (newBtn, e)-> 37 | newId = idOrNull newBtn 38 | oldBtn = @selected 39 | oldId = idOrNull oldBtn 40 | if newId isnt oldId 41 | if oldBtn isnt null then removeClass oldBtn, @selectedClass 42 | if newBtn isnt null then addClass newBtn, @selectedClass 43 | @selected = newBtn 44 | for handler in @handlers.change 45 | handler( e, newId, oldId ) 46 | return 47 | 48 | _btnClickListener: (newBtn) -> (e) => @_changeActiveButton newBtn, e 49 | 50 | addEventListener: (name, handler)-> 51 | unless (handlers = @handlers[name])? 52 | throw new Error "Hander #{name} is not supported" 53 | handlers.push handler 54 | 55 | setButton: (newId) -> 56 | if newId is null 57 | @_changeActiveButton null, null 58 | else 59 | @_changeActiveButton document.getElementById(newId), null 60 | 61 | exports.windowWidth = -> 62 | #http://stackoverflow.com/questions/3437786/get-the-size-of-the-screen-current-web-page-and-browser-window 63 | window.innerWidth \ 64 | || document.documentElement.clientWidth\ 65 | || document.body.clientWidth 66 | exports.windowHeight = -> 67 | window.innerHeight \ 68 | || document.documentElement.clientHeight\ 69 | || document.body.clientHeight 70 | 71 | exports.documentWidth = -> 72 | document.documentElement.scrollWidth\ 73 | || document.body.scrollWidth 74 | 75 | 76 | 77 | if not HTMLCanvasElement.prototype.toBlob? 78 | Object.defineProperty HTMLCanvasElement.prototype, 'toBlob', { 79 | value: (callback, type, quality) -> 80 | binStr = atob @toDataURL(type, quality).split(',')[1] 81 | len = binStr.length 82 | arr = new Uint8Array(len) 83 | for i in [0...len] by 1 84 | arr[i] = binStr.charCodeAt(i) 85 | callback new Blob [arr], {type: type || 'image/png'} 86 | } 87 | 88 | 89 | exports.Debouncer = class Debouncer 90 | constructor: (@timeout, @callback) -> 91 | @timer = null 92 | fire: -> 93 | if @timer 94 | clearTimeout @timer 95 | @timer = setTimeout (=>@onTimer()), @timeout 96 | onTimer: -> 97 | @timer = null 98 | @callback() 99 | 100 | 101 | exports.getAjax = -> 102 | if window.XMLHttpRequest? 103 | return new XMLHttpRequest() 104 | else if window.ActiveXObject? 105 | return new ActiveXObject("Microsoft.XMLHTTP") 106 | 107 | exports.ValidatingInput = class ValidatingInput 108 | constructor: (@element, @parseValue, @stringifyValue, value, @stateStyleClasses={ok: "input-ok", error: "input-bad", modified: "input-editing"})-> 109 | @message=null 110 | if value? 111 | @setValue value 112 | else 113 | @_modified() 114 | @onparsed = null 115 | 116 | @element.addEventListener "reset", (e)=> 117 | console.log "reset" 118 | @_reset() 119 | 120 | @element.addEventListener "keydown", (e)=> 121 | if e.keyCode==27 122 | console.log "Esc" 123 | e.preventDefault() 124 | @_reset() 125 | 126 | @element.addEventListener "change", (e)=> 127 | console.log "changed" 128 | @_modified() 129 | 130 | @element.addEventListener "blur", (e)=> 131 | console.log "blur" 132 | @_exit() 133 | 134 | @element.addEventListener "input", (e)=> 135 | console.log "input" 136 | @_editing() 137 | 138 | setValue: (val)-> 139 | @value = val 140 | newText = @stringifyValue val 141 | @element.value = newText 142 | @_setClass @stateStyleClasses.ok 143 | 144 | revalidate: -> @_parse() 145 | 146 | _reset: -> 147 | @setValue @value 148 | 149 | _exit: -> 150 | if @message? 151 | @_reset() 152 | 153 | _editing: -> 154 | @_setMessage null 155 | @_setClass @stateStyleClasses.modified 156 | 157 | _setMessage: (msg)-> 158 | if msg? 159 | console.log msg 160 | @message = msg 161 | 162 | _setClass: (cls) -> 163 | removeClass @element, @stateStyleClasses.ok 164 | removeClass @element, @stateStyleClasses.error 165 | removeClass @element, @stateStyleClasses.modified 166 | 167 | addClass @element, cls 168 | 169 | _parse: -> 170 | try 171 | newVal = @parseValue @element.value 172 | if newVal? 173 | @value = newVal 174 | else 175 | throw new Error "parse function returned no value" 176 | 177 | @_setMessage null 178 | @_setClass @stateStyleClasses.ok 179 | return true 180 | catch e 181 | @_setMessage "Failed to parse value: #{e}" 182 | @_setClass @stateStyleClasses.error 183 | return false 184 | 185 | _modified: -> 186 | if @_parse() 187 | @onparsed? @value 188 | -------------------------------------------------------------------------------- /src/ui/mousetool.coffee: -------------------------------------------------------------------------------- 1 | M = require "../core/matrix3.coffee" 2 | {getCanvasCursorPosition} = require "./canvas_util.coffee" 3 | {Debouncer} = require "./htmlutil.coffee" 4 | 5 | exports.MouseTool = class MouseTool 6 | constructor: (@application) -> 7 | mouseMoved: -> 8 | mouseUp: -> 9 | mouseDown: -> 10 | 11 | moveView: (dx, dy) -> 12 | @application.getObserver().modifyView M.translationMatrix(dx, dy) 13 | rotateView: (angle) -> 14 | @application.getObserver().modifyView M.rotationMatrix angle 15 | 16 | 17 | 18 | exports.MouseToolCombo = class MouseToolCombo extends MouseTool 19 | constructor: (application, x0, y0) -> 20 | super application 21 | [@x0, @y0] = @application.canvas2relative x0, y0 22 | @angle0 = Math.atan2 @x0, @y0 23 | mouseMoved: (e)-> 24 | canvas = @application.getCanvas() 25 | [x, y] = @application.canvas2relative getCanvasCursorPosition(e, canvas)... 26 | dx = x - @x0 27 | dy = y - @y0 28 | 29 | @x0 = x 30 | @y0 = y 31 | 32 | newAngle = Math.atan2 x, y 33 | dAngle = newAngle - @angle0 34 | #Wrap angle increment into -PI ... PI diapason. 35 | if dAngle > Math.PI 36 | dAngle = dAngle - Math.PI*2 37 | else if dAngle < -Math.PI 38 | dAngle = dAngle + Math.PI*2 39 | @angle0 = newAngle 40 | 41 | #determine mixing ratio 42 | r2 = x**2 + y**2 43 | #pure rotation at the edge, 44 | #pure pan at the center 45 | q = Math.min(1.0, r2**1.5) 46 | 47 | mv = M.translationMatrix(dx*(1-q) , dy*(1-q)) 48 | rt = M.rotationMatrix dAngle*q 49 | @application.getObserver().modifyView M.mul(M.mul(mv,rt),mv) 50 | 51 | ### 52 | exports.MouseToolPan = class MouseToolPan extends MouseTool 53 | constructor: (application, @x0, @y0) -> 54 | super application 55 | @panEventDebouncer = new Debouncer 1000, => 56 | @application.getObserver.rebaseView() 57 | 58 | mouseMoved: (e)-> 59 | canvas = @application.getCanvas() 60 | [x, y] = getCanvasCursorPosition e, canvas 61 | dx = x - @x0 62 | dy = y - @y0 63 | 64 | @x0 = x 65 | @y0 = y 66 | k = 2.0 / canvas.height 67 | xc = (x - canvas.width*0.5)*k 68 | yc = (y - canvas.height*0.5)*k 69 | 70 | r2 = xc*xc + yc*yc 71 | s = 2 / Math.max(0.3, 1-r2) 72 | 73 | @moveView dx*k*s , dy*k*s 74 | @panEventDebouncer.fire() 75 | 76 | exports.MouseToolRotate = class MouseToolRotate extends MouseTool 77 | constructor: (application, x, y) -> 78 | super application 79 | canvas = @application.getCanvas() 80 | @xc = canvas.width * 0.5 81 | @yc = canvas.width * 0.5 82 | @angle0 = @angle x, y 83 | 84 | angle: (x,y) -> Math.atan2( x-@xc, y-@yc) 85 | 86 | mouseMoved: (e)-> 87 | canvas = @application.getCanvas() 88 | [x, y] = getCanvasCursorPosition e, canvas 89 | newAngle = @angle x, y 90 | dAngle = newAngle - @angle0 91 | @angle0 = newAngle 92 | @rotateView dAngle 93 | 94 | ### 95 | -------------------------------------------------------------------------------- /src/ui/navigator.coffee: -------------------------------------------------------------------------------- 1 | #search for cell clusters and navigate through them 2 | {allClusters} = require "../core/field.coffee" 3 | 4 | {DomBuilder} = require "./dom_builder.coffee" 5 | {E} = require "./htmlutil.coffee" 6 | 7 | exports.Navigator = class Navigator 8 | constructor: (@application, navigatorElemId="navigator-cluster-list", btnClearId="btn-nav-clear") -> 9 | @clustersElem = E navigatorElemId 10 | @btnClear = E btnClearId 11 | @clusters = [] 12 | @btnClear.style.display = 'none' 13 | 14 | search: (field)-> 15 | #field is ChainMap 16 | @clusters = allClusters field, @application.tiling 17 | @sortByDistance() 18 | @updateClusterList() 19 | @btnClear.style.display = if @clusters then '' else 'none' 20 | return @clusters.length 21 | 22 | sortByDistance: -> 23 | @clusters.sort (a, b) -> 24 | d = b[0].len() - a[0].len() 25 | return d if d isnt 0 26 | d = b.length - a.length 27 | return d 28 | 29 | sortBySize: -> 30 | @clusters.sort (a, b) -> 31 | d = b.length - a.length 32 | return d if d isnt 0 33 | d = b[0].len() - a[0].len() 34 | return d 35 | 36 | makeNavigateTo: (chain) -> (e) => 37 | e.preventDefault() 38 | #console.log JSON.stringify chain 39 | observer = @application.getObserver() 40 | if observer? 41 | observer.navigateTo chain 42 | return 43 | 44 | navigateToResult: (index) -> 45 | observer = @application.getObserver() 46 | if observer? 47 | observer.navigateTo @clusters[index][0] 48 | 49 | clear: -> 50 | @clusters = [] 51 | @clustersElem.innerHTML = "" 52 | @btnClear.style.display = 'none' 53 | 54 | updateClusterList: -> 55 | dom = new DomBuilder 56 | 57 | dom.tag("table") 58 | .tag("thead") 59 | .tag('tr') 60 | .tag('th').rtag('ssort').a("href","#sort-size").text('Cells').end().end() 61 | .tag('th').rtag('dsort').a("href","#sort-dist").text('Distance').end().end() 62 | .end() 63 | .end() 64 | 65 | dom.vars.ssort.addEventListener 'click', (e)=> 66 | e.preventDefault() 67 | @sortBySize() 68 | @updateClusterList() 69 | dom.vars.dsort.addEventListener 'click', (e)=> 70 | e.preventDefault() 71 | @sortByDistance() 72 | @updateClusterList() 73 | 74 | dom.tag "tbody" 75 | for cluster, idx in @clusters 76 | size = cluster.length 77 | dist = cluster[0].len() 78 | 79 | dom.tag("tr") 80 | .tag("td") 81 | .rtag("navtag", "a").a("href", "#nav-cluster#{idx}").text("#{size}").end() 82 | .end() 83 | .tag('td') 84 | .rtag("navtag1", "a").a("href", "#nav-cluster#{idx}").text("#{dist}").end() 85 | .end() 86 | .end() 87 | 88 | listener = @makeNavigateTo cluster[0] 89 | dom.vars.navtag.addEventListener "click", listener 90 | dom.vars.navtag1.addEventListener "click", listener 91 | 92 | dom.end() 93 | 94 | @clustersElem.innerHTML = "" 95 | @clustersElem.appendChild dom.finalize() 96 | 97 | -------------------------------------------------------------------------------- /src/ui/observer.coffee: -------------------------------------------------------------------------------- 1 | "use strict"; 2 | {unity} = require "../core/vondyck_chain.coffee" 3 | {makeXYT2path, poincare2hyperblic, hyperbolic2poincare, visibleNeighborhood, makeCellShapePoincare} = require "../core/poincare_view.coffee" 4 | #{eliminateFinalA} = require "../core/vondyck_rewriter.coffee" 5 | M = require "../core/matrix3.coffee" 6 | 7 | 8 | exports.FieldObserver = class FieldObserver 9 | constructor: (@tiling, @minCellSize=1.0/400.0, center = unity, @tfm = M.eye())-> 10 | 11 | @cells = null 12 | @center = null 13 | cells = visibleNeighborhood @tiling, @minCellSize 14 | @cellOffsets = (c.asStack() for c in cells) 15 | @isDrawingHomePtr = true 16 | @isDrawingLiveBorders = true 17 | @colorHomePtr = 'rgba(255,100,100,0.7)' 18 | @colorEmptyBorder = 'rgb(128,128,128)' 19 | @colorLiveBorder = 'rgb(192,192,192)' 20 | 21 | if center isnt unity 22 | @rebuildAt center 23 | else 24 | @cells = cells 25 | @center = center 26 | 27 | @cellTransforms = (@tiling.repr(c) for c in cells) 28 | @drawEmpty = true 29 | @jumpLimit = 1.5 30 | 31 | 32 | @viewUpdates = 0 33 | #precision falls from 1e-16 to 1e-9 in 1000 steps. 34 | @maxViewUpdatesBeforeCleanup = 50 35 | @xyt2path = makeXYT2path @tiling 36 | @pattern = ["red", "black", "green", "blue", "yellow", "cyan", "magenta", "gray", "orange"] 37 | 38 | @onFinish = null 39 | 40 | getHomePtrPos: -> 41 | xyt = [0.0,0.0,1.0] 42 | #@mtx = M.mul @t.repr(generatorMatrices), generatorMatrices.generatorPower(@letter, @p) 43 | # xyt = genPow(head.letter, -head.p) * ... * xyt0 44 | # 45 | # reference formula. 46 | # #xyt = M.mulv M.hyperbolicInv(@center.repr(@tiling)), xyt 47 | # it works, but matrix values can become too large. 48 | # 49 | stack = @center.asStack() 50 | #apply inverse transformations in reverse order 51 | for [letter, p] in stack by -1 52 | xyt = M.mulv @tiling.representation.generatorPower(letter, -p), xyt 53 | #Denormalize coordinates to avoid extremely large values. 54 | invT = 1.0/xyt[2] 55 | xyt[0] *= invT 56 | xyt[1] *= invT 57 | xyt[2] = 1.0 58 | #Finally add view transform 59 | xyt = M.mulv @tfm, xyt 60 | #(denormalizetion not required, view transform is not large) 61 | # And map to poincare circle 62 | hyperbolic2poincare xyt #get distance too 63 | 64 | getColorForState: (state) -> 65 | @pattern[ (state % @pattern.length + @pattern.length) % @pattern.length ] 66 | 67 | getViewCenter: ->@center 68 | getViewOffsetMatrix: ->@tfm 69 | setViewOffsetMatrix: (m) -> 70 | @tfm = m 71 | @renderGrid @tfm 72 | rebuildAt: (newCenter) -> 73 | @center = newCenter 74 | @cells = for offset in @cellOffsets 75 | #it is important to make copy since AR empties the array! 76 | @tiling.toCell @tiling.appendRewrite(newCenter, offset[..]) 77 | @_observedCellsChanged() 78 | return 79 | 80 | navigateTo: (chain, offsetMatrix=M.eye()) -> 81 | console.log "navigated to #{chain}" 82 | @rebuildAt chain 83 | @tfm = offsetMatrix 84 | @renderGrid @tfm 85 | return 86 | 87 | _observedCellsChanged: -> 88 | 89 | translateBy: (appendArray) -> 90 | #console.log "New center at #{ newCenter}" 91 | @rebuildAt @tiling.appendRewrite @center, appendArray 92 | 93 | canDraw: -> true 94 | 95 | draw: (cells, context, scale) -> 96 | context.scale scale, scale 97 | context.lineWidth = 1.0/scale 98 | #first borders 99 | #cells grouped by state 100 | state2cellIndexList = {} 101 | 102 | for cell, i in @cells 103 | state = cells.get(cell) ? 0 104 | if (state isnt 0) or @drawEmpty 105 | stateCells = state2cellIndexList[state] 106 | unless stateCells? 107 | state2cellIndexList[state] = stateCells = [] 108 | stateCells.push i 109 | 110 | for strState, cellIndices of state2cellIndexList 111 | state = parseInt strState, 10 112 | #console.log "Group: #{state}, #{JSON.stringify cellIndices}" 113 | 114 | context.beginPath() 115 | for cellIndex in cellIndices 116 | cellTfm = @cellTransforms[cellIndex] 117 | mtx = M.mul @tfm, cellTfm 118 | makeCellShapePoincare @tiling, mtx, context 119 | 120 | if state is 0 121 | context.strokeStyle = @colorEmptyBorder 122 | context.stroke() 123 | else 124 | context.fillStyle = @getColorForState state 125 | context.fill() 126 | if @isDrawingLiveBorders 127 | context.strokeStyle = @colorLiveBorder 128 | context.stroke() 129 | if @isDrawingHomePtr 130 | @drawHomePointer context 131 | #true because immediate-mode observer always finishes drawing. 132 | return true 133 | 134 | drawHomePointer: (context, size)-> 135 | size = 0.06 136 | [x,y,d] = @getHomePtrPos() 137 | angle = Math.PI - Math.atan2 x, y 138 | 139 | context.save() 140 | context.translate x,y 141 | context.scale size, size 142 | context.rotate angle 143 | 144 | context.fillStyle = @colorHomePtr 145 | context.beginPath() 146 | 147 | context.moveTo 0,0 148 | 149 | context.bezierCurveTo 0.4,-0.8, 1,-1, 1,-2 150 | context.bezierCurveTo 1,-2.6, 0.6,-3, 0,-3 151 | context.bezierCurveTo -0.6,-3, -1,-2.6, -1,-2 152 | context.bezierCurveTo -1,-1, -0.4, -0.8, 0,0 153 | 154 | context.closePath() 155 | context.fill() 156 | 157 | # context.translate 0, -1 158 | 159 | # context.rotate -angle 160 | # context.translate 0, -1 161 | # context.font = "12px sans" 162 | 163 | # context.fillStyle = 'rgba(255,100,100,1.0)' 164 | # context.textAlign = "center" 165 | # context.scale 0.09, 0.09 166 | # context.fillText("#{Math.round(d*10)/10}", 0, 0); 167 | 168 | context.restore() 169 | 170 | visibleCells: (cells) -> 171 | for cell in @cells when (value=cells.get(cell)) isnt null 172 | [cell, value] 173 | 174 | checkViewMatrix: -> 175 | #me = [-1,0,0, 0,-1,0, 0,0,-1] 176 | #d = M.add( me, M.mul(@tfm, M.hyperbolicInv @tfm)) 177 | #ad = (Math.abs(x) for x in d) 178 | #maxDiff = Math.max( ad ... ) 179 | #console.log "Step: #{@viewUpdates}, R: #{maxDiff}" 180 | if (@viewUpdates+=1) > @maxViewUpdatesBeforeCleanup 181 | @viewUpdates = 0 182 | @tfm = M.cleanupHyperbolicMoveMatrix @tfm 183 | #console.log "cleanup" 184 | 185 | modifyView: (m) -> 186 | @tfm = M.mul m, @tfm 187 | @checkViewMatrix() 188 | originDistance = @viewDistanceToOrigin() 189 | if originDistance > @jumpLimit 190 | @rebaseView() 191 | else 192 | @renderGrid @tfm 193 | 194 | renderGrid: (viewMatrix) -> 195 | #for immediaet mode observer, grid is rendered while drawing. 196 | @onFinish?() 197 | 198 | viewDistanceToOrigin: -> 199 | #viewCenter = M.mulv tfm, [0.0,0.0,1.0] 200 | #Math.acosh(viewCenter[2]) 201 | Math.acosh @tfm[8] 202 | 203 | #build new view around the cell which is currently at the center 204 | rebaseView: -> 205 | centerCoord = M.mulv M.hyperbolicInv(@tfm), [0.0, 0.0, 1.0] 206 | centerCoord[0] *= 1.9 207 | centerCoord[1] *= 1.9 208 | centerCoord[2] = Math.sqrt(1.0+centerCoord[0]**2 + centerCoord[1]**2) 209 | 210 | pathToCenterCell = @xyt2path centerCoord 211 | if pathToCenterCell is unity 212 | return 213 | #console.log "Jump by #{pathToCenterCell}" 214 | m = pathToCenterCell.repr @tiling 215 | 216 | #modifyView won't work, since it multiplies in different order. 217 | @tfm = M.mul @tfm, m 218 | @checkViewMatrix() 219 | 220 | #console.log JSON.stringify @tfm 221 | #move observation point 222 | @translateBy pathToCenterCell.asStack() 223 | @renderGrid @tfm 224 | 225 | straightenView: -> 226 | @rebaseView() 227 | originalTfm = @getViewOffsetMatrix() 228 | 229 | dAngle = Math.PI/@tiling.n 230 | minusEye = M.smul(-1, M.eye()) 231 | distanceToEye = (m) -> 232 | d = M.add m, minusEye 233 | Math.max (Math.abs(di) for di in d) ... 234 | 235 | bestRotationMtx = null 236 | bestDifference = null 237 | 238 | angleOffsets = [0.0] 239 | angleOffsets.push Math.PI/2 if @tiling.n % 2 is 1 240 | for additionalAngle in angleOffsets 241 | for i in [0...2*@tiling.n] 242 | angle = dAngle*i + additionalAngle 243 | rotMtx = M.rotationMatrix angle 244 | difference = distanceToEye M.mul originalTfm, M.hyperbolicInv rotMtx 245 | if (bestDifference is null) or (bestDifference > difference) 246 | bestDifference = difference 247 | bestRotationMtx = rotMtx 248 | @setViewOffsetMatrix bestRotationMtx 249 | 250 | 251 | 252 | #xp, yp in range [-1..1] 253 | cellFromPoint:(xp,yp) -> 254 | xyt = poincare2hyperblic xp, yp 255 | throw new Error("point outside") if xyt is null 256 | #inverse transform it... 257 | xyt = M.mulv (M.inv @tfm), xyt 258 | visibleCell = @xyt2path xyt 259 | @tiling.toCell @tiling.appendRewrite @center, visibleCell.asStack() 260 | 261 | shutdown: -> #nothing to do. 262 | 263 | -------------------------------------------------------------------------------- /src/ui/observer_remote.coffee: -------------------------------------------------------------------------------- 1 | "use strict" 2 | {FieldObserver} = require "./observer.coffee" 3 | {runCommands}= require "./context_delegate.coffee" 4 | 5 | exports.FieldObserverWithRemoreRenderer = class FieldObserverWithRemoreRenderer extends FieldObserver 6 | constructor: (tessellation, appendRewrite, minCellSize=1.0/400.0)-> 7 | super tessellation, appendRewrite, minCellSize 8 | @worker = new Worker "./render_worker.js" 9 | console.log "Worker created: #{@worker}" 10 | @worker.onmessage = (e) => @onMessage e 11 | 12 | @cellShapes = null 13 | 14 | @workerReady = false 15 | 16 | @rendering = true 17 | @cellSetState = 0 18 | @worker.postMessage ["I", [tessellation.group.n, tessellation.group.m, @cellTransforms]] 19 | 20 | @postponedRenderRequest = null 21 | 22 | 23 | _observedCellsChanged: -> 24 | console.log "Ignore all responces before answer..." 25 | @cellShapes = null 26 | @cellSetState+= 1 27 | return 28 | 29 | onMessage: (e) -> 30 | #console.log "message received: #{JSON.stringify e.data}" 31 | switch e.data[0] 32 | when "I" then @onInitialized e.data[1] ... 33 | when "R" then @renderFinished e.data[1], e.data[2] 34 | else throw new Error "Unexpected answer from worker: #{JSON.stringify e.data}" 35 | return 36 | 37 | onInitialized: (n,m) -> 38 | if (n is @tessellation.group.n) and (m is @tessellation.group.m) 39 | console.log "Worker initialized" 40 | @workerReady = true 41 | #now waiting for first rendered field. 42 | else 43 | console.log "Init OK message received, but mismatched. Probably, late message" 44 | 45 | _runPostponed: -> 46 | if @postponedRenderRequest isnt null 47 | @renderGrid @postponedRenderRequest 48 | @postponedRenderRequest = null 49 | 50 | renderFinished: (renderedCells, cellSetState) -> 51 | #console.log "worker finished rendering #{renderedCells.length} cells" 52 | @rendering = false 53 | if cellSetState is @cellSetState 54 | @cellShapes = renderedCells 55 | @onFinish?() 56 | #else 57 | # console.log "mismatch cell states: answer for #{cellSetState}, but current is #{@cellSetState}" 58 | @_runPostponed() 59 | 60 | renderGrid: (viewMatrix) -> 61 | if @rendering or not @workerReady 62 | @postponedRenderRequest = viewMatrix 63 | else 64 | @rendering = true 65 | @worker.postMessage ["R", viewMatrix, @cellSetState] 66 | 67 | canDraw: -> @cellShapes and @workerReady 68 | 69 | draw: (cells, context) -> 70 | if @cellShapes is null 71 | console.log "cell shapes null" 72 | return false if (not @cellShapes) or (not @workerReady) 73 | #first borders 74 | if @drawEmpty 75 | context.beginPath() 76 | for cell, i in @cells 77 | unless cells.get cell 78 | runCommands context, @cellShapes[i] 79 | context.stroke() 80 | 81 | #then cells 82 | context.beginPath() 83 | for cell, i in @cells 84 | if cells.get cell 85 | runCommands context, @cellShapes[i] 86 | context.fill() 87 | return true 88 | shutdown: -> 89 | @worker.terminate() 90 | -------------------------------------------------------------------------------- /src/ui/parseuri.coffee: -------------------------------------------------------------------------------- 1 | # parseUri 1.2.2 2 | # (c) Steven Levithan 3 | # MIT License 4 | exports.parseUri = parseUri = (str) -> 5 | o = parseUri.options 6 | m = o.parser[(if o.strictMode then "strict" else "loose")].exec(str) 7 | uri = {} 8 | i = 14 9 | uri[o.key[i]] = m[i] or "" while i-- 10 | uri[o.q.name] = {} 11 | uri[o.key[12]].replace o.q.parser, ($0, $1, $2) -> 12 | uri[o.q.name][$1] = $2 if $1 13 | 14 | for k, v of uri.queryKey 15 | uri.queryKey[k] = decodeURIComponent v 16 | 17 | uri 18 | parseUri.options = 19 | strictMode: false 20 | key: ["source", "protocol", "authority", "userInfo", "user", "password", "host", "port", "relative", "path", "directory", "file", "query", "anchor"] 21 | q: 22 | name: "queryKey" 23 | parser: /(?:^|&)([^&=]*)=?([^&]*)/g 24 | 25 | parser: 26 | strict: /^(?:([^:\/?#]+):)?(?:\/\/((?:(([^:@]*)(?::([^:@]*))?)?@)?([^:\/?#]*)(?::(\d*))?))?((((?:[^?#\/]*\/)*)([^?#]*))(?:\?([^#]*))?(?:#(.*))?)/ 27 | loose: /^(?:(?![^:@]+:[^:@\/]*@)([^:\/?#.]+):)?(?:\/\/)?((?:(([^:@]*)(?::([^:@]*))?)?@)?([^:\/?#]*)(?::(\d*))?)(((\/(?:[^?#](?![^?#\/]*\.[^?#\/.]+(?:[?#]|$)))*\/?)?([^?#\/]*))(?:\?([^#]*))?(?:#(.*))?)/ 28 | -------------------------------------------------------------------------------- /src/ui/render_worker.coffee: -------------------------------------------------------------------------------- 1 | #The purpose of this worker is to render bezier curves positions for Poincare tessellation. 2 | # 3 | {ContextDelegate} = require "./context_delegate.coffee" 4 | 5 | {Tessellation} = require "../core/hyperbolic_tessellation.coffee" 6 | M = require "../core/matrix3.coffee" 7 | 8 | cellMatrices = null 9 | tessellation = null 10 | 11 | initialize = (n, m, newCellMatrices) -> 12 | tessellation = new Tessellation n, m 13 | cellMatrices = newCellMatrices 14 | 15 | 16 | render = (viewMatrix) -> 17 | context = new ContextDelegate 18 | for m in cellMatrices 19 | tessellation.makeCellShapePoincare M.mul(viewMatrix,m), context 20 | context.take() 21 | 22 | self.onmessage = (e) -> 23 | switch e.data[0] 24 | when "I" 25 | [n, m, matrices] = e.data[1] 26 | console.log "Init tessellation {#{n};#{m}}" 27 | initialize n, m, matrices 28 | postMessage ["I", [n,m]] 29 | 30 | shapes = render M.eye() 31 | postMessage ["R", shapes, 0] 32 | 33 | when "R" 34 | id = e.data[2] 35 | shapes = render( e.data[1]) 36 | postMessage ["R", shapes, id] 37 | else 38 | console.log "Unknown message: #{JSON.stringify e.data}" 39 | 40 | 41 | -------------------------------------------------------------------------------- /styles.css: -------------------------------------------------------------------------------- 1 | /* Typographics */ 2 | html{ 3 | } 4 | body { 5 | font-family: Verdana,sans-serif; 6 | } 7 | 8 | /* UI */ 9 | .popup-container { 10 | position: absolute; 11 | top:0; 12 | left:0; 13 | width:100%; 14 | } 15 | div.popup-shadow { 16 | top:0; 17 | left:0; 18 | width:100%; 19 | height:100%; 20 | position: fixed; 21 | background-color: rgba(0,0,0,0.5); 22 | z-index: 1; 23 | } 24 | 25 | div.popup { 26 | position: relative; /*to make zindex work*/ 27 | height: 100%; 28 | width: 70%; 29 | margin: 2em auto 0 auto; 30 | overflow: auto; 31 | background-color: white; 32 | padding: 0 1em 1em 1em; 33 | border-radius: 0.5em; 34 | z-index: 2; 35 | } 36 | .toolbar { 37 | margin-left: auto; 38 | margin-right: auto; 39 | } 40 | .popup-container h1 { 41 | margin: 0 0 0.3em 0; 42 | text-align: center; 43 | color: #888; 44 | } 45 | 46 | .dialog-content{ 47 | margin: 1em 2em 2em 1em; 48 | } 49 | /*Main layout*/ 50 | 51 | #main{ 52 | display: table; 53 | text-align: center; 54 | width: 100%; 55 | } 56 | #references{ 57 | display:inine-block; 58 | position: absolute; 59 | top:0; 60 | right: 0; 61 | } 62 | 63 | #references a{ 64 | color: black; 65 | text-decoration: none; 66 | } 67 | #references a:hover{ 68 | text-decoration: underline; 69 | } 70 | 71 | #main-row{ 72 | display: table-row; 73 | } 74 | #main-row > div{ 75 | vertical-align: top; 76 | display: table-cell; 77 | } 78 | 79 | #main-content{ 80 | } 81 | #toolbar { 82 | text-align:center; 83 | } 84 | /*To position floating toolbars*/ 85 | #floating-wrapper{ 86 | position: relative; 87 | } 88 | div.overlay-toolbar{ 89 | position: absolute; 90 | display: inline-block; 91 | top: 1em; 92 | } 93 | 94 | #edit-button-container{ 95 | left:1em; 96 | } 97 | #pan-button-container{ 98 | right:1em; 99 | } 100 | span.button-group{ 101 | padding: 0 0.5em 0 0.5em; 102 | border-left: solid #CCC 1px; 103 | } 104 | span.button-group:first-child { 105 | border-left: none; 106 | 107 | } 108 | 109 | #sidebar { 110 | width: 12em; 111 | } 112 | 113 | #canvas-container{ 114 | text-align: center; 115 | padding: 1em; 116 | border: 0; 117 | margin: 1em; 118 | background-color: white; 119 | } 120 | #file-dialog-files{ 121 | overflow: auto; 122 | } 123 | 124 | table.files-table{ 125 | margin-left: auto; 126 | margin-right: auto; 127 | width: 100%; 128 | border-collapse: collapse; 129 | } 130 | 131 | table.files-table th { 132 | text-align: left; 133 | } 134 | tr.files-file-row { 135 | border: solid 1px silver;a 136 | } 137 | 138 | tr.files-file-row:hover { 139 | background-color: silver; 140 | } 141 | 142 | tr.files-grid-row, tr.files-func-row{ 143 | border: none; 144 | } 145 | 146 | tr.files-grid-row{ 147 | color: #03a; 148 | background-color: #ddd; 149 | } 150 | tr.files-grid-row td{ 151 | padding: 0.1em 0 0.1em 1em; 152 | } 153 | 154 | tr.files-func-row{ 155 | color: #040; 156 | background-color: #eee; 157 | } 158 | tr.files-func-row td{ 159 | padding: 0.1em 0 0.1em 3em; 160 | } 161 | 162 | /*Dialog: SVG export*/ 163 | #svg-image-container { 164 | text-align:center; 165 | overflow: auto; 166 | } 167 | #svg-image-container img{ 168 | border: solid 1px silver; 169 | } 170 | 171 | 172 | /* Tweak controls */ 173 | .numdisplay { 174 | min-width: 3em; 175 | display: inline-block; 176 | text-align: right; 177 | background-color: #f0f0f0; 178 | } 179 | 180 | .control-group { 181 | padding: 0.5em 0; 182 | margin: 0; 183 | border-top: solid #CCC 1px; 184 | } 185 | .control-group:first-child { 186 | border-top: none; 187 | } 188 | div.control-table { 189 | display: table; 190 | margin-left:auto; 191 | margin-right:auto; 192 | } 193 | div.control-table > div { 194 | display: table-row; 195 | } 196 | div.control-table > div > div { 197 | display: table-cell; 198 | text-align:left; 199 | } 200 | button{ 201 | margin: 0.2em 0; 202 | padding:0; 203 | 204 | display: inline-block; 205 | border: none; 206 | color: #000000; 207 | border-radius: 0.2em; 208 | -webkit-border-radius: 0.2em; 209 | -moz-border-radius: 0.2em; 210 | /*font-family: Verdana;*/ 211 | width: auto; 212 | height: auto; 213 | font-size: 1em; 214 | padding: 0.5em; 215 | -text-shadow: 0 1px 0 #FFFFFF; 216 | background-color: #E0E0E0; 217 | 218 | } 219 | 220 | button.button-small{ 221 | margin: 0; 222 | padding:0; 223 | 224 | display: inline-block; 225 | border: none; 226 | color: #000000; 227 | border-radius: 0; 228 | width: auto; 229 | height: auto; 230 | font-size: 1em; 231 | padding: 0; 232 | -text-shadow: 0 1px 0 #FFFFFF; 233 | background-color: #E0E0E0; 234 | 235 | } 236 | 237 | button:disabled, 238 | button[disabled]:hover{ 239 | background-color: #F0F0F0; 240 | color: #C0C0C0; 241 | } 242 | button:hover{ 243 | background-color: #C0E0E0; 244 | } 245 | button:active{ 246 | background-color: #C0F0F0; 247 | } 248 | 249 | button.button-active{ 250 | background-color: #C0F0C0; 251 | } 252 | 253 | button.dangerous:hover{ 254 | background-color: #E0C0C0; 255 | } 256 | button.dangerous:active{ 257 | background-color: #F0C0C0; 258 | } 259 | 260 | #state-selector button{ 261 | color: white; 262 | margin: 0 0.2em; 263 | } 264 | #state-selector button.btn-selected{ 265 | padding: 0.8em; 266 | } 267 | 268 | input{ 269 | background-color: white; 270 | border: solid 1px #C0C0C0; 271 | padding: 0.3em 0.1em; 272 | } 273 | 274 | input.short { 275 | width:3em; 276 | } 277 | input.medium { 278 | width:4.5em; 279 | } 280 | input.wide { 281 | width: 100%; 282 | } 283 | 284 | #navigator{ 285 | margin-top: 4em; 286 | } 287 | #navigator-wrap { 288 | overflow-y: auto; 289 | -overflow-x: visible; 290 | padding-left: 0.1em; 291 | } 292 | #navigator table{ 293 | border-collapse: collapse; 294 | } 295 | /* Navigator styles */ 296 | #navigator tr:hover{ 297 | background-color: silver; 298 | } 299 | #navigator td{ 300 | text-align: center; 301 | border: solid 1px silver; 302 | } 303 | 304 | #navigator table a 305 | { 306 | display:block; 307 | text-decoration:none; 308 | color: black; 309 | } 310 | 311 | textarea.code-entry { 312 | height: 20em; 313 | } 314 | 315 | /*Validating inputs*/ 316 | input.input-ok { 317 | color: black; 318 | } 319 | input.input-editing { 320 | color: blue; 321 | } 322 | input.input-bad { 323 | color: red; 324 | } -------------------------------------------------------------------------------- /tests/perftest_simulation.coffee: -------------------------------------------------------------------------------- 1 | #Performance testing 2 | # 3 | # 4 | 5 | {randomFillFixedNum} = require "../src/core/field.coffee" 6 | {ChainMap} = require "../src/core/chain_map.coffee" 7 | {RegularTiling} = require "../src/core/regular_tiling.coffee" 8 | {parseTransitionFunction} = require "../src/core/rule.coffee" 9 | {evaluateTotalisticAutomaton} = require "../src/core/cellular_automata.coffee" 10 | {unity} = require "../src/core/vondyck_chain.coffee" 11 | class RandomGenerator 12 | #From here: http://stackoverflow.com/questions/424292/seedable-javascript-random-number-generator 13 | constructor: (seed)-> 14 | @m = 0x80000000 # 2**31 15 | @a = 1103515245 16 | @c = 12345 17 | @state = seed ? (Math.floor(Math.random() * (@m-1))) 18 | 19 | nextInt: -> 20 | this.state = (this.a * this.state + this.c) % this.m 21 | 22 | # returns in range [0,1] 23 | nextFloat: -> @nextInt() / (@m - 1) 24 | 25 | # returns in range [start, end): including start, excluding end 26 | # can't modulu nextInt because of weak randomness in lower bits 27 | nextRange: (start, end) -> 28 | rangeSize = end - start 29 | randomUnder1 = @nextInt() / @m 30 | start + Math.floor(randomUnder1 * rangeSize) 31 | 32 | choice: (array) -> array[@nextRange(0, array.length)] 33 | 34 | #Fill randomly, visiting numCells cells around the origin 35 | randomFillBlob = (field, tiling, numCells, randomState ) -> 36 | visited = 0 37 | tiling.forFarNeighborhood unity, (cell, _)-> 38 | #Time to stop iteration? 39 | return false if visited >= numCells 40 | if (state = randomState()) isnt 0 41 | field.put cell, state 42 | visited+=1 43 | #Continue 44 | return true 45 | return 46 | 47 | 48 | 49 | runTestMildGrowing = (seed) -> 50 | rng = new RandomGenerator seed 51 | 52 | tiling = new RegularTiling 3, 8 53 | rule = parseTransitionFunction "B 3 S 2 6", tiling.n, tiling.m 54 | density = 0.4 55 | newState = -> if rng.nextFloat() < density then 1 else 0 56 | maxCells = 4000 57 | steps = 1000 58 | maxPop = 20000 59 | field = new ChainMap() 60 | randomFillBlob field, tiling, maxCells, newState 61 | 62 | console.log "Rule is: #{rule}" 63 | generation = 0 64 | 65 | console.time "eval" 66 | while (field.count < maxPop) and ((generation+=1) < steps) 67 | field = evaluateTotalisticAutomaton field, tiling, rule.evaluate.bind(rule), rule.plus, rule.plusInitial 68 | #console.log "g: #{generation}, pop: #{field.count}" 69 | console.timeEnd "eval" 70 | 71 | 72 | 73 | runTestMildGrowing 100 74 | runTestMildGrowing 101 75 | runTestMildGrowing 102 76 | -------------------------------------------------------------------------------- /tests/test_cellular_automaton.coffee: -------------------------------------------------------------------------------- 1 | assert = require "assert" 2 | {allClusters, exportField, importField, parseFieldData, randomStateGenerator, stringifyFieldData} = require "../src/core/field.coffee" 3 | {ChainMap} = require "../src/core/chain_map.coffee" 4 | {RegularTiling} = require "../src/core/regular_tiling.coffee" 5 | 6 | {neighborsSum, evaluateTotalisticAutomaton} = require "../src/core/cellular_automata.coffee" 7 | 8 | describe "evaluateTotalisticAutomaton", -> 9 | 10 | it "must persist single cell in rule B 3 S 0 2 3", -> 11 | 12 | ruleNext = (x,s) -> 13 | if x is 0 14 | if s is 3 then 1 else 0 15 | else if x is 1 16 | if s in [0,2,3] then 1 else 0 17 | 18 | [N, M] = [7, 3] 19 | tiling = new RegularTiling N, M 20 | unity = tiling.unity 21 | 22 | #prepare field with only one cell 23 | field = new ChainMap 24 | field.put unity, 1 25 | 26 | field1 = evaluateTotalisticAutomaton field, tiling, ruleNext 27 | 28 | #now check the field 29 | assert.equal field1.count, 1 30 | assert.equal field1.get(unity), 1 31 | 32 | it "must NOT persist single cell in rule B 3 S 2 3", -> 33 | 34 | ruleNext = (x,s) -> 35 | if x is 0 36 | if s is 3 then 1 else 0 37 | else if x is 1 38 | if s in [2,3] then 1 else 0 39 | else throw new Error("bad state #{x}") 40 | 41 | [N, M] = [7, 3] 42 | tiling = new RegularTiling N, M 43 | 44 | #prepare field with only one cell 45 | field = new ChainMap 46 | field.put tiling.unity, 1 47 | 48 | field1 = evaluateTotalisticAutomaton field, tiling, ruleNext 49 | 50 | #now check the field 51 | assert.equal field1.count, 0 52 | assert.equal field1.get(tiling.unity), null 53 | -------------------------------------------------------------------------------- /tests/test_chain_map.coffee: -------------------------------------------------------------------------------- 1 | assert = require "assert" 2 | {unity, newNode} = require "../src/core/vondyck_chain.coffee" 3 | {ChainMap} = require "../src/core/chain_map.coffee" 4 | 5 | describe "ChainMap", -> 6 | it "should support putting and removing empty chain", -> 7 | m = new ChainMap 8 | m.put unity, "empty" 9 | assert.equal m.get(unity), "empty" 10 | assert.equal m.count, 1 11 | 12 | m.put unity, "empty1" 13 | assert.equal m.get(unity), "empty1" 14 | assert.equal m.count, 1 15 | 16 | assert m.remove unity 17 | assert.equal m.get(unity), null 18 | assert.equal m.count, 0 19 | 20 | it "should support putting values wtih accumulation", -> 21 | m = new ChainMap 22 | e = unity 23 | a1 = newNode("a", 1, unity) 24 | b2 = newNode("b", 2, unity) 25 | a1b1 = newNode("a", 1, newNode("b", 1, unity)) 26 | 27 | #testing initial value for accumulation 28 | m.putAccumulate( a1b1, 1, ((x,y)->x+y), 10 ) 29 | assert.equal m.get(a1b1), 11 #(initial is 10) + 1 30 | #for second value, existing is used. 31 | m.putAccumulate( a1b1, 1, ((x,y)->x+y), 10 ) 32 | assert.equal m.get(a1b1), 12 #(previous is 11) + 1 33 | 34 | 35 | it "should support putting and removing non - empty chains", -> 36 | m = new ChainMap 37 | e = unity 38 | a1 = newNode("a", 1, unity) 39 | b2 = newNode("b", 2, unity) 40 | a1b1 = newNode("a", 1, newNode("b", 1, unity)) 41 | 42 | m.put e, "e" 43 | m.put a1, "a1" 44 | m.put a1b1, "a1b1" 45 | m.put b2, "b2" 46 | 47 | assert.equal m.count, 4 48 | 49 | assert.equal m.get(e), "e" 50 | assert.equal m.get(a1), "a1" 51 | assert.equal m.get(b2), "b2" 52 | assert.equal m.get(a1b1), "a1b1" 53 | 54 | it "should support copy", -> 55 | m = new ChainMap 56 | c1 = unity 57 | c2 = newNode 'a', 2, unity 58 | c3 = newNode 'b', 3, unity 59 | c4 = newNode 'a', -1, c3 60 | cells = [c1,c2,c3,c4] 61 | 62 | for cell, index in cells 63 | m.put cell, index 64 | 65 | m1 = m.copy() 66 | #ensure that copy is right 67 | for cell, index in cells 68 | assert.equal m1.get(cell), index 69 | 70 | assert.equal m1.count, m.count 71 | 72 | #ensure that copy is independednt 73 | for cell, index in cells 74 | m.put cell, index+100 75 | 76 | for cell, index in cells 77 | assert.equal m1.get(cell), index 78 | 79 | #ensure that copy is functional 80 | c5 = newNode 'b', 3, c4 81 | m1.put c5, 100 82 | assert.equal m1.get(c5), 100 83 | assert.equal m.get(c5), null 84 | for cell, index in cells 85 | assert.equal m1.get(cell), index 86 | 87 | 88 | it "should remove cells without corrupting data", -> 89 | m = new ChainMap 90 | c1 = unity 91 | c2 = newNode 'a', 2, unity 92 | c3 = newNode 'b', 3, unity 93 | c4 = newNode 'a', -1, c3 94 | cells = [c1,c2,c3,c4] 95 | 96 | for cell, index in cells 97 | m.put cell, index 98 | 99 | #check that data works fine 100 | for cell, index in cells 101 | assert.equal m.get(cell), index 102 | 103 | #now delete something 104 | 105 | m.remove c2 106 | expected = [0, null, 2,3] 107 | for cell, index in cells 108 | assert.equal m.get(cell), expected[index] 109 | 110 | m.remove c3 111 | expected = [0, null, null,3] 112 | for cell, index in cells 113 | assert.equal m.get(cell), expected[index] 114 | 115 | m.remove c4 116 | expected = [0, null, null,null] 117 | for cell, index in cells 118 | assert.equal m.get(cell), expected[index] 119 | 120 | m.remove c1 121 | expected = [null, null, null,null] 122 | for cell, index in cells 123 | assert.equal m.get(cell), expected[index] 124 | 125 | it "should support growing the table", -> 126 | 127 | m = new ChainMap 128 | initialTableSize = m.table.length 129 | 130 | for i1 in [-5..5] 131 | a1 = newNode 'a', i1, unity 132 | for i2 in [-5..5] 133 | a2 = newNode 'b', i2, a1 134 | for i3 in [-5..5] 135 | a3 = newNode 'a', i3, a2 136 | for i4 in [-5..5] 137 | a4 = newNode 'b', i4, a3 138 | for i5 in [-5..5] 139 | a5 = newNode 'a', i5, a4 140 | m.put a5, true 141 | 142 | assert.equal m.count, 11**5 143 | 144 | for i1 in [-5..5] 145 | a1 = newNode 'a', i1, unity 146 | for i2 in [-5..5] 147 | a2 = newNode 'b', i2, a1 148 | for i3 in [-5..5] 149 | a3 = newNode 'a', i3, a2 150 | for i4 in [-5..5] 151 | a4 = newNode 'b', i4, a3 152 | for i5 in [-5..5] 153 | a5 = newNode 'a', i5, a4 154 | assert m.get a5 155 | assert (m.table.length > initialTableSize) 156 | 157 | #check that collision count is sane 158 | it "must have sane collision count", -> 159 | cellSizes = (cell.length for cell in m.table) 160 | cellSizes.sort() 161 | assert cellSizes[cellSizes.length-1] < 100 162 | -------------------------------------------------------------------------------- /tests/test_decompose_to_translations.coffee: -------------------------------------------------------------------------------- 1 | assert = require "assert" 2 | M = require "../src/core/matrix3" 3 | {decomposeToTranslations, decomposeToTranslationsAggresively} = require "../src/core/decompose_to_translations" 4 | 5 | describe "decomposeToTranslations", -> 6 | it "must decompose unity matrix to itself", -> 7 | [t1,t2] = decomposeToTranslations M.eye() 8 | #console.log "Found:" 9 | #console.dir t1 10 | #console.dir t2 11 | 12 | assert.ok t1? 13 | assert.ok M.approxEq t1, M.eye() 14 | assert.ok t2? 15 | assert.ok M.approxEq t2, M.eye() 16 | 17 | it "must decompose translation matrix to itself and unity", -> 18 | t = M.translationMatrix 2, 3 19 | 20 | [t1,t2] = decomposeToTranslations t 21 | #console.log "Found:" 22 | #console.dir t1 23 | #console.dir t2 24 | 25 | assert.ok t1? 26 | assert.ok M.approxEq t1, M.eye() 27 | assert.ok t2? 28 | assert.ok M.approxEq t2, t 29 | 30 | it "must not decompose pure rotation matrix", -> 31 | t = M.rotationMatrix 0.4 32 | 33 | [t1,t2] = decomposeToTranslations t 34 | #console.log "Found:" 35 | #console.dir t1 36 | #console.dir t2 37 | 38 | assert.ok t1 is null 39 | assert.ok t2 is null 40 | 41 | it "must decompose some matrix", -> 42 | t = M.mul M.rotationMatrix(0.3), M.translationMatrix 2, 3 43 | 44 | [t1,t2] = decomposeToTranslations t 45 | 46 | assert.ok t1? 47 | assert.ok t2? 48 | 49 | tRestored = M.mul M.hyperbolicInv(t1), M.mul t2, t1 50 | assert.ok M.approxEq tRestored, t, 1e-5 51 | 52 | it "must decompose some hard matrix", -> 53 | t = [5.58512230547673, -4.886985710846093, 7.3536535480771485, 54 | 12.220681374785933, -7.783877858381329, 14.454542807651823, 55 | 13.399203126722638, -9.136267501130483, 16.248385405429882] 56 | 57 | [t1,t2] = decomposeToTranslations t 58 | 59 | assert.ok t1? 60 | assert.ok t2? 61 | 62 | tRestored = M.mul M.hyperbolicInv(t1), M.mul t2, t1 63 | assert.ok M.approxEq tRestored, t, 1e-4 64 | 65 | # describe "decomposeToTranslationsAggresively", -> 66 | # it "must decompose high-amplitude matrix", -> 67 | 68 | # #Obtained from practice. decmposition possible, but hard 69 | # m = [5.58512230547673, -4.886985710846093, 7.3536535480771485, 70 | # 12.220681374785933, -7.783877858381329, 14.454542807651823, 71 | # 13.399203126722638, -9.136267501130483, 16.248385405429882] 72 | 73 | 74 | # [t1,t2] = decomposeToTranslationsAggresively m 75 | # assert.ok t1? 76 | # assert.ok t2? 77 | 78 | # tRestored = M.mul M.hyperbolicInv(t1), M.mul t2, t1 79 | # console.log "restored" 80 | # console.dir tRestored 81 | # console.log "original" 82 | # console.dir t 83 | 84 | # assert.ok M.approxEq tRestored, t, 1e-5 85 | 86 | -------------------------------------------------------------------------------- /tests/test_field.coffee: -------------------------------------------------------------------------------- 1 | 2 | assert = require "assert" 3 | {allClusters, exportField, importField, parseFieldData, randomStateGenerator, stringifyFieldData, randomFillFixedNum} = require "../src/core/field" 4 | {unity, newNode} = require "../src/core/vondyck_chain.coffee" 5 | {ChainMap} = require "../src/core/chain_map.coffee" 6 | {RegularTiling} = require "../src/core/regular_tiling.coffee" 7 | 8 | describe "allClusters", -> 9 | 10 | #prepare data: rewriting ruleset for group 5;4 11 | # 12 | [N, M] = [5, 4] 13 | tiling = new RegularTiling N, M 14 | 15 | it "should give one cell, if only one central cell present", -> 16 | cells = new ChainMap 17 | cells.put unity, 1 18 | clusters = allClusters cells, tiling 19 | assert.equal clusters.length, 1 20 | assert.deepEqual clusters, [[unity]] #one cluster of 1 cell 21 | 22 | it "should give one cell, if only one central cell present", -> 23 | cells = new ChainMap 24 | c = tiling.toCell tiling.parse "Ab^2a^2" 25 | 26 | cells.put c, 1 27 | clusters = allClusters cells, tiling 28 | assert.equal clusters.length, 1 29 | assert.deepEqual clusters[0].length, 1 30 | 31 | assert clusters[0][0].equals c 32 | 33 | 34 | describe "exportField", -> 35 | it "must export empty field", -> 36 | f = new ChainMap 37 | tree = exportField f 38 | assert.deepEqual tree, {} 39 | 40 | it "must export field with only root cell", -> 41 | f = new ChainMap 42 | f.put unity, 1 43 | tree = exportField f 44 | assert.deepEqual tree, {v: 1} 45 | 46 | it "must export field with 1 non-root cell", -> 47 | f = new ChainMap 48 | #ab^3a^2 49 | chain = newNode 'a', 2, newNode 'b', 3, newNode 'a',1, unity 50 | f.put chain, "value" 51 | tree = exportField f 52 | assert.deepEqual tree, { 53 | cs:[{ 54 | a: 1 55 | cs: [{ 56 | b: 3 57 | cs: [{ 58 | a: 2 59 | v: "value" 60 | }]}]}]} 61 | 62 | describe "randomStateGenerator", -> 63 | makeStates = (nStates, nValues) -> 64 | gen = randomStateGenerator nStates 65 | (gen() for _ in [0...nValues]) 66 | 67 | it "must produce random values in required range", -> 68 | states = makeStates 5, 1000 69 | assert.equal states.length, 1000 70 | #should for sure contain at least one of values 1,2,3,4 71 | #should not contain 0 72 | #should not contain >=5 73 | counts = [0,0,0,0,0] 74 | for x in states 75 | counts[x] += 1 76 | assert.equal counts[0], 0 77 | assert(counts[1] > 0) 78 | assert(counts[2] > 0) 79 | assert(counts[3] > 0) 80 | assert(counts[4] > 0) 81 | assert.equal counts[1]+counts[2]+counts[3]+counts[4], 1000 82 | 83 | describe "stringifyFieldData", -> 84 | it "must stringify empty", -> 85 | f = {} 86 | assert.equal stringifyFieldData(f), "" 87 | it "must cell at origin", -> 88 | f = {v:1} 89 | assert.equal stringifyFieldData(f), "|1" 90 | it "must other cells", -> 91 | f ={ 92 | v:1 93 | cs: [ 94 | { 95 | a:1 96 | v:2 97 | },{ 98 | a:-2 99 | v:3 100 | }]} 101 | assert.equal stringifyFieldData(f), "|1(a|2)(A2|3)" 102 | 103 | describe "parseFieldData", -> 104 | it "must parse empty string", -> 105 | f = parseFieldData "" 106 | assert.deepEqual f, {} 107 | it "must parse cell at origin", -> 108 | f = parseFieldData "|1" 109 | assert.deepEqual f, {v:1} 110 | it "must parse non-trivial", -> 111 | tree = { 112 | cs:[{ 113 | a: 1 114 | cs: [{ 115 | b: -3 116 | cs: [{ 117 | a: 2 118 | v: 1 119 | }]}]}]} 120 | f = parseFieldData "(a(B3(a2|1)))" 121 | assert.deepEqual f, tree 122 | 123 | 124 | describe "importField", -> 125 | it "must import empty field correctly", -> 126 | f = importField {} 127 | assert.equal f.count, 0 128 | it "must import root cell correctly", -> 129 | f = importField {v: 1} 130 | assert.equal f.count, 1 131 | assert.equal f.get(unity), 1 132 | it "must import 1 non-root cell correctly", -> 133 | tree = { 134 | cs:[{ 135 | a: 1 136 | cs: [{ 137 | b: 3 138 | cs: [{ 139 | a: 2 140 | v: "value" 141 | }]}]}]} 142 | #ab^3a^2 143 | chain = newNode 'a', 2, newNode 'b', 3, newNode 'a',1, unity 144 | f = importField tree 145 | assert.equal f.count, 1 146 | assert.equal f.get(chain), 'value' 147 | 148 | 149 | it "must import some nontrivial exported field", -> 150 | f = new ChainMap 151 | #ab^3a^2 152 | chain1 = newNode 'a', 2, newNode 'b', 3, newNode 'a',1, unity 153 | #a^-1b^3a^2 154 | chain2 = newNode 'a', -1, newNode 'b', 3, newNode 'a',1, unity 155 | f.put unity, "value0" 156 | f.put chain1, "value1" 157 | f.put chain2, "value2" 158 | 159 | 160 | f1 = importField exportField f 161 | 162 | assert.equal f1.count, 3 163 | assert.equal f1.get(unity), 'value0' 164 | assert.equal f1.get(chain1), 'value1' 165 | assert.equal f1.get(chain2), 'value2' 166 | 167 | it "must support preprocessing imported data", -> 168 | f = new ChainMap 169 | #ab^3a^2 170 | chain1 = newNode 'a', 2, newNode 'b', 3, newNode 'a',1, unity 171 | #a^-1b^3a^2 172 | chain2 = newNode 'a', -1, newNode 'b', 3, newNode 'a',1, unity 173 | f.put unity, "value0" 174 | f.put chain1, "value1" 175 | f.put chain2, "value2" 176 | 177 | #Preprocessor simply appends b^2 to the chain 178 | preprocessor = (chain) -> newNode 'b', 2, chain 179 | 180 | f1 = importField exportField(f), null, preprocessor 181 | 182 | assert.equal f1.count, 3 183 | assert.equal 'value0', f1.get(newNode 'b', 2, unity), 184 | assert.equal 'value1', f1.get(newNode 'b', 2, newNode 'a', 2, newNode 'b', 3, newNode 'a',1, unity) 185 | assert.equal 'value2', f1.get(newNode 'b', 2, newNode 'a', -1, newNode 'b', 3, newNode 'a',1, unity) 186 | 187 | 188 | 189 | describe "randomFillFixedNum", -> 190 | it "must fill some reasonable number of cells", -> 191 | [N, M] = [5, 4] 192 | tiling = new RegularTiling N, M 193 | 194 | field = new ChainMap 195 | 196 | nCells = 10000 197 | randomFillFixedNum field, 0.4, unity, 10000, tiling 198 | 199 | #not guaranteed, but chances of failure are small. 200 | assert.ok field.count > 0.4*nCells*0.7 201 | assert.ok field.count < 0.4*nCells*1.3 202 | -------------------------------------------------------------------------------- /tests/test_fminsearch.coffee: -------------------------------------------------------------------------------- 1 | assert = require "assert" 2 | 3 | {fminsearch} = require "../src/core/fminsearch" 4 | 5 | 6 | near = (x, y, eps=1e-5) -> Math.abs(x-y) 9 | it "should find minimum of function of one argument (x-1)^2", -> 10 | f = ([x]) -> (x-1)**2 11 | 12 | res = fminsearch f, [0,0], 0.1 13 | assert.ok res.reached 14 | assert.ok near res.x[0], 1.0 15 | 16 | it "should find minimum of (x-1)^2 + (y-1)^2", -> 17 | 18 | f = ([x,y]) -> (x-1)**2 + (y-2)**2 19 | 20 | res = fminsearch f, [0,0], 0.1 21 | #console.log "solved:" 22 | #console.dir res 23 | assert.ok res.reached 24 | assert.ok near res.x[0], 1.0 25 | assert.ok near res.x[1], 2.0 26 | 27 | it "should find minimum of function of 4 arguments: (x-1)^2 + (y-1)^2 + (z-3)^2 + (t-4)^4", -> 28 | f = ([x,y,z,t]) -> (x-1)**2 + (y-2)**2 + (z-3)**2 + (t-4)**4 29 | 30 | res = fminsearch f, [0,0,0,0], 0.1 31 | #console.log "solved:" 32 | #console.dir res 33 | assert.ok res.reached 34 | assert.ok near res.x[0], 1.0 35 | assert.ok near res.x[1], 2.0 36 | assert.ok near res.x[2], 3.0 37 | assert.ok near res.x[3], 4.0 38 | 39 | it "should find minimum of the rozenbrock function, (1-x)**2 + 100*(y-x**2)**2", -> 40 | f = ([x,y]) -> (1-x)**2 + 100*(y-x**2)**2 41 | 42 | res = fminsearch f, [0,0], 0.1 43 | #console.log "solved:" 44 | #console.dir res 45 | assert.ok res.reached 46 | assert.ok near res.x[0], 1.0 47 | assert.ok near res.x[1], 1.0 48 | -------------------------------------------------------------------------------- /tests/test_hyperbolic_tessellation.coffee: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/dmishin/hyperbolic-ca-simulator/01142a9139fac5508e3476759c3a52570402ee0c/tests/test_hyperbolic_tessellation.coffee -------------------------------------------------------------------------------- /tests/test_knuth_bemdix.coffee: -------------------------------------------------------------------------------- 1 | {shortLex, overlap, splitBy, RewriteRuleset} = require "../src/core/knuth_bendix" 2 | #M = require "../src/core/matrix3" 3 | assert = require "assert" 4 | 5 | describe "TestComparatros", -> 6 | it "checks shortlex", -> 7 | 8 | assert.ok( shortLex( "", "a") ) 9 | assert.ok( shortLex( "", "") ) 10 | assert.ok( shortLex( "a", "a") ) 11 | assert.ok( shortLex( "a", "a") ) 12 | 13 | assert.ok( shortLex( "a", "bb") ) 14 | assert.ok( shortLex( "a", "b") ) 15 | assert.ok not( shortLex( "bb", "a") ) 16 | 17 | describe "RewriteRuleset", -> 18 | it "must support construction", -> 19 | r = new RewriteRuleset {"aa": "", "AAA": "a"} 20 | it "must support copying", -> 21 | r1 = r.copy() 22 | assert.ok r isnt r1 23 | assert.ok r.equals r1 24 | assert.ok r1.equals r 25 | 26 | r2 = new RewriteRuleset {"aa": "a"} 27 | assert.ok not r.equals r2 28 | assert.ok not r2.equals r 29 | 30 | r3 = new RewriteRuleset {"AAA": "a", "aa": ""} 31 | assert.ok r.equals r3 32 | assert.ok r3.equals r 33 | 34 | it "must rewrite according to the rules", -> 35 | r = new RewriteRuleset {"aa": ""} 36 | assert.equal r.rewrite(""), "" 37 | assert.equal r.rewrite("a"), "a" 38 | assert.equal r.rewrite("aa"), "" 39 | assert.equal r.rewrite("b"), "b" 40 | assert.equal r.rewrite("ba"), "ba" 41 | assert.equal r.rewrite("baa"), "b" 42 | 43 | it "must apply rewrites multiple times", -> 44 | r = new RewriteRuleset {"bc": "BC", "ABC": "alphabet"} 45 | assert.equal r.rewrite("bc"), "BC" 46 | assert.equal r.rewrite("abc"), "aBC" 47 | assert.equal r.rewrite("Abc"), "alphabet" 48 | assert.equal r.rewrite("AAbc"), "Aalphabet" 49 | 50 | 51 | 52 | describe "TestSplits", -> 53 | 54 | assertOverlap = (s1, s2, x, y, z)-> 55 | assert.deepEqual( overlap(s1, s2), [x, y, z]) 56 | 57 | assertSplit = (s1, s2, hasSplit, x, z)-> 58 | assert.deepEqual( splitBy(s1, s2), 59 | [hasSplit, 60 | if x? then x else null, 61 | if z? then z else null]) 62 | it "tests split", -> 63 | assertSplit( "123456", "34", 64 | true, "12", "56" ) 65 | assertSplit( "123456", "35", 66 | false, null, null ) 67 | assertSplit( "123456", "123456", 68 | true, "", "" ) 69 | assertSplit( "123456", "456", 70 | true, "123", "" ) 71 | 72 | it "tests overlap", -> 73 | 74 | assertOverlap("123", "234", "1","23", "4") 75 | assertOverlap("123", "1234", "","123", "4") 76 | assertOverlap("123", "123", "","123", "") 77 | assertOverlap("1123", "2345", "11","23", "45") 78 | assertOverlap("1123", "22345", "1123","", "22345") 79 | 80 | 81 | -------------------------------------------------------------------------------- /tests/test_lzw.coffee: -------------------------------------------------------------------------------- 1 | {lzw_encode, lzw_decode} = require "../src/ext/lzw.coffee" 2 | assert = require "assert" 3 | describe "lzw_encode", -> 4 | strings = ["", "a", "bbbbbbbbbabababbabab", "hello hello hello hello this is me"] 5 | 6 | it "should encode without error some strings", -> 7 | codes = (lzw_encode(s) for s in strings) 8 | 9 | for c1, i in codes 10 | for c2, j in codes 11 | if i isnt j 12 | assert c1 isnt c2, "Code for #{strings[i]} isnt #{strings[j]}" 13 | return 14 | 15 | it "should decode. giving same result", -> 16 | for s in strings 17 | code = lzw_encode s 18 | s1 = lzw_decode code 19 | assert.equal s1, s 20 | 21 | 22 | -------------------------------------------------------------------------------- /tests/test_matrix3.coffee: -------------------------------------------------------------------------------- 1 | assert = require "assert" 2 | 3 | M = require "../src/core/matrix3" 4 | 5 | describe "approxEq", -> 6 | it "must return true for equal matrices", -> 7 | assert.ok M.approxEq [0,0,0,0,0,0,0,0,0], [0,0,0,0,0,0,0,0,0] 8 | 9 | it "must return false for significantly in equal matrices", -> 10 | m1 = [0,0,0,0,0,0,0,0,0] 11 | for i in [0...9] 12 | m2 = [0,0,0,0,0,0,0,0,0] 13 | m2[i] = 1.0 14 | assert.ok not M.approxEq m1, m2 15 | 16 | 17 | describe "eye", -> 18 | it "msut equal unit matrix", -> 19 | m = (0.0 for i in [0...9]) 20 | 21 | for i in [0...3] 22 | M.set m, i, i, 1.0 23 | 24 | assert.ok M.approxEq(m, M.eye()) 25 | 26 | describe "mul", -> 27 | it "must multiply eye to itself", -> 28 | assert.ok M.approxEq M.eye(), M.mul(M.eye(), M.eye()) 29 | 30 | it "must return same non-eye matrix, if multiplied with eye", -> 31 | m = (i for i in [0...9]) 32 | 33 | assert.ok M.approxEq m, M.mul(m, M.eye()) 34 | assert.ok M.approxEq m, M.mul(M.eye(), m) 35 | 36 | it "must change non-eye matrix if squared", -> 37 | m = (i for i in [0...9]) 38 | assert.ok not M.approxEq m, M.mul(m, m) 39 | 40 | 41 | describe "rot", -> 42 | it "must return eye if rotation angle is 0", -> 43 | assert.ok M.approxEq M.eye(), M.rot(0,1,0.0) 44 | assert.ok M.approxEq M.eye(), M.rot(0,2,0.0) 45 | assert.ok M.approxEq M.eye(), M.rot(1,2,0.0) 46 | it "must return non-eye if rotation angle is not 0", -> 47 | assert.ok not M.approxEq M.eye(), M.rot(0,1,1.0) 48 | 49 | 50 | describe "smul", -> 51 | it "must return 0 if multiplied by 0", -> 52 | assert.ok M.approxEq M.zero(), M.smul( 0.0, M.eye()) 53 | 54 | it "must return same if multiplied by 1", -> 55 | assert.ok M.approxEq M.eye(), M.smul( 1.0, M.eye()) 56 | 57 | describe "translationMatrix", -> 58 | it "must return unity for zero translation", -> 59 | assert.ok M.approxEq M.eye(), M.translationMatrix(0,0) 60 | it "must return almost unity for very small translation", -> 61 | assert.ok M.approxEq M.eye(), M.translationMatrix(1e-5,1e-5), 1e-4 62 | 63 | it "must return matrix that correctly translates zero", -> 64 | T = M.translationMatrix 5,6 65 | zero = [0,0,1] 66 | expect = [5, 6, Math.sqrt(5**2+6**2+1)] 67 | assert.ok M.approxEqv expect, M.mulv(T,zero) 68 | 69 | describe "addScaledInplace", -> 70 | it "must modify matrix inplace", -> 71 | m = M.eye() 72 | m1 = [1,1,1, 1,1,1, 1,1,1] 73 | 74 | M.addScaledInplace m, m1, 1 75 | expect = [2,1,1, 1,2,1, 1,1,2] 76 | assert.ok M.approxEqv expect, m 77 | 78 | it "must add with coefficient", -> 79 | m = M.eye() 80 | m1 = [1,1,1, 1,1,1, 1,1,1] 81 | 82 | M.addScaledInplace m, m1, -2 83 | expect = [-1,-2,-2, -2,-1,-2, -2,-2,-1] 84 | assert.ok M.approxEqv expect, m 85 | 86 | 87 | # describe "powerPade", -> 88 | # it "must calculate powers of rotation matrices", -> 89 | # m = M.rotationMatrix 0.6 90 | # mpow = M.powerPade m, 1.3 91 | # expect = M.rotationMatrix(0.6*1.3) 92 | # assert.ok M.approxEqv mpow, expect, 1e-4 93 | 94 | # it "must calculate zeroth power of rotation matrix", -> 95 | # m = M.rotationMatrix 0.6 96 | # mpow = M.powerPade m, 0.0 97 | # assert.ok M.approxEqv mpow, M.eye(), 1e-4 98 | 99 | # it "must calculate 0.5th power of hyperbolic translation matrices", -> 100 | # m = M.translationMatrix 1.2, 4,5 101 | 102 | # sqrt_m = M.powerPade m, 0.5 103 | 104 | # sqrt_m2 = M.mul sqrt_m, sqrt_m 105 | 106 | # console.dir m 107 | # console.dir sqrt_m2 108 | # assert.ok M.approxEqv sqrt_m2, m, 1e-4 109 | 110 | # assert.ok M.approxEqv M.powerPade(m,0.0), M.eye(), 1e-4 111 | 112 | 113 | # it "must calculate zeroth power of rotation matrix", -> 114 | # m = M.rotationMatrix 0.6 115 | # mpow = M.powerPade m, 0.0 116 | # assert.ok M.approxEqv mpow, M.eye(), 1e-4 117 | 118 | # it "must calculate powers of identity matrix", -> 119 | # e = M.eye() 120 | 121 | # assert.ok M.approxEqv e, M.powerPade(e, 1.0) 122 | # assert.ok M.approxEqv e, M.powerPade(e, 0.5) 123 | # assert.ok M.approxEqv e, M.powerPade(e, 1.5) 124 | # assert.ok M.approxEqv e, M.powerPade(e, 0.0) 125 | 126 | # it "must calculate powers of zero matrix", -> 127 | # z = M.smul 0, M.eye() 128 | # assert.equal M.amplitude(z), 0 129 | 130 | # assert.ok M.approxEqv z, M.powerPade(z, 1.0) 131 | # assert.ok M.approxEqv z, M.powerPade(z, 0.5) 132 | # assert.ok M.approxEqv z, M.powerPade(z, 0.0) 133 | 134 | describe "amplitude", -> 135 | it "must return maximal absolute value of matrix element", -> 136 | m = [1,2,3,4,5,6,7,8,9] 137 | assert.equal M.amplitude(m), 9 138 | 139 | m = [1,-2,3,-4,5,-6,7,8,-9] 140 | assert.equal M.amplitude(m), 9 141 | 142 | m = [-9,2,3,4,5,6,7,8,1] 143 | assert.equal M.amplitude(m), 9 144 | 145 | m = [9,-2,3,-4,5,-6,7,8,1] 146 | assert.equal M.amplitude(m), 9 147 | 148 | m = [-3,2,3,9,5,6,7,8,1] 149 | assert.equal M.amplitude(m), 9 150 | 151 | m = [3,-2,3,-9,5,-6,7,8,1] 152 | assert.equal M.amplitude(m), 9 153 | 154 | 155 | describe "hyperbolicDecompose", -> 156 | 157 | almostEqual = (x, y, message) -> 158 | message = message ? "#{x} not appox equal #{y}" 159 | assert.ok Math.abs(x-y)<1e-6, message 160 | 161 | it "must decompose identity to zero translation and zero rotation", -> 162 | [rot, dx, dy] = M.hyperbolicDecompose M.eye() 163 | 164 | almostEqual rot, 0 165 | almostEqual dx, 0 166 | almostEqual dy, 0 167 | 168 | it "must decompose product of random nonzero translation and rotation", -> 169 | for attempt in [0...100] 170 | dx = Math.random()*10-5 171 | dy = Math.random()*10-5 172 | rot = (Math.random()*2-1)*Math.PI 173 | 174 | m = M.mul M.translationMatrix(dx,dy), M.rotationMatrix(rot) 175 | 176 | [rot1, dx1, dy1] = M.hyperbolicDecompose m 177 | 178 | message = """Incorrect decomposition. Code: 179 | [dx,dy,rot] = [#{dx}, #{dy}, #{rot}] 180 | m = M.mul M.translationMatrix(dx,dy), M.rotationMatrix(rot) 181 | rot1, dx1, dy1 = M.hyperbolicDecompose m 182 | #rot1 = #{rot1} 183 | #dx1 = #{dx1} 184 | #dy1 = #{dy1}""" 185 | 186 | almostEqual dx, dx1, message 187 | almostEqual dy, dy1, message 188 | drot = Math.abs(rot-rot1) 189 | assert.ok( (drot<1e-6) or Math.abs(drot-Math.PI*2)<1e-6, message ) 190 | 191 | 192 | describe "powers", -> 193 | 194 | it "must return array of N first powers of a matrix", -> 195 | 196 | a = [1,2,-1, 197 | 2,-1,0, 198 | -3,0,0] 199 | 200 | pows3 = M.powers a, 4 201 | 202 | assert.equal pows3.length, 4 203 | 204 | assert.ok M.approxEq pows3[0], M.eye() 205 | assert.ok M.approxEq pows3[1], a 206 | assert.ok M.approxEq pows3[2], M.mul(a,a) 207 | assert.ok M.approxEq pows3[3], M.mul(a,M.mul(a,a)) 208 | 209 | 210 | 211 | 212 | -------------------------------------------------------------------------------- /tests/test_new_group.coffee: -------------------------------------------------------------------------------- 1 | assert = require "assert" 2 | M = require "../src/core/matrix3.coffee" 3 | {VonDyck} = require "../src/core/vondyck.coffee" 4 | {RegularTiling} = require "../src/core/regular_tiling.coffee" 5 | 6 | # Knuth-Bendix solver for vonDyck groups, cleaned up API 7 | # 8 | 9 | describe "New API", -> 10 | 11 | it "Must work", -> 12 | group = new VonDyck 3, 4 13 | # aaa = bbbb = abab = 1 14 | 15 | assert.equal group.n, 3 16 | assert.equal group.m, 4 17 | assert.equal group.k, 2 18 | 19 | assert.equal group.type(), "spheric" #octahedron 20 | 21 | u = group.unity 22 | 23 | #Parsing and stringification 24 | x1 = u.a(1).b(-2).a(3) 25 | assert.equal x1.toString(), "aB^2a^3" 26 | 27 | x11 = group.parse "aB^2a^3" 28 | assert.ok x1.equals x11 29 | 30 | x12 = group.parse "A^3b2^A" 31 | assert.ok not x1.equals x12 32 | 33 | assert.ok u.equals(group.parse '') 34 | assert.ok u.equals(group.parse 'e') 35 | 36 | #Array conversion 37 | arr = u.a(2).b(-2).a(3).asStack() 38 | assert.deepEqual arr, [['a',3],['b',-2],['a',2]] 39 | 40 | #Normalization 41 | group.solve() 42 | 43 | x = group.appendRewrite group.unity, [['a',2],['b',3]] 44 | x1 = group.appendRewrite group.unity, [['a',2],['b',3]] 45 | x2 = group.appendRewrite group.unity, [['a',2],['a',1],['b',1],['a',1],['b',1],['b',-1]] 46 | 47 | x3 = group.rewrite u.b(3).a(2) 48 | 49 | assert.ok x.equals x1 50 | assert.ok x.equals x2 51 | assert.ok x.equals x3 52 | 53 | 54 | describe "New VonDyck", -> 55 | it "must detect group type", -> 56 | assert.equal (new VonDyck 3,3).type(), "spheric" 57 | assert.equal (new VonDyck 3,4).type(), "spheric" 58 | assert.equal (new VonDyck 3,5).type(), "spheric" 59 | assert.equal (new VonDyck 3,6).type(), "euclidean" #triangualr tiling 60 | assert.equal (new VonDyck 3,7).type(), "hyperbolic" #triangualr tiling 61 | 62 | assert.equal (new VonDyck 4,3).type(), "spheric" 63 | assert.equal (new VonDyck 4,4).type(), "euclidean" 64 | assert.equal (new VonDyck 4,5).type(), "hyperbolic" 65 | 66 | 67 | assert.equal (new VonDyck 5,3).type(), "spheric" 68 | assert.equal (new VonDyck 5,4).type(), "hyperbolic" 69 | 70 | describe "VonDyck.repr", -> 71 | group = new VonDyck 4, 5 72 | 73 | it "should return unity matrix for empty node", -> 74 | assert M.approxEq group.repr(group.unity), M.eye() 75 | assert not M.approxEq group.repr(group.parse "a"), M.eye() 76 | assert not M.approxEq group.repr(group.parse "b"), M.eye() 77 | assert M.approxEq group.repr(group.parse "abab"), M.eye() 78 | 79 | 80 | describe "VonDyck.inverse", -> 81 | n = 5 82 | m = 4 83 | group = new VonDyck n, m 84 | group.solve() 85 | unity = group.unity 86 | 87 | it "should inverse unity", -> 88 | assert unity.equals group.inverse unity 89 | 90 | it "should inverse simple 1-element values", -> 91 | c = group.parse "a" 92 | ic = group.parse "A" 93 | assert group.inverse(c).equals ic 94 | 95 | 96 | it "should return same chain after double rewrite", -> 97 | 98 | c = group.rewrite group.parse "ba^2B^2a^3b" 99 | ic = group.inverse c 100 | iic = group.inverse ic 101 | 102 | assert not c.equals ic 103 | assert c.equals iic 104 | 105 | describe "VonDyck.appendInverse", -> 106 | n = 5 107 | m = 4 108 | group = new VonDyck n, m 109 | group.solve() 110 | unity = group.unity 111 | 112 | it "unity * unity^-1 = unity", -> 113 | assert unity.equals group.appendInverse(unity, unity) 114 | 115 | it "For simple 1-element values, x * (x^-1) = unity", -> 116 | c = group.parse "a" 117 | assert group.appendInverse(c, c).equals unity 118 | 119 | it "For non-simple chain, x*(x^-1) = unitu", -> 120 | c = group.rewrite group.parse "ba^2B^2a^3b" 121 | assert not c.equals unity 122 | assert group.appendInverse(c, c).equals unity 123 | 124 | describe "VonDyck.append", -> 125 | n = 5 126 | m = 4 127 | group = new VonDyck n, m 128 | group.solve() 129 | unity=group.unity 130 | 131 | it "choud append unity", -> 132 | assert unity.equals group.append(unity, unity) 133 | 134 | c = group.parse 'a' 135 | assert c.equals group.append(c, unity) 136 | assert c.equals group.append(unity, c) 137 | 138 | it "shouls append inverse and return unity", -> 139 | 140 | c = group.rewrite unity.appendStack [['b',1],['a',2],['b',-2],['a',3],['b',1]] 141 | 142 | ic = group.inverse c 143 | 144 | assert unity.equals group.append c, ic 145 | assert unity.equals group.append ic, c 146 | 147 | 148 | describe "RegularTiling", -> 149 | it "must support cell coordinate normalization", -> 150 | tiling = new RegularTiling 3, 4 151 | #last A elimination 152 | x = tiling.parse "bab" #eliminated to 1 by adding a: bab+a = baba = e 153 | assert.ok tiling.toCell(x).equals(tiling.unity) 154 | 155 | 156 | checkTrimmingIsUnique = (chain) -> 157 | trimmedChain = tiling.toCell chain 158 | for aPower in [-tiling.n .. tiling.n] 159 | if aPower is 0 then continue 160 | chain1 = chain.a(aPower) 161 | if not tiling.toCell(chain1).equals(trimmedChain) 162 | throw new Error "Chain #{chain1} trimmed returns #{tiling.toCell chain1} != #{trimmedChain}" 163 | 164 | 165 | checkTrimmingIsUnique tiling.parse "e" 166 | checkTrimmingIsUnique tiling.parse "a" 167 | checkTrimmingIsUnique tiling.parse "A" 168 | checkTrimmingIsUnique tiling.parse "b" 169 | checkTrimmingIsUnique tiling.parse "B" 170 | checkTrimmingIsUnique tiling.parse "ba^2ba^2B" 171 | checkTrimmingIsUnique tiling.parse "Ba^3bab^2" 172 | 173 | describe "RegularTiling.moore", -> 174 | #prepare data: rewriting ruleset for group 5;4 175 | # 176 | [n, m] = [5, 4] 177 | tiling = new RegularTiling n, m 178 | unity = tiling.unity 179 | cells = [] 180 | 181 | cells.push unity 182 | cells.push tiling.toCell tiling.rewrite tiling.parse "b" 183 | cells.push tiling.toCell tiling.rewrite tiling.parse "b^2" 184 | cells.push tiling.toCell tiling.rewrite tiling.parse "ab^2" 185 | 186 | it "must return expected number of cells different from origin", -> 187 | for cell in cells 188 | neighbors = tiling.moore cell 189 | assert.equal neighbors.length, n*(m-2) 190 | 191 | for nei, i in neighbors 192 | assert not cell.equals nei 193 | 194 | for nei1, j in neighbors 195 | if i isnt j 196 | assert not nei.equals(nei1), "neighbors #{i}=#{nei1} and #{j}=#{nei1} must be not equal" 197 | return 198 | 199 | it "must be true that one of neighbor's neighbor is self", -> 200 | for cell in cells 201 | for nei in tiling.moore cell 202 | foundCell = 0 203 | for nei1 in tiling.moore nei 204 | if nei1.equals cell 205 | foundCell += 1 206 | assert.equal foundCell, 1, "Exactly 1 of the #{nei}'s neighbors must be original chain: #{cell}, but #{foundCell} found" 207 | return 208 | 209 | describe "RegularTiling.forFarNeighborhood", -> 210 | tiling = new RegularTiling 5, 4 211 | unity = tiling.unity 212 | #Make normalized node from array 213 | norm = (arr) -> tiling.toCell tiling.appendRewrite unity, arr 214 | 215 | chain1 = norm [['b',1], ['a', 2]] 216 | 217 | assert not chain1.equals unity 218 | 219 | it "should start enumeration from the original cell", -> 220 | tiling.forFarNeighborhood unity, (node, radius) -> 221 | assert.equal radius, 0, "Must start from 0 radius" 222 | assert.ok node.equals(unity), "Must start from the center" 223 | #Stop after the first. 224 | return false 225 | 226 | tiling.forFarNeighborhood chain1, (node, radius) -> 227 | assert.equal radius, 0, "Must start from 0 radius" 228 | assert.ok node.equals(chain1), "Must start from the center" 229 | #Stop after the first. 230 | return false 231 | 232 | it "should produce all different cells in strictly increasing order", -> 233 | visitedNodes = [] 234 | lastLevel = 0 235 | tiling.forFarNeighborhood chain1, (node, level) -> 236 | assert.ok (level is lastLevel) or (level is lastLevel+1) 237 | for visited in visitedNodes 238 | assert.ok not visited.equals node 239 | visitedNodes.push node 240 | lastLevel = level 241 | return level < 6 242 | 243 | assert.equal lastLevel, 6 244 | assert.ok visitedNodes.length > 10 245 | 246 | describe "RegularTiling.mooreNeighborhood", -> 247 | #prepare data: rewriting ruleset for group 5;4 248 | # 249 | [n, m] = [5, 4] 250 | tiling = new RegularTiling n, m 251 | unity = tiling.unity 252 | rewriteChain = (arr) -> tiling.appendRewrite unity, arr[..] 253 | 254 | cells = [] 255 | cells.push unity 256 | cells.push tiling.toCell rewriteChain [['b',1]] 257 | cells.push tiling.toCell rewriteChain [['b', 2]] 258 | cells.push tiling.toCell rewriteChain [['b', 2],['a', 1]] 259 | 260 | it "must return expected number of cells different from origin", -> 261 | for cell in cells 262 | neighbors = tiling.moore cell 263 | assert.equal neighbors.length, n*(m-2) 264 | 265 | for nei, i in neighbors 266 | assert not cell.equals nei 267 | 268 | for nei1, j in neighbors 269 | if i isnt j 270 | assert not nei.equals(nei1), "neighbors #{i}=#{nei1} and #{j}=#{nei1} must be not equal" 271 | return 272 | 273 | it "must be true that one of neighbor's neighbor is self", -> 274 | for cell in cells 275 | for nei in tiling.moore cell 276 | foundCell = 0 277 | for nei1 in tiling.moore nei 278 | if nei1.equals cell 279 | foundCell += 1 280 | assert.equal foundCell, 1, "Exactly 1 of the #{nei}'s neighbors must be original chain: #{cell}, but #{foundCell} found" 281 | return 282 | -------------------------------------------------------------------------------- /tests/test_parseuri.coffee: -------------------------------------------------------------------------------- 1 | {parseUri} = require "../src/ui/parseuri.coffee" 2 | assert = require "assert" 3 | 4 | describe "parseUri", -> 5 | it "should parse URI with parameters", -> 6 | uri = "https://sample.org/folder/root.html?p1=1&p2=2&p3=hello" 7 | 8 | parsed = parseUri uri 9 | 10 | assert.equal parsed.anchor, '' 11 | assert.equal parsed.query, 'p1=1&p2=2&p3=hello' 12 | assert.equal parsed.file, 'root.html' 13 | assert.equal parsed.directory, '/folder/' 14 | assert.equal parsed.path, '/folder/root.html' 15 | assert.equal parsed.relative, '/folder/root.html?p1=1&p2=2&p3=hello' 16 | assert.equal parsed.port, '' 17 | assert.equal parsed.host, 'sample.org' 18 | assert.equal parsed.password, '' 19 | assert.equal parsed.user, '' 20 | assert.equal parsed.userInfo, '' 21 | assert.equal parsed.authority, 'sample.org' 22 | assert.equal parsed.protocol, 'https' 23 | assert.equal parsed.source, 'https://sample.org/folder/root.html?p1=1&p2=2&p3=hello' 24 | 25 | assert.equal parsed.queryKey.p1, '1' 26 | assert.equal parsed.queryKey.p2, '2' 27 | assert.equal parsed.queryKey.p3, 'hello' 28 | -------------------------------------------------------------------------------- /tests/test_poincare_view.coffee: -------------------------------------------------------------------------------- 1 | assert = require "assert" 2 | 3 | {RegularTiling} = require "../src/core/regular_tiling.coffee" 4 | #{unity} = require "../src/core/vondyck_chain.coffee" 5 | {visiblePolygonSize} = require "../src/core/poincare_view.coffee" 6 | 7 | describe "visiblePolygonSize", -> 8 | 9 | tiling = new RegularTiling 5, 4 10 | 11 | cellPolygonSize = (chain) -> 12 | visiblePolygonSize tiling, tiling.repr(chain) 13 | 14 | it "must be positive, nonzero", -> 15 | assert 0 < cellPolygonSize tiling.unity 16 | assert 0 < cellPolygonSize tiling.parse "a" 17 | assert 0 < cellPolygonSize tiling.parse "b" 18 | 19 | it "must decrease when distance is increasing", -> 20 | 21 | size_0 = cellPolygonSize tiling.unity 22 | size_b1 = cellPolygonSize tiling.parse "b" 23 | 24 | assert size_b1 < size_0 25 | 26 | 27 | it "must not change only from rotation of the polygon", -> 28 | 29 | size_0 = cellPolygonSize tiling.unity 30 | size_a1 = cellPolygonSize tiling.parse "a" 31 | 32 | assert Math.abs(size_a1 - size_0) < 1e-3 33 | 34 | 35 | -------------------------------------------------------------------------------- /tests/test_triangle_group_representation.coffee: -------------------------------------------------------------------------------- 1 | {TriangleGroup, CenteredVonDyck} = require "../src/core/triangle_group_representation" 2 | M = require "../src/core/matrix3" 3 | assert = require "assert" 4 | 5 | 6 | powm = (m, n) -> 7 | mp = M.eye() 8 | for i in [0...n] 9 | mp = M.mul( mp, m) 10 | return mp 11 | 12 | 13 | # describe "TriangleGroup", -> 14 | 15 | # g = new TriangleGroup 2,3,5 16 | 17 | # it "must return non-identity matrices", -> 18 | # assert.ok not M.approxEq g.m_pqr[0], M.eye() 19 | # assert.ok not M.approxEq g.m_pqr[1], M.eye() 20 | # assert.ok not M.approxEq g.m_pqr[2], M.eye() 21 | 22 | # it "must have idempotent generators", -> 23 | # assert.ok M.approxEq M.eye(), M.mul(g.m_pqr[0], g.m_pqr[0]) 24 | # assert.ok M.approxEq M.eye(), M.mul(g.m_pqr[1], g.m_pqr[1]) 25 | # assert.ok M.approxEq M.eye(), M.mul(g.m_pqr[2], g.m_pqr[2]) 26 | 27 | # it "must give rotations for pairs fo different generators", -> 28 | # [p,q,r]= g.m_pqr 29 | # pq = M.mul p, q 30 | # pr = M.mul p, r 31 | # qr = M.mul q, r 32 | 33 | # assert.ok M.approxEq powm(pq,2), M.eye() 34 | # assert.ok M.approxEq powm(qr,3), M.eye() 35 | # assert.ok M.approxEq powm(pr,5), M.eye() 36 | 37 | # assert.ok not M.approxEq powm(pq,1), M.eye() 38 | # assert.ok not M.approxEq powm(qr,2), M.eye() 39 | # assert.ok not M.approxEq powm(pr,4), M.eye() 40 | 41 | 42 | 43 | 44 | describe "CenteredVonDyck(5,4)", -> 45 | g = new CenteredVonDyck(5,4) 46 | it "must produce generators with expected properties", -> 47 | assert.ok not M.approxEq g.a, M.eye() 48 | assert.ok not M.approxEq g.b, M.eye() 49 | 50 | assert.ok M.approxEq powm(g.a, 5), M.eye() 51 | assert.ok M.approxEq powm(g.b, 4), M.eye() 52 | 53 | ab = M.mul(g.a, g.b) 54 | 55 | assert.ok not M.approxEq ab, M.eye() 56 | assert.ok M.approxEq powm(ab,2), M.eye() 57 | 58 | it "must have stable point of A at (0,0,1)", -> 59 | v0 = [0.0, 0.0, 1.0] 60 | assert.ok M.approxEqv v0, v0 61 | assert.ok M.approxEqv v0, M.mulv(g.a, v0) 62 | assert.ok not M.approxEqv v0, M.mulv(g.b, v0) 63 | 64 | it "must provide coordinates of stable point of B", -> 65 | v1 = [g.sinh_r, 0, g.cosh_r] 66 | assert.ok M.approxEqv v1, v1 67 | assert.ok M.approxEqv v1, M.mulv(g.b, v1) 68 | assert.ok not M.approxEqv v1, M.mulv(g.a, v1) 69 | 70 | 71 | describe "CenteredVonDyck(5,4,3)", -> 72 | g = new CenteredVonDyck(5,4,3) 73 | it "must produce generators with expected properties", -> 74 | assert.ok not M.approxEq g.a, M.eye() 75 | assert.ok not M.approxEq g.b, M.eye() 76 | 77 | assert.ok M.approxEq powm(g.a, 5), M.eye() 78 | assert.ok M.approxEq powm(g.b, 4), M.eye() 79 | 80 | ab = M.mul(g.a, g.b) 81 | 82 | assert.ok not M.approxEq ab, M.eye() 83 | assert.ok M.approxEq powm(ab,3), M.eye() 84 | 85 | it "must have stable point of A at (0,0,1)", -> 86 | v0 = [0.0, 0.0, 1.0] 87 | assert.ok M.approxEqv v0, v0 88 | assert.ok M.approxEqv v0, M.mulv(g.a, v0) 89 | assert.ok not M.approxEqv v0, M.mulv(g.b, v0) 90 | 91 | it "must provide coordinates of stable point of B", -> 92 | v1 = [g.sinh_r, 0, g.cosh_r] 93 | assert.ok M.approxEqv v1, v1 94 | assert.ok M.approxEqv v1, M.mulv(g.b, v1) 95 | assert.ok not M.approxEqv v1, M.mulv(g.a, v1) 96 | 97 | 98 | 99 | describe "CenteredVonDyck.centerA/B/C", -> 100 | isNormalVector = ([x,y,t])->Math.abs(t**2-x**2-y**2-1) < 1e-5 101 | 102 | 103 | groups = [ [5,4,3], [5,4,2], [3,7,2], [7,3,2] ] 104 | 105 | for [n,m,k] in groups 106 | g = new CenteredVonDyck n,m,k 107 | 108 | it "must be located on the hyperboloid for group #{g}", -> 109 | assert isNormalVector g.centerA 110 | assert isNormalVector g.centerB 111 | assert isNormalVector g.centerAB 112 | 113 | it "must not be changed by the corresponding transforms for group #{g}",-> 114 | 115 | assert M.approxEqv g.centerA, M.mulv(g.a,g.centerA) 116 | assert M.approxEqv g.centerB, M.mulv(g.b,g.centerB) 117 | 118 | ab = M.mul(g.a, g.b) 119 | assert M.approxEqv g.centerAB, M.mulv(ab,g.centerAB) 120 | 121 | 122 | 123 | -------------------------------------------------------------------------------- /tests/test_utils.coffee: -------------------------------------------------------------------------------- 1 | assert = require "assert" 2 | 3 | {formatString, pad, mod} = require "../src/core/utils.coffee" 4 | 5 | describe "formatString", -> 6 | it "must format string with several args", -> 7 | out = formatString "{0} is dead, but {1} is alive! {0} {2}", ["ASP", "ASP.NET"] 8 | expect = "ASP is dead, but ASP.NET is alive! ASP {2}" 9 | assert.equal out, expect 10 | 11 | describe "pad", -> 12 | it "must pad small nums", -> 13 | assert.equal pad(4,4), "0004" 14 | assert.equal pad(4,5), "00004" 15 | 16 | assert.equal pad(123,5), "00123" 17 | 18 | it "must not truncate", -> 19 | assert.equal pad(123,2), "123" 20 | assert.equal pad(123,1), "123" 21 | 22 | describe "mod", -> 23 | it "must calculate modulo of positive values", -> 24 | assert.equal 0, mod 0, 5 25 | assert.equal 1, mod 1, 5 26 | assert.equal 2, mod 2, 5 27 | 28 | assert.equal 0, mod 25, 5 29 | assert.equal 1, mod 26, 5 30 | assert.equal 2, mod 27, 5 31 | 32 | it "must calculate modulo of negative values mathematically", -> 33 | assert.equal 0, mod -25, 5 34 | assert.equal 1, mod -24, 5 35 | assert.equal 2, mod -23, 5 36 | 37 | -------------------------------------------------------------------------------- /tests/test_vondyck_chain.coffee: -------------------------------------------------------------------------------- 1 | assert = require "assert" 2 | {unity, reverseShortlexLess, NodeA, NodeB, newNode, parseChain} = require "../src/core/vondyck_chain.coffee" 3 | 4 | describe "chain.equals", -> 5 | it "should return true for empty chains", -> 6 | assert unity.equals unity 7 | it "should return false for comparing non-empty with empty", -> 8 | a1 = new NodeA(1,unity) 9 | b1 = new NodeB(1,unity) 10 | assert not unity.equals a1 11 | assert not a1.equals unity 12 | assert not unity.equals b1 13 | assert not b1.equals unity 14 | 15 | it "should correctly compare chains of length 1", -> 16 | a1 = new NodeA(1,unity) 17 | a1_ = new NodeA(1,unity) 18 | 19 | b1 = new NodeB(1,unity) 20 | 21 | a2 = new NodeA(2,unity) 22 | 23 | 24 | assert a1.equals a1 25 | assert a1.equals a1_ 26 | assert a1_.equals a1 27 | 28 | 29 | assert not a1.equals a2 30 | assert not a2.equals a1 31 | assert not a1.equals b1 32 | assert not b1.equals a1 33 | 34 | it "should compare chains of length 2 and more", -> 35 | 36 | a1b1 = new NodeA(1, new NodeB(1, unity)) 37 | a1b2 = new NodeA(1, new NodeB(2, unity)) 38 | a1b1a3 = new NodeA(1, new NodeB(1, new NodeA(3, unity))) 39 | 40 | assert a1b1.equals a1b1 41 | assert not a1b1.equals a1b2 42 | assert not a1b1.equals a1b1a3 43 | assert not a1b1.equals unity 44 | 45 | describe "Node.hash", -> 46 | isNumber = (x) -> parseInt(''+x, 10) is x 47 | it "must return different values for empty node, nodes of lenght 1", -> 48 | e = unity 49 | a1 = newNode('a', 1, unity) 50 | a1b1 = newNode('a', 1, newNode('b', 1, unity)) 51 | a2 = newNode('a', 2, unity) 52 | b1 = newNode('b', 1, unity) 53 | b2 = newNode('b', 2, unity) 54 | 55 | chains = [e, a1, a2, b1, b2, a1b1] 56 | 57 | for c in chains 58 | assert isNumber c.hash() 59 | 60 | for c1, i in chains 61 | for c2, j in chains 62 | if i isnt j 63 | assert.notEqual c1.hash(), c2.hash(), "H #{c1} != H #{c2}" 64 | 65 | 66 | describe "Chain.toStr", -> 67 | it "should convert node to text", -> 68 | assert.equal 'e', ""+unity 69 | assert.equal 'a', "" + (newNode 'a', 1, unity) 70 | assert.equal 'A', "" + (newNode 'a', -1, unity) 71 | assert.equal 'b', "" + (newNode 'b', 1, unity) 72 | assert.equal 'B', "" + (newNode 'b', -1, unity) 73 | assert.equal 'a^3', "" + (newNode 'a', 3, unity) 74 | assert.equal 'Aba^3', "" + (newNode 'a', 3, newNode 'b',1, newNode 'a', -1, unity) 75 | 76 | describe "parseChain", -> 77 | it "should convert node to text", -> 78 | assert.ok parseChain('e').equals unity 79 | assert.ok parseChain('a').equals newNode 'a', 1, unity 80 | assert.ok parseChain('A').equals newNode 'a', -1, unity 81 | assert.ok parseChain('b').equals newNode 'b', 1, unity 82 | assert.ok parseChain('B').equals newNode 'b', -1, unity 83 | assert.ok parseChain('a^3').equals newNode 'a', 3, unity 84 | assert.ok parseChain('Aba^3').equals newNode 'a', 3, newNode 'b',1, newNode 'a', -1, unity 85 | 86 | 87 | 88 | describe "reverseShortlexLess", -> 89 | chain_a = newNode('a',1,unity) 90 | chain_B = newNode('b',-1,unity) 91 | chain_Baa = newNode('b',-1,newNode('a',2,unity)) 92 | it "should return false for equal chains", -> 93 | assert not reverseShortlexLess unity, unity 94 | assert not reverseShortlexLess chain_a, chain_a 95 | assert not reverseShortlexLess chain_B, chain_B 96 | assert not reverseShortlexLess chain_Baa, chain_Baa 97 | 98 | it "should compare chains of different len", -> 99 | assert reverseShortlexLess unity, chain_a 100 | assert reverseShortlexLess unity, chain_B 101 | assert reverseShortlexLess unity, chain_Baa 102 | 103 | assert not reverseShortlexLess chain_a, unity 104 | assert not reverseShortlexLess chain_B, unity 105 | assert not reverseShortlexLess chain_Baa, unity 106 | 107 | assert reverseShortlexLess chain_a, chain_Baa 108 | assert not reverseShortlexLess chain_Baa, chain_a 109 | 110 | 111 | it "should compare chains of same len", -> 112 | assert reverseShortlexLess chain_a, chain_B 113 | assert not reverseShortlexLess chain_B, chain_a 114 | 115 | 116 | 117 | -------------------------------------------------------------------------------- /tests/test_vondyck_rewriter.coffee: -------------------------------------------------------------------------------- 1 | assert = require "assert" 2 | {RewriteRuleset} = require "../src/core/knuth_bendix.coffee" 3 | {string2chain, chain2string, makeAppendRewriteRef, makeAppendRewrite, extendLastPowerRewriteTable} = require "../src/core/vondyck_rewriter.coffee" 4 | {unity, newNode} = require "../src/core/vondyck_chain.coffee" 5 | 6 | describe "string2chain", -> 7 | it "must convert empty string", -> 8 | assert.equal unity, string2chain "" 9 | 10 | it "must convert nonempty strings", -> 11 | assert string2chain("a").equals newNode('a', 1, unity) 12 | assert string2chain("A").equals newNode('a', -1, unity) 13 | assert string2chain("aa").equals newNode('a', 2, unity) 14 | assert string2chain("AA").equals newNode('a', -2, unity) 15 | 16 | it "must convert complex chains", -> 17 | #Todo 18 | 19 | 20 | 21 | describe "chain2string", -> 22 | it "must convert empty chain", -> 23 | assert.equal chain2string(unity), "" 24 | it "must convert simple nonempty chain", -> 25 | assert.equal chain2string(newNode('a', 2, unity)), "aa" 26 | assert.equal chain2string(newNode('a', -3, unity)), "AAA" 27 | assert.equal chain2string(newNode('b', 1, unity)), "b" 28 | 29 | it "must convert complex nonempty chain", -> 30 | c = newNode('a', -1, newNode('b', -3, newNode('a', 2, unity))) 31 | assert.equal chain2string(c), "aaBBBA" 32 | 33 | 34 | describe "Compiled rewriter", -> 35 | rewriteTable = new RewriteRuleset { 36 | aaBaa: 'AAbAA' 37 | ABaBA: 'bAAb' 38 | bb: 'BB' 39 | bAB: 'BBa' 40 | aaa: 'AA' 41 | AAA: 'aa' 42 | ab: 'BA' 43 | aBB: 'BAb' 44 | ba: 'AB' 45 | Bb: '' 46 | bB: '' 47 | Aa: '' 48 | aA: '' 49 | BBB: 'b' 50 | BAB: 'a' 51 | ABA: 'b' 52 | aaBA: 'AAb' 53 | ABaa: 'bAA' 54 | aaBaBA: 'AAbAAb' 55 | bAAbAA: 'ABaBaa' } 56 | n=5 57 | m=4 58 | 59 | 60 | refRewriter = makeAppendRewriteRef rewriteTable 61 | compiledRewriter = makeAppendRewrite rewriteTable 62 | 63 | doTest = ( stack, chain0=unity ) -> 64 | #console.log "should stringify #{JSON.stringify stack}" 65 | #console.log "#{chainRef} != #{chain}" 66 | chainRef = refRewriter chain0, stack[..] 67 | chain = compiledRewriter chain0, stack[..] 68 | assert (chainRef.equals chain), "#{chain0} ++ #{JSON.stringify stack} -> #{chainRef} (ref) != #{chain}" 69 | return 70 | 71 | walkChains = (stack, depth, callback) -> 72 | callback stack 73 | for a in [1...n] 74 | stack.push ['a',a] 75 | callback stack 76 | 77 | for b in [1...m] 78 | stack.push ['b', b] 79 | callback stack 80 | if depth > 0 81 | walkChains stack, depth-1, callback 82 | stack.pop() 83 | stack.pop() 84 | return 85 | 86 | #it "must produce same result as reference rewriter", -> 87 | # walkChains [], 3, doTest 88 | #chain = ba^-2b, stack: [["a",1]], refValue: a^-1b^-1ab^-1, value: ba^2b^-1 89 | doTest [["a",1]], newNode('b',1, newNode('a',-2, newNode('b',1,unity))) 90 | 91 | describe "extendLastPowerRewriteTable", -> 92 | it "must extend positive powers", -> 93 | r = new RewriteRuleset { 'ab': 'Ba', 'ba': 'B' } 94 | r1 = extendLastPowerRewriteTable r.copy(), 'a', -3, 3 95 | 96 | r1_expected = new RewriteRuleset 97 | 'ab': 'Ba', 98 | 'ba': 'B', 99 | #new ruels: 100 | 'baa': 'Ba', 101 | 'baaa': 'Baa' 102 | 103 | assert not r.equals(r1), "Extended ruleset is not equal to original, #{JSON.stringify r1} != #{JSON.stringify r}" 104 | assert r1.equals(r1_expected), "Extended ruleset is equal to expected, #{JSON.stringify r1} != #{JSON.stringify r1_expected}" 105 | 106 | it "must extend negative powers", -> 107 | r = new RewriteRuleset { 'ab': 'Ba', 'bA': 'B' } 108 | r1 = extendLastPowerRewriteTable r.copy(), 'a', -3, 3 109 | 110 | r1_expected = new RewriteRuleset 111 | 'ab': 'Ba', 112 | 'bA': 'B', 113 | #new ruels: 114 | 'bAA': 'BA', 115 | 'bAAA': 'BAA' 116 | 117 | assert not r.equals(r1), "Extended ruleset is not equal to original, #{JSON.stringify r1} != #{JSON.stringify r}" 118 | assert r1.equals(r1_expected), "Extended ruleset is equal to expected, #{JSON.stringify r1} != #{JSON.stringify r1_expected}" 119 | -------------------------------------------------------------------------------- /uploads/README.md: -------------------------------------------------------------------------------- 1 | Uploads folder 2 | -------------- 3 | --------------------------------------------------------------------------------