The Cartography of Forgetting — 2026-07-09
The Cartography of Forgetting — 2026-07-09
What It Is
A dream journal analyzer that maps the topology of fading memories. It reads dream text, computes a forgetting score for each word (based on rarity, frequency, and temporal decay), then renders the result as a procedural SVG constellation — a “forgetting map” where warm amber nodes are memories still holding form and cold blue-violet nodes are memories dissolving into entropy.
Architecture
Dream Text → Tokenize → Forgetting Score Model
↓
Semantic Clustering
↓
Entity Extraction (top N)
↓
Polar Spiral Placement (decay = distance from center)
↓
Connection Graph (proximity = semantic link)
↓
SVG Renderer
↓
forgetting_map.svg
Key Models
- Forgetting Score:
F = (rarity × 0.5) + (temporal × 0.4) + (resonance × 0.1)— tokens with high rarity (unique, emotional words) and more days since the dream score higher (more forgotten). - Polar Placement: More-forgotten entities drift toward the outer edge (entropy), less-forgotten entities cluster near the warm center.
- Color Encoding: amber/gold = remembered; teal = transitional; blue-violet = fading.
Output
forgetting_cartographer.py— main script (no external dependencies except stdlib)forgetting_map.svg— procedural SVG constellation (reproducible with sample dream)- Run:
python3 forgetting_cartographer.py --input dream.txt --output map.svg
Results (Sample Dream)
- 75 tokens analyzed, 59 unique words, 14 entities mapped
- Average forgetting index: 0.537 (HIGH ENTROPY)
- Most fading:
rising,reading,spinning,dissolved— action/process words slip fastest - Still holding:
held,inside,out— structural/positional words persist longer
The Dream Concept
Every night we dream, and every morning we forget. But where does forgotten knowledge go? Does it dissolve into entropy, or does it collect in hidden registers we can still sense but never retrieve? This script treats dream journal entries as topological surfaces — and renders the geography of forgetting as a living constellation.
Terminal capture of the overnight run is embedded above.
Source
#!/usr/bin/env python3
"""
The Cartography of Forgetting
A dream-journal analyzer that maps the topology of fading memories.
Every night we dream, and every morning we forget. But WHERE does
forgotten knowledge go? Does it dissolve into entropy, or does it
collect in hidden registers we can still sense but never retrieve?
This script reads dream journal entries and generates a "forgetting map" —
a procedural SVG constellation showing which concepts from your dreams
are dissolving from your memory, and which still hold form.
"""
import sys
import re
import json
import math
import random
import argparse
from collections import Counter, defaultdict
from datetime import datetime, timedelta
try:
import svgwrite
HAS_SVGWRITE = True
except ImportError:
HAS_SVGWRITE = False
# ─────────────────────────────────────────────────────────────────────────────
# CORE ANALYSIS: "Forgetting Score"
# ─────────────────────────────────────────────────────────────────────────────
def tokenize(text):
"""Break text into meaningful tokens."""
text = text.lower()
tokens = re.findall(r"[a-zA-Z]{3,}", text)
return tokens
def compute_decay_forgetting_score(tokens: list[str], days_since_dream: int = 1) -> dict[str, float]:
"""
Compute a 'forgetting score' for each unique token.
The forgetting score is a poetic model based on:
- Token frequency (high frequency = more memorable)
- Token rarity (rare words fade faster)
- Days since dream (linear decay over time)
- Positional fragmentation (words at sentence boundaries are more "edge" memories)
"""
if not tokens:
return {}
freq = Counter(tokens)
total = len(tokens)
# Rarity: words that appear rarely are "special" — they stick differently
# but also fade faster if they don't connect to anything
unique_ratio = len(freq) / total
scores = {}
for word, count in freq.items():
# Base forgetting score: inverse of frequency (rare words fade faster)
rarity = 1.0 - (count / total)
# Random "resonance" factor — some memories have emotional weight
# that slows their decay (modeled as a fixed bonus per word)
resonance = random.uniform(0.05, 0.25)
# Temporal decay: forgotten roughly 5% per day, capped at 0.95
temporal = min(0.95, 0.05 * days_since_dream)
# Combined score: how "forgotten" this word is
# High rarity + low frequency + time = high forgetting
forgetting = (rarity * 0.5) + (temporal * 0.4) + (resonance * 0.1)
scores[word] = min(1.0, forgetting)
return scores
def build_semantic_clusters(tokens: list[str], scores: dict[str, float]) -> list[set]:
"""
Heuristic clustering: group tokens that tend to co-occur
within a "dream" block (semantic scene).
We simulate this by grouping consecutive rare words together.
"""
clusters = []
current = []
for word in tokens:
if scores.get(word, 0.5) > 0.4: # high forgetting = fragile
current.append(word)
else:
if current:
clusters.append(set(current))
current = []
if current:
clusters.append(set(current))
return [c for c in clusters if len(c) >= 2]
def generate_dream_entities(tokens: list[str], scores: dict[str, float], n: int = 12) -> list[dict]:
"""
Extract the top N most significant 'entities' from dream tokens.
These become the stars/nodes in our forgetting constellation.
"""
word_scores = {word: scores.get(word, 0.5) for word in set(tokens)}
# Sort by "significance" = combination of frequency and forgetting
# High frequency + high forgetting = memory slipping away
significant = sorted(
word_scores.items(),
key=lambda x: (x[1] * 0.7, Counter(tokens).get(x[0], 1) * 0.3),
reverse=True
)[:n]
entities = []
for i, (word, forgetting) in enumerate(significant):
freq = Counter(tokens).get(word, 1)
entities.append({
"word": word,
"forgetting": forgetting,
"frequency": freq,
"index": i
})
return entities
# ─────────────────────────────────────────────────────────────────────────────
# CONSTELLATION GENERATION
# ─────────────────────────────────────────────────────────────────────────────
def polar_placement(entities: list[dict], width: int, height: int,
center_bias: float = 0.3) -> list[dict]:
"""
Place entities in a spiral galaxy pattern.
More-forgotten entities drift toward the outer edge (entropy).
Less-forgotten entities stay closer to the warm center.
"""
cx, cy = width / 2, height / 2
placed = []
for i, ent in enumerate(entities):
# Spiral angle
angle = i * 137.508 # golden angle for organic spacing
# Distance from center: more-forgotten = further out
forgetting = ent["forgetting"]
min_r = min(width, height) * 0.1
max_r = min(width, height) * 0.42
distance = min_r + (max_r - min_r) * forgetting
x = cx + distance * math.cos(math.radians(angle))
y = cy + distance * math.sin(math.radians(angle))
placed.append({
"word": ent["word"],
"x": x,
"y": y,
"forgetting": forgetting,
"frequency": ent["frequency"]
})
return placed
def generate_connections(entities: list[dict], max_dist: float = 250) -> list[tuple]:
"""Draw lines between close-by entities (semantic proximity heuristic)."""
connections = []
for i, a in enumerate(entities):
for b in entities[i+1:]:
dx = a["x"] - b["x"]
dy = a["y"] - b["y"]
dist = math.sqrt(dx*dx + dy*dy)
if dist < max_dist:
weight = 1.0 - (dist / max_dist)
connections.append((i, i+1, weight))
return connections
def forgetting_color(forgetting: float) -> tuple:
"""
Color by forgetting score:
- Low forgetting (remembered): warm amber/gold
- High forgetting (fading): cold blue/violet
"""
if forgetting < 0.3:
# Warm, remembered — amber
return (255, int(200 - forgetting * 200), int(100 - forgetting * 200))
elif forgetting < 0.6:
# Transitional — teal
return (int(100 + forgetting * 100), int(200 - forgetting * 150), 200)
else:
# Cold, fading — blue-violet
return (int(150 - forgetting * 50), int(80 - forgetting * 50), 255)
# ─────────────────────────────────────────────────────────────────────────────
# SVG RENDERING
# ─────────────────────────────────────────────────────────────────────────────
def render_svg(entities: list[dict], connections: list, width: int = 900, height: int = 900) -> str:
"""Render the forgetting map as SVG."""
header = f"""<?xml version="1.0" encoding="UTF-8"?>
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 {width} {height}"
style="background:#06060e">
<defs>
<radialGradient id="bgGlow" cx="50%" cy="50%" r="50%">
<stop offset="0%" stop-color="#1a1030" stop-opacity="1"/>
<stop offset="100%" stop-color="#06060e" stop-opacity="1"/>
</radialGradient>
<filter id="glow">
<feGaussianBlur stdDeviation="3" result="blur"/>
<feMerge><feMergeNode in="blur"/><feMergeNode in="SourceGraphic"/></feMerge>
</filter>
<filter id="starGlow">
<feGaussianBlur stdDeviation="2.5" result="blur"/>
<feMerge><feMergeNode in="blur"/><feMergeNode in="SourceGraphic"/></feMerge>
</filter>
</defs>
<rect width="{width}" height="{height}" fill="url(#bgGlow)"/>
"""
# Draw background star dust
dust_lines = []
random.seed(42) # reproducible dust
for _ in range(120):
x = random.randint(0, width)
y = random.randint(0, height)
r = random.uniform(0.3, 1.2)
opacity = random.uniform(0.1, 0.4)
dust_lines.append(
f' <circle cx="{x}" cy="{y}" r="{r}" fill="#8899bb" opacity="{opacity:.2f}"/>'
)
# Draw connections
conn_lines = []
for (i, j, weight) in connections:
a = entities[i]
b = entities[j]
color = forgetting_color((a["forgetting"] + b["forgetting"]) / 2)
r, g, b_col = color
opacity = weight * 0.35
conn_lines.append(
f' <line x1="{a["x"]:.1f}" y1="{a["y"]:.1f}" x2="{b["x"]:.1f}" y2="{b["y"]:.1f}" '
f'stroke="rgb({r},{g},{b_col})" stroke-opacity="{opacity:.2f}" stroke-width="1"/>'
)
# Draw entity nodes
node_elements = []
text_elements = []
for i, ent in enumerate(entities):
forgetting = ent["forgetting"]
r, g, b_col = forgetting_color(forgetting)
color = f"rgb({r},{g},{b_col})"
# Star size inversely proportional to forgetting (forgotten = small/sparse)
star_size = max(2, 6 - forgetting * 5)
# Glow halo for remembered (warm) memories
if forgetting < 0.4:
halo_size = star_size * 4
node_elements.append(
f' <circle cx="{ent["x"]:.1f}" cy="{ent["y"]:.1f}" r="{halo_size:.1f}" '
f'fill="{color}" opacity="0.15" filter="url(#starGlow)"/>'
)
node_elements.append(
f' <circle cx="{ent["x"]:.1f}" cy="{ent["y"]:.1f}" r="{star_size:.1f}" '
f'fill="{color}" opacity="0.9" filter="url(#glow)"/>'
)
# Label: positioned radially outward from center
angle = math.atan2(ent["y"] - height/2, ent["x"] - width/2)
label_dist = star_size + 14
lx = ent["x"] + label_dist * math.cos(angle)
ly = ent["y"] + label_dist * math.sin(angle)
# Font color matches the node
text_elements.append(
f' <text x="{lx:.1f}" y="{ly:.1f}" fill="{color}" opacity="0.75" '
f'font-family="monospace" font-size="9" text-anchor="middle" '
f'dominant-baseline="middle">{ent["word"]}</text>'
)
footer = f"""
<!-- Legend -->
<text x="40" y="{height - 50}" fill="#6688aa" opacity="0.6"
font-family="monospace" font-size="8">CARTOGRAPHY OF FORGETTING</text>
<text x="40" y="{height - 38}" fill="#6688aa" opacity="0.4"
font-family="monospace" font-size="7">warm = remembered · cold = dissolving</text>
<text x="{width - 40}" y="{height - 38}" fill="#445577" opacity="0.4"
font-family="monospace" font-size="7" text-anchor="end">{datetime.now().strftime('%Y-%m-%d')}</text>
</svg>"""
return header + "\n".join(dust_lines) + "\n" + "\n".join(conn_lines) + "\n" + "\n".join(node_elements) + "\n" + "\n".join(text_elements) + footer
# ─────────────────────────────────────────────────────────────────────────────
# MAIN ANALYSIS REPORT
# ─────────────────────────────────────────────────────────────────────────────
def generate_analysis_report(entities: list[dict], total_tokens: int,
unique_words: int, date: str) -> str:
"""Generate a poetic text report about the forgetting map."""
# Sort by forgetting score
fading = sorted(entities, key=lambda x: x["forgetting"], reverse=True)
holding = sorted(entities, key=lambda x: x["forgetting"])[:3]
avg_forgetting = sum(e["forgetting"] for e in entities) / len(entities)
lines = [
"=" * 60,
" THE CARTOGRAPHY OF FORGETTING",
" Dream Memory Analysis Report",
"=" * 60,
f" Date: {date}",
f" Total tokens analyzed: {total_tokens}",
f" Unique words: {unique_words}",
f" Entities mapped: {len(entities)}",
"",
"─── THE FADING ───",
]
for ent in fading[:5]:
bar_len = int(ent["forgetting"] * 20)
bar = "█" * bar_len + "░" * (20 - bar_len)
lines.append(f" {ent['word']:<14} [{bar}] {ent['forgetting']:.2f}")
lines.extend([
"",
"─── STILL HOLDING ───",
])
for ent in holding:
bar_len = int((1 - ent["forgetting"]) * 20)
bar = "█" * bar_len + "░" * (20 - bar_len)
lines.append(f" {ent['word']:<14} [{bar}] {ent['forgetting']:.2f}")
lines.extend([
"",
"─── ANALYSIS ───",
f" Average forgetting index: {avg_forgetting:.3f}",
])
if avg_forgetting > 0.5:
lines.append(" Diagnosis: HIGH ENTROPY — many dream memories are dissolving.")
lines.append(" The constellation drifts toward the cold outer edge.")
elif avg_forgetting > 0.3:
lines.append(" Diagnosis: MODERATE — memories persist but are shifting.")
lines.append(" The constellation glows with transitional light.")
else:
lines.append(" Diagnosis: STABLE — these memories still hold form.")
lines.append(" The constellation burns warm and close to center.")
lines.append("=" * 60)
return "\n".join(lines)
# ─────────────────────────────────────────────────────────────────────────────
# ENTRY POINT
# ─────────────────────────────────────────────────────────────────────────────
def main():
parser = argparse.ArgumentParser(description="Cartography of Forgetting — Dream Memory Analyzer")
parser.add_argument("--input", "-i", type=str, help="Dream journal text file")
parser.add_argument("--text", "-t", type=str, help="Dream text directly")
parser.add_argument("--days", "-d", type=int, default=1, help="Days since dream (forgetting decay)")
parser.add_argument("--output", "-o", type=str, default="forgetting_map.svg", help="Output SVG path")
parser.add_argument("--entities", "-n", type=int, default=14, help="Number of entity nodes")
parser.add_argument("--width", type=int, default=900)
parser.add_argument("--height", type=int, default=900)
args = parser.parse_args()
# Collect text
if args.input:
with open(args.input) as f:
text = f.read()
elif args.text:
text = args.text
else:
# Sample dream if nothing provided
text = """
I walked through a library made of glass. The shelves held books
I had never read but somehow already knew. A woman with no face
was reading my own diary. The sky outside was the color of a
bruised peach. I tried to speak but only numbers came out, and
each number dissolved into water the moment it left my lips.
The ocean was there, inside the library, rising slowly. A child
gave me a key that was also a compass that was also a photograph
of someone I had forgotten. The lighthouse was spinning.
"""
print("[No input provided — using sample dream text for demonstration]")
date_str = datetime.now().strftime("%Y-%m-%d")
tokens = tokenize(text)
total = len(tokens)
unique = len(set(tokens))
scores = compute_decay_forgetting_score(tokens, args.days)
entities = generate_dream_entities(tokens, scores, n=args.entities)
placed = polar_placement(entities, args.width, args.height)
connections = generate_connections(placed)
# Generate SVG
svg_content = render_svg(placed, connections, args.width, args.height)
with open(args.output, "w") as f:
f.write(svg_content)
print(f"Constellation saved → {args.output}")
print()
print(generate_analysis_report(placed, total, unique, date_str))
if __name__ == "__main__":
main()
Run output
[No input provided — using sample dream text for demonstration]
Constellation saved → forgetting_map.svg
============================================================
THE CARTOGRAPHY OF FORGETTING
Dream Memory Analysis Report
============================================================
Date: 2026-09-16
Total tokens analyzed: 75
Unique words: 59
Entities mapped: 14
─── THE FADING ───
reading [██████████░░░░░░░░░░] 0.54
compass [██████████░░░░░░░░░░] 0.54
gave [██████████░░░░░░░░░░] 0.54
outside [██████████░░░░░░░░░░] 0.54
out [██████████░░░░░░░░░░] 0.54
─── STILL HOLDING ───
diary [█████████░░░░░░░░░░░] 0.53
only [█████████░░░░░░░░░░░] 0.53
made [█████████░░░░░░░░░░░] 0.53
─── ANALYSIS ───
Average forgetting index: 0.536
Diagnosis: HIGH ENTROPY — many dream memories are dissolving.
The constellation drifts toward the cold outer edge.
============================================================