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

114
internal/detect/mad_test.go Normal file
View file

@ -0,0 +1,114 @@
package detect
import (
"testing"
"time"
"codeberg.org/pata1704/guenther/pkg/types"
"github.com/stretchr/testify/assert"
)
func TestMADDetector_Score(t *testing.T) {
detector := NewMADDetector(3.0, []float64{10.0}, []float64{1.0})
// 1. Score a normal value
res, err := detector.Score(types.FeatureVector{
Timestamp: time.Now(),
NormalizedVector: []float64{11},
})
assert.NoError(t, err)
assert.False(t, res.IsAnomaly, "Value 11 should not be an anomaly")
// 2. Score an extreme outlier
res, err = detector.Score(types.FeatureVector{
Timestamp: time.Now(),
NormalizedVector: []float64{100},
})
assert.NoError(t, err)
assert.True(t, res.IsAnomaly, "Value 100 should be an anomaly")
assert.Greater(t, res.Score, 3.0)
}
func TestMADDetector_CalibrationStability(t *testing.T) {
// 1. Create a detector that auto-calibrates on 100 idle vectors.
detector := NewMADDetectorAutoCalibrate(3.5, 100)
now := time.Now()
// 2. Feed 99 perfectly idle vectors.
// They should all use "Identity" fallback and return low scores (or 0 if val is 0).
for i := 0; i < 99; i++ {
fv := types.FeatureVector{
Timestamp: now.Add(time.Duration(i) * time.Second),
NormalizedVector: []float64{0.0, 0.0},
}
res, err := detector.Score(fv)
assert.NoError(t, err)
assert.Equal(t, 0.0, res.Score)
assert.Contains(t, res.Method, "warmup")
}
// 3. Feed the 100th vector. This triggers calibration.
// Since all 100 vectors were 0, the learned medians will be 0 and mads will be 0.
fv100 := types.FeatureVector{
Timestamp: now.Add(100 * time.Second),
NormalizedVector: []float64{0.0, 0.0},
}
res100, err := detector.Score(fv100)
assert.NoError(t, err)
assert.Equal(t, 0.0, res100.Score)
// After this call, mads should be [0.0, 0.0] but clamped to 0.01 during Score.
// 4. Feed the 101st vector: A "normal" burst (e.g. 1.0 baseline IQR).
// Without the floor, this would be 1.0 / (1.48 * 0) -> infinity (clamped).
// With the floor (0.01), it should be 1.0 / (1.4826 * 0.01) ≈ 67.45.
fv101 := types.FeatureVector{
Timestamp: now.Add(101 * time.Second),
NormalizedVector: []float64{1.0, 0.0},
}
res101, err := detector.Score(fv101)
assert.NoError(t, err)
// Check that the score is contained.
// 1.0 / (1.4826 * 0.01) = 67.449
assert.InDelta(t, 67.449, res101.Score, 0.1)
assert.True(t, res101.IsAnomaly)
assert.Equal(t, "MAD", res101.Method) // No longer "warmup"
// 5. Test with a very small variance but not 0.
// Suppose learned MAD was 0.0001. Score for val=1.0 would be 1.0 / 0.000148... ≈ 6745.
// Our floor (0.01) should still clamp this to 67.45.
detector.mu.Lock()
detector.mads = []float64{0.0001, 0.0}
detector.medians = []float64{0.0, 0.0}
detector.mu.Unlock()
resSmall, err := detector.Score(fv101)
assert.NoError(t, err)
assert.InDelta(t, 67.449, resSmall.Score, 0.1)
}
func TestMADDetector_IdentityPrior(t *testing.T) {
detector := NewMADDetectorAutoCalibrate(3.5, 10)
// Feature vector with a deviation of 2.0 baseline IQR.
// Using identity prior (mad=1.0), the score should be:
// score = |2.0| / (1.4826 * 1.0) = 2.0 / 1.4826 ≈ 1.3489
// Wait, scoreIdentity uses 0.6745 directly: math.Abs(val) / 0.6745
// 2.0 / 0.6745 ≈ 2.965
fv := types.FeatureVector{
NormalizedVector: []float64{2.0},
}
res, _ := detector.Score(fv)
assert.InDelta(t, 2.965, res.Score, 0.1)
assert.False(t, res.IsAnomaly) // 2.96 < 3.5
// Feature vector with deviation of 3.0.
// score = 3.0 / 0.6745 ≈ 4.44
fv2 := types.FeatureVector{
NormalizedVector: []float64{3.0},
}
res2, _ := detector.Score(fv2)
assert.InDelta(t, 4.44, res2.Score, 0.1)
assert.True(t, res2.IsAnomaly)
assert.Contains(t, res2.Details, "identity prior")
}