File Coverage

blib/lib/Math/PlanePath/QuintetCentres.pm
Criterion Covered Total %
statement 223 247 90.2
branch 63 74 85.1
condition 15 20 75.0
subroutine 25 31 80.6
pod 9 9 100.0
total 335 381 87.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             # Boundary of unit squares:
20             # 4*3^n
21             # QuintetCentres unit squares boundary a(n) = 4*3^(n-3)
22             # 12,36,108,324,972
23             # match 12,36,108,324,972
24             # A003946 G.f.: (1+x)/(1-3*x).
25             # A025579 a(1)=1, a(2)=2, a(n) = 4*3^(n-3) for n >= 3.
26             # A027327 a(n) = Sum{(k+1)*T(n,m-k)}, 0<=k<=m, where m=0 for n=0,1; m=n for n >= 2; T given by A026120.
27              
28              
29             package Math::PlanePath::QuintetCentres;
30 2     2   9058 use 5.004;
  2         14  
31 2     2   9 use strict;
  2         5  
  2         43  
32 2     2   1129 use POSIX 'ceil';
  2         17405  
  2         10  
33             #use List::Util 'min','max';
34             *min = \&Math::PlanePath::_min;
35             *max = \&Math::PlanePath::_max;
36              
37 2     2   4018 use vars '$VERSION', '@ISA';
  2         4  
  2         118  
38             $VERSION = 128;
39 2     2   1318 use Math::PlanePath;
  2         5  
  2         98  
40             @ISA = ('Math::PlanePath');
41             *_divrem_mutate = \&Math::PlanePath::_divrem_mutate;
42              
43 2     2   869 use Math::PlanePath::SacksSpiral;
  2         5  
  2         258  
44             *_rect_to_radius_range = \&Math::PlanePath::SacksSpiral::_rect_to_radius_range;
45              
46             use Math::PlanePath::Base::Generic
47 2         142 'is_infinite',
48 2     2   17 'round_nearest';
  2         4  
49             use Math::PlanePath::Base::Digits
50 2         123 'digit_split_lowtohigh',
51 2     2   1026 'round_up_pow';
  2         5  
52              
53             # uncomment this to run the ### lines
54             # use Smart::Comments;
55              
56              
57 2     2   14 use constant n_start => 0;
  2         4  
  2         133  
58 2         454 use constant parameter_info_array => [ { name => 'arms',
59             share_key => 'arms_4',
60             display => 'Arms',
61             type => 'integer',
62             minimum => 1,
63             maximum => 4,
64             default => 1,
65             width => 1,
66             description => 'Arms',
67 2     2   11 } ];
  2         4  
68              
69             {
70             my @x_negative_at_n = (undef, 112, 9, 2, 2);
71             sub x_negative_at_n {
72 0     0 1 0 my ($self) = @_;
73 0         0 return $x_negative_at_n[$self->{'arms'}];
74             }
75             }
76             {
77             my @y_negative_at_n = (undef, 2, 4, 6, 7);
78             sub y_negative_at_n {
79 0     0 1 0 my ($self) = @_;
80 0         0 return $y_negative_at_n[$self->{'arms'}];
81             }
82             }
83              
84 2     2   15 use constant dx_minimum => -1;
  2         4  
  2         95  
85 2     2   11 use constant dx_maximum => 1;
  2         3  
  2         129  
86 2     2   14 use constant dy_minimum => -1;
  2         3  
  2         161  
87 2     2   26 use constant dy_maximum => 1;
  2         5  
  2         250  
88              
89             *_UNDOCUMENTED__dxdy_list = \&Math::PlanePath::_UNDOCUMENTED__dxdy_list_eight;
90             {
91             my @_UNDOCUMENTED__dxdy_list_at_n = (undef, 18, 14, 11, 11);
92             sub _UNDOCUMENTED__dxdy_list_at_n {
93 0     0   0 my ($self) = @_;
94 0         0 return $_UNDOCUMENTED__dxdy_list_at_n[$self->{'arms'}];
95             }
96             }
97              
98 2     2   14 use constant dsumxy_minimum => -2; # diagonals
  2         11  
  2         261  
99 2     2   14 use constant dsumxy_maximum => 2;
  2         4  
  2         118  
100 2     2   13 use constant ddiffxy_minimum => -2;
  2         4  
  2         106  
101 2     2   12 use constant ddiffxy_maximum => 2;
  2         3  
  2         143  
102 2     2   14 use constant dir_maximum_dxdy => (1,-1); # South-East
  2         5  
  2         3768  
103              
104             # N=9 first straight, then for other arms 18,27,36
105             sub _UNDOCUMENTED__turn_any_straight_at_n {
106 0     0   0 my ($self) = @_;
107 0         0 return 9*$self->arms_count;
108             }
109              
110              
111             #------------------------------------------------------------------------------
112              
113             sub new {
114 45     45 1 13844 my $self = shift->SUPER::new(@_);
115 45   100     252 $self->{'arms'} = max(1, min(4, $self->{'arms'} || 1));
116 45         108 return $self;
117             }
118              
119             my @rot_to_x = (0,0,-1,-1);
120             my @rot_to_y = (0,1,1,0);
121             my @dir4_to_dx = (1,0,-1,0);
122             my @dir4_to_dy = (0,1,0,-1);
123             my @digit_reverse = (0,1,0,0,1);
124              
125             sub n_to_xy {
126 25051     25051 1 132198 my ($self, $n) = @_;
127             ### QuintetCentres n_to_xy(): "arms=$self->{'arms'} $n"
128              
129 25051 50       46069 if ($n < 0) {
130 0         0 return;
131             }
132 25051 50       49042 if (is_infinite($n)) {
133 0         0 return ($n,$n);
134             }
135              
136             {
137 25051         42885 my $int = int($n);
  25051         34813  
138 25051 100       44433 if ($n != $int) {
139 63         151 my ($x1,$y1) = $self->n_to_xy($int);
140 63         147 my ($x2,$y2) = $self->n_to_xy($int+$self->{'arms'});
141 63         108 my $frac = $n - $int; # inherit possible BigFloat
142 63         88 my $dx = $x2-$x1;
143 63         98 my $dy = $y2-$y1;
144 63         236 return ($frac*$dx + $x1, $frac*$dy + $y1);
145             }
146 24988         36282 $n = $int; # BigFloat int() gives BigInt, use that
147             }
148 24988         35732 my $zero = ($n * 0); # inherit BigInt 0
149              
150             # arm as initial rotation
151 24988         53572 my $rot = _divrem_mutate ($n, $self->{'arms'});
152              
153 24988         53271 my @digits = digit_split_lowtohigh($n,5);
154 24988         39232 my @sx;
155             my @sy;
156             {
157 24988         34078 my $sx = $zero + $dir4_to_dx[$rot];
  24988         37782  
158 24988         34774 my $sy = $zero + $dir4_to_dy[$rot];
159 24988         41775 foreach (@digits) {
160 117049         157324 push @sx, $sx;
161 117049         157545 push @sy, $sy;
162              
163             # 2*(sx,sy) + rot+90(sx,sy)
164 117049         190196 ($sx,$sy) = (2*$sx - $sy,
165             2*$sy + $sx);
166             }
167             ### @digits
168 24988         33440 my $rev = 0;
169 24988         52172 for (my $i = $#digits; $i >= 0; $i--) { # high to low
170             ### digit: $digits[$i]
171 117049 100       192253 if ($rev) {
172             ### reverse: "$digits[$i] to ".(5 - $digits[$i])
173 47858         65819 $digits[$i] = 4 - $digits[$i];
174             }
175 117049         209679 $rev ^= $digit_reverse[$digits[$i]];
176             ### now rev: $rev
177             }
178             }
179             ### reversed n: @digits
180              
181              
182 24988         36613 my $x =
183             my $y =
184             my $ox =
185             my $oy = $zero;
186              
187 24988         47303 while (defined (my $digit = shift @digits)) { # low to high
188 117049         154289 my $sx = shift @sx;
189 117049         156270 my $sy = shift @sy;
190             ### at: "$x,$y digit $digit side $sx,$sy"
191              
192             # if ($rot & 2) {
193             # ($sx,$sy) = (-$sx,-$sy);
194             # }
195             # if ($rot & 1) {
196             # ($sx,$sy) = (-$sy,$sx);
197             # }
198              
199 117049 100       229543 if ($digit == 0) {
    100          
    100          
    100          
200 33099         42606 $x -= $sx; # left at 180
201 33099         42096 $y -= $sy;
202              
203             } elsif ($digit == 1) {
204             # centre
205 24294         38933 ($x,$y) = (-$y,$x); # rotate -90
206             ### rotate to: "$x,$y"
207             # $rot--;
208              
209             } elsif ($digit == 2) {
210 19343         24911 $x += $sy; # down at -90
211 19343         25258 $y -= $sx;
212             ### offset to: "$x,$y"
213              
214             } elsif ($digit == 3) {
215 17372         28951 ($x,$y) = (-$y,$x); # rotate -90
216 17372         24448 $x += $sx; # right at 0
217 17372         22768 $y += $sy;
218             # $rot++;
219              
220             } else { # $digit == 4
221 22941         36093 ($x,$y) = ($y,-$x); # rotate +90
222 22941         31670 $x -= $sy; # up at +90
223 22941         30120 $y += $sx;
224             # $rot++;
225             }
226              
227 117049         149770 $ox += $sx;
228 117049         214929 $oy += $sy;
229             }
230              
231             ### final: "$x,$y origin $ox,$oy"
232 24988         61777 return ($x + $ox + $rot_to_x[$rot],
233             $y + $oy + $rot_to_y[$rot]);
234             }
235              
236              
237             # modulus 2*X+Y
238             # 3
239             # 0 2 4
240             # 1
241             #
242             # 0 is X=0,Y=0
243             #
244             my @modulus_to_x = (0,1,1,1,2);
245             my @modulus_to_y = (0,-1,0,1,0);
246              
247             my @modulus_to_digit
248             = (0,2,1,4,3, 0,0,10,30,20, # 0 base
249             0,4,3,1,2, 0,10,50,40,10, # 10
250             4,0,1,3,2, 60,20,40,50,20, # 20 rotated +90
251             2,1,3,4,0, 30,60,0,30,50, # 30
252             1,0,3,2,4, 30,20,70,40,40, # 40
253             3,4,1,2,0, 70,10,30,50,50, # 50 rotated +180
254             4,2,3,0,1, 60,60,20,70,10, # 60
255             2,3,1,0,4, 70,0,60,70,40, # 70 rotated +270
256             );
257             sub xy_to_n {
258 18655     18655 1 53497 my ($self, $x, $y) = @_;
259             ### QuintetCentres xy_to_n(): "$x, $y"
260              
261 18655         36535 $x = round_nearest($x);
262 18655         35713 $y = round_nearest($y);
263              
264 18655         38605 foreach my $overflow (2*$x + 2*$y, 2*$x - 2*$y) {
265 37310 50       68248 if (is_infinite($overflow)) { return $overflow; }
  0         0  
266             }
267              
268             # my $level_limit = log($x*$x + $y*$y + 1) * 1 * 2;
269             # if (is_infinite($level_limit)) { return $level_limit; }
270              
271 18655         41115 my @digits;
272             my $arm;
273 18655         0 my $state;
274 18655         24341 for (;;) {
275             # if ($level_limit-- < 0) {
276             # ### oops, level limit ...
277             # return undef;
278             # }
279 94492 100       186255 if ($x == 0) {
    100          
280 19762 100       34356 if ($y == 0) {
281             ### found first arm 0,0 ...
282 9639         13677 $arm = 0;
283 9639         12764 $state = 0;
284 9639         14320 last;
285             }
286 10123 100       18275 if ($y == 1) {
287             ### found second arm 0,1 ...
288 4935         6724 $arm = 1;
289 4935         6301 $state = 20;
290 4935         6973 last;
291             }
292             } elsif ($x == -1) {
293 7986 100       14683 if ($y == 1) {
294             ### found third arm -1,1 ...
295 2768         4038 $arm = 2;
296 2768         3594 $state = 50;
297 2768         3909 last;
298             }
299 5218 100       9331 if ($y == 0) {
300             ### found fourth arm -1,0 ...
301 1313         1908 $arm = 3;
302 1313         1751 $state = 70;
303 1313         1884 last;
304             }
305             }
306 75837         113306 my $m = (2*$x + $y) % 5;
307             ### at: "$x,$y digits=".join(',',@digits)
308             ### mod remainder: $m
309              
310 75837         101291 $x -= $modulus_to_x[$m];
311 75837         97675 $y -= $modulus_to_y[$m];
312 75837         102252 push @digits, $m;
313              
314             ### digit: "$m to $x,$y"
315             ### shrink to: ((2*$x + $y) / 5).','.((2*$y - $x) / 5)
316             ### assert: (2*$x + $y) % 5 == 0
317             ### assert: (2*$y - $x) % 5 == 0
318              
319             # shrink
320             # (2 -1) inverse (2 1)
321             # (1 2) (-1 2)
322             #
323 75837         144103 ($x,$y) = ((2*$x + $y) / 5,
324             (2*$y - $x) / 5);
325             }
326              
327             ### @digits
328 18655         29466 my $arms = $self->{'arms'};
329 18655 100       32848 if ($arm >= $arms) {
330 138         470 return undef;
331             }
332              
333 18517         24770 my $n = 0;
334 18517         29589 foreach my $m (reverse @digits) { # high to low
335             ### $m
336             ### digit: $modulus_to_digit[$state + $m]
337             ### state: $state
338             ### next state: $modulus_to_digit[$state+5 + $m]
339              
340 75314         108455 $n = 5*$n + $modulus_to_digit[$state + $m];
341 75314         117387 $state = $modulus_to_digit[$state+5 + $m];
342             }
343             ### final n along arm: $n
344              
345 18517         49187 return $n*$arms + $arm;
346             }
347              
348             #------------------------------------------------------------------------------
349              
350             # whole plane covered when arms==4
351             sub xy_is_visited {
352 0     0 1 0 my ($self, $x, $y) = @_;
353 0   0     0 return ($self->{'arms'} == 4
354             || defined($self->xy_to_n($x,$y)));
355             }
356              
357             #------------------------------------------------------------------------------
358              
359             # exact
360             sub rect_to_n_range {
361 134     134 1 21339 my ($self, $x1,$y1, $x2,$y2) = @_;
362             ### QuintetCurve rect_to_n_range(): "$x1,$y1 $x2,$y2"
363              
364 134         651 my ($r_lo, $r_hi) = _rect_to_radius_range($x1,$y1, $x2,$y2);
365 134         330 $r_hi *= 2;
366 134         452 my $level_plus_1 = ceil( log(max(1,$r_hi/4)) / log(sqrt(5)) ) + 2;
367              
368             # Simple over-estimate would be: return (0, 5**$level_plus_1);
369              
370 134         276 my $level_limit = $level_plus_1;
371             ### $level_limit
372 134 50       391 if (is_infinite($level_limit)) { return ($level_limit,$level_limit); }
  0         0  
373              
374 134         393 $x1 = round_nearest ($x1);
375 134         347 $y1 = round_nearest ($y1);
376 134         344 $x2 = round_nearest ($x2);
377 134         294 $y2 = round_nearest ($y2);
378 134 50       388 ($x1,$x2) = ($x2,$x1) if $x1 > $x2;
379 134 100       349 ($y1,$y2) = ($y2,$y1) if $y1 > $y2;
380             ### sorted range: "$x1,$y1 $x2,$y2"
381              
382             my $rect_dist = sub {
383 25843     25843   41246 my ($x,$y) = @_;
384 25843 100       50038 my $xd = ($x < $x1 ? $x1 - $x
    100          
385             : $x > $x2 ? $x - $x2
386             : 0);
387 25843 100       46948 my $yd = ($y < $y1 ? $y1 - $y
    100          
388             : $y > $y2 ? $y - $y2
389             : 0);
390 25843         43665 return ($xd*$xd + $yd*$yd);
391 134         925 };
392              
393 134         343 my $arms = $self->{'arms'};
394             ### $arms
395 134         256 my $n_lo;
396             {
397 134         254 my @hypot = (4);
  134         311  
398 134         249 my $top = 0;
399 134         246 for (;;) {
400 461         956 ARM_LO: foreach my $arm (0 .. $arms-1) {
401 461         702 my $i = 0;
402 461         664 my @digits;
403 461 100       925 if ($top > 0) {
404 327         751 @digits = ((0)x($top-1), 1);
405             } else {
406 134         264 @digits = (0);
407             }
408              
409 461         647 for (;;) {
410 12695         17476 my $n = 0;
411 12695         20734 foreach my $digit (reverse @digits) { # high to low
412 62107         87538 $n = 5*$n + $digit;
413             }
414 12695         17352 $n = $n*$arms + $arm;
415             ### lo consider: "i=$i digits=".join(',',reverse @digits)." is n=$n"
416              
417 12695         25934 my ($nx,$ny) = $self->n_to_xy($n);
418 12695         24226 my $nh = &$rect_dist ($nx,$ny);
419 12695 100 100     30874 if ($i == 0 && $nh == 0) {
420             ### lo found inside: $n
421 134 50 33     369 if (! defined $n_lo || $n < $n_lo) {
422 134         199 $n_lo = $n;
423             }
424 134         464 next ARM_LO;
425             }
426              
427 12561 100 100     32371 if ($i == 0 || $nh > $hypot[$i]) {
428             ### too far away: "nxy=$nx,$ny nh=$nh vs ".$hypot[$i]
429              
430 10953         22722 while (++$digits[$i] > 4) {
431 2535         3630 $digits[$i] = 0;
432 2535 100       6326 if (++$i <= $top) {
433             ### backtrack up ...
434             } else {
435             ### not found within this top and arm, next arm ...
436 327         898 next ARM_LO;
437             }
438             }
439             } else {
440             ### lo descend ...
441             ### assert: $i > 0
442 1608         2240 $i--;
443 1608         2476 $digits[$i] = 0;
444             }
445             }
446             }
447              
448             # if an $n_lo was found on any arm within this $top then done
449 461 100       940 if (defined $n_lo) {
450 134         275 last;
451             }
452              
453             ### lo extend top ...
454 327 50       692 if (++$top > $level_limit) {
455             ### nothing below level limit ...
456 0         0 return (1,0);
457             }
458 327         624 $hypot[$top] = 5 * $hypot[$top-1];
459             }
460             }
461              
462 134         240 my $n_hi = 0;
463 134         392 ARM_HI: foreach my $arm (reverse 0 .. $arms-1) {
464 134         366 my @digits = ((4) x $level_limit);
465 134         261 my $i = $#digits;
466 134         198 for (;;) {
467 13148         17507 my $n = 0;
468 13148         21496 foreach my $digit (reverse @digits) { # high to low
469 79965         112318 $n = 5*$n + $digit;
470             }
471 13148         17850 $n = $n*$arms + $arm;
472             ### hi consider: "arm=$arm i=$i digits=".join(',',reverse @digits)." is n=$n"
473              
474 13148         26341 my ($nx,$ny) = $self->n_to_xy($n);
475 13148         24777 my $nh = &$rect_dist ($nx,$ny);
476 13148 100 100     30421 if ($i == 0 && $nh == 0) {
477             ### hi found inside: $n
478 134 100       306 if ($n > $n_hi) {
479 128         210 $n_hi = $n;
480 128         491 next ARM_HI;
481             }
482             }
483              
484 13020 100 100     35363 if ($i == 0 || $nh > (4 * 5**$i)) {
485             ### too far away: "$nx,$ny nh=$nh vs ".(4 * 5**$i)
486              
487 10353         21890 while (--$digits[$i] < 0) {
488 2187         3124 $digits[$i] = 4;
489 2187 100       5278 if (++$i < $level_limit) {
490             ### hi backtrack up ...
491             } else {
492             ### hi nothing within level limit for this arm ...
493 6         20 next ARM_HI;
494             }
495             }
496              
497             } else {
498             ### hi descend
499             ### assert: $i > 0
500 2667         3637 $i--;
501 2667         4183 $digits[$i] = 4;
502             }
503             }
504             }
505              
506 134 100       357 if ($n_hi == 0) {
507             ### oops, lo found but hi not found
508 6         9 $n_hi = $n_lo;
509             }
510              
511 134         1312 return ($n_lo, $n_hi);
512             }
513              
514             #------------------------------------------------------------------------------
515             # levels
516              
517             # level=0
518             # level=1 0 to 4
519             # level=2 0 to 24 is 5^level-1
520             #
521             # multiple arms the same full points of arms=1
522             # so arms*5^level points numbered starting 0
523             # = 5^level*arms - 1
524             sub level_to_n_range {
525 6     6 1 1521 my ($self, $level) = @_;
526 6         22 return (0, 5**$level * $self->{'arms'} - 1);
527             }
528             sub n_to_level {
529 0     0 1   my ($self, $n) = @_;
530 0 0         if ($n < 0) { return undef; }
  0            
531 0 0         if (is_infinite($n)) { return $n; }
  0            
532 0           $n = round_nearest($n);
533 0           _divrem_mutate ($n, $self->{'arms'});
534 0           my ($pow, $exp) = round_up_pow ($n+1, 5);
535 0           return $exp;
536             }
537              
538             #------------------------------------------------------------------------------
539             1;
540             __END__