NathanRoll commited on
Commit
3b06380
·
verified ·
1 Parent(s): 9b9ffd9

Harden verifier and record metric validation

Browse files
app.py CHANGED
@@ -2815,14 +2815,18 @@ def current_top_for_case(case: str) -> dict[str, Any] | None:
2815
 
2816
 
2817
  def submitted_metric_for_result(result: dict[str, Any], current: dict[str, Any] | None) -> tuple[str, float | None]:
 
2818
  try:
2819
- side = float(result.get("side"))
2820
  except (TypeError, ValueError):
2821
- return "s", None
2822
- if not math.isfinite(side):
2823
- return "s", None
2824
- symbol = str((current or {}).get("metric_symbol") or "s")
2825
- return symbol, side * 0.5 if symbol == "r" else side
 
 
 
2826
 
2827
 
2828
  def submission_gate(result: dict[str, Any]) -> tuple[bool, str]:
@@ -2834,6 +2838,9 @@ def submission_gate(result: dict[str, Any]) -> tuple[bool, str]:
2834
  return False, "The evaluator could not compute a submitted metric."
2835
  if current is None:
2836
  return True, "No current top record exists for this case; a valid geometry can be submitted."
 
 
 
2837
 
2838
  current_metric = record_metric_float(current)
2839
  if current_metric is None:
@@ -2854,11 +2861,18 @@ def submission_gate(result: dict[str, Any]) -> tuple[bool, str]:
2854
 
2855
  def result_markdown(result: dict[str, Any], gate: tuple[bool, str] | None = None) -> str:
2856
  if result["ok"]:
 
 
 
 
 
 
2857
  text = (
2858
  "### Verification Passed\n\n"
2859
  f"- Case: `{result['case']}`\n"
2860
  f"- Items: `{result['n']}`\n"
2861
- f"- Container side/metric: `{result['side']:.10f}`\n"
 
2862
  f"- Density: `{result['density']:.6f}`\n"
2863
  f"- Max boundary excess: `{result['max_boundary_excess']:.3e}`\n"
2864
  f"- Max pair overlap depth: `{result['max_pair_overlap_depth']:.3e}`\n"
 
2815
 
2816
 
2817
  def submitted_metric_for_result(result: dict[str, Any], current: dict[str, Any] | None) -> tuple[str, float | None]:
2818
+ symbol = str(result.get("metric_symbol") or (current or {}).get("metric_symbol") or "s")
2819
  try:
2820
+ metric = float(result.get("metric_value"))
2821
  except (TypeError, ValueError):
2822
+ try:
2823
+ side = float(result.get("side"))
2824
+ except (TypeError, ValueError):
2825
+ return symbol, None
2826
+ metric = side * 0.5 if symbol == "r" else side
2827
+ if not math.isfinite(metric):
2828
+ return symbol, None
2829
+ return symbol, metric
2830
 
2831
 
2832
  def submission_gate(result: dict[str, Any]) -> tuple[bool, str]:
 
2838
  return False, "The evaluator could not compute a submitted metric."
2839
  if current is None:
2840
  return True, "No current top record exists for this case; a valid geometry can be submitted."
2841
+ current_symbol = str(current.get("metric_symbol") or symbol)
2842
+ if current_symbol != symbol:
2843
+ return False, f"Submitted metric symbol {symbol!r} does not match current record metric symbol {current_symbol!r}."
2844
 
2845
  current_metric = record_metric_float(current)
2846
  if current_metric is None:
 
2861
 
2862
  def result_markdown(result: dict[str, Any], gate: tuple[bool, str] | None = None) -> str:
2863
  if result["ok"]:
2864
+ metric_symbol_text = result.get("metric_symbol") or "s"
2865
+ metric_value_text = result.get("metric_value")
2866
+ try:
2867
+ metric_line = f"- Container metric: `{metric_symbol_text} = {float(metric_value_text):.10f}`\n"
2868
+ except (TypeError, ValueError):
2869
+ metric_line = ""
2870
  text = (
2871
  "### Verification Passed\n\n"
2872
  f"- Case: `{result['case']}`\n"
2873
  f"- Items: `{result['n']}`\n"
2874
+ f"{metric_line}"
2875
+ f"- Container side/diameter: `{result['side']:.10f}`\n"
2876
  f"- Density: `{result['density']:.6f}`\n"
2877
  f"- Max boundary excess: `{result['max_boundary_excess']:.3e}`\n"
2878
  f"- Max pair overlap depth: `{result['max_pair_overlap_depth']:.3e}`\n"
packing_benchmark/store.py CHANGED
@@ -344,8 +344,8 @@ class SolutionStore:
344
  if reference_side is None:
345
  reference_side = comparison.get("reference_side")
346
  side = float(result.side or 0.0)
347
- metric_symbol = self.submission_metric_symbol(result.case)
348
- metric_value = self.metric_value_for_side(side, metric_symbol)
349
  previous = self.current_best_for_case(result.case, metric_symbol)
350
  previous_bests = [self.previous_best_entry(previous)] if previous is not None else []
351
  if reference_side is None and previous is not None:
 
344
  if reference_side is None:
345
  reference_side = comparison.get("reference_side")
346
  side = float(result.side or 0.0)
347
+ metric_symbol = result.metric_symbol or self.submission_metric_symbol(result.case)
348
+ metric_value = float(result.metric_value) if result.metric_value is not None else self.metric_value_for_side(side, metric_symbol)
349
  previous = self.current_best_for_case(result.case, metric_symbol)
350
  previous_bests = [self.previous_best_entry(previous)] if previous is not None else []
351
  if reference_side is None and previous is not None:
packing_benchmark/verifier.py CHANGED
@@ -4,12 +4,14 @@ import copy
4
  import hashlib
5
  import json
6
  import math
 
7
  from dataclasses import dataclass, field
8
  from typing import Any
9
 
10
 
11
  PI = math.pi
12
  DEFAULT_TOLERANCE = 1.0e-8
 
13
 
14
 
15
  @dataclass
@@ -19,6 +21,8 @@ class VerificationResult:
19
  setup: str
20
  n: int
21
  side: float | None
 
 
22
  density: float | None
23
  max_boundary_excess: float
24
  max_pair_overlap_depth: float
@@ -33,6 +37,8 @@ class VerificationResult:
33
  "setup": self.setup,
34
  "n": self.n,
35
  "side": self.side,
 
 
36
  "density": self.density,
37
  "max_boundary_excess": self.max_boundary_excess,
38
  "max_pair_overlap_depth": self.max_pair_overlap_depth,
@@ -88,67 +94,107 @@ def regular_area(sides: int, radius: float) -> float:
88
  return 0.5 * sides * radius * radius * math.sin(2.0 * PI / sides)
89
 
90
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
91
  def shape_type(spec: dict[str, Any]) -> str:
92
  return str(spec.get("type", "")).strip().lower()
93
 
94
 
95
- def regular_radius(spec: dict[str, Any]) -> float:
96
- sides = int(spec["sides"])
 
 
97
  if sides < 3:
98
  raise ValueError("regular polygons need at least 3 sides")
 
 
 
 
 
99
  if "circumradius" in spec:
100
- radius = float(spec["circumradius"])
101
  elif "side_length" in spec:
102
- radius = float(spec["side_length"]) / (2.0 * math.sin(PI / sides))
 
103
  else:
104
  raise ValueError("regular_polygon requires side_length or circumradius")
105
- if radius <= 0.0 or not math.isfinite(radius):
106
  raise ValueError("regular_polygon radius must be positive")
107
  return radius
108
 
109
 
110
  def regular_side_length(spec: dict[str, Any]) -> float:
111
- sides = int(spec["sides"])
112
  if "side_length" in spec:
113
- side = float(spec["side_length"])
114
  else:
115
  side = 2.0 * regular_radius(spec) * math.sin(PI / sides)
116
- if side <= 0.0 or not math.isfinite(side):
117
  raise ValueError("regular_polygon side_length must be positive")
118
  return side
119
 
120
 
121
  def circle_radius(spec: dict[str, Any]) -> float:
122
  if "radius" in spec:
123
- radius = float(spec["radius"])
124
  elif "diameter" in spec:
125
- radius = 0.5 * float(spec["diameter"])
126
  else:
127
  raise ValueError("circle requires radius or diameter")
128
- if radius <= 0.0 or not math.isfinite(radius):
129
  raise ValueError("circle radius must be positive")
130
  return radius
131
 
132
 
133
  def rectangle_dims(spec: dict[str, Any]) -> tuple[float, float]:
134
- width = float(spec["width"])
135
- height = float(spec["height"])
136
- if width <= 0.0 or height <= 0.0 or not math.isfinite(width + height):
137
  raise ValueError("rectangle width and height must be positive")
138
  return width, height
139
 
140
 
 
 
 
 
141
  def make_item_shape(item: dict[str, Any], placement: dict[str, Any]) -> dict[str, Any]:
142
  kind = shape_type(item)
143
- x = float(placement.get("x", 0.0))
144
- y = float(placement.get("y", 0.0))
145
- rotation = float(placement.get("rotation_radians", 0.0))
146
- if not math.isfinite(x + y + rotation):
147
- raise ValueError("placement coordinates and rotation must be finite")
148
 
149
  if kind == "regular_polygon":
150
  radius = regular_radius(item)
151
- sides = int(item["sides"])
152
  return {
153
  "kind": "polygon",
154
  "vertices": regular_vertices(sides, radius, rotation=rotation, center=(x, y)),
@@ -174,10 +220,10 @@ def make_item_shape(item: dict[str, Any], placement: dict[str, Any]) -> dict[str
174
 
175
  def make_container_shape(container: dict[str, Any]) -> dict[str, Any]:
176
  kind = shape_type(container)
177
- rotation = float(container.get("orientation_radians", 0.0))
178
  if kind == "regular_polygon":
179
  radius = regular_radius(container)
180
- sides = int(container["sides"])
181
  return {
182
  "kind": "polygon",
183
  "vertices": regular_vertices(sides, radius, rotation=rotation),
@@ -190,6 +236,8 @@ def make_container_shape(container: dict[str, Any]) -> dict[str, Any]:
190
  "kind": "polygon",
191
  "vertices": rectangle_vertices(width, height, rotation=rotation),
192
  "side": max(width, height),
 
 
193
  "area": width * height,
194
  }
195
  if kind == "circle":
@@ -217,16 +265,26 @@ def solution_setup(solution: dict[str, Any]) -> str:
217
  def shape_label(spec: dict[str, Any]) -> str:
218
  kind = shape_type(spec)
219
  if kind == "regular_polygon":
220
- sides = int(spec.get("sides", 0))
221
  names = {3: "tri", 4: "squ", 5: "pen", 6: "hex", 7: "hep", 8: "oct"}
222
  return names.get(sides, f"{sides}gon")
223
  if kind == "circle":
224
  return "cir"
225
  if kind == "rectangle":
 
 
 
 
 
 
226
  return "rect"
227
  return kind or "shape"
228
 
229
 
 
 
 
 
230
  def solution_case(solution: dict[str, Any]) -> str:
231
  placements = solution.get("placements", [])
232
  n = len(placements) if isinstance(placements, list) else 0
@@ -242,6 +300,51 @@ def parsed_case_count(case: str) -> int | None:
242
  return None
243
 
244
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
245
  def polygon_axes(poly: list[tuple[float, float]]) -> list[tuple[float, float]]:
246
  axes: list[tuple[float, float]] = []
247
  for i, (x1, y1) in enumerate(poly):
@@ -368,13 +471,19 @@ def normalize_solution(solution: dict[str, Any]) -> dict[str, Any]:
368
  def verify_solution(solution: dict[str, Any], tolerance: float = DEFAULT_TOLERANCE) -> VerificationResult:
369
  errors: list[str] = []
370
  warnings: list[str] = []
371
- case = solution_case(solution)
372
- setup = solution_setup(solution)
373
  side: float | None = None
 
 
374
  density: float | None = None
375
  n = 0
376
 
377
  try:
 
 
 
 
378
  placements = solution.get("placements")
379
  if not isinstance(placements, list):
380
  raise ValueError("placements must be a list")
@@ -390,11 +499,31 @@ def verify_solution(solution: dict[str, Any], tolerance: float = DEFAULT_TOLERAN
390
  if not isinstance(item, dict) or not isinstance(container_spec, dict):
391
  raise ValueError("item and container must be objects")
392
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
393
  container = make_container_shape(container_spec)
 
394
  side = container["side"]
 
395
  shapes = [make_item_shape(item, placement) for placement in placements]
 
 
396
  item_area = sum(shape["area"] for shape in shapes)
397
  density = item_area / container["area"] if container["area"] > 0.0 else None
 
 
398
 
399
  max_boundary = max(boundary_excess(shape, container) for shape in shapes)
400
  max_overlap = -float("inf")
@@ -415,6 +544,8 @@ def verify_solution(solution: dict[str, Any], tolerance: float = DEFAULT_TOLERAN
415
  setup=setup,
416
  n=n,
417
  side=side,
 
 
418
  density=density,
419
  max_boundary_excess=max_boundary,
420
  max_pair_overlap_depth=max_overlap,
@@ -430,6 +561,8 @@ def verify_solution(solution: dict[str, Any], tolerance: float = DEFAULT_TOLERAN
430
  setup=setup,
431
  n=n,
432
  side=side,
 
 
433
  density=density,
434
  max_boundary_excess=float("nan"),
435
  max_pair_overlap_depth=float("nan"),
@@ -439,8 +572,55 @@ def verify_solution(solution: dict[str, Any], tolerance: float = DEFAULT_TOLERAN
439
  )
440
 
441
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
442
  def load_solution_json(text: str) -> dict[str, Any]:
443
- payload = json.loads(text)
 
 
 
444
  if not isinstance(payload, dict):
445
  raise ValueError("submission JSON must be an object")
446
  return payload
 
4
  import hashlib
5
  import json
6
  import math
7
+ import re
8
  from dataclasses import dataclass, field
9
  from typing import Any
10
 
11
 
12
  PI = math.pi
13
  DEFAULT_TOLERANCE = 1.0e-8
14
+ CASE_PATTERN = re.compile(r"^[a-z0-9]+in[a-z0-9]+@[1-9][0-9]*$")
15
 
16
 
17
  @dataclass
 
21
  setup: str
22
  n: int
23
  side: float | None
24
+ metric_symbol: str | None
25
+ metric_value: float | None
26
  density: float | None
27
  max_boundary_excess: float
28
  max_pair_overlap_depth: float
 
37
  "setup": self.setup,
38
  "n": self.n,
39
  "side": self.side,
40
+ "metric_symbol": self.metric_symbol,
41
+ "metric_value": self.metric_value,
42
  "density": self.density,
43
  "max_boundary_excess": self.max_boundary_excess,
44
  "max_pair_overlap_depth": self.max_pair_overlap_depth,
 
94
  return 0.5 * sides * radius * radius * math.sin(2.0 * PI / sides)
95
 
96
 
97
+ def finite_float(value: Any, label: str) -> float:
98
+ try:
99
+ out = float(value)
100
+ except (TypeError, ValueError):
101
+ raise ValueError(f"{label} must be numeric") from None
102
+ if not math.isfinite(out):
103
+ raise ValueError(f"{label} must be finite")
104
+ return out
105
+
106
+
107
+ def integer_value(value: Any, label: str) -> int:
108
+ if isinstance(value, bool):
109
+ raise ValueError(f"{label} must be an integer")
110
+ if isinstance(value, int):
111
+ out = value
112
+ elif isinstance(value, float):
113
+ if not math.isfinite(value) or not value.is_integer():
114
+ raise ValueError(f"{label} must be an integer")
115
+ out = int(value)
116
+ elif isinstance(value, str):
117
+ if not re.fullmatch(r"[+-]?\d+", value.strip()):
118
+ raise ValueError(f"{label} must be an integer")
119
+ out = int(value)
120
+ else:
121
+ raise ValueError(f"{label} must be an integer")
122
+ return out
123
+
124
+
125
  def shape_type(spec: dict[str, Any]) -> str:
126
  return str(spec.get("type", "")).strip().lower()
127
 
128
 
129
+ def regular_sides(spec: dict[str, Any]) -> int:
130
+ if "sides" not in spec:
131
+ raise ValueError("regular_polygon requires sides")
132
+ sides = integer_value(spec["sides"], "regular_polygon sides")
133
  if sides < 3:
134
  raise ValueError("regular polygons need at least 3 sides")
135
+ return sides
136
+
137
+
138
+ def regular_radius(spec: dict[str, Any]) -> float:
139
+ sides = regular_sides(spec)
140
  if "circumradius" in spec:
141
+ radius = finite_float(spec["circumradius"], "regular_polygon circumradius")
142
  elif "side_length" in spec:
143
+ side = finite_float(spec["side_length"], "regular_polygon side_length")
144
+ radius = side / (2.0 * math.sin(PI / sides))
145
  else:
146
  raise ValueError("regular_polygon requires side_length or circumradius")
147
+ if radius <= 0.0:
148
  raise ValueError("regular_polygon radius must be positive")
149
  return radius
150
 
151
 
152
  def regular_side_length(spec: dict[str, Any]) -> float:
153
+ sides = regular_sides(spec)
154
  if "side_length" in spec:
155
+ side = finite_float(spec["side_length"], "regular_polygon side_length")
156
  else:
157
  side = 2.0 * regular_radius(spec) * math.sin(PI / sides)
158
+ if side <= 0.0:
159
  raise ValueError("regular_polygon side_length must be positive")
160
  return side
161
 
162
 
163
  def circle_radius(spec: dict[str, Any]) -> float:
164
  if "radius" in spec:
165
+ radius = finite_float(spec["radius"], "circle radius")
166
  elif "diameter" in spec:
167
+ radius = 0.5 * finite_float(spec["diameter"], "circle diameter")
168
  else:
169
  raise ValueError("circle requires radius or diameter")
170
+ if radius <= 0.0:
171
  raise ValueError("circle radius must be positive")
172
  return radius
173
 
174
 
175
  def rectangle_dims(spec: dict[str, Any]) -> tuple[float, float]:
176
+ width = finite_float(spec["width"], "rectangle width")
177
+ height = finite_float(spec["height"], "rectangle height")
178
+ if width <= 0.0 or height <= 0.0:
179
  raise ValueError("rectangle width and height must be positive")
180
  return width, height
181
 
182
 
183
+ def optional_rotation(spec: dict[str, Any], field: str = "orientation_radians") -> float:
184
+ return finite_float(spec.get(field, 0.0), field)
185
+
186
+
187
  def make_item_shape(item: dict[str, Any], placement: dict[str, Any]) -> dict[str, Any]:
188
  kind = shape_type(item)
189
+ if not isinstance(placement, dict):
190
+ raise ValueError("each placement must be an object")
191
+ x = finite_float(placement.get("x", 0.0), "placement x")
192
+ y = finite_float(placement.get("y", 0.0), "placement y")
193
+ rotation = finite_float(placement.get("rotation_radians", 0.0), "placement rotation_radians")
194
 
195
  if kind == "regular_polygon":
196
  radius = regular_radius(item)
197
+ sides = regular_sides(item)
198
  return {
199
  "kind": "polygon",
200
  "vertices": regular_vertices(sides, radius, rotation=rotation, center=(x, y)),
 
220
 
221
  def make_container_shape(container: dict[str, Any]) -> dict[str, Any]:
222
  kind = shape_type(container)
223
+ rotation = optional_rotation(container)
224
  if kind == "regular_polygon":
225
  radius = regular_radius(container)
226
+ sides = regular_sides(container)
227
  return {
228
  "kind": "polygon",
229
  "vertices": regular_vertices(sides, radius, rotation=rotation),
 
236
  "kind": "polygon",
237
  "vertices": rectangle_vertices(width, height, rotation=rotation),
238
  "side": max(width, height),
239
+ "width": width,
240
+ "height": height,
241
  "area": width * height,
242
  }
243
  if kind == "circle":
 
265
  def shape_label(spec: dict[str, Any]) -> str:
266
  kind = shape_type(spec)
267
  if kind == "regular_polygon":
268
+ sides = regular_sides(spec)
269
  names = {3: "tri", 4: "squ", 5: "pen", 6: "hex", 7: "hep", 8: "oct"}
270
  return names.get(sides, f"{sides}gon")
271
  if kind == "circle":
272
  return "cir"
273
  if kind == "rectangle":
274
+ width, height = rectangle_dims(spec)
275
+ ratio = max(width, height) / min(width, height)
276
+ if abs(ratio - 1.0) <= 1.0e-9:
277
+ return "squ"
278
+ if abs(ratio - 2.0) <= 1.0e-9:
279
+ return "dom"
280
  return "rect"
281
  return kind or "shape"
282
 
283
 
284
+ def inferred_setup(item: dict[str, Any], container: dict[str, Any]) -> str:
285
+ return f"{shape_label(item)}in{shape_label(container)}"
286
+
287
+
288
  def solution_case(solution: dict[str, Any]) -> str:
289
  placements = solution.get("placements", [])
290
  n = len(placements) if isinstance(placements, list) else 0
 
300
  return None
301
 
302
 
303
+ def parsed_case_setup(case: str) -> str | None:
304
+ if "@" not in case:
305
+ return None
306
+ setup, count_text = case.rsplit("@", 1)
307
+ if not setup or not count_text:
308
+ return None
309
+ try:
310
+ count = int(count_text)
311
+ except ValueError:
312
+ return None
313
+ if count <= 0:
314
+ return None
315
+ return setup
316
+
317
+
318
+ def container_metric(container_shape: dict[str, Any]) -> tuple[str, float]:
319
+ if container_shape["kind"] == "circle":
320
+ return "r", float(container_shape["radius"])
321
+ width = container_shape.get("width")
322
+ height = container_shape.get("height")
323
+ if width is not None and height is not None:
324
+ short = min(float(width), float(height))
325
+ long = max(float(width), float(height))
326
+ if abs(long / short - 2.0) <= 1.0e-9:
327
+ return "s", short
328
+ return "s", float(container_shape["side"])
329
+
330
+
331
+ def finite_points(points: list[tuple[float, float]], label: str) -> None:
332
+ for x, y in points:
333
+ if not math.isfinite(x + y):
334
+ raise ValueError(f"{label} contains non-finite coordinates")
335
+
336
+
337
+ def validate_shape_finite(shape: dict[str, Any], label: str) -> None:
338
+ if shape["kind"] == "circle":
339
+ cx, cy = shape["center"]
340
+ if not math.isfinite(cx + cy + shape["radius"] + shape["area"]):
341
+ raise ValueError(f"{label} contains non-finite circle geometry")
342
+ return
343
+ finite_points(shape["vertices"], label)
344
+ if not math.isfinite(shape["area"]):
345
+ raise ValueError(f"{label} contains non-finite polygon area")
346
+
347
+
348
  def polygon_axes(poly: list[tuple[float, float]]) -> list[tuple[float, float]]:
349
  axes: list[tuple[float, float]] = []
350
  for i, (x1, y1) in enumerate(poly):
 
471
  def verify_solution(solution: dict[str, Any], tolerance: float = DEFAULT_TOLERANCE) -> VerificationResult:
472
  errors: list[str] = []
473
  warnings: list[str] = []
474
+ case = str(solution.get("case") or "")
475
+ setup = str(solution.get("setup") or "")
476
  side: float | None = None
477
+ metric_symbol: str | None = None
478
+ metric_value: float | None = None
479
  density: float | None = None
480
  n = 0
481
 
482
  try:
483
+ tolerance = finite_float(tolerance, "tolerance")
484
+ if tolerance < 0.0:
485
+ raise ValueError("tolerance must be non-negative")
486
+
487
  placements = solution.get("placements")
488
  if not isinstance(placements, list):
489
  raise ValueError("placements must be a list")
 
499
  if not isinstance(item, dict) or not isinstance(container_spec, dict):
500
  raise ValueError("item and container must be objects")
501
 
502
+ actual_setup = inferred_setup(item, container_spec)
503
+ if not setup:
504
+ setup = parsed_case_setup(case) or actual_setup
505
+ if not case:
506
+ case = f"{actual_setup}@{n}"
507
+ if not CASE_PATTERN.fullmatch(case):
508
+ errors.append("case must have form '<item>in<container>@<positive integer>'")
509
+ case_setup = parsed_case_setup(case)
510
+ if case_setup is not None and case_setup != actual_setup:
511
+ errors.append(f"case setup {case_setup!r} does not match item/container geometry {actual_setup!r}")
512
+ if setup != actual_setup:
513
+ errors.append(f"setup {setup!r} does not match item/container geometry {actual_setup!r}")
514
+ setup = actual_setup
515
+
516
  container = make_container_shape(container_spec)
517
+ validate_shape_finite(container, "container")
518
  side = container["side"]
519
+ metric_symbol, metric_value = container_metric(container)
520
  shapes = [make_item_shape(item, placement) for placement in placements]
521
+ for index, shape in enumerate(shapes, start=1):
522
+ validate_shape_finite(shape, f"placement {index}")
523
  item_area = sum(shape["area"] for shape in shapes)
524
  density = item_area / container["area"] if container["area"] > 0.0 else None
525
+ if density is None or not math.isfinite(density):
526
+ errors.append("density is not finite")
527
 
528
  max_boundary = max(boundary_excess(shape, container) for shape in shapes)
529
  max_overlap = -float("inf")
 
544
  setup=setup,
545
  n=n,
546
  side=side,
547
+ metric_symbol=metric_symbol,
548
+ metric_value=metric_value,
549
  density=density,
550
  max_boundary_excess=max_boundary,
551
  max_pair_overlap_depth=max_overlap,
 
561
  setup=setup,
562
  n=n,
563
  side=side,
564
+ metric_symbol=metric_symbol,
565
+ metric_value=metric_value,
566
  density=density,
567
  max_boundary_excess=float("nan"),
568
  max_pair_overlap_depth=float("nan"),
 
572
  )
573
 
574
 
575
+ def close_enough(a: Any, b: Any, tolerance: float) -> bool:
576
+ try:
577
+ left = float(a)
578
+ right = float(b)
579
+ except (TypeError, ValueError):
580
+ return False
581
+ if not math.isfinite(left + right):
582
+ return False
583
+ return abs(left - right) <= max(tolerance, abs(right) * tolerance)
584
+
585
+
586
+ def verify_record_solution(
587
+ record: dict[str, Any],
588
+ solution: dict[str, Any],
589
+ tolerance: float = DEFAULT_TOLERANCE,
590
+ ) -> list[str]:
591
+ """Verify that a stored leaderboard row agrees with its coordinate JSON."""
592
+
593
+ result = verify_solution(solution, tolerance=tolerance)
594
+ errors = list(result.errors)
595
+ if str(record.get("case") or "") != result.case:
596
+ errors.append(f"record case {record.get('case')!r} does not match verified case {result.case!r}")
597
+ if str(record.get("setup") or "") != result.setup:
598
+ errors.append(f"record setup {record.get('setup')!r} does not match verified setup {result.setup!r}")
599
+ try:
600
+ record_n = int(record.get("n"))
601
+ except (TypeError, ValueError):
602
+ record_n = -1
603
+ if record_n != result.n:
604
+ errors.append(f"record n {record.get('n')!r} does not match verified n {result.n}")
605
+
606
+ if record.get("side") is not None and not close_enough(record.get("side"), result.side, tolerance):
607
+ errors.append(f"record side {record.get('side')!r} does not match verified side {result.side!r}")
608
+
609
+ record_symbol = record.get("metric_symbol")
610
+ if record_symbol is not None and str(record_symbol) != str(result.metric_symbol):
611
+ errors.append(f"record metric_symbol {record_symbol!r} does not match verified metric_symbol {result.metric_symbol!r}")
612
+
613
+ record_metric = record.get("metric_value")
614
+ if record_metric is not None and not close_enough(record_metric, result.metric_value, tolerance):
615
+ errors.append(f"record metric_value {record_metric!r} does not match verified metric_value {result.metric_value!r}")
616
+ return errors
617
+
618
+
619
  def load_solution_json(text: str) -> dict[str, Any]:
620
+ def reject_constant(value: str) -> None:
621
+ raise ValueError(f"invalid JSON numeric constant {value}")
622
+
623
+ payload = json.loads(text, parse_constant=reject_constant)
624
  if not isinstance(payload, dict):
625
  raise ValueError("submission JSON must be an object")
626
  return payload
tests/test_verifier.py CHANGED
@@ -10,7 +10,7 @@ sys.path.insert(0, str(ROOT))
10
 
11
  from packing_benchmark.renderer import svg_markup # noqa: E402
12
  from packing_benchmark.store import SolutionStore # noqa: E402
13
- from packing_benchmark.verifier import verify_solution # noqa: E402
14
 
15
 
16
  def test_seed_records_verify() -> None:
@@ -34,6 +34,97 @@ def test_rejects_overlapping_pair() -> None:
34
  assert result.max_pair_overlap_depth > 0
35
 
36
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
37
  def test_store_and_svg_paths_exist() -> None:
38
  store = SolutionStore(ROOT / "data")
39
  records = store.best_records("All")
 
10
 
11
  from packing_benchmark.renderer import svg_markup # noqa: E402
12
  from packing_benchmark.store import SolutionStore # noqa: E402
13
+ from packing_benchmark.verifier import load_solution_json, verify_record_solution, verify_solution # noqa: E402
14
 
15
 
16
  def test_seed_records_verify() -> None:
 
34
  assert result.max_pair_overlap_depth > 0
35
 
36
 
37
+ def test_rejects_boundary_protrusion() -> None:
38
+ solution = {
39
+ "case": "squinsqu@1",
40
+ "item": {"type": "regular_polygon", "sides": 4, "side_length": 1},
41
+ "container": {"type": "regular_polygon", "sides": 4, "side_length": 1},
42
+ "placements": [
43
+ {"x": 0.6, "y": 0.0, "rotation_radians": 0},
44
+ ],
45
+ }
46
+ result = verify_solution(solution)
47
+ assert not result.ok
48
+ assert result.max_boundary_excess > 0
49
+
50
+
51
+ def test_rejects_wrong_case_family() -> None:
52
+ solution = {
53
+ "case": "triintri@1",
54
+ "item": {"type": "circle", "radius": 0.5},
55
+ "container": {"type": "regular_polygon", "sides": 4, "side_length": 2},
56
+ "placements": [
57
+ {"x": 0.0, "y": 0.0, "rotation_radians": 0},
58
+ ],
59
+ }
60
+ result = verify_solution(solution)
61
+ assert not result.ok
62
+ assert any("does not match item/container geometry" in error for error in result.errors)
63
+
64
+
65
+ def test_rejects_wrong_setup_field() -> None:
66
+ solution = {
67
+ "case": "cirinsqu@1",
68
+ "setup": "triintri",
69
+ "item": {"type": "circle", "radius": 0.5},
70
+ "container": {"type": "rectangle", "width": 2.0, "height": 2.0},
71
+ "placements": [
72
+ {"x": 0.0, "y": 0.0, "rotation_radians": 0},
73
+ ],
74
+ }
75
+ result = verify_solution(solution)
76
+ assert not result.ok
77
+ assert any("setup" in error and "does not match" in error for error in result.errors)
78
+
79
+
80
+ def test_circle_container_metric_is_radius() -> None:
81
+ solution = {
82
+ "case": "triincir@1",
83
+ "item": {"type": "regular_polygon", "sides": 3, "side_length": 1},
84
+ "container": {"type": "circle", "radius": 1.0},
85
+ "placements": [
86
+ {"x": 0.0, "y": 0.0, "rotation_radians": 0},
87
+ ],
88
+ }
89
+ result = verify_solution(solution)
90
+ assert result.ok
91
+ assert result.side == 2.0
92
+ assert result.metric_symbol == "r"
93
+ assert result.metric_value == 1.0
94
+
95
+
96
+ def test_rejects_non_finite_json_numbers() -> None:
97
+ try:
98
+ load_solution_json('{"case": "cirincir@1", "item": {"type": "circle", "radius": NaN}}')
99
+ except ValueError as exc:
100
+ assert "invalid JSON numeric constant" in str(exc)
101
+ else:
102
+ raise AssertionError("NaN JSON was accepted")
103
+
104
+
105
+ def test_record_solution_mismatch_is_detected() -> None:
106
+ solution = {
107
+ "case": "cirinsqu@1",
108
+ "item": {"type": "circle", "radius": 0.5},
109
+ "container": {"type": "rectangle", "width": 2.0, "height": 2.0},
110
+ "placements": [
111
+ {"x": 0.0, "y": 0.0, "rotation_radians": 0},
112
+ ],
113
+ }
114
+ errors = verify_record_solution(
115
+ {
116
+ "case": "cirinsqu@1",
117
+ "setup": "cirinsqu",
118
+ "n": 1,
119
+ "side": 3.0,
120
+ "metric_symbol": "s",
121
+ "metric_value": 3.0,
122
+ },
123
+ solution,
124
+ )
125
+ assert any("record side" in error for error in errors)
126
+
127
+
128
  def test_store_and_svg_paths_exist() -> None:
129
  store = SolutionStore(ROOT / "data")
130
  records = store.best_records("All")