Skip to content

Mood

emotional_memory.mood.MoodField

Bases: BaseModel

Global mood field — slow EMA of accumulated CoreAffect history.

valence : [-1.0, +1.0] negative ↔ positive arousal : [ 0.0, 1.0] calm ↔ highly activated dominance : [ 0.0, 1.0] submissive ↔ dominant/in-control inertia : [ 0.0, 1.0] 0=volatile, 1=completely stable

distance(other: MoodField) -> float

Euclidean distance in valence-arousal-dominance space.

Source code in src/emotional_memory/mood.py
def distance(self, other: MoodField) -> float:
    """Euclidean distance in valence-arousal-dominance space."""
    return math.sqrt(
        (self.valence - other.valence) ** 2
        + (self.arousal - other.arousal) ** 2
        + (self.dominance - other.dominance) ** 2
    )

regress(now: datetime, config: MoodDecayConfig) -> MoodField

Return a new MoodField regressed toward baseline by elapsed time.

Uses exponential decay

value(t) = baseline + (value - baseline) * exp(-t * ln2 / half_life)

Effective half-life is modulated by inertia: high-inertia moods resist regression (as in trauma or persistent elation).

Source code in src/emotional_memory/mood.py
def regress(self, now: datetime, config: MoodDecayConfig) -> MoodField:
    """Return a new MoodField regressed toward baseline by elapsed time.

    Uses exponential decay:
        value(t) = baseline + (value - baseline) * exp(-t * ln2 / half_life)

    Effective half-life is modulated by inertia: high-inertia moods
    resist regression (as in trauma or persistent elation).
    """
    elapsed = max(0.0, (now - self.timestamp).total_seconds())
    eff_hl = config.base_half_life_seconds * (1.0 + self.inertia * config.inertia_scale)
    if eff_hl <= 0.0:
        return self
    factor = math.exp(-elapsed * math.log(2) / eff_hl)
    return MoodField(
        valence=config.baseline_valence + (self.valence - config.baseline_valence) * factor,
        arousal=config.baseline_arousal + (self.arousal - config.baseline_arousal) * factor,
        dominance=config.baseline_dominance
        + (self.dominance - config.baseline_dominance) * factor,
        inertia=self.inertia,
        timestamp=now,
    )

update(core_affect: CoreAffect, alpha: float = 0.1, now: datetime | None = None, decay_config: MoodDecayConfig | None = None) -> MoodField

Return a new MoodField updated via exponential moving average.

If decay_config is provided, temporal regression toward baseline is applied first (accounting for elapsed silence since last update), then the EMA step runs on the regressed state.

Effective alpha is modulated by inertia

effective_alpha = alpha * (1 - inertia)

High inertia → mood barely moves per event. Low inertia → mood follows affect closely.

Dominance is a first-class PAD dimension on CoreAffect and is carried into the mood via the same EMA as valence and arousal (no separate valence/arousal-derived heuristic).

Source code in src/emotional_memory/mood.py
def update(
    self,
    core_affect: CoreAffect,
    alpha: float = 0.1,
    now: datetime | None = None,
    decay_config: MoodDecayConfig | None = None,
) -> MoodField:
    """Return a new MoodField updated via exponential moving average.

    If ``decay_config`` is provided, temporal regression toward baseline is
    applied first (accounting for elapsed silence since last update), then
    the EMA step runs on the regressed state.

    Effective alpha is modulated by inertia:
        effective_alpha = alpha * (1 - inertia)

    High inertia → mood barely moves per event.
    Low inertia  → mood follows affect closely.

    Dominance is a first-class PAD dimension on ``CoreAffect`` and is carried
    into the mood via the same EMA as valence and arousal (no separate
    valence/arousal-derived heuristic).
    """
    if now is None:
        now = datetime.now(tz=UTC)

    # Step 1: regress toward baseline for elapsed silent time
    base = self.regress(now, decay_config) if decay_config is not None else self

    eff_alpha = alpha * (1.0 - base.inertia)
    new_valence = (1.0 - eff_alpha) * base.valence + eff_alpha * core_affect.valence
    new_arousal = (1.0 - eff_alpha) * base.arousal + eff_alpha * core_affect.arousal
    # Dominance: read directly from CoreAffect (now a first-class PAD dimension).
    new_dominance = (1.0 - eff_alpha) * base.dominance + eff_alpha * core_affect.dominance
    return MoodField(
        valence=new_valence,
        arousal=new_arousal,
        dominance=new_dominance,
        inertia=base.inertia,
        timestamp=now,
    )

emotional_memory.mood.MoodDecayConfig

Bases: BaseModel

Parameters for time-based regression of MoodField toward baseline.

Implements exponential decay toward a resting baseline

regressed = baseline + (current - baseline) * exp(-t * ln2 / half_life)

The effective half-life is modulated by inertia so that a rigid mood (high inertia) regresses more slowly than a volatile one.

base_half_life_seconds: float = 3600.0 class-attribute instance-attribute

Base half-life in seconds (default 1 hour, must be > 0). Modulated by inertia.

baseline_arousal: float = 0.3 class-attribute instance-attribute

Resting arousal attractor (not zero — resting alertness, not inert).

baseline_dominance: float = 0.5 class-attribute instance-attribute

Resting dominance attractor (balanced perceived control).

baseline_valence: float = 0.0 class-attribute instance-attribute

Resting valence attractor (neutral).

inertia_scale: float = 2.0 class-attribute instance-attribute

Multiplier on inertia: effective_half_life = base * (1 + inertia * scale).