File Coverage

blib/lib/Math/Project3D.pm
Criterion Covered Total %
statement 169 179 94.4
branch 38 66 57.5
condition 14 23 60.8
subroutine 22 23 95.6
pod 10 10 100.0
total 253 301 84.0


line stmt bran cond sub pod time code
1            
2             # See the POD documentation at the end of this
3             # document for detailed copyright information.
4             # (c) 2002-2006 Steffen Mueller, all rights reserved.
5            
6             package Math::Project3D;
7            
8 2     2   22975 use strict;
  2         6  
  2         88  
9 2     2   11 use warnings;
  2         3  
  2         70  
10            
11 2     2   53 use 5.006;
  2         7  
  2         82  
12            
13 2     2   11 use vars qw/$VERSION/;
  2         3  
  2         124  
14            
15             $VERSION = 1.02;
16            
17 2     2   10 use Carp;
  2         4  
  2         153  
18            
19 2     2   3332 use Math::MatrixReal;
  2         61914  
  2         144  
20 2     2   1756 use Math::Project3D::Function;
  2         5  
  2         4325  
21            
22             sub _max {
23 199     199   218 my $max = 0;
24 199         298 for (@_) {
25 241 100       626 $max = $_ if $_ > $max;
26             }
27             $max
28 199         322 }
29             sub _new_from_rows {
30 0     0   0 my $class = 'Math::MatrixReal';
31 0         0 my $rows = shift;
32 0         0 my @rows;
33 0         0 foreach (@$rows) {
34 0         0 push @rows,
35 0 0       0 (ref($_) eq 'Math::MatrixReal' ? [@{$_->[0][0]}] : [@{$_}]);
  0         0  
36             }
37 0 0       0 return bless [
38             \@rows,
39             scalar(@rows),
40             _max(
41             map {
42 0         0 ref($_) eq 'Math::MatrixReal' ? $_->[2] : scalar(@$_)
43             }
44             @rows
45             ),
46             undef,
47             undef,
48             undef
49             ] => $class;
50             }
51            
52             sub _new_from_cols {
53 199     199   4665 my $class = 'Math::MatrixReal';
54 199         225 my $cols = shift;
55 241 100       806 my $num_rows = _max(
56             map {
57 199         258 ref($_) eq 'Math::MatrixReal' ? $_->[1] : scalar(@$_)
58             }
59             @$cols
60             );
61 199         250 my $num_cols = @$cols;
62            
63 199         189 my @rows;
64 199         205 my $cn = 0;
65 199         261 foreach (@$cols) {
66 241         281 my $col = $_;
67 241 100       426 if (ref($col) eq 'Math::MatrixReal') {
68 88         99 $col = $col->[0];
69 88         429 $rows[$_][$cn] = $col->[$_][0] for 0..$num_rows-1;
70             }
71             else {
72 153         678 $rows[$_][$cn] = $col->[$_] for 0..$num_rows-1;
73             }
74 241         426 $cn++;
75             }
76 199         765 return bless [
77             \@rows,
78             $num_rows,
79             $num_cols,
80             undef,
81             undef,
82             undef
83             ] => $class;
84             }
85            
86            
87             # class and object method new_function
88             #
89             # Wrapper around Math::Project3D->new()
90             # Returns compiled function (anon sub)
91             # As a class method, it does not have side effects.
92             # As an object method, it assigns the returned function to
93             # the object's function attribute.
94            
95             sub new_function {
96 6 50   6 1 2731 @_ > 1 or croak "No arguments supplied to 'new_function' method.";
97            
98 6         13 my $self = shift;
99            
100 6         21 my @components = @_;
101            
102 6         40 my $function = Math::Project3D::Function->new(@components);
103            
104 6 100       21 if (ref $self eq __PACKAGE__) {
105 3         6 $self->{function} = $function;
106             }
107            
108 6         30 return $function;
109             }
110            
111            
112             # Class and object method/constructor new
113             #
114             # Arguments are used in the object's anon hash.
115             # Creates new object.
116             # Returns MP3D instance.
117            
118             sub new {
119 3     3 1 1456 my $proto = shift;
120 3   33     23 my $class = ref $proto || $proto;
121            
122             # !!!FIXME!!! Insert argument checking here
123 3         15 my $self = {
124             function => undef,
125             @_
126             };
127            
128 3         81 bless $self => $class;
129            
130             # Some attributes are required.
131 3         12 my $missing_attribute = $self->_require_attributes(qw(
132             plane_basis_vector
133             plane_direction1
134             plane_direction2
135             ));
136 3 50       9 croak "Required attribute '$missing_attribute' missing."
137             if defined $missing_attribute;
138            
139             # Transform the vector/matrix specifications ((nested) array refs)
140             # into Math::MatrixReal's.
141 3         7 foreach ( qw(
142             plane_basis_vector
143             plane_direction1
144             plane_direction2
145             ) ) {
146            
147             # Argument checking, we either want an array ref or a MMR object.
148 9 50       24 croak "Invalid argument '$_' passed to new. Should be an array reference."
149             if not exists $self->{$_};
150            
151 9 50       26 next if ref $self->{$_} eq 'Math::MatrixReal';
152 9 50       22 croak "Invalid argument '$_' passed to new. Should be an array reference."
153             if ref $self->{$_} ne 'ARRAY';
154            
155             # Transform into an MMR object.
156 9         30 $self->{$_} = $self->_make_matrix($self->{$_});
157             }
158            
159             # Projection vector defaults to the normal vector of the plane,
160             # but may also be specified explicitly as array ref or MMR object.
161 3 100 66     32 if ( defined $self->{projection_vector} and
    50 33        
162             ref $self->{projection_vector} eq 'ARRAY' ) {
163            
164 1         4 $self->{projection_vector} = $self->_make_matrix( $self->{projection_vector} );
165            
166             } elsif ( not defined $self->{projection_vector} or
167             ref $self->{projection_vector} ne 'Math::MatrixReal' ) {
168            
169             # Defaults to the normal of the plane.
170 2         13 $self->{projection_vector} = $self->{plane_direction1}->vector_product(
171             $self->{plane_direction2}
172             );
173             }
174            
175             # Now generate the linear equation system that has to be solved by
176             # MMR in order to do project a point onto the plane.
177             #
178             # MMR does all the dirty work. (Thanks!) So we just create a matrix.
179             # (d, e be the directional vectors, n the vector we project along,
180             # p the basis point. Let i be the solution)
181             #
182             # x(t) + n1*i1 = d1*i2 + e1*i3 + p1
183             # y(t) + n2*i1 = d2*i2 + e2*i3 + p2
184             # z(t) + n3*i1 = d3*i2 + e3*i3 + p3
185             #
186             # This is the intersection of the plane and the corresponding orthogonal
187             # line through s(t). Another form:
188             #
189             # n1*i1 + d1*i2 + e1*i3 = p1 + x(t)
190             # n2*i1 + d2*i2 + e2*i3 = p2 + y(t)
191             # n3*i1 + d3*i2 + e3*i3 = p3 + z(t)
192             #
193             # linear_eq_system will be the matrix of n,d,e.
194             # result_vector will be p+s(t) (later)
195            
196 3         109 my $linear_eq_system = _new_from_cols(
197             [
198             $self->{projection_vector},
199             $self->{plane_direction1},
200             $self->{plane_direction2},
201             ]
202             );
203            
204             # Now we do the first step towards solving the equation system.
205             # This does not have to be repeated for every projection.
206 3         16 $self->{lr_matrix} = $linear_eq_system->decompose_LR();
207            
208 3         457 return $self;
209             }
210            
211            
212             # Method project
213             #
214             # Does the projection calculations for a single set of function
215             # parameters.
216             # Takes the function parameters as argument.
217             # Returns undef on failure (plane and P + lambda*projection_vector do
218             # not intersect!). On success:
219             # Returns the coefficients of the point on the plane,
220             # the distance of the source point from the plane in lengths
221             # of the projection vector.
222            
223             sub project {
224 34     34 1 7202 my $self = shift;
225            
226 34 50       69 croak "You need to assign a function before projecting it."
227             if not defined $self->get_function();
228            
229             # Apply function
230 34         943 my $point = _new_from_cols(
231             [
232             [ $self->{function}->(@_) ],
233             ]
234             );
235            
236             # Generate result_vector
237 34         150 my $result_vector = _new_from_cols(
238             [
239             $self->{plane_basis_vector} + $point
240             ]
241             );
242            
243             # Solve the l_e_s.
244 34         186 my ($dimension, $p_vector, undef) = $self->{lr_matrix}->solve_LR($result_vector);
245            
246             # Did we find a solution?
247 34 50       3669 return undef if not defined $dimension;
248            
249             # $dimension == 0 => one solution
250             # $dimension == 1 => straight line
251             # $dimension == 2 => plane (impossible :) )
252             # ...
253             # $dimension == 1 is possible (point and projection_vector part
254             # of the plane). Hence: !!!FIXME!!!
255            
256 34         108 return $p_vector->element(2,1), # coefficient 1
257             $p_vector->element(3,1), # coefficient 2
258             $p_vector->element(1,1); # distance in lengths of the projection vector
259             }
260            
261            
262             # Method project_range_callback
263             #
264             # Does the projection calculations for ranges of function parameters.
265             # Takes an code reference and a number array refs of ranges as argument.
266             # Ranges are specified using three values:
267             # [lower_bound, upper_bound, increment].
268             # Alternatively, one may specify only one element in any of the array
269             # references. This will then be a static parameter.
270             # The number of ranges (array references) corresponds to the number of
271             # parameters to the vectorial function.
272             # The callback is called for every set of result values with an array
273             # reference of parameters and the list of the three result values.
274            
275             sub project_range_callback {
276 1     1 1 16 my $self = shift;
277            
278 1 50       3 croak "You need to assign a function before projecting it."
279             if not defined $self->get_function();
280            
281             # Argument checking
282 1         2 my $callback = shift;
283 1 50       5 ref $callback eq 'CODE' or
284             croak "Invalid code reference passed to project_range_callback.";
285            
286 1         3 my @ranges = @_;
287            
288 7         15 croak "Invalid range parameters passed to project_range_callback"
289 1 50       2 if grep { ref $_ ne 'ARRAY' } @ranges;
290            
291            
292 1         3 my $function = $self->get_function();
293            
294             # Replace all ranges that consist of a single number with ranges
295             # that iterate from that number to itself.
296 1         4 foreach (@ranges) {
297 7 100       17 $_ = [$_->[0], $_->[0], 1] if @$_ == 1;
298             }
299            
300             # Calculate every range's length and store it in @lengths.
301 7         16 my @lengths = map {
302             # upper - lower / increment
303 1         4 int( ($_->[1] - $_->[0]) / $_->[2] ),
304             } @ranges;
305            
306             # Prepare counters for every range.
307 1         4 my @counters = (0) x scalar(@ranges);
308            
309             # Calculate the number if iterations needed.
310             # It is $_+1 and not $_ because the lengths are the lengths we
311             # need for the comparisons inside the long for loop. We save one
312             # op in there that way.
313 1         4 my $iterations = 1;
314 1         39 $iterations *= ( $_ + 1 ) for @lengths;
315            
316             # For all possible combinations of parameters...
317 1         5 for (my $i = 1; $i <= $iterations; $i++) {
318            
319             # Get current function parameters
320 45         1372 my @params;
321            
322             # Get one parameter for every range
323 45         96 for (my $range_no = 0; $range_no < @ranges; $range_no++) {
324             # lower + increment * current_count
325 315         776 push @params, $ranges[$range_no][0] +
326             $ranges[$range_no][2] * $counters[$range_no];
327             }
328            
329             # Increment outermost range by one. If it got out of bounds,
330             # make it 0 and increment the next range, etc.
331 45         51 my $j = 0;
332 45   100     184 while (defined $ranges[$j] and ++$counters[$j] > $lengths[$j]) {
333 33         38 $counters[$j] = 0;
334 33         115 $j++;
335             }
336            
337             # Apply function
338 45         1089 my $point = _new_from_cols(
339             [
340             [ $function->(@params) ],
341             ]
342             );
343            
344             # Generate result_vector
345 45         206 my $result_vector = _new_from_cols(
346             [
347             $self->{plane_basis_vector} + $point
348             ]
349             );
350            
351             # Solve the l_e_s.
352 45         194 my ($dimension, $p_vector, undef) = $self->{lr_matrix}->solve_LR($result_vector);
353            
354             # Did we find a solution?
355 45 50       4493 croak "Could not project $result_vector."
356             if not defined $dimension;
357            
358             # $dimension == 0 => one solution
359             # $dimension == 1 => straight line
360             # $dimension == 2 => plane (impossible :) )
361             # ...
362             # $dimension == 1 is possible (point and projection_vector part
363             # of the plane). Hence: !!!FIXME!!!
364            
365 45         114 $callback->(
366             $p_vector->element(2,1), # coefficient 1
367             $p_vector->element(3,1), # coefficient 2
368             $p_vector->element(1,1), # distance in lengths of the projection vector
369             $j, # how many ranges did we increment?
370             );
371             }
372            
373 1         43 return();
374             }
375            
376            
377             # Method project_list
378             #
379             # Wrapper around project(), therefore slow.
380             # Takes a list of array refs as argument. The array refs
381             # are to contain sets of function parameters.
382             # Calculates every set of parameters' projection and stores the
383             # three associated values (coefficients and distance coefficient)
384             # in an n*3 matrix (MMR obj) wheren is the number of points
385             # projected.
386             # Currently croaks if any of the points cannot be projected onto
387             # the plane using the given projection vector. (In R3 -> R2, try
388             # using the plane's normal vector which is guaranteed not to be
389             # parallel to the plane.)
390             # Returns that MMR object.
391            
392             sub project_list {
393 1     1 1 524 my $self = shift;
394 1 50       5 croak "No arguments passed to project_list()."
395             if @_ == 0;
396            
397 1 50       3 croak "You need to assign a function before projecting it."
398             if not defined $self->get_function();
399            
400             # Create result matrix to hold individual results.
401 1         10 my $result_matrix = Math::MatrixReal->new(scalar(@_), 3);
402            
403             # Keep track of the matrix row
404 1         44 my $result_no = 1;
405            
406 1         2 foreach my $array_ref (@_) {
407 12         29 my ($coeff1, $coeff2, $dist) = $self->project(@$array_ref);
408            
409 12 50       287 croak "Vector $result_no cannot be projected."
410             if not defined $coeff1;
411            
412             # Assign results
413 12         34 $result_matrix->assign($result_no, 1, $coeff1);
414 12         157 $result_matrix->assign($result_no, 2, $coeff2);
415 12         212 $result_matrix->assign($result_no, 3, $dist);
416 12         134 $result_no++;
417             }
418            
419 1         4 return $result_matrix;
420             }
421            
422            
423             # Accessor get_function
424             #
425             # No parameters.
426             # Returns the current function code ref.
427            
428             sub get_function {
429 59     59 1 497 my $self = shift;
430 59         155 return $self->{function};
431             }
432            
433            
434             # Accessor set_function
435             #
436             # Takes a code ref as argument.
437             # Sets the object's function to that code ref.
438             # Returns the code ref.
439            
440             sub set_function {
441 10     10 1 25 my $self = shift;
442 10         11 my $function = shift;
443            
444 10 50       28 ref $function eq 'CODE' or croak "Argument to set_function must be code reference.";
445            
446 10         14 $self->{function} = $function;
447            
448 10         16 return $self->{function};
449             }
450            
451            
452             # Private method _require_attributes
453             #
454             # Arguments must be a list of attribute names (strings).
455             # Tests for the existance of those attributes.
456             # Returns the missing attribute on failure, undef on success.
457            
458             sub _require_attributes {
459 3     3   5 my $self = shift;
460 3   50     29 exists $self->{$_} or return $_ foreach @_;
461 3         8 return undef;
462             }
463            
464            
465             # Private method _make_matrix
466             #
467             # Takes a list of array refs as arguments.
468             # Creates a Math::MatrixReal object from the arrays.
469             # Returns the Math::MatrixReal object.
470            
471             sub _make_matrix {
472 10     10   14 my $self = shift;
473 10 50       20 @_ or croak "No arguments passed to _make_matrix.";
474            
475 10         46 my $matrix = Math::MatrixReal->new_from_cols( [ @_ ] );
476            
477 10         2407 return $matrix;
478             }
479            
480            
481             # Evil method rotate
482             #
483             # Takes a vector as argument. (Either an MMR object or an array ref
484             # of an array containing three components.)
485             # The method will replace the function with a wrapper around the original
486             # function that rotates the result of the original function by the same
487             # angles that the z-axis (e3) needs to be turned to become the passed vector.
488             # Example: Passed [1,0,0] means that e3 will be rotated to become e1.
489             # Hence all points will be rotated by 90 degrees and orthogonally to e2.
490             # Returns the wrapper function and the old function as code references.
491             #
492             # Important note: You can apply this function multiple times. That means
493             # if you rotated the function once, you may do so again and the original
494             # rotated function will be wrapped again so that you effectively rotate
495             # it twice. Note, however, that you will sacrifice performance on the
496             # altar of recursion that way.
497             #
498             # !!!FIXME!!! This is slow. Sloooooow. Conceptually slow, but the
499             # implementation sucks badly, too. It is especially unnerving to me that
500             # the whole lexical scope of a myriad of intermediate result matrices and
501             # vectors is kept because we use closures. Closures are great, but not
502             # in bloated lexical scopes. How fix that without introducing either
503             # - an ugly additional method
504             # - ugly additional blocks to keep the scope clean. (Yuck!)
505            
506             sub rotate {
507 9     9 1 805 my $self = shift;
508            
509 9 50       19 croak "You need to assign a function before rotating it."
510             if not defined $self->get_function();
511            
512             # We want to rotate everything the same way we would
513             # have to rotate e3 (z unit vector) to become the unit
514             # vector parallel to $e3_.
515 9         12 my $e3_ = shift;
516            
517             # Make sure we work with a MMR vector
518 9 50       24 if (ref $e3_ ne 'Math::MatrixReal') {
519 9 50       19 croak "Invalid vector passed to rotate()."
520             if ref $e3_ ne 'ARRAY';
521            
522 9         20 $e3_ = _new_from_cols([$e3_]);
523             }
524            
525 9         30 $e3_ *= 1 / $e3_->length();
526            
527 9         369 my $e3 = _new_from_cols([[0,0,1]]);
528            
529             # The axis we want to rotate around
530 9         31 my $axis = $e3->vector_product($e3_);
531            
532             # The angle we want to rotate by.
533 9         336 my $angle = acos($e3->scalar_product($e3_));
534            
535             # Rotationsmatrix um Achse (Vektor) a = (a,b,c):
536             # [a*a a*b a*c]
537             # M(rot) = (1-cos(alpha)) * [a*b b*b b*c] +
538             # [a*c b*c c*c]
539             # [1 0 0 ]
540             # cos(alpha) * [0 1 0 ] +
541             # [0 0 1 ]
542             # [0 -c b ]
543             # sin(alpha) * [c 0 -a ]
544             # [-b a 0 ]
545             # (from: Merziger, Wirth: "Repetitorium der Hoeheren Mathematik"
546             # Binomi Verlag. ISBN 3-923923-33-3)
547            
548 9         32 my ($a, $b, $c) = ($axis->element(1,1), $axis->element(2,1), $axis->element(3,1));
549            
550 9         182 my $matrix1 = _new_from_cols(
551             [
552             [$a*$a, $a*$b, $a*$c],
553             [$b*$a, $b*$b, $b*$c],
554             [$c*$a, $c*$b, $c*$c],
555             ],
556             );
557            
558 9         43 my $matrix2 = Math::MatrixReal->new_diag([1,1,1]);
559            
560 9         324 my $matrix3 = _new_from_cols(
561             [
562             [0, -$c, $b ],
563             [$c, 0, -$a],
564             [-$b, $a, 0 ],
565             ],
566             );
567            
568             # What a painful birth, but here we are:
569 9         53 my $rot_matrix = (1-cos($angle)) * $matrix1 + cos($angle) * $matrix2 +
570             sin($angle) * $matrix3;
571            
572             # For (Rotated vector r=(r1,r2,r3), source vector x=(x1,x2,x3)):
573             # (eq1) r = $rot_matrix * x
574             # Hence:
575             # (eq2) x = transposed($rot_matrix) * r
576             #
577             # x and $rot_matrix (and therefore also transposed($rot_matrix)
578             # are know. Hance, (eq2) can be solved as a linear equation system
579             # of the form: ($rot_matrix be all elements a(ij))
580             #
581             # a11*r1 + a12*r2 + a13*r3 = x1
582             # a21*r1 + a22*r2 + a23*r3 = x2
583             # a31*r1 + a32*r2 + a33*r3 = x3
584            
585             # Transpose the rotation matrix and then decompose_LR
586 9         2571 $rot_matrix->transpose($rot_matrix);
587 9         250 $rot_matrix = $rot_matrix->decompose_LR();
588            
589             # Save old function
590 9         1315 my $old_function = $self->get_function;
591            
592             my $rotator = sub {
593            
594             # If the first argument is 'restore', we restore the old
595             # function from the lexical $old_function and return.
596             # This is needed for the unrotate() method.
597 11 100   11   64 $self->{function} = $old_function, return if $_[0] eq 'restore';
598            
599             # We apply the old function as usual.
600 2         56 my $source_vec = _new_from_cols(
601             [ [ $old_function->(@_) ] ],
602             );
603            
604             # But then we rotate the results.
605 2         8 my ($dimension, $result_vector, undef) = $rot_matrix->solve_LR($source_vec);
606            
607             # There should be a solution to the equation system.
608 2 50       216 croak "Dimension of rotation result is '$dimension' but should be 0."
609             if $dimension;
610            
611             # return the rotated vector
612             return (
613 2         8 $result_vector->element(1,1),
614             $result_vector->element(2,1),
615             $result_vector->element(3,1),
616             );
617 9         43 };
618            
619             # Set the new function wrapper to become the actual function
620             # that will be used for projection calculations.
621 9         20 $self->set_function($rotator);
622            
623             # The level of nested rotation has been increased.
624 9         10 $self->{rotated}++;
625            
626 9         67 return ($rotator, $old_function);
627             }
628            
629            
630             # method unrotate
631             #
632             # Removes the evil hack of rotation using an evil hack.
633             # Takes an optional integer as argument. Removes
634             # [integer] levels of rotation. 'Level of rotation'
635             # means: one rotation wrapper of the original function
636             # as wrapped using rotate().
637             # If no integer was passed, defaults to the total number
638             # of rotations that were made, effectively removing
639             # any rotation.
640             # Returns the number of wrappers (rotations) removed.
641            
642             sub unrotate {
643 2     2 1 7 my $self = shift;
644            
645 2 50       5 croak "You need to assign a function before rotating it."
646             if not defined $self->get_function();
647            
648             # How many levels of rotation to we want to remove?
649 2         4 my $level = shift;
650            
651             # Boundary checking for the level. Default to the number
652             # of rotation wrappers.
653 2 50 66     34 if (not defined $level or $level <= 0 or $level > $self->{rotated}) {
      66        
654 1         2 $level = $self->{rotated};
655             }
656            
657 2         3 $level = int $level; # I don't trust users.
658            
659             # If we're rotated at all.
660 2 50       5 if ($self->{rotated}) {
661            
662             # Count the number of wrappers removed.
663 2         4 my $no_unrotated = 0;
664            
665             # While we still want to unrotate and while we still can
666 2   66     15 while ($level != 0 and $self->{rotated} != 0) {
667            
668             # Let the function restore its parent.
669 9         15 $self->{function}->('restore');
670            
671 9         11 $self->{rotated}--;
672 9         8 $level--;
673 9         32 $no_unrotated++;
674             }
675            
676 2         4 return $no_unrotated;
677            
678             } else {
679             # We aren't even rotated.
680 0         0 return 0;
681            
682             }
683             }
684            
685            
686             # Function acos (arc cosine)
687             # Not a method.
688 9     9 1 181 sub acos { atan2( sqrt( 1 - $_[0] * $_[0] ), $_[0] ) }
689            
690             1;
691            
692             __END__