diff --git a/B08_Quantity/B08_Quantity_Engine_Preparation.py b/B08_Quantity/B08_Quantity_Engine_Preparation.py index 35c64a9f..bcb52002 100644 --- a/B08_Quantity/B08_Quantity_Engine_Preparation.py +++ b/B08_Quantity/B08_Quantity_Engine_Preparation.py @@ -72,13 +72,23 @@ def batter_frame_count(slope_rows: Iterable[dict[str, Any]]) -> tuple[int, list[ return count, notes -def level_frame_count(slope_rows: Iterable[dict[str, Any]]) -> tuple[int, list[str]]: - """수평 규준틀 — **중심점 성토 높이 5m 이상**에 설치(품셈 11-3 [주]①). +def level_frame_count(slope_rows: Iterable[dict[str, Any]]) -> tuple[int | None, list[str]]: + """수평 규준틀 — **성토고 5m 이상 측점마다** 한 개소(품셈 11-3 [주]①). - ⚠ 성토고는 사면표에 없다 — 그 값을 받기 전에는 **0 으로 때우지 않고 미확보**로 둔다. + ⚠ 「비탈길이 10m 이상 20m마다」인 비탈규준틀과 **기준이 다르다** — 이쪽은 **지점 조건**이라 + 간격이 없다. 두 기준을 같은 식으로 쓰면 조용히 틀린다. + ⚠ 성토고 칸이 아예 없으면 **0 으로 때우지 않고** 미확보로 둔다 — 「없음」과 다르다. """ - return 0, [ - "중심점 성토고 5m 이상 지점에 설치(품셈 11-3 [주]①)인데 성토고가 이 표에 없어 개소를 못 셈" + rows = list(slope_rows) + if not rows: + return None, ["사면표가 없어 성토고를 못 봄"] + if all("fill_height_m" not in row for row in rows): + return None, ["성토고가 사면표에 없어 개소를 못 셈 (품셈 11-3 [주]① 「성토고 5m 이상」)"] + tall = [ + row for row in rows if float(row.get("fill_height_m") or 0.0) >= LEVEL_MIN_FILL_HEIGHT_M + ] + return len(tall), [ + f"성토고 {LEVEL_MIN_FILL_HEIGHT_M:g}m 이상 측점 {len(tall)}곳 (품셈 11-3 [주]①)" ] @@ -119,15 +129,7 @@ def preparation_rows( "work_item_code": "FP-09-21", }, _batter_frame_row(list(slope_rows)), - { - "group": "준비공", - "item": "수평 규준틀", - "unit": "개소", - "amount": None, - "status": STATUS_PENDING, - "reason": level_frame_count(())[1][0], - "work_item_code": "FP-11-03", - }, + _level_frame_row(list(slope_rows)), ] @@ -179,6 +181,25 @@ def _batter_frame_row(slope_rows: list[dict[str, Any]]) -> dict[str, Any]: } +def _level_frame_row(slope_rows: list[dict[str, Any]]) -> dict[str, Any]: + """수평 규준틀 한 줄. 성토고를 못 보면 미확보, 보면 개소가 선다.""" + count, notes = level_frame_count(slope_rows) + return { + "group": "준비공", + "item": "수평 규준틀", + "unit": "개소", + "amount": float(count) if count is not None else None, + "status": STATUS_READY if count is not None else STATUS_PENDING, + "reason": "; ".join(notes) + + ( + " · 재료량은 품셈 11-3 [주]④ 「설계수량에 따른다」라 미확보" + if count is not None + else "" + ), + "work_item_code": "FP-11-03", + } + + def erosion_rows(structures: Iterable[dict[str, Any]] = ()) -> list[dict[str, Any]]: """사방공 줄 — 이 노선에 사방 시설이 **있을 때만** 값이 선다.""" found = sorted( diff --git a/B08_Quantity/B08_Quantity_Engine_SlopeArea.py b/B08_Quantity/B08_Quantity_Engine_SlopeArea.py index d5df001e..47d92d1e 100644 --- a/B08_Quantity/B08_Quantity_Engine_SlopeArea.py +++ b/B08_Quantity/B08_Quantity_Engine_SlopeArea.py @@ -69,6 +69,8 @@ class SlopeAreaRow: chainage_m: float distance_m: float = 0.0 berm_width_m: float = 0.0 + # 성토고(m) — 수평 규준틀 개소 판정(품셈 11-3 [주]① 「성토고 5m 이상」)이 쓴다. + fill_height_m: float = 0.0 unclosed: bool = False lengths: dict[str, float] = field(default_factory=dict) areas: dict[str, float] = field(default_factory=dict) @@ -102,6 +104,7 @@ def build_rows( row = SlopeAreaRow( chainage_m=slope.chainage_m, berm_width_m=slope.berm_width_m, + fill_height_m=slope.fill_height_m, unclosed=slope.unclosed, ) for series, faces in SERIES: @@ -153,6 +156,7 @@ def build_table( "chainage_m": row.chainage_m, "distance_m": row.distance_m, "berm_width_m": row.berm_width_m, + "fill_height_m": row.fill_height_m, "unclosed": row.unclosed, "lengths": row.lengths, "areas": row.areas, diff --git a/B08_Quantity/B08_Quantity_Engine_SlopeLength.py b/B08_Quantity/B08_Quantity_Engine_SlopeLength.py index 8f6a3d28..e7497743 100644 --- a/B08_Quantity/B08_Quantity_Engine_SlopeLength.py +++ b/B08_Quantity/B08_Quantity_Engine_SlopeLength.py @@ -59,6 +59,10 @@ class StationSlope: cut_length_m: float = 0.0 fill_length_m: float = 0.0 berm_width_m: float = 0.0 + # 성토고(m) — 성토 사면 조각들의 **수직 낙차 합**. 노면 끝에서 원지반까지 내려간 높이다. + # ⚠ 좌우가 다르면 **큰 쪽**을 쓴다. 「중심점 성토고 5m 이상」(품셈 11-3 [주]①) 판정은 + # 가장 높은 쪽이 기준이고, 양쪽을 더하면 실제보다 두 배가 된다. + fill_height_m: float = 0.0 segments: tuple[SlopeSegment, ...] = () # 사면이 샘플 범위 끝까지 원지반을 못 만나 **면적이 잘린** 측점. # 설계 엔진이 `slope_unclosed` 로 이미 경고하는 값을 그대로 물고 온다. 잘린 측점은 @@ -204,10 +208,15 @@ def station_slope(chainage_m: float, design: dict[str, Any]) -> StationSlope: for side in ("left", "right"): segments.extend(_side_segments(design, side, ratios)) berm = design.get("berm") or {} + fill_by_side = { + side: sum(abs(s.rise_m) for s in segments if s.role == "fill" and s.side == side) + for side in ("left", "right") + } return StationSlope( chainage_m=float(chainage_m), cut_length_m=sum(s.length_m for s in segments if s.role == "cut"), fill_length_m=sum(s.length_m for s in segments if s.role == "fill"), + fill_height_m=max(fill_by_side.values(), default=0.0), berm_width_m=_num(berm.get("width_m")) or 0.0, segments=tuple(segments), unclosed=bool(design.get("slope_unclosed")),