1
2=encoding utf8
3
4=head1 NAME
5
6Math::Symbolic::VectorCalculus - Symbolically comp. grad, Jacobi matrices etc.
7
8=head1 SYNOPSIS
9
10  use Math::Symbolic qw/:all/;
11  use Math::Symbolic::VectorCalculus; # not loaded by Math::Symbolic
12
13  @gradient = grad 'x+y*z';
14  # or:
15  $function = parse_from_string('a*b^c');
16  @gradient = grad $function;
17  # or:
18  @signature = qw(x y z);
19  @gradient = grad 'a*x+b*y+c*z', @signature; # Gradient only for x, y, z
20  # or:
21  @gradient = grad $function, @signature;
22
23  # Similar syntax variations as with the gradient:
24  $divergence = div @functions;
25  $divergence = div @functions, @signature;
26
27  # Again, similar DWIM syntax variations as with grad:
28  @rotation = rot @functions;
29  @rotation = rot @functions, @signature;
30
31  # Signatures always inferred from the functions here:
32  @matrix = Jacobi @functions;
33  # $matrix is now array of array references. These hold
34  # Math::Symbolic trees. Or:
35  @matrix = Jacobi @functions, @signature;
36
37  # Similar to Jacobi:
38  @matrix = Hesse $function;
39  # or:
40  @matrix = Hesse $function, @signature;
41
42  $wronsky_determinant = WronskyDet @functions, @vars;
43  # or:
44  $wronsky_determinant = WronskyDet @functions; # functions of 1 variable
45
46  $differential = TotalDifferential $function;
47  $differential = TotalDifferential $function, @signature;
48  $differential = TotalDifferential $function, @signature, @point;
49
50  $dir_deriv = DirectionalDerivative $function, @vector;
51  $dir_deriv = DirectionalDerivative $function, @vector, @signature;
52
53  $taylor = TaylorPolyTwoDim $function, $var1, $var2, $degree;
54  $taylor = TaylorPolyTwoDim $function, $var1, $var2,
55                             $degree, $var1_0, $var2_0;
56  # example:
57  $taylor = TaylorPolyTwoDim 'sin(x)*cos(y)', 'x', 'y', 2;
58
59=head1 DESCRIPTION
60
61This module provides several subroutines related to
62vector calculus such as computing gradients, divergence, rotation,
63and Jacobi/Hesse Matrices of Math::Symbolic trees.
64Furthermore it provides means of computing directional derivatives
65and the total differential of a scalar function and the
66Wronsky Determinant of a set of n scalar functions.
67
68Please note that the code herein may or may not be refactored into
69the OO-interface of the Math::Symbolic module in the future.
70
71=head2 EXPORT
72
73None by default.
74
75You may choose to have any of the following routines exported to the
76calling namespace. ':all' tag exports all of the following:
77
78  grad
79  div
80  rot
81  Jacobi
82  Hesse
83  WronskyDet
84  TotalDifferential
85  DirectionalDerivative
86  TaylorPolyTwoDim
87
88=head1 SUBROUTINES
89
90=cut
91
92package Math::Symbolic::VectorCalculus;
93
94use 5.006;
95use strict;
96use warnings;
97
98use Carp;
99
100use Math::Symbolic qw/:all/;
101use Math::Symbolic::MiscAlgebra qw/det/;
102
103require Exporter;
104our @ISA         = qw(Exporter);
105our %EXPORT_TAGS = (
106    'all' => [
107        qw(
108          grad
109          div
110          rot
111          Jacobi
112          Hesse
113          TotalDifferential
114          DirectionalDerivative
115          TaylorPolyTwoDim
116          WronskyDet
117          )
118    ]
119);
120
121our @EXPORT_OK = ( @{ $EXPORT_TAGS{'all'} } );
122
123our $VERSION = '0.612';
124
125=begin comment
126
127_combined_signature returns the combined signature of unique variable names
128of all Math::Symbolic trees passed to it.
129
130=end comment
131
132=cut
133
134sub _combined_signature {
135    my %seen = map { ( $_, undef ) } map { ( $_->signature() ) } @_;
136    return [ sort keys %seen ];
137}
138
139=head2 grad
140
141This subroutine computes the gradient of a Math::Symbolic tree representing
142a function.
143
144The gradient of a function f(x1, x2, ..., xn) is defined as the vector:
145
146  ( df(x1, x2, ..., xn) / d(x1),
147    df(x1, x2, ..., xn) / d(x2),
148    ...,
149    df(x1, x2, ..., xn) / d(xn) )
150
151(These are all partial derivatives.) Any good book on calculus will have
152more details on this.
153
154grad uses prototypes to allow for a variety of usages. In its most basic form,
155it accepts only one argument which may either be a Math::Symbolic tree or a
156string both of which will be interpreted as the function to compute the
157gradient for. Optionally, you may specify a second argument which must
158be a (literal) array of Math::Symbolic::Variable objects or valid
159Math::Symbolic variable names (strings). These variables will the be used for
160the gradient instead of the x1, ..., xn inferred from the function signature.
161
162=cut
163
164sub grad ($;\@) {
165    my $original = shift;
166    $original = parse_from_string($original)
167      unless ref($original) =~ /^Math::Symbolic/;
168    my $signature = shift;
169
170    my @funcs;
171    my @signature =
172      ( defined $signature ? @$signature : $original->signature() );
173
174    foreach (@signature) {
175        my $var  = Math::Symbolic::Variable->new($_);
176        my $func = Math::Symbolic::Operator->new(
177            {
178                type     => U_P_DERIVATIVE,
179                operands => [ $original->new(), $var ],
180            }
181        );
182        push @funcs, $func;
183    }
184    return @funcs;
185}
186
187=head2 div
188
189This subroutine computes the divergence of a set of Math::Symbolic trees
190representing a vectorial function.
191
192The divergence of a vectorial function
193F = (f1(x1, ..., xn), ..., fn(x1, ..., xn)) is defined like follows:
194
195  sum_from_i=1_to_n( dfi(x1, ..., xn) / dxi )
196
197That is, the sum of all partial derivatives of the i-th component function
198to the i-th coordinate. See your favourite book on calculus for details.
199Obviously, it is important to keep in mind that the number of function
200components must be equal to the number of variables/coordinates.
201
202Similar to grad, div uses prototypes to offer a comfortable interface.
203First argument must be a (literal) array of strings and Math::Symbolic trees
204which represent the vectorial function's components. If no second argument
205is passed, the variables used for computing the divergence will be
206inferred from the functions. That means the function signatures will be
207joined to form a signature for the vectorial function.
208
209If the optional second argument is specified, it has to be a (literal)
210array of Math::Symbolic::Variable objects and valid variable names (strings).
211These will then be interpreted as the list of variables for computing the
212divergence.
213
214=cut
215
216sub div (\@;\@) {
217    my @originals =
218      map { ( ref($_) =~ /^Math::Symbolic/ ) ? $_ : parse_from_string($_) }
219      @{ +shift };
220
221    my $signature = shift;
222    $signature = _combined_signature(@originals)
223      if not defined $signature;
224
225    if ( @$signature != @originals ) {
226        die "Variable count does not function count for divergence.";
227    }
228
229    my @signature = map { Math::Symbolic::Variable->new($_) } @$signature;
230
231    my $div = Math::Symbolic::Operator->new(
232        {
233            type     => U_P_DERIVATIVE,
234            operands => [ shift(@originals)->new(), shift @signature ],
235        }
236    );
237
238    foreach (@originals) {
239        $div = Math::Symbolic::Operator->new(
240            '+', $div,
241            Math::Symbolic::Operator->new(
242                {
243                    type     => U_P_DERIVATIVE,
244                    operands => [ $_->new(), shift @signature ],
245                }
246            )
247        );
248    }
249    return $div;
250}
251
252=head2 rot
253
254This subroutine computes the rotation of a set of three Math::Symbolic trees
255representing a vectorial function.
256
257The rotation of a vectorial function
258F = (f1(x1, x2, x3), f2(x1, x2, x3), f3(x1, x2, x3)) is defined as the
259following vector:
260
261  ( ( df3/dx2 - df2/dx3 ),
262    ( df1/dx3 - df3/dx1 ),
263    ( df2/dx1 - df1/dx2 ) )
264
265Or "nabla x F" for short. Again, I have to refer to the literature for
266the details on what rotation is. Please note that there have to be
267exactly three function components and three coordinates because the cross
268product and hence rotation is only defined in three dimensions.
269
270As with the previously introduced subroutines div and grad, rot
271offers a prototyped interface.
272First argument must be a (literal) array of strings and Math::Symbolic trees
273which represent the vectorial function's components. If no second argument
274is passed, the variables used for computing the rotation will be
275inferred from the functions. That means the function signatures will be
276joined to form a signature for the vectorial function.
277
278If the optional second argument is specified, it has to be a (literal)
279array of Math::Symbolic::Variable objects and valid variable names (strings).
280These will then be interpreted as the list of variables for computing the
281rotation. (And please excuse my copying the last two paragraphs from above.)
282
283=cut
284
285sub rot (\@;\@) {
286    my $originals = shift;
287    my @originals =
288      map { ( ref($_) =~ /^Math::Symbolic/ ) ? $_ : parse_from_string($_) }
289      @$originals;
290
291    my $signature = shift;
292    $signature = _combined_signature(@originals)
293      unless defined $signature;
294
295    if ( @originals != 3 ) {
296        die "Rotation only defined for functions of three components.";
297    }
298    if ( @$signature != 3 ) {
299        die "Rotation only defined for three variables.";
300    }
301
302    return (
303        Math::Symbolic::Operator->new(
304            '-',
305            Math::Symbolic::Operator->new(
306                {
307                    type     => U_P_DERIVATIVE,
308                    operands => [ $originals[2]->new(), $signature->[1] ],
309                }
310            ),
311            Math::Symbolic::Operator->new(
312                {
313                    type     => U_P_DERIVATIVE,
314                    operands => [ $originals[1]->new(), $signature->[2] ],
315                }
316            )
317        ),
318        Math::Symbolic::Operator->new(
319            '-',
320            Math::Symbolic::Operator->new(
321                {
322                    type     => U_P_DERIVATIVE,
323                    operands => [ $originals[0]->new(), $signature->[2] ],
324                }
325            ),
326            Math::Symbolic::Operator->new(
327                {
328                    type     => U_P_DERIVATIVE,
329                    operands => [ $originals[2]->new(), $signature->[0] ],
330                }
331            )
332        ),
333        Math::Symbolic::Operator->new(
334            '-',
335            Math::Symbolic::Operator->new(
336                {
337                    type     => U_P_DERIVATIVE,
338                    operands => [ $originals[1]->new(), $signature->[0] ],
339                }
340            ),
341            Math::Symbolic::Operator->new(
342                {
343                    type     => U_P_DERIVATIVE,
344                    operands => [ $originals[0]->new(), $signature->[1] ],
345                }
346            )
347        )
348    );
349}
350
351=head2 Jacobi
352
353Jacobi() returns the Jacobi matrix of a given vectorial function.
354It expects any number of arguments (strings and/or Math::Symbolic trees)
355which will be interpreted as the vectorial function's components.
356Variables used for computing the matrix are, by default, inferred from the
357combined signature of the components. By specifying a second literal
358array of variable names as (second) argument, you may override this
359behaviour.
360
361The Jacobi matrix is the vector of gradient vectors of the vectorial
362function's components.
363
364=cut
365
366sub Jacobi (\@;\@) {
367    my @funcs =
368      map { ( ref($_) =~ /^Math::Symbolic/ ) ? $_ : parse_from_string($_) }
369      @{ +shift() };
370
371    my $signature = shift;
372    my @signature = (
373        defined $signature
374        ? (
375            map {
376                ( ref($_) =~ /^Math::Symbolic/ )
377                  ? $_
378                  : parse_from_string($_)
379              } @$signature
380          )
381        : ( @{ +_combined_signature(@funcs) } )
382    );
383
384    return map { [ grad $_, @signature ] } @funcs;
385}
386
387=head2 Hesse
388
389Hesse() returns the Hesse matrix of a given scalar function. First
390argument must be a string (to be parsed as a Math::Symbolic tree)
391or a Math::Symbolic tree. As with Jacobi(), Hesse() optionally
392accepts an array of signature variables as second argument.
393
394The Hesse matrix is the Jacobi matrix of the gradient of a scalar function.
395
396=cut
397
398sub Hesse ($;\@) {
399    my $function = shift;
400    $function = parse_from_string($function)
401      unless ref($function) =~ /^Math::Symbolic/;
402    my $signature = shift;
403    my @signature = (
404        defined $signature
405        ? (
406            map {
407                ( ref($_) =~ /^Math::Symbolic/ )
408                  ? $_
409                  : parse_from_string($_)
410              } @$signature
411          )
412        : $function->signature()
413    );
414
415    my @gradient = grad $function, @signature;
416    return Jacobi @gradient, @signature;
417}
418
419=head2 TotalDifferential
420
421This function computes the total differential of a scalar function of
422multiple variables in a certain point.
423
424First argument must be the function to derive. The second argument is
425an optional (literal) array of variable names (strings) and
426Math::Symbolic::Variable objects to be used for deriving. If the argument
427is not specified, the functions signature will be used. The third argument
428is also an optional array and denotes the set of variable (names) to use for
429indicating the point for which to evaluate the differential. It must have
430the same number of elements as the second argument.
431If not specified the variable names used as coordinated (the second argument)
432with an appended '_0' will be used as the point's components.
433
434=cut
435
436sub TotalDifferential ($;\@\@) {
437    my $function = shift;
438    $function = parse_from_string($function)
439      unless ref($function) =~ /^Math::Symbolic/;
440
441    my $sig = shift;
442    $sig = [ $function->signature() ] if not defined $sig;
443    my @sig = map { Math::Symbolic::Variable->new($_) } @$sig;
444
445    my $point = shift;
446    $point = [ map { $_->name() . '_0' } @sig ] if not defined $point;
447    my @point = map { Math::Symbolic::Variable->new($_) } @$point;
448
449    if ( @point != @sig ) {
450        croak "Signature dimension does not match point dimension.";
451    }
452
453    my @grad = grad $function, @sig;
454    if ( @grad != @sig ) {
455        croak "Signature dimension does not match function grad dim.";
456    }
457
458    foreach (@grad) {
459        my @point_copy = @point;
460        $_->implement( map { ( $_->name() => shift(@point_copy) ) } @sig );
461    }
462
463    my $d =
464      Math::Symbolic::Operator->new( '*', shift(@grad),
465        Math::Symbolic::Operator->new( '-', shift(@sig), shift(@point) ) );
466
467    $d +=
468      Math::Symbolic::Operator->new( '*', shift(@grad),
469        Math::Symbolic::Operator->new( '-', shift(@sig), shift(@point) ) )
470      while @grad;
471
472    return $d;
473}
474
475=head2 DirectionalDerivative
476
477DirectionalDerivative computes the directional derivative of a scalar function
478in the direction of a specified vector. With f being the function and X, A being
479vectors, it looks like this: (this is a partial derivative)
480
481  df(X)/dA = grad(f(X)) * (A / |A|)
482
483First argument must be the function to derive (either a string or a valid
484Math::Symbolic tree). Second argument must be vector into whose direction to
485derive. It is to be specified as an array of variable names and objects.
486Third argument is the optional signature to be used for computing the gradient.
487Please see the documentation of the grad function for details. It's
488dimension must match that of the directional vector.
489
490=cut
491
492sub DirectionalDerivative ($\@;\@) {
493    my $function = shift;
494    $function = parse_from_string($function)
495      unless ref($function) =~ /^Math::Symbolic/;
496
497    my $vec = shift;
498    my @vec = map { Math::Symbolic::Variable->new($_) } @$vec;
499
500    my $sig = shift;
501    $sig = [ $function->signature() ] if not defined $sig;
502    my @sig = map { Math::Symbolic::Variable->new($_) } @$sig;
503
504    if ( @vec != @sig ) {
505        croak "Signature dimension does not match vector dimension.";
506    }
507
508    my @grad = grad $function, @sig;
509    if ( @grad != @sig ) {
510        croak "Signature dimension does not match function grad dim.";
511    }
512
513    my $two     = Math::Symbolic::Constant->new(2);
514    my @squares =
515      map { Math::Symbolic::Operator->new( '^', $_, $two ) } @vec;
516
517    my $abs_vec = shift @squares;
518    $abs_vec += shift(@squares) while @squares;
519
520    $abs_vec =
521      Math::Symbolic::Operator->new( '^', $abs_vec,
522        Math::Symbolic::Constant->new( 1 / 2 ) );
523
524    @vec = map { $_ / $abs_vec } @vec;
525
526    my $dd = Math::Symbolic::Operator->new( '*', shift(@grad), shift(@vec) );
527
528    $dd += Math::Symbolic::Operator->new( '*', shift(@grad), shift(@vec) )
529      while @grad;
530
531    return $dd;
532}
533
534=begin comment
535
536This computes the taylor binomial
537
538  (d/dx*(x-x0)+d/dy*(y-y0))^n * f(x0, y0)
539
540=end comment
541
542=cut
543
544sub _taylor_binomial {
545    my $f  = shift;
546    my $a  = shift;
547    my $b  = shift;
548    my $a0 = shift;
549    my $b0 = shift;
550    my $n  = shift;
551
552    $f = $f->new();
553    my $da = $a - $a0;
554    my $db = $b - $b0;
555
556    $f->implement( $a->name() => $a0, $b->name() => $b0 );
557
558    return Math::Symbolic::Constant->one() if $n == 0;
559    return $da *
560      Math::Symbolic::Operator->new( 'partial_derivative', $f->new(), $a0 ) +
561      $db *
562      Math::Symbolic::Operator->new( 'partial_derivative', $f->new(), $b0 )
563      if $n == 1;
564
565    my $n_obj = Math::Symbolic::Constant->new($n);
566
567    my $p_a_deriv = $f->new();
568    $p_a_deriv =
569      Math::Symbolic::Operator->new( 'partial_derivative', $p_a_deriv, $a0 )
570      for 1 .. $n;
571
572    my $res =
573      Math::Symbolic::Operator->new( '*', $p_a_deriv,
574        Math::Symbolic::Operator->new( '^', $da, $n_obj ) );
575
576    foreach my $k ( 1 .. $n - 1 ) {
577        $p_a_deriv = $p_a_deriv->op1()->new();
578
579        my $deriv = $p_a_deriv;
580        $deriv =
581          Math::Symbolic::Operator->new( 'partial_derivative', $deriv, $b0 )
582          for 1 .. $k;
583
584        my $k_obj = Math::Symbolic::Constant->new($k);
585        $res += Math::Symbolic::Operator->new(
586            '*',
587            Math::Symbolic::Constant->new( _over( $n, $k ) ),
588            Math::Symbolic::Operator->new(
589                '*', $deriv,
590                Math::Symbolic::Operator->new(
591                    '*',
592                    Math::Symbolic::Operator->new(
593                        '^', $da, Math::Symbolic::Constant->new( $n - $k )
594                    ),
595                    Math::Symbolic::Operator->new( '^', $db, $k_obj )
596                )
597            )
598        );
599    }
600
601    my $p_b_deriv = $f->new();
602    $p_b_deriv =
603      Math::Symbolic::Operator->new( 'partial_derivative', $p_b_deriv, $b0 )
604      for 1 .. $n;
605
606    $res +=
607      Math::Symbolic::Operator->new( '*', $p_b_deriv,
608        Math::Symbolic::Operator->new( '^', $db, $n_obj ) );
609
610    return $res;
611}
612
613=begin comment
614
615This computes
616
617  / n \
618  |   |
619  \ k /
620
621=end comment
622
623=cut
624
625sub _over {
626    my $n = shift;
627    my $k = shift;
628
629    return 1 if $k == 0;
630    return _over( $n, $n - $k ) if $k > $n / 2;
631
632    my $prod = 1;
633    my $i    = $n;
634    my $j    = $k;
635    while ( $i > $k ) {
636        $prod *= $i;
637        $prod /= $j if $j > 1;
638        $i--;
639        $j--;
640    }
641
642    return ($prod);
643}
644
645=begin comment
646
647_faculty() computes the product that is the faculty of the
648first argument.
649
650=end comment
651
652=cut
653
654sub _faculty {
655    my $num = shift;
656    croak "Cannot calculate faculty of negative numbers."
657      if $num < 0;
658    my $fac = Math::Symbolic::Constant->one();
659    return $fac if $num <= 1;
660    for ( my $i = 2 ; $i <= $num ; $i++ ) {
661        $fac *= Math::Symbolic::Constant->new($i);
662    }
663    return $fac;
664}
665
666=head2 TaylorPolyTwoDim
667
668This subroutine computes the Taylor Polynomial for functions of two
669variables. Please refer to the documentation of the TaylorPolynomial
670function in the Math::Symbolic::MiscCalculus package for an explanation
671of single dimensional Taylor Polynomials. This is the counterpart in
672two dimensions.
673
674First argument must be the function to approximate with the Taylor Polynomial
675either as a string or a Math::Symbolic tree. Second and third argument
676must be the names of the two coordinates. (These may alternatively be
677Math::Symbolic::Variable objects.) Fourth argument must be
678the degree of the Taylor Polynomial. Fifth and Sixth arguments are optional
679and specify the names of the variables to introduce as the point of
680approximation. These default to the names of the coordinates with '_0'
681appended.
682
683=cut
684
685sub TaylorPolyTwoDim ($$$$;$$) {
686    my $function = shift;
687    $function = parse_from_string($function)
688      unless ref($function) =~ /^Math::Symbolic/;
689
690    my $x1 = shift;
691    $x1 = Math::Symbolic::Variable->new($x1)
692      unless ref($x1) eq 'Math::Symbolic::Variable';
693    my $x2 = shift;
694    $x2 = Math::Symbolic::Variable->new($x2)
695      unless ref($x2) eq 'Math::Symbolic::Variable';
696
697    my $n = shift;
698
699    my $x1_0 = shift;
700    $x1_0 = $x1->name() . '_0' if not defined $x1_0;
701    $x1_0 = Math::Symbolic::Variable->new($x1_0)
702      unless ref($x1_0) eq 'Math::Symbolic::Variable';
703
704    my $x2_0 = shift;
705    $x2_0 = $x2->name() . '_0' if not defined $x2_0;
706    $x2_0 = Math::Symbolic::Variable->new($x2_0)
707      unless ref($x2_0) eq 'Math::Symbolic::Variable';
708
709    my $x1_n = $x1->name();
710    my $x2_n = $x2->name();
711
712    my $dx1 = $x1 - $x1_0;
713    my $dx2 = $x2 - $x2_0;
714
715    my $copy = $function->new();
716    $copy->implement( $x1_n => $x1_0, $x2_n => $x2_0 );
717
718    my $taylor = $copy;
719
720    return $taylor if $n == 0;
721
722    foreach my $k ( 1 .. $n ) {
723        $taylor +=
724          Math::Symbolic::Operator->new( '/',
725            _taylor_binomial( $function->new(), $x1, $x2, $x1_0, $x2_0, $k ),
726            _faculty($k) );
727    }
728
729    return $taylor;
730}
731
732=head2 WronskyDet
733
734WronskyDet() computes the Wronsky Determinant of a set of n functions.
735
736First argument is required and a (literal) array of n functions. Second
737argument is optional and a (literal) array of n variables or variable names.
738If the second argument is omitted, the variables used for deriving are inferred
739from function signatures. This requires, however, that the function signatures
740have exactly one element. (And the function this exactly one variable.)
741
742=cut
743
744sub WronskyDet (\@;\@) {
745    my $functions = shift;
746    my @functions =
747      map { ( ref($_) =~ /^Math::Symbolic/ ) ? $_ : parse_from_string($_) }
748      @$functions;
749    my $vars = shift;
750    my @vars = ( defined $vars ? @$vars : () );
751    @vars = map {
752        my @sig = $_->signature();
753        croak "Cannot infer function signature for WronskyDet."
754          if @sig != 1;
755        shift @sig;
756    } @functions if not defined $vars;
757    @vars = map { Math::Symbolic::Variable->new($_) } @vars;
758    croak "Number of vars doesn't match num of functions in WronskyDet."
759      if not @vars == @functions;
760
761    my @matrix;
762    push @matrix, [@functions];
763    foreach ( 2 .. @functions ) {
764        my $i = 0;
765        @functions = map {
766            Math::Symbolic::Operator->new( 'partial_derivative', $_,
767                $vars[ $i++ ] )
768        } @functions;
769        push @matrix, [@functions];
770    }
771    return det @matrix;
772}
773
7741;
775__END__
776
777=head1 AUTHOR
778
779Please send feedback, bug reports, and support requests to the Math::Symbolic
780support mailing list:
781math-symbolic-support at lists dot sourceforge dot net. Please
782consider letting us know how you use Math::Symbolic. Thank you.
783
784If you're interested in helping with the development or extending the
785module's functionality, please contact the developers' mailing list:
786math-symbolic-develop at lists dot sourceforge dot net.
787
788List of contributors:
789
790  Steffen M�ller, symbolic-module at steffen-mueller dot net
791  Stray Toaster, mwk at users dot sourceforge dot net
792  Oliver Ebenh�h
793
794=head1 SEE ALSO
795
796New versions of this module can be found on
797http://steffen-mueller.net or CPAN. The module development takes place on
798Sourceforge at http://sourceforge.net/projects/math-symbolic/
799
800L<Math::Symbolic>
801
802=cut
803
804