-
Notifications
You must be signed in to change notification settings - Fork 11
Roundness
Click on the Compute roundness file button creates a file with the roundness measures, from the code in Features/Roundness.js.
Try to run the stabilization of Max stable + Identity on some tiling, and you will understand why we are talking about some "roundness".
The formal definition of roundness is given is this arXiv article. Given a configuration, the set of tiles is partitioned into:
-
outerTilesforTileObjectthavingt.sand = t.limit-1grains and connected to the border by a path of tiles also having.sand = .limit-1, -
innerTilesfor all otherTileObjects.
Then:
- the
outerRadiusis the biggest radius of a circle centered on(0,0)and having allouterTilesin its exterior (inscribed), - the
innerRadiusis the smallest radius of a circle centered on(0,0)and having allinnerTilesin its interior (circumscribed).
The roundness of the configuration is the difference innerRadius - outerRadius.
The event listener calls asynchronously (await) the main function makeRoundnessFile on the current Tiling (currentTiling), which returns a file containing the roundness measures, formatted as one line of the form innerRadius/outerRadius/roundness, per step (we plan to create graphics using our beloved tikz), from Max stable + Identity to Max stable (included).
Since tiles are polygons, the radius of a circumscribed circle (which is convex, that's the trick) can simply be computed as a Math.max on the distances of each tile bounds to coordinate (0,0), themselves computed with distance(xA,yA,xB,yB) from Utils/Geometry.js. However the inscribed circle may be tangent to some tile edge! To this purpose we have implemented distancePointSegment(x,y,xA,yA,xB,yB) in Utils/Geometry.js
Computes the radius of the inscribed and circumscribed circles of the entire tiling (all tiles). The former is an upper bound on the outer radius, and the latter is an upper bound on the inner radius.
The annoying part of this is to recover which edges of border tiles are on the border of the tiling, in order to compute the inscribed circle (see Technical considerations). Once borderEdges contains the pairs of pairs of coordinates corresponding to tile edges on the border of the tiling, its just:
let borderEdgeDistances = borderEdges.map(e => distancePointSegment(0,0,e[0][0],e[0][1],e[1][0],e[1][1]));
let inscribed = Math.min(...borderEdgeDistances);
let borderDistances = borderEdges.flat(1).map(b => distance(0,0,b[0],b[1]));
let circumscribed = Math.max(...borderDistances);First set up Max stable + Identity.
Get the value of checkbox roundShow in show_roundness, whether to animate the roundness or not.
After computing the roundness (call to tiling.get_roundness()) and then tiling.iterate():
- add
three.LineLooptoapp.scene, - wait for
delay, - update
tiling.colorTiles().
It precomputes some global variables (see below), initialize the others, and then the main loop:
- iterate the tiling (
tiling.iterate()) until stabilization, measuring the roundness at each step by callingtiling.get_roundness().
Prints some logs, and compile roundness measure in a String roundness_file_text
in order to return the intended roundness_file.
The main technicalities are in tiling.get_roundness(), which relies on some global variables to speed up the computation.
Computed once by makeRoundnessFile:
-
borderTiles, tiles touching the border (ids only, which are integers), -
smallestDistancedict, smallest distance of each tile to(0,0)(this also has to do with tile edges, see Technical considerations), -
biggestDistancedict, biggest distance of each tile to(0,0), -
inscribedCircleRadius, radius of inscribed circle for the whole tiling (call tocircles_raddi), -
circumscribedCircleRadius, radius of circumscribed circle for the whole tiling (call tocircles_raddi).
Updated at each step by tiling.get_roundness():
-
phase = 1;,get_roundnessis separated into three subsequent phases according to this, -
outerTiles, the Array of allouterTiles(ids only, which are integers), -
frontierTiles, the Array ofouterTileshaving a neighbor ininnerTiles.
The method get_roundness() is added to the class Tiling. It relies on global variables (see above), and returns [outerRadius,innerRadius] for the current configuration.
The procedure is separated into three phases (phase transitions are printed in the log), according to the value of phase:
- Phase 1: erratic behavior,
- Phase 2: inner radius smaller than inscribed circle radius,
- Phase 3: border tiles are all outer tiles.
Remark that we can intuitively expect a direct transition from phase 1 to phase 3
when the condition of phase 2 is met
(because tiles are polygons, but r_error may mess it up?)
Also Remark that no phase regression is expected,
otherwise an outer tiles (from frontierTiles) receives some grain and triggers the toppling of all outerTiles.
Phase regressions are nevertheless handled.
Tile Objects and their attribute values are recovered from this.tiles[id] (where id are integers).
In this case the sandpile behavior is still erratic, so outerTiles and frontierTiles are simply recomputed from scratch.
- A FIFO Array of tiles
tileStackis used to discover by BFS the set ofouterTiles. -
frontierTilesare found by scanning thesandcontent ofouterTiles'neighbors. - set
innerTiles_touches_border = (borderTiles.filter(id => !outerTiles.includes(id)).length == 0);.
Phase transition is simply checked as
if(innerRadius <= inscribedCircleRadius+r_error){where r_error is the rounding error set to 0.001.
During this phase the roundness is computed as in phase 1.
Phase transition is checked as
if(frontierTiles.filter(id => this.tiles[id].sand != this.tiles[id].limit-1).length == 0){The circle now shrinks.
If it does not, meaning that some tile from
frontierTilesof the last iteration is not anymore anouterTiles(checked withfrontierTiles.filter(id => this.tiles[id].sand != this.tiles[id].limit-1).length > 0), then we recompute from scratch by calling phase 2 (phase transition is not logged, its just a hack). This is unexpected, as it would trigger the toppling of allouterTiles, and regression to phase 2 then 1.
- Discover new
outerTileswith a FIFO+BFS as in phase 1 and 2, starting fromfrontierTiles. -
frontierTilesare found as in phase 1, fromfrontierTilesand newouterTilesonly to fasten the process. - Check regression to phase 2 or 1, even if this should never happen when we are here: regression may happen during the hack above (calling phase 2).
-
outerRadiusis basically computed asMath.minoffrontierTiles.map(id => smallestDistancedict.get(id)), with some special treatments whenouterTilesis empty or contains all tiles (in this later casefrontierTilesis empty andMath.minwould return-Inifnity...). -
innerRadiusis computed from a subset ofinnerTilescalledinnerTiles_sub, and made fromborderTilesplus neighbors offrontierTiles. Then it isMath.maxofinnerTiles_sub.map(id => biggestDistancedict.get(id)), with a special treatment wheninnerTilesis empty. - Phase transitions are checked.
In order to improve the speed, makeRoundnessFileFast(tiling) is a faster version of roundness, computing only phases 2 and 3:
- iterate phase 1 until phase 2 is reached, checked faster thanks to a pre-check:
-
inscribedTilesis a subset of tiles (onlyid) required to be outerTiles in order to haveinnerRadius <= inscribedCircleRadius, - pre-check that they may all be
outerTileswith
if(inscribedTiles.filter(id => tiling.tiles[id].sand != tiling.tiles[id].limit-1).length == 0){
- if pre-check passed, call
.get_roundness()to updatephaseby side-effect.
-
- check that phase 3 is immediately reached, otherwise fail (log and
return;). Because our main optimizations are based on the fast that the circle slowly shrinks, and that this can be discovered locally fromfrontierTiles. - then record roundness, expecting no regression of
frontierTiles, i.e.frontierTilesfrom the previous iteration are stillouterTilesof the current iteration (otherwise a warning message is logged and the behavior is not guaranteed).
The first line of the output file contains the starting step of phase 2.
Some experiments (see lines with //TIME, now fully in comments) led to the conclusion that updating outerTiles initially took more than 90% of makeRoundnessFileFast execution time. We therefore made the following optimizations on the BFS search for new outerTiles based on tileStack:
-
main: use
outerTiles_submaintained as the subset of outerTiles being frontierTiles or neighbors of frontierTiles (the main slowdown was caused by some use ofouterTiles.includes(id)for each element oftileStack, butouterTileswas growing until containing all tiling's tiles), - start the
tileStackonly with the neighbors offrontierTileswhich are notouterTiles(not inouterTiles_sub), this uses our assumption about no regression offrontierTiles, - add no already known
outerTiles(outerTiles_sub) totileStack, - remove duplicates in initializations of
tileStackandouterTiles_sub.
It is now 4 to 5 times faster than initially ✌️
The slider button calls radii() to display (in Control bar) and show (in canvas) the inscribed and circumscribed radii of the current tiling.
The slider button calls roundness() to display (in Control bar) and show (in canvas) the outer and inner radii of the current configuration.
Calls export_frontierTikz() to export the frontier (edges between inner and outer tiles) of the current configuration to tikz, with outer and inner radii.