Spaces:
Sleeping
Sleeping
Split benchmark into pages and clarify attribution
Browse files
app.py
CHANGED
|
@@ -44,6 +44,8 @@ CSS = """
|
|
| 44 |
--line-soft: #d4cab8;
|
| 45 |
--green: #b9e7bd;
|
| 46 |
--green-strong: #7fc98a;
|
|
|
|
|
|
|
| 47 |
--link: #174f8a;
|
| 48 |
--radius: 6px;
|
| 49 |
}
|
|
@@ -281,6 +283,25 @@ gradio-app,
|
|
| 281 |
color: var(--ink);
|
| 282 |
}
|
| 283 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 284 |
.record-top {
|
| 285 |
padding: 9px 10px 0;
|
| 286 |
}
|
|
@@ -432,6 +453,35 @@ gradio-app,
|
|
| 432 |
font-size: 13px;
|
| 433 |
}
|
| 434 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 435 |
.submit-shell,
|
| 436 |
.schema-shell {
|
| 437 |
width: min(1180px, calc(100% - 32px));
|
|
@@ -511,6 +561,16 @@ gradio-app,
|
|
| 511 |
max-height: 315px;
|
| 512 |
}
|
| 513 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 514 |
@media (max-width: 720px) {
|
| 515 |
.contain,
|
| 516 |
.submit-shell,
|
|
@@ -614,6 +674,8 @@ MONTHS = (
|
|
| 614 |
"December",
|
| 615 |
)
|
| 616 |
|
|
|
|
|
|
|
| 617 |
|
| 618 |
def esc(value: Any) -> str:
|
| 619 |
return html.escape("" if value is None else str(value), quote=True)
|
|
@@ -709,6 +771,18 @@ def author_name(record: dict[str, Any]) -> str:
|
|
| 709 |
return str(record.get("submitted_by") or "unknown").strip() or "unknown"
|
| 710 |
|
| 711 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 712 |
def metric_symbol(record: dict[str, Any]) -> str:
|
| 713 |
return str(record.get("metric_symbol") or "s")
|
| 714 |
|
|
@@ -725,6 +799,17 @@ def metric_value(record: dict[str, Any]) -> str:
|
|
| 725 |
return str(value)
|
| 726 |
|
| 727 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 728 |
def compact_expression(record: dict[str, Any]) -> str:
|
| 729 |
expression = str(record.get("metric_expression") or "").strip()
|
| 730 |
expression = re.sub(r"\s+", " ", expression)
|
|
@@ -864,9 +949,32 @@ def visual_provenance(record: dict[str, Any], visual_record: dict[str, Any]) ->
|
|
| 864 |
return "Coordinate rendering attached to this case."
|
| 865 |
|
| 866 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 867 |
def record_details(record: dict[str, Any], visual_record: dict[str, Any], source: str, image_name: str, expression: str, analytical: Any) -> str:
|
| 868 |
rows: list[str] = []
|
| 869 |
rows.append(detail_row("Rendering", esc(visual_provenance(record, visual_record))))
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 870 |
if record.get("record_type") == "verified":
|
| 871 |
rows.append(detail_row("Submitted", esc(display_date(record))))
|
| 872 |
notes = str(record.get("notes") or "").strip()
|
|
@@ -902,19 +1010,23 @@ def record_details(record: dict[str, Any], visual_record: dict[str, Any], source
|
|
| 902 |
def record_card(record: dict[str, Any], coordinates: dict[str, dict[str, Any]]) -> str:
|
| 903 |
verified = record.get("record_type") == "verified"
|
| 904 |
visual_record = visual_record_for(record, coordinates)
|
|
|
|
| 905 |
source = source_link(record)
|
| 906 |
expression = compact_expression(record)
|
| 907 |
image_name = source_image_name(record)
|
| 908 |
analytical = friedman_reference(record).get("analytical_or_proved")
|
| 909 |
-
visible_author =
|
|
|
|
|
|
|
|
|
|
| 910 |
return f"""
|
| 911 |
-
<article class="record-card {
|
| 912 |
<div class="record-top">
|
| 913 |
<div class="record-case">{esc(record.get("case"))}</div>
|
| 914 |
</div>
|
| 915 |
{record_visual(record, coordinates)}
|
| 916 |
<div class="record-body">
|
| 917 |
-
<div class="metric"><strong>{esc(metric_symbol(
|
| 918 |
<div class="expression">{esc(expression)}</div>
|
| 919 |
<div class="record-author">by <strong>{esc(visible_author)}</strong></div>
|
| 920 |
{record_details(record, visual_record, source, image_name, expression, analytical)}
|
|
@@ -944,6 +1056,40 @@ def setup_summary(setup: str) -> dict[str, Any]:
|
|
| 944 |
}
|
| 945 |
|
| 946 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 947 |
def family_preview(setup: str, coordinates: dict[str, dict[str, Any]]) -> str:
|
| 948 |
record = coordinates.get(f"{setup}@10")
|
| 949 |
if record is None:
|
|
@@ -965,7 +1111,7 @@ def family_preview(setup: str, coordinates: dict[str, dict[str, Any]]) -> str:
|
|
| 965 |
def family_card(summary: dict[str, Any], coordinates: dict[str, dict[str, Any]]) -> str:
|
| 966 |
setup = str(summary["setup"])
|
| 967 |
return f"""
|
| 968 |
-
<
|
| 969 |
{family_preview(setup, coordinates)}
|
| 970 |
<div class="family-card-top">
|
| 971 |
<div>
|
|
@@ -973,32 +1119,54 @@ def family_card(summary: dict[str, Any], coordinates: dict[str, dict[str, Any]])
|
|
| 973 |
<h3>{esc(summary["title"])}</h3>
|
| 974 |
</div>
|
| 975 |
</div>
|
| 976 |
-
</
|
| 977 |
"""
|
| 978 |
|
| 979 |
|
| 980 |
-
def
|
|
|
|
|
|
|
|
|
|
|
|
|
| 981 |
setup = str(summary["setup"])
|
| 982 |
records = summary["records"]
|
| 983 |
cards = "".join(record_card(record, coordinates) for record in records)
|
| 984 |
return f"""
|
| 985 |
-
<
|
|
|
|
|
|
|
|
|
|
| 986 |
<div class="family-title-card">
|
| 987 |
-
<div
|
| 988 |
-
|
| 989 |
-
|
|
|
|
|
|
|
| 990 |
</div>
|
| 991 |
<div class="record-grid">{cards}</div>
|
| 992 |
-
<
|
| 993 |
</section>
|
|
|
|
| 994 |
"""
|
| 995 |
|
| 996 |
|
| 997 |
-
def
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 998 |
summaries = [setup_summary(setup) for setup in setup_choices()]
|
| 999 |
coordinates = coordinate_records_by_case()
|
| 1000 |
family_cards = "".join(family_card(summary, coordinates) for summary in summaries)
|
| 1001 |
-
sections = "".join(family_section(summary, coordinates) for summary in summaries)
|
| 1002 |
return f"""
|
| 1003 |
<main class="packing-shell" id="top">
|
| 1004 |
<section class="hero-stage">
|
|
@@ -1006,39 +1174,114 @@ def browse_html() -> str:
|
|
| 1006 |
<div>
|
| 1007 |
<h1>Packing Benchmark</h1>
|
| 1008 |
<p class="hero-copy">
|
| 1009 |
-
Equal-copy packing records with canonical coordinate JSON
|
| 1010 |
-
|
| 1011 |
-
|
|
|
|
| 1012 |
</p>
|
| 1013 |
</div>
|
| 1014 |
</div>
|
| 1015 |
</section>
|
| 1016 |
-
|
| 1017 |
-
<div class="contain top-nav-inner">
|
| 1018 |
-
<a href="#top" aria-label="Packing Benchmark home">Packing Benchmark</a>
|
| 1019 |
-
<div class="nav-links">
|
| 1020 |
-
<a href="#families">Families</a>
|
| 1021 |
-
<a href="#records">Records</a>
|
| 1022 |
-
<a href="#submit-panel">Submit</a>
|
| 1023 |
-
<a href="#schema-panel">Schema</a>
|
| 1024 |
-
</div>
|
| 1025 |
-
</div>
|
| 1026 |
-
</nav>
|
| 1027 |
<section class="section" id="families">
|
| 1028 |
<div class="contain">
|
| 1029 |
<div class="section-head">
|
| 1030 |
<div>
|
| 1031 |
<h2>Choose a family</h2>
|
| 1032 |
<p class="section-note">
|
| 1033 |
-
|
| 1034 |
</p>
|
| 1035 |
</div>
|
| 1036 |
</div>
|
| 1037 |
<div class="family-grid">{family_cards}</div>
|
| 1038 |
</div>
|
| 1039 |
</section>
|
| 1040 |
-
|
| 1041 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1042 |
</section>
|
| 1043 |
</main>
|
| 1044 |
"""
|
|
@@ -1107,44 +1350,62 @@ def submit_solution(
|
|
| 1107 |
pretty = json.dumps(stored, indent=2, sort_keys=True)
|
| 1108 |
sync_status = record.get("sync_status", "dataset sync disabled")
|
| 1109 |
message = result_markdown(result) + f"\n\nSaved as record `{record['id']}`.\n\nDataset sync: `{sync_status}`."
|
| 1110 |
-
return message, preview_html(stored), pretty,
|
| 1111 |
except Exception as exc:
|
| 1112 |
-
return f"### Submission rejected\n\n- {exc}", "", json_text,
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1113 |
|
| 1114 |
|
| 1115 |
THEME = gr.themes.Base()
|
| 1116 |
|
| 1117 |
|
| 1118 |
with gr.Blocks(title="Packing Benchmark") as demo:
|
| 1119 |
-
|
| 1120 |
-
|
| 1121 |
-
|
| 1122 |
-
|
| 1123 |
-
|
| 1124 |
-
|
| 1125 |
-
|
| 1126 |
-
|
| 1127 |
-
|
| 1128 |
-
|
| 1129 |
-
|
| 1130 |
-
|
| 1131 |
-
|
| 1132 |
-
|
| 1133 |
-
|
| 1134 |
-
|
| 1135 |
-
|
| 1136 |
-
|
| 1137 |
-
|
| 1138 |
-
|
| 1139 |
-
|
| 1140 |
-
|
| 1141 |
-
|
| 1142 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1143 |
verify_btn.click(verify_only, inputs=[json_code, json_file, tolerance], outputs=[report, preview, normalized_json])
|
| 1144 |
submit_btn.click(
|
| 1145 |
submit_solution,
|
| 1146 |
inputs=[json_code, json_file, submitter, notes, source_url, tolerance],
|
| 1147 |
-
outputs=[report, preview, normalized_json, browse_page],
|
| 1148 |
)
|
| 1149 |
|
| 1150 |
|
|
|
|
| 44 |
--line-soft: #d4cab8;
|
| 45 |
--green: #b9e7bd;
|
| 46 |
--green-strong: #7fc98a;
|
| 47 |
+
--orange: #f4b36a;
|
| 48 |
+
--orange-soft: #ffe1ba;
|
| 49 |
--link: #174f8a;
|
| 50 |
--radius: 6px;
|
| 51 |
}
|
|
|
|
| 283 |
color: var(--ink);
|
| 284 |
}
|
| 285 |
|
| 286 |
+
.record-card.needs-recovery {
|
| 287 |
+
background: var(--orange-soft);
|
| 288 |
+
border-color: #a65b0b;
|
| 289 |
+
}
|
| 290 |
+
|
| 291 |
+
.record-card.needs-recovery .record-case::after {
|
| 292 |
+
content: "needs recovery";
|
| 293 |
+
display: inline-block;
|
| 294 |
+
margin-left: 7px;
|
| 295 |
+
padding: 1px 5px;
|
| 296 |
+
border: 1px solid #a65b0b;
|
| 297 |
+
border-radius: 3px;
|
| 298 |
+
color: #5c3100;
|
| 299 |
+
font-family: Helvetica, Arial, sans-serif !important;
|
| 300 |
+
font-size: 10px;
|
| 301 |
+
font-weight: 700;
|
| 302 |
+
text-transform: uppercase;
|
| 303 |
+
}
|
| 304 |
+
|
| 305 |
.record-top {
|
| 306 |
padding: 9px 10px 0;
|
| 307 |
}
|
|
|
|
| 453 |
font-size: 13px;
|
| 454 |
}
|
| 455 |
|
| 456 |
+
.page-card {
|
| 457 |
+
background: var(--paper);
|
| 458 |
+
border: 1px solid var(--line);
|
| 459 |
+
border-radius: var(--radius);
|
| 460 |
+
padding: 16px;
|
| 461 |
+
}
|
| 462 |
+
|
| 463 |
+
.leaderboard-table {
|
| 464 |
+
width: 100%;
|
| 465 |
+
border-collapse: collapse;
|
| 466 |
+
background: var(--paper);
|
| 467 |
+
border: 1px solid var(--line);
|
| 468 |
+
}
|
| 469 |
+
|
| 470 |
+
.leaderboard-table th,
|
| 471 |
+
.leaderboard-table td {
|
| 472 |
+
padding: 10px 12px;
|
| 473 |
+
border-bottom: 1px solid var(--line-soft);
|
| 474 |
+
color: var(--ink);
|
| 475 |
+
text-align: left;
|
| 476 |
+
vertical-align: top;
|
| 477 |
+
}
|
| 478 |
+
|
| 479 |
+
.leaderboard-table th {
|
| 480 |
+
background: var(--paper-soft);
|
| 481 |
+
font-size: 12px;
|
| 482 |
+
text-transform: uppercase;
|
| 483 |
+
}
|
| 484 |
+
|
| 485 |
.submit-shell,
|
| 486 |
.schema-shell {
|
| 487 |
width: min(1180px, calc(100% - 32px));
|
|
|
|
| 561 |
max-height: 315px;
|
| 562 |
}
|
| 563 |
|
| 564 |
+
.gradio-container .tab-nav {
|
| 565 |
+
background: var(--page) !important;
|
| 566 |
+
border-bottom: 1px solid var(--line) !important;
|
| 567 |
+
}
|
| 568 |
+
|
| 569 |
+
.gradio-container .tab-nav button {
|
| 570 |
+
background: transparent !important;
|
| 571 |
+
border: 0 !important;
|
| 572 |
+
}
|
| 573 |
+
|
| 574 |
@media (max-width: 720px) {
|
| 575 |
.contain,
|
| 576 |
.submit-shell,
|
|
|
|
| 674 |
"December",
|
| 675 |
)
|
| 676 |
|
| 677 |
+
RECOVERY_TOLERANCE_REL = 1.0e-3
|
| 678 |
+
|
| 679 |
|
| 680 |
def esc(value: Any) -> str:
|
| 681 |
return html.escape("" if value is None else str(value), quote=True)
|
|
|
|
| 771 |
return str(record.get("submitted_by") or "unknown").strip() or "unknown"
|
| 772 |
|
| 773 |
|
| 774 |
+
def display_author(record: dict[str, Any]) -> str:
|
| 775 |
+
if record.get("record_type") == "reference":
|
| 776 |
+
return reference_credit(record)
|
| 777 |
+
author = author_name(record)
|
| 778 |
+
notes = str(record.get("notes") or "").lower()
|
| 779 |
+
seeded = "seeded from" in notes or record.get("frontend_seed")
|
| 780 |
+
prior = reference_credit(record)
|
| 781 |
+
if author.lower().startswith("nathan roll") and seeded and prior not in {"unknown", "Erich Friedman Packing Center"}:
|
| 782 |
+
return prior
|
| 783 |
+
return author
|
| 784 |
+
|
| 785 |
+
|
| 786 |
def metric_symbol(record: dict[str, Any]) -> str:
|
| 787 |
return str(record.get("metric_symbol") or "s")
|
| 788 |
|
|
|
|
| 799 |
return str(value)
|
| 800 |
|
| 801 |
|
| 802 |
+
def numeric_metric(record: dict[str, Any]) -> float | None:
|
| 803 |
+
value = record.get("metric_value")
|
| 804 |
+
if value is None:
|
| 805 |
+
value = record.get("side")
|
| 806 |
+
try:
|
| 807 |
+
parsed = float(value)
|
| 808 |
+
except (TypeError, ValueError):
|
| 809 |
+
return None
|
| 810 |
+
return parsed if math.isfinite(parsed) else None
|
| 811 |
+
|
| 812 |
+
|
| 813 |
def compact_expression(record: dict[str, Any]) -> str:
|
| 814 |
expression = str(record.get("metric_expression") or "").strip()
|
| 815 |
expression = re.sub(r"\s+", " ", expression)
|
|
|
|
| 949 |
return "Coordinate rendering attached to this case."
|
| 950 |
|
| 951 |
|
| 952 |
+
def display_metric_record(record: dict[str, Any], visual_record: dict[str, Any]) -> dict[str, Any]:
|
| 953 |
+
if visual_record.get("verified") and visual_record.get("solution_path"):
|
| 954 |
+
return visual_record
|
| 955 |
+
return record
|
| 956 |
+
|
| 957 |
+
|
| 958 |
+
def needs_recovery(record: dict[str, Any], visual_record: dict[str, Any]) -> bool:
|
| 959 |
+
if record.get("record_type") != "reference":
|
| 960 |
+
return False
|
| 961 |
+
reference = numeric_metric(record)
|
| 962 |
+
visual = numeric_metric(visual_record)
|
| 963 |
+
if reference is None or visual is None:
|
| 964 |
+
return True
|
| 965 |
+
return visual > reference * (1.0 + RECOVERY_TOLERANCE_REL)
|
| 966 |
+
|
| 967 |
+
|
| 968 |
def record_details(record: dict[str, Any], visual_record: dict[str, Any], source: str, image_name: str, expression: str, analytical: Any) -> str:
|
| 969 |
rows: list[str] = []
|
| 970 |
rows.append(detail_row("Rendering", esc(visual_provenance(record, visual_record))))
|
| 971 |
+
if record.get("record_type") == "reference" and visual_record.get("verified"):
|
| 972 |
+
rows.append(
|
| 973 |
+
detail_row(
|
| 974 |
+
"Friedman reference",
|
| 975 |
+
f"{esc(metric_symbol(record))} = {esc(metric_value(record))} by {esc(reference_credit(record))}, {esc(reference_date(record))}",
|
| 976 |
+
)
|
| 977 |
+
)
|
| 978 |
if record.get("record_type") == "verified":
|
| 979 |
rows.append(detail_row("Submitted", esc(display_date(record))))
|
| 980 |
notes = str(record.get("notes") or "").strip()
|
|
|
|
| 1010 |
def record_card(record: dict[str, Any], coordinates: dict[str, dict[str, Any]]) -> str:
|
| 1011 |
verified = record.get("record_type") == "verified"
|
| 1012 |
visual_record = visual_record_for(record, coordinates)
|
| 1013 |
+
display_record = display_metric_record(record, visual_record)
|
| 1014 |
source = source_link(record)
|
| 1015 |
expression = compact_expression(record)
|
| 1016 |
image_name = source_image_name(record)
|
| 1017 |
analytical = friedman_reference(record).get("analytical_or_proved")
|
| 1018 |
+
visible_author = display_author(record)
|
| 1019 |
+
card_class = "verified" if verified else "reference"
|
| 1020 |
+
if needs_recovery(record, visual_record):
|
| 1021 |
+
card_class += " needs-recovery"
|
| 1022 |
return f"""
|
| 1023 |
+
<article class="record-card {card_class}">
|
| 1024 |
<div class="record-top">
|
| 1025 |
<div class="record-case">{esc(record.get("case"))}</div>
|
| 1026 |
</div>
|
| 1027 |
{record_visual(record, coordinates)}
|
| 1028 |
<div class="record-body">
|
| 1029 |
+
<div class="metric"><strong>{esc(metric_symbol(display_record))} = {esc(metric_value(display_record))}</strong><span>n={esc(record.get("n"))}</span></div>
|
| 1030 |
<div class="expression">{esc(expression)}</div>
|
| 1031 |
<div class="record-author">by <strong>{esc(visible_author)}</strong></div>
|
| 1032 |
{record_details(record, visual_record, source, image_name, expression, analytical)}
|
|
|
|
| 1056 |
}
|
| 1057 |
|
| 1058 |
|
| 1059 |
+
def shape_phrase(code: str, plural: bool = False) -> str:
|
| 1060 |
+
names = {
|
| 1061 |
+
"cir": ("circle", "circles"),
|
| 1062 |
+
"dom": ("domino", "dominoes"),
|
| 1063 |
+
"tri": ("triangle", "triangles"),
|
| 1064 |
+
"squ": ("square", "squares"),
|
| 1065 |
+
"pen": ("regular pentagon", "regular pentagons"),
|
| 1066 |
+
"hex": ("regular hexagon", "regular hexagons"),
|
| 1067 |
+
"oct": ("regular octagon", "regular octagons"),
|
| 1068 |
+
}
|
| 1069 |
+
singular, many = names.get(code, (code, code + "s"))
|
| 1070 |
+
return many if plural else singular
|
| 1071 |
+
|
| 1072 |
+
|
| 1073 |
+
def parse_setup_codes(setup: str) -> tuple[str, str]:
|
| 1074 |
+
if "in" not in setup:
|
| 1075 |
+
return setup, ""
|
| 1076 |
+
return tuple(setup.split("in", 1)) # type: ignore[return-value]
|
| 1077 |
+
|
| 1078 |
+
|
| 1079 |
+
def family_description(summary: dict[str, Any]) -> str:
|
| 1080 |
+
item_code, container_code = parse_setup_codes(str(summary["setup"]))
|
| 1081 |
+
item = shape_phrase(item_code, plural=True)
|
| 1082 |
+
container = shape_phrase(container_code)
|
| 1083 |
+
updated = f" Friedman lists this family as updated {esc(summary['updated'])}." if summary.get("updated") else ""
|
| 1084 |
+
return (
|
| 1085 |
+
f"This page tracks the smallest known container metric for packing n equal {esc(item)} "
|
| 1086 |
+
f"inside a {esc(container)}. The number shown on each card is the verified coordinate metric "
|
| 1087 |
+
f"for the rendering; the Friedman reference value and source information are in Details. "
|
| 1088 |
+
f"Orange cards need more recovery work because the verified coordinate JSON does not yet match "
|
| 1089 |
+
f"the listed reference within {RECOVERY_TOLERANCE_REL:.2%}.{updated}"
|
| 1090 |
+
)
|
| 1091 |
+
|
| 1092 |
+
|
| 1093 |
def family_preview(setup: str, coordinates: dict[str, dict[str, Any]]) -> str:
|
| 1094 |
record = coordinates.get(f"{setup}@10")
|
| 1095 |
if record is None:
|
|
|
|
| 1111 |
def family_card(summary: dict[str, Any], coordinates: dict[str, dict[str, Any]]) -> str:
|
| 1112 |
setup = str(summary["setup"])
|
| 1113 |
return f"""
|
| 1114 |
+
<div class="family-card">
|
| 1115 |
{family_preview(setup, coordinates)}
|
| 1116 |
<div class="family-card-top">
|
| 1117 |
<div>
|
|
|
|
| 1119 |
<h3>{esc(summary["title"])}</h3>
|
| 1120 |
</div>
|
| 1121 |
</div>
|
| 1122 |
+
</div>
|
| 1123 |
"""
|
| 1124 |
|
| 1125 |
|
| 1126 |
+
def family_page_html(setup: str | None = None) -> str:
|
| 1127 |
+
choices = setup_choices()
|
| 1128 |
+
setup = setup if setup in choices else (choices[0] if choices else "")
|
| 1129 |
+
summary = setup_summary(setup) if setup else {"records": [], "setup": "", "title": ""}
|
| 1130 |
+
coordinates = coordinate_records_by_case()
|
| 1131 |
setup = str(summary["setup"])
|
| 1132 |
records = summary["records"]
|
| 1133 |
cards = "".join(record_card(record, coordinates) for record in records)
|
| 1134 |
return f"""
|
| 1135 |
+
<main class="packing-shell">
|
| 1136 |
+
{top_nav()}
|
| 1137 |
+
<section class="section">
|
| 1138 |
+
<div class="contain">
|
| 1139 |
<div class="family-title-card">
|
| 1140 |
+
<div>
|
| 1141 |
+
<div class="family-code">{esc(setup)}</div>
|
| 1142 |
+
<h2>{esc(summary["title"])}</h2>
|
| 1143 |
+
<p class="family-subtitle">{family_description(summary)}</p>
|
| 1144 |
+
</div>
|
| 1145 |
</div>
|
| 1146 |
<div class="record-grid">{cards}</div>
|
| 1147 |
+
</div>
|
| 1148 |
</section>
|
| 1149 |
+
</main>
|
| 1150 |
"""
|
| 1151 |
|
| 1152 |
|
| 1153 |
+
def top_nav() -> str:
|
| 1154 |
+
return """
|
| 1155 |
+
<nav class="top-nav">
|
| 1156 |
+
<div class="contain top-nav-inner">
|
| 1157 |
+
<a href="#top" aria-label="Packing Benchmark home">Packing Benchmark</a>
|
| 1158 |
+
<div class="nav-links">
|
| 1159 |
+
<a href="https://erich-friedman.github.io/packing/" target="_blank" rel="noopener noreferrer">Friedman</a>
|
| 1160 |
+
</div>
|
| 1161 |
+
</div>
|
| 1162 |
+
</nav>
|
| 1163 |
+
"""
|
| 1164 |
+
|
| 1165 |
+
|
| 1166 |
+
def directory_html() -> str:
|
| 1167 |
summaries = [setup_summary(setup) for setup in setup_choices()]
|
| 1168 |
coordinates = coordinate_records_by_case()
|
| 1169 |
family_cards = "".join(family_card(summary, coordinates) for summary in summaries)
|
|
|
|
| 1170 |
return f"""
|
| 1171 |
<main class="packing-shell" id="top">
|
| 1172 |
<section class="hero-stage">
|
|
|
|
| 1174 |
<div>
|
| 1175 |
<h1>Packing Benchmark</h1>
|
| 1176 |
<p class="hero-copy">
|
| 1177 |
+
Equal-copy packing records with canonical coordinate JSON, initialized from the public reference
|
| 1178 |
+
families on <a href="https://erich-friedman.github.io/packing/" target="_blank" rel="noopener noreferrer">Erich Friedman's Packing Center</a>.
|
| 1179 |
+
Friedman rows keep their credited finder; rendered cards report our verified coordinate metric and
|
| 1180 |
+
mark unrecovered references in orange.
|
| 1181 |
</p>
|
| 1182 |
</div>
|
| 1183 |
</div>
|
| 1184 |
</section>
|
| 1185 |
+
{top_nav()}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1186 |
<section class="section" id="families">
|
| 1187 |
<div class="contain">
|
| 1188 |
<div class="section-head">
|
| 1189 |
<div>
|
| 1190 |
<h2>Choose a family</h2>
|
| 1191 |
<p class="section-note">
|
| 1192 |
+
Use the Family Records page to open a family. Each tile previews the n=10 rendering for that setup.
|
| 1193 |
</p>
|
| 1194 |
</div>
|
| 1195 |
</div>
|
| 1196 |
<div class="family-grid">{family_cards}</div>
|
| 1197 |
</div>
|
| 1198 |
</section>
|
| 1199 |
+
</main>
|
| 1200 |
+
"""
|
| 1201 |
+
|
| 1202 |
+
|
| 1203 |
+
def browse_html() -> str:
|
| 1204 |
+
return directory_html()
|
| 1205 |
+
|
| 1206 |
+
|
| 1207 |
+
def leaderboard_html() -> str:
|
| 1208 |
+
rows: dict[str, dict[str, Any]] = {}
|
| 1209 |
+
coordinates = coordinate_records_by_case()
|
| 1210 |
+
for record in STORE.public_records(show_all=False):
|
| 1211 |
+
author = display_author(record)
|
| 1212 |
+
entry = rows.setdefault(author, {"wins": 0, "verified": 0, "orange": 0, "examples": []})
|
| 1213 |
+
entry["wins"] += 1
|
| 1214 |
+
if record.get("record_type") == "verified":
|
| 1215 |
+
entry["verified"] += 1
|
| 1216 |
+
visual = visual_record_for(record, coordinates)
|
| 1217 |
+
if needs_recovery(record, visual):
|
| 1218 |
+
entry["orange"] += 1
|
| 1219 |
+
if len(entry["examples"]) < 5:
|
| 1220 |
+
entry["examples"].append(str(record.get("case")))
|
| 1221 |
+
|
| 1222 |
+
sorted_rows = sorted(rows.items(), key=lambda item: (-int(item[1]["wins"]), item[0].lower()))
|
| 1223 |
+
body = "\n".join(
|
| 1224 |
+
f"""
|
| 1225 |
+
<tr>
|
| 1226 |
+
<td>{rank}</td>
|
| 1227 |
+
<td><strong>{esc(author)}</strong></td>
|
| 1228 |
+
<td>{stats['wins']}</td>
|
| 1229 |
+
<td>{stats['verified']}</td>
|
| 1230 |
+
<td>{stats['orange']}</td>
|
| 1231 |
+
<td>{esc(', '.join(stats['examples']))}</td>
|
| 1232 |
+
</tr>
|
| 1233 |
+
"""
|
| 1234 |
+
for rank, (author, stats) in enumerate(sorted_rows, start=1)
|
| 1235 |
+
)
|
| 1236 |
+
return f"""
|
| 1237 |
+
<main class="packing-shell">
|
| 1238 |
+
{top_nav()}
|
| 1239 |
+
<section class="section">
|
| 1240 |
+
<div class="contain">
|
| 1241 |
+
<div class="section-head">
|
| 1242 |
+
<div>
|
| 1243 |
+
<h2>Leaderboard</h2>
|
| 1244 |
+
<p class="section-note">
|
| 1245 |
+
Counts current winning records by credited author. If an improved coordinate layout was seeded
|
| 1246 |
+
from an existing Friedman reference, the credit stays with the prior finder rather than Nathan.
|
| 1247 |
+
</p>
|
| 1248 |
+
</div>
|
| 1249 |
+
</div>
|
| 1250 |
+
<table class="leaderboard-table">
|
| 1251 |
+
<thead>
|
| 1252 |
+
<tr><th>#</th><th>Author</th><th>Wins</th><th>Verified submissions</th><th>Needs recovery</th><th>Examples</th></tr>
|
| 1253 |
+
</thead>
|
| 1254 |
+
<tbody>{body}</tbody>
|
| 1255 |
+
</table>
|
| 1256 |
+
</div>
|
| 1257 |
+
</section>
|
| 1258 |
+
</main>
|
| 1259 |
+
"""
|
| 1260 |
+
|
| 1261 |
+
|
| 1262 |
+
def submit_schema_intro_html() -> str:
|
| 1263 |
+
return f"""
|
| 1264 |
+
<main class="packing-shell">
|
| 1265 |
+
{top_nav()}
|
| 1266 |
+
<section class="section">
|
| 1267 |
+
<div class="contain">
|
| 1268 |
+
<div class="section-head">
|
| 1269 |
+
<div>
|
| 1270 |
+
<h2>Submit JSON</h2>
|
| 1271 |
+
<p class="section-note">
|
| 1272 |
+
Paste canonical coordinates, verify them locally in the browser session, then submit the verified
|
| 1273 |
+
record. The schema is intentionally small: regular polygons, circles, and rectangles only.
|
| 1274 |
+
</p>
|
| 1275 |
+
</div>
|
| 1276 |
+
</div>
|
| 1277 |
+
<div class="page-card">
|
| 1278 |
+
<p><strong>Coordinate convention:</strong> origin at the container center; item placements are item
|
| 1279 |
+
centers; rotations are radians counterclockwise. Touching is legal. Overlap and protrusion are rejected.</p>
|
| 1280 |
+
<p><strong>Supported shapes:</strong> <code>regular_polygon</code>, <code>circle</code>, and
|
| 1281 |
+
<code>rectangle</code>. Include <code>case</code>, <code>item</code>, <code>container</code>, and
|
| 1282 |
+
<code>placements</code>.</p>
|
| 1283 |
+
</div>
|
| 1284 |
+
</div>
|
| 1285 |
</section>
|
| 1286 |
</main>
|
| 1287 |
"""
|
|
|
|
| 1350 |
pretty = json.dumps(stored, indent=2, sort_keys=True)
|
| 1351 |
sync_status = record.get("sync_status", "dataset sync disabled")
|
| 1352 |
message = result_markdown(result) + f"\n\nSaved as record `{record['id']}`.\n\nDataset sync: `{sync_status}`."
|
| 1353 |
+
return message, preview_html(stored), pretty, directory_html(), leaderboard_html()
|
| 1354 |
except Exception as exc:
|
| 1355 |
+
return f"### Submission rejected\n\n- {exc}", "", json_text, directory_html(), leaderboard_html()
|
| 1356 |
+
|
| 1357 |
+
|
| 1358 |
+
def update_family_page(setup: str):
|
| 1359 |
+
return family_page_html(setup)
|
| 1360 |
|
| 1361 |
|
| 1362 |
THEME = gr.themes.Base()
|
| 1363 |
|
| 1364 |
|
| 1365 |
with gr.Blocks(title="Packing Benchmark") as demo:
|
| 1366 |
+
with gr.Tabs():
|
| 1367 |
+
with gr.Tab("Families"):
|
| 1368 |
+
browse_page = gr.HTML(directory_html())
|
| 1369 |
+
|
| 1370 |
+
with gr.Tab("Family Records"):
|
| 1371 |
+
choices = setup_choices()
|
| 1372 |
+
initial_setup = choices[0] if choices else ""
|
| 1373 |
+
family_select = gr.Dropdown(
|
| 1374 |
+
choices=choices,
|
| 1375 |
+
value=initial_setup,
|
| 1376 |
+
label="Family",
|
| 1377 |
+
elem_id="family-selector",
|
| 1378 |
+
)
|
| 1379 |
+
family_page = gr.HTML(family_page_html(initial_setup))
|
| 1380 |
+
|
| 1381 |
+
with gr.Tab("Leaderboard"):
|
| 1382 |
+
leaderboard_page = gr.HTML(leaderboard_html())
|
| 1383 |
+
|
| 1384 |
+
with gr.Tab("Submit JSON"):
|
| 1385 |
+
gr.HTML(submit_schema_intro_html())
|
| 1386 |
+
with gr.Group(elem_classes=["submit-shell"]):
|
| 1387 |
+
with gr.Row():
|
| 1388 |
+
with gr.Column(scale=7):
|
| 1389 |
+
json_code = gr.Code(value=SAMPLE_JSON, language="json", label="Canonical coordinate JSON", lines=18)
|
| 1390 |
+
json_file = gr.File(label="or upload .json", file_types=[".json"], type="filepath")
|
| 1391 |
+
with gr.Column(scale=5):
|
| 1392 |
+
submitter = gr.Textbox(label="Submitted by", placeholder="Name, handle, or lab")
|
| 1393 |
+
source_url = gr.Textbox(label="Source URL", placeholder="Optional paper, repo, or run link")
|
| 1394 |
+
notes = gr.Textbox(label="Notes", lines=3, placeholder="Optional method or provenance note")
|
| 1395 |
+
tolerance = gr.Number(value=DEFAULT_TOLERANCE, label="Verification tolerance")
|
| 1396 |
+
with gr.Row():
|
| 1397 |
+
verify_btn = gr.Button("Verify", variant="secondary")
|
| 1398 |
+
submit_btn = gr.Button("Submit Verified Record", variant="primary")
|
| 1399 |
+
report = gr.Markdown()
|
| 1400 |
+
preview = gr.HTML(label="Browser render")
|
| 1401 |
+
normalized_json = gr.Code(label="Normalized JSON", language="json", lines=10)
|
| 1402 |
+
|
| 1403 |
+
family_select.change(update_family_page, inputs=[family_select], outputs=[family_page])
|
| 1404 |
verify_btn.click(verify_only, inputs=[json_code, json_file, tolerance], outputs=[report, preview, normalized_json])
|
| 1405 |
submit_btn.click(
|
| 1406 |
submit_solution,
|
| 1407 |
inputs=[json_code, json_file, submitter, notes, source_url, tolerance],
|
| 1408 |
+
outputs=[report, preview, normalized_json, browse_page, leaderboard_page],
|
| 1409 |
)
|
| 1410 |
|
| 1411 |
|