##-*- Mode: CPerl -*-
##======================================================================
## Header Administrivia
##======================================================================
use PDL::VectorValued::Dev;
my $VERSION = '1.24.1'; ##-- update with perl-reversion from Perl::Version module
pp_setversion($VERSION);
##------------------------------------------------------
## pm headers
pp_addpm({At=>'Top'},<<'EOPM');
=pod
=head1 NAME
PDL::CCS::Ufunc - Ufuncs for compressed storage sparse PDLs
=head1 SYNOPSIS
use PDL;
use PDL::CCS::Ufunc;
##---------------------------------------------------------------------
## ... stuff happens
=cut
EOPM
## /pm headers
##------------------------------------------------------
##------------------------------------------------------
## Exports: None
#pp_export_nothing();
##------------------------------------------------------
## Includes / defines
pp_addhdr(<<'EOH');
#include <math.h>
#include "../Utils/ccsutils.h"
#ifndef INFINITY
# define INFINITY (1.0/0.0)
#endif
EOH
##------------------------------------------------------
## integer types etc.
require "../Config.pm";
##======================================================================
## C Utilities
##======================================================================
# (none)
##======================================================================
## PDL::PP Wrappers
##======================================================================
##======================================================================
## Operations: Accumulators (Ufuncs)
##======================================================================
##--------------------------------------------------------------
## Operations: Accumulators (Ufuncs): Generic
## %vvpp_def_hash = ccs_accum_hash($op_codename, $op_docname, %args)
## + known %args
## out_type => $pptype_or_undef, ##-- set type of output $nzvals (default: match input $nzvals)
## init_missingOut => $ppcode_or_undef, ##-- sets value missingOut: default: 'missingOut=missingVal;'
## init_code => $ppcode_or_undef, ##-- misc initialization
## tmp_type => $ppcode_or_undef, ##-- default: $GENERIC(nzvalsOut)
## tmp_addmissing => $ppcode_or_undef, ##-- updates C var 'tmp' before insertion (may reference nMissing)
## tmp_addval => $ppcode_or_undef, ##-- add PP value $nzvalsIn(NnzIn=>nnzii) to tmp
## tmp_reset => $ppcode_or_undef, ##-- reset tmp on index change (default: tmp=$nzvalsIn(NnzIn=>nnzii)) --> QUITE USELESS
## tmp_init => $ppcode_or_undef, ##-- special case for $tmp_reset with nzii==0; default CROAKs if NnzIn==0
## doc_addmissing => $addmissing_doc, ##-- doc for 'addmissing'
## setbad => $ppcode_or_undef, ##-- post-broadcastloop bad-handling code
## extra => \%extraPPArgs, ##-- extra args for vvpp_def()
sub ccs_accum_hash {
my ($op_codename,$op_docname,%args) = @_;
return (
Pars => ("\n "
.join(";\n ",
"indx ixIn(Ndims,NnzIn)", ##-- sorted nz-indices of projected dimensions (1..Ndims), with repeats
' nzvalsIn(NnzIn)', ##-- all nz-values
' missing()', ##-- missing value
"indx N()", ##-- size of 0th dimension (<=0 to ignore missing values)
"indx [o]ixOut(Ndims,NnzOut)", ##-- unique indices of projected dimensions
(
($args{out_type} ? ($args{out_type}.' ') : ' ').'[o]nzvalsOut(NnzOut)'
), ##-- unique nz-values of projected dims which contain >=1 input nz
"indx [o]nOut()", ##-- number of unique output index-vectors
'')),
HandleBad => ($args{HandleBad} // 1),
##-- pmcode
PMCode => q(
sub PDL::ccs_accum_).${op_codename}.q( {
my ($ixIn,$nzvalsIn, $missing,$N, $ixOut,$nzvalsOut,$nOut) = @_;
$nOut //= PDL->null;
$ixOut //= PDL->null;
$nzvalsOut //= PDL->null;
&PDL::_ccs_accum_).${op_codename}.q(_int($ixIn,$nzvalsIn, $missing,$N, $ixOut,$nzvalsOut,$nOut);
##
##-- auto-trim
my $trim_slice = "0:".($nOut->max-1);
$ixOut = $ixOut->slice(",$trim_slice");
$nzvalsOut = $nzvalsOut->slice($trim_slice);
##
##-- return
return wantarray ? ($ixOut,$nzvalsOut,$nOut) : $nzvalsOut;
}
),
##-- dimension-twiddling via RedoDimsCode
RedoDimsCode => q{
if ( CCS_PDL_IS_NULL($PDL(ixOut)) && CCS_PDL_IS_NULL($PDL(nzvalsOut)) ) {
/*-- strangely, SIZE(NnzOut)==1 here if both ixOut and nzvalsOut are passed as null --*/
$SIZE(NnzOut) = $SIZE(NnzIn);
}
},
##-- pp code
Code => q(
int cmpval, carp_unsorted=0;
broadcastloop %{
PDL_Indx nnzii_prev=-1, nnzii=0, nnzoi=0;
PDL_Indx sizeNnzIn=$SIZE(NnzIn), sizeNnzOut=$SIZE(NnzOut), nMissing, nMissingInit;
PDL_Indx ival1,ival2;
$GENERIC(nzvalsOut) missingOut;
$GENERIC(nzvalsIn) missingVal = $missing();
).($args{decls} ? $args{decls} : '').q(
).($args{tmp_type} ? $args{tmp_type} : '$GENERIC(nzvalsOut)').q( tmp;
//
//-- init
).($args{init_code}||'').q(
).($args{init_missingOut} || 'missingOut = missingVal;').q(
nMissingInit = $N()-1;
nMissing = nMissingInit;
).(defined($args{tmp_init}) ? $args{tmp_init}
: (defined($args{tmp_reset}) ? $args{tmp_reset}
: 'if ($SIZE(NnzIn) == 0) $CROAK("called with empty nzvalsIn"); tmp = $nzvalsIn(NnzIn=>0);')
).q( /* initialize tmp */
//
//-- loop
for (nnzii_prev=0,nnzii=1; nnzii<sizeNnzIn && nnzoi<sizeNnzOut; nnzii_prev=nnzii++) {
$CMPVEC('$ixIn(NnzIn=>nnzii)','$ixIn(NnzIn=>nnzii_prev)','Ndims','cmpval',var1=>'ival1',var2=>'ival2');
if (cmpval > 0) {
//-- CASE: ix > ix_prev : insert accumulated value
).($args{tmp_addmissing}||"").q(
//-- always insert output value
loop (Ndims) %{ $ixOut(NnzOut=>nnzoi) = $ixIn(NnzIn=>nnzii_prev); %}
$nzvalsOut(NnzOut=>nnzoi) = tmp;
nnzoi++;
//
// ... and reset temps
).(defined($args{tmp_reset}) ? $args{tmp_reset} : 'tmp = $nzvalsIn(NnzIn=>nnzii);').q( /* reset tmp */
nMissing = nMissingInit;
}
else if (cmpval <= 0) {
// CASE: ix >= ix_prev : accumulate to temps
).($args{tmp_addval}||'').q(;
nMissing--;
if (cmpval < 0) { carp_unsorted=1; } /*-- CASE: ix < ix_prev : GARBAGE (treat as equal) --*/
}
}
//
//-- sanity check).'
if (nnzii<sizeNnzIn) {
warn("PDL::ccs_accum_'.${op_codename}.'(): too few output values provided: some input values were ignored");
}'.q(
//
//-- set final computed output values
).($args{tmp_addmissing}||'').q(
if (nnzii_prev >= 0 && nnzii_prev < $SIZE(NnzIn)) {
loop (Ndims) %{ $ixOut(NnzOut=>nnzoi) = $ixIn(NnzIn=>nnzii_prev); %}
}
if ($SIZE(NnzOut) > nnzoi) {
$nzvalsOut(NnzOut=>nnzoi) = tmp;
nnzoi++;
}
$nOut() = nnzoi;
//
//-- set any remaining output values to 0 (indices) or "N*missing" (values)
for ( ; nnzoi<sizeNnzOut; nnzoi++) {
loop (Ndims) %{ $ixOut(NnzOut=>nnzoi) = 0; %}
$nzvalsOut(NnzOut=>nnzoi) = missingOut;
}
%}
//
//-- carp?).'
if (carp_unsorted) {
warn("PDL::ccs_accum_'.${op_codename}.'(): unsorted input vector list detected: output will be incorrect");
}'.q(
//
//-- set BAD-flags
).($args{setbad} || q(
if ($PDLSTATEISBAD(ixIn)) { $PDLSTATESETBAD(ixOut); } else { $PDLSTATESETGOOD(ixOut); }
if ($PDLSTATEISBAD(nzvalsIn)) { $PDLSTATESETBAD(nzvalsOut); } else { $PDLSTATESETGOOD(nzvalsOut); }
)).q(
//-- END
),
##-- docs
Doc => q(
Accumulated ).${op_docname}.q( over values $nzvalsIn() associated with non-missing vector-valued keys $ixIn().
On return,
$ixOut() holds the unique non-"missing" values of $ixIn(),
$nzvalsOut() holds the associated values,
and
$nOut() stores the number of unique non-missing values computed.
).($args{doc_addmissing}||'').q(
Returned PDLs are implicitly sliced such that NnzOut==$nOut().
In scalar context, returns only $nzvalsOut().
),
($args{extra} ? %{$args{extra}} : qw()),
); ##--/ccs_accum_hash: return
} ##--/ccs_accum_hash: sub
sub ccs_accum_def {
vvpp_def(('ccs_accum_'.$_[0]), ccs_accum_hash(@_));
}
##--------------------------------------------------------------
## Operations: Accumulators (Ufuncs): prod
ccs_accum_def('prod', 'product',
init_missingOut=>'if ($N() > 0) { missingOut = pow(missingVal, $N()); } else { missingOut = missingVal; }',
tmp_init => 'tmp = $SIZE(NnzIn) == 0 ? 1 : $nzvalsIn(NnzIn=>0);',
tmp_addmissing =>'if (nMissing > 0) { tmp *= pow(missingVal, nMissing); }',
tmp_addval =>'tmp *= $nzvalsIn(NnzIn=>nnzii);',
doc_addmissing => <<'EOMD',
If $N() is specified and greater than zero, then the quantity:
$missing ** ($N - (rlevec($ixIn))[0])
is multiplied into $nzvalsOut: this is probably What You Want if you are computing the product over a virtual
dimension in a sparse index-encoded PDL (see PDL::CCS::Nd for a wrapper class).
EOMD
);
##--------------------------------------------------------------
## Operations: Accumulators (Ufuncs): dprod
ccs_accum_def('dprod', 'double-precision product',
out_type =>'double',
init_missingOut=>'if ($N() > 0) { missingOut = pow(missingVal, $N()); } else { missingOut = missingVal; }',
tmp_init => 'tmp = $SIZE(NnzIn) == 0 ? 1 : $nzvalsIn(NnzIn=>0);',
tmp_addmissing =>'if (nMissing > 0) { tmp *= pow(missingVal, nMissing); }',
tmp_addval =>'tmp *= $nzvalsIn(NnzIn=>nnzii);',
doc_addmissing => <<'EOMD',
If $N() is specified and greater than zero, then the quantity:
$missing ** ($N - (rlevec($ixIn))[0])
is multiplied into $nzvalsOut: this is probably What You Want if you are computing the product over a virtual
dimension in a sparse index-encoded PDL (see PDL::CCS::Nd for a wrapper class).
EOMD
);
##--------------------------------------------------------------
## Operations: Accumulators (Ufuncs): sum
ccs_accum_def('sum', 'sum',
init_missingOut=>'if ($N() > 0) { missingOut = $N() * missingVal; } else { missingOut = missingVal; }',
tmp_init => 'tmp = $SIZE(NnzIn) == 0 ? 0 : $nzvalsIn(NnzIn=>0);',
tmp_addmissing =>'if (nMissing > 0) { tmp += nMissing * missingVal; }',
tmp_addval =>'tmp += $nzvalsIn(NnzIn=>nnzii);',
doc_addmissing => <<'EOMD',
If $N() is specified and greater than zero, then the quantity:
$missing * ($N - (rlevec($ixIn))[0])
is added to $nzvalsOut: this is probably What You Want if you are summing over a virtual
dimension in a sparse index-encoded PDL (see PDL::CCS::Nd for a wrapper class).
EOMD
);
##--------------------------------------------------------------
## Operations: Accumulators (Ufuncs): dsum
ccs_accum_def('dsum', 'double-precision sum',
out_type =>'double',
init_missingOut=>'if ($N() > 0) { missingOut = $N() * missingVal; } else { missingOut = missingVal; }',
tmp_init => 'tmp = $SIZE(NnzIn) == 0 ? 0.0 : $nzvalsIn(NnzIn=>0);',
tmp_addmissing =>'if (nMissing > 0) { tmp += nMissing * missingVal; }',
tmp_addval =>'tmp += $nzvalsIn(NnzIn=>nnzii);',
doc_addmissing => <<'EOMD',
If $N() is specified and greater than zero, then the quantity:
$missing * ($N - (rlevec($ixIn))[0])
is added to $nzvalsOut: this is probably What You Want if you are summing over a virtual
dimension in a sparse index-encoded PDL (see PDL::CCS::Nd for a wrapper class).
EOMD
);
##--------------------------------------------------------------
## Operations: Accumulators (Ufuncs): or
ccs_accum_def('or', 'logical "or"',
tmp_type => 'signed char',
init_missingOut=>'missingOut = missingVal;',
tmp_addmissing =>'if (nMissing > 0) { tmp = tmp || missingVal; } tmp = !!tmp; /* canonicalize */',
tmp_addval =>'tmp = tmp || $nzvalsIn(NnzIn=>nnzii);',
doc_addmissing => <<'EOMD',
If $N() is specified and greater than zero, $missing() is logically (or)ed
into each result value at each output index with a run length of less than $N() in $ixIn().
This is probably What You Want.
EOMD
);
##--------------------------------------------------------------
## Operations: Accumulators (Ufuncs): and
ccs_accum_def('and', 'logical "and"',
tmp_type => 'signed char',
init_missingOut=>'missingOut = missingVal;',
tmp_addmissing =>'if (nMissing > 0) { tmp = tmp && missingVal; } tmp = !!tmp; /* canonicalize */',
tmp_addval =>'tmp = tmp && $nzvalsIn(NnzIn=>nnzii);',
doc_addmissing => <<'EOMD',
If $N() is specified and greater than zero, $missing() is logically (and)ed
into each result value at each output index with a run length of less than $N() in $ixIn().
This is probably What You Want.
EOMD
);
##--------------------------------------------------------------
## Operations: Accumulators (Ufuncs): bor
ccs_accum_def('bor', 'bitwise "or"',
extra => { GenericTypes=>$PDL::CCS::Config::ccsConfig{INT_TYPE_CHRS} },
init_missingOut=>'missingOut = missingVal;',
tmp_addmissing =>'if (nMissing > 0) { tmp |= missingVal; }',
tmp_addval =>'tmp |= $nzvalsIn(NnzIn=>nnzii);',
doc_addmissing => <<'EOMD',
If $N() is specified and greater than zero, $missing() is bitwise (or)ed
into each result value at each output index with a run length of less than $N() in $ixIn().
This is probably What You Want.
EOMD
);
##--------------------------------------------------------------
## Operations: Accumulators (Ufuncs): band
ccs_accum_def('band', 'bitwise "and"',
extra => { GenericTypes=>$PDL::CCS::Config::ccsConfig{INT_TYPE_CHRS} },
init_missingOut=>'missingOut = missingVal;',
tmp_addmissing =>'if (nMissing > 0) { tmp &= missingVal; }',
tmp_addval =>'tmp &= $nzvalsIn(NnzIn=>nnzii);',
doc_addmissing => <<'EOMD',
If $N() is specified and greater than zero, $missing() is bitwise (and)ed
into each result value at each output index with a run length of less than $N() in $ixIn().
This is probably What You Want.
EOMD
);
##--------------------------------------------------------------
## Operations: Accumulators (Ufuncs): maximum
ccs_accum_def('maximum', 'maximum',
decls =>'$GENERIC(nzvalsIn) curval;',
init_missingOut=>'missingOut = missingVal;',
tmp_addmissing =>'if (nMissing > 0 && missingVal > tmp) { tmp = missingVal; }',
tmp_addval =>'curval=$nzvalsIn(NnzIn=>nnzii); if (curval>tmp) tmp=curval;',
doc_addmissing => <<'EOMD',
If $N() is specified and greater than zero,
and if $missing() is greater than any listed value for a vector key with a run-length
of less than $N(), then $missing() is used as the output value for that key.
This is probably What You Want.
EOMD
);
##--------------------------------------------------------------
## Operations: Accumulators (Ufuncs): minimum
ccs_accum_def('minimum', 'minimum',
decls =>'$GENERIC(nzvalsIn) curval;',
init_missingOut=>'missingOut = missingVal;',
tmp_addmissing =>'if (nMissing > 0 && missingVal < tmp) { tmp = missingVal; }',
tmp_addval =>'curval=$nzvalsIn(NnzIn=>nnzii); if (curval<tmp) tmp=curval;',
doc_addmissing => <<'EOMD',
If $N() is specified and greater than zero,
and if $missing() is less than any listed value for a vector key with a run-length
of less than $N(), then $missing() is used as the output value for that key.
This is probably What You Want.
EOMD
);
##--------------------------------------------------------------
## Operations: Accumulators (Ufuncs): maximum_nz_ind ~ maximum_ind
ccs_accum_def('maximum_nz_ind', 'maximum_nz_ind',
out_type =>'indx',
out_type_perl =>'indx',
decls =>'$GENERIC(nzvalsIn) curval, bestval;',
init_missingOut=>'missingOut = -1;',
tmp_addmissing =>'if (nMissing > 0 && $ISGOOD(missing()) && missingVal > bestval) { tmp=missingOut; }',
tmp_addval =>'curval=$nzvalsIn(NnzIn=>nnzii); if (curval>bestval) { bestval=curval; tmp=nnzii; }',
tmp_reset =>'curval=$nzvalsIn(NnzIn=>nnzii); bestval=curval; tmp=nnzii;',
doc_addmissing => <<'EOMD',
Output indices index $nzvalsIn, -1 indicates that the missing value is maximal.
EOMD
);
##--------------------------------------------------------------
## Operations: Accumulators (Ufuncs): minimum_nz_ind ~ minimum_ind
ccs_accum_def('minimum_nz_ind', 'minimum_nz_ind',
out_type =>'indx',
out_type_perl =>'indx',
decls =>'$GENERIC(nzvalsIn) curval, bestval;',
init_missingOut=>'missingOut = -1;',
tmp_addmissing =>'if (nMissing > 0 && $ISGOOD(missing()) && missingVal < bestval) { tmp=missingOut; }',
tmp_addval =>'curval=$nzvalsIn(NnzIn=>nnzii); if (curval<bestval) { bestval=curval; tmp=nnzii; }',
tmp_reset =>'curval=$nzvalsIn(NnzIn=>nnzii); bestval=curval; tmp=nnzii;',
doc_addmissing => <<'EOMD',
Output indices index $nzvalsIn, -1 indicates that the missing value is minimal.
EOMD
);
##--------------------------------------------------------------
## Operations: Accumulators (Ufuncs): nbad
require PDL::Bad;
ccs_accum_def('nbad', 'number of bad values',
out_type =>'indx',
#
#init_missingOut=>'missingOut=$N();',
init_missingOut=>'missingOut=missingVal;', ##-- not really right, but compatible
($PDL::Bad::Status
? (
tmp_addmissing =>'if (nMissing > 0 && $ISBAD(missing())) { tmp += nMissing; } /* bad support available */',
tmp_addval =>'if ( $ISBAD(nzvalsIn(NnzIn=>nnzii)) ) tmp++;',
tmp_reset =>'tmp = ( $ISBAD(nzvalsIn(NnzIn=>nnzii)) ) ? 1 : 0;',
) : (
tmp_addmissing =>';/* NO bad support available */',
tmp_addval =>';',
tmp_reset =>'tmp = 0;',
)),
setbad => q{
if ($PDLSTATEISBAD(ixIn)) { $PDLSTATESETBAD(ixOut); } else { $PDLSTATESETGOOD(ixOut); }
$PDLSTATESETGOOD(nzvalsOut); /*-- nzvalsOut state is always good --*/
},
doc_addmissing => <<'EOMD',
Should handle missing values appropriately.
EOMD
);
##--------------------------------------------------------------
## Operations: Accumulators (Ufuncs): ngood
ccs_accum_def('ngood', 'number of good values',
out_type =>'indx',
max_type_perl =>'indx', #$PDL::CCS::Config::ccsConfig{INT_TYPE_MAX_IONAME},
init_missingOut=>'missingOut=missingVal;', ##-- not really right, but compatible
($PDL::Bad::Status
? (
tmp_addmissing =>'if (nMissing > 0 && $ISGOOD(missing())) { tmp += nMissing; } /* bad support available */',
tmp_addval =>'if ( $ISGOOD(nzvalsIn(NnzIn=>nnzii)) ) tmp++;',
tmp_reset =>'tmp = ( $ISGOOD(nzvalsIn(NnzIn=>nnzii)) ) ? 1 : 0;',
) : (
tmp_addmissing =>';/* NO bad support available */',
tmp_addval =>'tmp++;',
tmp_reset =>'tmp=1;'
)),
setbad => q{
if ($PDLSTATEISBAD(ixIn)) { $PDLSTATESETBAD(ixOut); } else { $PDLSTATESETGOOD(ixOut); }
$PDLSTATESETGOOD(nzvalsOut); /*-- nzvalsOut state is always good --*/
},
doc_addmissing => <<'EOMD',
Should handle missing values appropriately.
EOMD
);
##--------------------------------------------------------------
## Operations: Accumulators (Ufuncs): nnz
ccs_accum_def('nnz', 'number of non-zero values',
out_type =>'indx',
init_missingOut=>'missingOut=missingVal;', ##-- not really right, but compatible
tmp_addmissing =>'if (nMissing > 0 && missingVal != 0) { tmp += nMissing; }',
tmp_addval =>'if ($nzvalsIn(NnzIn=>nnzii) != 0) tmp++;',
tmp_reset =>'tmp = ( $nzvalsIn(NnzIn=>nnzii) != 0 ) ? 1 : 0;',
setbad => q{
if ($PDLSTATEISBAD(ixIn)) { $PDLSTATESETBAD(ixOut); } else { $PDLSTATESETGOOD(ixOut); }
$PDLSTATESETGOOD(nzvalsOut); /*-- nzvalsOut state is always good --*/
},
doc_addmissing => <<'EOMD',
Should handle missing values appropriately.
EOMD
);
##--------------------------------------------------------------
## Operations: Accumulators (Ufuncs): average
ccs_accum_def('average', 'average',
decls => 'PDL_Indx ntmp;',
out_type => 'float+',
init_missingOut=>'if ($N() > 0) { missingOut=missingVal; } else { missingOut=INFINITY; }',
tmp_reset =>'tmp = $nzvalsIn(NnzIn=>nnzii); ntmp=1;',
tmp_addval =>'tmp += $nzvalsIn(NnzIn=>nnzii); ntmp++;',
tmp_addmissing =>(
'if (nMissing > 0) { tmp += nMissing * missingVal; }
if ($N() > 0) { tmp /= $N(); } else { tmp /= ntmp; }'
),
doc_addmissing => <<'EOMD',
If $N() is specified and greater than zero, then the quantity:
$missing * ($N - (rlevec($ixIn))[0]) / $N
is added to $nzvalsOut: this is probably What You Want if you are averaging over a virtual
dimension in a sparse index-encoded PDL (see PDL::CCS::Nd for a wrapper class).
EOMD
);
##--------------------------------------------------------------
## Operations: Accumulators (Ufuncs): NYI
pp_addpm(<<'EOPM');
=pod
=head1 TODO / NOT YET IMPLEMENTED
=over 4
=item extrema indices
maximum_ind, minimum_ind: not quite compatible...
=item statistical aggregates
daverage, medover, oddmedover, pctover, ...
=item cumulative functions
cumusumover, cumuprodover, ...
=item other stuff
zcover, intover, minmaximum
=back
=cut
EOPM
##======================================================================
## Footer Administrivia
##======================================================================
##------------------------------------------------------
## pm additions: footer
pp_addpm(<<'EOPM');
##---------------------------------------------------------------------
=pod
=head1 ACKNOWLEDGEMENTS
Perl by Larry Wall.
PDL by Karl Glazebrook, Tuomas J. Lukka, Christian Soeller, and others.
=cut
##----------------------------------------------------------------------
=pod
=head1 KNOWN BUGS
Probably many.
=cut
##---------------------------------------------------------------------
=pod
=head1 AUTHOR
Bryan Jurish E<lt>moocow@cpan.orgE<gt>
=head2 Copyright Policy
Copyright (C) 2007-2024, Bryan Jurish. All rights reserved.
This package is free software, and entirely without warranty.
You may redistribute it and/or modify it under the same terms
as Perl itself.
=head1 SEE ALSO
perl(1), PDL(3perl)
=cut
EOPM
# Always make sure that you finish your PP declarations with
# pp_done
pp_done();
##----------------------------------------------------------------------