File Coverage

blib/lib/Math/Polynomial/Multivariate.pm
Criterion Covered Total %
statement 311 311 100.0
branch 70 70 100.0
condition 15 15 100.0
subroutine 49 49 100.0
pod 20 20 100.0
total 465 465 100.0


line stmt bran cond sub pod time code
1             # Copyright (c) 2011-2017 Martin Becker. All rights reserved.
2             # This package is free software; you can redistribute it and/or modify it
3             # under the same terms as Perl itself.
4              
5             package Math::Polynomial::Multivariate;
6              
7 5     5   459343 use 5.008;
  5         43  
8 5     5   25 use strict;
  5         11  
  5         97  
9 5     5   21 use warnings;
  5         10  
  5         126  
10 5     5   24 use Carp qw(croak);
  5         9  
  5         532  
11              
12             use overload (
13 5         47 q[neg] => \&_neg,
14             q[+] => \&_add,
15             q[-] => \&_sub,
16             q[*] => \&_mul,
17             q[**] => \&_pow,
18             q[==] => \&_eq,
19             q[!=] => \&_ne,
20             q[!] => 'is_null',
21             q[bool] => 'is_not_null',
22             q[""] => 'as_string',
23             q[fallback] => undef,
24 5     5   2395 );
  5         2317  
25              
26             # ----- object definition -----
27              
28             # Math::Polynomial::Multivariate=ARRAY(...)
29              
30             # .......... index .......... # .......... value ..........
31 5     5   789 use constant F_TERMS => 0; # monomial terms hashref
  5         10  
  5         331  
32 5     5   27 use constant F_ZERO => 1; # zero element of coefficient space
  5         6  
  5         199  
33 5     5   27 use constant F_ONE => 2; # unit element of coefficient space
  5         10  
  5         298  
34 5     5   33 use constant NFIELDS => 3;
  5         10  
  5         225  
35              
36             # a term value is an anonymous arrayref:
37             # .......... index .......... # .......... value ..........
38 5     5   37 use constant T_COEFF => 0; # coefficient
  5         8  
  5         190  
39 5     5   22 use constant T_VARS => 1; # variables hashref, mapping names to exponents
  5         9  
  5         199  
40              
41 5     5   25 use constant K_SEP => chr 0;
  5         7  
  5         262  
42 5     5   24 use constant K_SEP_RE => qr/\0/;
  5         15  
  5         281  
43              
44 5     5   24 use constant _MINUS_INFINITY => - (~0) ** (~0);
  5         8  
  5         12794  
45              
46             # ----- class data -----
47              
48             our $VERSION = '0.006';
49              
50             # ----- private subroutines -----
51              
52             # create a "clean" copy of a variables hashref, without zero exponents
53             sub _vclean {
54 14     14   25 my %vars = %{$_[0]};
  14         53  
55 14         42 delete @vars{grep {!$vars{$_}} keys %vars};
  18         59  
56             die "hash of integer exponents expected"
57 14 100       37 if grep {!eval { $_ == int abs $_ }} values %vars;
  13         24  
  13         58  
58 13         35 return \%vars;
59             }
60              
61             # map a clean variables hashref to a key
62             sub _key {
63 117     117   228 my ($vars) = @_;
64             return
65             join K_SEP,
66 98         448 map { ($_, chr $vars->{$_}) }
67 117         172 reverse sort keys %{$vars};
  117         386  
68             }
69              
70             # map an arbitrary variables hashref to a key
71             sub _keyvc {
72 8     8   19 my ($vars) = @_;
73             return
74             join K_SEP,
75 6         35 map { ($_, chr $vars->{$_}) }
76             reverse sort
77 6         23 grep { $vars->{$_} }
78 8         16 keys %{$vars};
  8         32  
79             }
80              
81             # check if an arbitrary operand is a multivariate polynomial
82              
83 104 100   104   316 sub _isa_pol ($) { ref($_[0]) && eval { $_[0]->isa(__PACKAGE__) } }
  95         460  
84              
85             # ----- private methods -----
86              
87             # extract coefficient space information from an invocant and some coefficient
88             sub _space {
89 124     124   236 my ($this, $const) = @_;
90 124         232 my $class = ref $this;
91 124         194 my ($zero, $one);
92 124 100       258 if ($class) {
93 113         162 ($zero, $one) = @{$this}[F_ZERO, F_ONE];
  113         343  
94             }
95             else {
96 11         20 $class = $this;
97 11         28 $zero = $const - $const;
98 11         566 $one = $const ** 0;
99             }
100 124         2237 return ($class, $zero, $one);
101             }
102              
103             # create an independent duplicate of an object
104             sub _clone {
105 51     51   90 my ($this) = @_;
106 51         87 my $terms = $this->[F_TERMS];
107             return
108             bless [
109 61         88 { map {($_ => [@{$terms->{$_}}])} keys %{$terms} },
  61         190  
  51         154  
110 51         72 @{$this}[F_ZERO, F_ONE]
  51         179  
111             ], ref $this;
112             }
113              
114             # multiply a polynomial by variables with exponents
115             sub _mul_vars {
116 3     3   6 my ($this, $vars) = @_;
117             return
118             bless [
119             {
120             map {
121 3         5 my %tvars = %{$_->[T_VARS]};
  3         6  
122 3         5 while (my ($var, $exp) = each %{$vars}) {
  5         12  
123 2         6 $tvars{$var} += $exp;
124             }
125 3         7 _key(\%tvars) => [$_->[T_COEFF], \%tvars]
126 3         6 } values %{$this->[F_TERMS]}
127             },
128 3         4 @{$this}[F_ZERO, F_ONE]
  3         10  
129             ], ref $this;
130             }
131              
132             # overloaded operators
133              
134             sub _neg {
135 1     1   5 my ($this) = @_;
136 1         2 my $result = $this->_clone;
137 1         2 foreach my $term (values %{$result->[F_TERMS]}) {
  1         3  
138 1         2 $term->[T_COEFF] = -$term->[T_COEFF];
139             }
140 1         3 return $result;
141             }
142              
143             sub _add {
144 31     31   1437 my ($this, $that) = @_;
145 31 100       62 $that = $this->const($that) if !_isa_pol($that);
146 31         91 my $result = $this->_clone;
147 31         61 my $rterms = $result->[F_TERMS];
148 31         55 my $zero = $this->[F_ZERO];
149 31         45 while (my ($key, $term) = each %{$that->[F_TERMS]}) {
  64         227  
150 33 100       72 if (exists $rterms->{$key}) {
151 7 100       21 if ($zero == ($rterms->{$key}->[T_COEFF] += $term->[T_COEFF])) {
152 1         5 delete $rterms->{$key};
153             }
154             }
155             else {
156 26         37 $rterms->{$key} = [@{$term}];
  26         78  
157             }
158             }
159 31         123 return $result;
160             }
161              
162             sub _sub {
163 14     14   43 my ($this, $that, $swap) = @_;
164 14 100       35 $that = $this->const($that) if !_isa_pol($that);
165 14 100       43 ($this, $that) = ($that, $this) if $swap;
166 14         35 my $result = $this->_clone;
167 14         27 my $rterms = $result->[F_TERMS];
168 14         27 my $zero = $this->[F_ZERO];
169 14         23 while (my ($key, $term) = each %{$that->[F_TERMS]}) {
  30         241  
170 16 100       35 if (exists $rterms->{$key}) {
171 7 100       28 if ($zero == ($rterms->{$key}->[T_COEFF] -= $term->[T_COEFF])) {
172 6         559 delete $rterms->{$key};
173             }
174             }
175             else {
176 9         17 my $rterm = $rterms->{$key} = [@{$term}];
  9         24  
177 9         28 $rterm->[T_COEFF] = -$rterm->[T_COEFF];
178             }
179             }
180 14         55 return $result;
181             }
182              
183             sub _mul {
184 40     40   852 my ($this, $that) = @_;
185 40 100       91 $that = $this->const($that) if !_isa_pol($that);
186 40         106 my $result = $this->null;
187 40         75 my $bterms = $that->[F_TERMS];
188 40         65 my $rterms = $result->[F_TERMS];
189 40         62 my $zero = $this->[F_ZERO];
190 40         61 foreach my $aterm (values %{$this->[F_TERMS]}) {
  40         115  
191 53         83 my @avars = %{$aterm->[T_VARS]};
  53         129  
192 53         87 foreach my $bterm (values %{$bterms}) {
  53         115  
193 61         125 my %vars = @avars;
194 61         137 my $const = $aterm->[T_COEFF] * $bterm->[T_COEFF];
195 61         4008 my $bvars = $bterm->[T_VARS];
196 61         93 foreach my $bv (keys %{$bvars}) {
  61         131  
197 36         77 $vars{$bv} += $bvars->{$bv};
198             }
199 61         136 my $key = _key(\%vars);
200 61 100       153 if (exists $rterms->{$key}) {
201 4         21 $rterms->{$key}->[T_COEFF] += $const;
202             }
203             else {
204 57         192 $rterms->{$key} = [$const, \%vars];
205             }
206             }
207             }
208 40         273 delete @{$rterms}{
209 40         376 grep {$zero == $rterms->{$_}->[T_COEFF]} keys %{$rterms}
  57         267  
  40         105  
210             };
211 40         174 return $result;
212             }
213              
214             sub _pow {
215 8     8   887 my ($this, $exp, $swap) = @_;
216 8 100 100     190 croak 'illegal exponent' if $swap || $exp != int abs $exp;
217 6 100       14 return $this->const($this->[F_ONE]) if !$exp;
218 5         13 my $result = $this->_clone;
219 5         13 while (--$exp > 0) {
220 6         14 $result *= $this;
221             }
222 5         16 return $result;
223             }
224              
225             sub _eq_ne {
226 17     17   38 my ($this, $that, $eq) = @_;
227 17 100       35 $that = $this->const($that) if !_isa_pol($that);
228 17         36 my $aterms = $this->[F_TERMS];
229 17         29 my $bterms = $that->[F_TERMS];
230 17 100       35 return !$eq if keys(%{$aterms}) != keys(%{$bterms});
  17         34  
  17         50  
231 13         23 while (my ($key, $term) = each %{$aterms}) {
  21         186  
232             return !$eq
233             if !exists($bterms->{$key}) ||
234 15 100 100     86 $term->[T_COEFF] != $bterms->{$key}->[T_COEFF];
235             }
236 6         25 return $eq;
237             }
238              
239             sub _eq {
240 10     10   1118 my ($this, $that) = @_;
241 10         28 return $this->_eq_ne($that, !0);
242             }
243              
244             sub _ne {
245 7     7   1601 my ($this, $that) = @_;
246 7         19 return $this->_eq_ne($that, !1);
247             }
248              
249             # ----- public methods -----
250              
251             # constructors
252              
253             sub null {
254 75     75 1 154 my ($this) = @_;
255 75         148 my ($class, $zero, $one) = $this->_space(1);
256 75         225 return bless [{}, $zero, $one], $class;
257             }
258              
259             sub const {
260 27     27 1 4574 my ($this, $value) = @_;
261 27         79 my ($class, $zero, $one) = $this->_space($value);
262 27 100       129 my %terms = $zero == $value? (): (q[] => [$value, {}]);
263 27         1690 return bless [\%terms, $zero, $one], $class;
264             }
265              
266             sub var {
267 8     8 1 40 my ($this, $varname) = @_;
268 8         25 my ($class, $zero, $one) = $this->_space(1);
269 8         24 my $vars = { $varname => 1 };
270 8         23 my $key = _key($vars);
271 8         28 my %terms = ($key => [$one, $vars]);
272 8         35 return bless [\%terms, $zero, $one], $class;
273             }
274              
275             sub monomial {
276 14     14 1 47 my ($this, $const, $vars) = @_;
277 14         40 my ($class, $zero, $one) = $this->_space($const);
278 14         43 $vars = _vclean($vars);
279 13 100       48 my %terms = $zero == $const? (): (_key($vars) => [$const, $vars]);
280 13         61 return bless[\%terms, $zero, $one], $class;
281             }
282              
283             sub subst {
284 8     8 1 1378 my ($this, $varname, $that) = @_;
285 8         19 my @exps = (-1, $this->exponents_of($varname));
286 8 100       26 return $this->null if 1 == @exps;
287 7         14 my $exp = pop @exps;
288 7         19 my $result = $this->factor_of($varname, $exp);
289 7         13 my $nexp = pop @exps;
290 7         18 while ($exp > 0) {
291 13         34 $result *= $that;
292 13 100       46 if (--$exp == $nexp) {
293 6         16 $result += $this->factor_of($varname, $exp);
294 6         21 $nexp = pop @exps;
295             }
296             }
297 7         25 return $result;
298             }
299              
300             sub partial_derivative {
301 3     3 1 926 my ($this, $varname, $cast) = @_;
302 3         12 my $result = $this->null;
303 3         14 foreach my $exp ($this->exponents_of($varname)) {
304 9 100       30 next if !$exp;
305 6 100       22 my $const = $cast? $cast->($exp): $exp;
306 6         29 $result +=
307             $this->factor_of($varname, $exp) *
308             $this->monomial($const, { $varname => $exp-1 });
309             }
310 3         14 return $result;
311             }
312              
313             sub flatten {
314 1     1 1 3 my ($this) = @_;
315 1         4 while (_isa_pol(my $aux = $this->[F_ZERO])) {
316 1         2 foreach my $term (values %{$this->[F_TERMS]}) {
  1         6  
317 3         7 $aux += $term->[T_COEFF]->_mul_vars($term->[T_VARS]);
318             }
319 1         3 $this = $aux;
320             }
321 1         3 return $this;
322             }
323              
324             sub extract {
325 1     1 1 4 my ($this, $var) = @_;
326             my %terms =
327             map {
328 1 100       4 my $vars = { $_? ($var => $_): () };
  3         10  
329 3         7 _key($vars) => [$this->factor_of($var, $_), $vars]
330             } $this->exponents_of($var);
331             return
332 1         4 bless [
333             \%terms,
334             $this->null,
335             $this->const($this->[F_ONE])
336             ], ref $this;
337             }
338              
339             # inspection methods
340              
341 2     2 1 5 sub is_null { !keys %{$_[0]->[F_TERMS]} }
  2         13  
342 2     2 1 5 sub is_not_null { !!keys %{$_[0]->[F_TERMS]} }
  2         10  
343              
344             sub variables {
345 9     9 1 1894 my ($this) = @_;
346 9         18 my %vars = map {%{$_->[T_VARS]}} values %{$this->[F_TERMS]};
  22         31  
  22         75  
  9         34  
347 9         54 return sort keys %vars;
348             }
349              
350             sub exponents_of {
351 14     14 1 937 my ($this, $varname) = @_;
352 14         31 my $terms = $this->[F_TERMS];
353 14         24 my %exps = ();
354 14         24 foreach my $term (values %{$terms}) {
  14         42  
355 36   100     130 $exps{$term->[T_VARS]->{$varname} || 0} = undef;
356             }
357 14         66 return sort { $a <=> $b } keys %exps;
  24         73  
358             }
359              
360             sub factor_of {
361 26     26 1 868 my ($this, $varname, $exp) = @_;
362 26         62 my $terms = $this->[F_TERMS];
363 26         56 my $result = $this->null;
364 26         51 my $rterms = $result->[F_TERMS];
365 26         37 foreach my $term (values %{$terms}) {
  26         57  
366 77   100     227 my $aexp = $term->[T_VARS]->{$varname} || 0;
367 77 100       170 if ($aexp == $exp) {
368 30         46 my %vars = %{$term->[T_VARS]};
  30         94  
369 30         66 delete $vars{$varname};
370 30         87 $rterms->{_key(\%vars)} = [$term->[T_COEFF], \%vars];
371             }
372             }
373 26         83 return $result;
374             }
375              
376             sub coefficient {
377 8     8 1 3461 my ($this, $vars) = @_;
378 8         25 my $key = _keyvc($vars);
379 8         22 my $terms = $this->[F_TERMS];
380 8 100       41 return exists($terms->{$key})? $terms->{$key}->[T_COEFF]: $this->[F_ZERO];
381             }
382              
383             sub degree {
384 5     5 1 464 my ($this) = @_;
385 5         12 my $terms = $this->[F_TERMS];
386 5 100       10 return _MINUS_INFINITY if !keys %{$terms};
  5         29  
387 3         10 my $max_degree = 0;
388 3         7 foreach my $term (values %{$terms}) {
  3         11  
389 9         15 my $degree = 0;
390 9         14 foreach my $e (values %{$term->[T_VARS]}) {
  9         20  
391 9         18 $degree += $e;
392             }
393 9 100       24 if ($max_degree < $degree) {
394 5         19 $max_degree = $degree;
395             }
396             }
397 3         13 return $max_degree;
398             }
399              
400             sub multidegree {
401 4     4 1 963 my ($this) = @_;
402 4         35 my $terms = $this->[F_TERMS];
403 4         10 my %result = ();
404 4         6 foreach my $term (values %{$terms}) {
  4         12  
405 4         8 while (my ($name, $exp) = each %{$term->[T_VARS]}) {
  10         40  
406 6 100 100     28 if (($result{$name} || 0) < $exp) {
407 5         13 $result{$name} = $exp;
408             }
409             }
410             }
411 4         14 return \%result;
412             }
413              
414 2     2 1 4 sub number_of_terms { scalar keys %{$_[0]->[F_TERMS]} }
  2         10  
415              
416             sub evaluate {
417 5     5 1 3122 my ($this, $values) = @_;
418 5         16 my @vars = $this->variables;
419 5 100       14 if (my @miss = grep { !exists $values->{$_} } @vars) {
  8         32  
420 2 100       6 my $s = 1 == @miss? q[]: 's';
421 2         209 croak "missing variable$s: @miss";
422             }
423 3         7 my $result = $this;
424 3         10 foreach my $varname (@vars) {
425 4         21 $result = $result->subst($varname, $this->const($values->{$varname}));
426             }
427 3         9 return $result->coefficient({});
428             }
429              
430             sub as_monomials {
431 43     43 1 1709 my ($this) = @_;
432 43         76 my $terms = $this->[F_TERMS];
433 43 100       107 return scalar keys %{$terms} if !wantarray;
  1         7  
434 42 100       53 return ([$this->[F_ZERO], {}]) if !keys %{$terms};
  42         154  
435             return
436             map {
437 56         88 my ($coeff, $vars) = @{$terms->{$_}};
  56         117  
438 56         97 [$coeff, {%{$vars}}]
  56         242  
439 33         59 } sort keys %{$terms};
  33         108  
440             }
441              
442             sub as_string {
443 41     41 1 8295 my ($this) = @_;
444 41         84 my $one = $this->[F_ONE];
445             return
446             sprintf '(%s)',
447             join q[ + ],
448             map {
449 41         100 my ($const, $vars) = @{$_};
  62         194  
  62         129  
450             join q[*],
451             keys(%{$vars}) && $one == $const? (): $const,
452             map {
453 44         82 my $exp = $vars->{$_};
454 44 100       321 1 == $exp? $_: "$_^$exp"
455             }
456 62 100 100     104 sort keys %{$vars}
  62         413  
457             }
458             $this->as_monomials;
459             }
460              
461             1;
462             __END__