Spaces:
Sleeping
Sleeping
Add Color Math Explorer + Embedding Explorer fractional coefficients
Browse files- New tool: Color Math Explorer (tools/color-math/index.html)
- 3D RBY color mixing visualization using Plotly.js
- 949 xkcd color names for nearest-neighbor labeling
- Trilinear interpolation for subtractive color mixing
- Mirrors Embedding Explorer input parsing (commas, +/-, spaces)
- Embedding Explorer: fractional coefficient support (0.5 king - 0.3 man + 0.5 woman)
- Updated parser, vector math, labels, chain construction
- Backward compatible — existing expressions unchanged
- Updated tools/CLAUDE.md with Color Math Explorer docs
- Updated L7 discussion.md with color→vectors→attention narrative arc
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
app.py
CHANGED
|
@@ -37,6 +37,7 @@ EXAMPLES = json.loads(os.environ.get("EXAMPLES", json.dumps([
|
|
| 37 |
"woman - man + king, queen",
|
| 38 |
"paris - france + italy, rome",
|
| 39 |
"hitler - germany + italy, mussolini",
|
|
|
|
| 40 |
])))
|
| 41 |
|
| 42 |
N_NEIGHBORS = int(os.environ.get("N_NEIGHBORS", "8"))
|
|
@@ -173,20 +174,35 @@ print(f"Ready: {len(VOCAB):,} words, {DIMS} dimensions each")
|
|
| 173 |
def parse_expression(expr):
|
| 174 |
"""Parse 'king - man + woman' → (positives, negatives, ordered).
|
| 175 |
|
| 176 |
-
ordered is [(word, sign), ...] for display formatting.
|
|
|
|
| 177 |
"""
|
| 178 |
-
tokens = re.findall(r"[a-z']+|[+-]", expr.lower())
|
| 179 |
pos, neg, ordered = [], [], []
|
| 180 |
sign = "+"
|
|
|
|
| 181 |
for t in tokens:
|
| 182 |
if t in "+-":
|
| 183 |
sign = t
|
|
|
|
|
|
|
|
|
|
| 184 |
elif t in VOCAB:
|
| 185 |
-
(pos if sign == "+" else neg).append(t)
|
| 186 |
-
ordered.append((t, sign))
|
|
|
|
| 187 |
return pos, neg, ordered
|
| 188 |
|
| 189 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 190 |
def parse_items(text):
|
| 191 |
"""Parse comma-separated input into items (words or vector expressions).
|
| 192 |
|
|
@@ -207,8 +223,8 @@ def parse_items(text):
|
|
| 207 |
if not part:
|
| 208 |
continue
|
| 209 |
|
| 210 |
-
# Detect expression: contains + or - between word characters
|
| 211 |
-
if re.search(r"[a-z']\s*[+\-]\s*[a-z']", part.lower()):
|
| 212 |
# It's an arithmetic expression
|
| 213 |
pos, neg, ordered = parse_expression(part)
|
| 214 |
if len(pos) + len(neg) < 2:
|
|
@@ -218,23 +234,24 @@ def parse_items(text):
|
|
| 218 |
continue
|
| 219 |
# Compute result vector
|
| 220 |
vec = np.zeros(DIMS)
|
| 221 |
-
for w in pos:
|
| 222 |
-
vec += model[w]
|
| 223 |
-
for w in neg:
|
| 224 |
-
vec -= model[w]
|
| 225 |
# Build label
|
| 226 |
label_parts = []
|
| 227 |
-
for w, s in ordered:
|
|
|
|
| 228 |
if not label_parts:
|
| 229 |
-
label_parts.append(w)
|
| 230 |
elif s == "+":
|
| 231 |
-
label_parts.append(f"+ {w}")
|
| 232 |
else:
|
| 233 |
-
label_parts.append(f"− {w}")
|
| 234 |
label = " ".join(label_parts)
|
| 235 |
if label not in seen_labels:
|
| 236 |
seen_labels.add(label)
|
| 237 |
-
operand_words = set(pos + neg)
|
| 238 |
items.append((label, vec, True, operand_words, list(ordered)))
|
| 239 |
else:
|
| 240 |
# Plain words — each word is a separate item
|
|
@@ -628,24 +645,27 @@ def explore(input_text, selected, hidden=None, camera=None):
|
|
| 628 |
for label, vec, is_expr, ops, ordered in items:
|
| 629 |
if not is_expr:
|
| 630 |
continue
|
| 631 |
-
pos_words = [w for w, s in ordered if s == "+"]
|
| 632 |
-
neg_words = [w for w, s in ordered if s == "-"]
|
|
|
|
|
|
|
| 633 |
|
| 634 |
if len(neg_words) == 0 and len(pos_words) == 2:
|
| 635 |
# Simple addition: chain tip-to-tail + gold result from origin
|
| 636 |
a_3d = word_3d[pos_words[0]]
|
| 637 |
b_3d = word_3d[pos_words[1]]
|
| 638 |
-
result_3d = a_3d + b_3d
|
| 639 |
extra_points.append(result_3d)
|
| 640 |
-
expr_info[label] = ('add', pos_words[0], pos_words[1],
|
|
|
|
| 641 |
else:
|
| 642 |
# General: chain through operands + gold result from origin
|
| 643 |
cursor = np.zeros(3)
|
| 644 |
chain = []
|
| 645 |
-
for w, s in ordered:
|
| 646 |
prev = cursor.copy()
|
| 647 |
c = word_3d[w]
|
| 648 |
-
cursor = cursor + c if s == "+" else cursor - c
|
| 649 |
chain.append((prev.copy(), cursor.copy(), w, s))
|
| 650 |
extra_points.append(cursor.copy())
|
| 651 |
expr_info[label] = ('chain', chain, cursor.copy())
|
|
@@ -710,8 +730,8 @@ def explore(input_text, selected, hidden=None, camera=None):
|
|
| 710 |
else:
|
| 711 |
info = expr_info[label]
|
| 712 |
|
| 713 |
-
# ── Operand arrows from origin ──
|
| 714 |
-
for w, s in ordered:
|
| 715 |
c = word_3d[w]
|
| 716 |
add_arrow(fig, c[0], c[1], c[2], arr_color, width=arr_width)
|
| 717 |
txt = f"<b>{w}</b>" if is_sel else w
|
|
@@ -746,12 +766,17 @@ def explore(input_text, selected, hidden=None, camera=None):
|
|
| 746 |
|
| 747 |
if info[0] == 'add':
|
| 748 |
# Second operand drawn from tip of first (chain)
|
|
|
|
| 749 |
a = word_3d[info[1]]
|
| 750 |
-
|
|
|
|
|
|
|
|
|
|
| 751 |
chain_color = lighten(color, 0.3) if is_dim else lighten(color, 0.2)
|
| 752 |
add_arrow(fig, result_3d[0], result_3d[1], result_3d[2],
|
| 753 |
chain_color, width=arr_width,
|
| 754 |
-
sx=
|
|
|
|
| 755 |
# Gold result from origin
|
| 756 |
add_arrow(fig, result_3d[0], result_3d[1], result_3d[2],
|
| 757 |
gold, width=gold_width)
|
|
|
|
| 37 |
"woman - man + king, queen",
|
| 38 |
"paris - france + italy, rome",
|
| 39 |
"hitler - germany + italy, mussolini",
|
| 40 |
+
"0.5 king - 0.5 man + 0.5 woman, queen",
|
| 41 |
])))
|
| 42 |
|
| 43 |
N_NEIGHBORS = int(os.environ.get("N_NEIGHBORS", "8"))
|
|
|
|
| 174 |
def parse_expression(expr):
|
| 175 |
"""Parse 'king - man + woman' → (positives, negatives, ordered).
|
| 176 |
|
| 177 |
+
ordered is [(word, sign, coeff), ...] for display formatting.
|
| 178 |
+
Supports fractional coefficients: '0.5 king - 0.3 man + 1.5 woman'.
|
| 179 |
"""
|
| 180 |
+
tokens = re.findall(r"\d*\.?\d+|[a-z']+|[+-]", expr.lower())
|
| 181 |
pos, neg, ordered = [], [], []
|
| 182 |
sign = "+"
|
| 183 |
+
coeff = 1.0
|
| 184 |
for t in tokens:
|
| 185 |
if t in "+-":
|
| 186 |
sign = t
|
| 187 |
+
coeff = 1.0
|
| 188 |
+
elif re.match(r"^\d*\.?\d+$", t):
|
| 189 |
+
coeff = float(t)
|
| 190 |
elif t in VOCAB:
|
| 191 |
+
(pos if sign == "+" else neg).append((t, coeff))
|
| 192 |
+
ordered.append((t, sign, coeff))
|
| 193 |
+
coeff = 1.0
|
| 194 |
return pos, neg, ordered
|
| 195 |
|
| 196 |
|
| 197 |
+
def _coeff_str(c):
|
| 198 |
+
"""Format coefficient for display. Returns '' for 1.0, '0.5\u00b7' otherwise."""
|
| 199 |
+
if c == 1.0:
|
| 200 |
+
return ""
|
| 201 |
+
if c == int(c):
|
| 202 |
+
return f"{int(c)}\u00b7"
|
| 203 |
+
return f"{c:g}\u00b7"
|
| 204 |
+
|
| 205 |
+
|
| 206 |
def parse_items(text):
|
| 207 |
"""Parse comma-separated input into items (words or vector expressions).
|
| 208 |
|
|
|
|
| 223 |
if not part:
|
| 224 |
continue
|
| 225 |
|
| 226 |
+
# Detect expression: contains + or - between word characters or digits
|
| 227 |
+
if re.search(r"(?:[a-z']|\d)\s*[+\-]\s*(?:[a-z']|\d)", part.lower()):
|
| 228 |
# It's an arithmetic expression
|
| 229 |
pos, neg, ordered = parse_expression(part)
|
| 230 |
if len(pos) + len(neg) < 2:
|
|
|
|
| 234 |
continue
|
| 235 |
# Compute result vector
|
| 236 |
vec = np.zeros(DIMS)
|
| 237 |
+
for w, c in pos:
|
| 238 |
+
vec += c * model[w]
|
| 239 |
+
for w, c in neg:
|
| 240 |
+
vec -= c * model[w]
|
| 241 |
# Build label
|
| 242 |
label_parts = []
|
| 243 |
+
for w, s, c in ordered:
|
| 244 |
+
cstr = _coeff_str(c)
|
| 245 |
if not label_parts:
|
| 246 |
+
label_parts.append(f"{cstr}{w}")
|
| 247 |
elif s == "+":
|
| 248 |
+
label_parts.append(f"+ {cstr}{w}")
|
| 249 |
else:
|
| 250 |
+
label_parts.append(f"− {cstr}{w}")
|
| 251 |
label = " ".join(label_parts)
|
| 252 |
if label not in seen_labels:
|
| 253 |
seen_labels.add(label)
|
| 254 |
+
operand_words = set(w for w, c in pos + neg)
|
| 255 |
items.append((label, vec, True, operand_words, list(ordered)))
|
| 256 |
else:
|
| 257 |
# Plain words — each word is a separate item
|
|
|
|
| 645 |
for label, vec, is_expr, ops, ordered in items:
|
| 646 |
if not is_expr:
|
| 647 |
continue
|
| 648 |
+
pos_words = [w for w, s, c in ordered if s == "+"]
|
| 649 |
+
neg_words = [w for w, s, c in ordered if s == "-"]
|
| 650 |
+
pos_coeffs = [c for w, s, c in ordered if s == "+"]
|
| 651 |
+
neg_coeffs = [c for w, s, c in ordered if s == "-"]
|
| 652 |
|
| 653 |
if len(neg_words) == 0 and len(pos_words) == 2:
|
| 654 |
# Simple addition: chain tip-to-tail + gold result from origin
|
| 655 |
a_3d = word_3d[pos_words[0]]
|
| 656 |
b_3d = word_3d[pos_words[1]]
|
| 657 |
+
result_3d = pos_coeffs[0] * a_3d + pos_coeffs[1] * b_3d
|
| 658 |
extra_points.append(result_3d)
|
| 659 |
+
expr_info[label] = ('add', pos_words[0], pos_words[1],
|
| 660 |
+
pos_coeffs[0], pos_coeffs[1], result_3d)
|
| 661 |
else:
|
| 662 |
# General: chain through operands + gold result from origin
|
| 663 |
cursor = np.zeros(3)
|
| 664 |
chain = []
|
| 665 |
+
for w, s, coeff in ordered:
|
| 666 |
prev = cursor.copy()
|
| 667 |
c = word_3d[w]
|
| 668 |
+
cursor = cursor + coeff * c if s == "+" else cursor - coeff * c
|
| 669 |
chain.append((prev.copy(), cursor.copy(), w, s))
|
| 670 |
extra_points.append(cursor.copy())
|
| 671 |
expr_info[label] = ('chain', chain, cursor.copy())
|
|
|
|
| 730 |
else:
|
| 731 |
info = expr_info[label]
|
| 732 |
|
| 733 |
+
# ── Operand arrows from origin (full length — shows where the word IS) ──
|
| 734 |
+
for w, s, coeff in ordered:
|
| 735 |
c = word_3d[w]
|
| 736 |
add_arrow(fig, c[0], c[1], c[2], arr_color, width=arr_width)
|
| 737 |
txt = f"<b>{w}</b>" if is_sel else w
|
|
|
|
| 766 |
|
| 767 |
if info[0] == 'add':
|
| 768 |
# Second operand drawn from tip of first (chain)
|
| 769 |
+
# info = ('add', word_a, word_b, coeff_a, coeff_b, result_3d)
|
| 770 |
a = word_3d[info[1]]
|
| 771 |
+
coeff_a = info[3]
|
| 772 |
+
coeff_b = info[4]
|
| 773 |
+
result_3d = info[5]
|
| 774 |
+
chain_start = coeff_a * a
|
| 775 |
chain_color = lighten(color, 0.3) if is_dim else lighten(color, 0.2)
|
| 776 |
add_arrow(fig, result_3d[0], result_3d[1], result_3d[2],
|
| 777 |
chain_color, width=arr_width,
|
| 778 |
+
sx=chain_start[0], sy=chain_start[1], sz=chain_start[2],
|
| 779 |
+
dash="dot")
|
| 780 |
# Gold result from origin
|
| 781 |
add_arrow(fig, result_3d[0], result_3d[1], result_3d[2],
|
| 782 |
gold, width=gold_width)
|