view release on metacpan or search on metacpan
Since this only affects how the trees are walked etc and not build,
the voting method can be switched between majority and mean.
- adjust how iforest info displays tag info
0.4.0 2026-07-03/22:45
- Implement named features and methods for testing single rows using
tagged data.
- The C backend can be built and installed at install time, meaning
nothing needs built unless changing the opts. See the docs for
more details.
- new IF_NO_OPENMP=1 selects/builds the serial C backend: no
libgomp linkage and no OpenMP runtime in the process at all
(unlike OMP_NUM_THREADS=1, which just caps the thread count);
IF_NO_OPENMP=0 re-enables OpenMP over a serial install default
- new IF_RUNTIME_BUILD=1 ignores the prebuilt object and forces
the classic runtime build even when the flags match
- iforest accel updated to reflect various changes to the C backend
- scoring: the per-leaf path-length adjustment c(size) is now
precomputed at tree-pack time and stored in the (previously
unused) third slot of packed leaf records, removing a log()
call per point per tree from the C scoring hot loop -- about
the dot product resolves which branch is taken, hiding the
next node's memory latency under the FMA work (~7-10% faster
extended-mode scoring on top of the tiling; axis path is
untouched -- its single compare has no work to hide a
prefetch under); purely a hint, results unchanged
0.3.0 2026-07-02/23:00
- lots of POD fixes/cleanup
- various further C optmizations
- fit() can now handle training data with missing (undef) feature
cells, selectable via the new `missing =>` constructor option:
die :: croak on undef in the training data (default)
zero :: treat a missing cell as the value 0
impute :: fill with the per-feature mean/median (see `impute_with`)
nan :: range over present values and route missing rows to the
right child, consistently at fit and score time
- new `impute_with => 'mean'|'median'` option for impute mode ...
missing strategy + impute fill vector are persisted in saved models;
models from older releases load as `zero` (the prior undef -> 0
scoring behaviour)
- the C build is now tunable via environment variables read at first
- C acceleration via Inline::C for core fit and predict ops
- OpenMP support for parallel multi-threaded fitting and predict ops
- SIMD (AVX/SSE) acceleration where available
- Data packing support for compact model storage (new `pack` CLI command)
- Parallel fit capability
- New `score_predict_split` method
- New `accel` CLI command for querying available acceleration flags
- New `bench` CLI command for running built-in benchmarks
- New `info` CLI command with expanded model introspection
- Benchmarking scripts covering fit, predict, scoring, and accel modes
- Tests: accel flag detection, accel selection, undef column handling,
data packing, parallel fit, sklearn comparison (including undef),
and CLI
- minor tweaks to `csv2plot` for a bit nicer rendering
- minor POD fixes
0.1.0 2026-06-23/03:15
- add csv2plot helper command for graphing
0.0.1 2026-06-21/21:45
- initial release
t/37-majority-voting.t
t/38-online-basic.t
t/39-online-stream.t
t/41-mungers.t
t/42-prototype.t
t/43-explain.t
t/91-streamd.t
t/92-streamc.t
t/01-accel-flags.t
t/90-cli-commands.t
t/02-accel-selection.t
t/03-fit-determinism.t
t/04-accel-tuning.t
t/05-prebuilt-env.t
t/81-sklearn-real-data.t
t/data/README
t/data/regenerate.pl
t/data/sklearn-reference.py
t/data/glass.csv
t/data/glass.labels
t/data/glass.sklearn
benchmarking/bench-streamd.pl view on Meta::CPAN
# process; the ceiling everything else is measured against. The
# gap between it and the socket numbers is protocol + JSON + IPC
# overhead, not model work.
# 2. batch-size sweep -- prequential rows per {"rows": [...]}
# message; directly informs streamc's --batch choice.
# 3. modes -- prequential vs score vs learn at a fixed
# batch size.
# 4. row forms -- positional arrays vs tagged objects (the
# tagged form pays hashref building + tagged_row_to_array).
# 5. concurrent clients -- total throughput with 1/2/4 connections
# pumping at once. The daemon is a single select loop sharing one
# model, so this should stay ~flat: it measures fairness overhead,
# not parallel speedup.
# 6. command latency -- ping round trips (pure protocol floor)
# and the wall cost of an on-demand save.
#
# Reference numbers (2026-07-08, 8-core dev box, C backend,
# Cpanel::JSON::XS, 100 trees, window 2048, eta 32, 5 features):
# in-process score_learn ~2,770 pts/s; over the socket ~2,700 pts/s at
# any batch >= 16 (~2% overhead) and ~2,500 pts/s even at batch 1;
# score mode ~29,000 pts/s (no learning -- the tree walk is cheap, the
benchmarking/bench-streamd.pl view on Meta::CPAN
'--save-interval' => 3600, # no interval saves mid-benchmark
'-n' => $N_TREES,
'--window' => $WINDOW,
'--eta' => $ETA,
'-s' => 42,
( map { ( '-t' => $_ ) } @TAGS ),
) or die "exec failed: $!";
} ## end if ( !$daemon )
for ( 1 .. 100 ) {
last if -S $sock;
select( undef, undef, undef, 0.1 ); ## no critic (ProhibitSleepViaSelect)
}
die "daemon never came up; see $tmp/streamd.log\n" unless -S $sock;
END {
kill( 'TERM', $daemon ) if $daemon && kill( 0, $daemon );
}
# --- wire helpers ----------------------------------------------------------
my %BUF;
lib/Algorithm/Classifier/IsolationForest.pm view on Meta::CPAN
# Example:
# _to_double( $lo + rand() * ( $hi - $lo ) ); # what the C builder stores
sub _to_double { unpack 'd', pack 'd', $_[0] }
# ---------------------------------------------------------------------------
# Optional Inline::C accelerator for the scoring hot path.
#
# pack_input_xs(data_sv, out_sv, n_pts, n_feats, miss_mode, fill_sv)
# Walks the Perl arrayref-of-arrayrefs and writes a packed double buffer
# into out_sv. Replaces the dominant per-call Perl map-pack loop.
# miss_mode selects how an undef cell is packed: 0 => 0.0, 1 => the
# per-feature fill from fill_sv (impute), 2 => NaN (nan strategy).
#
# score_all_xs(nodes_av, idx_av, val_av, x_sv, sm_sv,
# n_pts, n_feats, n_trees, use_openmp)
# Sums path lengths for all n_pts query points across all n_trees trees
# in one call. Outer loop over points is OpenMP-parallel when the
# module was built with OpenMP (each iteration writes to a unique sm[i],
# so no synchronisation is needed). Tree pointers are extracted from
# the AVs before the parallel region; the parallel region touches only
# raw int / double buffers.
lib/Algorithm/Classifier/IsolationForest.pm view on Meta::CPAN
/* pack_input_xs(data_sv, out_sv, n_pts, n_feats, miss_mode, fill_sv)
*
* Walks a Perl arrayref-of-arrayrefs (n_pts rows of n_feats doubles each)
* directly in C and writes the packed double buffer into out_sv (which the
* caller pre-allocates with "\0" x (n_pts*n_feats*8)). Replaces
*
* pack('d*', map { my $r=$_; map { $r->[$_] // 0 } 0..$nf-1 } @$data)
*
* which was the dominant per-call overhead for high feature counts.
*
* miss_mode selects what an undef cell (or missing row) becomes:
* 0 => 0.0 (the 'die'/'zero' missing strategies)
* 1 => fill[k] (the 'impute' strategy; fill_sv is a packed
* double buffer of n_feats per-feature fill values)
* 2 => NaN (the 'nan' strategy; the C scorer's `<` / `<=`
* comparisons are both false for NaN, so a point
* missing the split feature falls to the right
* child -- matching how fit() routes it)
* fill_sv is only dereferenced when miss_mode == 1. */
void pack_input_xs(SV* data_sv, SV* out_sv, int n_pts, int n_feats,
int miss_mode, SV* fill_sv){
lib/Algorithm/Classifier/IsolationForest.pm view on Meta::CPAN
*
* Replaces the pure-Perl _subsample + _build_tree + _axis_split /
* _oblique_split recursion with an equivalent C implementation that
* partitions plain `int` row-index arrays instead of copying arrayrefs
* of Perl SVs at every split. Random draws go through Drand01() --
* the exact generator Perl's own rand()/srand() use internally -- in
* the same call order the Perl code used, so a fit() with a given
* seed produces BIT-IDENTICAL trees whether use_c is on or off. This
* is what lets fit() reuse the existing `use_c` knob instead of a new
* one: switching backends never changes the model, only how fast it's
* built. (Verified by t/02-accel-selection.t's "identical seed =>
* identical trees" subtest, which exercises both backends.)
*
* Output trees are plain Perl arrayrefs in the same node shape
* _build_tree produces (leaf/axis/oblique -- see the file-top
* comment), so every downstream consumer (_pack_tree, to_json,
* from_json, the pure-Perl scorer) is unchanged.
*
* x_sv: packed row-major double buffer, n_pts rows of n_feats each
* (from pack_input_xs -- NaN marks a missing cell under the
* 'nan' missing-strategy).
lib/Algorithm/Classifier/IsolationForest.pm view on Meta::CPAN
* writes n_feats doubles into out_rv.
*
* Values are collected in row order (i = 0..n_pts-1), the same order
* the Perl version's `grep { defined } map { $_->[$f] } @data` walks
* them in, so the mean's left-to-right summation lands on the exact
* same float as the Perl path -- use_c toggles speed here, not the
* computed fill, matching the rest of the module.
*
* The median is an exact order statistic (not summation-dependent), so
* it matches the Perl path's sort-based median by definition regardless
* of which selection algorithm finds it. Croaks with the same message
* as the Perl fallback if a feature has no present values anywhere in
* the dataset. */
typedef struct { double *v; size_t n, cap; } DVec;
static void dvec_push(DVec *d, double x) {
if (d->n == d->cap) {
size_t newcap = d->cap ? d->cap * 2 : 64;
d->v = (double*)realloc(d->v, newcap * sizeof(double));
d->cap = newcap;
}
lib/Algorithm/Classifier/IsolationForest.pm view on Meta::CPAN
_dswap(&a[mid], &a[hi]);
pivot = a[hi];
i = lo;
for (j = lo; j < hi; j++) {
if (a[j] < pivot) { _dswap(&a[i], &a[j]); i++; }
}
_dswap(&a[i], &a[hi]);
return i;
}
/* Quickselect: returns the k-th smallest (0-indexed) of a[0..n-1],
* reordering a[] in the process (fine -- it's a private scratch copy).
* O(n) average case vs. a full O(n log n) sort. */
static double _kth_smallest(double *a, int n, int k) {
int lo = 0, hi = n - 1;
while (lo < hi) {
int p = _partition_lomuto(a, lo, hi);
if (p == k) return a[p];
if (p < k) lo = p + 1; else hi = p - 1;
}
return a[lo];
}
/* Median of a[0..n-1] (reorders a[]). Odd n: the single middle order
* statistic. Even n: quickselect finds the lower-median at k = n/2-1,
* which leaves every a[i > k] >= a[k] (the standard quickselect
* post-condition) -- so the upper-median is just the min of that
* remaining slice, one more linear scan instead of a second full
* selection pass. */
static double _median_select(double *a, int n) {
if (n % 2 == 1) {
return _kth_smallest(a, n, n / 2);
} else {
int k = n / 2 - 1;
double lower = _kth_smallest(a, n, k);
double upper = a[k + 1];
int i;
for (i = k + 2; i < n; i++) {
if (a[i] < upper) upper = a[i];
}
lib/Algorithm/Classifier/IsolationForest.pm view on Meta::CPAN
av_clear(out);
if (n_feats > 0) av_extend(out, n_feats - 1);
for (f = 0; f < n_feats; f++) {
double result;
if (how == 0) {
double sum = 0.0;
for (i = 0; i < (int)cols[f].n; i++) sum += cols[f].v[i];
result = sum / (double)cols[f].n;
} else {
result = _median_select(cols[f].v, (int)cols[f].n);
}
av_store(out, f, newSVnv(result));
free(cols[f].v);
}
free(cols);
}
/* ---------------------------------------------------------------------
* Online Isolation Forest (Algorithm::Classifier::IsolationForest::
* Online) learn / unlearn / score-row accelerators.
lib/Algorithm/Classifier/IsolationForest.pm view on Meta::CPAN
=item * C<IF_NATIVE=1> -- shorthand for C<IF_ARCH=native>; ignored if
C<IF_ARCH> is also set. Prefer a specific C<IF_ARCH> value over this on
a machine you don't control exclusively (a shared build host, a
container base image): blanket C<-march=native> pulls in whatever
instruction sets the build host happens to have, including AVX-512 on
some Intel CPUs -- which is known to trigger clock throttling under
sustained heavy use and can make throughput I<worse> than a
conservative target like C<x86-64-v3> (AVX2, no AVX-512). If in doubt,
benchmark both before committing to one.
=item * C<IF_NO_OPENMP=1> -- build (or select) the serial C backend: the
OpenMP compile attempt is skipped entirely, so the resulting object has
no libgomp linkage and never starts an OpenMP runtime inside the
process. This differs from C<OMP_NUM_THREADS=1>, which merely runs the
parallel code on one thread but still loads libgomp. Set at
C<perl Makefile.PL> time it yields a serial prebuilt object; set at run
time against an OpenMP prebuilt install it triggers a runtime serial
build (needing a compiler). An explicit C<IF_NO_OPENMP=0> re-enables
OpenMP over a serial configure-time default.
=back
lib/Algorithm/Classifier/IsolationForest.pm view on Meta::CPAN
# k-boundary shift the cut to the nearest gap -- see
# _threshold_from_ranked), so it sits strictly between attainable values:
# unambiguous and robust to the tiny float rounding introduced by JSON
# serialisation.
#
# Under voting => 'majority' the value predict() thresholds against is
# the PER-TREE score, so the quantity to rank is each training point's
# majority pivot -- the per-tree cutoff at which that point loses its
# majority (see _majority_pivot_scores). A point is flagged iff its
# pivot >= threshold, exactly the relation the mean-mode score has, so
# the midpoint selection below serves both modes unchanged.
$self->_learn_contamination_threshold($train)
if defined $self->{contamination};
$self->_rebuild_c_trees() if $self->{_use_c};
return $self;
} ## end sub fit
=head2 fit_tagged(\@rows)
Trains the model on an arrayref of hashrefs of named feature values --
lib/Algorithm/Classifier/IsolationForest.pm view on Meta::CPAN
bounded memory via Floyd's algorithm), then build each tree from its
sub-sample exactly as L</fit> would. With the offset table from pass 1 this
B<seeks straight to the sampled rows> instead of re-scanning the file.
=item 3.
B<Threshold> (only when C<contamination> is set) -- score every row against
the built forest and place the cut at the contamination boundary. The cut
is B<exact>, not estimated: a single min-heap of the C<k+1> largest scores
(C<k = round(contamination * n)>) captures everything
L</decision_threshold> selection needs, with a rare extra pass to resolve a
tie block straddling the boundary. Under C<< use_c => 1 >> (the default when
the C backend is present) this pass runs through the C scorer, so it stays
fast even over millions of rows.
=back
Neither census variant parses cells into numbers or checks for missing
values: only the rows that actually train (via gather) or are scored (the
threshold pass) are validated, so a malformed or missing cell in a
never-used row is not reported. In particular, under C<< missing => 'die' >>
lib/Algorithm/Classifier/IsolationForest.pm view on Meta::CPAN
# $n_trees :: the forest's tree count, an integer >= 1.
#
# Returns: the required vote count, an integer in [1, $n_trees].
#
# Example:
# _min_votes(100); # 51
# _min_votes(101); # 51
sub _min_votes { return int( $_[0] / 2 ) + 1 }
#-------------------------------------------------------------------------------
# Contamination threshold selection: given the training scores ranked
# descending and the target flag count k, return a cutoff sitting midway
# inside the gap between the last flagged and the first unflagged score.
#
# Tied scores at the k-boundary make an exact count of k unattainable (the
# tie block can only go one way or the other) AND make the naive midpoint
# degenerate -- it equals the tied value, leaving predict()'s >= comparison
# balanced on exact float equality. Mean-mode scores are continuous enough
# that this practically never happens, but majority-mode pivots are
# structurally quantized (path lengths at the depth cap take few distinct
# values -- see _majority_pivot_scores), so ties there are the norm, and
lib/Algorithm/Classifier/IsolationForest.pm view on Meta::CPAN
# closest to k, preferring the flagging side on a dead heat.
#
# Args:
# $desc :: arrayref of the per-point statistic sorted DESCENDING -- the
# mean-mode anomaly scores, or the majority pivots under
# voting => 'majority'. Must be non-empty.
# $k :: how many points the contamination rate wants flagged, an integer
# in [1, scalar @$desc].
#
# Returns: the cutoff, a float positioned so that `statistic >= cutoff`
# selects the intended points. Never equal to a value in $desc.
#
# Example:
# _threshold_from_ranked( [ 0.71, 0.68, 0.52, 0.50 ], 2 ); # 0.60
# _threshold_from_ranked( [ 0.71, 0.68, 0.68, 0.50 ], 2 ); # 0.59, tie
#-------------------------------------------------------------------------------
sub _threshold_from_ranked {
my ( $desc, $k ) = @_;
my $n = scalar @$desc;
return $desc->[ $n - 1 ] - 1e-9 if $k >= $n; # flag everything
lib/Algorithm/Classifier/IsolationForest.pm view on Meta::CPAN
} ## end sub _vote_counts_perl
# Learn the contamination cutoff for the CURRENT voting mode from a training
# set. Ranks the per-point quantity the active aggregation thresholds against
# -- the mean-mode anomaly score, or the majority pivot under
# voting => 'majority' -- and lands the cutoff midway inside a real gap between
# flagged and unflagged values (ties at the k-boundary shift it to the nearest
# gap; see _threshold_from_ranked), so it sits strictly between attainable
# values: unambiguous and robust to the float rounding JSON introduces. A
# point is flagged iff its statistic >= threshold in either mode, so the
# midpoint selection serves both unchanged. Shared by fit() (which passes the
# prepared training set after dropping any stale packed buffers) and
# set_voting() (which passes the caller-supplied training set against the
# live, fully packed forest); $data may hold raw undef cells either way, since
# the scorers below densify from missing_fill.
#
# Args:
# $data :: the training set, an arrayref of feature-value arrayrefs. Raw
# undef cells are fine.
#
# Returns: nothing. Sets $self->{threshold} as its whole purpose.
lib/Algorithm/Classifier/IsolationForest.pm view on Meta::CPAN
} ## end sub _learn_contamination_threshold
#-------------------------------------------------------------------------------
# Contamination support for majority voting: each training point's majority
# pivot -- the per-tree score threshold at which the point loses its
# majority. A point is flagged at cutoff theta iff at least min_votes of
# its per-tree path lengths h satisfy h <= -c*log2(theta), which holds iff
# its min_votes-th SMALLEST path length h_(maj) does, i.e. iff
# 2**(-h_(maj)/c) >= theta. So the pivot m = 2**(-h_(maj)/c) relates to
# the majority-mode threshold exactly as the mean-mode score relates to
# its threshold, and fit()'s midpoint selection works on either unchanged.
#
# Pure Perl by necessity: the per-tree path lengths never cross the C
# boundary individually (score_all_xs/vote_all_xs only return per-point
# aggregates), and fit() has already dropped any stale packed buffers when
# this runs -- the same situation as mean mode's training-set scoring pass.
#
# Args:
# $data :: arrayref of feature-value arrayrefs, raw or prepared -- it goes
# through _prepare_perl_input here either way.
#
lib/Algorithm/Classifier/IsolationForest.pm view on Meta::CPAN
# plus c(leaf size) when the leaf still holds several points.
#
# Node layout (arrayref, slot 0 = type):
# _NODE_LEAF [0, size]
# _NODE_AXIS [1, attr, split, left, right]
# _NODE_OBLIQUE [2, \@idx, \@coef, b, left, right]
#
# The type tag is also used as a loop sentinel: 0 (_NODE_LEAF) is falsy.
# No $self argument -- the node type encodes everything needed.
#-------------------------------------------------------------------------------
# The optional $nan flag selects the nan-strategy routing: a point missing
# the split feature goes to the right child (matching the C scorer, where
# the NaN comparison is false). Without it, undef is coerced to 0 -- the
# behaviour the die/zero/impute strategies rely on (their data is dense by
# the time it reaches here, so the "// 0" is normally a no-op).
#
# Args:
# $x :: one sample, an arrayref of feature values. undef cells are
# allowed and handled per $nan.
# $node :: the node to start walking from, normally a tree root.
# $depth :: the depth credited to $node, 0 for a root. Callers only pass
lib/Algorithm/Classifier/IsolationForest.pm view on Meta::CPAN
: [ (0) x $nf ];
$self->{missing_fill} = $fill if $m eq 'impute';
delete $self->{_fill_packed};
return $data if $self->{_use_c};
return _densify( $data, $fill );
} ## end sub _prepare_fit_data
# Per-feature fill value (mean or median of the present values) for impute
# mode. Croaks if a feature has no present value to learn from. The
# optional $how_override selects the statistic independently of the
# model's impute_with knob (used by _compute_feature_baselines, which
# always wants the median).
#
# Args:
# $data :: the rows to learn from, an arrayref of feature-value
# arrayrefs. undef cells are skipped rather than counted.
# $how_override :: 'mean' or 'median' to force the statistic, or undef to
# follow the model's impute_with.
#
# Returns: an arrayref of n_features fill values. Croaks naming the column
lib/Algorithm/Classifier/IsolationForest.pm view on Meta::CPAN
#
# Example:
# $self->_compute_impute_fill( \@rows ); # per impute_with
# $self->_compute_impute_fill( \@rows, 'median' ); # forced median
sub _compute_impute_fill {
my ( $self, $data, $how_override ) = @_;
my $nf = $self->{n_features};
my $how = $how_override // $self->{impute_with};
# C fast path: walks the raw data directly and finds the median via
# quickselect (O(n) average) instead of the Perl fallback's full sort
# (O(n log n)). Produces the same fill values either way -- see
# impute_fill_xs's file-top comment -- so use_c only changes speed
# here, matching the rest of the module.
if ( $self->{_use_c} ) {
my $n = scalar @$data;
my $how_flag = $how eq 'median' ? 1 : 0;
my $fill = [];
impute_fill_xs( $data, $n, $nf, $how_flag, $fill );
return $fill;
}
lib/Algorithm/Classifier/IsolationForest/App/Command/streamd.pm view on Meta::CPAN
unlink $old and _log( 'pruned ' . $old );
}
return 1;
} ## end sub _prune_models
#-------------------------------------------------------------------------------
# connection handling
#-------------------------------------------------------------------------------
# Close one client connection and forget everything about it. The single
# teardown path, so a connection can never be left in a selector after its
# socket is gone.
#
# Args:
# $s :: the client socket.
# $rsel :: the read IO::Select set it may be registered in.
# $wsel :: the write IO::Select set it may be registered in.
#
# Returns: 1. Callers return its value directly, which is why the read
# and write loops read as "return _drop(...)".
#
lib/Algorithm/Classifier/IsolationForest/Online.pm view on Meta::CPAN
package Algorithm::Classifier::IsolationForest::Online;
use strict;
use warnings;
use Carp qw(croak);
use JSON::PP ();
use File::Slurp qw(read_file write_file);
# Runtime-only dependency: tagged_row_to_array is delegated to the parent
# class (identical semantics, no point duplicating it) and the
# contamination threshold selection reuses _threshold_from_ranked. The
# parent never loads this module at compile time (its from_json requires
# it on demand), so there is no cycle.
use Algorithm::Classifier::IsolationForest ();
our $VERSION = '0.6.0';
# Node layout. Unlike the batch forest's nodes, online nodes are mutable
# and carry a running point count plus the bounding box (per-feature
# lo/hi) of every point that has passed through them -- that box is what
# split simulation samples from, since points themselves are never stored
t/02-accel-selection.t view on Meta::CPAN
#!perl
# 02-accel-selection.t
#
# Exercises the per-instance acceleration selection knobs `use_c` and
# `use_openmp` exposed by new() and verifies they actually steer which
# code path runs:
#
# * Defaults come from the package flags $HAS_C / $HAS_OPENMP.
# * use_c => 0 disables the Inline::C scoring backend even when the
# module compiled it in. After fit() such an instance has no
# _c_nodes attached, so we know scoring is hitting the pure-Perl
# fallback.
# * use_c => 1 is honoured when $HAS_C is set, ignored (clamped via
# truthiness) otherwise.
t/02-accel-selection.t view on Meta::CPAN
$out,
qr/Active backend:.*-- (prebuilt at install time|compiled at run time)/,
'Active backend summary includes the C object source'
);
} else {
like( $out, qr/C object\s*:\s*none/, 'C object line says none without a C backend' );
}
# Cross-check the per-feature status lines against the package
# flags the test process observed. This is what makes this test
# actually verify selection rather than just "doesn't crash".
if ($HAS_C) {
like( $out, qr/Inline::C\s*:\s*available/, 'CLI reports Inline::C available, matching $HAS_C' );
} else {
like( $out, qr/Inline::C\s*:\s*not available/, 'CLI reports Inline::C not available, matching $HAS_C' );
}
if ($HAS_OPENMP) {
like( $out, qr/OpenMP\s*:\s*available/, 'CLI reports OpenMP available, matching $HAS_OPENMP' );
} else {
like( $out, qr/OpenMP\s*:\s*not available/, 'CLI reports OpenMP not available, matching $HAS_OPENMP' );
}
t/03-fit-determinism.t view on Meta::CPAN
#!perl
# 03-fit-determinism.t
#
# Verifies that fit() with a given `seed` produces reproducible trees,
# across every tree-building backend the module can select:
#
# * pure Perl (use_c => 0)
# * serial C (use_c => 1)
# * OpenMP-parallel C (use_c => 1, use_openmp_fit => 1)
#
# Covers:
# 1. Each backend, run twice with the same seed, builds bit-identical
# trees (not just bit-identical scores -- the actual tree structure).
# 2. Pure-Perl and serial-C build BIT-IDENTICAL trees for the same seed,
# in both axis and extended mode, and across every `missing`
t/37-majority-voting.t view on Meta::CPAN
use strict;
use warnings;
use Test::More;
use File::Spec;
use Algorithm::Classifier::IsolationForest;
my $CLASS = 'Algorithm::Classifier::IsolationForest';
my $HAS_C = $Algorithm::Classifier::IsolationForest::HAS_C ? 1 : 0;
# Uniform cluster plus unmistakable outliers, as in 02-accel-selection.t.
srand(11);
my @data;
push @data, [ rand(), rand(), rand() ] for 1 .. 60;
push @data, [ 12, 12, 12 ], [ -11, -11, -11 ], [ 10, -10, 9 ];
my @outlier_idx = ( 60, 61, 62 );
subtest 'constructor validation' => sub {
my $f = $CLASS->new( n_trees => 10, sample_size => 16 );
is( $f->{voting}, 'mean', 'voting defaults to mean' );
t/91-streamd.t view on Meta::CPAN
my $pid = fork();
die "fork failed: $!" unless defined $pid;
if ( !$pid ) {
open( STDOUT, '>>', $logf ) or die $!;
open( STDERR, '>>', $logf ) or die $!;
exec( $^X, '-Ilib', $bin, 'streamd', '-f', @argv ) or die "exec failed: $!";
}
push @ALL_PIDS, $pid;
for ( 1 .. 100 ) {
last if -S $wait_sock;
select( undef, undef, undef, 0.1 ); ## no critic (ProhibitSleepViaSelect)
}
return $pid;
} ## end sub spawn_daemon
sub start_daemon {
my (@extra) = @_;
return spawn_daemon(
$sock,
'--socket' => $sock,
'--pid' => $pidf,
t/91-streamd.t view on Meta::CPAN
is( rt( $c2, { cmd => 'stats' } )->{ok}{connections}, 2, 'stats sees both connections' );
# Per-connection mode: c2 goes learn-only, c stays prequential.
is_deeply( rt( $c2, { cmd => 'mode', mode => 'learn' } ), { ok => { mode => 'learn' } }, 'mode command' );
is_deeply( rt( $c2, { row => [ 0.3, 0.3 ] } ), { ok => { learned => 1 } },
'c2 rows now learn without scoring' );
ok( defined rt( $c, { row => [ 0.3, 0.3 ] } )->{score}, 'c still gets scores' );
close $c2;
# Give the daemon a beat to notice the close.
select( undef, undef, undef, 0.3 ); ## no critic (ProhibitSleepViaSelect)
is( rt( $c, { cmd => 'stats' } )->{ok}{connections}, 1, 'closed connection is reaped' );
}; ## end 'multiple concurrent connections' => sub
subtest 'saves: command, interval, symlink' => sub {
my $r = rt( $c, { cmd => 'save', tag => 's1' } );
like( $r->{ok}{saved}, qr/\Aoiforest-\d{8}-\d{6}(?:-\d+)?\.json\z/, 'save returns the file name' );
is( $r->{tag}, 's1', 'save reply carries the tag' );
ok( -f "$mdir/$r->{ok}{saved}", 'the timestamped file exists' );
ok( -l $latest, 'latest.json is a symlink' );
is( readlink($latest), $r->{ok}{saved}, 'and points at the newest save (relative target)' );
# Interval saves happen only after learning; learn then wait past the
# 1s interval.
my $count_before = () = glob("$mdir/oiforest-*.json");
rt( $c, { row => [ 0.2, 0.8 ] } );
select( undef, undef, undef, 2.5 ); ## no critic (ProhibitSleepViaSelect)
rt( $c, { cmd => 'ping' } ); # tick the loop
my $count_after = () = glob("$mdir/oiforest-*.json");
cmp_ok( $count_after, '>', $count_before, 'a periodic save fired after learning' );
}; ## end 'saves: command, interval, symlink' => sub
my $seen_at_shutdown = rt( $c, { cmd => 'stats' } )->{ok}{seen};
subtest 'clean shutdown and resume' => sub {
is( stop_daemon($daemon), 0, 'SIGTERM exits 0' );
undef $daemon;
t/92-streamc.t view on Meta::CPAN
my $pid = fork();
die "fork failed: $!" unless defined $pid;
if ( !$pid ) {
open( STDOUT, '>>', $logf ) or die $!;
open( STDERR, '>>', $logf ) or die $!;
exec( $^X, '-Ilib', $bin, 'streamd', '-f', @argv ) or die "exec failed: $!";
}
push @ALL_PIDS, $pid;
for ( 1 .. 100 ) {
last if -S $wait_sock;
select( undef, undef, undef, 0.1 ); ## no critic (ProhibitSleepViaSelect)
}
return $pid;
} ## end sub spawn_daemon
# The daemon under test: a named set, so streamc's --set resolution is
# exercised by every call.
my $daemon = spawn_daemon(
"$tmp/alpha.sock",
'--set' => 'alpha',
'--socket' => $tmp,