File Coverage

blib/lib/Math/PlanePath/GosperIslands.pm
Criterion Covered Total %
statement 186 217 85.7
branch 30 46 65.2
condition 4 5 80.0
subroutine 27 27 100.0
pod 4 4 100.0
total 251 299 83.9


line stmt bran cond sub pod time code
1             # Copyright 2011, 2012, 2013, 2014, 2015, 2016, 2017, 2018, 2019 Kevin Ryde
2              
3             # This file is part of Math-PlanePath.
4             #
5             # Math-PlanePath is free software; you can redistribute it and/or modify
6             # it under the terms of the GNU General Public License as published by the
7             # Free Software Foundation; either version 3, or (at your option) any later
8             # version.
9             #
10             # Math-PlanePath is distributed in the hope that it will be useful, but
11             # WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY
12             # or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License
13             # for more details.
14             #
15             # You should have received a copy of the GNU General Public License along
16             # with Math-PlanePath. If not, see .
17              
18              
19              
20             # math-image --path=GosperIslands --lines --scale=10
21             # math-image --path=GosperIslands --all --output=numbers_dash
22             #
23             # fractal dimension 2*log(3) / log(7) = 1.12915, A113211 decimal
24              
25             # Check:
26             # A017926 boundary length, unit equilateral triangles
27             # A229977 boundary length, initial hexagon=1
28              
29              
30             package Math::PlanePath::GosperIslands;
31 2     2   1420 use 5.004;
  2         8  
32 2     2   11 use strict;
  2         5  
  2         57  
33 2     2   471 use POSIX 'ceil';
  2         6334  
  2         11  
34 2     2   2301 use Math::Libm 'hypot';
  2         8350  
  2         163  
35             #use List::Util 'max';
36             *max = \&Math::PlanePath::_max;
37              
38 2     2   15 use vars '$VERSION', '@ISA';
  2         4  
  2         114  
39             $VERSION = 128;
40 2     2   1398 use Math::PlanePath;
  2         14  
  2         86  
41             @ISA = ('Math::PlanePath');
42              
43             use Math::PlanePath::Base::Generic
44 2         90 'is_infinite',
45 2     2   13 'round_nearest';
  2         4  
46             use Math::PlanePath::Base::Digits
47 2         124 'round_down_pow',
48 2     2   975 'digit_split_lowtohigh';
  2         6  
49              
50 2     2   1011 use Math::PlanePath::SacksSpiral;
  2         6  
  2         67  
51              
52             # uncomment this to run the ### lines
53             # use Smart::Comments;
54              
55              
56 2     2   14 use constant n_frac_discontinuity => 0;
  2         4  
  2         108  
57 2     2   12 use constant x_negative_at_n => 3;
  2         6  
  2         82  
58 2     2   11 use constant y_negative_at_n => 5;
  2         5  
  2         94  
59 2     2   11 use constant sumabsxy_minimum => 2; # minimum X=2,Y=0 or X=1,Y=1
  2         4  
  2         92  
60 2     2   12 use constant rsquared_minimum => 2; # minimum X=1,Y=1
  2         5  
  2         87  
61              
62             # jump across rings is upwards, so dY maximum unbounded but minimum=-1
63 2     2   12 use constant dy_minimum => -1;
  2         4  
  2         105  
64              
65             # dX and dY unbounded jumping between rings, with the jump position rotating
66             # around slowly with the twistiness of the ring.
67 2     2   15 use constant absdx_minimum => 1;
  2         4  
  2         107  
68              
69             # jump across rings is ENE, so dSum maximum unbounded but minimum=-2
70 2     2   12 use constant dsumxy_minimum => -2;
  2         3  
  2         113  
71              
72             # jump across rings is ENE, so dDiffXY minimum unbounded but maximum=+2
73 2     2   13 use constant ddiffxy_maximum => 2;
  2         3  
  2         120  
74              
75 2     2   14 use constant dir_maximum_dxdy => (1,-1); # South-East
  2         4  
  2         107  
76 2     2   12 use constant turn_any_straight => 0; # never straight
  2         5  
  2         3100  
77              
78              
79             #------------------------------------------------------------------------------
80             sub new {
81 5     5 1 1172 my $self = shift->SUPER::new (@_);
82 5   50     39 $self->{'sides'} ||= 6; # default
83 5         12 return $self;
84             }
85              
86             # innermost hexagon level 0
87             # level 0 len=6
88             # level 1 len=18
89             # each sidelen = 3^level
90             # each ringlen = 6*3^level
91             #
92             # Nstart(level) = 1 + ringlen(0) + ... + ringlen(level-1)
93             # = 1 + 6* [ 3^0 + ... + 3^(level-1) ]
94             # = 1 + 6* [ (3^level - 1) / 2 ]
95             # = 1 + 3*(3^level - 1)
96             # = 1 + 3*3^level - 3
97             # = 3^(level+1) - 2
98             #
99             # 3^(level+1) = N+2
100             # level+1 = log3(N+2)
101             # level = log3(N+2) - 1
102             #
103             # sides=3
104             # ringlen = 3*3^level
105             # Nstart(level) = 1 + 3* [ 3^0 + ... + 3^(level-1) ]
106             # = 1 + 3* [ (3^level - 1) / 2 ]
107             # = 1 + 3*(3^level - 1)/2
108             # = 1 + (3*3^level - 3)/2
109             # = (3*3^level - 3 + 2)/2
110             # = (3^(level+1) - 1)/2
111             # 3^(level+1) - 1 = 2*N
112             # 3^(level+1) = 2*N+1
113              
114             my @_xend = (2, 5);
115             my @_yend = (0, 1);
116             my @_hypotend = (4, 28);
117             sub _ends_for_level {
118 238610     238610   378141 my ($level) = @_;
119 238610 100       473261 if ($#_xend < $level) {
120 11         22 my $x = $_xend[-1];
121 11         18 my $y = $_yend[-1];
122 11         24 do {
123 28         66 ($x,$y) = ((5*$x - 3*$y)/2, # 2*$x + rotate +60
124             ($x + 5*$y)/2); # 2*$y + rotate +60
125             ### _ends_for_level() push: scalar(@_xend)." $x,$y"
126             # ### assert: "$x,$y" eq join(','__PACKAGE__->n_to_xy(scalar(@xend) ** 3))
127 28         62 push @_xend, $x;
128 28         49 push @_yend, $y;
129 28         90 push @_hypotend, $_hypotend[-1] * 7;
130             } while ($#_xend < $level);
131             }
132             }
133              
134             my @level_x = (0);
135             my @level_y = (0);
136              
137             sub n_to_xy {
138 517     517 1 105383 my ($self, $n) = @_;
139             ### GosperIslands n_to_xy(): $n
140 517 50       1531 if ($n < 1) {
141 0         0 return;
142             }
143 517 50       1494 if (is_infinite($n)) {
144 0         0 return ($n,$n);
145             }
146              
147 517         1177 my $sides = $self->{'sides'};
148 517 50       2082 my ($pow, $level) = round_down_pow (($sides == 6 ? $n+2 : 2*$n+1),
149             3);
150 517 50       1234 my $base = ($sides == 6 ? $pow-2 : ($pow-1)/2);
151             ### $level
152             ### $base
153              
154 517         788 $n -= $base; # remainder
155 517         819 my $sidelen = $pow / 3;
156 517         819 $level--;
157 517         964 my $side = int ($n / $sidelen);
158 517         1152 my ($x, $y) = _side_n_to_xy ($n - $side*$sidelen);
159              
160 517         1393 _ends_for_level($level);
161             ### raw xy: "$x,$y"
162             ### pos: "$_xend[$level],$_yend[$level]"
163              
164 517 50       1011 if ($sides == 6) {
165 517         1221 ($x,$y) = (($x+3*$y)/-2, # rotate +120
166             ($x-$y)/2);
167             ### rotate xy: "$x,$y"
168              
169 517         2573 $x += $_xend[$level];
170 517         920 $y += $_yend[$level];
171 517 100       1199 if ($side >= 3) {
172 150         257 $x = -$x; # rotate 180
173 150         218 $y = -$y;
174 150         220 $side -= 3;
175             }
176 517 100       1171 if ($side == 1) {
    100          
177 148         376 ($x,$y) = (($x-3*$y)/2, # rotate +60
178             ($x+$y)/2);
179             } elsif ($side == 2) {
180 138         356 ($x,$y) = (($x+3*$y)/-2, # rotate +120
181             ($x-$y)/2);
182             }
183             } else {
184 0         0 my $xend = $_xend[$level];
185 0         0 my $yend = $_yend[$level];
186 0         0 my $xp = 0;
187 0         0 my $yp = 0;
188 0         0 foreach (0 .. $level-1) {
189 0         0 $xp +=$_xend[$_];
190 0         0 $yp +=$_yend[$_];
191             }
192             ### ends: "$xend,$yend prev $xp,$yp"
193 0         0 ($xp,$yp) = (($xp-3*$yp)/-2, # rotate -120
194             ($xp+$yp)/-2);
195             # ($xp,$yp) = (($xp-3*$yp)/2, # rotate +60
196             # ($xp+$yp)/2);
197 0 0       0 if ($side == 0) {
    0          
    0          
198 0         0 $x += $xp;
199 0         0 $y += $yp;
200             } elsif ($side == 1) {
201 0         0 ($x,$y) = (($x+3*$y)/-2, # rotate +120
202             ($x-$y)/2);
203 0         0 $x += $xp;
204 0         0 $y += $yp;
205 0         0 $x += $xend;
206 0         0 $y += $yend;
207             } elsif ($side == 2) {
208 0         0 ($x,$y) = (($x-3*$y)/-2, # rotate +240==-120
209             ($x+$y)/-2);
210 0         0 $x += $xp;
211 0         0 $y += $yp;
212 0         0 $x += $xend;
213 0         0 $y += $yend;
214 0         0 ($xend,$yend) = (($xend+3*$yend)/-2, # rotate +120
215             ($xend-$yend)/2);
216 0         0 $x += $xend;
217 0         0 $y += $yend;
218             }
219             }
220 517         1580 return ($x,$y);
221             }
222              
223             # ENHANCE-ME: share with GosperSide ?
224             sub _side_n_to_xy {
225 517     517   916 my ($n) = @_;
226             ### _side_n_to_xy(): $n
227 517 50       1175 if ($n < 0) {
228 0         0 return;
229             }
230 517 50       1098 if (is_infinite($n)) {
231 0         0 return ($n,$n);
232             }
233              
234 517         937 my $x;
235 517         718 my $y = 0;
236             {
237 517         804 my $int = int($n);
  517         1098  
238 517         832 $x = 2*($n - $int);
239 517         904 $n = $int; # BigFloat int() gives BigInt, use that
240             }
241 517         728 my $xend = 2;
242 517         753 my $yend = 0;
243              
244 517         1097 foreach my $digit (digit_split_lowtohigh($n,3)) {
245 2713         4119 my $xend_offset = 3*($xend-$yend)/2; # end and end +60
246 2713         4057 my $yend_offset = ($xend+3*$yend)/2;
247              
248             ### at: "$x,$y"
249             ### $digit
250             ### $xend
251             ### $yend
252             ### $xend_offset
253             ### $yend_offset
254              
255 2713 100       5419 if ($digit == 1) {
    100          
256 924         2145 ($x,$y) = (($x-3*$y)/2 + $xend, # rotate +60
257             ($x+$y)/2 + $yend);
258             } elsif ($digit == 2) {
259 987         1520 $x += $xend_offset; # offset and offset +60
260 987         1457 $y += $yend_offset;
261             }
262 2713         3893 $xend += $xend_offset; # offset and offset +60
263 2713         4412 $yend += $yend_offset;
264             }
265              
266             ### final: "$x,$y"
267 517         1279 return ($x, $y);
268             }
269              
270              
271             #------------------------------------------------------------------------------
272             # xy_to_n()
273              
274             # innermost origin 0,0 N=0 level 0,
275             # first hexagon level 1
276             #
277             # Nstart(level) = 3^level - 2
278             # sidelength(level) = 3^(level-1)
279             # so Nstart(level) + side * sidelength(level)
280             # = 3^level - 2 + side * 3^(level-1)
281             # = (3 + side) * 3^(level-1) - 2
282             #
283             sub xy_to_n {
284 5618     5618 1 51184 my ($self, $x_full, $y_full) = @_;
285 5618         13045 $x_full = round_nearest($x_full);
286 5618         11260 $y_full = round_nearest($y_full);
287             ### GosperIslands xy_to_n(): "$x_full,$y_full"
288              
289 5618         12713 foreach my $overflow (3*$x_full+3*$y_full, 3*$x_full-3*$y_full) {
290 11236 50       23831 if (is_infinite($overflow)) { return $overflow; }
  0         0  
291             }
292 5618 50       12547 if (($x_full + $y_full) % 2) {
293             ### odd point, not reached ...
294 0         0 return undef;
295             }
296              
297 5618         16025 my $r = hypot($x_full,$y_full);
298 5618         19014 my $level_limit = ceil(log($r+1)/log(sqrt(7)));
299             ### $r
300             ### $level_limit
301 5618 50       11803 if (is_infinite($level_limit)) {
302 0         0 return $level_limit;
303             }
304              
305 5618         13941 for my $level (0 .. $level_limit + 1) {
306 34312         71191 _ends_for_level($level);
307 34312         54794 my $xend = $_xend[$level];
308 34312         49479 my $yend = $_yend[$level];
309              
310 34312         49996 my $x = $x_full - $xend;
311 34312         49124 my $y = $y_full - $yend;
312             ### level end: "$xend,$yend"
313             ### level subtract to: "$x,$y"
314 34312         63270 ($x,$y) = ((3*$y-$x)/2, # rotate -120;
315             ($x+$y)/-2);
316             ### level rotate to: "$x,$y"
317              
318 34312         58119 foreach my $side (0 .. 5) {
319             ### try: "level=$level side=$side $x,$y"
320 203271 100       330050 if (defined (my $n = _xy_to_n_in_level($x,$y,$level))) {
321 873         3087 return ($side+3)*3**$level + $n - 2;
322             }
323 202398         296070 $x -= $xend;
324 202398         269023 $y -= $yend;
325             ### subtract to: "$x,$y"
326 202398         426815 ($x,$y) = (($x+3*$y)/2, # rotate -60
327             ($y-$x)/2);
328              
329             }
330             }
331 4745         12256 return undef;
332             }
333              
334             sub _xy_to_n_in_level {
335 203781     203781   353502 my ($x, $y, $level) = @_;
336              
337 203781         404714 _ends_for_level($level);
338 203781         316340 my @pending_n = (0);
339 203781         306924 my @pending_x = ($x);
340 203781         293048 my @pending_y = ($y);
341 203781         286440 my @pending_level = ($level);
342              
343 203781         344097 while (@pending_n) {
344 403924         567313 my $n = pop @pending_n;
345 403924         557064 $x = pop @pending_x;
346 403924         554197 $y = pop @pending_y;
347 403924         554167 $level = pop @pending_level;
348             ### consider: "$x,$y n=$n level=$level"
349              
350 403924 100       691639 if ($level == 0) {
351 48046 100 100     94547 if ($x == 0 && $y == 0) {
352 1383         4586 return $n;
353             }
354             ### level=0 and not 0,0
355 46663         86930 next;
356             }
357 355878 100       728425 if (($x*$x+3*$y*$y) > $_hypotend[$level] * 1.1) {
358             ### radius out of range: ($x*$x+3*$y*$y)." cf end ".$_hypotend[$level]
359 286570         529075 next;
360             }
361              
362 69308         92176 $level--;
363 69308         94974 $n *= 3;
364 69308         97663 my $xend = $_xend[$level];
365 69308         93328 my $yend = $_yend[$level];
366             ### descend: "end=$xend,$yend on level=$level"
367              
368             # digit 0
369 69308         103839 push @pending_n, $n;
370 69308         95358 push @pending_x, $x;
371 69308         96671 push @pending_y, $y;
372 69308         93512 push @pending_level, $level;
373             ### push: "$x,$y digit=0"
374              
375             # digit 1
376 69308         98609 $x -= $xend;
377 69308         90132 $y -= $yend;
378 69308         141320 ($x,$y) = (($x+3*$y)/2, # rotate -60
379             ($y-$x)/2);
380 69308         105187 push @pending_n, $n + 1;
381 69308         103186 push @pending_x, $x;
382 69308         92509 push @pending_y, $y;
383 69308         94556 push @pending_level, $level;
384             ### push: "$x,$y digit=1"
385              
386             # digit 2
387 69308         96338 $x -= $xend;
388 69308         90241 $y -= $yend;
389 69308         131606 ($x,$y) = (($x-3*$y)/2, # rotate +60
390             ($x+$y)/2);
391 69308         103422 push @pending_n, $n + 2;
392 69308         103645 push @pending_x, $x;
393 69308         96844 push @pending_y, $y;
394 69308         130174 push @pending_level, $level;
395             ### push: "$x,$y digit=2"
396             }
397              
398 202398         419490 return undef;
399             }
400              
401             # Each
402             # *---
403             # /
404             # ---*
405             # is width=5 heightflat=1 is
406             # hypot^2 = 5*5 + 3 * 1*1
407             # = 25+3
408             # = 28
409             # hypot = 2*sqrt(7)
410             #
411             # comes in closer to
412             # level=1 n=9,x=2,y=2 is hypot=sqrt(2*2+3*2*2) = sqrt(16) = 4
413             # level=2 n=31,x=2,y=6 is hypot=sqrt(2*2+3*6*6) = sqrt(112) = sqrt(7)*4
414             # so
415             # radius = 4 * sqrt(7)^(level-1)
416             # radius/4 = sqrt(7)^(level-1)
417             # level-1 = log(radius/4) / log(sqrt(7))
418             # level = log(radius/4) / log(sqrt(7)) + 1
419             #
420             # Or
421             # level=1 n=9,x=2,y=2 is h=2*2+3*2*2 = 16
422             # level=2 n=31,x=2,y=6 is h=2*2+3*6*6 = 112 = 7*16
423             # h = 16 * 7^(level-1)
424             # h/16 = 7^(level-1)
425             # level-1 = log(h/16) / log(7)
426             # level = log(h/16) / log(7) + 1
427             #
428             # is the next outer level, so high covering is the end of the previous
429             # level,
430             #
431             # Nstart(level) - 1 = 3^(level+2) - 2 - 1
432             # = 3^(level+2) - 3
433             #
434             # not exact
435             sub rect_to_n_range {
436 18     18 1 2055 my ($self, $x1,$y1, $x2,$y2) = @_;
437 18         37 $y1 *= sqrt(3);
438 18         30 $y2 *= sqrt(3);
439 18         59 my ($r_lo, $r_hi) = Math::PlanePath::SacksSpiral::_rect_to_radius_range
440             ($x1,$y1, $x2,$y2);
441 18         34 $r_hi *= 2;
442 18         43 my $level_plus_1 = ceil( log(max(1,$r_hi/4)) / log(sqrt(7)) ) + 2;
443 18         54 return (1, 3**$level_plus_1 - 3);
444             }
445              
446             1;
447             __END__