File Coverage

blib/lib/Math/PlanePath/FactorRationals.pm
Criterion Covered Total %
statement 131 203 64.5
branch 33 62 53.2
condition 5 11 45.4
subroutine 23 28 82.1
pod 4 4 100.0
total 196 308 63.6


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             # Multiples of prime make grid.
20              
21             # [13] L. S. Johnston, Denumerability of the rational number system, Amer. Math. Monthly, 55 (Feb.
22             # 1948), no. 2, 65-70.
23             # www.jstor.org/stable/2305738
24              
25              
26             # prime factors q1,..qk of n
27             # f(m/n) = m^2*n^2/ (q1q2...qk)
28              
29             # Kevin McCrimmon, 1960
30             #
31             # integer prod p[i]^a[i] -> rational prod p[i]^b[i]
32             # b[i] = a[2i-1] if a[2i-1]!=0
33             # b[1]=a[1], b[2]=a[3], b[3]=a[5]
34             # b[i] = -a[2k] if a[2i-1]=0 and is kth such
35             #
36             # b[i] = f(a[i]) where f(n) = (-1)^(n+1) * floor((n+1)/2)
37             # f(0) = 0
38             # f(1) = 1
39             # f(2) = -1
40             # f(3) = 2
41             # f(4) = -2
42              
43             # Gerald Freilich, 1965
44             #
45             # f(n) = n/2 if n even n>=0
46             # = -(n+1)/2 if n odd n>0
47             # f(0)=0/2 = 0
48             # f(1)=-(1+1)/2 = -1
49             # f(2)=2/2 = 1
50             # f(3)=-(3+1)/2 = -2
51             # f(4)=4/2 = 2
52              
53             # Yoram Sagher, "Counting the rationals", American Math Monthly, Nov 1989,
54             # page 823. http://www.jstor.org/stable/2324846
55             #
56             # m = p1^e1.p2^e2...
57             # n = q1^f1.q2^f2...
58             # f(m/n) = p1^2e1.p2^2e2... . q1^(2f1-1).q2^(2f2-1)...
59             # so 0 -> 0 0 -> 0
60             # num 1 -> 2 1 -> -1
61             # 2 -> 4 2 -> 1
62             # den -1 1 -> 2*1-1 = 1 3 -> -2
63             # -2 2 -> 2*2-1 = 3 4 -> 2
64              
65             # Umberto Cerruti, "Ordinare i razionali Gli alberi di Keplero e di
66             # Calkin-Wilf", following T.J. Heard
67             # B(2k)=-k even=negative and zero
68             # B(2k-1)=k odd=positive
69             # which is Y/X invert
70             # B(0 =2*0) = 0
71             # B(1 =2*1-1) = 1
72             # B(2 =2*1) = -1
73             # B(3 =2*2-1) = 2
74             # B(4 =2*2) = -2
75              
76              
77             package Math::PlanePath::FactorRationals;
78 1     1   1388 use 5.004;
  1         4  
79 1     1   5 use strict;
  1         2  
  1         23  
80 1     1   4 use Carp 'croak';
  1         2  
  1         47  
81 1     1   5 use List::Util 'min';
  1         3  
  1         100  
82             #use List::Util 'max';
83             *max = \&Math::PlanePath::_max;
84              
85 1     1   7 use vars '$VERSION', '@ISA';
  1         2  
  1         70  
86             $VERSION = 128;
87 1     1   760 use Math::PlanePath;
  1         2  
  1         43  
88             @ISA = ('Math::PlanePath');
89              
90             use Math::PlanePath::Base::Generic
91 1         44 'is_infinite',
92 1     1   6 'round_nearest';
  1         2  
93             use Math::PlanePath::Base::Digits
94 1     1   539 'digit_join_lowtohigh';
  1         3  
  1         68  
95              
96 1     1   524 use Math::PlanePath::CoprimeColumns;
  1         2  
  1         66  
97             *_coprime = \&Math::PlanePath::CoprimeColumns::_coprime;
98              
99             # uncomment this to run the ### lines
100             # use Smart::Comments;
101              
102              
103             # Not yet.
104 1         57 use constant parameter_info_array =>
105             [ { name => 'factor_coding',
106             display => 'Sign Encoding',
107             type => 'enum',
108             default => 'even/odd',
109             choices => ['even/odd','odd/even',
110             'negabinary','revbinary',
111             ],
112             choices_display => ['Even/Odd','Odd/Even',
113             'Negabinary','Revbinary',
114             ],
115             },
116 1     1   6 ];
  1         2  
117              
118 1     1   5 use constant class_x_negative => 0;
  1         2  
  1         39  
119 1     1   11 use constant class_y_negative => 0;
  1         1  
  1         40  
120 1     1   5 use constant x_minimum => 1;
  1         2  
  1         40  
121 1     1   13 use constant y_minimum => 1;
  1         4  
  1         56  
122 1     1   7 use constant gcdxy_maximum => 1; # no common factor
  1         9  
  1         47  
123 1     1   6 use constant absdy_minimum => 1;
  1         2  
  1         1400  
124              
125             # factor_coding=even/odd
126             # factor_coding=odd/even
127             # dir_minimum_dxdy() suspect dir approaches 0.
128             # Eg. N=5324 = 2^2.11^3 dx=3,dy=92 0.97925
129             # N=642735 = 3^5.23^2 dX=45 dY=4 Dir4=0.05644
130             # 642736 = 2^4.17^2.139
131             # dir_maximum_dxdy() suspect approaches 360 degrees
132             # use constant dir_maximum_dxdy => (0,0); # the default
133             #
134             # factor_coding=negabinary
135             # dir_minimum_dxdy() = East 1,0 at N=1
136             # dir_maximum_dxdy() believe approaches 360 degrees
137             # Eg. N=40=2^3.5 X=5, Y=2
138             # N=41=41 X=41, Y=1
139             # N=multiple 8 and solitary primes, followed by N+1=prime is dX=big, dY=-1
140             #
141             # factor_coding=revbinary
142             # dir_maximum_dxdy() approaches 360 degrees dY=-1, dX=big
143             # Eg. N=7208=2^3*17*53 X=17*53 Y=2
144             # N=7209=3^4*89 X=3^4*89 Y=1
145             # dX=6308 dY=-1
146              
147              
148             #------------------------------------------------------------------------------
149             # even/odd
150              
151             # $n>=0, return a positive if even or negative if odd
152             # $n==0 return 0
153             # $n==1 return -1
154             # $n==2 return +1
155             # $n==3 return -2
156             # $n==4 return +2
157             sub _pos_to_pn__even_odd {
158 81     81   122 my ($n) = @_;
159 81 100       177 return ($n % 2 ? -1-$n : $n) / 2;
160             }
161              
162             # # $n is positive or negative, return even for positive or odd for negative.
163             # # $n==0 return 0
164             # # $n==-1 return 1
165             # # $n==+1 return 2
166             # # $n==-2 return 3
167             # # $n==+2 return 4
168             # sub _pn_to_pos__even_odd {
169             # my ($n) = @_;
170             # return ($n >= 0 ? 2*$n : -1-2*$n);
171             # }
172              
173             #------------------------------------------------------------------------------
174             # odd/even
175              
176             # $n>=0, return a positive if even or negative if odd
177             # $n==0 return 0
178             # $n==1 return +1
179             # $n==2 return -1
180             # $n==3 return +2
181             # $n==4 return -2
182             sub _pos_to_pn__odd_even {
183 0     0   0 my ($n) = @_;
184 0 0       0 return ($n % 2 ? $n+1 : -$n) / 2;
185             }
186              
187             # # $n is positive or negative, return odd for positive or even for negative.
188             # # $n==0 return 0
189             # # $n==+1 return 1
190             # # $n==-1 return 2
191             # # $n==+2 return 3
192             # # $n==-2 return 4
193             # sub _pn_to_pos__odd_even {
194             # my ($n) = @_;
195             # return ($n <= 0 ? -2*$n : 2*$n-1);
196             # }
197              
198             #------------------------------------------------------------------------------
199             # negabinary
200              
201             sub _pn_to_pos__negabinary {
202 0     0   0 my ($n) = @_;
203 0         0 my @bits;
204 0         0 while ($n) {
205 0         0 my $bit = ($n % 2);
206 0         0 push @bits, $bit;
207 0         0 $n -= $bit;
208 0         0 $n /= 2;
209 0         0 $n = -$n;
210             }
211 0         0 return digit_join_lowtohigh(\@bits, 2,
212             $n); # zero
213             }
214             sub _pos_to_pn__negabinary {
215 0     0   0 my ($n) = @_;
216 0         0 return (($n & 0x55555555) - ($n & 0xAAAAAAAA));
217             }
218              
219             #------------------------------------------------------------------------------
220             # revbinary
221             # A065620 pos -> pn
222             # A065621 pn(+ve) -> pos
223             # A048724 pn(-ve) -> pos n XOR 2n
224             # A048725 A048724 twice
225             # cf
226             # A073122 minimizing by taking +/- powers cf A072219 A072339
227              
228             # rev = 2^e0 - 2^e1 + 2^e2 - 2^e3 + ... + (-1)^t*2^et
229             # 0 <= e0 < e1 < e2 ...
230             sub _pos_to_pn__revbinary {
231 0     0   0 my ($n) = @_;
232 0         0 my $sign = 1;
233 0         0 my $ret = 0;
234 0         0 for (my $bit = 1; $bit <= $n; $bit *= 2) {
235 0 0       0 if ($n & $bit) {
236 0         0 $ret += $bit*$sign;
237 0         0 $sign = -$sign;
238             }
239             }
240 0         0 return $ret;
241             }
242             sub _pn_to_pos__revbinary {
243 0     0   0 my ($n) = @_;
244 0         0 my @bits;
245 0         0 while ($n) {
246 0         0 my $bit = ($n % 2);
247 0         0 push @bits, $bit;
248 0         0 $n -= $bit;
249 0         0 $n /= 2;
250 0 0       0 if ($bit) {
251 0         0 $n = -$n;
252             }
253             }
254 0         0 return digit_join_lowtohigh(\@bits, 2,
255             $n); # zero
256             }
257              
258             #------------------------------------------------------------------------------
259              
260             my %factor_coding__pos_to_pn = ('even/odd' => \&_pos_to_pn__even_odd,
261             'odd/even' => \&_pos_to_pn__odd_even,
262             negabinary => \&_pos_to_pn__negabinary,
263             revbinary => \&_pos_to_pn__revbinary,
264             );
265             my %factor_coding__pn_to_pos = (# 'even/odd' => \&_pn_to_pos__even_odd,
266             # 'odd/even' => \&_pn_to_pos__odd_even,
267             negabinary => \&_pn_to_pos__negabinary,
268             revbinary => \&_pn_to_pos__revbinary,
269             );
270              
271             sub new {
272 1     1 1 11 my $self = shift->SUPER::new(@_);
273              
274 1   50     12 my $factor_coding = ($self->{'factor_coding'} ||= 'even/odd');
275 1 50       4 $factor_coding__pos_to_pn{$factor_coding}
276             or croak "Unrecognised factor_coding: ",$factor_coding;
277              
278 1         3 return $self;
279             }
280              
281             sub n_to_xy {
282 45     45 1 4928 my ($self, $n) = @_;
283             ### FactorRationals n_to_xy(): "$n"
284              
285 45 50       110 if ($n < 1) { return; }
  0         0  
286 45 50       104 if (is_infinite($n)) { return ($n,$n); }
  0         0  
287              
288             # what to do for fractional $n?
289             {
290 45         75 my $int = int($n);
  45         69  
291 45 50       85 if ($n != $int) {
292             ### frac ...
293 0         0 my $frac = $n - $int; # inherit possible BigFloat/BigRat
294 0         0 my ($x1,$y1) = $self->n_to_xy($int);
295 0         0 my ($x2,$y2) = $self->n_to_xy($int+1);
296 0         0 my $dx = $x2-$x1;
297 0         0 my $dy = $y2-$y1;
298 0         0 return ($frac*$dx + $x1, $frac*$dy + $y1);
299             }
300 45         64 $n = $int;
301             }
302              
303 45         69 my $zero = $n * 0;
304              
305 45         95 my $pos_to_pn = $factor_coding__pos_to_pn{$self->{'factor_coding'}};
306 45         66 my $x = my $y = ($n * 0) + 1; # inherit bignum 1
307 45         83 my ($limit,$overflow) = _limit($n);
308             ### $limit
309 45         68 my $divisor = 2;
310 45         59 my $dstep = 1;
311 45         90 while ($divisor <= $limit) {
312 69 100       137 if (($n % $divisor) == 0) {
313 36         52 my $count = 0;
314 36         46 for (;;) {
315 309         372 $count++;
316 309         420 $n /= $divisor;
317 309 100       530 if ($n % $divisor) {
318 36         62 my $pn = &$pos_to_pn($count);
319             ### $count
320             ### $pn
321 36         63 my $pow = ($divisor+$zero) ** abs($pn);
322 36 100       124 if ($pn >= 0) {
323 17         24 $x *= $pow;
324             } else {
325 19         33 $y *= $pow;
326             }
327 36         53 last;
328             }
329             }
330 36         85 ($limit,$overflow) = _limit($n);
331             ### $limit
332             }
333 69         98 $divisor += $dstep;
334 69         125 $dstep = 2;
335             }
336 45 50       77 if ($overflow) {
337             ### n too big ...
338 0         0 return;
339             }
340              
341             ### remaining $n is prime, count=1: "n=$n"
342 45         82 my $pn = &$pos_to_pn(1);
343             ### $pn
344 45         77 my $pow = $n ** abs($pn);
345 45 50       70 if ($pn >= 0) {
346 0         0 $x *= $pow;
347             } else {
348 45         84 $y *= $pow;
349             }
350              
351             ### result: "$x, $y"
352 45         107 return ($x, $y);
353             }
354              
355             sub xy_to_n {
356 5670     5670 1 56224 my ($self, $x, $y) = @_;
357              
358 5670         10156 $x = round_nearest ($x);
359 5670         11084 $y = round_nearest ($y);
360             ### FactorRationals xy_to_n(): "x=$x y=$y"
361              
362 5670 100 100     16766 if ($x < 1 || $y < 1) {
363 625         1094 return undef; # negatives and -infinity
364             }
365 5045 50       9373 if (is_infinite($x)) { return $x; } # +infinity or nan
  0         0  
366 5045 50       10472 if (is_infinite($y)) { return $y; } # +infinity or nan
  0         0  
367              
368 5045 50 33     16026 if ($self->{'factor_coding'} eq 'negabinary'
369             || $self->{'factor_coding'} eq 'revbinary') {
370             ### negabinary or revbinary ...
371 0         0 my $pn_to_pos = $factor_coding__pn_to_pos{$self->{'factor_coding'}};
372 0         0 my $n = 1;
373 0         0 my $zero = $x * 0 * $y;
374              
375             # Factorize both $x and $y and apply their pn_to_pos encoded powers to
376             # make $n. A common factor between $x and $y is noticed if $divisor
377             # divides both.
378              
379 0         0 my ($limit,$overflow) = _limit(max($x,$y));
380 0         0 my $dstep = 1;
381 0         0 for (my $divisor = 2; $divisor <= $limit; $divisor += $dstep, $dstep=2) {
382 0         0 my $count = 0;
383 0 0       0 if ($x % $divisor == 0) {
    0          
384 0 0       0 if ($y % $divisor == 0) {
385 0         0 return undef; # common factor
386             }
387 0         0 while ($x % $divisor == 0) {
388 0         0 $count++;
389 0         0 $x /= $divisor; # mutate loop variable
390             }
391             } elsif ($y % $divisor == 0) {
392 0         0 while ($y % $divisor == 0) {
393 0         0 $count--;
394 0         0 $y /= $divisor; # mutate loop variable
395             }
396             } else {
397 0         0 next;
398             }
399              
400             # Here $count > 0 if from $x or $count < 0 if from $y.
401             ### $count
402             ### pn: &$pn_to_pos($count)
403              
404 0         0 $count = &$pn_to_pos($count);
405 0         0 $n *= ($divisor+$zero) ** $count;
406              
407             # new search limit, perhaps smaller than before
408 0         0 ($limit,$overflow) = _limit(max($x,$y));
409             }
410              
411 0 0       0 if ($overflow) {
412             ### x,y too big to find all primes ...
413 0         0 return undef;
414             }
415              
416             # Here $x and $y are primes.
417 0 0 0     0 if ($x > 1 && $x == $y) {
418             ### common factor final remaining prime x,y ...
419 0         0 return undef;
420             }
421              
422             # $x is power p^1 which is negabinary=1 or revbinary=1 so multiply into
423             # $n. $y is power p^-1 and -1 is negabinary=3 so cube and multiply into
424             # $n.
425 0         0 $n *= $x;
426 0         0 $n *= $y*$y*$y;
427              
428 0         0 return $n;
429              
430             } else {
431             ### assert: $self->{'factor_coding'} eq 'even/odd' || $self->{'factor_coding'} eq 'odd/even'
432 5045 50       9292 if ($self->{'factor_coding'} eq 'odd/even') {
433 0         0 ($x,$y) = ($y,$x);
434             }
435              
436             # Factorize $y so as to make an odd power of its primes. Only need to
437             # divide out one copy of each prime, but by dividing out them all the
438             # $limit to search up to is reduced, usually by a lot.
439             #
440             # $ymult is $y with one copy of each prime factor divided out.
441             # $ychop is $y with all primes divided out as they're found.
442             # $y itself is unchanged.
443             #
444 5045         7302 my $ychop = my $ymult = $y;
445              
446 5045         7884 my ($limit,$overflow) = _limit($ychop);
447 5045         7357 my $dstep = 1;
448 5045         9615 for (my $divisor = 2; $divisor <= $limit; $divisor += $dstep, $dstep=2) {
449 9349 100       18931 next if $ychop % $divisor;
450              
451 3751 100       6573 if ($x % $divisor == 0) {
452             ### common factor with X ...
453 2810         5549 return undef;
454             }
455 941         1341 $ymult /= $divisor; # one of $divisor divided out
456 941         1216 do {
457 1370         2495 $ychop /= $divisor; # all of $divisor divided out
458             } until ($ychop % $divisor);
459 941         1498 ($limit,$overflow) = _limit($ychop); # new lower $limit, perhaps
460             }
461              
462 2235 50       3733 if ($overflow) {
463 0         0 return undef; # Y too big to find all primes
464             }
465              
466             # remaining $ychop is a prime, or $ychop==1
467 2235 100       3829 if ($ychop > 1) {
468 1958 100       3522 if ($x % $ychop == 0) {
469             ### common factor with X ...
470 183         386 return undef;
471             }
472 1775         2662 $ymult /= $ychop;
473             }
474              
475 2052         5167 return $x*$x * $y*$ymult;
476             }
477             }
478              
479             #------------------------------------------------------------------------------
480              
481             # all rationals X,Y >= 1 with no common factor
482 1     1   584 use Math::PlanePath::DiagonalRationals;
  1         3  
  1         222  
483             *xy_is_visited = Math::PlanePath::DiagonalRationals->can('xy_is_visited');
484              
485             #------------------------------------------------------------------------------
486              
487             # even/odd
488             # X=2^10 -> N=2^20 is X^2
489             # Y=3 -> N=3
490             # Y=3^2 -> N=3^3
491             # Y=3^3 -> N=3^5
492             # Y=3^4 -> N=3^7
493             # Y*Y / distinct prime factors
494             #
495             # negabinary
496             # X=prime^2 -> N=prime^6 is X^3
497             # X=prime^6 -> N=prime^26 is X^4.33
498             # maximum 101010...10110 -> 1101010...10 approaches factor 5
499             # same for negatives
500             #
501             # revbinary
502             # X=prime^k -> N=prime^(3k) ix X^3
503              
504             # not exact
505             sub rect_to_n_range {
506 47     47 1 7622 my ($self, $x1,$y1, $x2,$y2) = @_;
507             ### rect_to_n_range()
508              
509 47         123 $x1 = round_nearest ($x1);
510 47         95 $y1 = round_nearest ($y1);
511 47         88 $x2 = round_nearest ($x2);
512 47         90 $y2 = round_nearest ($y2);
513              
514 47         99 my $n = max($x1,$x2) * max($y1,$y2);
515 47         74 my $n_squared = $n * $n;
516             return (1,
517             ($self->{'factor_coding'} eq 'negabinary'
518             ? ($n_squared*$n_squared) * $n # X^5*Y^5
519 47 50       177 : $self->{'factor_coding'} eq 'revbinary'
    50          
520             ? $n_squared * $n # X^3*Y^3
521             # even/odd, odd/even
522             : $n_squared)); # X^2*Y^2
523             }
524              
525              
526             #------------------------------------------------------------------------------
527              
528             # _limit() returns ($limit,$overflow).
529             #
530             # $limit is the biggest divisor to attempt trial division of $n. If $n <
531             # 2^32 then $limit=sqrt($n) and that will find all primes. If $n >= 2^32
532             # then $limit is smaller than sqrt($n), being calculated from the length of
533             # $n so as to make a roughly constant amount of time doing divisions. But
534             # $limit is always at least 50 so as to divide by primes up to 50.
535             #
536             # $overflow is a boolean, true if $n is too big to search for all primes and
537             # $limit is something smaller than sqrt($n). $overflow is false if $limit
538             # has not been capped and is enough to find all primes.
539             #
540             sub _limit {
541 6067     6067   9380 my ($n) = @_;
542 6067         10249 my $limit = int(sqrt($n));
543 6067         13763 my $cap = max (int(65536 * 10 / length($n)),
544             50);
545 6067 100       10389 if ($limit > $cap) {
546 1         3 return ($cap, 1);
547             } else {
548 6066         13686 return ($limit, 0);
549             }
550             }
551              
552             1;
553             __END__