fix(455 433-13③ 옛 정책 경고): 옛 정책 빈 법정 최소 R · L = 첫 계획선 설계 기준(criteria) — 화면 readAlignment ↔ 서버 rebuild 같은 규칙 · 초안 경고 = 저장 뒤 경고

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
2026-10-18 21:43:08 +09:00
co-authored by Claude Opus 5.5
parent 83968bf6ed
commit 6836bcef1f
5 changed files with 166 additions and 1 deletions
@@ -28,6 +28,7 @@
from collections import Counter
from collections.abc import Callable
from dataclasses import replace
from typing import Any
import numpy as np
@@ -553,6 +554,16 @@ def rebuild_alignment_profile(
policy = AlignmentPolicy.from_dict(stored.get("policy") or {})
previous = (longitudinal.get("design_profiles") or [{}])[0]
criteria = previous.get("criteria") or {}
# 옛 정책엔 법정 최소 R · L 이 없음 — 첫 계획선 설계 기준(criteria = 설계속도 법정값)으로
# 채움(433-13 · TS `readAlignment` 짝)
policy = replace(
policy,
min_vertical_radius_m=policy.min_vertical_radius_m
or float(criteria.get("min_vertical_radius_m") or 0.0),
min_curve_length_m=policy.min_curve_length_m
or float(criteria.get("min_curve_length_m") or 0.0),
)
alignment = build_alignment(
base_s=base_s,
base_z=base_z,
@@ -562,7 +573,6 @@ def rebuild_alignment_profile(
policy=policy,
edits=edits,
)
criteria = previous.get("criteria") or {}
options = GradeDesignOptions(
max_grade_pct=float(criteria.get("max_grade_pct") or policy.max_grade_pct),
max_reverse_grade_pct=float(criteria.get("max_reverse_grade_pct") or 5.0),
@@ -17,6 +17,16 @@ export function readAlignment(data: LongitudinalSection): ProfileAlignment | nul
station_offsets: candidate.edits?.station_offsets ?? {},
curve_radii: candidate.edits?.curve_radii ?? {},
};
// 옛 정책엔 법정 최소 R · L 이 없음 — 첫 계획선 설계 기준(criteria = 설계속도 법정값)으로 채움
// (433-13 · 서버 `rebuild_alignment_profile` 짝)
const criteria = data.design_profiles?.[0]?.criteria;
const policy = candidate.policy;
if (criteria && (!policy.min_vertical_radius_m || !policy.min_curve_length_m))
candidate.policy = {
...policy,
min_vertical_radius_m: policy.min_vertical_radius_m || criteria.min_vertical_radius_m || 0,
min_curve_length_m: policy.min_curve_length_m || criteria.min_curve_length_m || 0,
};
return candidate;
}
+2
View File
@@ -180,6 +180,8 @@ export interface DesignProfile {
samples: DesignProfileSample[];
balance_segments: DesignProfileSegment[];
summary: DesignProfileSummary;
/** 첫 계획선 설계 기준(서버 `GradeDesignOptions.as_dict`) — 옛 정책 빈 법정 최소 R · L 채움(433-13). */
criteria?: { min_vertical_radius_m?: number | null; min_curve_length_m?: number | null };
}
export interface LongitudinalSection {
@@ -0,0 +1,38 @@
/* 455 (433-13 ③) 옛 정책 경고 — 진짜 `B05_Profile_UI_Profile_Data.ts` readAlignment +
* `B05_Profile_UI_Profile_Alignment.ts` buildAlignment 를 Node 로 돌림.
* 입력 argv[2] = 종단 정본 JSON · 표준출력 = { warnings: [법정 미달 글], policy }.
*/
const fs = require("fs");
const path = require("path");
const ts = require(path.join(__dirname, "..", "..", "config", "node_modules", "typescript"));
const ROOT = path.join(__dirname, "..", "..");
const cache = new Map();
function loadTs(file) {
const full = path.resolve(file);
if (cache.has(full)) return cache.get(full);
const js = ts.transpileModule(fs.readFileSync(full, "utf8"), {
compilerOptions: { module: ts.ModuleKind.CommonJS, target: ts.ScriptTarget.ES2020 },
}).outputText;
const box = { exports: {} };
cache.set(full, box.exports);
const localRequire = (request) => {
if (request.startsWith(".")) return loadTs(path.join(path.dirname(full), `${request}.ts`));
return require(request);
};
new Function("exports", "module", "require", js)(box.exports, box, localRequire);
return box.exports;
}
const LOGIC = path.join(ROOT, "B05_Profile", "logic");
const data = loadTs(path.join(LOGIC, "B05_Profile_UI_Profile_Data.ts"));
const align = loadTs(path.join(LOGIC, "B05_Profile_UI_Profile_Alignment.ts"));
const longitudinal = JSON.parse(fs.readFileSync(process.argv[2], "utf8"));
const stored = data.readAlignment(longitudinal);
const built = align.buildAlignment(align.toAlignmentBase(stored), align.emptyEdits());
process.stdout.write(
JSON.stringify({
warnings: built.warnings.filter((text) => text.includes("법정 최소")),
policy: stored.policy,
}),
);
@@ -0,0 +1,105 @@
"""455 (433-13 ③) 옛 정책 종단 — 법정 최소 R · L 미달 경고가 초안 · 저장 뒤 같음.
까닭: 옛 저장 정책엔 `min_vertical_radius_m` · `min_curve_length_m` 가 없어 화면 초안(저장
정책 그대로) 경고 0 · 노선 [저장](새 정책)에서야 뜸. 고침: 빈 값은 첫 계획선 설계 기준(criteria =
설계속도 법정값)
— TS `readAlignment` ↔ 서버 `rebuild_alignment_profile` 같은 규칙(`helper_455_old_policy.cjs`).
"""
from __future__ import annotations
import copy
import json
import math
import subprocess
import sys
from pathlib import Path
HERE = Path(__file__).resolve().parent
ROOT = HERE.parents[1]
if str(ROOT) not in sys.path:
sys.path.insert(0, str(ROOT))
from B05_Profile.B05_Profile_Engine_Grade import GradeDesignOptions # noqa: E402
from B05_Profile.B05_Profile_Engine_Grade_Profile import ( # noqa: E402
design_ground_following_profile,
rebuild_alignment_profile,
)
# 설계속도 40 — 법정 최소 R 450 · L 40 > 기본 L 20 → 곡선마다 미달
OPTIONS = GradeDesignOptions(
max_grade_pct=14.0,
max_reverse_grade_pct=5.0,
min_vertical_radius_m=450.0,
min_curve_length_m=40.0,
min_tangent_length_m=20.0,
vertical_curve_skip_delta_pct=0.5,
design_speed_kph=40,
)
def _ground(s: float) -> float:
return 100.0 + 0.05 * s + 2.0 * math.sin(s / 30.0)
def _fresh() -> dict:
samples = [
{"chainage_m": float(s), "elevation_m": round(_ground(s), 6), "valid": True}
for s in range(0, 401)
]
stations = [{"chainage_m": float(s), "kind": "regular"} for s in range(0, 401, 20)]
data = {"length_m": 400.0, "samples": samples, "stations": stations}
alignment, profile = design_ground_following_profile(data, OPTIONS, station_interval_m=20.0)
data["profile_alignment"], data["design_profiles"] = alignment, [profile]
return data
def _old() -> dict:
data = _fresh()
for key in ("min_vertical_radius_m", "min_curve_length_m"):
data["profile_alignment"]["policy"].pop(key)
return data
def _law(warnings: list[str]) -> list[str]:
return [text for text in warnings if "법정 최소" in text]
def _ts(tmp_path: Path, data: dict) -> dict:
source = tmp_path / "in455.json"
source.write_text(json.dumps(data, ensure_ascii=False), encoding="utf-8")
proc = subprocess.run( # noqa: S603 — 고정 실행 파일
["node", str(HERE / "helper_455_old_policy.cjs"), str(source)],
capture_output=True,
text=True,
encoding="utf-8",
cwd=str(ROOT),
check=False,
)
assert proc.returncode == 0, proc.stderr
return json.loads(proc.stdout)
def test_새_정책_경고_기준(tmp_path: Path) -> None:
fresh = _fresh()
law = _law(fresh["profile_alignment"]["warnings"])
assert law
assert _ts(tmp_path, fresh)["warnings"] == law
def test_옛_정책_초안_경고_저장_뒤와_같음(tmp_path: Path) -> None:
expected = _law(_fresh()["profile_alignment"]["warnings"])
old = _old()
ts = _ts(tmp_path, copy.deepcopy(old))
assert ts["policy"]["min_vertical_radius_m"] == 450.0
assert ts["policy"]["min_curve_length_m"] == 40.0
assert ts["warnings"] == expected
alignment, _profile = rebuild_alignment_profile(old, {})
assert _law(alignment["warnings"]) == expected
assert alignment["policy"]["min_curve_length_m"] == 40.0
def test_criteria_없는_판은_그대로(tmp_path: Path) -> None:
old = _old()
old["design_profiles"][0].pop("criteria")
assert _ts(tmp_path, copy.deepcopy(old))["warnings"] == []