This commit is contained in:
2026-07-23 19:31:52 +09:00
parent e52823294f
commit f633dc0a49
9 changed files with 245 additions and 149 deletions
@@ -8,7 +8,7 @@
기하 규칙 (사용자 확정 사항):
- 변화점은 **기준 측점 위에만** 놓인다 → 곡선 중심이 측점 수직선상에 있다.
- 종단곡선은 변화점 대칭 배치이며 좌우에 직선 구간이 반드시 남는다.
- 곡선 기본 길이 L = 측점간격 × `curve_length_ratio`, 반경 R = L / |A| 로 파생.
- 곡선 기본 **반경** R = 측점간격 × `curve_radius_ratio`, 길이 L = R × |A| 로 파생.
수치 최적화(직선 분할 DP·표고 결정)는 [[B05_wf2_Route_Engine_Grade_Solver]]가,
기준 해석과 오케스트레이션은 [[B05_wf2_Route_Engine_Grade]]가 담당한다. 이 모듈은
@@ -37,8 +37,7 @@ class AlignmentPolicy:
"""계획선 선형·편집 정책. config 기본값 위에 노선별 해석값을 얹는다."""
station_interval_m: float
curve_length_ratio: float
curve_length_min_m: float
curve_radius_ratio: float
curve_tangent_max_ratio: float
curve_skip_legal_exception: bool
balance_tolerance_percent: float
@@ -62,8 +61,7 @@ class AlignmentPolicy:
config = FOREST_ROAD_PROFILE_ALIGNMENT
return cls(
station_interval_m=float(station_interval_m),
curve_length_ratio=float(config["curve_length_ratio"]),
curve_length_min_m=float(config["curve_length_min_m"]),
curve_radius_ratio=float(config["curve_radius_ratio"]),
curve_tangent_max_ratio=float(config["curve_tangent_max_ratio"]),
curve_skip_legal_exception=bool(config["curve_skip_legal_exception"]),
balance_tolerance_percent=float(config["balance_tolerance_percent"]),
@@ -82,11 +80,8 @@ class AlignmentPolicy:
config = FOREST_ROAD_PROFILE_ALIGNMENT
return cls(
station_interval_m=float(payload.get("station_interval_m") or 20.0),
curve_length_ratio=float(
payload.get("curve_length_ratio", config["curve_length_ratio"])
),
curve_length_min_m=float(
payload.get("curve_length_min_m", config["curve_length_min_m"])
curve_radius_ratio=float(
payload.get("curve_radius_ratio", config["curve_radius_ratio"])
),
curve_tangent_max_ratio=float(
payload.get("curve_tangent_max_ratio", config["curve_tangent_max_ratio"])
@@ -109,19 +104,22 @@ class AlignmentPolicy:
)
@property
def default_curve_length_m(self) -> float:
"""측점간격 기준 기본 종단곡선 길이."""
return max(self.curve_length_min_m, self.station_interval_m * self.curve_length_ratio)
def default_curve_radius_m(self) -> float:
"""측점간격 기준 기본 종단곡선 반경.
R을 1차 값으로 고정해야 계획고를 편집해도 곡선 반경이 흔들리지 않는다.
곡선 길이는 L = R × |대수차| 로 따라 움직인다.
"""
return max(1e-6, self.station_interval_m * self.curve_radius_ratio)
def as_dict(self) -> dict[str, Any]:
"""프론트엔드가 동일 기하를 재현하는 데 필요한 상수 묶음."""
return {
"station_interval_m": self.station_interval_m,
"curve_length_ratio": self.curve_length_ratio,
"curve_length_min_m": self.curve_length_min_m,
"curve_radius_ratio": self.curve_radius_ratio,
"curve_tangent_max_ratio": self.curve_tangent_max_ratio,
"curve_skip_legal_exception": self.curve_skip_legal_exception,
"default_curve_length_m": self.default_curve_length_m,
"default_curve_radius_m": self.default_curve_radius_m,
"balance_tolerance_percent": self.balance_tolerance_percent,
"edit_step_m": self.edit_step_m,
"grade_violation_policy": self.grade_violation_policy,
@@ -174,14 +172,17 @@ def build_curves(
pvi_s: np.ndarray,
pvi_z: np.ndarray,
policy: AlignmentPolicy,
curve_lengths: dict[str, float] | None = None,
curve_radii: dict[str, float] | None = None,
) -> tuple[list[dict[str, Any]], list[str]]:
"""각 변화점에 대칭 종단곡선을 삽입하고 곡선 제원을 만든다.
**반경 R이 1차 값**이고 곡선길이는 L = R × |대수차| 로 따라온다. 계획고를 편집하면
대수차가 바뀌므로 L은 변하지만 R은 지정한 값 그대로 유지된다.
곡선 반쪽 길이는 짧은 쪽 인접 직선의 `curve_tangent_max_ratio` 이내로 제한해
좌우에 직선이 반드시 남게 한다(사용자가 R을 키워도 곡선끼리 겹치지 않는다).
좌우에 직선이 반드시 남게 한다(R을 크게 넣어도 곡선끼리 겹치지 않는다).
"""
overrides = curve_lengths or {}
overrides = curve_radii or {}
spans = pvi_s[1:] - pvi_s[:-1]
grades = (pvi_z[1:] - pvi_z[:-1]) / np.where(spans > 0, spans, 1.0)
curves: list[dict[str, Any]] = []
@@ -201,11 +202,12 @@ def build_curves(
omitted = skip_allowed and policy.curve_skip_legal_exception
requested = overrides.get(key)
try:
length = float(requested) if requested is not None else policy.default_curve_length_m
radius = float(requested) if requested is not None else policy.default_curve_radius_m
except (TypeError, ValueError):
length = policy.default_curve_length_m
if not np.isfinite(length) or length <= 0:
length = policy.default_curve_length_m
radius = policy.default_curve_radius_m
if not np.isfinite(radius) or radius <= 0:
radius = policy.default_curve_radius_m
length = radius * abs(delta)
half_limit = (
min(float(spans[index - 1]), float(spans[index])) * policy.curve_tangent_max_ratio
)
@@ -214,8 +216,8 @@ def build_curves(
continue
if length / 2.0 - half > 1e-6:
warnings.append(
f"chainage {chainage:.1f}m: 인접 직선이 짧아 종단곡선 길이를 "
f"{half * 2.0:.1f}m로 줄였습니다."
f"chainage {chainage:.1f}m: 인접 직선이 짧아 종단곡선 반경을 "
f"{half * 2.0 / abs(delta):.0f}m(길이 {half * 2.0:.1f}m)로 줄였습니다."
)
length = half * 2.0
curves.append(
@@ -353,14 +355,14 @@ def build_alignment(
for key, value in (edits.get("station_offsets") or {}).items()
if value is not None
}
curve_lengths = {
curve_radii = {
str(key): float(value)
for key, value in (edits.get("curve_lengths") or {}).items()
for key, value in (edits.get("curve_radii") or {}).items()
if value is not None
}
pvi_s, pvi_z, sources = resolve_pvi(base_s, base_z, station_offsets)
curves, warnings = build_curves(pvi_s, pvi_z, policy, curve_lengths)
curves, warnings = build_curves(pvi_s, pvi_z, policy, curve_radii)
segments = _segments(pvi_s, pvi_z)
plan = evaluate(pvi_s, pvi_z, curves, chainage)
difference = plan - ground
@@ -420,7 +422,7 @@ def build_alignment(
{"chainage_m": round(float(s), 6), "elevation_m": round(float(z), 6)}
for s, z in zip(base_s, base_z)
],
"edits": {"station_offsets": station_offsets, "curve_lengths": curve_lengths},
"edits": {"station_offsets": station_offsets, "curve_radii": curve_radii},
"pvi": pvi_rows,
"segments": segments,
"curves": [