josefchen commited on
Commit
a4a0b9d
verified
1 Parent(s): 3015aab

Replace static constellations with interactive 3D Atlas (drag to rotate, color by food group or cuisine, basket diamonds mint, neighbours amber)

Browse files
Files changed (2) hide show
  1. __pycache__/app.cpython-310.pyc +0 -0
  2. 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("Constellations"):
2008
- with gr.Tabs():
2009
- with gr.Tab("Sibling alignment"):
2010
- gr.Markdown("Same ingredient on all three UMAPs. The spectrum-of-models thesis as a single image.")
2011
- with gr.Row():
2012
- sa_ing = gr.Dropdown(choices=ALL_INGREDIENTS, value="basil", label="Ingredient")
2013
- sa_btn = gr.Button("Render", variant="primary")
2014
- sa_plot = gr.Plot(label="")
2015
- sa_btn.click(render_sibling_alignment, inputs=[sa_ing], outputs=[sa_plot], show_progress="full")
2016
- sa_ing.change(render_sibling_alignment, inputs=[sa_ing], outputs=[sa_plot])
2017
- demo.load(render_sibling_alignment, inputs=[sa_ing], outputs=[sa_plot])
2018
-
2019
- with gr.Tab("Recipe constellation"):
2020
- gr.Markdown("A recipe drawn as a constellation on the UMAP, edges to nearest basket-mates.")
2021
- with gr.Row():
2022
- rc_sib = gr.Radio(choices=["cooc","core","chem"], value="chem", label="Sibling")
2023
- rc_ings = gr.Dropdown(choices=ALL_INGREDIENTS, multiselect=True,
2024
- value=["tomato","basil","garlic","olive_oil","mozzarella_cheese"],
2025
- label="Recipe ingredients", max_choices=12)
2026
- rc_btn = gr.Button("Render", variant="primary")
2027
- rc_plot = gr.Plot(label="")
2028
- rc_btn.click(render_recipe_constellation, inputs=[rc_sib, rc_ings], outputs=[rc_plot], show_progress="full")
2029
- demo.load(render_recipe_constellation, inputs=[rc_sib, rc_ings], outputs=[rc_plot])
2030
- gr.Examples(
2031
- examples=[
2032
- ["chem", ["tomato","basil","garlic","olive_oil","mozzarella_cheese"]],
2033
- ["chem", ["chicken","lemongrass","coconut_milk","fish_sauce","lime","ginger","chili_pepper"]],
2034
- ["chem", ["beef","cumin","coriander","onion","garlic","tomato","cinnamon"]],
2035
- ["core", ["chocolate","strawberry","cream","sugar","vanilla"]],
2036
- ],
2037
- inputs=[rc_sib, rc_ings], label="Try a recipe",
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"):