Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
199 changes: 199 additions & 0 deletions src/emotigrad/personalities.py
Original file line number Diff line number Diff line change
Expand Up @@ -74,13 +74,212 @@ def __call__(
return f"🔎 Step {step}: current loss {loss:.4f}"


def nervous(loss: float, prev_loss: Optional[float], step: int) -> Optional[str]:
"""A nervous, anxious personality that worries about everything."""
if prev_loss is None:
return f"😰 Oh no, here we go... Initial loss is {loss:.4f}. I hope this works..."

if loss < prev_loss:
return (
f"😅 Phew! Loss dropped from {prev_loss:.4f} to {loss:.4f}. "
"But what if it goes back up?!"
)

if loss > prev_loss:
return (
f"😱 I KNEW IT! Loss went up from {prev_loss:.4f} to {loss:.4f}! "
"Is everything okay?!"
)

return f"😬 Loss is exactly the same... {loss:.4f}. That's... concerning?"


def chaotic(loss: float, prev_loss: Optional[float], step: int) -> Optional[str]:
"""A chaotic, unpredictable personality that says random things."""
import random

if prev_loss is None:
chaos_starts = [
f"🎲 CHAOS BEGINS! Loss: {loss:.4f}! LET'S GOOOOO!",
f"🌪️ *appears from nowhere* Oh, we're training? Loss is {loss:.4f}!",
f"🃏 Wild card activated! Starting loss: {loss:.4f}!",
]
return random.choice(chaos_starts)

if loss < prev_loss:
good_chaos = [
f"🎉 YEET! {prev_loss:.4f} → {loss:.4f}! *does a backflip*",
f"🦄 Loss improved! {prev_loss:.4f} → {loss:.4f}! Is this magic?!",
f"🚀 TO THE MOON! Well, to lower loss at least: {loss:.4f}!",
]
return random.choice(good_chaos)

if loss > prev_loss:
bad_chaos = [
f"💥 BOOM! Loss exploded: {prev_loss:.4f} → {loss:.4f}! EXCITING!",
f"🎢 Wheeeee! Loss went UP to {loss:.4f}! What a ride!",
f"🔥 This is fine. Loss: {loss:.4f}. Everything is fine. 🔥",
]
return random.choice(bad_chaos)

return f"🌀 Time is a flat circle. Loss: {loss:.4f}. Always has been."


def arrogant(loss: float, prev_loss: Optional[float], step: int) -> Optional[str]:
"""An arrogant, condescending personality that thinks it knows better."""
if prev_loss is None:
return (
f"🧐 *adjusts monocle* Initial loss of {loss:.4f}? "
"I suppose that's... acceptable for a beginner."
)

if loss < prev_loss:
return (
f"😏 Obviously the loss improved ({prev_loss:.4f} → {loss:.4f}). "
"You're welcome for my guidance."
)

if loss > prev_loss:
return (
f"🙄 Loss increased to {loss:.4f}? "
"Perhaps you should have listened to my earlier suggestions."
)

return f"😤 No change at {loss:.4f}. Clearly, you need my expertise more than ever."


def tired(loss: float, prev_loss: Optional[float], step: int) -> Optional[str]:
"""A tired, exhausted personality that just wants this to be over."""
if prev_loss is None:
return f"😴 *yawn* Oh, we're starting? Loss is {loss:.4f}... wake me when it's over."

if loss < prev_loss:
return (
f"😪 Cool, loss went down... {prev_loss:.4f} → {loss:.4f}... "
"can I go back to sleep now?"
)

if loss > prev_loss:
return (
f"😩 Ugh, loss went up to {loss:.4f}. Of course it did. "
"I'm too tired for this."
)

return f"💤 Loss is still {loss:.4f}... zzzz..."


def hype(loss: float, prev_loss: Optional[float], step: int) -> Optional[str]:
"""An extremely hyped, enthusiastic personality."""
if prev_loss is None:
return (
f"🔥🔥🔥 LET'S GOOOOOO!!! Initial loss: {loss:.4f}! "
"THIS IS GONNA BE AMAZING!!!"
)

if loss < prev_loss:
return (
f"🎊🎊🎊 YOOOOO!!! LOSS DROPPED FROM {prev_loss:.4f} TO {loss:.4f}!!! "
"WE'RE LITERALLY UNSTOPPABLE!!! 💪💪💪"
)

if loss > prev_loss:
return (
f"😤😤😤 OKAY SO LOSS WENT UP TO {loss:.4f} BUT THAT'S JUST "
"MAKING THE COMEBACK EVEN MORE EPIC!!! LET'S GO!!!"
)

return f"⚡⚡⚡ LOSS HOLDING STEADY AT {loss:.4f}!!! THE TENSION IS REAL!!!"


def academic(loss: float, prev_loss: Optional[float], step: int) -> Optional[str]:
"""An academic, research-paper style personality."""
if prev_loss is None:
return (
f"📊 Initial observation: loss function yields {loss:.4f}. "
"Proceeding with gradient descent optimization."
)

delta = loss - prev_loss
pct_change = (delta / prev_loss) * 100 if prev_loss != 0 else 0

if loss < prev_loss:
return (
f"📈 Statistically significant improvement observed. "
f"Loss decreased from {prev_loss:.4f} to {loss:.4f} "
f"(Δ = {delta:.4f}, {pct_change:.2f}% reduction)."
)

if loss > prev_loss:
return (
f"📉 Note: Loss increased from {prev_loss:.4f} to {loss:.4f} "
f"(Δ = {delta:.4f}, {abs(pct_change):.2f}% increase). "
"Further investigation may be warranted."
)

return (
f"📋 No statistically significant change detected. "
f"Loss remains at {loss:.4f}. Null hypothesis cannot be rejected."
)


def pirate(loss: float, prev_loss: Optional[float], step: int) -> Optional[str]:
"""A pirate-themed personality. Arrr!"""
if prev_loss is None:
return f"🏴‍☠️ Ahoy! We be settin' sail! Initial loss be {loss:.4f}, matey!"

if loss < prev_loss:
return (
f"⚓ Shiver me timbers! Loss dropped from {prev_loss:.4f} to {loss:.4f}! "
"That be treasure, arr!"
)

if loss > prev_loss:
return (
f"☠️ Blimey! Loss went up to {loss:.4f}! "
"We be sailin' into rough waters, ye scallywag!"
)

return f"🦜 The seas be calm, loss steady at {loss:.4f}. Onwards, me hearties!"


def zen(loss: float, prev_loss: Optional[float], step: int) -> Optional[str]:
"""A zen, peaceful personality focused on the journey."""
if prev_loss is None:
return (
f"🧘 The journey of a thousand gradients begins with a single step. "
f"Loss: {loss:.4f}."
)

if loss < prev_loss:
return (
f"☯️ Like water flowing downhill, the loss descends: "
f"{prev_loss:.4f} → {loss:.4f}. Breathe."
)

if loss > prev_loss:
return (
f"🍃 The wind sometimes blows against us. Loss: {loss:.4f}. "
"This too shall pass."
)

return f"🌸 Stillness. Loss remains at {loss:.4f}. Find peace in the plateau."


# --- Registry ---------------------------------------------------------------


_PERSONALITY_REGISTRY: Dict[str, Personality] = {
"wholesome": wholesome,
"sassy": sassy,
"quiet": QuietPersonality(), # instance is fine; it's still callable
"nervous": nervous,
"chaotic": chaotic,
"arrogant": arrogant,
"tired": tired,
"hype": hype,
"academic": academic,
"pirate": pirate,
"zen": zen,
}


Expand Down
163 changes: 163 additions & 0 deletions tests/test_personalities.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,163 @@
# tests/test_personalities.py
"""Tests for the built-in personalities."""

import pytest

from emotigrad.personalities import (
academic,
arrogant,
chaotic,
get_personality,
hype,
list_personalities,
nervous,
pirate,
register_personality,
sassy,
tired,
wholesome,
zen,
)


class TestPersonalityFunctions:
"""Test individual personality functions."""

@pytest.mark.parametrize(
"personality_fn",
[wholesome, sassy, nervous, arrogant, tired, hype, academic, pirate, zen],
)
def test_personality_returns_string_on_first_step(self, personality_fn):
"""All personalities should return a string message on the first step."""
result = personality_fn(loss=1.0, prev_loss=None, step=1)
assert isinstance(result, str)
assert len(result) > 0

@pytest.mark.parametrize(
"personality_fn",
[wholesome, sassy, nervous, arrogant, tired, hype, academic, pirate, zen],
)
def test_personality_returns_string_on_loss_decrease(self, personality_fn):
"""All personalities should return a string when loss decreases."""
result = personality_fn(loss=0.5, prev_loss=1.0, step=2)
assert isinstance(result, str)
assert len(result) > 0

@pytest.mark.parametrize(
"personality_fn",
[wholesome, sassy, nervous, arrogant, tired, hype, academic, pirate, zen],
)
def test_personality_returns_string_on_loss_increase(self, personality_fn):
"""All personalities should return a string when loss increases."""
result = personality_fn(loss=1.5, prev_loss=1.0, step=2)
assert isinstance(result, str)
assert len(result) > 0

@pytest.mark.parametrize(
"personality_fn",
[wholesome, sassy, nervous, arrogant, tired, hype, academic, pirate, zen],
)
def test_personality_handles_equal_loss(self, personality_fn):
"""All personalities should handle equal loss (may return None or string)."""
result = personality_fn(loss=1.0, prev_loss=1.0, step=2)
# Result can be None or a string
assert result is None or isinstance(result, str)


class TestChaoticPersonality:
"""Special tests for chaotic personality due to its random nature."""

def test_chaotic_returns_different_messages(self):
"""Chaotic personality should have variety in its messages."""
# Run multiple times to increase chance of seeing variation
messages = set()
for _ in range(20):
msg = chaotic(loss=0.5, prev_loss=1.0, step=2)
messages.add(msg)

# We expect at least 2 different messages out of 20 attempts
# (there are 3 possible messages for loss decrease)
assert len(messages) >= 2


class TestAcademicPersonality:
"""Special tests for academic personality to verify statistical output."""

def test_academic_includes_delta(self):
"""Academic personality should include delta in loss change messages."""
result = academic(loss=0.8, prev_loss=1.0, step=2)
assert "Δ" in result or "delta" in result.lower() or "-0.2" in result

def test_academic_includes_percentage(self):
"""Academic personality should include percentage change."""
result = academic(loss=0.8, prev_loss=1.0, step=2)
assert "%" in result


class TestRegistry:
"""Tests for the personality registry."""

def test_list_personalities_includes_new_personalities(self):
"""The registry should include all new personalities."""
available = list_personalities()
expected = [
"wholesome",
"sassy",
"quiet",
"nervous",
"chaotic",
"arrogant",
"tired",
"hype",
"academic",
"pirate",
"zen",
]
for name in expected:
assert name in available, f"'{name}' should be in the registry"

def test_get_personality_returns_callable(self):
"""get_personality should return callable personalities."""
for name in list_personalities():
personality = get_personality(name)
assert callable(personality)

def test_get_personality_case_insensitive(self):
"""get_personality should be case-insensitive."""
assert get_personality("NERVOUS") == get_personality("nervous")
assert get_personality("Academic") == get_personality("academic")

def test_get_personality_raises_on_unknown(self):
"""get_personality should raise KeyError for unknown personalities."""
with pytest.raises(KeyError):
get_personality("nonexistent_personality")

def test_register_personality(self):
"""Test registering a custom personality."""

def custom_personality(loss, prev_loss, step):
return "Custom message"

register_personality("custom_test", custom_personality)

assert "custom_test" in list_personalities()
assert get_personality("custom_test") == custom_personality

def test_register_personality_prevents_overwrite_by_default(self):
"""Registering an existing name should raise ValueError."""
with pytest.raises(ValueError):
register_personality("wholesome", lambda l, p, s: "test")

def test_register_personality_allows_overwrite_when_specified(self):
"""Overwrite should work when explicitly allowed."""

def new_wholesome(loss, prev_loss, step):
return "New wholesome"

register_personality("wholesome", new_wholesome, overwrite=True)
assert get_personality("wholesome") == new_wholesome

# Restore original
from emotigrad.personalities import wholesome as original_wholesome

register_personality("wholesome", original_wholesome, overwrite=True)
Loading