File Coverage

blib/lib/Vector/Object3D/Matrix.pm
Criterion Covered Total %
statement 7 9 77.7
branch n/a
condition n/a
subroutine 3 3 100.0
pod n/a
total 10 12 83.3


line stmt bran cond sub pod time code
1             package Vector::Object3D::Matrix;
2              
3             =head1 NAME
4              
5             Vector::Object3D::Matrix - Matrix definitions and basic operations
6              
7             =head2 SYNOPSIS
8              
9             use Vector::Object3D::Matrix;
10              
11             # Create an instance of a class:
12             my $matrix = Vector::Object3D::Matrix->new(rows => [[-2, 2], [2, 1], [-1, -1]]);
13             my $matrix = Vector::Object3D::Matrix->new(cols => [[-2, 2, -1], [2, 1, -1]]);
14              
15             # Create a new object as a copy of an existing object:
16             my $copy = $matrix->copy;
17              
18             # Get number of columns/rows from a matrix object:
19             my $num_cols = $matrix->num_cols;
20             my $num_rows = $matrix->num_rows;
21              
22             # Fetch matrix data as an array of column/row values:
23             my $cols = $matrix->get_cols;
24             my $rows = $matrix->get_rows;
25              
26             # Set new precision value (which is used while printing out data and comparing
27             # the matrix object with others):
28             my $precision = 2;
29             $matrix->set(parameter => 'precision', value => $precision);
30              
31             # Get currently used precision value (undef indicates maximum possible precision
32             # which is designated to the Perl core):
33             my $precision = $matrix->get(parameter => 'precision');
34              
35             # Print out formatted matrix data:
36             $matrix->print(fh => $fh, precision => $precision);
37              
38             # Produce a matrix that is a result of scalar multiplication:
39             my $matrix2 = 2 * $matrix1;
40              
41             # Produce a matrix that is a result of matrix multiplication:
42             my $matrix3 = $matrix1 * $matrix2;
43              
44             # Add two matrices:
45             my $matrix4 = $matrix1 + $matrix2;
46              
47             # Subtract one matrix from another:
48             my $matrix5 = $matrix1 - $matrix2;
49              
50             # Compare two matrix objects:
51             my $are_the_same = $matrix1 == $matrix2;
52              
53             # Append another column to a matrix object:
54             $matrix->add(col => [2, -1, 3]);
55              
56             # Add another row to a matrix object:
57             $matrix->add(row => [0, 1, -2]);
58              
59             =head1 DESCRIPTION
60              
61             Although C<Vector::Object3D::Matrix> was originally meant as an auxiliary package supporting all the necessary calculations performed in the 3D space that are handled by C<Vector::Object3D> module only, it may still be used as a standalone module providing support for basic matrix operations (please note however that there are plenty more advanced modules available on CPAN already that serve exactly same purpose).
62              
63             Matrix definitions and basic operations like multiplication, addition and subtraction are implemented. It is also feasible to print out text-based contents of a matrix object to the standard output. Auxiliary static methods allow setting up 2D/3D transformation matrices.
64              
65             =head1 METHODS
66              
67             =head2 new
68              
69             Create an instance of a C<Vector::Object3D::Matrix> class:
70              
71             my $rows = [[-2, 2], [2, 1], [-1, -1]];
72             my $matrix = Vector::Object3D::Matrix->new(rows => $rows);
73              
74             my $cols = [[-2, 2, -1], [2, 1, -1]];
75             my $matrix = Vector::Object3D::Matrix->new(cols => $cols);
76              
77             There are two individual means of C<Vector::Object3D::Matrix> object construction, provided list of either rows or columns of numeric values. Although data is internally always stored as rows, cols constructor parameter takes precedence over rows in case both values are provided at the same time.
78              
79             =cut
80              
81             our $VERSION = '0.01';
82              
83 1     1   1518 use strict;
  1         4  
  1         38  
84 1     1   5 use warnings;
  1         2  
  1         28  
85              
86 1     1   499 use Moose;
  0            
  0            
87             with 'Vector::Object3D::Parameters';
88             with 'Vector::Object3D::Matrix::Transform';
89              
90             use Carp qw(croak);
91             use Data::Dumper;
92             use IO::Scalar;
93             use List::Util qw(max);
94             use Storable 'dclone';
95              
96             use overload
97             '*' => \&_multiplication,
98             '+' => \&_addition,
99             '-' => \&_subtraction,
100             '==' => \&_comparison,
101             '!=' => \&_negative_comparison;
102              
103             has rows => (
104             is => 'ro',
105             isa => 'ArrayRef[ArrayRef[Num]]',
106             required => 1,
107             );
108              
109             sub build_default_parameter_values {
110             my %parameter_values = (
111             precision => undef,
112             );
113              
114             return \%parameter_values;
115             }
116              
117             around BUILDARGS => sub {
118             my ($orig, $class, %args) = @_;
119              
120             my $cols = $args{cols};
121              
122             my $rows = defined $cols ? $class->_to_rows(cols => $cols) : $args{rows};
123              
124             return $class->$orig(rows => dclone $rows);
125             };
126              
127             sub BUILD {
128             my ($self) = @_;
129              
130             # Prepare printable version of matrix data:
131             my $data;
132             my $sh = new IO::Scalar \$data;
133             $self->print(fh => $sh);
134              
135             # Check rows data for inconsistencies:
136             my $rows = $self->get_rows;
137             my $num_cols = $self->num_cols;
138              
139             for my $row (@{$rows}) {
140             croak qq{Inconsistent matrix initialization data (number of columns varies between different rows): ${data}} unless @{$row} == $num_cols;
141             }
142              
143             return;
144             }
145              
146             sub _to_cols {
147             my ($class, %args) = @_;
148              
149             my $rows = $args{rows};
150              
151             my @cols;
152              
153             for my $vals (@{$rows}) {
154              
155             for (my $col = 0; $col < @{$vals}; $col++) {
156              
157             push @{ $cols[$col] }, $vals->[$col];
158             }
159             }
160              
161             return \@cols;
162             }
163              
164             sub _to_rows {
165             my ($class, %args) = @_;
166              
167             my $cols = $args{cols};
168              
169             my @rows;
170              
171             for my $vals (@{$cols}) {
172              
173             for (my $row = 0; $row < @{$vals}; $row++) {
174              
175             push @{ $rows[$row] }, $vals->[$row];
176             }
177             }
178              
179             return \@rows;
180             }
181              
182             =head2 copy
183              
184             Create a new C<Vector::Object3D::Matrix> object as a copy of an existing object:
185              
186             my $copy = $matrix->copy;
187              
188             =cut
189              
190             sub copy {
191             my ($self) = @_;
192              
193             my $rows = $self->rows;
194              
195             my $class = $self->meta->name;
196             my $copy = $class->new(rows => $rows);
197              
198             return $copy;
199             }
200              
201             =head2 num_cols
202              
203             Get number of columns from a matrix object:
204              
205             my $num_cols = $matrix->num_cols;
206              
207             =cut
208              
209             sub num_cols {
210             my ($self) = @_;
211              
212             my $cols = $self->get_cols;
213              
214             return scalar @{$cols};
215             }
216              
217             =head2 num_rows
218              
219             Get number of rows from a matrix object:
220              
221             my $num_rows = $matrix->num_rows;
222              
223             =cut
224              
225             sub num_rows {
226             my ($self) = @_;
227              
228             my $rows = $self->get_rows;
229              
230             return scalar @{$rows};
231             }
232              
233             =head2 get_cols
234              
235             Fetch matrix data as an array of column values:
236              
237             my $cols = $matrix->get_cols;
238              
239             =cut
240              
241             sub get_cols {
242             my ($self) = @_;
243              
244             my $rows = $self->rows;
245              
246             my $cols = $self->_to_cols(rows => $rows);
247              
248             return $cols;
249             }
250              
251             =head2 get_rows
252              
253             Fetch matrix data as an array of row values:
254              
255             my $rows = $matrix->get_rows;
256              
257             =cut
258              
259             sub get_rows {
260             my ($self) = @_;
261              
262             my $rows = $self->rows;
263              
264             return dclone($rows);
265             }
266              
267             =head2 add
268              
269             Append another column to a matrix instance:
270              
271             $matrix->add(col => [2, -1, 3]);
272              
273             Add another row to an existing matrix object:
274              
275             $matrix->add(row => [0, 1, -2]);
276              
277             Both add scenarios (adding a new column and adding a new row) will skip adding values that would exceed the other size of a matrix and fill in the missing ones with zeroes.
278              
279             =cut
280              
281             sub add {
282             my ($self, %args) = @_;
283              
284             my $col = $args{col};
285             my $row = $args{row};
286              
287             if (defined $col) {
288              
289             # Number of rows in a column needs to match a number of rows in a
290             # matrix object, otherwise adding a new column would not make sense:
291             my $num_rows = $self->num_rows;
292             my $new_col = dclone $col;
293              
294             if (@{$new_col} > $num_rows) {
295             $self->_reduce_array_length($new_col, $num_rows);
296             }
297             else {
298             $self->_fill_up_array_with_zeroes($new_col, $num_rows);
299             }
300              
301             for (my $i = 0; $i < @{$new_col}; $i++) {
302             my $val = $new_col->[$i];
303             push @{$self->rows->[$i]}, $val;
304             }
305             }
306             else {
307              
308             # Number of columns in a row needs to match a number of columns in
309             # a matrix object, otherwise adding a new row would not make sense:
310             my $num_cols = $self->num_cols;
311             my $new_row = dclone $row;
312              
313             if (@{$new_row} > $num_cols) {
314             $self->_reduce_array_length($new_row, $num_cols);
315             }
316             else {
317             $self->_fill_up_array_with_zeroes($new_row, $num_cols);
318             }
319              
320             push @{$self->rows}, $new_row;
321             }
322              
323             return;
324             }
325              
326             sub _reduce_array_length {
327             my ($self, $array, $length) = @_;
328              
329             splice @{$array}, $length;
330              
331             return;
332             }
333              
334             sub _fill_up_array_with_zeroes {
335             my ($self, $array, $length) = @_;
336              
337             push @{$array}, split //, 0 x ($length - @{$array});
338              
339             return;
340             }
341              
342             =head2 set
343              
344             Set new precision value (which is used while comparing matrix objects with each other):
345              
346             my $precision = 2;
347             $matrix->set(parameter => 'precision', value => $precision);
348              
349             =head2 get
350              
351             Get currently used precision value (undef indicates maximum possible precision which is designated to the Perl core):
352              
353             my $precision = $matrix->get(parameter => 'precision');
354              
355             =head2 print
356              
357             Print out text-formatted matrix data (which might be, for instance, useful for debugging purposes):
358              
359             $matrix->print(fh => $fh, precision => $precision);
360              
361             C<fh> defaults to the standard output. C<precision> is intended for internal use by string format specifier that outputs individual matrix values as decimal floating points, and defaults to 2.
362              
363             =cut
364              
365             sub print {
366             my ($self, %args) = @_;
367              
368             my $fh = $args{fh} || *STDOUT;
369             my $precision = $args{precision} || 2;
370              
371             $precision =~ s/\D//;
372              
373             # Calculate maximum possible length of a single matrix item:
374             my $maxlen = $self->_get_item_max_length($precision);
375              
376             my $stdout = select $fh;
377              
378             foreach my $row (@{$self->rows}) {
379             print qq{\n[ };
380             foreach my $val (@{$row}) {
381             printf qq{%${maxlen}.${precision}f }, $val;
382             }
383             print qq{]};
384             }
385              
386             select $stdout;
387              
388             return;
389             }
390              
391             sub _get_item_max_length {
392             my ($self, $precision) = @_;
393              
394             my @values = map { @{$_} } @{$self->get_rows};
395             my @lengths = map { length sprintf qq{%.${precision}f}, $_ } @values;
396              
397             my $max = max(@lengths);
398             return $max;
399             }
400              
401             =head2 get_rotation_matrix
402              
403             Construct rotation matrix on a 2D plane:
404              
405             my $rotateMatrix2D = Vector::Object3D::Matrix->get_rotation_matrix(
406             rotate_xy => (30 * $pi / 180),
407             );
408              
409             Construct rotation matrix in a 3D space:
410              
411             my $rotateMatrix3D = Vector::Object3D::Matrix->get_rotation_matrix(
412             rotate_xy => (30 * $pi / 180),
413             rotate_yz => -30 * ($pi / 180),
414             rotate_xz => 45 * ($pi / 180),
415             );
416              
417             =head2 get_scaling_matrix
418              
419             Construct scaling matrix on a 2D plane:
420              
421             my $scaleMatrix2D = Vector::Object3D::Matrix->get_scaling_matrix(
422             scale_x => 2,
423             scale_y => 2,
424             );
425              
426             Construct scaling matrix in a 3D space:
427              
428             my $scaleMatrix3D = Vector::Object3D::Matrix->get_scaling_matrix(
429             scale_x => 2,
430             scale_y => 2,
431             scale_z => 3,
432             );
433              
434             =head2 get_translation_matrix
435              
436             Construct translation matrix on a 2D plane:
437              
438             my $translateMatrix2D = Vector::Object3D::Matrix->get_translation_matrix(
439             shift_x => -2,
440             shift_y => 1,
441             );
442              
443             Construct translation matrix in a 3D space:
444              
445             my $translateMatrix3D = Vector::Object3D::Matrix->get_translation_matrix(
446             shift_x => -2,
447             shift_y => 1,
448             shift_z => 3,
449             );
450              
451             =head1 OPERATORS
452              
453             =head2 multiply (*)
454              
455             Produce a matrix that is a result of scalar multiplication:
456              
457             my $matrix2 = 2 * $matrix1;
458              
459             Produce a matrix that is a result of matrix multiplication:
460              
461             my $matrix3 = $matrix1 * $matrix2;
462              
463             =cut
464              
465             sub _multiplication {
466             my ($self, $arg) = @_;
467              
468             if (ref $arg eq __PACKAGE__) {
469             return $self->_multiplication_by_matrix($arg);
470             }
471             elsif ($arg =~ m/^-?(\d+|\.\d+|\d+\.\d+)$/) {
472             return $self->_multiplication_by_number($arg);
473             }
474             else {
475             croak "Incorrect call of overloaded operator '*' method";
476             }
477             }
478              
479             sub _multiplication_by_matrix {
480             my ($self, $arg) = @_;
481              
482             my $num_cols1 = $self->num_cols;
483             my $num_rows2 = $arg->num_rows;
484              
485             unless ($num_cols1 == $num_rows2) {
486             my ($matrix1, $matrix2);
487              
488             my $old_fh = open my $fh, '>', \$matrix1;
489             $self->print(fh => $fh);
490              
491             open $fh, '>', \$matrix2;
492             $arg->print(fh => $fh);
493              
494             close $fh;
495             select $old_fh;
496              
497             croak "Number of columns of the first matrix (${num_cols1}) does not match number of rows of the second matrix (${num_rows2}) - incompatibility makes matrix multiplication impossible:\n\nMATRIX #1$matrix1\n\nMATRIX #2$matrix2";
498             }
499              
500             my $rows1 = $self->get_rows;
501             my $rows2 = $arg->get_rows;
502             my $rows = [];
503              
504             my $num_rows = $self->num_rows;
505             my $num_cols = $arg->num_cols;
506              
507             for (my $i = 0; $i < $num_rows; $i++) {
508             for (my $j = 0; $j < $num_cols; $j++) {
509             my $val = 0;
510             for (my $k = 0; $k < $num_cols1; $k++) {
511             $val += $rows1->[$i]->[$k] * $rows2->[$k]->[$j];
512             }
513             $rows->[$i]->[$j] = $val;
514             }
515             }
516              
517             my $result = (ref $self)->new(rows => $rows);
518             return $result;
519             }
520              
521             sub _multiplication_by_number {
522             my ($self, $arg) = @_;
523              
524             my $rows = $self->get_rows;
525              
526             for my $row (@{$rows}) {
527             $row = [ map { $_ * $arg } @{$row} ];
528             }
529              
530             my $result = (ref $self)->new(rows => $rows);
531             return $result;
532             }
533              
534             =head2 add (+)
535              
536             Add two matrices:
537              
538             my $matrix3 = $matrix1 + $matrix2;
539              
540             A matrix may be added to another one if they both share exactly same dimensions.
541              
542             =cut
543              
544             sub _addition {
545             my ($self, $arg) = @_;
546              
547             if (ref $arg eq __PACKAGE__) {
548             return $self->_addition_to_matrix($arg);
549             }
550             else {
551             croak "Incorrect use of overloaded operator '+' method";
552             }
553             }
554              
555             sub _addition_to_matrix {
556             my ($self, $arg) = @_;
557              
558             my $num_rows1 = $self->num_rows;
559             my $num_rows2 = $arg->num_rows;
560             my $num_cols1 = $self->num_cols;
561             my $num_cols2 = $arg->num_cols;
562              
563             croak "Size of the first matrix (${num_rows1}x${num_cols1}) does not match size of the second matrix (${num_rows2}x${num_cols2}) - incompatibility makes matrix addition impossible" unless $num_rows1 == $num_rows2 and $num_cols1 == $num_cols2;
564              
565             my $rows1 = $self->get_rows;
566             my $rows2 = $arg->get_rows;
567             my $rows = [];
568              
569             for (my $i = 0; $i < $num_rows1; $i++) {
570             for (my $j = 0; $j < $num_cols1; $j++) {
571             $rows->[$i]->[$j] = $rows1->[$i]->[$j] + $rows2->[$i]->[$j];
572             }
573             }
574              
575             my $result = (ref $self)->new(rows => $rows);
576             return $result;
577             }
578              
579             =head2 subtract (-)
580              
581             Subtract one matrix from another:
582              
583             my $matrix3 = $matrix1 - $matrix2;
584              
585             A matrix may be subtracted from another one if they both share exactly same dimensions.
586              
587             =cut
588              
589             sub _subtraction {
590             my ($self, $arg) = @_;
591              
592             if (ref $arg eq __PACKAGE__) {
593             return $self->_subtraction_from_matrix($arg);
594             }
595             else {
596             croak "Incorrect use of overloaded operator '-' method";
597             }
598             }
599              
600             sub _subtraction_from_matrix {
601             my ($self, $arg) = @_;
602              
603             my $num_rows1 = $self->num_rows;
604             my $num_rows2 = $arg->num_rows;
605             my $num_cols1 = $self->num_cols;
606             my $num_cols2 = $arg->num_cols;
607              
608             croak "Size of the first matrix (${num_rows1}x${num_cols1}) does not match size of the second matrix (${num_rows2}x${num_cols2}) - incompatibility makes matrix subtraction impossible" unless $num_rows1 == $num_rows2 and $num_cols1 == $num_cols2;
609              
610             return $self + -1 * $arg;
611             }
612              
613             =head2 compare (==)
614              
615             Compare two matrix objects:
616              
617             my $are_the_same = $matrix1 == $matrix2;
618              
619             Overloaded comparison operator evaluates to true whenever two matrix objects are identical (same number of rows, columns and identical values in the corresponding cells).
620              
621             =cut
622              
623             sub _comparison {
624             my ($self, $arg) = @_;
625              
626             return 0 unless $self->num_cols == $arg->num_cols;
627             return 0 unless $self->num_rows == $arg->num_rows;
628              
629             # Get compare precision for both matrices:
630             my $precision1 = $self->get(parameter => 'precision');
631             $precision1 = defined $precision1 ? '.' . $precision1 : '';
632             my $precision2 = $arg->get(parameter => 'precision');
633             $precision2 = defined $precision2 ? '.' . $precision2 : '';
634              
635             my $rows1 = $self->get_rows;
636             my $rows2 = $arg->get_rows;
637              
638             for (my $i = 0; $i < $self->num_rows; $i++) {
639             for (my $j = 0; $j < $self->num_cols; $j++) {
640              
641             my $val1 = sprintf qq{%${precision1}f}, $rows1->[$i][$j];
642             $val1 =~ s/^(.*\..*?)0*$/$1/;
643             $val1 =~ s/\.$//;
644              
645             my $val2 = sprintf qq{%${precision2}f}, $rows2->[$i][$j];
646             $val2 =~ s/^(.*\..*?)0*$/$1/;
647             $val2 =~ s/\.$//;
648              
649             return 0 if $val1 ne $val2;
650             }
651             }
652              
653             return 1;
654             }
655              
656             =head2 negative compare (!=)
657              
658             Compare two matrix objects:
659              
660             my $are_not_the_same = $matrix1 != $matrix2;
661              
662             Overloaded negative comparison operator evaluates to true whenever two matrix objects differ (unequal number of rows, columns or diverse values in the corresponding cells).
663              
664             =cut
665              
666             sub _negative_comparison {
667             my ($self, $arg) = @_;
668              
669             return not $self->_comparison($arg);
670             }
671              
672             =head1 BUGS
673              
674             There are no known bugs at the moment. Please report any bugs or feature requests.
675              
676             =head1 EXPORT
677              
678             C<Vector::Object3D::Matrix> exports nothing neither by default nor explicitly.
679              
680             =head1 SEE ALSO
681              
682             L<Inline::Octave>, L<Math::Cephes::Matrix>, L<Math::Matrix>, L<PDL::MatrixOps>, L<Vector::Object3D>, L<Vector::Object3D::Matrix::Transform>, L<Vector::Object3D::Parameters>.
683              
684             =head1 AUTHOR
685              
686             Pawel Krol, E<lt>pawelkrol@cpan.orgE<gt>.
687              
688             =head1 VERSION
689              
690             Version 0.01 (2012-12-24)
691              
692             =head1 COPYRIGHT AND LICENSE
693              
694             Copyright (C) 2012 by Pawel Krol.
695              
696             This library is free open source software; you can redistribute it and/or modify it under the same terms as Perl itself, either Perl version 5.8.6 or, at your option, any later version of Perl 5 you may have available.
697              
698             PLEASE NOTE THAT IT COMES WITHOUT A WARRANTY OF ANY KIND!
699              
700             =cut
701              
702             no Moose;
703             __PACKAGE__->meta->make_immutable;
704              
705             1;