File Coverage

blib/lib/PDL/NDBin.pm
Criterion Covered Total %
statement 299 316 94.6
branch 119 154 77.2
condition 37 43 86.0
subroutine 43 43 100.0
pod 13 13 100.0
total 511 569 89.8


line stmt bran cond sub pod time code
1             package PDL::NDBin;
2             # ABSTRACT: Multidimensional binning & histogramming
3             $PDL::NDBin::VERSION = '0.019';
4 3     3   855484 use strict;
  3         16  
  3         88  
5 3     3   16 use warnings;
  3         6  
  3         94  
6 3     3   14 use Exporter;
  3         6  
  3         113  
7 3     3   16 use List::Util qw( reduce );
  3         6  
  3         191  
8 3     3   1824 use List::MoreUtils qw( pairwise );
  3         38622  
  3         28  
9 3     3   4742 use Math::Round qw( nlowmult nhimult );
  3         24234  
  3         184  
10 3     3   21 use PDL::Lite; # do not import any functions into this namespace
  3         6  
  3         20  
11 3     3   1709 use PDL::NDBin::Iterator;
  3         8  
  3         95  
12 3     3   1399 use PDL::NDBin::Actions_PP;
  3         6  
  3         46  
13 3     3   1843 use PDL::NDBin::Utils_PP;
  3         8  
  3         22  
14 3     3   1801 use Log::Any qw( $log );
  3         25409  
  3         14  
15 3     3   8559 use Data::Dumper;
  3         20688  
  3         195  
16 3     3   1603 use UUID::Tiny qw( :std );
  3         40478  
  3         574  
17 3     3   38 use POSIX qw( ceil );
  3         7  
  3         29  
18 3     3   1171 use Params::Validate qw( validate validate_pos validate_with ARRAYREF CODEREF HASHREF SCALAR );
  3         8  
  3         220  
19 3     3   20 use Carp;
  3         6  
  3         162  
20 3     3   1465 use Class::Load qw( load_class );
  3         36432  
  3         12943  
21              
22              
23             our @ISA = qw( Exporter );
24             our @EXPORT = qw( );
25             our @EXPORT_OK = qw( ndbinning ndbin );
26             our %EXPORT_TAGS = ( all => [ qw( ndbinning ndbin ) ] );
27              
28             # the list of valid keys
29             my %valid_key = map { $_ => 1 } qw( axes vars );
30              
31              
32             my @axis_params = qw( max min n round step grid );
33             my ( %axis_params, %axis_flags );
34             @axis_params{@axis_params} = (1) x @axis_params;
35             @axis_flags{@axis_params} = map { 1<<$_ } 0..@axis_params-1;
36              
37             my %axis_allowed =
38             map { reduce( sub { $a | $b }, 0, @axis_flags{@$_} ) => 1 }
39             [ ],
40             [ qw( n ) ],
41             [ qw( min ) ],
42             [ qw( max ) ],
43             [ qw( step ) ],
44             [ qw( min step ) ],
45             [ qw( max step ) ],
46             [ qw( min n ) ],
47             [ qw( max n ) ],
48             [ qw( round n ) ],
49             [ qw( round step ) ],
50             [ qw( n step ) ],
51             [ qw( n step round ) ],
52             [ qw( min n step ) ],
53             [ qw( max n step ) ],
54             [ qw( min max n ) ],
55             [ qw( min max step ) ],
56             [ qw( grid ) ];
57              
58             sub add_axis
59             {
60 272     272 1 6868 my $self = shift;
61 272         5162 my %params = validate( @_, {
62             max => 0,
63             min => 0,
64             n => 0,
65             name => 1,
66             pdl => 0,
67             round => 0,
68             step => 0,
69             grid => 0,
70             } );
71 269         1886 $log->tracef( 'adding axis with specs %s', \%params );
72              
73 269   100 954   2488 my $pmask = reduce { $a | ($b||0) } 0, @axis_flags{ keys %params };
  954         2388  
74             croak( "inconsistent or incomplete parameters: ", keys %params )
75 269 50       1197 unless $axis_allowed{ $pmask };
76              
77 269         490 push @{ $self->{axes} }, \%params;
  269         1105  
78             }
79              
80              
81             sub add_var
82             {
83 211     211 1 2231 my $self = shift;
84 211         3373 my %params = validate( @_, {
85             action => { type => CODEREF | HASHREF | SCALAR },
86             name => 1,
87             pdl => 0,
88             } );
89 211         1287 $log->tracef( 'adding variable with specs %s', \%params );
90 211         651 push @{ $self->{vars} }, \%params;
  211         633  
91             }
92              
93              
94             sub new
95             {
96 244     244 1 297097 my $class = shift;
97 244         4398 my %params = validate( @_, {
98             axes => { optional => 1, type => ARRAYREF },
99             vars => { optional => 1, type => ARRAYREF },
100             } );
101 244 50       1573 $log->debug( 'new: arguments = ' . Dumper \%params ) if $log->is_debug;
102 244         2719 my $self = bless { axes => [], vars => [] }, $class;
103             # axes
104 244   100     844 $params{axes} ||= []; # be sure we can dereference
105 244         349 my @axes = @{ $params{axes} };
  244         512  
106 244         489 for my $axis ( @axes ) {
107 187         319 my @pat = ( 1 ); # one mandatory argument
108 187 100       409 if( @$axis > 1 ) { push @pat, (0) x (@$axis - 1) } # followed by n-1 optional arguments
  177         444  
109 187         1366 my( $name ) = validate_pos( @$axis, @pat );
110 186         502 shift @$axis; # remove name
111 186         462 $self->add_axis( name => $name, @$axis );
112             }
113             # vars
114 240   100     811 $params{vars} ||= []; # be sure we can dereference
115 240         322 my @vars = @{ $params{vars} };
  240         483  
116 240         416 for my $var ( @vars ) {
117 108         644 my( $name, $action ) = validate_pos( @$var, 1, 1 );
118 108         324 $self->add_var( name => $name, action => $action );
119             }
120 240         1076 return $self;
121             }
122              
123              
124 1419 100   1419 1 20651 sub axes { wantarray ? @{ $_[0]->{axes} } : $_[0]->{axes} }
  1187         2486  
125 1595 100   1595 1 8436 sub vars { wantarray ? @{ $_[0]->{vars} } : $_[0]->{vars} }
  1133         2398  
126              
127             sub _make_instance_hashref
128             {
129 198     198   2926 my %params = validate_with(
130             params => \@_,
131             spec => {
132             N => 1,
133             class => 1,
134             coderef => 0,
135             },
136             allow_extra => 1,
137             );
138 198         1022 my $short_class = delete $params{class};
139 198 100       668 my $full_class = substr( $short_class, 0, 1 ) eq '+'
140             ? substr( $short_class, 1 )
141             : "PDL::NDBin::Action::$short_class";
142 198         676 load_class( $full_class );
143 198         14825 return $full_class->new( %params );
144             }
145              
146             sub _make_instance
147             {
148 198     198   2532 my %params = validate( @_, {
149             action => 1,
150             N => 1,
151             } );
152 198 100       1091 if( ref $params{action} eq 'CODE' ) {
    100          
153             return _make_instance_hashref(
154             class => '+PDL::NDBin::Action::CodeRef',
155             N => $params{N},
156             coderef => $params{action},
157 37         102 );
158             }
159             elsif( ref $params{action} eq 'HASH' ) {
160             return _make_instance_hashref(
161 4         17 %{ $params{action} },
162             N => $params{N},
163 4         8 );
164             }
165             else {
166             return _make_instance_hashref(
167             class => $params{action},
168             N => $params{N},
169 157         398 );
170             }
171             }
172              
173              
174             sub feed
175             {
176 242     242 1 11255 my $self = shift;
177 242         559 my %pdls = @_;
178 242         880 while( my( $name, $pdl ) = each %pdls ) {
179 266         440 for my $v ( $self->axes, $self->vars ) {
180 588 100       1866 $v->{pdl} = $pdl if $v->{name} eq $name;
181             }
182             }
183             }
184              
185             sub _check_all_pdls_present
186             {
187 232     232   336 my $self = shift;
188 232         273 my %warned_for;
189 232         381 for my $v ( $self->axes, $self->vars ) {
190 492 100       1015 next if defined $v->{pdl};
191 76 50       198 next if $v->{action} eq 'Count'; # those variables don't need data
192 0         0 my $name = $v->{name};
193 0 0       0 next if $warned_for{ $name };
194 0         0 $log->error( "no data for $name" );
195 0         0 $warned_for{ $name }++;
196             }
197             }
198              
199             sub _check_pdl_length
200             {
201 232     232   312 my $self = shift;
202             # checking whether the lengths of all axes and variables are equal can
203             # only be done here (in a loop), and not in autoscale_axis()
204 232         310 my $length;
205 232         337 for my $v ( $self->axes, $self->vars ) {
206 492 100       1442 $length = $v->{pdl}->nelem unless defined $length;
207             # variables don't always need a pdl, or may be happy with a
208             # null pdl; let the action figure it out.
209             # note that the test isempty() is not a good test for null
210             # pdls, but until I have a better one, this will have to do
211 492 100 100     1622 next if $v->{action} && ( ! defined $v->{pdl} || $v->{pdl}->isempty );
      100        
212 400 50       1890 if( $v->{pdl}->nelem != $length ) {
213             croak( join '', 'number of elements (',
214 0         0 $v->{pdl}->nelem, ") of '$v->{name}'",
215             " is different from previous ($length)" );
216             }
217             }
218             }
219              
220              
221             sub autoscale_axis
222             {
223 262     262 1 674 my $axis = shift;
224             # return early if step, min, and n have already been calculated
225 262 100 100     1090 if( defined $axis->{step} && defined $axis->{min} && defined $axis->{n} ) {
      100        
226 157         423 $log->tracef( 'step, min, n already calculated for %s; not recalculating', $axis );
227 157         547 return;
228             }
229             # first get & sanify the arguments
230 105 50       235 croak( 'need coordinates' ) unless defined $axis->{pdl};
231             # return if axis is empty
232 105 100       287 if( $axis->{pdl}->isempty ) {
233 1         12 $axis->{n} = 0;
234 1         3 return;
235             }
236              
237             # return early if a grid has been supplied
238 104 100       683 if( defined $axis->{grid} ) {
239              
240 9         27 $axis->{grid} = PDL::Core::topdl( $axis->{grid} );
241             croak( "grid supplied for %s must be one-dimensional with at least two elements", $axis )
242 9 50 33     173 if $axis->{grid}->nelem < 2 || $axis->{grid}->ndims > 1;
243 9         83 _validate_grid( $axis->{grid} );
244             # number of bins is one less than number of bin edges
245 6         22 $axis->{n} = $axis->{grid}->nelem - 1;
246 6         20 $log->tracef( 'grid supplied for %s; no need to autoscale', $axis );
247 6         24 return;
248             }
249              
250              
251 95 100       337 $axis->{min} = $axis->{pdl}->min unless defined $axis->{min};
252              
253              
254 95 100       4103 $axis->{max} = $axis->{pdl}->max unless defined $axis->{max};
255 95 100 66     3226 if( defined $axis->{round} and $axis->{round} > 0 ) {
256 2         11 $axis->{min} = nlowmult( $axis->{round}, $axis->{min} );
257 2         32 $axis->{max} = nhimult( $axis->{round}, $axis->{max} );
258             }
259 95 50       253 croak( 'max < min is invalid' ) if $axis->{max} < $axis->{min};
260 95 100       247 if( $axis->{pdl}->type >= PDL::float ) {
261 64 100       2346 croak( 'cannot bin with min = max' ) if $axis->{min} == $axis->{max};
262             }
263             # calculate the range
264             # for floating-point data, we need to augment the range by 1 unit - see
265             # the discussion under IMPLEMENTATION NOTES for more details
266 94         1272 my $range = $axis->{max} - $axis->{min};
267 94 100       198 if( $axis->{pdl}->type < PDL::float ) {
268 31         820 $range += 1;
269             }
270             # if step size has been supplied by user, check it
271 94 100       1862 if( defined $axis->{step} ) {
272 23 50       64 croak( 'step size must be > 0' ) unless $axis->{step} > 0;
273 23 50 66     57 if( $axis->{pdl}->type < PDL::float && $axis->{step} < 1 ) {
274 0         0 croak( "step size = $axis->{step} < 1 is not allowed when binning integral data" );
275             }
276             }
277             # number of bins I
278 94 100       823 if( defined $axis->{n} ) {
279 46 50       109 croak( 'number of bins must be > 0' ) unless $axis->{n} > 0;
280 46 50       226 croak( 'number of bins must be integral' ) if ceil( $axis->{n} ) - $axis->{n} > 0;
281             }
282             else {
283 48 100       95 if( defined $axis->{step} ) {
284             # data range and step size were verified above,
285             # so the result of this calculation is
286             # guaranteed to be > 0
287 23         149 $axis->{n} = ceil( $range / $axis->{step} );
288             }
289             else {
290             # if neither number of bins nor step size is defined,
291             # use some reasonable default (which used to be the
292             # behaviour of hist() in versions of PDL inferior to
293             # 2.4.12) (see F)
294 25 100       111 $axis->{n} = $axis->{pdl}->nelem > 100 ? 100 : $axis->{pdl}->nelem;
295             }
296             }
297             # step size I
298             # if we get here, the data range is certain to be larger than
299             # zero, and I is sure to be defined and valid (either
300             # because it was supplied explicitly and verified to be valid,
301             # or because it was calculated automatically)
302 94 100       245 if( ! defined $axis->{step} ) {
303             # result of this calculation is guaranteed to be > 0
304 71         167 $axis->{step} = $range / $axis->{n};
305 71 100       156 if( $axis->{pdl}->type < PDL::float ) {
306 23 100       699 croak( 'there are more bins than distinct values' ) if $axis->{step} < 1;
307             }
308             }
309             }
310              
311              
312             sub autoscale
313             {
314 232     232 1 329 my $self = shift;
315 232         519 $self->feed( @_ );
316 232         566 $self->_check_all_pdls_present;
317 232         538 $self->_check_pdl_length;
318 232         545 autoscale_axis( $_ ) for $self->axes;
319             }
320              
321              
322             sub labels
323             {
324 8     8 1 578 my $self = shift;
325 8         25 $self->autoscale( @_ );
326             my @list = map {
327 8         44 my $axis = $_;
  10         57  
328              
329 10 100       19 if ( defined $axis->{grid} ) {
330              
331             [ map {
332 8         84 { range => [ $axis->{grid}->at($_), $axis->{grid}->at($_+1) ] }
333 3         12 } 0..$axis->{grid}->nelem -2
334             ]
335              
336             }
337              
338             else {
339              
340 7         11 my ( $pdl, $min, $step ) = @{ $axis }{ qw( pdl min step ) };
  7         17  
341             [ map {
342             { # anonymous hash
343 20 100       518 range => $pdl->type() >= PDL::float()
    100          
344             ? [ $min + $step*$_, $min + $step*($_+1) ]
345             : $step > 1
346             ? [ nhimult( 1, $min + $step*$_ ), nhimult( 1, $min + $step*($_+1) - 1 ) ]
347             : $min + $step*$_
348             }
349 7         18 } 0 .. $axis->{n}-1 ];
350              
351             }
352              
353             } $self->axes;
354              
355 16 50       320 return wantarray ? @list : \@list;
356             }
357              
358              
359             sub process
360             {
361 222     222 1 3230 my $self = shift;
362              
363             # sanity check
364 222 100       284 croak( 'no axes supplied' ) unless @{ $self->axes };
  222         466  
365             # default action, when no variables are given, is to produce a histogram
366 220 100       289 $self->add_var( name => 'histogram', action => 'Count' ) unless @{ $self->vars };
  220         392  
367              
368             #
369 220         599 $self->autoscale( @_ );
370              
371             # process axes
372 215         1233 my $idx = 0; # flattened bin number
373 215         295 my @n; # number of bins in each direction
374             # find the last axis and flatten all axes into one dimension, working
375             # our way backwards from the last to the first axis
376 215         374 for my $axis ( reverse $self->axes ) {
377 242 50       668 if ( $log->is_debug ) {
378 0         0 $log->debug( 'input (' . $axis->{pdl}->info . ') = ' . $axis->{pdl} );
379 0 0       0 if ( ! defined $axis->{grid} ) {
380 0         0 $log->debug( "bin with parameters step=$axis->{step}, min=$axis->{min}, n=$axis->{n}" );
381             }
382             else {
383 0         0 $log->debug( "bin with parameters grid=$axis->{grid}" );
384             }
385             }
386 242 100       2045 croak( 'I cannot bin unless n > 0' ) unless $axis->{n} > 0;
387 241         498 unshift @n, $axis->{n}; # remember that we are working backwards!
388 241 100       473 if ( defined $axis->{grid} ) {
389 3         159 $idx = $axis->{pdl}->_flatten_into_grid( $idx, $axis->{grid} );
390             }
391             else {
392 238         4925 $idx = $axis->{pdl}->_flatten_into( $idx, $axis->{step}, $axis->{min}, $axis->{n} );
393             }
394             }
395 214 50       703 $log->debug( 'idx (' . $idx->info . ') = ' . $idx ) if $log->is_debug;
396 214         1695 $self->{n} = \@n;
397              
398 214     27   1384 my $N = reduce { $a * $b } @n; # total number of bins
  27         61  
399 214 50       793 croak( 'I need at least one bin' ) unless $N;
400 214         450 my @vars = map $_->{pdl}, $self->vars;
401 214   100     769 $self->{instances} ||= [ map { _make_instance( N => $N, action => $_->{action} ) } $self->vars ];
  198         442  
402              
403             #
404             {
405 214         381 local $Data::Dumper::Terse = 1;
  214         381  
406 214         626 $log->trace( 'process: $self = ' . Dumper $self );
407             }
408              
409             # now visit all the bins
410 214         28927 my $iter = PDL::NDBin::Iterator->new( bins => \@n, array => \@vars, idx => $idx );
411 214         611 $log->debug( 'iterator object created: ' . Dumper $iter );
412 214         19341 while( $iter->advance ) {
413 396         1016 my $i = $iter->var;
414 396         1099 $self->{instances}->[ $i ]->process( $iter );
415             }
416              
417 211         1448 return $self;
418             }
419              
420              
421             sub output
422             {
423 187     187 1 1128 my $self = shift;
424 187 50       427 return unless defined wantarray;
425 187 100       453 unless( defined $self->{result} ) {
426             # reshape output
427 181         299 my $n = $self->{n};
428 181         237 my @output = map { $_->result } @{ $self->{instances} };
  186         463  
  181         427  
429 181         413 for my $pdl ( @output ) { $pdl->reshape( @$n ) }
  186         706  
430 181 50       7632 if( $log->is_debug ) { $log->debug( 'output: output (' . $_->info . ') = ' . $_ ) for @output }
  0         0  
431 181     186   2045 $self->{result} = { pairwise { $a->{name} => $b } @{ $self->vars }, @output };
  186         765  
  181         740  
432 181 50       751 if( $log->is_debug ) { $log->debug( 'output: result = ' . Dumper $self->{result} ) }
  0         0  
433             }
434 187 50       1758 return wantarray ? %{ $self->{result} } : $self->{result};
  0         0  
435             }
436              
437              
438             sub _consume (&\@)
439             {
440 94     94   186 my ( $f, $list ) = @_;
441 94         241 for my $i ( 0 .. $#$list ) {
442 310         585 local *_ = \$list->[$i];
443 310 100       459 if( not $f->() ) { return splice @$list, 0, $i }
  32         158  
444             }
445             # If we get here, either the list is empty, or all values in the list
446             # meet the condition. In either case, splicing the entire list does
447             # what we want.
448 62         241 return splice @$list;
449             }
450              
451             sub _expand_axes
452             {
453 52     52   80 my ( @out, $hash, @num );
454 52         106 while( @_ ) {
455 97 100       123 if( eval { $_[0]->isa('PDL') } ) {
  97 100       416  
    100          
456             # a new axis; push the existing one on the output list
457 58 50       131 push @out, $hash if $hash;
458 58         183 $hash = { pdl => shift };
459             }
460             elsif( ref $_[0] eq 'HASH' ) {
461             # the user has supplied a hash directly, which may or
462             # may not yet contain a key-value pair pdl => $pdl
463 26 50       60 $hash = { } unless $hash;
464 26         68 push @out, { %$hash, %{ +shift } };
  26         72  
465 26         92 undef $hash; # do not collapse consecutive hashes into one, too confusing
466             }
467 30     30   351 elsif( @num = _consume { /^[-+]?(\d+(\.\d*)?|\.\d+)([Ee][-+]?\d+)?$/ } @_ ) {
468 8 50       20 croak( 'no axis given' ) unless $hash;
469 8 100       34 croak( "too many arguments to axis in `@num'" ) if @num > 3;
470             # a series of floating-point numbers
471 7 50       24 $hash->{min} = $num[0] if @num > 0;
472 7 50       18 $hash->{max} = $num[1] if @num > 1;
473 7 50       39 $hash->{step} = $num[2] if @num > 2;
474             }
475             #elsif( @num = ( $_[0] =~ m{^((?:\d+(?:\.\d*)?|\.\d+)(?:[Ee][-+]?\d+)?/)+$}g ) and shift ) {
476             # DOES NOT WORK YET - TODO
477             # print "GMT-style axis spec found! (@num)\n";
478             # croak( 'no axis given' ) unless $hash;
479             # croak( "too many arguments to axis in `@num'" ) if @num > 3;
480             # # a string specification of the form 'min/max/step', a la GMT
481             # $hash->{min} = $num[0] if @num > 0;
482             # $hash->{max} = $num[1] if @num > 1;
483             # $hash->{step} = $num[2] if @num > 2;
484             #}
485             else {
486 5         60 croak( "while expanding axes: invalid argument at `@_'" );
487             }
488             }
489 46 100       183 push @out, $hash if $hash;
490 46         102 return @out;
491             }
492              
493              
494 115     115   319 sub _random_name { create_uuid( UUID_RANDOM ) }
495              
496              
497             sub ndbinning
498             {
499             #
500 26     26 1 17004 my $binner = __PACKAGE__->new;
501              
502             # leading arguments are axes and axis specifications
503             #
504             # PDL overloads the `eq' and `ne' operators; by checking for a PDL
505             # first, we avoid (invalid) comparisons between piddles and strings in
506             # the `grep'
507 26 100   148   105 my @leading = _consume { eval { $_->isa('PDL') } || ! $valid_key{ $_ } } @_;
  148         179  
  148         903  
508              
509             # consume and process axes
510             # axes require three numerical specifications following it
511 26   66     127 while( @leading > 3 && eval { $leading[0]->isa('PDL') } && ! grep ref, @leading[ 1 .. 3 ] ) {
  32   100     229  
512 31         102 my( $pdl, $step, $min, $n ) = splice @leading, 0, 4;
513 31         72 $binner->add_axis( name => _random_name, pdl => $pdl, step => $step, min => $min, n => $n );
514             }
515 26 100       55 if( @leading ) { croak( "error parsing arguments in `@leading'" ) }
  7         35  
516              
517             # remaining arguments are key => value pairs
518 19         43 my $args = { @_ };
519 19         55 my @invalid_keys = grep ! $valid_key{ $_ }, keys %$args;
520 19 50       35 croak( "invalid key(s) @invalid_keys" ) if @invalid_keys;
521              
522             # axes
523 19   50     88 $args->{axes} ||= [];
524 19         29 my @axes = @{ $args->{axes} };
  19         33  
525 19         34 for my $axis ( @axes ) {
526 0         0 my $pdl = shift @$axis;
527 0         0 $binner->add_axis( name => _random_name, pdl => $pdl, @$axis );
528             }
529              
530             # variables
531 19   100     58 $args->{vars} ||= [];
532 19         21 for my $var ( @{ $args->{vars} } ) {
  19         40  
533 13 50       32 if( @$var == 2 ) {
534 13         27 my( $pdl, $action ) = @$var;
535 13         27 $binner->add_var( name => _random_name, pdl => $pdl, action => $action );
536             }
537 0         0 else { croak( "wrong number of arguments for var: @$var" ) }
538             }
539              
540             #
541 19         115 $binner->process;
542 18         49 my $output = $binner->output;
543 18         29 my @result = map $output->{ $_->{name} }, @{ $binner->vars };
  18         28  
544 18 50       232 return wantarray ? @result : $result[0];
545             }
546              
547              
548             sub ndbin
549             {
550             #
551 55     55 1 43752 my $binner = __PACKAGE__->new;
552              
553             # leading arguments are axes and axis specifications
554             #
555             # PDL overloads the `eq' and `ne' operators; by checking for a PDL
556             # first, we avoid (invalid) comparisons between piddles and strings in
557             # the `grep'
558 55 100   132   250 if( my @leading = _consume { eval { $_->isa('PDL') } || ! $valid_key{ $_ } } @_ ) {
  132 100       491  
  132         905  
559 52         129 my @axes = _expand_axes( @leading );
560 46         157 $binner->add_axis( name => _random_name, %$_ ) for @axes;
561             }
562              
563             # remaining arguments are key => value pairs
564 49         181 my $args = { @_ };
565 49         148 my @invalid_keys = grep ! $valid_key{ $_ }, keys %$args;
566 49 50       109 croak( "invalid key(s) @invalid_keys" ) if @invalid_keys;
567              
568             # axes
569 49   100     212 $args->{axes} ||= [];
570 49         84 my @axes = @{ $args->{axes} };
  49         79  
571 49         100 for my $axis ( @axes ) {
572 3         6 my $pdl = shift @$axis;
573 3         7 $binner->add_axis( name => _random_name, pdl => $pdl, @$axis );
574             }
575              
576             # variables
577 49   100     162 $args->{vars} ||= [];
578 49         64 for my $var ( @{ $args->{vars} } ) {
  49         93  
579 16 50       33 if( @$var == 2 ) {
580 16         34 my( $pdl, $action ) = @$var;
581 16         33 $binner->add_var( name => _random_name, pdl => $pdl, action => $action );
582             }
583 0         0 else { croak( "wrong number of arguments for var: @$var" ) }
584             }
585              
586 49         124 $binner->process;
587 42         107 my $output = $binner->output;
588 42         65 my @result = map $output->{ $_->{name} }, @{ $binner->vars };
  42         71  
589 42 50       451 return wantarray ? @result : $result[0];
590             }
591              
592             1;
593              
594             __END__