Skip to main content

Emotion algebra based on Plutchik's Wheel and Cambria's Hourglass of Emotions — signed integer arithmetic over a 4-axis affective space

Project description

emotion-algebra

Signed integer arithmetic over a 4-axis affective space, grounded in Plutchik's Wheel of Emotions (1980) and Cambria's Hourglass of Emotions (2012). Emotions are first-class mathematical objects; every operator returns a typed result.

from emotion_algebra.emotions import get_emotion

anger = get_emotion("anger")

anger + 1          # → rage          (intensity up)
anger - 1          # → annoyance     (intensity down)
-anger             # → fear          (opposite pole)
anger >> 2         # → hyper anger
anger * fear       # → CompositeEmotion (cross-axis product)

Installation

pip install emotion-algebra

Optional extras:

Extra Deps Enables
[lexicon] pandas lexicons.py — word→emotion CSV lookup
[fast] ahocorasick-ner Phrase-aware Aho-Corasick backend for score_text/from_text
[viz] matplotlib viz.py — Plutchik wheel, circumplex, timeline, Lövheim cube
[all] all of the above everything

deepmoji-onnx is a core dependency — neural text→emoji→emotion scoring via DeepMojiONNXAdapter is always available.


The model

Four axes (Cambria 2012)

Axis Positive pole Negative pole Hedonic?
Sensitivity rage → anger → annoyance apprehension → fear → terror No
Attention vigilance → anticipation → interest distraction → surprise → amazement No
Pleasantness ecstasy → joy → serenity pensiveness → sadness → grief Yes
Aptitude admiration → trust → acceptance boredom → disgust → loathing Yes

Each axis has three integer intensity levels: ±1 (mild), ±2 (primary), ±3 (intense).

Valence, polarity, and arousal

  • Valence — the Pleasantness axis alone, the hedonic axis. anger.valence == 0; joy.valence == 2.
  • Polarity — the overall sentiment score, using Cambria's published four-axis formula (P + |At| − |S| + Ap) / 3. trust.valence == 0 but trust.polarity == +0.22.
  • Arousalabs(emotional_flow). Axis-independent activation intensity.
  • Type — Russell Circumplex quadrant, classified on polarity.

Valence and polarity are deliberately different quantities — see docs/valence_arousal.md.

Feelings (Plutchik dyads)

Adding two emotions on different axes builds a CompositeEmotion. The named dyad is a Feeling, looked up from its two components:

from emotion_algebra.emotions import get_emotion
from emotion_algebra.feelings import get_feeling_from_emotions

joy   = get_emotion("joy")
trust = get_emotion("trust")

joy + trust                              # → CompositeEmotion (the raw cross-axis sum)
get_feeling_from_emotions("joy", "trust")  # → 'love'          (the named dyad)

Ten names are claimed by both the Feeling and CompositeEmotion lineages. Use resolve() — it prefers the Feeling and the preference is explicit, not an accident of import order. See docs/taxonomy.md.


Feature overview

Feature API Module
Core emotion algebra Emotion, +, -, *, <<, >> plutchik.py
Named feelings (dyads) Feeling, get_feeling() feelings.py
Multi-axis composites CompositeEmotion composite_emotions.py
Continuous space FloatEmotion, from_embedding() float_emotion.py
Stateful accumulation EmotionalState, EmotionTimeline state.py
Word lexicon from_text(), score_text() text.py
Emoji mapping from_emoji(), score_emojis(), DeepMojiAdapter emoji.py
Mixed word+emoji score_mixed(), from_mixed() text.py
Geometry emotion_distance(), closest_emotion() distance.py
Cognitive appraisal Appraisal, appraisal_to_emotion() appraisal.py
Continuous appraisal appraisal_to_float_emotion(), appraisal_to_lovheim() appraisal.py
Emotion blending FloatEmotion.blend(joy, trust) float_emotion.py
Need-deficit emotions need_deficit_to_emotion(), CIADrive, MaxNeefNeed, MurrayNeed needs.py
Behavioural reactions REACTIONS, REACTION_TO_EMOTION_MAP behaviour.py
Neurochemistry LovheimPoint, closest_affect(), affect_blend() lovheim.py
PAD / VAD interop to_pad(), from_pad(), pad_distance() pad.py
Name resolution resolve(), is_ambiguous(), collision_table() taxonomy.py
Similarity emotion_similarity() (distance or cosine) distance.py
Serialization to_dict()/from_dict(), to_json()/from_json(), versioned schema serialization.py
Text analysis analyze() — spans, negation, intensifiers text.py
Plots plot_wheel(), plot_circumplex(), plot_timeline(), plot_lovheim() viz.py
CLI python -m emotion_algebra __main__.py

Highlights

Neurochemistry — Lövheim's cube

Three monoamine axes whose eight corners are Tomkins' basic affects (Lövheim 2012). Bidirectionally bridged to the Hourglass space, so an agent's emotional state has a live neurochemical readout:

from emotion_algebra import EmotionalState, get_emotion

state = EmotionalState()
state.apply(get_emotion("ecstasy"))

state.to_lovheim()                  # LovheimPoint(serotonin=1, dopamine=1, noradrenaline=0)
state.to_lovheim().closest_affect() # 'enjoyment/joy'
state.to_lovheim().affect_blend()   # exact distribution over all eight affects

The module documents two properties it provably cannot have — it cannot reach positive Aptitude (Tomkins has no trust affect), and the bridge is not injective (Plutchik's opposites are exact negatives, and Lövheim puts them at adjacent corners). Both are theorems, and both are locked by tests. See docs/lovheim.md.

Text analysis with valence shifters

from emotion_algebra.text import analyze

analyze("I am joyful").dominant.name            # 'joy'
analyze("I am not joyful").dominant.name        # 'sadness'    (negation)
analyze("I am very joyful").dominant.name       # 'ecstasy'    (intensifier)
analyze("I am slightly joyful").dominant.name   # 'serenity'   (downtoner)

Windowed negation and intensifier handling, following Taboada et al. (2011).

It is an actual algebra

The laws are written down in docs/laws.md and machine-checked with hypothesis — identity, involution of negation, associativity (and exactly where it fails), metric axioms, conversion coherence.


Quick reference

from emotion_algebra import (
    get_emotion, get_feeling,
    EmotionalState, EmotionTimeline,
    from_text, score_text, score_mixed, from_mixed,
    from_emoji, score_emojis, DeepMojiAdapter,
    register_emoji, unregister_emoji,
    emotion_distance, closest_emotion, emotion_clusters,
    Appraisal, appraisal_to_emotion, appraisal_to_float_emotion,
    float_emotion_to_neuro_deltas,
    FloatEmotion,
    CIADrive, MaxNeefNeed, MurrayNeed,
    need_deficit_to_emotion, need_deficit_to_float_emotion,
)

# --- Emotion properties ---
e = get_emotion("joy")
e.emotional_flow   # 2
e.valence          # 2    (pleasantness axis)
e.arousal          # 2
e.type             # "excited positive"
e.opposite_emotion # sadness
e.as_array         # np.array([0, 0, 2, 0])

# --- State accumulation ---
state = EmotionalState()
state.apply(get_emotion("joy"), weight=0.8)
state.apply(get_emotion("trust"), weight=0.5)
state.decay(0.9)
state.dominant()   # → Emotion or None

# --- Text analysis ---
from_text("rage and fury")          # → Emotion (lexicon)
score_mixed("I'm so happy 😄🎉")   # → EmotionalState (words + emoji)
from_mixed("grief 😭")             # → dominant Emotion

# --- Emoji ---
from_emoji("😊")                   # → Emotion("serenity")
register_emoji("🤖", "trust")      # custom mapping

# --- DeepMoji bridge ---
adapter = DeepMojiAdapter()
adapter.from_scores({"😂": 0.6, "😭": 0.4})  # → Emotion
adapter.score_state({"😂": 0.6, "😭": 0.4})  # → EmotionalState

# --- Geometry ---
a, b = get_emotion("anger"), get_emotion("joy")
emotion_distance(a, b)             # Euclidean distance in 4D Hourglass space
closest_emotion([2, 0, 1, 0])     # nearest named Emotion to a float vector

# --- Cognitive appraisal (Scherer CPM) ---
# Discrete (v1): categorical fields → named Emotion
a = Appraisal(goal_relevance="relevant", goal_congruence="incongruent",
              agency="other", coping_potential="low")
appraisal_to_emotion(a)           # → fear

# Continuous (v2): float fields → FloatEmotion → neuro deltas
a = Appraisal(novelty=0.8, goal_relevance=0.9, goal_congruence=0.3,
              coping_potential=0.2, intrinsic_pleasantness=0.4)
fe = appraisal_to_float_emotion(a)
d, s, adr = float_emotion_to_neuro_deltas(fe)  # (dopamine, serotonin, adrenaline)

# --- Emotion blending ---
joy   = get_emotion("joy")
trust = get_emotion("trust")
FloatEmotion.blend(joy, trust)                 # → pleasantness + aptitude
FloatEmotion.blend(joy, trust, weights=[0.7, 0.3], scale=0.8)

# --- Need-deficit emotions (Max-Neef + Murray) ---
need_deficit_to_emotion(MaxNeefNeed.PROTECTION)  # → fear
need_deficit_to_emotion(MurrayNeed.NURTURANCE)   # → sadness
need_deficit_to_float_emotion("freedom")          # → FloatEmotion (anger axis)

# --- Continuous space ---
FloatEmotion(sensitivity=1.5, pleasantness=-0.8)
FloatEmotion.from_embedding(np.array([0.3, -0.1, 0.7, 0.2]))

CLI

# Info about an emotion, feeling, or dimension
python -m emotion_algebra info anger
python -m emotion_algebra info love
python -m emotion_algebra info pleasantness

# Evaluate an expression
python -m emotion_algebra "joy + trust"
python -m emotion_algebra "rage - 1"

# Single emoji
python -m emotion_algebra 😊

# Interactive REPL (all 24 emotions pre-loaded)
python -m emotion_algebra

Scientific references

  • Plutchik, R. (1980). A general psychoevolutionary theory of emotion. In R. Plutchik & H. Kellerman (Eds.), Emotion: Theory, research, and experience (Vol. 1, pp. 3–33).
  • Cambria, E., Livingstone, A., & Hussain, A. (2012). The Hourglass of Emotions. In A. Esposito et al. (Eds.), Cognitive Behavioural Systems, LNCS 7403.
  • Russell, J. A. (1980). A circumplex model of affect. Journal of Personality and Social Psychology, 39(6), 1161–1178.
  • Posner, J., Russell, J. A., & Peterson, B. S. (2005). The circumplex model of affect: An integrative approach. Development and Psychopathology, 17(3), 715–734.
  • Felbo, B., Mislove, A., Søgaard, A., Rahwan, I., & Lehmann, S. (2017). Using millions of emoji occurrences to learn any-domain representations for detecting sentiment, emotion and sarcasm. EMNLP 2017.
  • Scherer, K. R. (2001). Appraisal considered as a process of multilevel sequential checking. In K. R. Scherer et al. (Eds.), Appraisal processes in emotion (pp. 92–120).
  • Max-Neef, M. (1991). Human Scale Development: Conception, Application and Further Reflections. Apex Press.
  • Murray, H. A. (1938). Explorations in Personality. Oxford University Press.
  • Lövheim, H. (2012). A new three-dimensional model for emotions and monoamine neurotransmitters. Medical Hypotheses, 78(2), 341–348.

License

Apache 2.0

Project details


Download files

Download the file for your platform. If you're not sure which to choose, learn more about installing packages.

Source Distribution

emotion_algebra-2.0.0a1.tar.gz (647.3 kB view details)

Uploaded Source

Built Distribution

If you're not sure about the file name format, learn more about wheel file names.

emotion_algebra-2.0.0a1-py3-none-any.whl (618.9 kB view details)

Uploaded Python 3

File details

Details for the file emotion_algebra-2.0.0a1.tar.gz.

File metadata

  • Download URL: emotion_algebra-2.0.0a1.tar.gz
  • Upload date:
  • Size: 647.3 kB
  • Tags: Source
  • Uploaded using Trusted Publishing? No
  • Uploaded via: twine/6.1.0 CPython/3.13.12

File hashes

Hashes for emotion_algebra-2.0.0a1.tar.gz
Algorithm Hash digest
SHA256 c58df3ec1c02ff7fc1443ed7b99422c3d258aed0a905b35ebb78280eef517dea
MD5 15b087af0f0f10deab7d47c34de8d8f7
BLAKE2b-256 7d33bb7c4c24ab8351765b5b3ae792734d15df3cab2849272acf9c4f8b34ebc6

See more details on using hashes here.

File details

Details for the file emotion_algebra-2.0.0a1-py3-none-any.whl.

File metadata

File hashes

Hashes for emotion_algebra-2.0.0a1-py3-none-any.whl
Algorithm Hash digest
SHA256 8ea30f2f98566718af50bef9810fb4a1f5f8a2e828a2c458e9be480d4b9195f6
MD5 f8bb1c0a9fc1e13c98cbe1eefe4ec346
BLAKE2b-256 2f34e87f47dc82776b7a2dadb1bb55823d051b9618da579579aea15936b663a6

See more details on using hashes here.

Supported by

AWS Cloud computing and Security Sponsor Datadog Monitoring Depot Continuous Integration Fastly CDN Google Download Analytics Pingdom Monitoring Sentry Error logging StatusPage Status page