hv-writhe-stories

Narrative writhe: a topological invariant for stories. Encode a story as a sequence of signed beats. The sum is the writhe W. Writhe mod 2 is preserved by narrative R2 and R3 moves, and broken by R1. Applied to ten canonical narratives.

Size: ~20 KB source, no weights. Runtime: <1 s for the full benchmark. Dependencies: NumPy only.

The primitive

A story is a sequence of beats, each with a valence in {βˆ’2, …, +2}:

valence interpretation
+2 strong positive turn toward the protagonist's goal
+1 small positive turn
0 neutral / transitional
βˆ’1 small negative turn
βˆ’2 strong negative turn (loss, betrayal, death)

Writhe W = Ξ£ valence(beat_k) β€” the sum.

Trajectory T_k = Ξ£_{j≀k} valence(beat_j) β€” the protagonist's fortune over time.

Sign class: comedic (W > 0), tragic (W < 0), balanced (W = 0).

Mod-2 class: W mod 2. A single-bit invariant.

The three Reidemeister moves on stories

move operation effect on W effect on mod-2
R1 insert one beat with valence d W β†’ W + d flips iff d odd
R2 insert a matched pair (+m, βˆ’m) W β†’ W preserved
R3 swap two non-adjacent beats W β†’ W preserved

Proposition. Writhe mod 2 is invariant under R2 and R3, and changes by d mod 2 under R1 with valence d.

The moves have narrative interpretations:

  • R1 = add a self-contained beat that changes the outcome (a late triumph or tragedy).
  • R2 = add a false alarm: a setback immediately reversed.
  • R3 = reorder non-adjacent beats (non-chronological retelling).

R2 and R3 preserve writhe. R1 preserves mod-2 only for even valences.

Headline numbers

Ten canonical narratives, hand-encoded:

story genre beats W mod-2 class
Oedipus Rex tragedy 6 βˆ’6 0 tragic
Hamlet tragedy 6 βˆ’7 1 tragic
Romeo and Juliet tragedy 5 βˆ’3 1 tragic
Breaking Bad tragedy 6 βˆ’7 1 tragic
Pride and Prejudice comedy 5 +2 0 comedic
Fleabag comedy 5 0 0 balanced
Hero's Journey myth 7 βˆ’1 1 tragic
The Lord of the Rings myth 6 βˆ’2 0 tragic
The Matrix myth 6 βˆ’3 1 tragic
Waiting for Godot absurdist 6 0 0 balanced

80/80 consistency checks pass.

The genre-label mismatch

Seven of ten stories have sign classes matching their genre labels. The three mismatches are informative:

Mythic reads as tragic. Hero's Journey (W = βˆ’1), The Lord of the Rings (W = βˆ’2), and The Matrix (W = βˆ’3) all have negative writhe. The hero's journey ends with a boon, but the trials dominate the sum. The monomyth is about surviving loss, not about accumulating gain.

Fleabag reads as balanced. Its ending is bittersweet β€” the protagonist loses the Priest but gains self-acceptance. W = 0. Genre labels call it comedy because it is funny, not because it ends in triumph.

The interpretive claim:

Genre labels describe how a work is received and sold. Writhe describes how its beats are arranged. The two correlate (7/10 in this corpus) but are not identical. The mismatches are the most interesting cases, because they reveal that a work's structural trajectory can differ from its marketing category.

Mod-2 classes

The corpus partitions into four mod-2 / sign-class combinations:

sign class mod-2 stories
comedic 0 Pride and Prejudice
balanced 0 Fleabag, Waiting for Godot
tragic 0 Oedipus Rex, The Lord of the Rings
tragic 1 Hamlet, Breaking Bad, Romeo and Juliet, Hero's Journey, The Matrix

Note: tragedy splits across two mod-2 classes. Oedipus Rex has W = βˆ’6 (even); Hamlet has W = βˆ’7 (odd). The mod-2 invariant captures a structural parity that genre labels do not.

How to use

from hv_writhe_stories import Beat, Story

# Encode a story
story = Story(
    name='My_Story',
    beats=[
        Beat(-1, 'ordinary life, restless'),
        Beat(-2, 'inciting rupture'),
        Beat(+1, 'training, awakening'),
        Beat(-1, 'betrayal'),
        Beat(-2, 'near defeat'),
        Beat(+2, 'resurrection'),
    ],
)

# Read the writhe
print(story.writhe())        # 2 - 6 + 2 - 1 + 1 - 1 = ...
print(story.mod2())          # 0 or 1
print(story.sign_class())    # 'comedic' | 'tragic' | 'balanced'

# Apply Reidemeister moves
story_r1 = story.apply_R1(position=2, direction=+1)   # flips mod-2
story_r2 = story.apply_R2(position=3, magnitude=1)    # preserves
story_r3 = story.apply_R3(i=0, j=3)                   # preserves

# Visualize the trajectory
print(story.ascii_plot(width=60, height=10))
Downloads last month
23
Inference Providers NEW
This model isn't deployed by any Inference Provider. πŸ™‹ Ask for provider support