fix(B05): 노선 추천 균형 종단을 본 설계 횡단과 맞춤 — 곡선표 확폭 · 비탈 안 닫힘 넓힘 · 암반 경계 · 누가토량 ±500㎥(78)
- 확폭은 노선 곡선표로(후보 · 편집 띠 · [확인]) · 없으면 종전 실측 반경 - 반폭에서 비탈이 안 닫히면 지표면 끝까지 넓힌 샘플로 다시 셈(본 설계 extend_unclosed 와 같은 뜻) - 암반 경계 기본 오프셋(본 설계 기본 설계와 같음) - 균형 목표 = 누가토량 끝값(절토 다짐 − 성토) ±500㎥ · 고른 선은 보간 말고 바로 셈해 λ 를 더 좁힘 - 후보 줄 세우기는 보간값 그대로(빠름) · 화면에 낼 선만 바로 셈 - 추천 창 한 줄: 불균형 % → 누가토량 ±㎥ Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_019EMfGERKUg5nMMNQsyVZ2j
This commit is contained in:
@@ -8,12 +8,14 @@
|
||||
|
||||
방법:
|
||||
1. 측점(20 m)마다 지반 횡단(±20 m · 0.5 m)을 DEM 에서 뜬다 — B05 측점 생성과 같은 꼴
|
||||
(좌 = +offset · 상단측 = 좌우 평균 표고가 높은 쪽 · 그쪽 절토).
|
||||
2. 측점마다 계획고 ±Δ 의 절 · 성 단면적 곡선 — B06 `compute_cross_design` 그대로(지반유형
|
||||
리핑암 = B06 기본값 · 곡선부 확폭 = B05 측점 생성과 같은 `_plan_radii` · `_curve_widenings`
|
||||
— 빼면 확정 뒤 성토가 1.7배로 벌어짐 · PLAN 77-7) · `AREA_STEP_M` 간격으로 재고 사이는 보간.
|
||||
(좌 = +offset · 상단측 = 좌우 평균 표고가 높은 쪽 · 그쪽 절토). 비탈이 안 닫히는 측점용으로
|
||||
`CROSS_WIDE_M` 까지 더 떠 둔다(지표면 끝에서 자름).
|
||||
2. 측점마다 계획고 ±Δ 의 절 · 성 단면적 곡선 — B06 `compute_cross_design` 그대로. 본 설계
|
||||
기본 설계(`attach_default_designs`)와 같은 입력: 지반유형 리핑암 · 암반 경계 기본 오프셋 ·
|
||||
곡선부 확폭은 곡선표(없으면 측점별 실측 반경) · 비탈이 안 닫히면 넓힌 샘플로 다시 셈
|
||||
(`extend_unclosed` 와 같은 뜻 · PLAN 78). `AREA_STEP_M` 간격으로 재고 사이는 보간.
|
||||
3. 동적계획(앞 두 측점 상태)으로 측점 계획고를 고른다 — 목표 = 절토(다짐 환산) + 성토 ㎥ +
|
||||
λ × (성토 − 절토). λ 를 이분해 |절 − 성| / 큰 쪽이 허용(10 %) 안인 가장 작은 λ.
|
||||
λ × (성토 − 절토). λ 를 이분해 누가토량 끝값(절 다짐 − 성)이 ±허용 ㎥ 안인 가장 작은 λ.
|
||||
· 종단기울기 상한 — 넘지 못함.
|
||||
· 종단곡선 — 측점마다 변화점(20 m)이라 곡선 반쪽이 직선의 0.45 를 못 넘어 L ≤ 18 m <
|
||||
법정 최소 L(20 m 이상). 그래서 대수차가 곡선 생략 한도(비포장 5 %) 안이어야 함 — 넘으면
|
||||
@@ -36,6 +38,8 @@ from config.config_system_design import (
|
||||
EARTHWORK_CONVERSION_FACTORS,
|
||||
FOREST_ROAD_PROFILE_ALIGNMENT,
|
||||
FOREST_ROAD_PROFILE_CRITERIA,
|
||||
ROUTE_RECOMMEND_MASS_TOLERANCE_M3,
|
||||
STANDARD_ROCK_BOUNDARY_DEFAULT_OFFSET_M,
|
||||
)
|
||||
|
||||
if TYPE_CHECKING:
|
||||
@@ -45,21 +49,28 @@ STATION_M = 20.0 # 측점 간격 — 추천 비교값(`..._Metrics.STATION_M`)
|
||||
TAIL_MERGE_M = 5.0 # 종점과 이보다 가까운 정규 측점은 뺌(짧은 끝 구간의 기울기 튐 방지)
|
||||
CROSS_HALF_M = 20.0 # 지반 횡단 반폭 · 간격 — B06 횡단 파일과 같음
|
||||
CROSS_STEP_M = 0.5
|
||||
# 비탈 안 닫힘 측점의 넓힌 샘플 끝 — 배열 크기 한도일 뿐 · 지표면 끝이 먼저면 거기서 자름
|
||||
CROSS_WIDE_M = 60.0
|
||||
LEVEL_RANGE_M = 6.0 # 계획고 후보 = 상한 안 기준선 ± 이 폭
|
||||
LEVEL_STEP_M = 0.25
|
||||
AREA_STEP_M = 2.0 # 단면적 곡선을 재는 계획고 간격(사이는 PCHIP — 1 m 간격과 3 % 안)
|
||||
AREA_STEP_M = 2.0 # 단면적 곡선을 재는 계획고 간격(사이는 PCHIP — 고를 때만 · 끝 토량은 `_exact`)
|
||||
GROUND_TYPE = "ripping_rock" # B06 기본 지반유형(`attach_default_designs`)
|
||||
CURVE_PENALTY_M3 = 500.0 # 대수차가 생략 한도를 1 %p 넘을 때 벌점(㎥)
|
||||
LAMBDA_MAX = 0.95
|
||||
LAMBDA_STEPS = 14
|
||||
EXACT_STEPS = 6 # 고른 계획선의 실제 누가토량으로 λ 를 더 좁히는 횟수
|
||||
PIPE_SNAP_M = 5.0 # 관 자리가 노선에서 이 안이면 그 노선의 관으로 봄
|
||||
GRADE_EPS = 1e-6
|
||||
|
||||
|
||||
def route_sections(
|
||||
vertices: list[tuple[float, float]], terrain: Terrain
|
||||
vertices: list[tuple[float, float]],
|
||||
terrain: Terrain,
|
||||
design_curves: list[dict[str, Any]] | None = None,
|
||||
) -> tuple[np.ndarray, np.ndarray, list[list[dict[str, Any]]], list[str], list[dict[str, Any]]]:
|
||||
"""측점 누가거리 · 중심 지반고 · 측점별 지반 횡단 샘플 · 단면유형(상단측 절토) · 곡선부 확폭."""
|
||||
"""측점 누가거리 · 중심 지반고 · 측점별 지반 횡단 샘플 · 단면유형(상단측 절토) · 곡선부 확폭.
|
||||
|
||||
샘플은 `CROSS_WIDE_M` 까지 — ±`CROSS_HALF_M` 밖은 지표면이 끊기기 전까지만 담는다."""
|
||||
xy = np.asarray(vertices, dtype=np.float64)
|
||||
keep = np.r_[True, np.hypot(*np.diff(xy, axis=0).T) > 1e-9]
|
||||
xy = xy[keep]
|
||||
@@ -74,7 +85,7 @@ def route_sections(
|
||||
tangent = np.diff(xy, axis=0)[index] / seg[index][:, None]
|
||||
left = np.column_stack([-tangent[:, 1], tangent[:, 0]])
|
||||
center = np.column_stack([np.interp(at, chain, xy[:, 0]), np.interp(at, chain, xy[:, 1])])
|
||||
offsets = np.arange(-CROSS_HALF_M, CROSS_HALF_M + 1e-9, CROSS_STEP_M)
|
||||
offsets = np.arange(-CROSS_WIDE_M, CROSS_WIDE_M + 1e-9, CROSS_STEP_M)
|
||||
points = center[:, None, :] + left[:, None, :] * offsets[None, :, None]
|
||||
lookup = points.reshape(-1, 2)[:, ::-1]
|
||||
grid = (terrain.y, terrain.x)
|
||||
@@ -90,32 +101,90 @@ def route_sections(
|
||||
raise ValueError("노선 위 지반고가 없어 종단을 세울 수 없습니다.")
|
||||
fine = ~np.isnan(ground)
|
||||
ground = np.interp(at, at[fine], ground[fine])
|
||||
inner = np.abs(offsets) <= CROSS_HALF_M + 1e-9
|
||||
sections, modes = [], []
|
||||
for row, flags in zip(z, ok):
|
||||
# 반폭 밖은 가운데부터 끊김 없이 이어진 유효 샘플만(본 설계 넓힘이 지표면 끝에서 멈춤)
|
||||
reach = inner.copy()
|
||||
for side in (offsets > CROSS_HALF_M + 1e-9, offsets < -CROSS_HALF_M - 1e-9):
|
||||
index = np.flatnonzero(side)
|
||||
index = index if offsets[index[0]] > 0 else index[::-1]
|
||||
reach[index] = np.cumprod(flags[index]).astype(bool)
|
||||
sections.append(
|
||||
[
|
||||
{"offset_m": float(o), "elevation_m": float(e) if f else None, "valid": bool(f)}
|
||||
for o, e, f in zip(offsets, row, flags)
|
||||
for o, e, f, r in zip(offsets, row, flags, reach)
|
||||
if r
|
||||
]
|
||||
)
|
||||
lefts, rights = row[(offsets > 0) & flags], row[(offsets < 0) & flags]
|
||||
lefts = row[(offsets > 0) & inner & flags]
|
||||
rights = row[(offsets < 0) & inner & flags]
|
||||
up = "right" if lefts.size and rights.size and rights.mean() > lefts.mean() else "left"
|
||||
modes.append(f"{up}_cut")
|
||||
return at, ground, sections, modes, _widenings(xy, chain, at)
|
||||
return at, ground, sections, modes, _widenings(xy, chain, at, design_curves)
|
||||
|
||||
|
||||
def _widenings(xy: np.ndarray, chain: np.ndarray, at: np.ndarray) -> list[dict[str, Any]]:
|
||||
"""측점마다 `compute_cross_design` 확폭 인자 — B05 측점 생성(곡선표 없을 때)과 같은 셈."""
|
||||
from B05_Profile.B05_Profile_Engine_Sections_Core import _curve_widenings, _plan_radii
|
||||
def _widenings(
|
||||
xy: np.ndarray,
|
||||
chain: np.ndarray,
|
||||
at: np.ndarray,
|
||||
design_curves: list[dict[str, Any]] | None = None,
|
||||
) -> list[dict[str, Any]]:
|
||||
"""측점마다 `compute_cross_design` 확폭 인자 — B05 측점 생성(`generate_sections`)과 같은 셈:
|
||||
곡선표가 노선에 맞으면 그것 · 없으면 측점별 실측 반경."""
|
||||
from B05_Profile.B05_Profile_Engine_Sections_Core import (
|
||||
_curve_widenings,
|
||||
_design_curve_spans,
|
||||
_design_curve_widenings,
|
||||
_plan_radii,
|
||||
)
|
||||
|
||||
radii, outer = _plan_radii(xy, chain, at, float(chain[-1]))
|
||||
widenings, sides = _curve_widenings(at, radii, outer)
|
||||
spans = _design_curve_spans(xy, chain, design_curves or [])
|
||||
if spans:
|
||||
radii, sides, widenings = _design_curve_widenings(at, spans)
|
||||
else:
|
||||
radii, outer = _plan_radii(xy, chain, at, float(chain[-1]))
|
||||
widenings, sides = _curve_widenings(at, radii, outer)
|
||||
return [
|
||||
{"plan_radius_m": r, "curve_outer_side": s, "curve_widening_m": w}
|
||||
for r, s, w in zip(radii, sides, widenings)
|
||||
]
|
||||
|
||||
|
||||
def _station_areas(
|
||||
samples: list[dict[str, Any]], mode: str, elevation: float, widen: dict[str, Any]
|
||||
) -> tuple[float, float, float]:
|
||||
"""측점 하나 · 계획고 하나의 (절토 자연 ㎡, 절토 다짐 환산 ㎡, 성토 ㎡) — 본 설계 기본 설계와
|
||||
같은 입력. 반폭에서 비탈이 안 닫히면 넓힌 샘플로 다시 셈(본 설계 `extend_unclosed`)."""
|
||||
from B06_Section.B06_Section_Engine_Design import compute_cross_design
|
||||
|
||||
def design(pick: list[dict[str, Any]]) -> dict[str, Any]:
|
||||
return compute_cross_design(
|
||||
pick,
|
||||
elevation,
|
||||
ground_type=GROUND_TYPE,
|
||||
section_mode=mode,
|
||||
rock_boundary_offset_m=STANDARD_ROCK_BOUNDARY_DEFAULT_OFFSET_M,
|
||||
**widen,
|
||||
)
|
||||
|
||||
narrow = [s for s in samples if abs(s["offset_m"]) <= CROSS_HALF_M + 1e-9]
|
||||
try:
|
||||
r = design(narrow)
|
||||
if r.get("slope_unclosed") and len(samples) > len(narrow):
|
||||
r = design(samples)
|
||||
except ValueError: # 지반 샘플이 모자란 측점 — 토량 모름(0)
|
||||
return 0.0, 0.0, 0.0
|
||||
factors = EARTHWORK_CONVERSION_FACTORS
|
||||
rock = factors.get(r.get("cut_rock_kind") or GROUND_TYPE, factors[GROUND_TYPE])
|
||||
return (
|
||||
r["cut_area_m2"],
|
||||
r["cut_soil_area_m2"] * factors["soil"]["compacted"]
|
||||
+ r["cut_rock_area_m2"] * rock["compacted"],
|
||||
r["fill_area_m2"],
|
||||
)
|
||||
|
||||
|
||||
def _area_curves(
|
||||
sections: list[list[dict[str, Any]]],
|
||||
modes: list[str],
|
||||
@@ -123,10 +192,7 @@ def _area_curves(
|
||||
levels: np.ndarray,
|
||||
curves: list[dict[str, Any]] | None = None,
|
||||
) -> tuple[np.ndarray, np.ndarray, np.ndarray]:
|
||||
"""측점 · 계획고 후보마다 (절토 자연 ㎡, 절토 다짐 환산 ㎡, 성토 ㎡)."""
|
||||
from B06_Section.B06_Section_Engine_Design import compute_cross_design
|
||||
|
||||
factors = EARTHWORK_CONVERSION_FACTORS
|
||||
"""측점 · 계획고 후보마다 (절토 자연 ㎡, 절토 다짐 ㎡, 성토 ㎡) — `AREA_STEP_M` 마디 보간."""
|
||||
shape = levels.shape
|
||||
cut, cut_c, fill = np.zeros(shape), np.zeros(shape), np.zeros(shape)
|
||||
for i, (samples, mode) in enumerate(zip(sections, modes)):
|
||||
@@ -134,29 +200,7 @@ def _area_curves(
|
||||
delta = levels[i] - ground[i]
|
||||
lo = np.floor(delta.min() / AREA_STEP_M) * AREA_STEP_M
|
||||
at = np.arange(lo, delta.max() + AREA_STEP_M, AREA_STEP_M)
|
||||
rows = []
|
||||
for d in at:
|
||||
try:
|
||||
r = compute_cross_design(
|
||||
samples,
|
||||
float(ground[i] + d),
|
||||
ground_type=GROUND_TYPE,
|
||||
section_mode=mode,
|
||||
**widen,
|
||||
)
|
||||
except ValueError: # 지반 샘플이 모자란 측점 — 토량 모름(0)
|
||||
rows.append((0.0, 0.0, 0.0))
|
||||
continue
|
||||
rock = factors.get(r.get("cut_rock_kind") or GROUND_TYPE, factors[GROUND_TYPE])
|
||||
rows.append(
|
||||
(
|
||||
r["cut_area_m2"],
|
||||
r["cut_soil_area_m2"] * factors["soil"]["compacted"]
|
||||
+ r["cut_rock_area_m2"] * rock["compacted"],
|
||||
r["fill_area_m2"],
|
||||
)
|
||||
)
|
||||
table = np.asarray(rows)
|
||||
table = np.asarray([_station_areas(samples, mode, float(ground[i] + d), widen) for d in at])
|
||||
# 단면적은 계획고에 대해 볼록(대략 2차)이라 직선 보간은 크게 잡힌다 — 단조 3차(PCHIP)
|
||||
if len(at) > 1:
|
||||
curve = PchipInterpolator(at, table, axis=0)(delta)
|
||||
@@ -219,10 +263,12 @@ def balanced_profile(
|
||||
max_grade: float,
|
||||
design_speed: int = 20,
|
||||
curves: list[dict[str, Any]] | None = None,
|
||||
exact: bool = True,
|
||||
) -> dict[str, Any]:
|
||||
"""상한(종단기울기 · 종단곡선)을 지키며 절 · 성토를 횡단 ㎥ 로 맞춘 계획선과 토량.
|
||||
|
||||
시점 · 종점은 지반고(확정 체인 기본 오프셋 0 과 같음) — 그 길이 없을 때만 풀어 다시 찾는다.
|
||||
`exact` 끄면 보간값 그대로(후보 줄 세우기 — 빠름) · 켜면 고른 선을 바로 셈해 누가토량을 맞춘다.
|
||||
"""
|
||||
started = time.perf_counter()
|
||||
spans = np.diff(chainage)
|
||||
@@ -242,12 +288,31 @@ def balanced_profile(
|
||||
room = 2.0 * FOREST_ROAD_PROFILE_ALIGNMENT["curve_tangent_max_ratio"] * float(spans.min())
|
||||
fits = criteria["min_curve_length_m"] <= room
|
||||
delta_max = max(skip, room / criteria["min_vertical_radius_m"]) if fits else skip
|
||||
tolerance = FOREST_ROAD_PROFILE_ALIGNMENT["balance_tolerance_percent"] / 100.0
|
||||
tolerance = ROUTE_RECOMMEND_MASS_TOLERANCE_M3
|
||||
pinned = np.full(levels.shape, np.inf)
|
||||
for end in (0, -1):
|
||||
pinned[end, int(np.argmin(np.abs(levels[end] - ground[end])))] = 0.0
|
||||
pinned[1:-1] = 0.0
|
||||
|
||||
def measured(result: dict[str, Any]) -> dict[str, Any]:
|
||||
# 보간은 비탈이 갑자기 안 닫히는 측점에서 크게 틀림(누가 ±1,400㎥) — 고른 선만 바로 셈
|
||||
plan = levels[np.arange(len(chainage)), result["path"]]
|
||||
rows = np.asarray(
|
||||
[
|
||||
_station_areas(sections[i], modes[i], float(plan[i]), curves[i] if curves else {})
|
||||
for i in range(len(chainage))
|
||||
]
|
||||
)
|
||||
a, c, f = (float((weight * rows[:, k]).sum()) for k in range(3))
|
||||
return {
|
||||
**result,
|
||||
"cut": a,
|
||||
"cut_c": c,
|
||||
"fill": f,
|
||||
"gap": f - c,
|
||||
"imbalance": (f - c) / max(c, f, 1e-9),
|
||||
}
|
||||
|
||||
def solve(lam: float) -> dict[str, Any] | None:
|
||||
base = cut_c + fill + lam * (fill - cut_c)
|
||||
path = _dp(levels, base + pinned, spans, max_grade, delta_max)
|
||||
@@ -263,6 +328,7 @@ def balanced_profile(
|
||||
"cut": float(cut[pick, path].sum()),
|
||||
"cut_c": c,
|
||||
"fill": f,
|
||||
"gap": f - c, # 누가토량 끝값의 반대 부호
|
||||
"imbalance": (f - c) / max(c, f, 1e-9),
|
||||
}
|
||||
|
||||
@@ -270,8 +336,8 @@ def balanced_profile(
|
||||
if tried[0] is None:
|
||||
raise ValueError("종단기울기 상한 안에서 계획고를 고를 수 없습니다.")
|
||||
first = tried[0]
|
||||
if abs(first["imbalance"]) > tolerance:
|
||||
sign = 1.0 if first["imbalance"] > 0 else -1.0
|
||||
if abs(first["gap"]) > tolerance:
|
||||
sign = 1.0 if first["gap"] > 0 else -1.0
|
||||
low, high = 0.0, LAMBDA_MAX
|
||||
for _ in range(LAMBDA_STEPS):
|
||||
mid = 0.5 * (low + high)
|
||||
@@ -279,16 +345,36 @@ def balanced_profile(
|
||||
if result is None:
|
||||
break
|
||||
tried.append(result)
|
||||
if abs(result["imbalance"]) <= tolerance or result["imbalance"] * sign < 0:
|
||||
if abs(result["gap"]) <= tolerance or result["gap"] * sign < 0:
|
||||
high = mid
|
||||
else:
|
||||
low = mid
|
||||
within = [r for r in tried if abs(r["imbalance"]) <= tolerance]
|
||||
within = [r for r in tried if abs(r["gap"]) <= tolerance]
|
||||
best = (
|
||||
min(within, key=lambda r: abs(r["lambda"]))
|
||||
if within
|
||||
else min(tried, key=lambda r: abs(r["imbalance"]))
|
||||
else min(tried, key=lambda r: abs(r["gap"]))
|
||||
)
|
||||
best = measured(best) if exact else best
|
||||
if exact and abs(best["gap"]) > tolerance: # 실제 누가토량으로 λ 를 더 좁힘
|
||||
start, sign = best["lambda"], 1.0 if best["gap"] > 0 else -1.0
|
||||
checked, low, high = [best], start, sign * LAMBDA_MAX
|
||||
for _ in range(EXACT_STEPS):
|
||||
result = solve(0.5 * (low + high))
|
||||
if result is None:
|
||||
break
|
||||
result = measured(result)
|
||||
checked.append(result)
|
||||
if abs(result["gap"]) <= tolerance or result["gap"] * sign < 0:
|
||||
high = result["lambda"]
|
||||
else:
|
||||
low = result["lambda"]
|
||||
within = [r for r in checked if abs(r["gap"]) <= tolerance]
|
||||
best = (
|
||||
min(within, key=lambda r: abs(r["lambda"] - start))
|
||||
if within
|
||||
else min(checked, key=lambda r: abs(r["gap"]))
|
||||
)
|
||||
plan = levels[np.arange(len(chainage)), best["path"]]
|
||||
grades = np.diff(plan) / spans
|
||||
changes = np.abs(np.diff(grades))
|
||||
@@ -302,8 +388,10 @@ def balanced_profile(
|
||||
"borrow_m3": round(max(best["fill"] - best["cut_c"], 0.0), 1),
|
||||
"spoil_m3": round(max(best["cut_c"] - best["fill"], 0.0), 1),
|
||||
"imbalance_pct": round(best["imbalance"] * 100.0, 1),
|
||||
"balanced": abs(best["imbalance"]) <= tolerance,
|
||||
"tolerance_pct": round(tolerance * 100.0, 1),
|
||||
"mass_end_m3": round(-best["gap"], 1), # 누가토량 끝값 = 절토(다짐) − 성토
|
||||
"balanced": abs(best["gap"]) <= tolerance,
|
||||
"mass_tolerance_m3": round(tolerance, 1),
|
||||
"exact": exact,
|
||||
"max_grade_pct": round(float(np.abs(grades).max()) * 100.0 if grades.size else 0.0, 2),
|
||||
"grade_limit_pct": round(max_grade * 100.0, 2),
|
||||
"curve_over": int((changes > delta_max + GRADE_EPS).sum()),
|
||||
@@ -315,10 +403,18 @@ def balanced_profile(
|
||||
|
||||
|
||||
def route_profile(
|
||||
vertices: list[tuple[float, float]], terrain: Terrain, *, max_grade: float, design_speed: int
|
||||
vertices: list[tuple[float, float]],
|
||||
terrain: Terrain,
|
||||
*,
|
||||
max_grade: float,
|
||||
design_speed: int,
|
||||
design_curves: list[dict[str, Any]] | None = None,
|
||||
exact: bool = True,
|
||||
) -> dict[str, Any]:
|
||||
"""후보 노선 하나의 균형 종단 — 측점 횡단을 뜨고 `balanced_profile`."""
|
||||
chainage, ground, sections, modes, curves = route_sections(vertices, terrain)
|
||||
"""후보 노선 하나의 균형 종단 — 측점 횡단을 뜨고 `balanced_profile`.
|
||||
|
||||
`design_curves` = 그 노선의 곡선표(`RouteCurve.as_dict` 꼴) — 확폭을 본 설계와 같게 매긴다."""
|
||||
chainage, ground, sections, modes, curves = route_sections(vertices, terrain, design_curves)
|
||||
if len(chainage) < 2:
|
||||
raise ValueError("노선이 짧아 종단을 세울 수 없습니다.")
|
||||
return balanced_profile(
|
||||
@@ -329,6 +425,7 @@ def route_profile(
|
||||
max_grade=max_grade,
|
||||
design_speed=design_speed,
|
||||
curves=curves,
|
||||
exact=exact,
|
||||
)
|
||||
|
||||
|
||||
@@ -384,24 +481,35 @@ def attach_profile(
|
||||
*,
|
||||
max_grade: float,
|
||||
design_speed: int,
|
||||
design_curves: list[dict[str, Any]] | None = None,
|
||||
exact: bool = True,
|
||||
) -> None:
|
||||
"""후보 비교값에 균형 종단을 붙이고 토공 점수를 그것으로 바꾼다(한 번만 · 묶음 캐시에 남음).
|
||||
"""후보 비교값에 균형 종단을 붙이고 토공 점수를 그것으로 바꾼다(묶음 캐시에 남음).
|
||||
|
||||
점수 = 절토(자연) + 성토 + 반입 + 사토 ㎥ — 가늠값(폭 4 m 띠 · 비탈면 뺌)은
|
||||
`earth_estimate_m3` 로 남기고, 공사비 어림의 토공 몫도 이 점수로 바꾼다."""
|
||||
if "profile" in metrics:
|
||||
`earth_estimate_m3` 로 남기고, 공사비 어림의 토공 몫도 이 점수로 바꾼다. 줄 세우기용
|
||||
(`exact` 끔)으로 붙은 것은 화면에 낼 때(`exact` 켬) 한 번 더 바로 셈해 갈아 끼운다."""
|
||||
held = metrics.get("profile")
|
||||
if held and ("error" in held or held.get("exact") or not exact):
|
||||
return
|
||||
try:
|
||||
profile = route_profile(vertices, terrain, max_grade=max_grade, design_speed=design_speed)
|
||||
profile = route_profile(
|
||||
vertices,
|
||||
terrain,
|
||||
max_grade=max_grade,
|
||||
design_speed=design_speed,
|
||||
design_curves=design_curves,
|
||||
exact=exact,
|
||||
)
|
||||
except ValueError as error:
|
||||
metrics["profile"] = {"error": str(error)}
|
||||
return
|
||||
estimate = metrics["cut_m3"] + metrics["fill_m3"]
|
||||
metrics["cost_index_base"] = metrics["cost_index"] # 「종단 반영」 끔이 되돌아갈 값
|
||||
metrics.setdefault("cost_index_base", metrics["cost_index"]) # 「종단 반영」 끔이 되돌아갈 값
|
||||
metrics["profile"] = profile
|
||||
metrics["earth_estimate_m3"] = round(estimate, 1)
|
||||
metrics["cost_index"] = round(
|
||||
metrics["cost_index"] + COST_EARTH_M3 * (profile_earth(profile) - estimate)
|
||||
metrics["cost_index_base"] + COST_EARTH_M3 * (profile_earth(profile) - estimate)
|
||||
)
|
||||
|
||||
|
||||
|
||||
Reference in New Issue
Block a user