Skip to content

Decay

emotional_memory.decay.DecayConfig

Bases: BaseModel

Parameters controlling how consolidation strength decays over time.

arousal_modulation: float = 0.5 class-attribute instance-attribute

How much high arousal slows decay. 0 = no effect, 1 = fully suppressed at max arousal.

base_decay: float = 0.5 class-attribute instance-attribute

Power-law decay exponent baseline. Higher = faster decay.

floor_arousal_threshold: float = 0.7 class-attribute instance-attribute

Memories encoded above this arousal level have a minimum strength floor.

floor_value: float = 0.1 class-attribute instance-attribute

Minimum consolidation_strength for high-arousal memories.

min_seconds: float = 1.0 class-attribute instance-attribute

Avoid division by zero: treat elapsed < min_seconds as min_seconds.

power: float = 1.0 class-attribute instance-attribute

Scaling exponent applied to the decay formula. Values > 1 accelerate decay; values < 1 slow it. 1.0 = standard ACT-R power-law.

retrieval_boost: float = 0.1 class-attribute instance-attribute

Reduction in decay exponent per retrieval (spacing effect).

emotional_memory.decay.compute_effective_strength(tag: EmotionalTag, now: datetime, config: DecayConfig) -> float

Compute current consolidation strength given elapsed time.

Formula

effective_decay = base_decay * (1 - arousal_modulation * arousal_at_encoding) * (1 / (1 + retrieval_boost * retrieval_count))

strength(t) = initial_strength * elapsed_seconds ^ (-effective_decay) strength(t) = max(strength(t), min(floor, initial)) # if arousal > threshold

The floor is clamped to the initial strength so it can never raise a weak memory above where it started.

At t=0 (elapsed < min_seconds), returns initial consolidation_strength.

Source code in src/emotional_memory/decay.py
def compute_effective_strength(
    tag: EmotionalTag,
    now: datetime,
    config: DecayConfig,
) -> float:
    """Compute current consolidation strength given elapsed time.

    Formula:
        effective_decay = base_decay
                          * (1 - arousal_modulation * arousal_at_encoding)
                          * (1 / (1 + retrieval_boost * retrieval_count))

        strength(t) = initial_strength * elapsed_seconds ^ (-effective_decay)
        strength(t) = max(strength(t), min(floor, initial))  # if arousal > threshold

    The floor is clamped to the initial strength so it can never raise a weak
    memory above where it started.

    At t=0 (elapsed < min_seconds), returns initial consolidation_strength.
    """
    elapsed = (now - tag.timestamp).total_seconds()
    elapsed = max(elapsed, config.min_seconds)

    arousal = tag.core_affect.arousal
    effective_decay = (
        config.base_decay
        * (1.0 - config.arousal_modulation * arousal)
        * (1.0 / (1.0 + config.retrieval_boost * tag.retrieval_count))
    )
    effective_decay = max(effective_decay, 0.0)

    strength = tag.consolidation_strength * (elapsed ** (-effective_decay * config.power))
    # Decay must never boost strength above the initial consolidation value.
    # (sub-min_seconds elapsed times can produce elapsed^(-x) > 1 when min_seconds < 1)
    strength = min(strength, tag.consolidation_strength)
    strength = max(0.0, strength)

    if arousal >= config.floor_arousal_threshold:
        # Clamp the floor to the initial value: the arousal floor must not boost a
        # weak memory (consolidation_strength < floor_value) above where it started,
        # preserving the "never above initial" invariant enforced just above.
        strength = max(strength, min(config.floor_value, tag.consolidation_strength))

    return float(strength)