File Coverage

blib/lib/PDL/FuncND.pm
Criterion Covered Total %
statement 136 138 98.5
branch 65 84 77.3
condition 30 45 66.6
subroutine 23 23 100.0
pod 5 5 100.0
total 259 295 87.8


line stmt bran cond sub pod time code
1             package PDL::FuncND;
2              
3             # ABSTRACT: N dimensional version of functions
4              
5 9     9   3101430 use strict;
  9         23  
  9         383  
6 9     9   45 use warnings;
  9         16  
  9         516  
7              
8 9     9   2022 use PDL::LiteF;
  9         2899  
  9         76  
9 9     9   243681 use PDL::Constants qw[ PI ];
  9         18856  
  9         730  
10 9     9   69 use PDL::Math qw[ lgamma ];
  9         37  
  9         79  
11 9     9   952 use PDL::MatrixOps qw[ determinant identity ];
  9         21  
  9         80  
12 9     9   4572 use PDL::Transform qw[];
  9         76262  
  9         317  
13 9     9   77 use PDL::Options qw[ iparse ];
  9         22  
  9         689  
14              
15 9     9   54 use Scalar::Util qw[ looks_like_number ];
  9         14  
  9         504  
16              
17 9     9   51 use base 'PDL::Exporter';
  9         18  
  9         1389  
18              
19 9     9   60 use Carp;
  9         16  
  9         24122  
20              
21             our @EXPORT_OK = qw[
22             cauchyND
23             gaussND
24             lorentzND
25             mahalanobis
26             moffatND
27             ];
28              
29             our %EXPORT_TAGS = ( Func => [@EXPORT_OK], );
30              
31             #<<< no tidy
32             our $VERSION = '0.13';
33             #>>>
34              
35             # keep option handling uniform. barf if there's a problem
36             sub _handle_options { ## no critic (Subroutines::ProhibitExcessComplexity)
37              
38             # remove known arguments from @_ so can use new_from_specification
39 53     53   424776 my $self = shift;
40 53         109 my $opt = shift;
41              
42 53         111 my ( $vectors, $output, $ndims, $center, $scale );
43              
44 53 100       210 if ( $opt->{vectors} ) {
45             croak( "first argument must be a piddle if vectors option is set\n" )
46 18 50       35 unless eval { $self->isa( 'PDL' ) };
  18         119  
47              
48 18         35 $output = shift;
49 18         33 $vectors = $self;
50              
51 18 50       123 croak( 'wrong dimensions for input vectors; expected <= 2, got ', $vectors->ndims )
52             if $vectors->ndims > 2;
53              
54             # transform 0D piddle into 1D
55 18 100       73 $vectors = $vectors->dummy( 0 )
56             if $vectors->ndims == 0;
57              
58 18         316 ( $ndims ) = $vectors->dims;
59              
60             $vectors = $opt->{transform}->apply( $vectors )
61 18 100       77 if defined $opt->{transform};
62              
63 18 100       315 if ( defined $opt->{center} ) {
64             croak( "cannot use center = 'auto' if vectors is set\n" )
65 5 100 66     223 if !ref $opt->{center} && $opt->{center} eq 'auto';
66              
67             croak( "cannot use center = [ $opt->{center}[0], ... ] if vectors is set\n" )
68             if 'ARRAY' eq ref $opt->{center}
69             && !ref $opt->{center}[0]
70 4 100 66     428 && !looks_like_number( $opt->{center}[0] );
      100        
71              
72 2         9 $center = PDL::Core::topdl( $opt->{center} );
73             }
74             }
75              
76             else {
77 35 100       349 $output
78             = @_
79             ? $self->new_from_specification( @_ )
80             : $self->new_or_inplace;
81              
82 35         78228 $ndims = $output->ndims;
83              
84 35         179 $vectors = $output->ndcoords->reshape( $ndims, $output->nelem );
85              
86             $vectors->inplace->apply( $opt->{transform} )
87 35 100       233907 if defined $opt->{transform};
88              
89 35 100       205133 if ( defined $opt->{center} ) {
90 34 100 100     527 if ( !ref $opt->{center} && $opt->{center} eq 'auto' ) {
    100 66        
      100        
91 3         57 $center = ( pdl( [ $output->dims ] ) - 1 ) / 2;
92             $center->inplace->apply( $opt->{transform} )
93 3 100       551 if defined $opt->{transform};
94             }
95             elsif ('ARRAY' eq ref $opt->{center}
96             && !ref $opt->{center}[0]
97             && $opt->{center}[0] eq 'offset' )
98             {
99              
100 2         14 $center = ( pdl( [ $output->dims ] ) - 1 ) / 2;
101              
102             # inplace bug ( PDL == 2.4.11, at least) causes SEGV
103             # if this is done in place. so. don't.
104             $center = $center->apply( $opt->{transform} )
105 2 100       207 if defined $opt->{transform};
106              
107             # allow:
108             # offset => piddle
109             # offset => [ ... ]
110             # offset => ( list )
111              
112             # NOTE!!!! bug in topdl ( PDL == 2.4.11, at least )
113             # topdl only uses the first argument
114 2         171 my @offset = @{ $opt->{center} };
  2         6  
115 2         4 shift @offset;
116              
117 2 50       8 $center += PDL::Core::topdl( @offset > 1 ? \@offset : @offset );
118             }
119              
120             else {
121              
122 29         97 $center = PDL::Core::topdl( $opt->{center} );
123              
124             }
125             }
126             else {
127              
128 1         5 $center = zeroes( $vectors->type, $ndims );
129              
130             }
131             }
132              
133             # for 1D output $center may be a 0D PDL; this causes grief;
134             # promote it to a 1D
135 50 100 100     2579 $center = $center->dummy( 0 ) if defined $center && $center->ndims == 0;
136              
137 50 50 33     636 croak( "center vector has wrong dimensions\n" )
      66        
138             if defined $center
139             && ( $center->ndims != 1
140             || ( $center->dims )[0] != $ndims );
141              
142             ## no critic( ControlStructures::ProhibitCascadingIfElse )
143              
144             # handle scale
145             # scalar -> symmetric, independent
146 50 100 100     339 if ( !defined $opt->{scale} || !ref $opt->{scale} ) {
    100 66        
    50 33        
    50 33        
      33        
      33        
147              
148 34 100       187 $scale = identity( $ndims ) * ( defined $opt->{scale} ? $opt->{scale}**2 : 1 );
149             }
150              
151             # 1D piddle of length N
152             elsif ( 'ARRAY' eq ref $opt->{scale}
153 10         43 && @{ $opt->{scale} } == $ndims )
154             {
155 10         62 $scale = identity( $ndims ) * pdl( $opt->{scale} )**2;
156             }
157              
158             # 1D piddle of length N
159 6         83 elsif (eval { $opt->{scale}->isa( 'PDL' ) }
160             && $opt->{scale}->ndims == 1
161             && ( $opt->{scale}->dims )[0] == $ndims )
162             {
163 0         0 $scale = identity( $ndims ) * $opt->{scale}**2;
164             }
165              
166             # full matrix N^N piddle
167 6         111 elsif (eval { $opt->{scale}->isa( 'PDL' ) }
168             && $opt->{scale}->ndims == 2
169             && all( pdl( $opt->{scale}->dims ) == pdl( $ndims, $ndims ) ) )
170             {
171 6         1429 $scale = $opt->{scale};
172             }
173              
174             else {
175 0         0 croak(
176             "scale argument is not a scalar, an array of length $ndims,",
177             " or a piddle of shape ($ndims) or shape ($ndims,$ndims)\n"
178             );
179             }
180              
181             # apply a rotation to the scale matrix
182 50 100       6040 if ( defined $opt->{theta} ) {
183 1 50       5 croak( "theta may only be used for 2D PDFs\n" )
184             if $ndims != 2;
185              
186             my $R = pdl( [ cos( $opt->{theta} ), -sin( $opt->{theta} ) ],
187 1         9 [ sin( $opt->{theta} ), cos( $opt->{theta} ) ] );
188 1         113 $scale = $R->transpose x $scale x $R;
189             }
190              
191             return (
192 50         622 vectors => $vectors,
193             output => $output,
194             ndims => $ndims,
195             center => $center,
196             ndims => $ndims,
197             scale => $scale
198             );
199              
200             }
201              
202              
203             ############################################################################
204              
205 38     38   1420 sub _gamma { exp( ( lgamma( @_ ) )[0] ) }
206              
207             ############################################################################
208              
209             sub _genericND {
210              
211 41     41   147 my ( $xopts, $sub ) = ( shift, shift );
212              
213             # handle being called as a method or a function
214 41 50       92 my $self = eval { ref $_[0] && $_[0]->isa( 'PDL' ) } ? shift @_ : 'PDL';
  41 50       551  
215              
216             # handle options.
217 41 50       173 my $opt = 'HASH' eq ref $_[-1] ? pop( @_ ) : {};
218 41         501 my %opt = iparse( {
219             center => undef,
220             scale => 1,
221             transform => undef,
222             vectors => 0,
223             theta => undef,
224             %$xopts,
225             },
226             $opt
227             );
228              
229 41         26378 my %par = _handle_options( $self, \%opt, @_ );
230              
231             my $d2 = mahalanobis(
232             $par{vectors},
233             $par{scale},
234             {
235             squared => 1,
236             center => $par{center},
237 41         322 } );
238              
239 41         260 my $retval = $sub->( $d2, \%opt, \%par );
240              
241 41         67706 my $output = $par{output};
242              
243 41 100       142 if ( $opt{vectors} ) {
244 12         25 $output = $retval;
245             }
246              
247             else {
248 29         279 $output .= $retval->reshape( $output->dims );
249             }
250              
251             return wantarray
252             ? (
253             vals => $output,
254             center => $par{center},
255             scale => $par{scale},
256             transform => $par{transform} )
257 41 50       14159 : $output;
258              
259             }
260              
261             ############################################################################
262              
263             # from http://en.wikipedia.org/wiki/Cauchy_distribution#Multivariate_Cauchy_distribution
264              
265             # 1 + k
266             # Gamma(-----)
267             # 2
268             # ------------------------------------------------------------
269             # 1 + k
270             # -----
271             # 1 k/2 1/2 T -1 2
272             # Gamma(-) pi |S| (1 + (x - mu) S (x - mu))
273             # 2
274             #
275              
276              
277             sub _cauchyND {
278              
279 10     10   24 my ( $d2, $opt, $par ) = @_;
280              
281 10         35 my $k = $par->{ndims};
282              
283             my $pdf
284             = _gamma( ( 1 + $k ) / 2 )
285 10         44 / ( _gamma( 1 / 2 ) * PI**( $k / 2 ) * sqrt( determinant( $par->{scale} ) ) * ( 1 + $d2 )
286             **( ( 1 + $k ) / 2 ) );
287              
288 10 50       5700 return $opt->{log} ? log( $pdf ) : $pdf;
289             }
290              
291             sub cauchyND {
292              
293 10     10 1 341006 unshift @_, { log => 0 }, \&_cauchyND;
294 10         42 goto \&_genericND;
295             }
296              
297             *PDL::cauchyND = \&cauchyND;
298              
299             ############################################################################
300              
301             sub _gaussND {
302              
303 12     12   36 my ( $d2, $opt, $par ) = @_;
304              
305 12         26 my $tmp = $d2;
306              
307 12 50       54 if ( $opt->{norm} == 1 ) {
308              
309 12         97 $tmp += $par->{ndims} * log( 2 * PI ) + log( determinant( $par->{scale} ) );
310              
311             }
312              
313 12         10150 my $log_pdf = -$tmp / 2;
314              
315 12 50       38863 return $opt->{log} ? $log_pdf : exp( $log_pdf );
316              
317             }
318              
319             sub gaussND {
320              
321 12     12 1 1946226 unshift @_,
322             {
323             log => 0,
324             norm => 1,
325             },
326             \&_gaussND;
327 12         66 goto \&_genericND;
328             }
329              
330              
331             *PDL::gaussND = \&gaussND;
332              
333             ############################################################################
334              
335              
336             sub _lorentzND {
337              
338 10     10   34 my ( $d2, $opt, undef ) = @_;
339              
340 10         53 return 1 / ( 1 + $d2 );
341              
342             }
343              
344             sub lorentzND {
345              
346 10     10 1 452383 unshift @_, {}, \&_lorentzND;
347 10         50 goto \&_genericND;
348             }
349              
350              
351             *PDL::lorentzND = \&lorentzND;
352              
353             ############################################################################
354              
355              
356             sub _moffatND {
357              
358 9     9   30 my ( $d2, $opt, $par ) = @_;
359              
360             croak( "missing beta parameter\n" )
361 9 50       33 unless defined $opt->{beta};
362              
363 9         20 my $n = $par->{ndims};
364              
365 9         45 my $tmp = ( 1 + $d2 )**-$opt->{beta};
366              
367 9 50       605 if ( $opt->{norm} == 1 ) {
368              
369             # scale is a matrix with elements ~alpha**2
370             # det(scale) ~ (alpha**2)**$n
371             # sqrt(det(scale)) ~ alpha**$n
372              
373 9         48 my $alpha_n = sqrt( determinant( $par->{scale} ) );
374              
375 9         1386 $tmp *= _gamma( $opt->{beta} ) / _gamma( $opt->{beta} - $n / 2 ) / ( PI**( $n / 2 ) * $alpha_n );
376              
377             }
378              
379 9         647 return $tmp;
380             }
381              
382             sub moffatND {
383              
384 9     9 1 436225 unshift @_,
385             {
386             alpha => undef,
387             beta => undef,
388             norm => 1,
389             },
390             \&_moffatND;
391 9         41 goto \&_genericND;
392              
393             }
394              
395             *PDL::moffatND = \&moffatND;
396              
397             ############################################################################
398              
399              
400              
401             sub mahalanobis {
402              
403             # handle options.
404 50 50   50 1 728532 my $opt = 'HASH' eq ref $_[-1] ? pop( @_ ) : {};
405 50         353 my %opt = PDL::Options::iparse( {
406             center => undef,
407             inverted => 0,
408             squared => 0,
409             },
410             $opt
411             );
412              
413 50         25051 my ( $x, $scale, $out ) = @_;
414              
415 50         1394 my $xc;
416 50 100       175 if ( defined $opt{center} ) {
417 33         183 my $c = PDL::Core::topdl( $opt{center} );
418 33         476 $xc = $x - $c;
419             }
420             else {
421 17         41 $xc = $x;
422             }
423              
424             # may be passed a single vector; be nice
425 50 100       88282 my $m = $x->ndims > 1 ? ( $x->dims )[-1] : 1;
426              
427 50 50       374 $out = zeroes( double, $m )
428             unless defined $out;
429              
430             # if this is 1D, $scale may come in with shapes [], [1], or [1][1]
431             # we want the latter
432 50 100       11934 $scale = $scale->dummy( 1 ) if $scale->dims < 2;
433              
434             # invert the matrix if it hasn't already been inverted
435             $scale = $scale->inv
436 50 100       434 unless $opt{inverted};
437              
438 50         207585 inner2( $xc, $scale, $xc, $out );
439              
440 50 50       380 $out->inplace->sqrt unless $opt{squared};
441              
442 50         3257 return $out;
443             }
444              
445             *PDL::mahalanobis = \&mahalanobis;
446              
447             #
448             # This file is part of PDL-FuncND
449             #
450             # This software is Copyright (c) 2025 by Smithsonian Astrophysical Observatory.
451             #
452             # This is free software, licensed under:
453             #
454             # The GNU General Public License, Version 3, June 2007
455             #
456              
457             1;
458              
459             __END__