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, 2020 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   1438 use 5.004;
  2         8  
32 2     2   11 use strict;
  2         4  
  2         60  
33 2     2   518 use POSIX 'ceil';
  2         6337  
  2         9  
34 2     2   2562 use Math::Libm 'hypot';
  2         8822  
  2         164  
35             #use List::Util 'max';
36             *max = \&Math::PlanePath::_max;
37              
38 2     2   14 use vars '$VERSION', '@ISA';
  2         5  
  2         115  
39             $VERSION = 129;
40 2     2   1555 use Math::PlanePath;
  2         13  
  2         88  
41             @ISA = ('Math::PlanePath');
42              
43             use Math::PlanePath::Base::Generic
44 2         93 'is_infinite',
45 2     2   13 'round_nearest';
  2         4  
46             use Math::PlanePath::Base::Digits
47 2         120 'round_down_pow',
48 2     2   1066 'digit_split_lowtohigh';
  2         6  
49              
50 2     2   1123 use Math::PlanePath::SacksSpiral;
  2         6  
  2         71  
51              
52             # uncomment this to run the ### lines
53             # use Smart::Comments;
54              
55              
56 2     2   15 use constant n_frac_discontinuity => 0;
  2         4  
  2         124  
57 2     2   13 use constant x_negative_at_n => 3;
  2         4  
  2         87  
58 2     2   11 use constant y_negative_at_n => 5;
  2         4  
  2         93  
59 2     2   11 use constant sumabsxy_minimum => 2; # minimum X=2,Y=0 or X=1,Y=1
  2         5  
  2         93  
60 2     2   14 use constant rsquared_minimum => 2; # minimum X=1,Y=1
  2         4  
  2         91  
61              
62             # jump across rings is upwards, so dY maximum unbounded but minimum=-1
63 2     2   11 use constant dy_minimum => -1;
  2         4  
  2         112  
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         3  
  2         106  
68              
69             # jump across rings is ENE, so dSum maximum unbounded but minimum=-2
70 2     2   13 use constant dsumxy_minimum => -2;
  2         4  
  2         111  
71              
72             # jump across rings is ENE, so dDiffXY minimum unbounded but maximum=+2
73 2     2   14 use constant ddiffxy_maximum => 2;
  2         4  
  2         125  
74              
75 2     2   14 use constant dir_maximum_dxdy => (1,-1); # South-East
  2         5  
  2         109  
76 2     2   13 use constant turn_any_straight => 0; # never straight
  2         12  
  2         3241  
77              
78              
79             #------------------------------------------------------------------------------
80             sub new {
81 5     5 1 847 my $self = shift->SUPER::new (@_);
82 5   50     37 $self->{'sides'} ||= 6; # default
83 5         11 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 237776     237776   337868 my ($level) = @_;
119 237776 100       443936 if ($#_xend < $level) {
120 13         23 my $x = $_xend[-1];
121 13         25 my $y = $_yend[-1];
122 13         19 do {
123 28         65 ($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         54 push @_xend, $x;
128 28         45 push @_yend, $y;
129 28         85 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 49175 my ($self, $n) = @_;
139             ### GosperIslands n_to_xy(): $n
140 517 50       1418 if ($n < 1) {
141 0         0 return;
142             }
143 517 50       1377 if (is_infinite($n)) {
144 0         0 return ($n,$n);
145             }
146              
147 517         1124 my $sides = $self->{'sides'};
148 517 50       1727 my ($pow, $level) = round_down_pow (($sides == 6 ? $n+2 : 2*$n+1),
149             3);
150 517 50       1082 my $base = ($sides == 6 ? $pow-2 : ($pow-1)/2);
151             ### $level
152             ### $base
153              
154 517         777 $n -= $base; # remainder
155 517         775 my $sidelen = $pow / 3;
156 517         660 $level--;
157 517         839 my $side = int ($n / $sidelen);
158 517         1072 my ($x, $y) = _side_n_to_xy ($n - $side*$sidelen);
159              
160 517         1379 _ends_for_level($level);
161             ### raw xy: "$x,$y"
162             ### pos: "$_xend[$level],$_yend[$level]"
163              
164 517 50       974 if ($sides == 6) {
165 517         1099 ($x,$y) = (($x+3*$y)/-2, # rotate +120
166             ($x-$y)/2);
167             ### rotate xy: "$x,$y"
168              
169 517         922 $x += $_xend[$level];
170 517         730 $y += $_yend[$level];
171 517 100       943 if ($side >= 3) {
172 179         338 $x = -$x; # rotate 180
173 179         258 $y = -$y;
174 179         245 $side -= 3;
175             }
176 517 100       1318 if ($side == 1) {
    100          
177 161         321 ($x,$y) = (($x-3*$y)/2, # rotate +60
178             ($x+$y)/2);
179             } elsif ($side == 2) {
180 126         267 ($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         1354 return ($x,$y);
221             }
222              
223             # ENHANCE-ME: share with GosperSide ?
224             sub _side_n_to_xy {
225 517     517   892 my ($n) = @_;
226             ### _side_n_to_xy(): $n
227 517 50       1034 if ($n < 0) {
228 0         0 return;
229             }
230 517 50       989 if (is_infinite($n)) {
231 0         0 return ($n,$n);
232             }
233              
234 517         938 my $x;
235 517         725 my $y = 0;
236             {
237 517         862 my $int = int($n);
  517         714  
238 517         744 $x = 2*($n - $int);
239 517         785 $n = $int; # BigFloat int() gives BigInt, use that
240             }
241 517         768 my $xend = 2;
242 517         689 my $yend = 0;
243              
244 517         1157 foreach my $digit (digit_split_lowtohigh($n,3)) {
245 2597         3733 my $xend_offset = 3*($xend-$yend)/2; # end and end +60
246 2597         3804 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 2597 100       4774 if ($digit == 1) {
    100          
256 933         1972 ($x,$y) = (($x-3*$y)/2 + $xend, # rotate +60
257             ($x+$y)/2 + $yend);
258             } elsif ($digit == 2) {
259 943         1263 $x += $xend_offset; # offset and offset +60
260 943         1268 $y += $yend_offset;
261             }
262 2597         3493 $xend += $xend_offset; # offset and offset +60
263 2597         3934 $yend += $yend_offset;
264             }
265              
266             ### final: "$x,$y"
267 517         1078 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 50985 my ($self, $x_full, $y_full) = @_;
285 5618         12735 $x_full = round_nearest($x_full);
286 5618         11304 $y_full = round_nearest($y_full);
287             ### GosperIslands xy_to_n(): "$x_full,$y_full"
288              
289 5618         12613 foreach my $overflow (3*$x_full+3*$y_full, 3*$x_full-3*$y_full) {
290 11236 50       22557 if (is_infinite($overflow)) { return $overflow; }
  0         0  
291             }
292 5618 50       13190 if (($x_full + $y_full) % 2) {
293             ### odd point, not reached ...
294 0         0 return undef;
295             }
296              
297 5618         15585 my $r = hypot($x_full,$y_full);
298 5618         18051 my $level_limit = ceil(log($r+1)/log(sqrt(7)));
299             ### $r
300             ### $level_limit
301 5618 50       11992 if (is_infinite($level_limit)) {
302 0         0 return $level_limit;
303             }
304              
305 5618         12734 for my $level (0 .. $level_limit + 1) {
306 34182         70764 _ends_for_level($level);
307 34182         52173 my $xend = $_xend[$level];
308 34182         47651 my $yend = $_yend[$level];
309              
310 34182         47922 my $x = $x_full - $xend;
311 34182         46472 my $y = $y_full - $yend;
312             ### level end: "$xend,$yend"
313             ### level subtract to: "$x,$y"
314 34182         63610 ($x,$y) = ((3*$y-$x)/2, # rotate -120;
315             ($x+$y)/-2);
316             ### level rotate to: "$x,$y"
317              
318 34182         54549 foreach my $side (0 .. 5) {
319             ### try: "level=$level side=$side $x,$y"
320 202567 100       322447 if (defined (my $n = _xy_to_n_in_level($x,$y,$level))) {
321 873         2822 return ($side+3)*3**$level + $n - 2;
322             }
323 201694         286918 $x -= $xend;
324 201694         258654 $y -= $yend;
325             ### subtract to: "$x,$y"
326 201694         413704 ($x,$y) = (($x+3*$y)/2, # rotate -60
327             ($y-$x)/2);
328              
329             }
330             }
331 4745         12465 return undef;
332             }
333              
334             sub _xy_to_n_in_level {
335 203077     203077   315504 my ($x, $y, $level) = @_;
336              
337 203077         392430 _ends_for_level($level);
338 203077         297694 my @pending_n = (0);
339 203077         296638 my @pending_x = ($x);
340 203077         276927 my @pending_y = ($y);
341 203077         263172 my @pending_level = ($level);
342              
343 203077         350730 while (@pending_n) {
344 401972         553912 my $n = pop @pending_n;
345 401972         530493 $x = pop @pending_x;
346 401972         541382 $y = pop @pending_y;
347 401972         528659 $level = pop @pending_level;
348             ### consider: "$x,$y n=$n level=$level"
349              
350 401972 100       684406 if ($level == 0) {
351 48036 100 100     97407 if ($x == 0 && $y == 0) {
352 1383         4584 return $n;
353             }
354             ### level=0 and not 0,0
355 46653         88759 next;
356             }
357 353936 100       718939 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 285107         510535 next;
360             }
361              
362 68829         86581 $level--;
363 68829         86876 $n *= 3;
364 68829         95218 my $xend = $_xend[$level];
365 68829         89295 my $yend = $_yend[$level];
366             ### descend: "end=$xend,$yend on level=$level"
367              
368             # digit 0
369 68829         96538 push @pending_n, $n;
370 68829         92573 push @pending_x, $x;
371 68829         90759 push @pending_y, $y;
372 68829         91125 push @pending_level, $level;
373             ### push: "$x,$y digit=0"
374              
375             # digit 1
376 68829         95013 $x -= $xend;
377 68829         87174 $y -= $yend;
378 68829         132706 ($x,$y) = (($x+3*$y)/2, # rotate -60
379             ($y-$x)/2);
380 68829         103565 push @pending_n, $n + 1;
381 68829         98480 push @pending_x, $x;
382 68829         90046 push @pending_y, $y;
383 68829         90505 push @pending_level, $level;
384             ### push: "$x,$y digit=1"
385              
386             # digit 2
387 68829         89216 $x -= $xend;
388 68829         87394 $y -= $yend;
389 68829         124673 ($x,$y) = (($x-3*$y)/2, # rotate +60
390             ($x+$y)/2);
391 68829         97844 push @pending_n, $n + 2;
392 68829         96973 push @pending_x, $x;
393 68829         92725 push @pending_y, $y;
394 68829         128238 push @pending_level, $level;
395             ### push: "$x,$y digit=2"
396             }
397              
398 201694         440968 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 1468 my ($self, $x1,$y1, $x2,$y2) = @_;
437 18         36 $y1 *= sqrt(3);
438 18         25 $y2 *= sqrt(3);
439 18         56 my ($r_lo, $r_hi) = Math::PlanePath::SacksSpiral::_rect_to_radius_range
440             ($x1,$y1, $x2,$y2);
441 18         36 $r_hi *= 2;
442 18         41 my $level_plus_1 = ceil( log(max(1,$r_hi/4)) / log(sqrt(7)) ) + 2;
443 18         55 return (1, 3**$level_plus_1 - 3);
444             }
445              
446             1;
447             __END__