fix(455 433-13③ 옛 정책 경고): 옛 정책 빈 법정 최소 R · L = 첫 계획선 설계 기준(criteria) — 화면 readAlignment ↔ 서버 rebuild 같은 규칙 · 초안 경고 = 저장 뒤 경고
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
@@ -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;
|
||||
}
|
||||
|
||||
|
||||
@@ -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"] == []
|
||||
Reference in New Issue
Block a user