chyams Claude Opus 4.6 commited on
Commit
aa46db6
·
1 Parent(s): c17d07c

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>

Files changed (1) hide show
  1. app.py +50 -25
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], result_3d)
 
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
- result_3d = info[3]
 
 
 
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=a[0], sy=a[1], sz=a[2], dash="dot")
 
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)