Merge remote-tracking branch 'origin/sub_laptop_3' into sub_laptop_5

This commit is contained in:
2026-09-29 20:41:12 +09:00
2 changed files with 83 additions and 12 deletions
+71 -6
View File
@@ -113,6 +113,60 @@ def test_each_branch_best_at_own_goal(hill: Terrain) -> None:
assert all(top <= r["metrics"]["max_grade_pct"] + 1e-6 for r in runs.values())
# 비중 끝값 · 단조(PLAN 23-3e) — 예상 · 토공 · 기울기 한쪽 100 이면 그 갈래와 같은 선
ENDS = {
"follow": {"expected": 100, "earthwork": 0, "grade": 0},
"earthwork": {"expected": 0, "earthwork": 100, "grade": 0},
"gentle": {"expected": 0, "earthwork": 0, "grade": 100},
}
# 예상 100 → 0(나머지 반반) · 기울기 100 → 토공 100
TOWARD_OTHERS = [
(100, 0, 0),
(90, 5, 5),
(75, 12.5, 12.5),
(50, 25, 25),
(25, 37.5, 37.5),
(0, 50, 50),
]
TOWARD_EARTH = [(0, 0, 100), (0, 25, 75), (0, 50, 50), (0, 75, 25), (0, 100, 0)]
def _gap(first: dict, second: dict) -> float:
return LineString(first["planned"]).hausdorff_distance(LineString(second["planned"]))
def _check_weight_ends(run) -> None:
for mode, weights in ENDS.items():
end, own = run("weighted", weights), run(mode)
assert _gap(end, own) < 0.01, (mode, _gap(end, own))
assert end["nodes"] == own["nodes"]
assert end["metrics"]["length_m"] == own["metrics"]["length_m"]
def _check_weight_monotone(run) -> None:
"""비중을 한쪽으로 옮기면 그 목표 값은 나빠지지 않음(후보가 같으니 가중합 최소의 성질)."""
def metrics(steps):
keys = ("expected", "earthwork", "grade")
return [run("weighted", dict(zip(keys, step)))["metrics"] for step in steps]
offsets = [m["offset_avg_m"] for m in metrics(TOWARD_OTHERS)]
assert offsets == sorted(offsets), offsets
earth = metrics(TOWARD_EARTH)
cuts = [m["cut_m3"] + m["fill_m3"] for m in earth]
tops = [m["max_grade_pct"] for m in earth]
assert cuts == sorted(cuts, reverse=True), cuts
assert tops == sorted(tops), tops
def test_weight_ends_match_branches(hill: Terrain) -> None:
_check_weight_ends(lambda mode, weights=None: _run(hill, mode, weights=weights))
def test_weight_moves_monotone(hill: Terrain) -> None:
_check_weight_monotone(lambda mode, weights=None: _run(hill, mode, weights=weights))
def test_follow_stays_on_expected_where_allowed() -> None:
"""고른 경사(2 %) — 추종은 예상노선을 그대로(격자 칸 반 안쪽)."""
ramp = _terrain(lambda xx, yy: 0.02 * xx)
@@ -301,18 +355,19 @@ def project_runs() -> dict:
streams = stream_lines(_reproject_features(features, to_metric))
route = read_planned_route_csv(expected_route_csv_path(PROJECT))
expected = [(float(v.x), float(v.y)) for v in route.vertices]
return {
mode: generate_initial_route(
def run(mode: str, weights: dict | None = None) -> dict:
return generate_initial_route(
expected,
terrain,
streams,
mode=mode,
weights=None,
weights=weights,
max_grade=GRADE,
min_radius_m=RADIUS,
)
for mode in MODES
}
return {**{mode: run(mode) for mode in MODES}, "run": run}
def test_project_follow_is_preprocess_initial(project_runs: dict) -> None:
@@ -331,7 +386,7 @@ def test_project_follow_is_preprocess_initial(project_runs: dict) -> None:
def test_project_branches_differ_by_goal(project_runs: dict) -> None:
"""갈래끼리 선이 수 m 이상 갈리고 · 각 갈래가 제 목표 비교값에서 가장 좋음."""
searched = {mode: project_runs[mode] for mode in SEARCHED}
searched = {mode: project_runs[mode] for mode in SEARCHED} # 비중은 50/25/25
for first in SEARCHED:
for second in SEARCHED:
if first < second:
@@ -344,3 +399,13 @@ def test_project_branches_differ_by_goal(project_runs: dict) -> None:
assert all(top <= r["metrics"]["max_grade_pct"] for r in searched.values())
for result in searched.values():
assert result["metrics"]["over_limit"]["total"] == len(result["metrics"]["violations"])
def test_project_weight_ends_match_branches(project_runs: dict) -> None:
"""랩탑_보조 — 비중 100/0/0 = 추적(1078.01 m) · 0/100/0 = 토공 최소 · 0/0/100 = 기울기 순한."""
_check_weight_ends(project_runs["run"])
assert project_runs["run"]("weighted", ENDS["follow"])["metrics"]["length_m"] == 1078.01
def test_project_weight_moves_monotone(project_runs: dict) -> None:
_check_weight_monotone(project_runs["run"])