guenther/internal/detect/copod.go

98 lines
3.2 KiB
Go
Raw Permalink Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

// Package detect provides anomaly detection algorithms and ensemble logic.
package detect
import (
"fmt"
"log"
"codeberg.org/pata1704/copod"
"codeberg.org/pata1704/guenther/pkg/types"
)
// COPODDetector implements the AnomalyDetector interface by wrapping the
// external codeberg.org/pata1704/copod package.
//
// Streaming mode: Score calls Update internally, so the sliding-window buffer
// stays current without requiring a separate Update call. Callers (like SEAD)
// only need to call Score per time step.
//
// Fit seeds the buffer with a batch of normal vectors. If Fit is not called
// the detector starts cold and returns score=0 until the buffer has enough
// points (controlled by bufferSize in the underlying library).
type COPODDetector struct {
detector *copod.Detector
}
// NewCOPODDetector initialises the streaming COPOD detector wrapper.
//
// - bufferSize: sliding-window capacity. Recommended: 100200.
// - threshold: score cutoff for standalone IsAnomaly. When used inside
// SEAD the threshold is ignored (SEAD applies its own adaptive threshold).
func NewCOPODDetector(bufferSize int, threshold float64) (*COPODDetector, error) {
det, err := copod.NewDetector(bufferSize, threshold)
if err != nil {
return nil, fmt.Errorf("copod: initialize wrapped detector: %w", err)
}
return &COPODDetector{
detector: det,
}, nil
}
// Fit seeds the COPOD history buffer with a slice of labelled-normal vectors.
func (c *COPODDetector) Fit(vectors []types.FeatureVector) error {
for _, v := range vectors {
if err := c.update(v); err != nil {
return err
}
}
return nil
}
// Update adds a single observation to the sliding window.
// Safe to call concurrently with Score.
func (c *COPODDetector) Update(vector types.FeatureVector) error {
return c.update(vector)
}
// Score computes the COPOD anomaly score for the given vector and
// simultaneously updates the internal sliding window with the scored vector.
//
// The self-update ensures COPOD's buffer reflects the current data stream
// without requiring a separate Update call after every Score. This is
// consistent with the RRCF and IsolationForest detectors which also
// update themselves inside Score.
func (c *COPODDetector) Score(vector types.FeatureVector) (types.AnomalyResult, error) {
vec := copod.FeatureVector{
NormalizedVector: vector.NormalizedVector,
Timestamp: vector.Timestamp,
}
// Score first, then append to the buffer so the scored point does not
// bias its own copula calculation (score-then-insert, same as RRCF).
res, err := c.detector.Score(vec)
if err != nil {
return types.AnomalyResult{}, fmt.Errorf("copod: score: %w", err)
}
if err := c.update(vector); err != nil {
// Log but don't fail: the score is already computed.
log.Printf("copod: update after score: %v", err)
}
return types.AnomalyResult{
Timestamp: res.Timestamp,
Score: res.Score,
IsAnomaly: res.IsAnomaly,
Confidence: res.Confidence,
Method: res.Method,
}, nil
}
// update is the internal helper that adds vector to the copod sliding window.
func (c *COPODDetector) update(vector types.FeatureVector) error {
vec := copod.FeatureVector{
NormalizedVector: vector.NormalizedVector,
Timestamp: vector.Timestamp,
}
return c.detector.Update(vec)
}