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   9358 use 5.004;
  2         12  
31 2     2   10 use strict;
  2         4  
  2         43  
32 2     2   1024 use POSIX 'ceil';
  2         16577  
  2         10  
33             #use List::Util 'min','max';
34             *min = \&Math::PlanePath::_min;
35             *max = \&Math::PlanePath::_max;
36              
37 2     2   2943 use vars '$VERSION', '@ISA';
  2         5  
  2         111  
38             $VERSION = 129;
39 2     2   1417 use Math::PlanePath;
  2         4  
  2         97  
40             @ISA = ('Math::PlanePath');
41             *_divrem_mutate = \&Math::PlanePath::_divrem_mutate;
42              
43 2     2   924 use Math::PlanePath::SacksSpiral;
  2         6  
  2         96  
44             *_rect_to_radius_range = \&Math::PlanePath::SacksSpiral::_rect_to_radius_range;
45              
46             use Math::PlanePath::Base::Generic
47 2         112 'is_infinite',
48 2     2   15 'round_nearest';
  2         3  
49             use Math::PlanePath::Base::Digits
50 2         127 'digit_split_lowtohigh',
51 2     2   978 '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         3  
  2         134  
58 2         301 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         5  
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         5  
  2         95  
85 2     2   12 use constant dx_maximum => 1;
  2         3  
  2         117  
86 2     2   22 use constant dy_minimum => -1;
  2         4  
  2         141  
87 2     2   14 use constant dy_maximum => 1;
  2         4  
  2         248  
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         4  
  2         124  
99 2     2   13 use constant dsumxy_maximum => 2;
  2         4  
  2         111  
100 2     2   12 use constant ddiffxy_minimum => -2;
  2         4  
  2         107  
101 2     2   11 use constant ddiffxy_maximum => 2;
  2         12  
  2         117  
102 2     2   11 use constant dir_maximum_dxdy => (1,-1); # South-East
  2         4  
  2         3738  
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 8123 my $self = shift->SUPER::new(@_);
115 45   100     274 $self->{'arms'} = max(1, min(4, $self->{'arms'} || 1));
116 45         109 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 26546     26546 1 104493 my ($self, $n) = @_;
127             ### QuintetCentres n_to_xy(): "arms=$self->{'arms'} $n"
128              
129 26546 50       50735 if ($n < 0) {
130 0         0 return;
131             }
132 26546 50       56258 if (is_infinite($n)) {
133 0         0 return ($n,$n);
134             }
135              
136             {
137 26546         49331 my $int = int($n);
  26546         40838  
138 26546 100       49450 if ($n != $int) {
139 63         162 my ($x1,$y1) = $self->n_to_xy($int);
140 63         155 my ($x2,$y2) = $self->n_to_xy($int+$self->{'arms'});
141 63         106 my $frac = $n - $int; # inherit possible BigFloat
142 63         110 my $dx = $x2-$x1;
143 63         86 my $dy = $y2-$y1;
144 63         240 return ($frac*$dx + $x1, $frac*$dy + $y1);
145             }
146 26483         41016 $n = $int; # BigFloat int() gives BigInt, use that
147             }
148 26483         39474 my $zero = ($n * 0); # inherit BigInt 0
149              
150             # arm as initial rotation
151 26483         59012 my $rot = _divrem_mutate ($n, $self->{'arms'});
152              
153 26483         59625 my @digits = digit_split_lowtohigh($n,5);
154 26483         45135 my @sx;
155             my @sy;
156             {
157 26483         37798 my $sx = $zero + $dir4_to_dx[$rot];
  26483         45025  
158 26483         39581 my $sy = $zero + $dir4_to_dy[$rot];
159 26483         45575 foreach (@digits) {
160 129849         186259 push @sx, $sx;
161 129849         186169 push @sy, $sy;
162              
163             # 2*(sx,sy) + rot+90(sx,sy)
164 129849         225963 ($sx,$sy) = (2*$sx - $sy,
165             2*$sy + $sx);
166             }
167             ### @digits
168 26483         39340 my $rev = 0;
169 26483         58847 for (my $i = $#digits; $i >= 0; $i--) { # high to low
170             ### digit: $digits[$i]
171 129849 100       228936 if ($rev) {
172             ### reverse: "$digits[$i] to ".(5 - $digits[$i])
173 56523         85555 $digits[$i] = 4 - $digits[$i];
174             }
175 129849         246192 $rev ^= $digit_reverse[$digits[$i]];
176             ### now rev: $rev
177             }
178             }
179             ### reversed n: @digits
180              
181              
182 26483         42732 my $x =
183             my $y =
184             my $ox =
185             my $oy = $zero;
186              
187 26483         53934 while (defined (my $digit = shift @digits)) { # low to high
188 129849         190120 my $sx = shift @sx;
189 129849         184639 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 129849 100       266000 if ($digit == 0) {
    100          
    100          
    100          
200 36891         52206 $x -= $sx; # left at 180
201 36891         50662 $y -= $sy;
202              
203             } elsif ($digit == 1) {
204             # centre
205 26786         46084 ($x,$y) = (-$y,$x); # rotate -90
206             ### rotate to: "$x,$y"
207             # $rot--;
208              
209             } elsif ($digit == 2) {
210 20199         28056 $x += $sy; # down at -90
211 20199         28112 $y -= $sx;
212             ### offset to: "$x,$y"
213              
214             } elsif ($digit == 3) {
215 19367         33963 ($x,$y) = (-$y,$x); # rotate -90
216 19367         27567 $x += $sx; # right at 0
217 19367         28331 $y += $sy;
218             # $rot++;
219              
220             } else { # $digit == 4
221 26606         44768 ($x,$y) = ($y,-$x); # rotate +90
222 26606         39669 $x -= $sy; # up at +90
223 26606         36928 $y += $sx;
224             # $rot++;
225             }
226              
227 129849         181753 $ox += $sx;
228 129849         258822 $oy += $sy;
229             }
230              
231             ### final: "$x,$y origin $ox,$oy"
232 26483         70098 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 18650     18650 1 52011 my ($self, $x, $y) = @_;
259             ### QuintetCentres xy_to_n(): "$x, $y"
260              
261 18650         37342 $x = round_nearest($x);
262 18650         36194 $y = round_nearest($y);
263              
264 18650         39505 foreach my $overflow (2*$x + 2*$y, 2*$x - 2*$y) {
265 37300 50       74244 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 18650         41827 my @digits;
272             my $arm;
273 18650         0 my $state;
274 18650         25766 for (;;) {
275             # if ($level_limit-- < 0) {
276             # ### oops, level limit ...
277             # return undef;
278             # }
279 94468 100       192018 if ($x == 0) {
    100          
280 19756 100       37279 if ($y == 0) {
281             ### found first arm 0,0 ...
282 9633         14332 $arm = 0;
283 9633         13599 $state = 0;
284 9633         14795 last;
285             }
286 10123 100       19417 if ($y == 1) {
287             ### found second arm 0,1 ...
288 4935         6960 $arm = 1;
289 4935         6899 $state = 20;
290 4935         7941 last;
291             }
292             } elsif ($x == -1) {
293 7988 100       15370 if ($y == 1) {
294             ### found third arm -1,1 ...
295 2768         4047 $arm = 2;
296 2768         4032 $state = 50;
297 2768         4189 last;
298             }
299 5220 100       9362 if ($y == 0) {
300             ### found fourth arm -1,0 ...
301 1314         1992 $arm = 3;
302 1314         1884 $state = 70;
303 1314         2005 last;
304             }
305             }
306 75818         121801 my $m = (2*$x + $y) % 5;
307             ### at: "$x,$y digits=".join(',',@digits)
308             ### mod remainder: $m
309              
310 75818         105627 $x -= $modulus_to_x[$m];
311 75818         105411 $y -= $modulus_to_y[$m];
312 75818         110713 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 75818         148845 ($x,$y) = ((2*$x + $y) / 5,
324             (2*$y - $x) / 5);
325             }
326              
327             ### @digits
328 18650         30102 my $arms = $self->{'arms'};
329 18650 100       34593 if ($arm >= $arms) {
330 139         484 return undef;
331             }
332              
333 18511         26404 my $n = 0;
334 18511         30627 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 75292         113835 $n = 5*$n + $modulus_to_digit[$state + $m];
341 75292         122831 $state = $modulus_to_digit[$state+5 + $m];
342             }
343             ### final n along arm: $n
344              
345 18511         50325 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 12710 my ($self, $x1,$y1, $x2,$y2) = @_;
362             ### QuintetCurve rect_to_n_range(): "$x1,$y1 $x2,$y2"
363              
364 134         693 my ($r_lo, $r_hi) = _rect_to_radius_range($x1,$y1, $x2,$y2);
365 134         304 $r_hi *= 2;
366 134         436 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         293 my $level_limit = $level_plus_1;
371             ### $level_limit
372 134 50       410 if (is_infinite($level_limit)) { return ($level_limit,$level_limit); }
  0         0  
373              
374 134         410 $x1 = round_nearest ($x1);
375 134         339 $y1 = round_nearest ($y1);
376 134         340 $x2 = round_nearest ($x2);
377 134         359 $y2 = round_nearest ($y2);
378 134 50       336 ($x1,$x2) = ($x2,$x1) if $x1 > $x2;
379 134 100       322 ($y1,$y2) = ($y2,$y1) if $y1 > $y2;
380             ### sorted range: "$x1,$y1 $x2,$y2"
381              
382             my $rect_dist = sub {
383 26136     26136   42286 my ($x,$y) = @_;
384 26136 100       52139 my $xd = ($x < $x1 ? $x1 - $x
    100          
385             : $x > $x2 ? $x - $x2
386             : 0);
387 26136 100       49502 my $yd = ($y < $y1 ? $y1 - $y
    100          
388             : $y > $y2 ? $y - $y2
389             : 0);
390 26136         46749 return ($xd*$xd + $yd*$yd);
391 134         949 };
392              
393 134         339 my $arms = $self->{'arms'};
394             ### $arms
395 134         239 my $n_lo;
396             {
397 134         214 my @hypot = (4);
  134         284  
398 134         228 my $top = 0;
399 134         224 for (;;) {
400 457         944 ARM_LO: foreach my $arm (0 .. $arms-1) {
401 457         702 my $i = 0;
402 457         628 my @digits;
403 457 100       938 if ($top > 0) {
404 323         753 @digits = ((0)x($top-1), 1);
405             } else {
406 134         261 @digits = (0);
407             }
408              
409 457         698 for (;;) {
410 12263         17564 my $n = 0;
411 12263         20243 foreach my $digit (reverse @digits) { # high to low
412 61515         89182 $n = 5*$n + $digit;
413             }
414 12263         17335 $n = $n*$arms + $arm;
415             ### lo consider: "i=$i digits=".join(',',reverse @digits)." is n=$n"
416              
417 12263         24535 my ($nx,$ny) = $self->n_to_xy($n);
418 12263         23655 my $nh = &$rect_dist ($nx,$ny);
419 12263 100 100     30483 if ($i == 0 && $nh == 0) {
420             ### lo found inside: $n
421 134 50 33     410 if (! defined $n_lo || $n < $n_lo) {
422 134         218 $n_lo = $n;
423             }
424 134         471 next ARM_LO;
425             }
426              
427 12129 100 100     31783 if ($i == 0 || $nh > $hypot[$i]) {
428             ### too far away: "nxy=$nx,$ny nh=$nh vs ".$hypot[$i]
429              
430 10631         22828 while (++$digits[$i] > 4) {
431 2447         3518 $digits[$i] = 0;
432 2447 100       6031 if (++$i <= $top) {
433             ### backtrack up ...
434             } else {
435             ### not found within this top and arm, next arm ...
436 323         872 next ARM_LO;
437             }
438             }
439             } else {
440             ### lo descend ...
441             ### assert: $i > 0
442 1498         2551 $i--;
443 1498         2455 $digits[$i] = 0;
444             }
445             }
446             }
447              
448             # if an $n_lo was found on any arm within this $top then done
449 457 100       1377 if (defined $n_lo) {
450 134         284 last;
451             }
452              
453             ### lo extend top ...
454 323 50       697 if (++$top > $level_limit) {
455             ### nothing below level limit ...
456 0         0 return (1,0);
457             }
458 323         685 $hypot[$top] = 5 * $hypot[$top-1];
459             }
460             }
461              
462 134         271 my $n_hi = 0;
463 134         474 ARM_HI: foreach my $arm (reverse 0 .. $arms-1) {
464 134         339 my @digits = ((4) x $level_limit);
465 134         247 my $i = $#digits;
466 134         207 for (;;) {
467 13873         19488 my $n = 0;
468 13873         23459 foreach my $digit (reverse @digits) { # high to low
469 91569         130715 $n = 5*$n + $digit;
470             }
471 13873         19732 $n = $n*$arms + $arm;
472             ### hi consider: "arm=$arm i=$i digits=".join(',',reverse @digits)." is n=$n"
473              
474 13873         28380 my ($nx,$ny) = $self->n_to_xy($n);
475 13873         27650 my $nh = &$rect_dist ($nx,$ny);
476 13873 100 100     32782 if ($i == 0 && $nh == 0) {
477             ### hi found inside: $n
478 134 100       329 if ($n > $n_hi) {
479 128         240 $n_hi = $n;
480 128         490 next ARM_HI;
481             }
482             }
483              
484 13745 100 100     38069 if ($i == 0 || $nh > (4 * 5**$i)) {
485             ### too far away: "$nx,$ny nh=$nh vs ".(4 * 5**$i)
486              
487 10927         24080 while (--$digits[$i] < 0) {
488 2331         3323 $digits[$i] = 4;
489 2331 100       5662 if (++$i < $level_limit) {
490             ### hi backtrack up ...
491             } else {
492             ### hi nothing within level limit for this arm ...
493 6         18 next ARM_HI;
494             }
495             }
496              
497             } else {
498             ### hi descend
499             ### assert: $i > 0
500 2818         4182 $i--;
501 2818         4525 $digits[$i] = 4;
502             }
503             }
504             }
505              
506 134 100       341 if ($n_hi == 0) {
507             ### oops, lo found but hi not found
508 6         11 $n_hi = $n_lo;
509             }
510              
511 134         1400 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 407 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__