237 lines
9.0 KiB
Python
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"])
|