├── .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 |
11 |
12 |
13 |
14 |
15 |
16 |
17 |
18 | Your browser does not support HTML5 video.
19 |
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 |
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 |
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 |
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 |
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 |
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 |
138 | Uploading animations is only supported, when the page is run locally. To do it:
139 |
140 | Download sources
141 | $get clone https://github.com/dmishin/hyperbolic-ca-simulator.git
142 |
143 | 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 |
149 | Use python 3 to start local server
150 | $python http_server_with_upload.py
151 |
152 | Open the local site: http://localhost:8000/index.html
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 | Locate a spaceship (use Search and find far configurations)
164 | Adjust view to display it better, then use "Straighten view". (hotkey: Alt+S)
165 | Remember world state using MS (hotkey: M).
166 | Set animation start
167 | Simulate for several steps until spaceship returns into the original configuration (hotkey: N).
168 | Adjust view and use "Straighten view" to put spaceship exactly in the same position as in step #2.
169 | Set animation end
170 | 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.
171 |
172 | 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"\nUpload 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(("\nDirectory listing for %s \n" % displaypath).encode())
204 | f.write(b" \n")
205 | #f.write(b"\n")
209 | f.write(b" \n\n")
210 | for name in list:
211 | fullname = os.path.join(path, name)
212 | displayname = linkname = name
213 | # Append / for directories or @ for symbolic links
214 | if os.path.isdir(fullname):
215 | displayname = name + "/"
216 | linkname = name + "/"
217 | if os.path.islink(fullname):
218 | displayname = name + "@"
219 | # Note: a link to a directory displays with @ and links with /
220 | f.write(('%s \n'
221 | % (urllib.parse.quote(linkname), cgi.escape(displayname))).encode())
222 | f.write(b" \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 |
10 |
11 |
12 |
13 |
14 |
15 |
16 |
115 |
116 |
117 |
118 |
119 |
120 | Reset
121 | Random
122 |
123 |
124 | Step
125 | Go
126 | Stop
127 |
128 |
129 | Home
130 |
131 |
132 |
133 | States:
134 |
135 |
136 |
137 |
138 |
139 |
140 | Pan
141 |
142 |
143 | Edit
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 |
--------------------------------------------------------------------------------