File Coverage

blib/lib/Math/BSpline/Curve/Role/Approximation.pm
Criterion Covered Total %
statement 134 205 65.3
branch 10 32 31.2
condition 7 17 41.1
subroutine 19 22 86.3
pod 0 2 0.0
total 170 278 61.1


line stmt bran cond sub pod time code
1             package Math::BSpline::Curve::Role::Approximation;
2             $Math::BSpline::Curve::Role::Approximation::VERSION = '0.002';
3             # ABSTRACT: fitting a B-spline curve to a point set
4              
5 5     5   49804 use 5.014;
  5         20  
6 5     5   30 use warnings;
  5         13  
  5         150  
7              
8 5     5   29 use Moo::Role;
  5         12  
  5         27  
9 5     5   1953 use List::Util 1.26 ('sum0');
  5         162  
  5         413  
10 5     5   38 use Scalar::Util 1.26 ('blessed');
  5         111  
  5         297  
11 5     5   35 use Ref::Util 0.010 ('is_plain_arrayref');
  5         76  
  5         311  
12 5     5   46 use Module::Runtime 0.012 ('require_module');
  5         146  
  5         46  
13 5     5   314 use Math::BSpline::Basis 0.001;
  5         72  
  5         167  
14 5     5   2623 use Math::Matrix::Banded 0.004;
  5         106624  
  5         10741  
15              
16             requires (
17             'new',
18             );
19              
20              
21             sub fit {
22 3     3 0 3387 my ($class, @args) = @_;
23              
24             # We support both a hash and a hashref as args.
25 3 50       20 my $args = @args == 1 ? $args[0] : {@args};
26 3   50     15 $args ||= {};
27              
28 3         9 foreach ('degree', 'n_control_points', 'points') {
29 9 50       27 if (!exists($args->{$_})) {
30 0         0 $Math::BSpline::Curve::logger->error(
31             "Missing required arguments: $_",
32             );
33 0         0 return undef;
34             }
35             }
36              
37 3         13 my $apr = $class->_fit_approximate_unbounded($args);
38 3 50       54 if ($apr) {
39             return $class->new(
40             degree => $args->{degree},
41             knot_vector => $apr->{knot_vector},
42             control_points => $apr->{control_points},
43 3         92 );
44             }
45             else {
46 0         0 return undef;
47             }
48             }
49              
50              
51             sub calculate_parameters {
52 2     2 0 7 my ($class, $points, $lengths, $total_length) = @_;
53              
54             # We support omission of $lengths/$total_length or only of
55             # $total_length.
56 2 50       10 ($lengths, $total_length) = $class->_fit_calculate_lengths($points)
57             if (!$lengths);
58 2   33     7 $total_length //= sum0(@$lengths);
59              
60 2         4 my $phi = [0];
61 2         4 my $cur_phi = 0;
62 2         10 for (my $i=1;$i<@$points-1;$i++) {
63 396         612 $cur_phi += $lengths->[$i-1] / $total_length;
64 396         829 push(@$phi, $cur_phi);
65             }
66 2         4 push(@$phi, 1);
67              
68 2         18 return $phi;
69             }
70              
71              
72             sub _fit_approximate_unbounded {
73 3     3   8 my ($class, $args) = @_;
74              
75             my $phi = $args->{phi} // $class->calculate_parameters(
76             $args->{points},
77 3   66     14 );
78             my $U = $args->{knot_vector} // $class->_fit_calculate_knot_vector(
79             $args->{degree},
80 3   33     21 $args->{n_control_points} - 1,
81             $phi,
82             );
83              
84             # We have not constructed the curve object, yet. Hence, we need
85             # construct our own basis.
86             my $basis = Math::BSpline::Basis->new(
87             degree => $args->{degree},
88 3         65 knot_vector => $U,
89             );
90              
91 3         2253 my $Nt = $class->_fit_calculate_Nt(
92             $basis,
93             $phi,
94             );
95 3         17 my $NtN = $Nt->AAt;
96 3         103813 my $result = $NtN->decompose_LU;
97 3 50       98484 if (!$result) {
98 0         0 $Math::BSpline::Curve::logger->errorf("Fit failed");
99 0         0 return undef;
100             }
101              
102             my $R = $class->_fit_calculate_R(
103             $Nt,
104             $args->{points},
105 3         29 );
106 3 50       15 return undef if (!defined($R));
107              
108 3         12 my $Pc = [map { $NtN->solve_LU($_) } @$R];
  6         4249  
109             my $P = $class->_fit_calculate_control_points(
110             $Pc,
111             $args->{vector_class},
112 3         3842 );
113 3 50       14 return undef if (!defined($P));
114              
115             return {
116 3         222 knot_vector => $U,
117             control_points => $P,
118             };
119             }
120              
121              
122             sub _fit_calculate_lengths {
123 2     2   5 my ($class, $points) = @_;
124              
125 2         4 my $lengths = [];
126 2         4 my $total_length = 0;
127 2         6 my $prv_point = $points->[0];
128 2 50       7 if (is_plain_arrayref($prv_point)) {
    0          
    0          
    0          
    0          
129 2         4 my $dim = scalar(@$prv_point);
130 2         8 for (my $i=1;$i<@$points;$i++) {
131 398         606 my $cur_point = $points->[$i];
132             my $cur_dist = [
133 398         647 map { $cur_point->[$_] - $prv_point->[$_] } (0..($dim-1)),
  796         1599  
134             ];
135             my $cur_length = sqrt(
136             sum0(
137 398         715 map { $cur_dist->[$_]**2 } (0..($dim-1)),
  796         1703  
138             ),
139             );
140              
141 398         761 push(@$lengths, $cur_length);
142 398         568 $total_length += $cur_length;
143              
144 398         922 $prv_point = $cur_point;
145             }
146             }
147             elsif ($prv_point->isa('Math::GSL::Vector')) {
148 0         0 for (my $i=1;$i<@$points;$i++) {
149 0         0 my $cur_point = $points->[$i];
150 0         0 my $cur_dist = $cur_point - $prv_point;
151 0         0 my $cur_length = $cur_dist->norm(2);
152              
153 0         0 push(@$lengths, $cur_length);
154 0         0 $total_length += $cur_length;
155              
156 0         0 $prv_point = $cur_point;
157             }
158             }
159             elsif ($prv_point->isa('Math::MatrixReal')) {
160 0         0 for (my $i=1;$i<@$points;$i++) {
161 0         0 my $cur_point = $points->[$i];
162 0         0 my $cur_dist = $cur_point - $prv_point;
163 0         0 my $cur_length = $cur_dist->norm_p(2);
164              
165 0         0 push(@$lengths, $cur_length);
166 0         0 $total_length += $cur_length;
167              
168 0         0 $prv_point = $cur_point;
169             }
170             }
171             elsif ($prv_point->isa('Math::Vector::Real')) {
172 0         0 for (my $i=1;$i<@$points;$i++) {
173 0         0 my $cur_point = $points->[$i];
174 0         0 my $cur_dist = $cur_point - $prv_point;
175 0         0 my $cur_length = abs($cur_dist);
176              
177 0         0 push(@$lengths, $cur_length);
178 0         0 $total_length += $cur_length;
179              
180 0         0 $prv_point = $cur_point;
181             }
182             }
183             elsif ($prv_point->isa('Math::VectorReal')) {
184 0         0 for (my $i=1;$i<@$points;$i++) {
185 0         0 my $cur_point = $points->[$i];
186 0         0 my $cur_dist = $cur_point - $prv_point;
187 0         0 my $cur_length = $cur_dist->length;
188              
189 0         0 push(@$lengths, $cur_length);
190 0         0 $total_length += $cur_length;
191              
192 0         0 $prv_point = $cur_point;
193             }
194             }
195             else {
196             # We assume that * is overloaded as dot product
197 0         0 for (my $i=1;$i<@$points;$i++) {
198 0         0 my $cur_point = $points->[$i];
199 0         0 my $cur_dist = $cur_point - $prv_point;
200 0         0 my $cur_length = sqrt($cur_dist * $cur_dist);
201              
202 0         0 push(@$lengths, $cur_length);
203 0         0 $total_length += $cur_length;
204              
205 0         0 $prv_point = $cur_point;
206             }
207             }
208              
209 2         8 return($lengths, $total_length);
210             }
211              
212              
213             sub _fit_calculate_knot_vector {
214 3     3   11 my ($class, $p, $n, $phi) = @_;
215 3         8 my $m = @$phi + 1;
216              
217 3         9 my $U = [map { 0 } (0..$p)];
  12         24  
218 3         12 my $step = ($m + 1) / ($n - $p + 1);
219 3         10 for (my $j=1;$j<=$n-$p;$j++) {
220 108         186 my $i = int($j * $step);
221 108         167 my $t = $j * $step - $i;
222 108         268 push(
223             @$U,
224             (1 - $t) * $phi->[$i - 1] + $t * $phi->[$i],
225             )
226             }
227 3         8 push(@$U, map { 1 } (0..$p));
  12         21  
228              
229 3         12 return $U;
230             }
231              
232              
233             sub _fit_calculate_Nt {
234 3     3   11 my ($class, $basis, $phi) = @_;
235 3         11 my $p = $basis->degree;
236 3         62 my $U = $basis->knot_vector;
237 3         30 my $n = (@$U - 1) - $p - 1;
238              
239 3         22 my $Nt = Math::Matrix::Banded->new(
240             M => $n + 1,
241             N => @$phi,
242             );
243 3         2218 for (my $j=0;$j<@$phi;$j++) {
244 600         41396 my $u = $phi->[$j];
245 600         1668 my $s = $basis->find_knot_span($u);
246 600         31449 my $Nip = $basis->evaluate_basis_functions($s, $u);
247 600         51138 for (my $i=0;$i<@$Nip;$i++) {
248 2400         226120 $Nt->element($s - $p + $i, $j, $Nip->[$i]);
249             }
250             }
251              
252 3         137 return $Nt;
253             }
254              
255              
256             sub _fit_calculate_R_plain_arrayref {
257 3     3   9 my ($class, $Nt, $points) = @_;
258              
259             # Because a plain arrayref does not overload scalar
260             # multiplication we need to first split the array of points into
261             # d (dimension of points) arrays of single numbers.
262 3         7 my $dim = scalar(@{$points->[0]});
  3         11  
263             my $cmp_lists = [
264 3         12 map { [] } (1..$dim),
  6         19  
265             ];
266 3         16 for (my $i=0;$i<@$points;$i++) {
267 600         917 my $v = $points->[$i];
268 600         1208 for (my $j=0;$j<$dim;$j++) {
269 1200         1668 push(@{$cmp_lists->[$j]}, $v->[$j]);
  1200         3187  
270             }
271             }
272              
273             my $R_cmp_lists = [
274 3         10 map { $Nt->multiply_vector($_) } @$cmp_lists,
  6         8519  
275             ];
276              
277 3         7914 my $R = [];
278 3         11 for (my $i=0;$i<@{$R_cmp_lists->[0]};$i++) {
  123         258  
279 120         208 my $components = [map { $R_cmp_lists->[$_]->[$i]} (0..($dim-1))];
  240         484  
280 120         291 for (my $l=0;$l<$dim;$l++) {
281 240   100     457 $R->[$l] //= [];
282 240         580 $R->[$l]->[$i] = $components->[$l];
283             }
284             }
285              
286 3         44 return $R;
287             }
288              
289              
290             sub _fit_calculate_R_math_gsl_vector {
291 0     0   0 my ($class, $Nt, $points) = @_;
292              
293             # We can do the matrix multiplication for all dimensions in
294             # one go because multiply_vector only does addition and scalar
295             # multiplication, so if these are overloaded it can treat a
296             # vector object just like a number.
297 0         0 my $R_vec = $Nt->multiply_vector($points);
298 0         0 my $R = [];
299 0         0 for (my $i=0;$i<@$R_vec;$i++) {
300 0         0 my $components = [$R_vec->[$i]->as_list];
301 0         0 for (my $l=0;$l<@$components;$l++) {
302 0   0     0 $R->[$l] //= [];
303 0         0 $R->[$l]->[$i] = $components->[$l];
304             }
305             }
306              
307 0         0 return $R;
308             }
309              
310              
311             sub _fit_calculate_R {
312 3     3   14 my ($class, $Nt, $points) = @_;
313              
314             # If the points are let's say 3-dimensional vectors, then we
315             # want to return three array references with the x, y, and z
316             # components of each point.
317              
318 3         10 my $v0 = $points->[0];
319 3 50       19 if (is_plain_arrayref($v0)) {
    0          
320 3         18 return $class->_fit_calculate_R_plain_arrayref($Nt, $points);
321             }
322             elsif ($v0->isa('Math::GSL::Vector')) {
323 0         0 return $class->_fit_calculate_R_math_gsl_vector($Nt, $points);
324             }
325             # elsif ($v0->isa('Math::MatrixReal')) {
326             # return $class->_fit_calculate_R_math_matrixreal($Nt, $points);
327             # }
328             # elsif ($v0->isa('Math::Vector::Real')) {
329             # return $class->_fit_calculate_R_math_vector_real($Nt, $points);
330             # }
331             # elsif ($v0->isa('Math::VectorReal')) {
332             # return $class->_fit_calculate_R_math_vectorreal($Nt, $points);
333             # }
334             else {
335 0   0     0 $Math::BSpline::Curve::logger->errorf(
336             "Unsupported vector class: %s",
337             blessed($v0) // 'undef',
338             );
339 0         0 return undef;
340             }
341             }
342              
343              
344             sub _fit_calculate_control_points_plain_arrayref {
345 3     3   9 my ($class, $Pc) = @_;
346              
347 3         9 my $P = [];
348 3         11 for (my $i=0;$i<@{$Pc->[0]};$i++) {
  123         283  
349             push(
350             @$P,
351 120         198 [map { $_->[$i] } @$Pc],
  240         485  
352             );
353             }
354              
355 3         10 return $P;
356             }
357              
358              
359             sub _fit_calculate_control_points_math_gsl_vector {
360 0     0   0 my ($class, $Pc) = @_;
361              
362 0         0 require_module('Math::GSL::Vector');
363              
364 0         0 my $P = [];
365 0         0 for (my $i=0;$i<@{$Pc->[0]};$i++) {
  0         0  
366             push(
367             @$P,
368 0         0 Math::GSL::Vector->new([map { $_->[$i] } @$Pc]),
  0         0  
369             );
370             }
371              
372 0         0 return $P;
373             }
374              
375              
376             sub _fit_calculate_control_points_math_matrixreal {
377 0     0   0 my ($class, $Pc) = @_;
378              
379 0         0 require_module('Math::MatrixReal');
380              
381 0         0 my $P = [];
382 0         0 for (my $i=0;$i<@{$Pc->[0]};$i++) {
  0         0  
383             push(
384             @$P,
385             Math::MatrixReal->new_from_cols(
386             [
387 0         0 [map { $_->[$i] } @$Pc],
  0         0  
388             ],
389             ),
390             );
391             }
392              
393 0         0 return $P;
394             }
395              
396              
397             sub _fit_calculate_control_points {
398 3     3   20 my ($class, $Pc, $vtc) = @_;
399              
400 3 50       13 if (!$vtc) {
    0          
401 3         14 return $class->_fit_calculate_control_points_plain_arrayref($Pc);
402             }
403             elsif ($vtc eq 'Math::GSL::Vector') {
404 0           return $class->_fit_calculate_control_points_math_gsl_vector($Pc);
405             }
406             # elsif ($vtc eq 'Math::MatrixReal') {
407             # return $class->_fit_calculate_control_points_math_matrixreal($Pc);
408             # }
409             # elsif ($vtc eq 'Math::Vector::Real') {
410             # return $class->_fit_calculate_control_points_math_vector_real($Pc);
411             # }
412             # elsif ($vtc eq 'Math::VectorReal') {
413             # return $class->_fit_calculate_control_points_math_vectorreal($Pc);
414             # }
415             else {
416 0           $Math::BSpline::Curve::logger->errorf(
417             "Unsupported vector class: %s",
418             $vtc,
419             );
420 0           return undef;
421             }
422             }
423              
424              
425             1;
426              
427             __END__