Algorithm-Classifier-IsolationForest

 view release on metacpan or  search on metacpan

Changes  view on Meta::CPAN

          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

Changes  view on Meta::CPAN

          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

Changes  view on Meta::CPAN

        - 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

MANIFEST  view on Meta::CPAN

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,



( run in 1.812 second using v1.01-cache-2.11-cpan-e623d60df62 )