Files
Aislo/B06_wf3_ProfileCross/B06_wf3_ProfileCross_Engine_Design.py
T
2026-07-21 20:12:44 +09:00

237 lines
9.0 KiB
Python

"""B06 측점 표준횡단 설계 계산 엔진.
지반유형(토사/리핑암/발파암)과 단면유형(좌절/우절/양절/양성)에 따라 표준횡단
설계선을 구성하고, 지반선과의 차이로 절·성토 단면적을 산출한다. B06에서 사용자가
버튼을 누를 때 즉시 호출되며, 여기서 나온 값은 잠정치로 저장되고 B07 상세설계에서
확정치로 대체된다.
좌표 규약(generate_sections cad_exchange 준수): offset_m 양수=좌, 음수=우.
경사비는 수평:수직 = ratio:1 (예: 1:1.2 → ratio=1.2).
"""
from typing import Any
from config.config_system import (
SECTION_CARRIAGEWAY_WIDTH_M,
SECTION_DESIGN_TEMPLATES,
SECTION_DITCH_SIDES,
SECTION_FILL_SLOPE_RATIO,
SECTION_GROUND_TYPE_PRESET,
SECTION_MODES,
SECTION_ROADBED_WIDTH_M,
)
def _side_role(section_mode: str) -> tuple[str, str]:
"""단면유형 → (좌측 역할, 우측 역할). 역할은 'cut' 또는 'fill'."""
if section_mode == "left_cut":
return "cut", "fill"
if section_mode == "right_cut":
return "fill", "cut"
if section_mode == "both_cut":
return "cut", "cut"
if section_mode == "both_fill":
return "fill", "fill"
raise ValueError(f"지원하지 않는 단면유형입니다: {section_mode}")
def _resolve_ditch_side(section_mode: str, ditch_side: str | None) -> str:
"""측구(배수) 배치 측을 결정한다.
편절편성은 절토측이 곧 측구측이라 자동 결정하고, 양절·양성은 배수 방향을
사용자 지정(ditch_side)에 맡긴다(미지정 시 좌측 기본).
"""
if section_mode == "left_cut":
return "left"
if section_mode == "right_cut":
return "right"
if ditch_side in SECTION_DITCH_SIDES:
return ditch_side
return "left"
def _design_elevation_on_side(
offset_m: float,
ground_m: float,
role: str,
design_elevation_m: float,
half_width_m: float,
cut_slope_ratio: float,
fill_slope_ratio: float,
) -> float:
"""노체 밖 한 offset의 설계 표고를 절토/성토 규칙으로 계산한다.
절토측: 노면 가장자리에서 경사면이 위로 올라가다 지반선을 만나면 지반을 따른다.
성토측: 가장자리에서 경사면이 아래로 내려가다 지반선을 만나면 지반을 따른다.
"""
edge_distance = abs(offset_m) - half_width_m
if role == "cut":
slope_line = design_elevation_m + edge_distance / cut_slope_ratio
return min(slope_line, ground_m)
fill_line = design_elevation_m - edge_distance / fill_slope_ratio
return max(fill_line, ground_m)
def _trapezoid_areas(offsets: list[float], diffs: list[float]) -> tuple[float, float]:
"""오프셋 순 (지반-설계) 차이를 사다리꼴 적분해 (절토, 성토) 면적을 반환한다.
diff>0(지반이 설계보다 높음)=절토, diff<0=성토. 부호가 바뀌는 구간은 영교점에서
나눠 절·성토가 섞이지 않게 한다.
"""
cut_area = 0.0
fill_area = 0.0
for index in range(1, len(offsets)):
x0, x1 = offsets[index - 1], offsets[index]
d0, d1 = diffs[index - 1], diffs[index]
width = x1 - x0
if width <= 0:
continue
if d0 == 0 and d1 == 0:
continue
if d0 * d1 < 0:
# 부호 변화: 영교점에서 두 삼각형으로 분리
zero_ratio = d0 / (d0 - d1)
x_zero = x0 + width * zero_ratio
left_area = 0.5 * (x_zero - x0) * abs(d0)
right_area = 0.5 * (x1 - x_zero) * abs(d1)
if d0 > 0:
cut_area += left_area
fill_area += right_area
else:
fill_area += left_area
cut_area += right_area
continue
area = 0.5 * (d0 + d1) * width
if area >= 0:
cut_area += area
else:
fill_area += -area
return cut_area, fill_area
def compute_cross_design(
samples: list[dict[str, Any]],
design_elevation_m: float | None,
*,
ground_type: str,
section_mode: str,
ditch_side: str | None = None,
roadbed_width_m: float = SECTION_ROADBED_WIDTH_M,
fill_slope_ratio: float = SECTION_FILL_SLOPE_RATIO,
) -> dict[str, Any]:
"""측점 하나의 표준횡단 설계선과 절·성토 단면적을 계산한다.
samples: [{offset_m, elevation_m, valid}] 지반선 원시 샘플.
design_elevation_m: 중심선 계획고(노면고). None이면 계산 불가.
"""
if ground_type not in SECTION_GROUND_TYPE_PRESET:
raise ValueError(f"지원하지 않는 지반유형입니다: {ground_type}")
if section_mode not in SECTION_MODES:
raise ValueError(f"지원하지 않는 단면유형입니다: {section_mode}")
if design_elevation_m is None:
raise ValueError("계획고(design_elevation_m)가 없어 횡단 설계를 계산할 수 없습니다.")
preset_key = SECTION_GROUND_TYPE_PRESET[ground_type]
preset = SECTION_DESIGN_TEMPLATES[preset_key]
cut_slope_ratio = float(preset["cut_slope_ratio"])
ditch_width_m = float(preset["ditch_width_m"])
ditch_depth_m = float(preset["ditch_depth_m"])
half_width_m = roadbed_width_m / 2.0
left_role, right_role = _side_role(section_mode)
resolved_ditch_side = _resolve_ditch_side(section_mode, ditch_side)
valid = sorted(
(
(float(s["offset_m"]), float(s["elevation_m"]))
for s in samples
if s.get("valid") is not False
and s.get("offset_m") is not None
and s.get("elevation_m") is not None
),
key=lambda pair: pair[0],
)
if len(valid) < 2:
raise ValueError("유효한 지반 샘플이 부족해 횡단 설계를 계산할 수 없습니다.")
offsets: list[float] = []
diffs: list[float] = []
design_line: list[dict[str, float]] = []
for offset_m, ground_m in valid:
if abs(offset_m) <= half_width_m + 1e-9:
design_z = design_elevation_m
else:
role = left_role if offset_m > 0 else right_role
design_z = _design_elevation_on_side(
offset_m,
ground_m,
role,
design_elevation_m,
half_width_m,
cut_slope_ratio,
fill_slope_ratio,
)
offsets.append(offset_m)
diffs.append(ground_m - design_z)
design_line.append({"offset_m": round(offset_m, 4), "elevation_m": round(design_z, 4)})
cut_area, fill_area = _trapezoid_areas(offsets, diffs)
# 측구는 절토측에서 굴착되므로 절토 단면적에 사다리꼴 근사로 가산한다.
ditch_area = ditch_depth_m * (ditch_width_m + ditch_width_m * 0.5) / 2.0
cut_area += ditch_area
return {
"ground_type": ground_type,
"geometry_preset": preset_key,
"section_mode": section_mode,
"ditch_side": resolved_ditch_side,
"cut_slope_ratio": round(cut_slope_ratio, 4),
"fill_slope_ratio": round(fill_slope_ratio, 4),
"roadbed_width_m": round(roadbed_width_m, 4),
"carriageway_width_m": round(SECTION_CARRIAGEWAY_WIDTH_M, 4),
"ditch": {"width_m": ditch_width_m, "depth_m": ditch_depth_m},
"design_elevation_m": round(float(design_elevation_m), 4),
"cut_area_m2": round(cut_area, 4),
"fill_area_m2": round(fill_area, 4),
"ditch_area_m2": round(ditch_area, 4),
"design_line": design_line,
}
def design_elevation_from_longitudinal(
longitudinal: dict[str, Any], chainage_m: float
) -> float | None:
"""종단 계획선(design_profiles) 샘플을 chainage 기준 선형보간해 계획고를 구한다.
프론트 designElevationAt과 동일 규칙(범위 밖 양 끝값 클램프). 계획선이 없으면
None을 반환해 지반고 폴백/오류 처리를 호출부에 맡긴다.
"""
profiles = longitudinal.get("design_profiles") if isinstance(longitudinal, dict) else None
if not isinstance(profiles, list) or not profiles:
return None
samples = [
s
for s in profiles[0].get("samples", [])
if isinstance(s.get("elevation_m"), (int, float))
and isinstance(s.get("chainage_m"), (int, float))
]
if not samples:
return None
if chainage_m <= samples[0]["chainage_m"]:
return float(samples[0]["elevation_m"])
last = samples[-1]
if chainage_m >= last["chainage_m"]:
return float(last["elevation_m"])
for index in range(1, len(samples)):
previous = samples[index - 1]
current = samples[index]
if chainage_m > current["chainage_m"]:
continue
span = current["chainage_m"] - previous["chainage_m"]
if span <= 0:
return float(current["elevation_m"])
ratio = (chainage_m - previous["chainage_m"]) / span
return float(
previous["elevation_m"] + (current["elevation_m"] - previous["elevation_m"]) * ratio
)
return float(last["elevation_m"])