commit for version used in evaluation of thesis

This commit is contained in:
Patryk Hegenberg 2026-03-29 10:03:18 +02:00
commit 72635dc7b9
27 changed files with 6084 additions and 0 deletions

98
internal/detect/copod.go Normal file
View file

@ -0,0 +1,98 @@
// 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)
}