more performance characterization work
This commit is contained in:
parent
42e46c67e0
commit
2049353ee9
9 changed files with 2423 additions and 64 deletions
2
DOCS.md
2
DOCS.md
|
|
@ -272,7 +272,7 @@ Separately from the observational trace tooling, the router may run a bounded po
|
|||
Lower-level search and collision modules are semi-private implementation details. They remain accessible through deep imports for advanced use, but they are unstable and may change without notice. The stable supported entrypoint is `route(problem, options=...)`.
|
||||
|
||||
The current implementation structure is summarized in **[docs/architecture.md](docs/architecture.md)**. The committed example-corpus counter baseline is tracked in **[docs/performance.md](docs/performance.md)**.
|
||||
Use `scripts/diff_performance_baseline.py` to compare a fresh local run against that baseline. Use `scripts/record_conflict_trace.py` for opt-in conflict-hotspot traces and `scripts/record_frontier_trace.py` for hotspot-adjacent prune traces. The counter baseline is currently observational and is not enforced as a CI gate.
|
||||
Use `scripts/diff_performance_baseline.py` to compare a fresh local run against that baseline. Use `scripts/record_conflict_trace.py` for opt-in conflict-hotspot traces, `scripts/record_frontier_trace.py` for hotspot-adjacent prune traces, and `scripts/characterize_pair_local_search.py` to sweep example_07-style no-warm runs for pair-local repair behavior. The counter baseline is currently observational and is not enforced as a CI gate.
|
||||
|
||||
## 11. Tuning Notes
|
||||
|
||||
|
|
|
|||
2108
docs/pair_local_characterization.json
Normal file
2108
docs/pair_local_characterization.json
Normal file
File diff suppressed because it is too large
Load diff
30
docs/pair_local_characterization.md
Normal file
30
docs/pair_local_characterization.md
Normal file
|
|
@ -0,0 +1,30 @@
|
|||
# Pair-Local Search Characterization
|
||||
|
||||
Generated at 2026-04-02T15:53:29-07:00 by `scripts/characterize_pair_local_search.py`.
|
||||
|
||||
Grid: `num_nets=[6, 8, 10]`, `seed=[41, 42, 43]`, repeats=2.
|
||||
|
||||
| Nets | Seed | Repeat | Duration (s) | Valid | Reached | Pair Pairs | Pair Accepts | Pair Nodes | Nodes | Checks |
|
||||
| :-- | :-- | :-- | --: | --: | --: | --: | --: | --: | --: | --: |
|
||||
| 6 | 41 | 0 | 0.5462 | 1 | 6 | 0 | 0 | 0 | 500 | 674 |
|
||||
| 6 | 41 | 1 | 0.5256 | 1 | 6 | 0 | 0 | 0 | 500 | 674 |
|
||||
| 6 | 42 | 0 | 0.5241 | 1 | 6 | 0 | 0 | 0 | 503 | 683 |
|
||||
| 6 | 42 | 1 | 0.5477 | 1 | 6 | 0 | 0 | 0 | 503 | 683 |
|
||||
| 6 | 43 | 0 | 0.5199 | 1 | 6 | 0 | 0 | 0 | 493 | 654 |
|
||||
| 6 | 43 | 1 | 0.5160 | 1 | 6 | 0 | 0 | 0 | 493 | 654 |
|
||||
| 8 | 41 | 0 | 1.8818 | 8 | 8 | 2 | 2 | 38 | 1558 | 4313 |
|
||||
| 8 | 41 | 1 | 1.8618 | 8 | 8 | 2 | 2 | 38 | 1558 | 4313 |
|
||||
| 8 | 42 | 0 | 1.4850 | 8 | 8 | 1 | 1 | 19 | 1440 | 3799 |
|
||||
| 8 | 42 | 1 | 1.4636 | 8 | 8 | 1 | 1 | 19 | 1440 | 3799 |
|
||||
| 8 | 43 | 0 | 1.0652 | 8 | 8 | 0 | 0 | 0 | 939 | 1844 |
|
||||
| 8 | 43 | 1 | 1.0502 | 8 | 8 | 0 | 0 | 0 | 939 | 1844 |
|
||||
| 10 | 41 | 0 | 2.8617 | 8 | 10 | 2 | 2 | 41 | 2223 | 6208 |
|
||||
| 10 | 41 | 1 | 2.8282 | 8 | 10 | 2 | 2 | 41 | 2223 | 6208 |
|
||||
| 10 | 42 | 0 | 2.0356 | 10 | 10 | 2 | 2 | 68 | 1764 | 4625 |
|
||||
| 10 | 42 | 1 | 2.0052 | 10 | 10 | 2 | 2 | 68 | 1764 | 4625 |
|
||||
| 10 | 43 | 0 | 50.1863 | 10 | 10 | 2 | 2 | 38 | 61259 | 165223 |
|
||||
| 10 | 43 | 1 | 50.4019 | 10 | 10 | 2 | 2 | 38 | 61259 | 165223 |
|
||||
|
||||
## Recommendation
|
||||
|
||||
No smaller stable pair-local smoke scenario satisfied the rule `valid_results == total_results`, `pair_local_search_accepts >= 1`, and `duration_s <= 1.0` across all repeats.
|
||||
|
|
@ -7,6 +7,9 @@ Use `scripts/diff_performance_baseline.py` to compare a fresh run against that s
|
|||
|
||||
The default baseline table below covers the standard example corpus only. The heavier `example_07_large_scale_routing_no_warm_start` canary remains performance-only and is tracked through targeted diffs plus the conflict/frontier trace artifacts.
|
||||
|
||||
Use `scripts/characterize_pair_local_search.py` when you want a small parameter sweep over example_07-style no-warm runs instead of a single canary reading.
|
||||
The current tracked sweep output lives in `docs/pair_local_characterization.json` and `docs/pair_local_characterization.md`.
|
||||
|
||||
| Scenario | Duration (s) | Total | Valid | Reached | Iter | Nets Routed | Nodes | Ray Casts | Moves Gen | Moves Added | Dyn Tree | Visibility Builds | Congestion Checks | Verify Calls |
|
||||
| :-- | --: | --: | --: | --: | --: | --: | --: | --: | --: | --: | --: | --: | --: | --: |
|
||||
| example_01_simple_route | 0.0040 | 1 | 1 | 1 | 1 | 1 | 2 | 10 | 11 | 7 | 0 | 0 | 0 | 4 |
|
||||
|
|
@ -31,6 +34,8 @@ For the current accepted branch, the most important performance-only canary is `
|
|||
- `pair_local_search_accepts`
|
||||
- `pair_local_search_nodes_expanded`
|
||||
|
||||
The latest tracked characterization sweep confirms there is no smaller stable pair-local smoke case under the `<=1.0s` rule, so the 10-net no-warm-start canary remains the primary regression target for this behavior.
|
||||
|
||||
Tracked metric keys:
|
||||
|
||||
nodes_expanded, moves_generated, moves_added, pruned_closed_set, pruned_hard_collision, pruned_cost, route_iterations, nets_routed, nets_reached_target, warm_start_paths_built, warm_start_paths_used, refine_path_calls, timeout_events, iteration_reverify_calls, iteration_reverified_nets, iteration_conflicting_nets, iteration_conflict_edges, nets_carried_forward, score_component_calls, score_component_total_ns, path_cost_calls, danger_map_lookup_calls, danger_map_cache_hits, danger_map_cache_misses, danger_map_query_calls, danger_map_total_ns, move_cache_abs_hits, move_cache_abs_misses, move_cache_rel_hits, move_cache_rel_misses, guidance_match_moves, guidance_match_moves_straight, guidance_match_moves_bend90, guidance_match_moves_sbend, guidance_bonus_applied, guidance_bonus_applied_straight, guidance_bonus_applied_bend90, guidance_bonus_applied_sbend, static_safe_cache_hits, hard_collision_cache_hits, congestion_cache_hits, congestion_cache_misses, congestion_presence_cache_hits, congestion_presence_cache_misses, congestion_presence_skips, congestion_candidate_precheck_hits, congestion_candidate_precheck_misses, congestion_candidate_precheck_skips, congestion_grid_net_cache_hits, congestion_grid_net_cache_misses, congestion_grid_span_cache_hits, congestion_grid_span_cache_misses, congestion_candidate_nets, congestion_net_envelope_cache_hits, congestion_net_envelope_cache_misses, dynamic_path_objects_added, dynamic_path_objects_removed, dynamic_tree_rebuilds, dynamic_grid_rebuilds, static_tree_rebuilds, static_raw_tree_rebuilds, static_net_tree_rebuilds, visibility_corner_index_builds, visibility_builds, visibility_corner_pairs_checked, visibility_corner_queries_exact, visibility_corner_hits_exact, visibility_point_queries, visibility_point_cache_hits, visibility_point_cache_misses, visibility_tangent_candidate_scans, visibility_tangent_candidate_corner_checks, visibility_tangent_candidate_ray_tests, ray_cast_calls, ray_cast_calls_straight_static, ray_cast_calls_expand_snap, ray_cast_calls_expand_forward, ray_cast_calls_visibility_build, ray_cast_calls_visibility_query, ray_cast_calls_visibility_tangent, ray_cast_calls_other, ray_cast_candidate_bounds, ray_cast_exact_geometry_checks, congestion_check_calls, congestion_lazy_resolutions, congestion_lazy_requeues, congestion_candidate_ids, congestion_exact_pair_checks, verify_path_report_calls, verify_static_buffer_ops, verify_dynamic_candidate_nets, verify_dynamic_exact_pair_checks, refinement_windows_considered, refinement_static_bounds_checked, refinement_dynamic_bounds_checked, refinement_candidate_side_extents, refinement_candidates_built, refinement_candidates_verified, refinement_candidates_accepted, pair_local_search_pairs_considered, pair_local_search_attempts, pair_local_search_accepts, pair_local_search_nodes_expanded
|
||||
|
|
|
|||
|
|
@ -432,17 +432,19 @@ def trace_example_07_no_warm_start() -> RoutingRunResult:
|
|||
return _trace_example_07_variant(warm_start_enabled=False)
|
||||
|
||||
|
||||
def _snapshot_example_07_variant(
|
||||
name: str,
|
||||
def _build_example_07_variant_stack(
|
||||
*,
|
||||
num_nets: int,
|
||||
seed: int,
|
||||
warm_start_enabled: bool,
|
||||
) -> ScenarioSnapshot:
|
||||
capture_conflict_trace: bool = False,
|
||||
capture_frontier_trace: bool = False,
|
||||
) -> tuple[CostEvaluator, AStarMetrics, PathFinder]:
|
||||
bounds = (0, 0, 1000, 1000)
|
||||
obstacles = [
|
||||
box(450, 0, 550, 400),
|
||||
box(450, 600, 550, 1000),
|
||||
]
|
||||
num_nets = 10
|
||||
start_x = 50
|
||||
start_y_base = 500 - (num_nets * 10.0) / 2.0
|
||||
end_x = 950
|
||||
|
|
@ -477,74 +479,30 @@ def _snapshot_example_07_variant(
|
|||
"multiplier": 1.4,
|
||||
"net_order": "shortest",
|
||||
"capture_expanded": True,
|
||||
"capture_conflict_trace": capture_conflict_trace,
|
||||
"capture_frontier_trace": capture_frontier_trace,
|
||||
"shuffle_nets": True,
|
||||
"seed": 42,
|
||||
"seed": seed,
|
||||
"warm_start_enabled": warm_start_enabled,
|
||||
},
|
||||
)
|
||||
|
||||
def iteration_callback(idx: int, current_results: dict[str, RoutingResult]) -> None:
|
||||
_ = current_results
|
||||
new_greedy = max(1.1, 1.5 - ((idx + 1) / 10.0) * 0.4)
|
||||
evaluator.greedy_h_weight = new_greedy
|
||||
metrics.reset_per_route()
|
||||
|
||||
t0 = perf_counter()
|
||||
results = pathfinder.route_all(iteration_callback=iteration_callback)
|
||||
t1 = perf_counter()
|
||||
return _make_snapshot(name, results, t1 - t0, pathfinder.metrics.snapshot())
|
||||
return evaluator, metrics, pathfinder
|
||||
|
||||
|
||||
def _trace_example_07_variant(
|
||||
def _run_example_07_variant(
|
||||
*,
|
||||
num_nets: int,
|
||||
seed: int,
|
||||
warm_start_enabled: bool,
|
||||
capture_conflict_trace: bool = False,
|
||||
capture_frontier_trace: bool = False,
|
||||
) -> RoutingRunResult:
|
||||
bounds = (0, 0, 1000, 1000)
|
||||
obstacles = [
|
||||
box(450, 0, 550, 400),
|
||||
box(450, 600, 550, 1000),
|
||||
]
|
||||
num_nets = 10
|
||||
start_x = 50
|
||||
start_y_base = 500 - (num_nets * 10.0) / 2.0
|
||||
end_x = 950
|
||||
end_y_base = 100
|
||||
end_y_pitch = 800.0 / (num_nets - 1)
|
||||
|
||||
netlist = {}
|
||||
for index in range(num_nets):
|
||||
sy = int(round(start_y_base + index * 10.0))
|
||||
ey = int(round(end_y_base + index * end_y_pitch))
|
||||
netlist[f"net_{index:02d}"] = (Port(start_x, sy, 0), Port(end_x, ey, 0))
|
||||
widths = dict.fromkeys(netlist, 2.0)
|
||||
_, evaluator, metrics, pathfinder = _build_routing_stack(
|
||||
bounds=bounds,
|
||||
netlist=netlist,
|
||||
widths=widths,
|
||||
clearance=6.0,
|
||||
obstacles=obstacles,
|
||||
evaluator_kwargs={
|
||||
"greedy_h_weight": 1.5,
|
||||
"unit_length_cost": 0.1,
|
||||
"bend_penalty": 100.0,
|
||||
"sbend_penalty": 400.0,
|
||||
},
|
||||
request_kwargs={
|
||||
"node_limit": 2000000,
|
||||
"bend_radii": [50.0],
|
||||
"sbend_radii": [50.0],
|
||||
"bend_clip_margin": 10.0,
|
||||
"max_iterations": 15,
|
||||
"base_penalty": 100.0,
|
||||
"multiplier": 1.4,
|
||||
"net_order": "shortest",
|
||||
"capture_expanded": True,
|
||||
"capture_conflict_trace": True,
|
||||
"capture_frontier_trace": True,
|
||||
"shuffle_nets": True,
|
||||
"seed": 42,
|
||||
"warm_start_enabled": warm_start_enabled,
|
||||
},
|
||||
evaluator, metrics, pathfinder = _build_example_07_variant_stack(
|
||||
num_nets=num_nets,
|
||||
seed=seed,
|
||||
warm_start_enabled=warm_start_enabled,
|
||||
capture_conflict_trace=capture_conflict_trace,
|
||||
capture_frontier_trace=capture_frontier_trace,
|
||||
)
|
||||
|
||||
def iteration_callback(idx: int, current_results: dict[str, RoutingResult]) -> None:
|
||||
|
|
@ -557,6 +515,34 @@ def _trace_example_07_variant(
|
|||
return _make_run_result(results, pathfinder)
|
||||
|
||||
|
||||
def _snapshot_example_07_variant(
|
||||
name: str,
|
||||
*,
|
||||
warm_start_enabled: bool,
|
||||
) -> ScenarioSnapshot:
|
||||
t0 = perf_counter()
|
||||
run = _run_example_07_variant(
|
||||
num_nets=10,
|
||||
seed=42,
|
||||
warm_start_enabled=warm_start_enabled,
|
||||
)
|
||||
t1 = perf_counter()
|
||||
return _make_snapshot(name, run.results_by_net, t1 - t0, run.metrics)
|
||||
|
||||
|
||||
def _trace_example_07_variant(
|
||||
*,
|
||||
warm_start_enabled: bool,
|
||||
) -> RoutingRunResult:
|
||||
return _run_example_07_variant(
|
||||
num_nets=10,
|
||||
seed=42,
|
||||
warm_start_enabled=warm_start_enabled,
|
||||
capture_conflict_trace=True,
|
||||
capture_frontier_trace=True,
|
||||
)
|
||||
|
||||
|
||||
def run_example_07() -> ScenarioOutcome:
|
||||
return snapshot_example_07().as_outcome()
|
||||
|
||||
|
|
|
|||
|
|
@ -15,7 +15,7 @@ if TYPE_CHECKING:
|
|||
RUN_PERFORMANCE = os.environ.get("INIRE_RUN_PERFORMANCE") == "1"
|
||||
PERFORMANCE_REPEATS = 3
|
||||
REGRESSION_FACTOR = 1.5
|
||||
NO_WARM_START_REGRESSION_SECONDS = 180.0
|
||||
NO_WARM_START_REGRESSION_SECONDS = 15.0
|
||||
|
||||
# Baselines are measured from clean 6a28dcf-style runs without plotting.
|
||||
BASELINE_SECONDS = {
|
||||
|
|
|
|||
|
|
@ -22,6 +22,7 @@ from inire.tests.example_scenarios import (
|
|||
AStarMetrics,
|
||||
snapshot_example_05,
|
||||
snapshot_example_07_no_warm_start,
|
||||
trace_example_07_no_warm_start,
|
||||
)
|
||||
|
||||
|
||||
|
|
@ -57,6 +58,28 @@ def test_example_07_no_warm_start_canary_improves_validity() -> None:
|
|||
assert snapshot.total_results == 10
|
||||
assert snapshot.reached_targets == 10
|
||||
assert snapshot.valid_results == 10
|
||||
assert snapshot.metrics.warm_start_paths_built == 0
|
||||
assert snapshot.metrics.warm_start_paths_used == 0
|
||||
assert snapshot.metrics.pair_local_search_pairs_considered >= 1
|
||||
assert snapshot.metrics.pair_local_search_accepts >= 1
|
||||
assert snapshot.metrics.pair_local_search_nodes_expanded <= 128
|
||||
assert snapshot.metrics.nodes_expanded <= 2500
|
||||
assert snapshot.metrics.congestion_check_calls <= 6000
|
||||
|
||||
|
||||
def test_example_07_no_warm_start_trace_finishes_without_conflict_edges() -> None:
|
||||
run = trace_example_07_no_warm_start()
|
||||
|
||||
assert len(run.results_by_net) == 10
|
||||
assert sum(result.is_valid for result in run.results_by_net.values()) == 10
|
||||
assert sum(result.reached_target for result in run.results_by_net.values()) == 10
|
||||
assert run.metrics.pair_local_search_pairs_considered >= 1
|
||||
assert run.metrics.pair_local_search_accepts >= 1
|
||||
|
||||
final_entry = run.conflict_trace[-1]
|
||||
assert final_entry.stage == "final"
|
||||
assert len(final_entry.completed_net_ids) == 10
|
||||
assert final_entry.conflict_edges == ()
|
||||
|
||||
|
||||
def test_example_06_clipped_bbox_margin_restores_legacy_seed() -> None:
|
||||
|
|
|
|||
|
|
@ -274,3 +274,33 @@ def test_record_frontier_trace_script_writes_selected_scenario(tmp_path: Path) -
|
|||
assert payload["generator"] == "scripts/record_frontier_trace.py"
|
||||
assert [entry["name"] for entry in payload["scenarios"]] == ["example_05_orientation_stress"]
|
||||
assert (tmp_path / "frontier_trace.md").exists()
|
||||
|
||||
|
||||
def test_characterize_pair_local_search_script_writes_outputs(tmp_path: Path) -> None:
|
||||
repo_root = Path(__file__).resolve().parents[2]
|
||||
script_path = repo_root / "scripts" / "characterize_pair_local_search.py"
|
||||
|
||||
subprocess.run(
|
||||
[
|
||||
sys.executable,
|
||||
str(script_path),
|
||||
"--output-dir",
|
||||
str(tmp_path),
|
||||
"--num-nets",
|
||||
"6",
|
||||
"--seeds",
|
||||
"41",
|
||||
"--repeats",
|
||||
"1",
|
||||
],
|
||||
check=True,
|
||||
)
|
||||
|
||||
payload = json.loads((tmp_path / "pair_local_characterization.json").read_text())
|
||||
assert payload["generated_at"]
|
||||
assert payload["generator"] == "scripts/characterize_pair_local_search.py"
|
||||
assert payload["grid"]["num_nets"] == [6]
|
||||
assert payload["grid"]["seeds"] == [41]
|
||||
assert payload["grid"]["repeats"] == 1
|
||||
assert len(payload["cases"]) == 1
|
||||
assert (tmp_path / "pair_local_characterization.md").exists()
|
||||
|
|
|
|||
177
scripts/characterize_pair_local_search.py
Normal file
177
scripts/characterize_pair_local_search.py
Normal file
|
|
@ -0,0 +1,177 @@
|
|||
#!/usr/bin/env python3
|
||||
from __future__ import annotations
|
||||
|
||||
import argparse
|
||||
import json
|
||||
from dataclasses import asdict
|
||||
from datetime import datetime
|
||||
from pathlib import Path
|
||||
from time import perf_counter
|
||||
|
||||
from inire.tests.example_scenarios import _run_example_07_variant
|
||||
|
||||
|
||||
def _parse_csv_ints(raw: str) -> tuple[int, ...]:
|
||||
return tuple(int(part) for part in raw.split(",") if part.strip())
|
||||
|
||||
|
||||
def _run_case(num_nets: int, seed: int) -> dict[str, object]:
|
||||
t0 = perf_counter()
|
||||
run = _run_example_07_variant(
|
||||
num_nets=num_nets,
|
||||
seed=seed,
|
||||
warm_start_enabled=False,
|
||||
)
|
||||
duration_s = perf_counter() - t0
|
||||
return {
|
||||
"duration_s": duration_s,
|
||||
"summary": {
|
||||
"total_results": len(run.results_by_net),
|
||||
"valid_results": sum(1 for result in run.results_by_net.values() if result.is_valid),
|
||||
"reached_targets": sum(1 for result in run.results_by_net.values() if result.reached_target),
|
||||
},
|
||||
"metrics": asdict(run.metrics),
|
||||
}
|
||||
|
||||
|
||||
def _is_smoke_candidate(entry: dict[str, object]) -> bool:
|
||||
summary = entry["summary"]
|
||||
metrics = entry["metrics"]
|
||||
return (
|
||||
summary["valid_results"] == summary["total_results"]
|
||||
and metrics["pair_local_search_accepts"] >= 1
|
||||
and entry["duration_s"] <= 1.0
|
||||
)
|
||||
|
||||
|
||||
def _select_smoke_case(cases: list[dict[str, object]]) -> dict[str, object] | None:
|
||||
grouped: dict[tuple[int, int], list[dict[str, object]]] = {}
|
||||
for case in cases:
|
||||
key = (case["num_nets"], case["seed"])
|
||||
grouped.setdefault(key, []).append(case)
|
||||
|
||||
candidates = []
|
||||
for (num_nets, seed), repeats in grouped.items():
|
||||
if repeats and all(_is_smoke_candidate(repeat) for repeat in repeats):
|
||||
candidates.append({"num_nets": num_nets, "seed": seed})
|
||||
if not candidates:
|
||||
return None
|
||||
candidates.sort(key=lambda item: (item["num_nets"], item["seed"]))
|
||||
return candidates[0]
|
||||
|
||||
|
||||
def _render_markdown(payload: dict[str, object]) -> str:
|
||||
lines = [
|
||||
"# Pair-Local Search Characterization",
|
||||
"",
|
||||
f"Generated at {payload['generated_at']} by `{payload['generator']}`.",
|
||||
"",
|
||||
f"Grid: `num_nets={payload['grid']['num_nets']}`, `seed={payload['grid']['seeds']}`, repeats={payload['grid']['repeats']}.",
|
||||
"",
|
||||
"| Nets | Seed | Repeat | Duration (s) | Valid | Reached | Pair Pairs | Pair Accepts | Pair Nodes | Nodes | Checks |",
|
||||
"| :-- | :-- | :-- | --: | --: | --: | --: | --: | --: | --: | --: |",
|
||||
]
|
||||
for case in payload["cases"]:
|
||||
summary = case["summary"]
|
||||
metrics = case["metrics"]
|
||||
lines.append(
|
||||
"| "
|
||||
f"{case['num_nets']} | "
|
||||
f"{case['seed']} | "
|
||||
f"{case['repeat']} | "
|
||||
f"{case['duration_s']:.4f} | "
|
||||
f"{summary['valid_results']} | "
|
||||
f"{summary['reached_targets']} | "
|
||||
f"{metrics['pair_local_search_pairs_considered']} | "
|
||||
f"{metrics['pair_local_search_accepts']} | "
|
||||
f"{metrics['pair_local_search_nodes_expanded']} | "
|
||||
f"{metrics['nodes_expanded']} | "
|
||||
f"{metrics['congestion_check_calls']} |"
|
||||
)
|
||||
|
||||
lines.extend(["", "## Recommendation", ""])
|
||||
recommended = payload["recommended_smoke_scenario"]
|
||||
if recommended is None:
|
||||
lines.append(
|
||||
"No smaller stable pair-local smoke scenario satisfied the rule "
|
||||
"`valid_results == total_results`, `pair_local_search_accepts >= 1`, and `duration_s <= 1.0` across all repeats."
|
||||
)
|
||||
else:
|
||||
lines.append(
|
||||
f"Recommended smoke scenario: `num_nets={recommended['num_nets']}`, `seed={recommended['seed']}`."
|
||||
)
|
||||
return "\n".join(lines)
|
||||
|
||||
|
||||
def main() -> None:
|
||||
parser = argparse.ArgumentParser(description="Characterize pair-local search across example_07-style no-warm runs.")
|
||||
parser.add_argument(
|
||||
"--num-nets",
|
||||
default="6,8,10",
|
||||
help="Comma-separated num_nets values to sweep. Default: 6,8,10.",
|
||||
)
|
||||
parser.add_argument(
|
||||
"--seeds",
|
||||
default="41,42,43",
|
||||
help="Comma-separated seed values to sweep. Default: 41,42,43.",
|
||||
)
|
||||
parser.add_argument(
|
||||
"--repeats",
|
||||
type=int,
|
||||
default=2,
|
||||
help="Number of repeated runs per (num_nets, seed). Default: 2.",
|
||||
)
|
||||
parser.add_argument(
|
||||
"--output-dir",
|
||||
type=Path,
|
||||
default=None,
|
||||
help="Directory to write pair_local_characterization.json and .md into. Defaults to <repo>/docs.",
|
||||
)
|
||||
args = parser.parse_args()
|
||||
|
||||
repo_root = Path(__file__).resolve().parents[1]
|
||||
output_dir = repo_root / "docs" if args.output_dir is None else args.output_dir.resolve()
|
||||
output_dir.mkdir(exist_ok=True)
|
||||
|
||||
num_nets_values = _parse_csv_ints(args.num_nets)
|
||||
seed_values = _parse_csv_ints(args.seeds)
|
||||
|
||||
cases: list[dict[str, object]] = []
|
||||
for num_nets in num_nets_values:
|
||||
for seed in seed_values:
|
||||
for repeat in range(args.repeats):
|
||||
case = _run_case(num_nets, seed)
|
||||
case["num_nets"] = num_nets
|
||||
case["seed"] = seed
|
||||
case["repeat"] = repeat
|
||||
cases.append(case)
|
||||
|
||||
payload = {
|
||||
"generated_at": datetime.now().astimezone().isoformat(timespec="seconds"),
|
||||
"generator": "scripts/characterize_pair_local_search.py",
|
||||
"grid": {
|
||||
"num_nets": list(num_nets_values),
|
||||
"seeds": list(seed_values),
|
||||
"repeats": args.repeats,
|
||||
},
|
||||
"cases": cases,
|
||||
"recommended_smoke_scenario": _select_smoke_case(cases),
|
||||
}
|
||||
|
||||
json_path = output_dir / "pair_local_characterization.json"
|
||||
markdown_path = output_dir / "pair_local_characterization.md"
|
||||
json_path.write_text(json.dumps(payload, indent=2, sort_keys=True) + "\n")
|
||||
markdown_path.write_text(_render_markdown(payload) + "\n")
|
||||
|
||||
if json_path.is_relative_to(repo_root):
|
||||
print(f"Wrote {json_path.relative_to(repo_root)}")
|
||||
else:
|
||||
print(f"Wrote {json_path}")
|
||||
if markdown_path.is_relative_to(repo_root):
|
||||
print(f"Wrote {markdown_path.relative_to(repo_root)}")
|
||||
else:
|
||||
print(f"Wrote {markdown_path}")
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
Loading…
Add table
Add a link
Reference in a new issue