Michael Rabinovich Cursor commited on
Commit
2a0d6fb
·
1 Parent(s): 0c1ec25

metrics: tighten Metrics page copy

Browse files

Fix the "Editing tasks" anchor landing short (bottom spacer), drop the
per-section "Full derivation" links in favor of a single docs/metrics.md
pointer at the end, trim wording (Boolean kernel, "(pieces)", "(e.g.
through-holes)", "not the mean", the report-overlay note), and remove
em-dashes.

Co-authored-by: Cursor <cursoragent@cursor.com>

Files changed (1) hide show
  1. metrics_page.py +35 -42
metrics_page.py CHANGED
@@ -98,7 +98,8 @@ code { background: #eef0f4; padding: 1px 5px; border-radius: 4px;
98
  table { border-collapse: collapse; width: 100%; margin: 12px 0; font-size: 0.92em; }
99
  th, td { border: 1px solid #e3e5ea; padding: 7px 10px; text-align: left; }
100
  th { background: #f5f7fa; }
101
- .deep { font-size: 0.86em; color: #5b6170; margin-top: 12px; }
 
102
  .toc { background: #fff; border: 1px solid #e3e5ea; border-radius: 12px;
103
  padding: 14px 20px; margin: 16px 0; }
104
  .toc ul { margin: 6px 0 0; padding-left: 18px; }
@@ -115,18 +116,11 @@ figure.fig figcaption { font-size: 0.84em; color: #5b6170; margin-top: 6px;
115
 
116
  def _section(
117
  *, anchor: str, css_class: str, tag: str, title: str, body: str,
118
- deep_dive: str | None = None,
119
  ) -> str:
120
- deep = (
121
- f'<p class="deep">Full derivation: '
122
- f'<a href="{deep_dive}" target="_blank" rel="noopener">{deep_dive}</a></p>'
123
- if deep_dive
124
- else ""
125
- )
126
  return (
127
  f'<section class="card {css_class}" id="{anchor}">'
128
  f'<h2><span class="axis-tag">{tag}</span>{title}</h2>'
129
- f"{body}{deep}"
130
  "</section>"
131
  )
132
 
@@ -150,8 +144,8 @@ def build_metrics_page() -> str:
150
  " = 0.4*shape + 0.4*interface + 0.2*topology otherwise"
151
  "</pre>"
152
  "<p class='note'>(This is the <b>generation</b> composition. "
153
- "<b>Editing</b> tasks renormalize the shape axis and reweight "
154
- f'see <a href="#{a["editing"]}">Editing tasks</a>.)</p>'
155
  "<table><thead><tr><th>Component</th><th>Range</th>"
156
  "<th>What it asks</th></tr></thead><tbody>"
157
  f'<tr><td><a href="#{a["validity"]}">CAD Validity</a> (gate)</td>'
@@ -164,7 +158,7 @@ def build_metrics_page() -> str:
164
  "<td>[0, 1]</td><td>Does it bolt up to the same fixture?</td></tr>"
165
  "</tbody></table>"
166
  "<h3>Why three axes</h3>"
167
- "<p>They are orthogonal by construction each catches errors the "
168
  "others are blind to:</p>"
169
  "<ul>"
170
  "<li><b>Shape</b> catches wrong bulk geometry; blind to topology.</li>"
@@ -176,7 +170,6 @@ def build_metrics_page() -> str:
176
  "<p class='note'>Outputs are rigidly aligned to the ground truth "
177
  "(rotation + translation only, never scale) before scoring.</p>"
178
  ),
179
- deep_dive=f"{_DOCS_BASE}/metrics.md",
180
  )
181
 
182
  validity = _section(
@@ -190,16 +183,15 @@ def build_metrics_page() -> str:
190
  "<code>cad_score = 0</code>, so an invalid solid never beats a worse "
191
  "but valid one. Passing requires all of:</p>"
192
  "<ol>"
193
- "<li><b>Well-formed BREP</b> no per-face / edge / vertex errors "
194
  "(self-intersecting wires, edges off their surface, etc.).</li>"
195
- "<li><b>Watertight</b> every shell is closed; no naked or free "
196
  "edges.</li>"
197
- "<li><b>Meshable as a closed orientable manifold</b> tessellates "
198
  "to a manifold, closed (3F = 2E), orientation-consistent triangle "
199
  "mesh.</li>"
200
  "</ol>"
201
  ),
202
- deep_dive=f"{_DOCS_BASE}/metrics/cad_validity.md",
203
  )
204
 
205
  shape = _section(
@@ -222,12 +214,11 @@ def build_metrics_page() -> str:
222
  "combine into F1.</p>"
223
  "<h3>Volume IoU</h3>"
224
  "<p>Shared volume of the two solids over their combined volume "
225
- "(intersection over union), via a Boolean kernel.</p>"
226
  "<p class='note'>Both use a tolerance proportional to part size, so "
227
- "small features can move without shifting the score those are "
228
  f'covered by <a href="#{a["interface"]}">interface match</a>.</p>'
229
  ),
230
- deep_dive=f"{_DOCS_BASE}/metrics/shape_similarity.md",
231
  )
232
 
233
  topology = _section(
@@ -240,9 +231,8 @@ def build_metrics_page() -> str:
240
  "through-holes, and internal voids? It compares the three "
241
  "<b>Betti numbers</b> of the solid:</p>"
242
  "<ul>"
243
- "<li><b>b&#8320;</b>: connected solid components (pieces).</li>"
244
- "<li><b>b&#8321;</b>: independent through-handles (e.g. "
245
- "through-holes).</li>"
246
  "<li><b>b&#8322;</b>: enclosed internal voids (cavities).</li>"
247
  "</ul>"
248
  "<p>Each axis gets a fuzzy log-ratio against GT, sharpened by "
@@ -251,14 +241,13 @@ def build_metrics_page() -> str:
251
  "s_i = ((min(cand,gt) + 1) / (max(cand,gt) + 1)) ^ 2\n"
252
  "topology_match = s_0 * s_1 * s_2"
253
  "</pre>"
254
- "<p>The product (not the mean) means one wrong count collapses the "
255
  "score: topology is discrete, so two of three right is not a partial "
256
  "match. Example: GT (1,2,0) vs candidate (1,4,0) scores "
257
  "(3/5)&#178; = 0.36. Blind features (blind pockets, fillets, "
258
  "chamfers) are topologically trivial and covered by the other "
259
  "axes.</p>"
260
  ),
261
- deep_dive=f"{_DOCS_BASE}/metrics/topo_match.md",
262
  )
263
 
264
  interface = _section(
@@ -271,13 +260,13 @@ def build_metrics_page() -> str:
271
  "region of space the candidate must match in shape, size, and "
272
  "position:</p>"
273
  "<ul>"
274
- "<li><b>Keep-out (KOR)</b> must be empty (a bolt hole, a slot).</li>"
275
- "<li><b>Keep-in (KIR)</b> must be solid (a locating boss, a "
276
  "pin).</li>"
277
  "</ul>"
278
  "<h3>Mating groups</h3>"
279
  "<p>The features that must seat together against a single fixture "
280
- "form one <b>mating group</b> here, two bolt holes and a slot that "
281
  "one jig drops into. A part can have several independent groups (say "
282
  "a bolt pattern on one face and a boss on another), and each group "
283
  "is scored on its own.</p>"
@@ -292,24 +281,19 @@ def build_metrics_page() -> str:
292
  "<h3>Scoring</h3>"
293
  "<p>Per group:</p>"
294
  "<ol>"
295
- "<li><b>Per-feature fit</b> volumetric IoU against the region "
296
  "(with a thin shell of opposite material, so both oversize and "
297
  "undersize lose points).</li>"
298
- "<li><b>Bounded pose search</b> &#177;1&#176; and &#177;1% of part "
299
  "size per axis, so a feature isn't penalized for the residual of "
300
  "whole-part alignment.</li>"
301
- "<li><b>Pass/fail ramp</b> IoU &#8805; 0.95 &#8594; 1, &#8804; 0.80 "
302
  "&#8594; 0, linear between; a sloppy fit scores 0.</li>"
303
  "</ol>"
304
  "<p>A group scores as its <b>worst</b> feature (the minimum); the "
305
  "fixture scores as the <b>mean</b> over its groups, so nailing one "
306
  "interface and missing another still earns partial credit.</p>"
307
- "<p class='note'>In the report's overlay: <b>blue</b> where it fits, "
308
- "<b>red</b> where the candidate has material it shouldn't (too much), "
309
- "<b>amber</b> where it's missing material it should have (too "
310
- "little).</p>"
311
  ),
312
- deep_dive=f"{_DOCS_BASE}/metrics/interface_match.md",
313
  )
314
 
315
  editing = _section(
@@ -334,12 +318,11 @@ def build_metrics_page() -> str:
334
  "therefore caps at 0.3 + 0.1 = 0.4, and any real shape improvement "
335
  "clears it.</p>"
336
  ),
337
- deep_dive=f"{_DOCS_BASE}/metrics.md#editing-tasks-no-op-renormalization",
338
  )
339
 
340
  toc = (
341
  '<nav class="toc"><b>On this page</b><ul>'
342
- f'<li><a href="#{a["cad_score"]}">CAD Score &mdash; how one part is scored</a></li>'
343
  f'<li><a href="#{a["validity"]}">CAD Validity (gate)</a></li>'
344
  f'<li><a href="#{a["shape"]}">Shape Similarity</a></li>'
345
  f'<li><a href="#{a["topology"]}">Topology Match</a></li>'
@@ -348,19 +331,29 @@ def build_metrics_page() -> str:
348
  "</ul></nav>"
349
  )
350
 
 
 
 
 
 
 
 
 
 
 
 
351
  return (
352
  "<!DOCTYPE html><html lang='en'><head>"
353
  "<meta charset='utf-8'>"
354
  "<meta name='viewport' content='width=device-width, initial-scale=1'>"
355
- "<title>CADGenBench &mdash; Metrics</title>"
356
  f"<style>{_CSS}</style>"
357
  "</head><body>"
358
  "<h1>Metrics</h1>"
359
  "<p class='lede'>How CADGenBench scores one generated CAD part against "
360
  "the ground truth. These metrics are new, so this page explains each "
361
- "one; the canonical reference lives in the "
362
- f'<a href="{_DOCS_BASE}/metrics.md" target="_blank" rel="noopener">'
363
- "code repo</a>.</p>"
364
  f"{toc}{overview}{validity}{shape}{topology}{interface}{editing}"
 
365
  "</body></html>"
366
  )
 
98
  table { border-collapse: collapse; width: 100%; margin: 12px 0; font-size: 0.92em; }
99
  th, td { border: 1px solid #e3e5ea; padding: 7px 10px; text-align: left; }
100
  th { background: #f5f7fa; }
101
+ .deep { font-size: 0.9em; color: #5b6170; margin: 20px 0 0; }
102
+ .endspace { height: 60vh; }
103
  .toc { background: #fff; border: 1px solid #e3e5ea; border-radius: 12px;
104
  padding: 14px 20px; margin: 16px 0; }
105
  .toc ul { margin: 6px 0 0; padding-left: 18px; }
 
116
 
117
  def _section(
118
  *, anchor: str, css_class: str, tag: str, title: str, body: str,
 
119
  ) -> str:
 
 
 
 
 
 
120
  return (
121
  f'<section class="card {css_class}" id="{anchor}">'
122
  f'<h2><span class="axis-tag">{tag}</span>{title}</h2>'
123
+ f"{body}"
124
  "</section>"
125
  )
126
 
 
144
  " = 0.4*shape + 0.4*interface + 0.2*topology otherwise"
145
  "</pre>"
146
  "<p class='note'>(This is the <b>generation</b> composition. "
147
+ "<b>Editing</b> tasks renormalize the shape axis and reweight; "
148
+ f'see <a href="#{a["editing"]}">Editing tasks</a> below.)</p>'
149
  "<table><thead><tr><th>Component</th><th>Range</th>"
150
  "<th>What it asks</th></tr></thead><tbody>"
151
  f'<tr><td><a href="#{a["validity"]}">CAD Validity</a> (gate)</td>'
 
158
  "<td>[0, 1]</td><td>Does it bolt up to the same fixture?</td></tr>"
159
  "</tbody></table>"
160
  "<h3>Why three axes</h3>"
161
+ "<p>They are orthogonal by construction: each catches errors the "
162
  "others are blind to:</p>"
163
  "<ul>"
164
  "<li><b>Shape</b> catches wrong bulk geometry; blind to topology.</li>"
 
170
  "<p class='note'>Outputs are rigidly aligned to the ground truth "
171
  "(rotation + translation only, never scale) before scoring.</p>"
172
  ),
 
173
  )
174
 
175
  validity = _section(
 
183
  "<code>cad_score = 0</code>, so an invalid solid never beats a worse "
184
  "but valid one. Passing requires all of:</p>"
185
  "<ol>"
186
+ "<li><b>Well-formed BREP</b>: no per-face / edge / vertex errors "
187
  "(self-intersecting wires, edges off their surface, etc.).</li>"
188
+ "<li><b>Watertight</b>: every shell is closed; no naked or free "
189
  "edges.</li>"
190
+ "<li><b>Meshable as a closed orientable manifold</b>: tessellates "
191
  "to a manifold, closed (3F = 2E), orientation-consistent triangle "
192
  "mesh.</li>"
193
  "</ol>"
194
  ),
 
195
  )
196
 
197
  shape = _section(
 
214
  "combine into F1.</p>"
215
  "<h3>Volume IoU</h3>"
216
  "<p>Shared volume of the two solids over their combined volume "
217
+ "(intersection over union).</p>"
218
  "<p class='note'>Both use a tolerance proportional to part size, so "
219
+ "small features can move without shifting the score; those are "
220
  f'covered by <a href="#{a["interface"]}">interface match</a>.</p>'
221
  ),
 
222
  )
223
 
224
  topology = _section(
 
231
  "through-holes, and internal voids? It compares the three "
232
  "<b>Betti numbers</b> of the solid:</p>"
233
  "<ul>"
234
+ "<li><b>b&#8320;</b>: connected solid components.</li>"
235
+ "<li><b>b&#8321;</b>: independent through-handles.</li>"
 
236
  "<li><b>b&#8322;</b>: enclosed internal voids (cavities).</li>"
237
  "</ul>"
238
  "<p>Each axis gets a fuzzy log-ratio against GT, sharpened by "
 
241
  "s_i = ((min(cand,gt) + 1) / (max(cand,gt) + 1)) ^ 2\n"
242
  "topology_match = s_0 * s_1 * s_2"
243
  "</pre>"
244
+ "<p>The product means one wrong count collapses the "
245
  "score: topology is discrete, so two of three right is not a partial "
246
  "match. Example: GT (1,2,0) vs candidate (1,4,0) scores "
247
  "(3/5)&#178; = 0.36. Blind features (blind pockets, fillets, "
248
  "chamfers) are topologically trivial and covered by the other "
249
  "axes.</p>"
250
  ),
 
251
  )
252
 
253
  interface = _section(
 
260
  "region of space the candidate must match in shape, size, and "
261
  "position:</p>"
262
  "<ul>"
263
+ "<li><b>Keep-out (KOR)</b>: must be empty (a bolt hole, a slot).</li>"
264
+ "<li><b>Keep-in (KIR)</b>: must be solid (a locating boss, a "
265
  "pin).</li>"
266
  "</ul>"
267
  "<h3>Mating groups</h3>"
268
  "<p>The features that must seat together against a single fixture "
269
+ "form one <b>mating group</b>: here, two bolt holes and a slot that "
270
  "one jig drops into. A part can have several independent groups (say "
271
  "a bolt pattern on one face and a boss on another), and each group "
272
  "is scored on its own.</p>"
 
281
  "<h3>Scoring</h3>"
282
  "<p>Per group:</p>"
283
  "<ol>"
284
+ "<li><b>Per-feature fit</b>: volumetric IoU against the region "
285
  "(with a thin shell of opposite material, so both oversize and "
286
  "undersize lose points).</li>"
287
+ "<li><b>Bounded pose search</b>: &#177;1&#176; and &#177;1% of part "
288
  "size per axis, so a feature isn't penalized for the residual of "
289
  "whole-part alignment.</li>"
290
+ "<li><b>Pass/fail ramp</b>: IoU &#8805; 0.95 &#8594; 1, &#8804; 0.80 "
291
  "&#8594; 0, linear between; a sloppy fit scores 0.</li>"
292
  "</ol>"
293
  "<p>A group scores as its <b>worst</b> feature (the minimum); the "
294
  "fixture scores as the <b>mean</b> over its groups, so nailing one "
295
  "interface and missing another still earns partial credit.</p>"
 
 
 
 
296
  ),
 
297
  )
298
 
299
  editing = _section(
 
318
  "therefore caps at 0.3 + 0.1 = 0.4, and any real shape improvement "
319
  "clears it.</p>"
320
  ),
 
321
  )
322
 
323
  toc = (
324
  '<nav class="toc"><b>On this page</b><ul>'
325
+ f'<li><a href="#{a["cad_score"]}">CAD Score: how one part is scored</a></li>'
326
  f'<li><a href="#{a["validity"]}">CAD Validity (gate)</a></li>'
327
  f'<li><a href="#{a["shape"]}">Shape Similarity</a></li>'
328
  f'<li><a href="#{a["topology"]}">Topology Match</a></li>'
 
331
  "</ul></nav>"
332
  )
333
 
334
+ footer = (
335
+ '<p class="deep">For the full definitions and derivations, see the '
336
+ f'metrics reference in the code: '
337
+ f'<a href="{_DOCS_BASE}/metrics.md" target="_blank" rel="noopener">'
338
+ "docs/metrics.md</a>.</p>"
339
+ # Trailing space so the last section can scroll to the top of the
340
+ # viewport when reached via an in-page anchor (e.g. the "Editing
341
+ # tasks" link); without it a near-bottom target lands mid-screen.
342
+ '<div class="endspace" aria-hidden="true"></div>'
343
+ )
344
+
345
  return (
346
  "<!DOCTYPE html><html lang='en'><head>"
347
  "<meta charset='utf-8'>"
348
  "<meta name='viewport' content='width=device-width, initial-scale=1'>"
349
+ "<title>CADGenBench Metrics</title>"
350
  f"<style>{_CSS}</style>"
351
  "</head><body>"
352
  "<h1>Metrics</h1>"
353
  "<p class='lede'>How CADGenBench scores one generated CAD part against "
354
  "the ground truth. These metrics are new, so this page explains each "
355
+ "one.</p>"
 
 
356
  f"{toc}{overview}{validity}{shape}{topology}{interface}{editing}"
357
+ f"{footer}"
358
  "</body></html>"
359
  )