Source code for mmm_framework.config.interactions

"""Cross-channel interaction / synergy configuration (#142).

The base model is strictly additive across channels, so it cannot express that
two channels together do more (synergy / halo — TV priming Search) or less
(cannibalization) than the sum of their parts. A :class:`ChannelInteraction`
adds a ``beta_ij · sat(x_i) · sat(x_j)`` term for a named pair, with a
sign-aware prior shrunk toward zero.
"""

from __future__ import annotations

from typing import Literal

from pydantic import BaseModel, Field, field_validator


[docs] class ChannelInteraction(BaseModel): """One named channel-pair interaction term. ``expected_sign`` encodes the belief: ``"positive"`` (synergy) uses a HalfNormal so the effect can only lift; ``"negative"`` (cannibalization) its reflection; ``"any"`` a Normal centered at 0. Either way the prior shrinks toward zero, since interactions are weakly identified without designed variation. """ channel_a: str channel_b: str prior_sigma: float = Field(default=0.3, gt=0) expected_sign: Literal["positive", "negative", "any"] = "any" model_config = {"extra": "forbid"} @field_validator("channel_b") @classmethod def _distinct(cls, v: str, info) -> str: if v == info.data.get("channel_a"): raise ValueError("A channel cannot interact with itself.") return v @property def name(self) -> str: return f"{self.channel_a}_x_{self.channel_b}"