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 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   9025 use 5.004;
  2         27  
31 2     2   11 use strict;
  2         4  
  2         43  
32 2     2   999 use POSIX 'ceil';
  2         15701  
  2         10  
33             #use List::Util 'min','max';
34             *min = \&Math::PlanePath::_min;
35             *max = \&Math::PlanePath::_max;
36              
37 2     2   2899 use vars '$VERSION', '@ISA';
  2         4  
  2         133  
38             $VERSION = 127;
39 2     2   1463 use Math::PlanePath;
  2         7  
  2         101  
40             @ISA = ('Math::PlanePath');
41             *_divrem_mutate = \&Math::PlanePath::_divrem_mutate;
42              
43 2     2   906 use Math::PlanePath::SacksSpiral;
  2         7  
  2         104  
44             *_rect_to_radius_range = \&Math::PlanePath::SacksSpiral::_rect_to_radius_range;
45              
46             use Math::PlanePath::Base::Generic
47 2         107 'is_infinite',
48 2     2   16 'round_nearest';
  2         4  
49             use Math::PlanePath::Base::Digits
50 2         123 'digit_split_lowtohigh',
51 2     2   910 'round_up_pow';
  2         6  
52              
53             # uncomment this to run the ### lines
54             # use Smart::Comments;
55              
56              
57 2     2   15 use constant n_start => 0;
  2         3  
  2         159  
58 2         269 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   13 } ];
  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   12 use constant dx_minimum => -1;
  2         4  
  2         103  
85 2     2   12 use constant dx_maximum => 1;
  2         3  
  2         87  
86 2     2   11 use constant dy_minimum => -1;
  2         11  
  2         122  
87 2     2   19 use constant dy_maximum => 1;
  2         6  
  2         263  
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         3  
  2         101  
99 2     2   11 use constant dsumxy_maximum => 2;
  2         5  
  2         116  
100 2     2   12 use constant ddiffxy_minimum => -2;
  2         5  
  2         99  
101 2     2   13 use constant ddiffxy_maximum => 2;
  2         4  
  2         119  
102 2     2   13 use constant dir_maximum_dxdy => (1,-1); # South-East
  2         3  
  2         3698  
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 8159 my $self = shift->SUPER::new(@_);
115 45   100     286 $self->{'arms'} = max(1, min(4, $self->{'arms'} || 1));
116 45         117 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 23124     23124 1 98508 my ($self, $n) = @_;
127             ### QuintetCentres n_to_xy(): "arms=$self->{'arms'} $n"
128              
129 23124 50       42260 if ($n < 0) {
130 0         0 return;
131             }
132 23124 50       45129 if (is_infinite($n)) {
133 0         0 return ($n,$n);
134             }
135              
136             {
137 23124         39889 my $int = int($n);
  23124         32886  
138 23124 100       39371 if ($n != $int) {
139 63         145 my ($x1,$y1) = $self->n_to_xy($int);
140 63         154 my ($x2,$y2) = $self->n_to_xy($int+$self->{'arms'});
141 63         107 my $frac = $n - $int; # inherit possible BigFloat
142 63         96 my $dx = $x2-$x1;
143 63         93 my $dy = $y2-$y1;
144 63         222 return ($frac*$dx + $x1, $frac*$dy + $y1);
145             }
146 23061         33120 $n = $int; # BigFloat int() gives BigInt, use that
147             }
148 23061         33078 my $zero = ($n * 0); # inherit BigInt 0
149              
150             # arm as initial rotation
151 23061         50320 my $rot = _divrem_mutate ($n, $self->{'arms'});
152              
153 23061         49634 my @digits = digit_split_lowtohigh($n,5);
154 23061         35654 my @sx;
155             my @sy;
156             {
157 23061         30999 my $sx = $zero + $dir4_to_dx[$rot];
  23061         35585  
158 23061         33230 my $sy = $zero + $dir4_to_dy[$rot];
159 23061         37262 foreach (@digits) {
160 105690         144932 push @sx, $sx;
161 105690         138265 push @sy, $sy;
162              
163             # 2*(sx,sy) + rot+90(sx,sy)
164 105690         180138 ($sx,$sy) = (2*$sx - $sy,
165             2*$sy + $sx);
166             }
167             ### @digits
168 23061         31562 my $rev = 0;
169 23061         51258 for (my $i = $#digits; $i >= 0; $i--) { # high to low
170             ### digit: $digits[$i]
171 105690 100       173869 if ($rev) {
172             ### reverse: "$digits[$i] to ".(5 - $digits[$i])
173 45240         65544 $digits[$i] = 4 - $digits[$i];
174             }
175 105690         203493 $rev ^= $digit_reverse[$digits[$i]];
176             ### now rev: $rev
177             }
178             }
179             ### reversed n: @digits
180              
181              
182 23061         34593 my $x =
183             my $y =
184             my $ox =
185             my $oy = $zero;
186              
187 23061         44814 while (defined (my $digit = shift @digits)) { # low to high
188 105690         138669 my $sx = shift @sx;
189 105690         141075 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 105690 100       216806 if ($digit == 0) {
    100          
    100          
    100          
200 26259         34148 $x -= $sx; # left at 180
201 26259         34050 $y -= $sy;
202              
203             } elsif ($digit == 1) {
204             # centre
205 22393         36698 ($x,$y) = (-$y,$x); # rotate -90
206             ### rotate to: "$x,$y"
207             # $rot--;
208              
209             } elsif ($digit == 2) {
210 17835         23336 $x += $sy; # down at -90
211 17835         23384 $y -= $sx;
212             ### offset to: "$x,$y"
213              
214             } elsif ($digit == 3) {
215 17076         28705 ($x,$y) = (-$y,$x); # rotate -90
216 17076         23796 $x += $sx; # right at 0
217 17076         22444 $y += $sy;
218             # $rot++;
219              
220             } else { # $digit == 4
221 22127         35940 ($x,$y) = ($y,-$x); # rotate +90
222 22127         28891 $x -= $sy; # up at +90
223 22127         29552 $y += $sx;
224             # $rot++;
225             }
226              
227 105690         139987 $ox += $sx;
228 105690         201463 $oy += $sy;
229             }
230              
231             ### final: "$x,$y origin $ox,$oy"
232 23061         59886 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 18672     18672 1 55364 my ($self, $x, $y) = @_;
259             ### QuintetCentres xy_to_n(): "$x, $y"
260              
261 18672         37053 $x = round_nearest($x);
262 18672         37223 $y = round_nearest($y);
263              
264 18672         40708 foreach my $overflow (2*$x + 2*$y, 2*$x - 2*$y) {
265 37344 50       73412 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 18672         42105 my @digits;
272             my $arm;
273 18672         0 my $state;
274 18672         25287 for (;;) {
275             # if ($level_limit-- < 0) {
276             # ### oops, level limit ...
277             # return undef;
278             # }
279 94592 100       194859 if ($x == 0) {
    100          
280 19784 100       35932 if ($y == 0) {
281             ### found first arm 0,0 ...
282 9653         12562 $arm = 0;
283 9653         12446 $state = 0;
284 9653         14393 last;
285             }
286 10131 100       18504 if ($y == 1) {
287             ### found second arm 0,1 ...
288 4935         6755 $arm = 1;
289 4935         6586 $state = 20;
290 4935         6947 last;
291             }
292             } elsif ($x == -1) {
293 7987 100       14964 if ($y == 1) {
294             ### found third arm -1,1 ...
295 2768         3916 $arm = 2;
296 2768         3604 $state = 50;
297 2768         4040 last;
298             }
299 5219 100       9310 if ($y == 0) {
300             ### found fourth arm -1,0 ...
301 1316         1802 $arm = 3;
302 1316         1641 $state = 70;
303 1316         1912 last;
304             }
305             }
306 75920         119436 my $m = (2*$x + $y) % 5;
307             ### at: "$x,$y digits=".join(',',@digits)
308             ### mod remainder: $m
309              
310 75920         106087 $x -= $modulus_to_x[$m];
311 75920         100483 $y -= $modulus_to_y[$m];
312 75920         104452 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 75920         154832 ($x,$y) = ((2*$x + $y) / 5,
324             (2*$y - $x) / 5);
325             }
326              
327             ### @digits
328 18672         30133 my $arms = $self->{'arms'};
329 18672 100       33104 if ($arm >= $arms) {
330 141         492 return undef;
331             }
332              
333 18531         25196 my $n = 0;
334 18531         30723 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 75391         109103 $n = 5*$n + $modulus_to_digit[$state + $m];
341 75391         119464 $state = $modulus_to_digit[$state+5 + $m];
342             }
343             ### final n along arm: $n
344              
345 18531         49634 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 12633 my ($self, $x1,$y1, $x2,$y2) = @_;
362             ### QuintetCurve rect_to_n_range(): "$x1,$y1 $x2,$y2"
363              
364 134         626 my ($r_lo, $r_hi) = _rect_to_radius_range($x1,$y1, $x2,$y2);
365 134         320 $r_hi *= 2;
366 134         454 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         272 my $level_limit = $level_plus_1;
371             ### $level_limit
372 134 50       427 if (is_infinite($level_limit)) { return ($level_limit,$level_limit); }
  0         0  
373              
374 134         420 $x1 = round_nearest ($x1);
375 134         351 $y1 = round_nearest ($y1);
376 134         333 $x2 = round_nearest ($x2);
377 134         293 $y2 = round_nearest ($y2);
378 134 50       390 ($x1,$x2) = ($x2,$x1) if $x1 > $x2;
379 134 100       321 ($y1,$y2) = ($y2,$y1) if $y1 > $y2;
380             ### sorted range: "$x1,$y1 $x2,$y2"
381              
382             my $rect_dist = sub {
383 24819     24819   39869 my ($x,$y) = @_;
384 24819 100       49539 my $xd = ($x < $x1 ? $x1 - $x
    100          
385             : $x > $x2 ? $x - $x2
386             : 0);
387 24819 100       46181 my $yd = ($y < $y1 ? $y1 - $y
    100          
388             : $y > $y2 ? $y - $y2
389             : 0);
390 24819         43451 return ($xd*$xd + $yd*$yd);
391 134         941 };
392              
393 134         319 my $arms = $self->{'arms'};
394             ### $arms
395 134         220 my $n_lo;
396             {
397 134         227 my @hypot = (4);
  134         296  
398 134         232 my $top = 0;
399 134         217 for (;;) {
400 441         916 ARM_LO: foreach my $arm (0 .. $arms-1) {
401 441         688 my $i = 0;
402 441         587 my @digits;
403 441 100       825 if ($top > 0) {
404 307         660 @digits = ((0)x($top-1), 1);
405             } else {
406 134         354 @digits = (0);
407             }
408              
409 441         624 for (;;) {
410 11103         15078 my $n = 0;
411 11103         18390 foreach my $digit (reverse @digits) { # high to low
412 54713         76699 $n = 5*$n + $digit;
413             }
414 11103         15336 $n = $n*$arms + $arm;
415             ### lo consider: "i=$i digits=".join(',',reverse @digits)." is n=$n"
416              
417 11103         22598 my ($nx,$ny) = $self->n_to_xy($n);
418 11103         22109 my $nh = &$rect_dist ($nx,$ny);
419 11103 100 100     27905 if ($i == 0 && $nh == 0) {
420             ### lo found inside: $n
421 134 50 33     406 if (! defined $n_lo || $n < $n_lo) {
422 134         222 $n_lo = $n;
423             }
424 134         455 next ARM_LO;
425             }
426              
427 10969 100 100     29636 if ($i == 0 || $nh > $hypot[$i]) {
428             ### too far away: "nxy=$nx,$ny nh=$nh vs ".$hypot[$i]
429              
430 9654         19941 while (++$digits[$i] > 4) {
431 2206         3142 $digits[$i] = 0;
432 2206 100       5400 if (++$i <= $top) {
433             ### backtrack up ...
434             } else {
435             ### not found within this top and arm, next arm ...
436 307         854 next ARM_LO;
437             }
438             }
439             } else {
440             ### lo descend ...
441             ### assert: $i > 0
442 1315         1924 $i--;
443 1315         2245 $digits[$i] = 0;
444             }
445             }
446             }
447              
448             # if an $n_lo was found on any arm within this $top then done
449 441 100       1001 if (defined $n_lo) {
450 134         266 last;
451             }
452              
453             ### lo extend top ...
454 307 50       657 if (++$top > $level_limit) {
455             ### nothing below level limit ...
456 0         0 return (1,0);
457             }
458 307         611 $hypot[$top] = 5 * $hypot[$top-1];
459             }
460             }
461              
462 134         231 my $n_hi = 0;
463 134         479 ARM_HI: foreach my $arm (reverse 0 .. $arms-1) {
464 134         379 my @digits = ((4) x $level_limit);
465 134         272 my $i = $#digits;
466 134         195 for (;;) {
467 13716         19199 my $n = 0;
468 13716         22378 foreach my $digit (reverse @digits) { # high to low
469 87145         122051 $n = 5*$n + $digit;
470             }
471 13716         19100 $n = $n*$arms + $arm;
472             ### hi consider: "arm=$arm i=$i digits=".join(',',reverse @digits)." is n=$n"
473              
474 13716         28469 my ($nx,$ny) = $self->n_to_xy($n);
475 13716         27449 my $nh = &$rect_dist ($nx,$ny);
476 13716 100 100     32535 if ($i == 0 && $nh == 0) {
477             ### hi found inside: $n
478 134 100       320 if ($n > $n_hi) {
479 128         226 $n_hi = $n;
480 128         559 next ARM_HI;
481             }
482             }
483              
484 13588 100 100     38601 if ($i == 0 || $nh > (4 * 5**$i)) {
485             ### too far away: "$nx,$ny nh=$nh vs ".(4 * 5**$i)
486              
487 10804         23791 while (--$digits[$i] < 0) {
488 2315         3182 $digits[$i] = 4;
489 2315 100       5787 if (++$i < $level_limit) {
490             ### hi backtrack up ...
491             } else {
492             ### hi nothing within level limit for this arm ...
493 6         21 next ARM_HI;
494             }
495             }
496              
497             } else {
498             ### hi descend
499             ### assert: $i > 0
500 2784         3922 $i--;
501 2784         4567 $digits[$i] = 4;
502             }
503             }
504             }
505              
506 134 100       334 if ($n_hi == 0) {
507             ### oops, lo found but hi not found
508 6         14 $n_hi = $n_lo;
509             }
510              
511 134         1344 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 391 my ($self, $level) = @_;
526 6         21 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__