Spaces:
Running
Running
Replace static constellations with interactive 3D Atlas (drag to rotate, color by food group or cuisine, basket diamonds mint, neighbours amber)
Browse files- __pycache__/app.cpython-310.pyc +0 -0
- app.py +157 -32
__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
|
@@ -999,6 +999,116 @@ def cultural_context(ingredient, sibling="chem", k=4):
|
|
| 999 |
# Constellations: sibling alignment + recipe constellation
|
| 1000 |
# =====================================================================
|
| 1001 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1002 |
def render_sibling_alignment(ingredient):
|
| 1003 |
if not ingredient or ingredient not in MODELS["cooc"].vocab:
|
| 1004 |
fig, ax = _gallery_axes(figsize=(16, 5))
|
|
@@ -2004,38 +2114,53 @@ Three sibling ingredient embeddings from [arXiv:2605.22391](https://arxiv.org/ab
|
|
| 2004 |
outputs=[cx_df, cx_md])
|
| 2005 |
|
| 2006 |
# ---------- Tab: CONSTELLATIONS ----------
|
| 2007 |
-
with gr.Tab("
|
| 2008 |
-
|
| 2009 |
-
|
| 2010 |
-
|
| 2011 |
-
|
| 2012 |
-
|
| 2013 |
-
|
| 2014 |
-
|
| 2015 |
-
|
| 2016 |
-
|
| 2017 |
-
|
| 2018 |
-
|
| 2019 |
-
|
| 2020 |
-
|
| 2021 |
-
|
| 2022 |
-
|
| 2023 |
-
|
| 2024 |
-
|
| 2025 |
-
|
| 2026 |
-
|
| 2027 |
-
|
| 2028 |
-
|
| 2029 |
-
|
| 2030 |
-
|
| 2031 |
-
|
| 2032 |
-
|
| 2033 |
-
|
| 2034 |
-
|
| 2035 |
-
|
| 2036 |
-
|
| 2037 |
-
|
| 2038 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 2039 |
|
| 2040 |
# ---------- Tab: PAPER STATS ----------
|
| 2041 |
with gr.Tab("Paper stats"):
|
|
|
|
| 999 |
# Constellations: sibling alignment + recipe constellation
|
| 1000 |
# =====================================================================
|
| 1001 |
|
| 1002 |
+
def render_3d_atlas(sibling, basket, show_neighbours=True, k=8, color_mode="food group"):
|
| 1003 |
+
"""Interactive 3D UMAP. Drag/scroll to navigate. Basket pops in mint, neighbours amber."""
|
| 1004 |
+
m = MODELS[sibling]
|
| 1005 |
+
coords2 = UMAP_DATA[sibling]
|
| 1006 |
+
n = len(NAMES_BY_IDX)
|
| 1007 |
+
# third axis = standardised PC1 of the embedding
|
| 1008 |
+
E = m.E - m.E.mean(axis=0, keepdims=True)
|
| 1009 |
+
_, _, Vt = np.linalg.svd(E, full_matrices=False)
|
| 1010 |
+
pc1 = E @ Vt[0]
|
| 1011 |
+
pc1 = (pc1 - pc1.mean()) / (pc1.std() + 1e-9)
|
| 1012 |
+
scale = (coords2.max() - coords2.min()) * 0.25
|
| 1013 |
+
z = (pc1 * scale).astype(np.float32)
|
| 1014 |
+
|
| 1015 |
+
# Colors: by food group OR by closest cuisine
|
| 1016 |
+
if color_mode == "cuisine":
|
| 1017 |
+
cuisine_pole_keys = [c for c in _CUISINES if f"cuisine:{c}" in m.supervised_poles]
|
| 1018 |
+
cuisine_poles = np.stack([_unit(m.supervised_poles[f"cuisine:{c}"]) for c in cuisine_pole_keys])
|
| 1019 |
+
Xn = m.E / np.linalg.norm(m.E, axis=1, keepdims=True)
|
| 1020 |
+
sims = Xn @ cuisine_poles.T
|
| 1021 |
+
best = sims.argmax(axis=1)
|
| 1022 |
+
palette = ["#288B79","#F4B86E","#E8C0E8","#9BC9E8","#D7E89B","#FFAA8A","#A8D5CA","#E8E0A0"]
|
| 1023 |
+
colors = [palette[best[i] % len(palette)] for i in range(n)]
|
| 1024 |
+
hover_text = [
|
| 1025 |
+
f"<b>{NAMES_BY_IDX[i]}</b><br>closest cuisine: {cuisine_pole_keys[best[i]].replace('_',' ')}<br>group: {FOOD_GROUPS[i]}"
|
| 1026 |
+
for i in range(n)
|
| 1027 |
+
]
|
| 1028 |
+
else:
|
| 1029 |
+
colors = [FG_COLORS.get(fg, "#cccccc") for fg in FOOD_GROUPS]
|
| 1030 |
+
hover_text = [f"<b>{NAMES_BY_IDX[i]}</b><br>group: {FOOD_GROUPS[i]}" for i in range(n)]
|
| 1031 |
+
|
| 1032 |
+
basket_set = set(basket or [])
|
| 1033 |
+
basket_idxs = [m.vocab[b] for b in (basket or []) if b in m.vocab]
|
| 1034 |
+
neighbour_set = set()
|
| 1035 |
+
if show_neighbours and basket_idxs:
|
| 1036 |
+
c = _basket_centroid(m, basket)
|
| 1037 |
+
if c is not None:
|
| 1038 |
+
for nm, _ in _topk(m, c, int(k), exclude=basket):
|
| 1039 |
+
neighbour_set.add(nm)
|
| 1040 |
+
|
| 1041 |
+
keep = lambda i: NAMES_BY_IDX[i] not in basket_set and NAMES_BY_IDX[i] not in neighbour_set
|
| 1042 |
+
bg_idx = [i for i in range(n) if keep(i)]
|
| 1043 |
+
|
| 1044 |
+
fig = go.Figure()
|
| 1045 |
+
fig.add_trace(go.Scatter3d(
|
| 1046 |
+
x=[float(coords2[i,0]) for i in bg_idx],
|
| 1047 |
+
y=[float(coords2[i,1]) for i in bg_idx],
|
| 1048 |
+
z=[float(z[i]) for i in bg_idx],
|
| 1049 |
+
mode="markers",
|
| 1050 |
+
marker=dict(size=3, color=[colors[i] for i in bg_idx], opacity=0.55, line=dict(width=0)),
|
| 1051 |
+
text=[hover_text[i] for i in bg_idx],
|
| 1052 |
+
hovertemplate="%{text}<extra></extra>",
|
| 1053 |
+
name="ingredients", showlegend=False,
|
| 1054 |
+
))
|
| 1055 |
+
if neighbour_set:
|
| 1056 |
+
ni = [i for i in range(n) if NAMES_BY_IDX[i] in neighbour_set]
|
| 1057 |
+
fig.add_trace(go.Scatter3d(
|
| 1058 |
+
x=[float(coords2[i,0]) for i in ni],
|
| 1059 |
+
y=[float(coords2[i,1]) for i in ni],
|
| 1060 |
+
z=[float(z[i]) for i in ni],
|
| 1061 |
+
mode="markers+text",
|
| 1062 |
+
marker=dict(size=8, color="#F59E0B", opacity=0.96,
|
| 1063 |
+
line=dict(color="#ffffff", width=1.5),
|
| 1064 |
+
symbol="circle"),
|
| 1065 |
+
text=[NAMES_BY_IDX[i].replace("_"," ") for i in ni],
|
| 1066 |
+
textposition="top center",
|
| 1067 |
+
textfont=dict(size=11, color="#1f2937", family="Inter"),
|
| 1068 |
+
hovertemplate="<b>%{text}</b> (neighbour)<extra></extra>",
|
| 1069 |
+
name=f"top-{int(k)} neighbours",
|
| 1070 |
+
))
|
| 1071 |
+
if basket_idxs:
|
| 1072 |
+
fig.add_trace(go.Scatter3d(
|
| 1073 |
+
x=[float(coords2[i,0]) for i in basket_idxs],
|
| 1074 |
+
y=[float(coords2[i,1]) for i in basket_idxs],
|
| 1075 |
+
z=[float(z[i]) for i in basket_idxs],
|
| 1076 |
+
mode="markers+text",
|
| 1077 |
+
marker=dict(size=15, color=KAIKAKU_ACCENT, symbol="diamond",
|
| 1078 |
+
line=dict(color="#0f172a", width=2)),
|
| 1079 |
+
text=[NAMES_BY_IDX[i].replace("_"," ") for i in basket_idxs],
|
| 1080 |
+
textposition="top center",
|
| 1081 |
+
textfont=dict(size=13, color="#0f172a", family="Inter"),
|
| 1082 |
+
hovertemplate="<b>%{text}</b> (basket)<extra></extra>",
|
| 1083 |
+
name="basket",
|
| 1084 |
+
))
|
| 1085 |
+
|
| 1086 |
+
fig.update_layout(
|
| 1087 |
+
title=dict(
|
| 1088 |
+
text=f"<b>3D Atlas</b> 路 Epicure-{sibling.capitalize()} 路 1,790 ingredients 路 drag to rotate, scroll to zoom",
|
| 1089 |
+
font=dict(size=13, color="#0f172a", family="Inter"), x=0.02, xanchor="left",
|
| 1090 |
+
),
|
| 1091 |
+
scene=dict(
|
| 1092 |
+
xaxis=dict(title="UMAP 1", color="#475569", gridcolor="#e2e8f0",
|
| 1093 |
+
backgroundcolor="#ffffff", showspikes=False, zeroline=False),
|
| 1094 |
+
yaxis=dict(title="UMAP 2", color="#475569", gridcolor="#e2e8f0",
|
| 1095 |
+
backgroundcolor="#ffffff", showspikes=False, zeroline=False),
|
| 1096 |
+
zaxis=dict(title="PC1", color="#475569", gridcolor="#e2e8f0",
|
| 1097 |
+
backgroundcolor="#ffffff", showspikes=False, zeroline=False),
|
| 1098 |
+
bgcolor="#ffffff",
|
| 1099 |
+
camera=dict(up=dict(x=0, y=0, z=1), eye=dict(x=1.6, y=1.6, z=1.0)),
|
| 1100 |
+
aspectmode="cube",
|
| 1101 |
+
),
|
| 1102 |
+
height=720,
|
| 1103 |
+
margin=dict(l=10, r=10, t=46, b=10),
|
| 1104 |
+
paper_bgcolor="#ffffff",
|
| 1105 |
+
legend=dict(orientation="h", yanchor="bottom", y=0.0, xanchor="right", x=1.0,
|
| 1106 |
+
font=dict(color="#0f172a", size=11), bgcolor="rgba(255,255,255,0.85)",
|
| 1107 |
+
bordercolor="#e2e8f0", borderwidth=1),
|
| 1108 |
+
)
|
| 1109 |
+
return fig
|
| 1110 |
+
|
| 1111 |
+
|
| 1112 |
def render_sibling_alignment(ingredient):
|
| 1113 |
if not ingredient or ingredient not in MODELS["cooc"].vocab:
|
| 1114 |
fig, ax = _gallery_axes(figsize=(16, 5))
|
|
|
|
| 2114 |
outputs=[cx_df, cx_md])
|
| 2115 |
|
| 2116 |
# ---------- Tab: CONSTELLATIONS ----------
|
| 2117 |
+
with gr.Tab("3D Atlas"):
|
| 2118 |
+
gr.Markdown(
|
| 2119 |
+
"**Interactive 3D map of all 1,790 ingredients.** "
|
| 2120 |
+
"Drag with mouse to rotate. Scroll to zoom. Hover for ingredient names. "
|
| 2121 |
+
"Basket members appear as mint diamonds; their nearest neighbours pop in amber."
|
| 2122 |
+
)
|
| 2123 |
+
with gr.Row():
|
| 2124 |
+
atl_sib = gr.Radio(choices=["cooc","core","chem"], value="chem", label="Sibling")
|
| 2125 |
+
atl_color = gr.Radio(choices=["food group", "cuisine"], value="food group",
|
| 2126 |
+
label="Color points by")
|
| 2127 |
+
atl_basket = gr.Dropdown(
|
| 2128 |
+
choices=ALL_INGREDIENTS,
|
| 2129 |
+
value=["miso","basil","chocolate","tomato"],
|
| 2130 |
+
label="Highlight these ingredients",
|
| 2131 |
+
multiselect=True, max_choices=15,
|
| 2132 |
+
)
|
| 2133 |
+
with gr.Row():
|
| 2134 |
+
atl_show_nb = gr.Checkbox(value=True, label="Show top-K neighbours")
|
| 2135 |
+
atl_k = gr.Slider(3, 20, value=8, step=1, label="K")
|
| 2136 |
+
atl_btn = gr.Button("Update", variant="primary")
|
| 2137 |
+
atl_plot = gr.Plot(
|
| 2138 |
+
value=render_3d_atlas("chem", ["miso","basil","chocolate","tomato"], True, 8, "food group"),
|
| 2139 |
+
label="",
|
| 2140 |
+
)
|
| 2141 |
+
atl_btn.click(render_3d_atlas,
|
| 2142 |
+
inputs=[atl_sib, atl_basket, atl_show_nb, atl_k, atl_color],
|
| 2143 |
+
outputs=atl_plot, show_progress="minimal")
|
| 2144 |
+
atl_sib.change(render_3d_atlas,
|
| 2145 |
+
inputs=[atl_sib, atl_basket, atl_show_nb, atl_k, atl_color],
|
| 2146 |
+
outputs=atl_plot, show_progress="minimal")
|
| 2147 |
+
atl_color.change(render_3d_atlas,
|
| 2148 |
+
inputs=[atl_sib, atl_basket, atl_show_nb, atl_k, atl_color],
|
| 2149 |
+
outputs=atl_plot, show_progress="minimal")
|
| 2150 |
+
atl_basket.change(render_3d_atlas,
|
| 2151 |
+
inputs=[atl_sib, atl_basket, atl_show_nb, atl_k, atl_color],
|
| 2152 |
+
outputs=atl_plot, show_progress="minimal")
|
| 2153 |
+
gr.Examples(
|
| 2154 |
+
examples=[
|
| 2155 |
+
["chem", "food group", ["miso","basil","chocolate","tomato"], True, 8],
|
| 2156 |
+
["chem", "cuisine", ["chicken","lemongrass","coconut_milk","fish_sauce"], True, 10],
|
| 2157 |
+
["core", "cuisine", ["tomato","mozzarella_cheese","basil","olive_oil"], True, 8],
|
| 2158 |
+
["chem", "food group", ["cumin","coriander","turmeric","cardamom","fenugreek_seed"], True, 10],
|
| 2159 |
+
["cooc", "food group", ["red_wine","brandy","whiskey","bourbon","cognac"], True, 8],
|
| 2160 |
+
],
|
| 2161 |
+
inputs=[atl_sib, atl_color, atl_basket, atl_show_nb, atl_k],
|
| 2162 |
+
label="Try one of these baskets",
|
| 2163 |
+
)
|
| 2164 |
|
| 2165 |
# ---------- Tab: PAPER STATS ----------
|
| 2166 |
with gr.Tab("Paper stats"):
|