Spaces:
Running
Running
Add Gallery tab: factor poster, cuisine compass, vector arithmetic art, phylogeny, cosine map, SLERP trajectory
Browse files- __pycache__/app.cpython-310.pyc +0 -0
- app.py +451 -0
__pycache__/app.cpython-310.pyc
CHANGED
|
Binary files a/__pycache__/app.cpython-310.pyc and b/__pycache__/app.cpython-310.pyc differ
|
|
|
app.py
CHANGED
|
@@ -528,6 +528,389 @@ def api_embed(ingredient, sibling="chem"):
|
|
| 528 |
if ingredient not in m.vocab: return {"error": f"'{ingredient}' not in vocab"}
|
| 529 |
return [float(x) for x in _unit(m.E[m.vocab[ingredient]]).tolist()]
|
| 530 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 531 |
# ===== Theme =====
|
| 532 |
|
| 533 |
THEME = gr.themes.Soft(
|
|
@@ -864,6 +1247,74 @@ Three sibling ingredient embeddings from [arXiv:2605.22391](https://arxiv.org/ab
|
|
| 864 |
label="Try one of these",
|
| 865 |
)
|
| 866 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 867 |
# ---- Hidden API endpoints ----
|
| 868 |
with gr.Group(visible=False):
|
| 869 |
api_in_s1 = gr.Textbox(visible=False)
|
|
|
|
| 528 |
if ingredient not in m.vocab: return {"error": f"'{ingredient}' not in vocab"}
|
| 529 |
return [float(x) for x in _unit(m.E[m.vocab[ingredient]]).tolist()]
|
| 530 |
|
| 531 |
+
# =====================================================================
|
| 532 |
+
# Gallery: six aesthetic visualisations.
|
| 533 |
+
# All use a shared dark-teal Kaikaku palette so they hang together.
|
| 534 |
+
# =====================================================================
|
| 535 |
+
|
| 536 |
+
GALLERY_BG = KAIKAKU_DARK
|
| 537 |
+
GALLERY_GRID = "#1F4548"
|
| 538 |
+
GALLERY_DUST = "#1A3D3F"
|
| 539 |
+
GALLERY_TEXT = "#E8F4F1"
|
| 540 |
+
GALLERY_TXTDIM = KAIKAKU_ACCENT_LIGHT
|
| 541 |
+
GALLERY_MODE_PALETTE = [
|
| 542 |
+
"#B5E6D2", "#F4B86E", "#E8C0E8", "#9BC9E8", "#D7E89B",
|
| 543 |
+
"#FFAA8A", "#A8D5CA", "#E8E0A0",
|
| 544 |
+
]
|
| 545 |
+
|
| 546 |
+
# All cuisine names we display
|
| 547 |
+
_CUISINES = ["East_Asian","Japanese","Southeast_Asian","South_Asian",
|
| 548 |
+
"Mediterranean","Eastern_European","Latin_American","Western_Atlantic"]
|
| 549 |
+
|
| 550 |
+
|
| 551 |
+
def _gallery_axes(figsize=(10, 9)):
|
| 552 |
+
fig, ax = plt.subplots(figsize=figsize, facecolor=GALLERY_BG)
|
| 553 |
+
ax.set_facecolor(GALLERY_BG)
|
| 554 |
+
for s in ax.spines.values(): s.set_visible(False)
|
| 555 |
+
ax.tick_params(left=False, bottom=False, labelleft=False, labelbottom=False)
|
| 556 |
+
return fig, ax
|
| 557 |
+
|
| 558 |
+
|
| 559 |
+
# ---------- (1) Factor decomposition poster ----------
|
| 560 |
+
|
| 561 |
+
def factor_options(sibling: str):
|
| 562 |
+
m = MODELS[sibling]
|
| 563 |
+
fids = sorted({md.property for md in m.modes if md.kind == "factor"},
|
| 564 |
+
key=lambda x: int(x.split("_")[-1]) if x.split("_")[-1].isdigit() else 99)
|
| 565 |
+
return fids
|
| 566 |
+
|
| 567 |
+
def render_factor_poster(sibling: str, factor_id: str):
|
| 568 |
+
"""Reference-screenshot style: dark teal background, faint contour texture,
|
| 569 |
+
GMM-mode point clusters in pastel colours, callouts with leader lines, bottom stat line."""
|
| 570 |
+
m = MODELS[sibling]
|
| 571 |
+
coords = UMAP_DATA[sibling]
|
| 572 |
+
factor_modes = [md for md in m.modes if md.kind == "factor" and md.property == factor_id]
|
| 573 |
+
if not factor_modes:
|
| 574 |
+
fig, ax = _gallery_axes()
|
| 575 |
+
ax.text(0.5, 0.5, "(no factor selected)", ha="center", va="center",
|
| 576 |
+
color=GALLERY_TXTDIM, transform=ax.transAxes)
|
| 577 |
+
return fig
|
| 578 |
+
|
| 579 |
+
fig, ax = _gallery_axes(figsize=(11, 10))
|
| 580 |
+
xmin, xmax = float(coords[:,0].min()-0.8), float(coords[:,0].max()+0.8)
|
| 581 |
+
ymin, ymax = float(coords[:,1].min()-0.8), float(coords[:,1].max()+0.8)
|
| 582 |
+
|
| 583 |
+
# Topographic background: KDE of all ingredients, contoured
|
| 584 |
+
try:
|
| 585 |
+
from scipy.stats import gaussian_kde
|
| 586 |
+
kde = gaussian_kde(coords.T, bw_method=0.18)
|
| 587 |
+
xx, yy = np.meshgrid(np.linspace(xmin, xmax, 160), np.linspace(ymin, ymax, 160))
|
| 588 |
+
zz = kde(np.vstack([xx.ravel(), yy.ravel()])).reshape(xx.shape)
|
| 589 |
+
ax.contour(xx, yy, zz, levels=14, colors=GALLERY_GRID, alpha=0.45, linewidths=0.6)
|
| 590 |
+
except Exception:
|
| 591 |
+
pass
|
| 592 |
+
|
| 593 |
+
# Faint background scatter of every ingredient
|
| 594 |
+
ax.scatter(coords[:, 0], coords[:, 1], s=2, c=GALLERY_DUST, alpha=0.5,
|
| 595 |
+
linewidths=0, zorder=2)
|
| 596 |
+
|
| 597 |
+
# Plot each mode's members + callout
|
| 598 |
+
name_to_idx = MODELS[sibling].vocab
|
| 599 |
+
used_corners = []
|
| 600 |
+
corners = [(xmax-0.5, ymax-0.5), (xmin+0.5, ymax-0.5),
|
| 601 |
+
(xmax-0.5, ymin+0.5), (xmin+0.5, ymin+0.5),
|
| 602 |
+
(xmax-0.5, (ymin+ymax)/2), (xmin+0.5, (ymin+ymax)/2)]
|
| 603 |
+
for i, mode in enumerate(factor_modes):
|
| 604 |
+
idxs = [name_to_idx[n] for n in mode.members if n in name_to_idx]
|
| 605 |
+
if not idxs: continue
|
| 606 |
+
color = GALLERY_MODE_PALETTE[i % len(GALLERY_MODE_PALETTE)]
|
| 607 |
+
pts = coords[idxs]
|
| 608 |
+
# Scatter with size jitter for a painterly look
|
| 609 |
+
sizes = np.random.RandomState(i).randint(50, 130, len(idxs))
|
| 610 |
+
ax.scatter(pts[:,0], pts[:,1], s=sizes, c=color, alpha=0.92,
|
| 611 |
+
edgecolors="white", linewidths=0.6, zorder=4)
|
| 612 |
+
# Callout
|
| 613 |
+
cx, cy = float(pts[:,0].mean()), float(pts[:,1].mean())
|
| 614 |
+
# pick a corner not yet used
|
| 615 |
+
corner = corners[i % len(corners)]
|
| 616 |
+
used_corners.append(corner)
|
| 617 |
+
ax.annotate(
|
| 618 |
+
f"M{mode.mode_id.split('M')[-1]} · {mode.label.upper()}",
|
| 619 |
+
xy=(cx, cy), xytext=corner,
|
| 620 |
+
color=GALLERY_TEXT, fontsize=9, family="monospace",
|
| 621 |
+
ha=("right" if corner[0] > (xmin+xmax)/2 else "left"),
|
| 622 |
+
va="center",
|
| 623 |
+
arrowprops=dict(arrowstyle="-", color=GALLERY_TXTDIM, lw=0.7,
|
| 624 |
+
connectionstyle="arc3,rad=0.1"),
|
| 625 |
+
zorder=5,
|
| 626 |
+
)
|
| 627 |
+
|
| 628 |
+
ax.set_xlim(xmin, xmax); ax.set_ylim(ymin, ymax); ax.set_aspect("equal")
|
| 629 |
+
|
| 630 |
+
# Title strip top-left
|
| 631 |
+
ax.text(0.02, 0.97,
|
| 632 |
+
f"EPICURE · FACTOR DECOMPOSITION · {factor_id.upper()}",
|
| 633 |
+
transform=ax.transAxes, color=GALLERY_TXTDIM,
|
| 634 |
+
fontsize=11, family="monospace", va="top")
|
| 635 |
+
|
| 636 |
+
# Bottom stat line
|
| 637 |
+
n_modes = len(factor_modes)
|
| 638 |
+
total_n = sum(md.n_members for md in factor_modes)
|
| 639 |
+
fig.text(0.5, 0.07,
|
| 640 |
+
"All of human cooking compressed into 2 megabytes.",
|
| 641 |
+
ha="center", color=GALLERY_TEXT, fontsize=15)
|
| 642 |
+
fig.text(0.5, 0.04,
|
| 643 |
+
f"SIBLING {sibling.upper()} · {n_modes} MODES · {total_n} INGREDIENTS · 300-D EMBEDDING",
|
| 644 |
+
ha="center", color=GALLERY_TXTDIM, fontsize=9, family="monospace")
|
| 645 |
+
plt.tight_layout(rect=[0, 0.09, 1, 1])
|
| 646 |
+
return fig
|
| 647 |
+
|
| 648 |
+
|
| 649 |
+
# ---------- (2) Cuisine compass (polar radar) ----------
|
| 650 |
+
|
| 651 |
+
def _cuisine_pole(m, region):
|
| 652 |
+
key = f"cuisine:{region}"
|
| 653 |
+
if key in m.supervised_poles:
|
| 654 |
+
return _unit(m.supervised_poles[key])
|
| 655 |
+
return None
|
| 656 |
+
|
| 657 |
+
def render_cuisine_compass(sibling: str, ingredients: list[str]):
|
| 658 |
+
m = MODELS[sibling]
|
| 659 |
+
valid = [n for n in (ingredients or []) if n in m.vocab]
|
| 660 |
+
if not valid:
|
| 661 |
+
fig = go.Figure()
|
| 662 |
+
fig.add_annotation(text="Pick at least one ingredient",
|
| 663 |
+
showarrow=False, font=dict(color=GALLERY_TXTDIM, size=14),
|
| 664 |
+
xref="paper", yref="paper", x=0.5, y=0.5)
|
| 665 |
+
fig.update_layout(paper_bgcolor=GALLERY_BG, plot_bgcolor=GALLERY_BG, height=520)
|
| 666 |
+
return fig
|
| 667 |
+
poles = {c: _cuisine_pole(m, c) for c in _CUISINES}
|
| 668 |
+
poles = {c: p for c, p in poles.items() if p is not None}
|
| 669 |
+
cuisines = list(poles.keys())
|
| 670 |
+
fig = go.Figure()
|
| 671 |
+
palette = GALLERY_MODE_PALETTE
|
| 672 |
+
for i, ing in enumerate(valid):
|
| 673 |
+
v = _unit(m.E[m.vocab[ing]])
|
| 674 |
+
radii = [float(v @ poles[c]) for c in cuisines]
|
| 675 |
+
# close the polygon
|
| 676 |
+
radii_closed = radii + [radii[0]]
|
| 677 |
+
labels_closed = cuisines + [cuisines[0]]
|
| 678 |
+
color = palette[i % len(palette)]
|
| 679 |
+
fig.add_trace(go.Scatterpolar(
|
| 680 |
+
r=radii_closed, theta=labels_closed,
|
| 681 |
+
fill="toself", name=ing,
|
| 682 |
+
fillcolor=f"rgba({int(color[1:3],16)},{int(color[3:5],16)},{int(color[5:7],16)},0.25)",
|
| 683 |
+
line=dict(color=color, width=2),
|
| 684 |
+
marker=dict(size=6, color=color),
|
| 685 |
+
hovertemplate="%{theta}<br>cos = %{r:.3f}<extra>" + ing + "</extra>",
|
| 686 |
+
))
|
| 687 |
+
fig.update_layout(
|
| 688 |
+
polar=dict(
|
| 689 |
+
bgcolor=GALLERY_BG,
|
| 690 |
+
radialaxis=dict(visible=True, gridcolor=GALLERY_GRID, range=[-0.2, 0.8],
|
| 691 |
+
tickfont=dict(color=GALLERY_TXTDIM, size=9),
|
| 692 |
+
angle=90, tickangle=90),
|
| 693 |
+
angularaxis=dict(gridcolor=GALLERY_GRID,
|
| 694 |
+
tickfont=dict(color=GALLERY_TEXT, size=11)),
|
| 695 |
+
),
|
| 696 |
+
paper_bgcolor=GALLERY_BG,
|
| 697 |
+
font=dict(color=GALLERY_TEXT),
|
| 698 |
+
legend=dict(font=dict(color=GALLERY_TEXT), bgcolor="rgba(0,0,0,0)"),
|
| 699 |
+
title=dict(text=f"CUISINE COMPASS · {sibling.upper()}",
|
| 700 |
+
font=dict(color=GALLERY_TXTDIM, size=12, family="monospace"),
|
| 701 |
+
x=0.02, xanchor="left"),
|
| 702 |
+
height=560, margin=dict(l=60, r=60, t=70, b=40),
|
| 703 |
+
)
|
| 704 |
+
return fig
|
| 705 |
+
|
| 706 |
+
|
| 707 |
+
# ---------- (7) Arithmetic vector art ----------
|
| 708 |
+
|
| 709 |
+
def render_arithmetic_vector(sibling: str, positives: list[str], negatives: list[str]):
|
| 710 |
+
"""Visualise centroid(positives) - centroid(negatives) as vector arrows on the UMAP."""
|
| 711 |
+
m = MODELS[sibling]
|
| 712 |
+
coords = UMAP_DATA[sibling]
|
| 713 |
+
fig, ax = _gallery_axes(figsize=(11, 9))
|
| 714 |
+
xmin, xmax = float(coords[:,0].min()-0.8), float(coords[:,0].max()+0.8)
|
| 715 |
+
ymin, ymax = float(coords[:,1].min()-0.8), float(coords[:,1].max()+0.8)
|
| 716 |
+
try:
|
| 717 |
+
from scipy.stats import gaussian_kde
|
| 718 |
+
kde = gaussian_kde(coords.T, bw_method=0.20)
|
| 719 |
+
xx, yy = np.meshgrid(np.linspace(xmin, xmax, 140), np.linspace(ymin, ymax, 140))
|
| 720 |
+
zz = kde(np.vstack([xx.ravel(), yy.ravel()])).reshape(xx.shape)
|
| 721 |
+
ax.contour(xx, yy, zz, levels=10, colors=GALLERY_GRID, alpha=0.4, linewidths=0.5)
|
| 722 |
+
except Exception: pass
|
| 723 |
+
ax.scatter(coords[:,0], coords[:,1], s=2, c=GALLERY_DUST, alpha=0.45)
|
| 724 |
+
|
| 725 |
+
if not positives:
|
| 726 |
+
ax.text(0.5, 0.5, "Pick at least one positive ingredient", ha="center",
|
| 727 |
+
va="center", transform=ax.transAxes, color=GALLERY_TXTDIM)
|
| 728 |
+
return fig
|
| 729 |
+
|
| 730 |
+
pos = _basket_centroid(m, positives)
|
| 731 |
+
neg = _basket_centroid(m, negatives) if negatives else None
|
| 732 |
+
q = _unit(pos - neg) if neg is not None else pos
|
| 733 |
+
|
| 734 |
+
def project(vec, k=8):
|
| 735 |
+
sims = m.E @ vec
|
| 736 |
+
idxs = np.argsort(-sims)[:k]
|
| 737 |
+
return coords[idxs].mean(axis=0), idxs
|
| 738 |
+
|
| 739 |
+
pos_pt, pos_top = project(pos)
|
| 740 |
+
res_pt, res_top = project(q)
|
| 741 |
+
|
| 742 |
+
# Plot positives in mint
|
| 743 |
+
for n in positives:
|
| 744 |
+
if n in m.vocab:
|
| 745 |
+
p = coords[m.vocab[n]]
|
| 746 |
+
ax.scatter([p[0]], [p[1]], s=140, c=KAIKAKU_ACCENT_LIGHT,
|
| 747 |
+
edgecolors="white", linewidths=0.8, zorder=4)
|
| 748 |
+
ax.text(p[0], p[1]+0.25, n, color=KAIKAKU_ACCENT_LIGHT, ha="center",
|
| 749 |
+
fontsize=10, fontweight="bold", zorder=5)
|
| 750 |
+
# Plot negatives in warm
|
| 751 |
+
for n in (negatives or []):
|
| 752 |
+
if n in m.vocab:
|
| 753 |
+
p = coords[m.vocab[n]]
|
| 754 |
+
ax.scatter([p[0]], [p[1]], s=140, c="#F4B86E",
|
| 755 |
+
edgecolors="white", linewidths=0.8, zorder=4)
|
| 756 |
+
ax.text(p[0], p[1]+0.25, "− " + n, color="#F4B86E", ha="center",
|
| 757 |
+
fontsize=10, fontweight="bold", zorder=5)
|
| 758 |
+
# Plot result as star
|
| 759 |
+
ax.scatter([res_pt[0]], [res_pt[1]], s=420, c="#FFFFFF", marker="*",
|
| 760 |
+
edgecolors=KAIKAKU_ACCENT_LIGHT, linewidths=1.5, zorder=6)
|
| 761 |
+
# Draw arrow from positives centroid -> result
|
| 762 |
+
ax.annotate("", xy=res_pt, xytext=pos_pt,
|
| 763 |
+
arrowprops=dict(arrowstyle="->", color=KAIKAKU_ACCENT_LIGHT, lw=1.8),
|
| 764 |
+
zorder=5)
|
| 765 |
+
# Label top-K of result
|
| 766 |
+
for idx in res_top[:5]:
|
| 767 |
+
p = coords[idx]
|
| 768 |
+
ax.text(p[0]+0.10, p[1], NAMES_BY_IDX[idx], color=GALLERY_TEXT,
|
| 769 |
+
fontsize=8, alpha=0.85, zorder=4)
|
| 770 |
+
ax.scatter([p[0]], [p[1]], s=30, c="#FFFFFF", alpha=0.6,
|
| 771 |
+
edgecolors=GALLERY_TXTDIM, linewidths=0.6, zorder=3)
|
| 772 |
+
|
| 773 |
+
ax.set_xlim(xmin, xmax); ax.set_ylim(ymin, ymax); ax.set_aspect("equal")
|
| 774 |
+
title = " + ".join(positives) + (" − " + " − ".join(negatives) if negatives else "")
|
| 775 |
+
ax.text(0.02, 0.97, f"VECTOR ARITHMETIC · {sibling.upper()}",
|
| 776 |
+
transform=ax.transAxes, color=GALLERY_TXTDIM,
|
| 777 |
+
fontsize=11, family="monospace", va="top")
|
| 778 |
+
ax.text(0.02, 0.93, title, transform=ax.transAxes, color=GALLERY_TEXT,
|
| 779 |
+
fontsize=13, va="top")
|
| 780 |
+
res_names = ", ".join(NAMES_BY_IDX[i] for i in res_top[:5])
|
| 781 |
+
fig.text(0.5, 0.04, "Result top-5: " + res_names,
|
| 782 |
+
ha="center", color=GALLERY_TXTDIM, fontsize=10, family="monospace")
|
| 783 |
+
plt.tight_layout(rect=[0, 0.06, 1, 1])
|
| 784 |
+
return fig
|
| 785 |
+
|
| 786 |
+
|
| 787 |
+
# ---------- (8) Cuisine phylogeny (dendrogram) ----------
|
| 788 |
+
|
| 789 |
+
def render_cuisine_phylogeny(sibling: str):
|
| 790 |
+
m = MODELS[sibling]
|
| 791 |
+
cuisines = [c for c in _CUISINES if f"cuisine:{c}" in m.supervised_poles]
|
| 792 |
+
if len(cuisines) < 2:
|
| 793 |
+
fig, ax = _gallery_axes(figsize=(10, 5))
|
| 794 |
+
ax.text(0.5, 0.5, "Cuisine poles unavailable for this sibling",
|
| 795 |
+
ha="center", va="center", transform=ax.transAxes, color=GALLERY_TXTDIM)
|
| 796 |
+
return fig
|
| 797 |
+
poles = np.stack([_unit(m.supervised_poles[f"cuisine:{c}"]) for c in cuisines])
|
| 798 |
+
# cosine distance matrix
|
| 799 |
+
D = 1 - poles @ poles.T
|
| 800 |
+
# condensed distance
|
| 801 |
+
n = len(cuisines)
|
| 802 |
+
cond = []
|
| 803 |
+
for i in range(n):
|
| 804 |
+
for j in range(i+1, n):
|
| 805 |
+
cond.append(max(0.0, float(D[i, j])))
|
| 806 |
+
from scipy.cluster.hierarchy import linkage, dendrogram
|
| 807 |
+
Z = linkage(np.array(cond), method="average")
|
| 808 |
+
fig, ax = _gallery_axes(figsize=(11, 6))
|
| 809 |
+
matplotlib.rcParams["lines.linewidth"] = 1.4
|
| 810 |
+
ddata = dendrogram(Z, labels=cuisines, ax=ax, color_threshold=0,
|
| 811 |
+
above_threshold_color=KAIKAKU_ACCENT_LIGHT,
|
| 812 |
+
leaf_font_size=11, leaf_rotation=0)
|
| 813 |
+
ax.tick_params(axis="x", colors=GALLERY_TEXT, labelsize=10, pad=8)
|
| 814 |
+
ax.tick_params(axis="y", colors=GALLERY_TXTDIM, labelsize=8, labelleft=True, left=True)
|
| 815 |
+
ax.spines["bottom"].set_visible(False)
|
| 816 |
+
ax.spines["left"].set_visible(True); ax.spines["left"].set_color(GALLERY_TXTDIM)
|
| 817 |
+
ax.set_ylabel("cosine distance", color=GALLERY_TXTDIM, fontsize=10)
|
| 818 |
+
ax.set_title("", color=GALLERY_TEXT)
|
| 819 |
+
fig.text(0.02, 0.95, f"CUISINE PHYLOGENY · {sibling.upper()}",
|
| 820 |
+
color=GALLERY_TXTDIM, fontsize=11, family="monospace")
|
| 821 |
+
fig.text(0.02, 0.92, "Hierarchical clustering of cuisine pole vectors (cosine, average linkage)",
|
| 822 |
+
color=GALLERY_TEXT, fontsize=10)
|
| 823 |
+
plt.tight_layout(rect=[0, 0, 1, 0.93])
|
| 824 |
+
return fig
|
| 825 |
+
|
| 826 |
+
|
| 827 |
+
# ---------- (6) Cuisine cosine map (matrix-art version of chord) ----------
|
| 828 |
+
|
| 829 |
+
def render_cuisine_cosine_map(sibling: str):
|
| 830 |
+
m = MODELS[sibling]
|
| 831 |
+
cuisines = [c for c in _CUISINES if f"cuisine:{c}" in m.supervised_poles]
|
| 832 |
+
poles = np.stack([_unit(m.supervised_poles[f"cuisine:{c}"]) for c in cuisines])
|
| 833 |
+
S = poles @ poles.T
|
| 834 |
+
fig, ax = _gallery_axes(figsize=(9, 8))
|
| 835 |
+
# custom colormap: deep teal -> mint
|
| 836 |
+
from matplotlib.colors import LinearSegmentedColormap
|
| 837 |
+
cmap = LinearSegmentedColormap.from_list("kaikaku", [GALLERY_BG, GALLERY_DUST, KAIKAKU_ACCENT, KAIKAKU_ACCENT_LIGHT])
|
| 838 |
+
im = ax.imshow(S, cmap=cmap, vmin=0.0, vmax=1.0, aspect="auto")
|
| 839 |
+
ax.set_xticks(range(len(cuisines))); ax.set_yticks(range(len(cuisines)))
|
| 840 |
+
ax.set_xticklabels([c.replace("_"," ") for c in cuisines], rotation=35, ha="right",
|
| 841 |
+
color=GALLERY_TEXT, fontsize=10)
|
| 842 |
+
ax.set_yticklabels([c.replace("_"," ") for c in cuisines], color=GALLERY_TEXT, fontsize=10)
|
| 843 |
+
ax.tick_params(left=True, bottom=False)
|
| 844 |
+
for i in range(len(cuisines)):
|
| 845 |
+
for j in range(len(cuisines)):
|
| 846 |
+
v = float(S[i,j])
|
| 847 |
+
ax.text(j, i, f"{v:.2f}", ha="center", va="center",
|
| 848 |
+
color=("white" if v < 0.5 else GALLERY_BG), fontsize=10)
|
| 849 |
+
cb = plt.colorbar(im, ax=ax, shrink=0.8)
|
| 850 |
+
cb.outline.set_visible(False)
|
| 851 |
+
cb.ax.yaxis.set_tick_params(color=GALLERY_TXTDIM)
|
| 852 |
+
plt.setp(plt.getp(cb.ax.axes, "yticklabels"), color=GALLERY_TXTDIM)
|
| 853 |
+
cb.set_label("cosine", color=GALLERY_TXTDIM)
|
| 854 |
+
fig.text(0.02, 0.96, f"CUISINE COSINE MAP · {sibling.upper()}",
|
| 855 |
+
color=GALLERY_TXTDIM, fontsize=11, family="monospace")
|
| 856 |
+
fig.text(0.02, 0.93, "Pairwise cosine similarity between cuisine pole vectors",
|
| 857 |
+
color=GALLERY_TEXT, fontsize=10)
|
| 858 |
+
plt.tight_layout(rect=[0, 0, 1, 0.92])
|
| 859 |
+
return fig
|
| 860 |
+
|
| 861 |
+
|
| 862 |
+
# ---------- (3) SLERP trajectory frames ----------
|
| 863 |
+
|
| 864 |
+
def render_slerp_trajectory(sibling: str, seed: str, direction: str, max_theta: int = 60):
|
| 865 |
+
"""Show the rotation as 5 stacked UMAP panels at increasing angles."""
|
| 866 |
+
m = MODELS[sibling]
|
| 867 |
+
coords = UMAP_DATA[sibling]
|
| 868 |
+
if seed not in m.vocab or direction not in m.supervised_poles:
|
| 869 |
+
fig, ax = _gallery_axes(figsize=(11, 4))
|
| 870 |
+
ax.text(0.5, 0.5, "Pick a valid seed and direction",
|
| 871 |
+
ha="center", va="center", transform=ax.transAxes, color=GALLERY_TXTDIM)
|
| 872 |
+
return fig
|
| 873 |
+
v = _unit(m.E[m.vocab[seed]])
|
| 874 |
+
d = _unit(m.supervised_poles[direction])
|
| 875 |
+
thetas = [0, int(max_theta*0.33), int(max_theta*0.66), int(max_theta)]
|
| 876 |
+
xmin, xmax = float(coords[:,0].min()-0.5), float(coords[:,0].max()+0.5)
|
| 877 |
+
ymin, ymax = float(coords[:,1].min()-0.5), float(coords[:,1].max()+0.5)
|
| 878 |
+
fig, axes = plt.subplots(1, len(thetas), figsize=(15, 4.2), facecolor=GALLERY_BG,
|
| 879 |
+
gridspec_kw=dict(wspace=0.05))
|
| 880 |
+
for ax, theta in zip(axes, thetas):
|
| 881 |
+
ax.set_facecolor(GALLERY_BG)
|
| 882 |
+
for s in ax.spines.values(): s.set_visible(False)
|
| 883 |
+
ax.tick_params(left=False, bottom=False, labelleft=False, labelbottom=False)
|
| 884 |
+
# background
|
| 885 |
+
ax.scatter(coords[:,0], coords[:,1], s=1.8, c=GALLERY_DUST, alpha=0.5)
|
| 886 |
+
q = _slerp(v, d, theta)
|
| 887 |
+
# top-K of current query
|
| 888 |
+
sims = m.E @ q
|
| 889 |
+
top = np.argsort(-sims)[:6]
|
| 890 |
+
# exclude seed
|
| 891 |
+
seed_idx = m.vocab[seed]
|
| 892 |
+
top = [i for i in top if i != seed_idx][:5]
|
| 893 |
+
# seed in mint
|
| 894 |
+
sp = coords[seed_idx]
|
| 895 |
+
ax.scatter([sp[0]], [sp[1]], s=200, c=KAIKAKU_ACCENT_LIGHT,
|
| 896 |
+
edgecolors="white", linewidths=0.8, marker="*", zorder=4)
|
| 897 |
+
ax.text(sp[0], sp[1]+0.3, seed, color=KAIKAKU_ACCENT_LIGHT, ha="center",
|
| 898 |
+
fontsize=10, fontweight="bold", zorder=5)
|
| 899 |
+
# rotated query top-K
|
| 900 |
+
for idx in top:
|
| 901 |
+
p = coords[idx]
|
| 902 |
+
ax.scatter([p[0]], [p[1]], s=80, c="#F4B86E", edgecolors="white",
|
| 903 |
+
linewidths=0.5, alpha=0.9, zorder=4)
|
| 904 |
+
ax.text(p[0]+0.05, p[1], NAMES_BY_IDX[idx], color=GALLERY_TEXT,
|
| 905 |
+
fontsize=8, alpha=0.9, zorder=4)
|
| 906 |
+
ax.set_xlim(xmin, xmax); ax.set_ylim(ymin, ymax); ax.set_aspect("equal")
|
| 907 |
+
ax.set_title(f"θ = {theta}°", color=GALLERY_TXTDIM, fontsize=11,
|
| 908 |
+
family="monospace", pad=6)
|
| 909 |
+
fig.suptitle(f"SLERP TRAJECTORY · {seed} → {direction} · {sibling.upper()}",
|
| 910 |
+
color=GALLERY_TXTDIM, fontsize=12, family="monospace", y=0.98)
|
| 911 |
+
return fig
|
| 912 |
+
|
| 913 |
+
|
| 914 |
# ===== Theme =====
|
| 915 |
|
| 916 |
THEME = gr.themes.Soft(
|
|
|
|
| 1247 |
label="Try one of these",
|
| 1248 |
)
|
| 1249 |
|
| 1250 |
+
# ---------- Tab 5: GALLERY ----------
|
| 1251 |
+
with gr.Tab("Gallery"):
|
| 1252 |
+
gr.Markdown("Six aesthetic views of the model. All rendered in the Kaikaku palette.")
|
| 1253 |
+
with gr.Tabs():
|
| 1254 |
+
# --- Factor poster ---
|
| 1255 |
+
with gr.Tab("Factor poster"):
|
| 1256 |
+
with gr.Row():
|
| 1257 |
+
fp_sib = gr.Radio(choices=["cooc","core","chem"], value="chem",
|
| 1258 |
+
label="Sibling", scale=1)
|
| 1259 |
+
fp_factor = gr.Dropdown(choices=factor_options("chem"),
|
| 1260 |
+
value=factor_options("chem")[0] if factor_options("chem") else None,
|
| 1261 |
+
label="Factor", scale=2)
|
| 1262 |
+
fp_btn = gr.Button("Render", variant="primary", scale=1)
|
| 1263 |
+
fp_plot = gr.Plot(label="", value=render_factor_poster("chem", factor_options("chem")[0] if factor_options("chem") else ""))
|
| 1264 |
+
fp_btn.click(render_factor_poster, inputs=[fp_sib, fp_factor], outputs=fp_plot, show_progress="full")
|
| 1265 |
+
fp_sib.change(lambda s: gr.Dropdown(choices=factor_options(s), value=factor_options(s)[0] if factor_options(s) else None),
|
| 1266 |
+
inputs=fp_sib, outputs=fp_factor)
|
| 1267 |
+
|
| 1268 |
+
# --- Cuisine compass ---
|
| 1269 |
+
with gr.Tab("Cuisine compass"):
|
| 1270 |
+
with gr.Row():
|
| 1271 |
+
cc_sib = gr.Radio(choices=["cooc","core","chem"], value="chem", label="Sibling")
|
| 1272 |
+
cc_ings = gr.Dropdown(choices=ALL_INGREDIENTS, value=["miso","basil","cumin"],
|
| 1273 |
+
label="Ingredients (1-5 polygons)", multiselect=True, max_choices=5)
|
| 1274 |
+
cc_btn = gr.Button("Render", variant="primary")
|
| 1275 |
+
cc_plot = gr.Plot(label="", value=render_cuisine_compass("chem", ["miso","basil","cumin"]))
|
| 1276 |
+
cc_btn.click(render_cuisine_compass, inputs=[cc_sib, cc_ings], outputs=cc_plot, show_progress="minimal")
|
| 1277 |
+
|
| 1278 |
+
# --- Arithmetic vector art ---
|
| 1279 |
+
with gr.Tab("Vector arithmetic"):
|
| 1280 |
+
with gr.Row():
|
| 1281 |
+
va_sib = gr.Radio(choices=["cooc","core","chem"], value="core", label="Sibling")
|
| 1282 |
+
va_pos = gr.Dropdown(choices=ALL_INGREDIENTS, value=["miso"], label="Positives", multiselect=True, max_choices=5)
|
| 1283 |
+
va_neg = gr.Dropdown(choices=ALL_INGREDIENTS, value=["salt"], label="Negatives", multiselect=True, max_choices=5)
|
| 1284 |
+
va_btn = gr.Button("Render", variant="primary")
|
| 1285 |
+
va_plot = gr.Plot(label="", value=render_arithmetic_vector("core", ["miso"], ["salt"]))
|
| 1286 |
+
va_btn.click(render_arithmetic_vector, inputs=[va_sib, va_pos, va_neg], outputs=va_plot, show_progress="full")
|
| 1287 |
+
|
| 1288 |
+
# --- Cuisine phylogeny ---
|
| 1289 |
+
with gr.Tab("Cuisine phylogeny"):
|
| 1290 |
+
with gr.Row():
|
| 1291 |
+
ph_sib = gr.Radio(choices=["cooc","core","chem"], value="chem", label="Sibling")
|
| 1292 |
+
ph_btn = gr.Button("Render", variant="primary")
|
| 1293 |
+
ph_plot = gr.Plot(label="", value=render_cuisine_phylogeny("chem"))
|
| 1294 |
+
ph_btn.click(render_cuisine_phylogeny, inputs=[ph_sib], outputs=ph_plot, show_progress="minimal")
|
| 1295 |
+
|
| 1296 |
+
# --- Cuisine cosine map ---
|
| 1297 |
+
with gr.Tab("Cuisine cosine map"):
|
| 1298 |
+
with gr.Row():
|
| 1299 |
+
cm_sib = gr.Radio(choices=["cooc","core","chem"], value="chem", label="Sibling")
|
| 1300 |
+
cm_btn = gr.Button("Render", variant="primary")
|
| 1301 |
+
cm_plot = gr.Plot(label="", value=render_cuisine_cosine_map("chem"))
|
| 1302 |
+
cm_btn.click(render_cuisine_cosine_map, inputs=[cm_sib], outputs=cm_plot, show_progress="minimal")
|
| 1303 |
+
|
| 1304 |
+
# --- SLERP trajectory ---
|
| 1305 |
+
with gr.Tab("SLERP trajectory"):
|
| 1306 |
+
with gr.Row():
|
| 1307 |
+
st_sib = gr.Radio(choices=["cooc","core","chem"], value="chem", label="Sibling")
|
| 1308 |
+
st_seed = gr.Dropdown(choices=ALL_INGREDIENTS, value="rice", label="Seed")
|
| 1309 |
+
st_dir = gr.Dropdown(choices=_supervised_choices("chem"),
|
| 1310 |
+
value="cuisine:South_Asian", label="Direction")
|
| 1311 |
+
st_max = gr.Slider(15, 90, value=60, step=15, label="Max θ (deg)")
|
| 1312 |
+
st_btn = gr.Button("Render", variant="primary")
|
| 1313 |
+
st_plot = gr.Plot(label="", value=render_slerp_trajectory("chem", "rice", "cuisine:South_Asian", 60))
|
| 1314 |
+
st_btn.click(render_slerp_trajectory, inputs=[st_sib, st_seed, st_dir, st_max], outputs=st_plot, show_progress="full")
|
| 1315 |
+
st_sib.change(lambda s: gr.Dropdown(choices=_supervised_choices(s), value=None),
|
| 1316 |
+
inputs=st_sib, outputs=st_dir)
|
| 1317 |
+
|
| 1318 |
# ---- Hidden API endpoints ----
|
| 1319 |
with gr.Group(visible=False):
|
| 1320 |
api_in_s1 = gr.Textbox(visible=False)
|