Files
Aislo/resources/tester/test_haul_plan_quantity_unpruned.py
T
eomsangdonandClaude Opus 5 20597071bd fix(b06): 유토 배분 수량은 거르지 않은 계획 · 잔진동 거르기는 그림에만(브레인 ①)
- computeHaulPlan 기본 = 거르지 않음, drawing: true 일 때만 진폭 2% 거르기
- 서버 정본 두 벌 — haul_plan(수량·B08) · haul_plan_drawing(토적도) · 화면 선반입은 그림용
- B07 토적도는 그림용 먼저, 옛 저장분은 haul_plan
- 936be972 실측: 운반 0 → 245.81㎥(7블록 15띠) · 토취 11,100.86 그대로 · 내역 154,857,626 → 155,207,971

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_016VBGFXB9AbJBwXP19z75Qq
2026-09-14 23:13:48 +09:00

200 lines
7.5 KiB
Python
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
"""유토 배분 — **수량은 거르지 않은 계획**, 잔진동 거르기는 **그림에만** (2026-09-14 브레인 ①).
실측(936be972 · ㉳ 샘플 넓힘 뒤): 성토가 늘어 곡선 진폭이 커지자 「진폭 × 2%」 거르기가
작은 절토 봉우리를 통째로 지워 **운반량 474.14㎥ → 0** 이 됐다. 거르기는 balloon 이
수십 개 깔리는 것을 막으려고 둔 **도면용 손질**이라 수량에 닿으면 안 된다.
① `computeHaulPlan` 기본은 거르지 않는다 — 작은 봉우리도 블록(운반)으로 남는다.
② `drawing: true` 일 때만 거른다 — 그림은 종전대로 깔끔하다.
③ 서버 정본은 둘을 따로 둔다 — `haul_plan`(수량) · `haul_plan_drawing`(토적도·화면).
④ B07 토적도는 그림용을 먼저 읽는다(없으면 옛 저장분 `haul_plan`).
"""
from __future__ import annotations
import json
import re
import subprocess
from pathlib import Path
import pytest
from B07_DesignDetail.B07_DesignDetail_Engine_Cad_MassHaul import build_mass_haul_drawing
PROJECT_ROOT = Path(__file__).resolve().parents[2]
TSC = PROJECT_ROOT / "config" / "node_modules" / "typescript" / "bin" / "tsc"
_RUNNER = """
import { readFileSync, writeFileSync } from "node:fs";
import { computeHaulPlan } from "./common_util_mass_haul_balance.js";
const [inputPath, outputPath] = process.argv.slice(2);
const input = JSON.parse(readFileSync(inputPath, "utf8"));
const out = input.cases.map((options) => {
const plan = computeHaulPlan(input.result, null, options);
return plan && {
hauled_m3: plan.hauled_m3, borrow_m3: plan.borrow_m3, blocks: plan.blocks.length,
};
});
writeFileSync(outputPath, JSON.stringify(out));
"""
def _bump_then_fill() -> dict:
"""앞에 작은 절토 봉우리(+10㎥) · 뒤에 큰 성토(1,000㎥).
진폭 2% 는 20㎥ 라 그림용 거르기에서는 봉우리가 지워진다."""
nets = [0.0, 10.0, -10.0] + [-100.0] * 10
points, cumulative = [], 0.0
for index, net in enumerate(nets):
cumulative += net
points.append(
{
"station_id": f"S{index:03d}",
"chainage_m": index * 20.0,
"net_volume_m3": net,
"cumulative_volume_m3": cumulative,
"cut_soil_m3": max(net, 0.0),
"cut_rock_m3": 0.0,
"cut_rr_m3": 0.0,
"cut_br_m3": 0.0,
"cut_compacted_m3": max(net, 0.0),
"fill_m3": max(-net, 0.0),
"natural_spoil": False,
"net_area_m2": net / 20.0,
}
)
return {
"points": points,
"cut_natural_m3": {"ea": 10.0, "rr": 0.0, "br": 0.0},
"cut_compacted_m3": 10.0,
"fill_compacted_m3": 1010.0,
"final_cumulative_m3": cumulative,
"surplus_m3": 0.0,
"shortage_m3": -cumulative,
"min_cumulative_m3": cumulative,
"max_cumulative_m3": 10.0,
"conversion": {"soil": 1.0, "ripping_rock": 1.0, "blasting_rock": 1.0},
}
def _run(tmp_path: Path, cases: list[dict]) -> list[dict | None]:
out = tmp_path / "js"
subprocess.run( # noqa: S603 — 고정 실행 파일
[
"node",
str(TSC),
str(PROJECT_ROOT / "common_util" / "common_util_mass_haul_balance.ts"),
"--outDir",
str(out),
"--module",
"esnext",
"--target",
"es2022",
"--moduleResolution",
"bundler",
"--ignoreConfig",
],
cwd=str(PROJECT_ROOT),
check=True,
capture_output=True,
)
for emitted in out.glob("*.js"):
text = emitted.read_text(encoding="utf-8")
emitted.write_text(
re.sub(r'(from "\./[^"]+?)(")', lambda m: m.group(1) + ".js" + m.group(2), text),
encoding="utf-8",
)
(out / "runner.mjs").write_text(_RUNNER, encoding="utf-8")
payload = tmp_path / "input.json"
result = tmp_path / "output.json"
payload.write_text(json.dumps({"result": _bump_then_fill(), "cases": cases}), encoding="utf-8")
subprocess.run( # noqa: S603
["node", str(out / "runner.mjs"), str(payload), str(result)],
cwd=str(PROJECT_ROOT),
check=True,
capture_output=True,
)
return json.loads(result.read_text(encoding="utf-8"))
@pytest.mark.skipif(not TSC.is_file(), reason="프론트엔드 의존성 미설치")
def test_수량_계획은_작은_봉우리도_운반으로_센다(tmp_path: Path) -> None:
quantity, drawing = _run(tmp_path, [{}, {"drawing": True}])
assert quantity is not None and drawing is not None
# ① 기본(수량) — 봉우리 10㎥ 가 뒤 성토로 옮겨진다.
assert quantity["hauled_m3"] == pytest.approx(10.0)
# ② 그림 — 종전대로 걸러져 블록이 없다.
assert drawing["blocks"] == 0
assert drawing["hauled_m3"] == pytest.approx(0.0)
# 토취(순 부족분)는 거르기와 무관하게 같다 — 운반만 사라졌던 것이다.
assert quantity["borrow_m3"] == pytest.approx(drawing["borrow_m3"])
def test_서버_정본은_수량과_그림을_따로_둔다() -> None:
source = (PROJECT_ROOT / "B06_Section" / "B06_Section_Server_Calc_Node.ts").read_text(
encoding="utf-8"
)
# ③ 화면 선반입(그림)은 거른 계획, 저장 정본은 둘 다.
assert "drawing: true" in source
assert "haul_plan_drawing" in source
def test_토적도는_그림용_계획을_먼저_그린다() -> None:
def band(volume: float) -> dict:
return {
"index": 1,
"equipment": "free_haul",
"volume_m3": volume,
"haul_distance_m": 10.0,
"ea_m3": volume,
"rr_m3": 0.0,
"br_m3": 0.0,
"level_base_m3": 0.0,
"level_apex_m3": volume,
"boundary_from_m": 0.0,
"boundary_to_m": 40.0,
"haul_from_m": 10.0,
"haul_to_m": 30.0,
}
def plan(volume: float) -> dict:
block = {
"index": 1,
"from_m": 0.0,
"to_m": 40.0,
"base_m3": 0.0,
"volume_m3": volume,
"direction": "forward",
"bands": [band(volume)],
}
return {"blocks": [block], "residuals": [], "transfers": []}
longitudinal = {"stations": [{"chainage_m": x, "station_id": f"s{x}"} for x in (0, 20, 40)]}
points = [
{"station_id": "s0", "chainage_m": 0.0, "cumulative_volume_m3": 0.0},
{"station_id": "s20", "chainage_m": 20.0, "cumulative_volume_m3": 700.0},
{"station_id": "s40", "chainage_m": 40.0, "cumulative_volume_m3": 0.0},
]
mass_haul = {"points": points, "haul_plan": plan(111.0), "haul_plan_drawing": plan(222.0)}
def labels(drawing: dict) -> list[str]:
out: list[str] = []
def walk(entity: dict) -> None:
if entity.get("type") == "Text":
out.append(entity["shapeData"]["label"])
for child in entity.get("children") or []:
walk(child)
for entity in drawing["entities"]:
walk(entity)
return out
drawn = labels(build_mass_haul_drawing(longitudinal, mass_haul, "mass_haul"))
assert "Q= 222.00M3" in drawn and "Q= 111.00M3" not in drawn
# 옛 저장분(그림용 없음)은 종전대로 `haul_plan` 을 그린다.
del mass_haul["haul_plan_drawing"]
legacy = labels(build_mass_haul_drawing(longitudinal, mass_haul, "mass_haul"))
assert "Q= 111.00M3" in legacy