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