The Perl Toolchain Summit needs more sponsors. If your company depends on Perl, please support this very important event.

=head1 NAME

Math::Orthonormalize - Gram-Schmidt Orthonormalization of vectors

=head1 SYNOPSIS

  use Math::Orthonormalize qw(:all);
  
  my @base_of_r_2 = (
    [2, 1],
    [1, 3]
  );
  my $vector = [1, 2, 3];
  
  my @orthonormalized = orthonormalize(@base_of_r_2);
  my @orthogonalized  = orthogonalize(@base_of_r_2);
  
  my $normalized      = normalize($vector);
  my $scaled          = scale(2, $vector);
  my $scalar          = scalar_product($vector1, $vector2);

=head1 DESCRIPTION

Math::Orthonormalize offers subroutines to compute normalized or
non-normalized orthogonal bases of Euclidean vector spaces. That means:
Given a vector base of R^n, it computes a new base of R^n whose individual
vectors are all orthogonal. If those new base vectors all have a length of
1, the base is orthonormalized.

The module uses the Gram-Schmidt Algorithm.

=head2 EXPORT

No subroutines are exported by default, but the standart Exporter semantics are
in place, including the ':all' tag that imports all of the exportable
subroutines which are listed below.

=cut

package Math::Orthonormalize;

use 5.006;
use strict;
use warnings;

use Math::Symbolic qw/parse_from_string/;
use Carp;

our $VERSION = '1.00';

require Exporter;
our @ISA = qw(Exporter);
our %EXPORT_TAGS = ( 'all' => [ qw(
		orthonormalize
		orthogonalize
		scalar_product
		normalize
		scale
) ] );
our @EXPORT_OK = ( @{ $EXPORT_TAGS{'all'} } );
our @EXPORT = ();

=head1 SUBROUTINES

=head2 orthonormalize

Takes any number (>1) of vectors (array refs of vector components) as argument
which form a base (that is, they are linearly independent) and returns
an orthogonalized and normalized base of the same vector space
(that is, n new array references).

=cut

sub orthonormalize {
	my @vectors = @_;
	croak "No arguments to orthogonalize"
	  if not @vectors;
	croak "Arguments to orthogonalize must be array refs (vectors)"
	  if grep {ref($_) ne 'ARRAY'} @vectors;
	my $dim = @{$vectors[0]};
	croak "Vectors must have same dimension for orthogonalization"
	  if grep {@$_ != $dim} @vectors;

	my @base = orthogonalize(@vectors);
	@base = map {normalize($_)} @base;
	return @base;
}


=head2 orthogonalize

Takes any number (>1) of vectors (array refs of vector components) as argument
which form a base (that is, they are linearly independent) and returns
an orthogonalized base of the same vector space (that is, n new array
references).

=cut

sub orthogonalize {
	my @vectors = @_;
	croak "No arguments to orthogonalize"
	  if not @vectors;
	croak "Arguments to orthogonalize must be array refs (vectors)"
	  if grep {ref($_) ne 'ARRAY'} @vectors;
	my $dim = @{$vectors[0]};
	croak "Vectors must have same dimension for orthogonalization"
	  if grep {@$_ != $dim} @vectors;
	
	my @newbase;
	push @newbase, $vectors[0];
	my @squares;
	push @squares, scalar_product($vectors[0], $vectors[0]) if @vectors > 1;
	foreach my $i (1..$#vectors) {
		my $new = $vectors[$i];
		foreach my $j (0..$#newbase) {
			my $vec = scale(
				scalar_product($vectors[$i], $newbase[$j])
				/ $squares[$j],
				$newbase[$j]
			);
			@$new = map {$_ -= shift @$vec} @$new;
		}
		push @newbase, $new;
		push @squares, scalar_product($new, $new) if $i < $#vectors;
	}
	return @newbase;
}

=head2 normalize

Normalizes a vector. That is, it changes the vector length to 1 without
changing the vector's direction.

Takes an array reference with the vector components as argument and returns
a new array reference containing the normalized vector components.

=cut

sub normalize {
	my $v = shift;
	croak "Argument to normalize() must be an array ref (vector)"
	  if not ref($v) eq 'ARRAY';
	croak "Cannot normalize 0-dimensional vectors"
	  if @$v == 0;
	my $sum = $v->[0]**2;
	$sum += $v->[$_]**2 for 1..$#$v;
	$sum = sqrt($sum);
	croak "Cannot normalize 0-vector"
	  if $sum == 0;
	return [map {$_ / $sum} @$v];
}

=head2 scale

Takes a scalar and a vector (array reference of vector components) as
arguments. Multiplies every component of the vector by the specified
scalar and returns a new array reference containing the scaled vector
components.

=cut

sub scale {
	croak "scale() takes a scalar and a vector (ary ref) as arguments"
	  if not @_ == 2 or not ref($_[1]) eq 'ARRAY';
	croak "Cannot handle 0-dimensional vectors"
	  if @{$_[1]} == 0;
	my $new = [];
	foreach (@{$_[1]}) {
		push @$new, $_[0] * $_;
	}
	return $new;
}

=head2 scalar_product

Computes the scalar product of two vectors. Expects two array references
with vector components (same number of components) as argument and
returns their scalar product.

=cut

sub scalar_product {
	croak "Invalid number of arguments to scalar_product()"
	  if not @_ == 2;
	my ($v1, $v2) = @_;
	croak "Cannot compute the scalar product of vectors of different ",
		"dimensions"
	  if @$v1 != @$v2;
	croak "Cannot deal with 0-dimensional vectors"
	  if @$v1 == 0;
	my $sum = $v1->[0] * $v2->[0];
	return $sum if @$v1 == 1;
	
	$sum += $v1->[$_] * $v2->[$_] for 1..$#$v1;
	return $sum;
}


1;
__END__

=head1 AUTHOR

Steffen Mueller, orthonormalize-module at steffen-mueller dot net

=head1 SEE ALSO

(German) Merziger, Wirth: "Repetitorium der Höheren Mathematik" (Binomi, 1999)

You may find the current versions of this module at http://steffen-mueller.net/
or on CPAN.

=head1 COPYRIGHT AND LICENSE

Copyright (C) 2004-2005 by Steffen Mueller

This library is free software; you can redistribute it and/or modify
it under the same terms as Perl itself, either Perl version 5.6.1 or,
at your option, any later version of Perl 5 you may have available.

=cut