File Coverage

blib/lib/Math/Polynomial/Cyclotomic.pm
Criterion Covered Total %
statement 218 218 100.0
branch 82 82 100.0
condition 48 53 90.5
subroutine 29 29 100.0
pod 8 16 50.0
total 385 398 96.7


line stmt bran cond sub pod time code
1             # Copyright (c) 2019-2021 by Martin Becker. This package is free software,
2             # licensed under The Artistic License 2.0 (GPL compatible).
3              
4             package Math::Polynomial::Cyclotomic;
5              
6 3     3   19744 use 5.006;
  3         22  
7 3     3   14 use strict;
  3         5  
  3         51  
8 3     3   11 use warnings;
  3         5  
  3         146  
9 3     3   17 use Carp qw(croak);
  3         5  
  3         191  
10 3     3   3352 use Math::BigInt try => 'GMP';
  3         83367  
  3         20  
11 3     3   71274 use Math::Polynomial 1.019;
  3         6577  
  3         102  
12 3         19 use Math::Prime::Util qw(
13             divisors factor factor_exp euler_phi gcd is_square_free is_power
14             moebius kronecker vecprod
15 3     3   3487 );
  3         29623  
16             require Exporter;
17              
18             our @ISA = qw(Exporter);
19             our @EXPORT_OK = qw(
20             cyclo_poly cyclo_factors cyclo_plusfactors cyclo_poly_iterate
21             cyclo_lucas_cd cyclo_schinzel_cd cyclo_int_factors cyclo_int_plusfactors
22             );
23             our %EXPORT_TAGS = (all => \@EXPORT_OK);
24             our $VERSION = '0.004';
25              
26             # some polynomial with coefficient type Math::BigInt
27             my $default = Math::Polynomial->new(Math::BigInt->new('1'));
28             $default->string_config({fold_sign => 1, times => '*'});
29              
30             # ----- private subroutines -----
31              
32             sub _cyclo_poly {
33 118     118   252 my ($poly, $table, $n, $divisors) = @_;
34 118 100       402 return $table->{$n} if exists $table->{$n};
35 62         302 my $m = vecprod(map { $_->[0] } factor_exp($n)); # $m = radix($n)
  81         247  
36 62         148 my $o = $m & 1;
37 62 100       136 $m >>= 1 if !$o;
38 62         87 my $p;
39 62 100       152 if (exists $table->{$m}) {
40 26         54 $p = $table->{$m};
41             }
42             else {
43 36         98 my $one = $poly->coeff_one;
44 36 100       259 my @d = $divisors? (grep { not $m % $_ } @{$divisors}): divisors($m);
  116         213  
  23         44  
45 36         108 $p = $poly->monomial(pop @d)->sub_const($one);
46 36 100       8847 if (@d) {
47 26         48 my $b = $d[-1];
48 26         63 $p /= $poly->monomial($b)->sub_const($one);
49 26         276966 for (my $i = 1; $i < $#d; ++$i) {
50 11         63333 my $r = $d[$i];
51 11 100       38 if ($b % $r) {
52 9   66     49 $p /= $table->{$r} || _cyclo_poly($poly, $table, $r, \@d);
53             }
54             }
55             }
56 36         77479 $table->{$m} = $p;
57             }
58 62 100       155 if (!$o) {
59 29 100       95 $p = -$p if $m == 1;
60 29         1191 $m <<= 1;
61 29         91 $p = $p->mirror;
62 29         2570 $table->{$m} = $p;
63             }
64 62 100       149 if ($m != $n) {
65 21         92 $p = $p->inflate($n/$m);
66 21         1531 $table->{$n} = $p;
67             }
68 62         230 return $p;
69             }
70              
71             # for a positive integer n, return r,s so that r is square-free and n=rs^2
72             sub _square_free_square {
73 16     16   40 my ($n) = @_;
74 16         43 my ($r, $s) = (1, 1);
75 16         117 foreach my $be (factor_exp($n)) {
76 27         44 my ($b, $e) = @{$be};
  27         56  
77 27 100       69 $r *= $b if $e & 1;
78 27         40 $e >>= 1;
79 27         71 while ($e) {
80 7 100       26 $s *= $b if $e & 1;
81 7 100       23 $e >>= 1 and $b *= $b;
82             }
83             }
84 16         42 return ($r, $s);
85             }
86              
87             # generate polynomials C, D, satisfying the identity of Aurifeuille,
88             # Le Lasseur and Lucas: for n > 1, square-free,
89             # n === 1 (mod 4): Phi_n(x) = C^2 - n*x*D^2
90             # else: Phi_2n(x) = C^2 - n*x*D^2
91             sub _lucas_cd {
92 10     10   29 my ($poly, $table, $n) = @_;
93 10         31 my $c1 = 1 == ($n & 3);
94 10 100       31 my $n1 = $c1? $n: $n + $n;
95 10 100       40 if (exists $table->{"$n1 $n"}) {
96 3         6 return (@{$table->{"$n1 $n"}})
  3         14  
97             }
98 7         24 my $c2 = !($n & 1);
99 7         28 my $ee = euler_phi($n1);
100 7         17 my $e = $ee >> 1;
101 7         32 my $one = $poly->coeff_one;
102 7         74 my $nul = $poly->coeff_zero;
103              
104 7         30 my $E = $e | 1;
105 7         20 my @q = ($one);
106 7         12 my $cos = $one;
107 7         15 my %MP = ();
108 7         27 for (my $k = 2; $k <= $E; ++$k) {
109 30 100       2118 if ($k & 1) {
110 15         94 push @q, $nul + kronecker($n, $k);
111 15         2604 next;
112             }
113 15 100 100     45 if ($c2 && $k & 2) {
114 2         3 push @q, $nul;
115 2         5 next;
116             }
117 13 100       39 $cos = -$cos if !$c1; # $cos is cos((n-1)*k*pi/4)
118 13         295 my $gk = gcd($n1, $k);
119 13   66     114 my $mp = $MP{$gk} ||= moebius($n1 / $gk) * euler_phi($gk);
120 13         85 push @q, $cos * $mp;
121             }
122              
123 7         18 my @cs = ($one);
124 7         16 my @ds = ($one);
125 7         20 $cs[$e] = $ds[$e-1] = $one;
126 7         22 for(my $k = 1, my $kk = $e - 1; $k <= $kk; ++$k, --$kk) {
127 15         2659 my ($c, $d) = ($nul, $nul);
128 15         40 for (my $i = 0, my $j = $k-1; $j >= 0; $i += 2, --$j) {
129 37         5422 my $cc = $cs[$j];
130 37         57 my $dd = $ds[$j];
131 37         78 my ($q1, $q2) = @q[$i, $i+1];
132 37         72 $c += $q1 * $n * $dd - $q2 * $cc;
133 37         14293 $d += $q[$i+2] * $cc - $q2 * $dd;
134             }
135 15         3603 $c /= ($k + $k);
136 15         3337 $cs[$k] = $cs[$kk] = $c;
137 15 100       64 $ds[$k] = $ds[$kk-1] = ($d + $c) / ($k + $k + 1) if $k < $kk;
138             }
139              
140 7         42 my $cp = $poly->new(@cs);
141 7         391 my $dp = $poly->new(@ds);
142 7         249 $table->{"$n1 $n"} = [$cp, $dp];
143 7         70 return ($cp, $dp);
144             }
145              
146             # generate polynomials C, D, satisfying the identity of Beeger and Schinzel:
147             # for k > 1, square-free, k1 = k (if k === 1 (mod 4)), k1 = 2*k (else),
148             # n = odd multiple of k1: Phi_n(x) = C^2 - k*x*D^2
149             sub _schinzel_cd {
150 25     25   82 my ($poly, $table, $n, $k) = @_;
151 25 100       124 my $K = ($k & 3) == 1? $k: $k << 1;
152 25         69 my $f = $n / $K;
153 25 100       71 return () if not $f & 1;
154 23         144 my $r = vecprod(map { $_->[0] } factor_exp($f));
  10         47  
155 23         112 my $m = $f / $r * gcd($f, $k);
156 23 100       62 if ($m > 1) {
157 3         6 $n /= $m;
158 3         5 $f /= $m;
159             }
160 23         63 my ($C, $D) = ();
161 23 100       123 if (exists $table->{"$n $k"}) {
162 17         35 ($C, $D) = @{$table->{"$n $k"}};
  17         77  
163             }
164             else {
165 6         25 ($C, $D) = _lucas_cd($poly, $table, $k);
166 6         16 my ($L, $M, $Q) = ();
167 6         28 foreach my $p (factor($f)) {
168 6         8632 my $p2 = $p >> 1;
169 6         28 my $kp2 = Math::BigInt->new($k)->bpow($p2);
170 6 100       1947 if (!defined $M) {
171 5         24 $Q = $poly->monomial(2, $k);
172 5         733 my $CC = $C->nest($Q);
173 5         17113 my $DD = $D->nest($Q)->shift_up(1)->mul_const($k);
174 5         13709 $L = $CC - $DD;
175 5         2262 $M = $CC + $DD;
176             }
177 6 100       2008 if (kronecker($k, $p) < 0) {
178 5         22 ($L, $M) = (
179             $L->nest($poly->monomial($p, $kp2)) / $M,
180             $M->nest($poly->monomial($p, $kp2)) / $L,
181             );
182             }
183             else {
184 1         7 ($L, $M) = (
185             $M->nest($poly->monomial($p, $kp2)) / $M,
186             $L->nest($poly->monomial($p, $kp2)) / $L,
187             );
188             }
189             }
190 6 100       379886 if (defined $M) {
191 5         27 $C = ($M + $L)->div_const(2)->unnest($Q);
192 5         126731 $D = (($M - $L) / $poly->monomial(1, $k << 1))->unnest($Q);
193 5         110447 $table->{"$n $k"} = [$C, $D];
194             }
195             }
196 23 100       80 if ($m > 1) {
197 3         15 $C = $C->inflate($m);
198 3         242 $D = $D->inflate($m)->shift_up($m >> 1);
199             }
200 23         353 return ($C, $D);
201             }
202              
203             # for r > 1, integer, square-free, s > 0, integer, x = r*s^2,
204             # r1 = r (if r === 1 (mod 4)), r1 = 2*r (else), n = odd multiple of r1,
205             # generate integer factors l < m such that l * m = Phi_n(x).
206             # if l = 1, return (m) else (l, m).
207             sub _schinzel_lm {
208 16     16   89 my ($poly, $table, $n, $r, $s, $x) = @_;
209 16         114 my ($c, $d) = _schinzel_cd($poly, $table, $n, $r);
210 16         55 my $cx = $c->evaluate($x);
211 16         16578 my $dx = $d->evaluate($x) * $r * $s;
212 16         15936 my ($l, $m) = ($cx - $dx, $cx + $dx);
213 16 100       3001 return $m if $l == $poly->coeff_one;
214 14         791 return ($l, $m);
215             }
216              
217             # ----- Math::Polynomial extension -----
218              
219             sub Math::Polynomial::cyclotomic {
220 12     12 0 5506 my ($this, $n, $table) = @_;
221 12   100     56 return _cyclo_poly($this, $table || {}, $n);
222             }
223              
224             sub Math::Polynomial::cyclo_factors {
225 6     6 0 5964 my ($this, $n, $table) = @_;
226 6         56 my @d = divisors($n);
227 6   100     29 $table ||= {};
228 6         16 return map { _cyclo_poly($this, $table, $_, \@d) } @d;
  28         58  
229             }
230              
231             sub Math::Polynomial::cyclo_int_factors {
232 13     13 0 43 my ($this, $x, $n, $table) = @_;
233 13 100 100     73 return ($this->coeff_zero) if !$n || $x == $this->coeff_one;
234 11 100 100     1510 return ($x - $this->coeff_one) if $n == 1 || !$x;
235 9   50     34 $table ||= {};
236 9 100       95 if (my $exp = is_power($x, 0, \my $root)) {
237 2         5 $x = $root;
238 2         5 $n *= $exp;
239             }
240 9         37 my ($r, $s) = _square_free_square($x);
241 9 100       35 my $r1 = (1 == ($r & 3))? $r: $r+$r;
242 9         68 my @d = divisors($n);
243 9   100     43 my $i = $x == 2 || 0;
244             return
245             map {
246 9 100 100     48 !($_ % $r1) && ($_ / $r1) & 1?
  45         19091  
247             _schinzel_lm($this, $table, $_, $r, $s, $x):
248             (_cyclo_poly($this, $table, $_, \@d))->evaluate($x)
249             } @d[$i .. $#d];
250             }
251              
252             sub Math::Polynomial::cyclo_plusfactors {
253 4     4 0 549 my ($this, $n, $table) = @_;
254 4         27 my @d = divisors($n << 1);
255 4         10 my $m = $n ^ ($n - 1);
256 4   100     18 $table ||= {};
257             return
258 8         18 map { _cyclo_poly($this, $table, $_, \@d) }
259 4         10 grep { !($_ & $m) } @d;
  20         36  
260             }
261              
262             sub Math::Polynomial::cyclo_int_plusfactors {
263 11     11 0 30 my ($this, $x, $n, $table) = @_;
264 11         58 my $u = $this->coeff_one;
265 11 100       58 return ($u + $u) if !$n;
266 10 100 100     82 return ($x + $u) if $n == 1 || !$x || $x == $u;
      100        
267 7 100       938 if (my $exp = is_power($x, 0, \my $root)) {
268 2         7 $x = $root;
269 2         6 $n *= $exp;
270             }
271 7         27 my ($r, $s) = _square_free_square($x);
272 7 100       31 my $r1 = (1 == ($r & 3))? $r: $r+$r;
273 7         55 my @d = divisors($n << 1);
274 7         21 my $m = $n ^ ($n - 1);
275 7   50     24 $table ||= {};
276             return
277             map {
278 23 100 100     10461 !($_ % $r1) && ($_ / $r1) & 1?
279             _schinzel_lm($this, $table, $_, $r, $s, $x):
280             (_cyclo_poly($this, $table, $_, \@d))->evaluate($x)
281 7         16 } grep { !($_ & $m) } @d;
  58         100  
282             }
283              
284             sub Math::Polynomial::cyclo_lucas_cd {
285 6     6 0 18 my ($this, $n, $table) = @_;
286 6 100 100     73 if ($n <= 1 || !is_square_free($n)) {
287 2         280 croak "$n: not a square-free integer greater than one";
288             }
289 4   50     21 return _lucas_cd($this, $table || {}, $n);
290             }
291              
292             sub Math::Polynomial::cyclo_schinzel_cd {
293 11     11 0 31 my ($this, $n, $k, $table) = @_;
294 11 100 100     96 if ($k <= 1 || !is_square_free($k)) {
295 2         190 croak "$k: not a square-free integer greater than one";
296             }
297 9   100     46 my @cd = _schinzel_cd($this, $table || {}, $n, $k);
298 9 100       32 if (!@cd) {
299 2 100       6 my $k1 = ($k & 3) == 1? 'k': '2*k';
300 2         177 croak "$n: n is not an odd multiple of $k1";
301             }
302 7         37 return @cd;
303             }
304              
305             sub Math::Polynomial::cyclo_poly_iterate {
306 4     4 0 1733 my ($this, $n, $table) = @_;
307 4   100     19 $n ||= 1;
308 4         6 --$n;
309 4   100     17 $table ||= {};
310             return
311             sub {
312 14     14   7489 ++$n;
313 14         34 _cyclo_poly($this, $table, $n);
314 4         45 };
315             }
316              
317             # ----- public subroutines -----
318              
319 9     9 1 6429 sub cyclo_poly { $default->cyclotomic(@_) }
320 2     2 1 1952 sub cyclo_factors { $default->cyclo_factors(@_) }
321 3     3 1 3129 sub cyclo_plusfactors { $default->cyclo_plusfactors(@_) }
322 2     2 1 1268 sub cyclo_poly_iterate { $default->cyclo_poly_iterate(@_) }
323 6     6 1 8917 sub cyclo_lucas_cd { $default->cyclo_lucas_cd(@_) }
324 11     11 1 11215 sub cyclo_schinzel_cd { $default->cyclo_schinzel_cd(@_) }
325 13     13 1 10032 sub cyclo_int_factors { $default->cyclo_int_factors(@_) }
326 11     11 1 15406 sub cyclo_int_plusfactors { $default->cyclo_int_plusfactors(@_) }
327              
328             1;
329              
330             __END__