test_decay.pyβ’14.4 kB
"""Tests for temporal decay functions."""
import time
import pytest
from mnemex.config import Config, set_config
from mnemex.core.decay import (
calculate_decay_lambda,
calculate_halflife,
calculate_score,
project_score_at_time,
time_until_threshold,
)
def test_calculate_score_basic():
"""Test basic score calculation."""
now = int(time.time())
# Fresh memory with use_count=1 should have non-zero score
score = calculate_score(
use_count=1,
last_used=now,
strength=1.0,
now=now,
lambda_=2.673e-6,
beta=0.6,
)
assert score > 0
assert score == pytest.approx(1.0, rel=0.01) # Should be close to 1 when just accessed
def test_calculate_score_decay():
"""Test that score decays over time."""
now = int(time.time())
one_day_ago = now - 86400
score_fresh = calculate_score(
use_count=1,
last_used=now,
strength=1.0,
now=now,
lambda_=2.673e-6,
beta=0.6,
)
score_old = calculate_score(
use_count=1,
last_used=one_day_ago,
strength=1.0,
now=now,
lambda_=2.673e-6,
beta=0.6,
)
assert score_old < score_fresh # Older memory has lower score
def test_calculate_score_use_count():
"""Test that higher use count increases score."""
now = int(time.time())
score_low = calculate_score(
use_count=1,
last_used=now,
strength=1.0,
now=now,
lambda_=2.673e-6,
beta=0.6,
)
score_high = calculate_score(
use_count=10,
last_used=now,
strength=1.0,
now=now,
lambda_=2.673e-6,
beta=0.6,
)
assert score_high > score_low # Higher use count = higher score
def test_calculate_decay_lambda():
"""Test decay lambda calculation from half-life."""
# 3-day half-life
lambda_3d = calculate_decay_lambda(3.0)
assert lambda_3d == pytest.approx(2.673e-6, rel=0.01)
# 7-day half-life
lambda_7d = calculate_decay_lambda(7.0)
assert lambda_7d < lambda_3d # Longer half-life = slower decay
def test_calculate_halflife():
"""Test half-life calculation from lambda."""
lambda_val = 2.673e-6
halflife = calculate_halflife(lambda_val)
assert halflife == pytest.approx(3.0, rel=0.01) # Should be 3 days
def test_time_until_threshold():
"""Test calculation of time until score drops below threshold."""
now = int(time.time())
# Memory with current score of 1.0
remaining = time_until_threshold(
current_score=1.0,
threshold=0.5, # Half the score
last_used=now,
lambda_=calculate_decay_lambda(3.0), # 3-day half-life
)
assert remaining is not None
# Should be approximately 3 days (259200 seconds)
assert remaining == pytest.approx(259200, rel=0.1)
def test_time_until_threshold_already_below():
"""Test time_until_threshold when already below threshold."""
now = int(time.time())
remaining = time_until_threshold(
current_score=0.3,
threshold=0.5,
last_used=now,
lambda_=2.673e-6,
)
assert remaining is None # Already below threshold
def test_project_score_at_time():
"""Test score projection to future time."""
now = int(time.time())
future = now + 86400 # 1 day from now
projected = project_score_at_time(
use_count=5,
last_used=now,
strength=1.0,
target_time=future,
lambda_=2.673e-6,
beta=0.6,
)
current = calculate_score(
use_count=5,
last_used=now,
strength=1.0,
now=now,
lambda_=2.673e-6,
beta=0.6,
)
assert projected < current # Future score should be lower due to decay
def test_calculate_score_default_now():
"""Test calculate_score with default now parameter (should use current time)."""
current_time = int(time.time())
# Call without now parameter - should use current time
score = calculate_score(
use_count=1,
last_used=current_time,
strength=1.0,
lambda_=2.673e-6,
beta=0.6,
)
# Score should be close to 1.0 since last_used is current time
assert score > 0
assert score == pytest.approx(1.0, rel=0.01)
def test_calculate_score_default_beta():
"""Test calculate_score with default beta parameter (should use config default)."""
config = Config()
set_config(config)
now = int(time.time())
# Call without beta parameter - should use config default (0.6)
score = calculate_score(
use_count=5,
last_used=now,
strength=1.0,
now=now,
lambda_=2.673e-6,
)
assert score > 0
def test_calculate_score_power_law_model():
"""Test calculate_score with power_law decay model."""
config = Config(decay_model="power_law", pl_alpha=1.1, pl_halflife_days=3.0)
set_config(config)
now = int(time.time())
one_day_ago = now - 86400
# Fresh memory
score_fresh = calculate_score(
use_count=1,
last_used=now,
strength=1.0,
)
# Old memory
score_old = calculate_score(
use_count=1,
last_used=one_day_ago,
strength=1.0,
)
assert score_fresh > 0
assert score_old > 0
assert score_old < score_fresh # Power law decay
def test_calculate_score_power_law_different_alpha():
"""Test power_law model with different alpha values."""
now = int(time.time())
one_day_ago = now - 86400
# Test with alpha = 0.8 (lighter tail)
config1 = Config(decay_model="power_law", pl_alpha=0.8, pl_halflife_days=3.0)
set_config(config1)
score1 = calculate_score(
use_count=1,
last_used=one_day_ago,
strength=1.0,
)
# Test with alpha = 1.5 (heavier tail)
config2 = Config(decay_model="power_law", pl_alpha=1.5, pl_halflife_days=3.0)
set_config(config2)
score2 = calculate_score(
use_count=1,
last_used=one_day_ago,
strength=1.0,
)
assert score1 > 0
assert score2 > 0
def test_calculate_score_two_component_model():
"""Test calculate_score with two_component decay model."""
config = Config(
decay_model="two_component",
tc_lambda_fast=1.603e-5,
tc_lambda_slow=1.147e-6,
tc_weight_fast=0.7,
)
set_config(config)
now = int(time.time())
one_day_ago = now - 86400
# Fresh memory
score_fresh = calculate_score(
use_count=1,
last_used=now,
strength=1.0,
)
# Old memory
score_old = calculate_score(
use_count=1,
last_used=one_day_ago,
strength=1.0,
)
assert score_fresh > 0
assert score_old > 0
assert score_old < score_fresh # Two-component decay
def test_calculate_score_two_component_different_weights():
"""Test two_component model with different weight values."""
now = int(time.time())
one_day_ago = now - 86400
# Test with different weights - both should produce valid scores
config1 = Config(
decay_model="two_component",
tc_lambda_fast=1.603e-5,
tc_lambda_slow=1.147e-6,
tc_weight_fast=0.9,
)
set_config(config1)
score1 = calculate_score(
use_count=1,
last_used=one_day_ago,
strength=1.0,
)
config2 = Config(
decay_model="two_component",
tc_lambda_fast=1.603e-5,
tc_lambda_slow=1.147e-6,
tc_weight_fast=0.3,
)
set_config(config2)
score2 = calculate_score(
use_count=1,
last_used=one_day_ago,
strength=1.0,
)
# Both should produce valid scores
assert score1 > 0
assert score1 < 1.0
assert score2 > 0
assert score2 < 1.0
def test_calculate_score_exponential_model():
"""Test calculate_score with exponential decay model via config."""
config = Config(decay_model="exponential", decay_lambda=2.673e-6)
set_config(config)
now = int(time.time())
one_day_ago = now - 86400
# Fresh memory
score_fresh = calculate_score(
use_count=1,
last_used=now,
strength=1.0,
)
# Old memory
score_old = calculate_score(
use_count=1,
last_used=one_day_ago,
strength=1.0,
)
assert score_fresh > 0
assert score_old > 0
assert score_old < score_fresh # Exponential decay
def test_time_until_threshold_exponential_from_config():
"""Test time_until_threshold with exponential model from config."""
config = Config(decay_model="exponential", decay_lambda=calculate_decay_lambda(3.0))
set_config(config)
now = int(time.time())
# Test without lambda_ parameter - should use config
remaining = time_until_threshold(
current_score=1.0,
threshold=0.5,
last_used=now,
)
assert remaining is not None
# Should be approximately 3 days (259200 seconds)
assert remaining == pytest.approx(259200, rel=0.1)
def test_time_until_threshold_power_law():
"""Test time_until_threshold with power_law model."""
config = Config(decay_model="power_law", pl_alpha=1.1, pl_halflife_days=3.0)
set_config(config)
now = int(time.time())
remaining = time_until_threshold(
current_score=1.0,
threshold=0.5,
last_used=now,
)
assert remaining is not None
assert remaining > 0
def test_time_until_threshold_two_component():
"""Test time_until_threshold with two_component model."""
config = Config(
decay_model="two_component",
tc_lambda_fast=1.603e-5,
tc_lambda_slow=1.147e-6,
tc_weight_fast=0.7,
)
set_config(config)
now = int(time.time())
remaining = time_until_threshold(
current_score=1.0,
threshold=0.5,
last_used=now,
)
assert remaining is not None
assert remaining > 0
def test_time_until_threshold_different_thresholds():
"""Test time_until_threshold with various threshold values."""
now = int(time.time())
# Higher threshold (0.9) - should reach it faster
remaining_high = time_until_threshold(
current_score=1.0,
threshold=0.9,
last_used=now,
lambda_=2.673e-6,
)
# Lower threshold (0.1) - should take longer
remaining_low = time_until_threshold(
current_score=1.0,
threshold=0.1,
last_used=now,
lambda_=2.673e-6,
)
assert remaining_high is not None
assert remaining_low is not None
assert remaining_high < remaining_low
def test_time_until_threshold_never_reaches():
"""Test time_until_threshold when score never reaches threshold (edge case)."""
# Set up a scenario where the decay is extremely slow
config = Config(decay_model="power_law", pl_alpha=0.01, pl_halflife_days=10000.0)
set_config(config)
now = int(time.time())
# Very high current score, very low threshold, but decay is so slow it might not reach
remaining = time_until_threshold(
current_score=1.0,
threshold=0.00001,
last_used=now,
)
# Should either return a very large number or None
# This tests the upper bound cap logic
if remaining is None:
assert True # Never reaches
else:
assert remaining >= 0
def test_lambda_halflife_roundtrip():
"""Test roundtrip conversion: halflife -> lambda -> halflife."""
original_halflife = 5.0 # days
# Convert to lambda
lambda_val = calculate_decay_lambda(original_halflife)
# Convert back to halflife
result_halflife = calculate_halflife(lambda_val)
assert result_halflife == pytest.approx(original_halflife, rel=1e-6)
def test_halflife_lambda_roundtrip():
"""Test roundtrip conversion: lambda -> halflife -> lambda."""
original_lambda = 1.5e-6
# Convert to halflife
halflife = calculate_halflife(original_lambda)
# Convert back to lambda
result_lambda = calculate_decay_lambda(halflife)
assert result_lambda == pytest.approx(original_lambda, rel=1e-6)
def test_calculate_decay_lambda_different_halflifes():
"""Test calculate_decay_lambda with different halflife values."""
lambda_1d = calculate_decay_lambda(1.0)
lambda_7d = calculate_decay_lambda(7.0)
lambda_30d = calculate_decay_lambda(30.0)
# Longer halflife = smaller lambda (slower decay)
assert lambda_1d > lambda_7d > lambda_30d
def test_project_score_past_time():
"""Test project_score_at_time with past time (should show higher score)."""
now = int(time.time())
two_days_ago = now - 2 * 86400
one_day_ago = now - 86400
# Project to past time
projected_past = project_score_at_time(
use_count=5,
last_used=two_days_ago,
strength=1.0,
target_time=one_day_ago,
lambda_=2.673e-6,
beta=0.6,
)
# Current score (2 days after last_used)
current = calculate_score(
use_count=5,
last_used=two_days_ago,
strength=1.0,
now=now,
lambda_=2.673e-6,
beta=0.6,
)
# Past projection should have higher score
assert projected_past > current
def test_project_score_different_models():
"""Test project_score_at_time with different decay models."""
now = int(time.time())
future = now + 86400
# Power law
config_pl = Config(decay_model="power_law", pl_alpha=1.1, pl_halflife_days=3.0)
set_config(config_pl)
score_pl = project_score_at_time(
use_count=5,
last_used=now,
strength=1.0,
target_time=future,
)
# Two component
config_tc = Config(
decay_model="two_component",
tc_lambda_fast=1.603e-5,
tc_lambda_slow=1.147e-6,
tc_weight_fast=0.7,
)
set_config(config_tc)
score_tc = project_score_at_time(
use_count=5,
last_used=now,
strength=1.0,
target_time=future,
)
# Exponential
config_exp = Config(decay_model="exponential", decay_lambda=2.673e-6)
set_config(config_exp)
score_exp = project_score_at_time(
use_count=5,
last_used=now,
strength=1.0,
target_time=future,
)
# All should produce valid scores
assert score_pl > 0
assert score_tc > 0
assert score_exp > 0