fix(계산묶음): 349-0p 초안 새 측점 암 경계 = 서버 기본값 — cross 단계가 입력 없는 측점에 기본 오프셋을 받음 · 소수 자리 관 측점 시험
- 검증_브레인232 를 검증_sub2b(abdc72d0 · 노선 330)로 복제해 같은 끌기(25+0 부근 꺾임점 북동 10 m)로 서버 [저장] 체인 실측: 미리보기 1,169.6 대 정본 1,184.0 · 갈린 곳은 960 측점 하나(초안에서 격자 열쇠라 저장 입력 없음 → 암 경계 비어 흙 · 암 나눔이 다름) - CrossStageInput.rockBoundaryDefaultM — 입력 없는 측점의 암 경계(서버 config 기본값) · 넘기면 미리보기 1,184.7 대 정본 1,184.0 - 넘기는 줄(design_pipeline_cross.ts `readRockBoundaryDefault(projectId)`)은 남의 도움 파일 가짜 모듈을 깨서 브레인 몫으로 남김 - 시험: test_349_0p_fractional_station 2(소수 자리 관 · 물넘이 · 세월교 측점 = 서버 보정 뒤 · 입력 없는 측점 암 경계) · 관련 97 통과 · typecheck 0 Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01QfHKQs7J3hQELKMWQMUJEy
This commit is contained in:
@@ -46,6 +46,7 @@ export type SectionSpecs = Partial<
|
|||||||
export interface CrossInput {
|
export interface CrossInput {
|
||||||
standard: unknown;
|
standard: unknown;
|
||||||
byKey: Record<string, Record<string, unknown>>;
|
byKey: Record<string, Record<string, unknown>>;
|
||||||
|
rockBoundaryDefaultM?: number | null;
|
||||||
}
|
}
|
||||||
|
|
||||||
const mean = (values: number[]): number => values.reduce((sum, v) => sum + v, 0) / values.length;
|
const mean = (values: number[]): number => values.reduce((sum, v) => sum + v, 0) / values.length;
|
||||||
@@ -76,7 +77,7 @@ export function crossWithUphill(
|
|||||||
sectionMode: `${uphillSide(station) ?? "left"}_cut`,
|
sectionMode: `${uphillSide(station) ?? "left"}_cut`,
|
||||||
...input.byKey[station.key],
|
...input.byKey[station.key],
|
||||||
};
|
};
|
||||||
return { standard: input.standard, byKey };
|
return { ...input, byKey };
|
||||||
}
|
}
|
||||||
|
|
||||||
const kindOf = (station: PipelineStation): SectionStation["kind"] =>
|
const kindOf = (station: PipelineStation): SectionStation["kind"] =>
|
||||||
|
|||||||
@@ -47,6 +47,8 @@ export type CrossStationInput = Partial<
|
|||||||
export interface CrossStageInput {
|
export interface CrossStageInput {
|
||||||
standard: StandardCrossSectionSpec;
|
standard: StandardCrossSectionSpec;
|
||||||
byKey: Record<string, CrossStationInput>;
|
byKey: Record<string, CrossStationInput>;
|
||||||
|
/** 암 경계 기본 오프셋(m · 서버 config) — 입력이 없는 측점(초안 새 측점)도 서버처럼 이 값(349-0p). */
|
||||||
|
rockBoundaryDefaultM?: number | null;
|
||||||
}
|
}
|
||||||
|
|
||||||
/** 측점 하나의 엔진 입력. */
|
/** 측점 하나의 엔진 입력. */
|
||||||
@@ -113,6 +115,8 @@ export function createCrossStage(deps: CrossStageDeps = {}) {
|
|||||||
Array.isArray(pipes) && Array.isArray(structures) ? facilityRanges(pipes, structures) : null;
|
Array.isArray(pipes) && Array.isArray(structures) ? facilityRanges(pipes, structures) : null;
|
||||||
const optionsOf = (station: PipelineStation): CrossDesignOptions => ({
|
const optionsOf = (station: PipelineStation): CrossDesignOptions => ({
|
||||||
...optionsFor(station, input.byKey[station.key], input.standard),
|
...optionsFor(station, input.byKey[station.key], input.standard),
|
||||||
|
rockBoundaryOffsetM:
|
||||||
|
input.byKey[station.key]?.rockBoundaryOffsetM ?? input.rockBoundaryDefaultM ?? null,
|
||||||
...(ranges ? facilityAt(ranges, station.chainageM) : {}),
|
...(ranges ? facilityAt(ranges, station.chainageM) : {}),
|
||||||
});
|
});
|
||||||
for (const station of ground.stations) {
|
for (const station of ground.stations) {
|
||||||
|
|||||||
@@ -49,7 +49,11 @@ function runStage(facilities) {
|
|||||||
generation: 1,
|
generation: 1,
|
||||||
range: null,
|
range: null,
|
||||||
inputs: {
|
inputs: {
|
||||||
cross: { standard: job.standard, byKey: job.byKey },
|
cross: {
|
||||||
|
standard: job.standard,
|
||||||
|
byKey: job.byKey,
|
||||||
|
rockBoundaryDefaultM: job.rockBoundaryDefaultM ?? null,
|
||||||
|
},
|
||||||
pipes: facilities ? job.pipes : null,
|
pipes: facilities ? job.pipes : null,
|
||||||
structures: facilities ? job.structures : null,
|
structures: facilities ? job.structures : null,
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -93,8 +93,8 @@ def _elevation(chainage: float) -> float:
|
|||||||
return round(100 + 0.6 * math.sin(chainage / 53) + 0.002 * chainage, 4)
|
return round(100 + 0.6 * math.sin(chainage / 53) + 0.002 * chainage, 4)
|
||||||
|
|
||||||
|
|
||||||
def _stations() -> list[dict]:
|
def _stations(pipes: list[dict] = PIPES) -> list[dict]:
|
||||||
pipe_at = {p["chainage_m"]: f"pipe:p{i}" for i, p in enumerate(PIPES)}
|
pipe_at = {p["chainage_m"]: f"pipe:p{i}" for i, p in enumerate(pipes)}
|
||||||
chainages = sorted({20.0 * i for i in range(21)} | set(pipe_at))
|
chainages = sorted({20.0 * i for i in range(21)} | set(pipe_at))
|
||||||
out = []
|
out = []
|
||||||
for c in chainages:
|
for c in chainages:
|
||||||
|
|||||||
@@ -0,0 +1,176 @@
|
|||||||
|
"""PLAN 349-0p — 관 · 구조물 측점이 소수 자리(386.6 · 150.3)인 노선 꼴 · 입력 없는 측점 암 경계.
|
||||||
|
|
||||||
|
· 세월교 · 물넘이 · 관이 소수 자리 누가거리에 선 노선 — cross 단계 결과(포장 · 노면 하강 · 기울임 ·
|
||||||
|
면적 · 흙 · 암 나눔) = 서버 [확정] 보정 뒤 설계
|
||||||
|
· 저장 입력이 없는 측점(초안 새 측점) — 암 경계 = 기본 오프셋(서버 config · 화면이 서버 문맥에서
|
||||||
|
기억한 값) · 기본값이 안 오면 옛 그대로(경계 없음 = 깎기 전부 암)
|
||||||
|
짝: `helper_349_0n_cross_enforce.cjs` · 틀 = `test_349_0n_cross_enforce_mirror.py`.
|
||||||
|
"""
|
||||||
|
|
||||||
|
from __future__ import annotations
|
||||||
|
|
||||||
|
import json
|
||||||
|
import subprocess
|
||||||
|
import sys
|
||||||
|
from pathlib import Path
|
||||||
|
|
||||||
|
import pytest
|
||||||
|
|
||||||
|
HERE = Path(__file__).resolve().parent
|
||||||
|
ROOT = HERE.parents[1]
|
||||||
|
for path in (ROOT, HERE):
|
||||||
|
if str(path) not in sys.path:
|
||||||
|
sys.path.insert(0, str(path))
|
||||||
|
|
||||||
|
from test_349_0n_cross_enforce_mirror import OFFSETS, _stations, _z # noqa: E402
|
||||||
|
|
||||||
|
import B05_Profile.B05_Profile_Structures_Repository as structures_repo # noqa: E402
|
||||||
|
import B06_Section.B06_Section_Engine_Tilt as tilt_mod # noqa: E402
|
||||||
|
import B06_Section.B06_Section_Router_Design as design_mod # noqa: E402
|
||||||
|
from B05_Profile.B05_Profile_Structures_Schema import StructureInstance # noqa: E402
|
||||||
|
from B06_Section.B06_Section_Engine_Culvert import culvert_sets_from # noqa: E402
|
||||||
|
from B06_Section.B06_Section_Engine_Design import compute_cross_design # noqa: E402
|
||||||
|
from common_util.common_util_drainage_pipes import parse_pipe_points # noqa: E402
|
||||||
|
from config.config_system_design import ( # noqa: E402
|
||||||
|
STANDARD_CROSS_SECTION,
|
||||||
|
STANDARD_ROCK_BOUNDARY_DEFAULT_OFFSET_M,
|
||||||
|
)
|
||||||
|
|
||||||
|
pytestmark = pytest.mark.skipif(
|
||||||
|
not (ROOT / "config" / "node_modules" / "typescript").is_dir(),
|
||||||
|
reason="프론트엔드 의존성 미설치",
|
||||||
|
)
|
||||||
|
|
||||||
|
PIPES = [
|
||||||
|
{"chainage_m": 150.3, "options": {"pipe_diameter_mm": 800}},
|
||||||
|
{
|
||||||
|
"chainage_m": 320.4,
|
||||||
|
"facility": "ford_pavement",
|
||||||
|
"options": {"ford_width_m": 12, "ford_slope_deg": 4},
|
||||||
|
},
|
||||||
|
{"chainage_m": 386.6, "facility": "ford_bridge", "options": {"ford_height_m": 0.25}},
|
||||||
|
]
|
||||||
|
STRUCTURES = [
|
||||||
|
StructureInstance(
|
||||||
|
structure_id="pv1",
|
||||||
|
type_id="gravel_surfacing",
|
||||||
|
placement="interval",
|
||||||
|
start_m=40.3,
|
||||||
|
end_m=80.6,
|
||||||
|
),
|
||||||
|
]
|
||||||
|
OWN_ROCK = -1.0 # 저장 입력이 있는 측점(짝수 격자)만 — 나머지는 초안 새 측점처럼 입력 없음
|
||||||
|
|
||||||
|
|
||||||
|
def _inputs(stations: list[dict]) -> dict:
|
||||||
|
return {
|
||||||
|
s["key"]: {"sectionMode": "left_cut", "rockBoundaryOffsetM": OWN_ROCK}
|
||||||
|
for s in stations
|
||||||
|
if s["key"].startswith("grid:") and int(s["key"][5:]) % 2 == 0
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
def _run(tmp: Path, name: str, default: float | None) -> dict:
|
||||||
|
stations = _stations(PIPES)
|
||||||
|
job = {
|
||||||
|
"pipes": PIPES,
|
||||||
|
"structures": [s.model_dump(mode="json") for s in STRUCTURES],
|
||||||
|
"stations": stations,
|
||||||
|
"probe": [0],
|
||||||
|
"standard": STANDARD_CROSS_SECTION,
|
||||||
|
"byKey": _inputs(stations),
|
||||||
|
"rockBoundaryDefaultM": default,
|
||||||
|
}
|
||||||
|
path = tmp / f"{name}.json"
|
||||||
|
path.write_text(json.dumps(job), encoding="utf-8")
|
||||||
|
proc = subprocess.run( # noqa: S603 — 고정 실행 파일
|
||||||
|
["node", str(HERE / "helper_349_0n_cross_enforce.cjs"), str(path)],
|
||||||
|
capture_output=True,
|
||||||
|
text=True,
|
||||||
|
encoding="utf-8",
|
||||||
|
timeout=120,
|
||||||
|
)
|
||||||
|
assert proc.returncode == 0, proc.stderr
|
||||||
|
return json.loads(proc.stdout)["stage"]["byKey"]
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.fixture(scope="module")
|
||||||
|
def r(tmp_path_factory) -> dict:
|
||||||
|
tmp = tmp_path_factory.mktemp("frac")
|
||||||
|
return {
|
||||||
|
"stations": _stations(PIPES),
|
||||||
|
"with": _run(tmp, "with", STANDARD_ROCK_BOUNDARY_DEFAULT_OFFSET_M),
|
||||||
|
"without": _run(tmp, "without", None),
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.fixture(scope="module")
|
||||||
|
def server(r) -> list[dict]:
|
||||||
|
"""서버 [확정] — 저장분(입력 없는 측점은 config 암 경계) → 포장 구간 → 세월교 하강."""
|
||||||
|
patch = pytest.MonkeyPatch()
|
||||||
|
sets = culvert_sets_from(parse_pipe_points(PIPES))
|
||||||
|
for module in (design_mod, tilt_mod):
|
||||||
|
patch.setattr(module, "load_culvert_sets", lambda _root: sets)
|
||||||
|
patch.setattr(module, "load_structures", lambda _root: (0, STRUCTURES))
|
||||||
|
patch.setattr(structures_repo, "load_structures", lambda _root: (0, STRUCTURES))
|
||||||
|
own = _inputs(r["stations"])
|
||||||
|
longitudinal = {
|
||||||
|
"design_profiles": [
|
||||||
|
{
|
||||||
|
"samples": [
|
||||||
|
{"chainage_m": s["chainageM"], "elevation_m": s["elevation"]}
|
||||||
|
for s in r["stations"]
|
||||||
|
]
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
sections = []
|
||||||
|
for s in r["stations"]:
|
||||||
|
samples = [
|
||||||
|
{"offset_m": o, "elevation_m": _z(o, s["chainageM"]), "valid": True} for o in OFFSETS
|
||||||
|
]
|
||||||
|
rock = OWN_ROCK if s["key"] in own else STANDARD_ROCK_BOUNDARY_DEFAULT_OFFSET_M
|
||||||
|
design = compute_cross_design(
|
||||||
|
samples,
|
||||||
|
s["elevation"],
|
||||||
|
ground_type="ripping_rock",
|
||||||
|
section_mode="left_cut",
|
||||||
|
ditch_type="standard",
|
||||||
|
standard=STANDARD_CROSS_SECTION,
|
||||||
|
rock_boundary_offset_m=rock,
|
||||||
|
)
|
||||||
|
sections.append({"chainage_m": s["chainageM"], "samples": samples, "design": design})
|
||||||
|
try:
|
||||||
|
root = Path("unused")
|
||||||
|
design_mod.enforce_pavement_ranges(longitudinal, sections, root, STANDARD_CROSS_SECTION)
|
||||||
|
design_mod.enforce_ford_surface_drops(longitudinal, sections, root, STANDARD_CROSS_SECTION)
|
||||||
|
finally:
|
||||||
|
patch.undo()
|
||||||
|
return sections
|
||||||
|
|
||||||
|
|
||||||
|
def test_소수_자리_측점_결과_서버와_같음(r, server):
|
||||||
|
keys = ("cut_area_m2", "cut_soil_area_m2", "cut_rock_area_m2", "fill_area_m2")
|
||||||
|
for station, section in zip(r["stations"], server, strict=True):
|
||||||
|
a, b = r["with"][station["key"]], section["design"]
|
||||||
|
for key in keys:
|
||||||
|
assert a[key] == pytest.approx(b[key], abs=2e-3), (station["key"], key)
|
||||||
|
for key in ("paved", "tilted", "surface_drop_m", "rock_boundary_offset_m"):
|
||||||
|
assert a.get(key) == b.get(key), (station["key"], key)
|
||||||
|
by = {s["key"]: s["chainageM"] for s in r["stations"]}
|
||||||
|
assert r["with"]["pipe:p2"]["surface_drop_m"] == 0.25 and by["pipe:p2"] == 386.6
|
||||||
|
assert r["with"]["pipe:p1"]["tilted"] and r["with"]["grid:16"]["paved"] # 320.4 ± 6
|
||||||
|
assert r["with"]["grid:4"]["paved"] and not r["with"]["grid:2"]["paved"] # 40.3 ~ 80.6
|
||||||
|
|
||||||
|
|
||||||
|
def test_입력_없는_측점_암_경계_기본값(r):
|
||||||
|
rock = [
|
||||||
|
s["key"]
|
||||||
|
for s in r["stations"]
|
||||||
|
if r["with"][s["key"]]["cut_area_m2"] > 0.01 and s["key"] not in _inputs(r["stations"])
|
||||||
|
]
|
||||||
|
assert rock # 깎기 있는 새 측점이 실제로 있음
|
||||||
|
for key in rock:
|
||||||
|
assert r["with"][key]["rock_boundary_offset_m"] == STANDARD_ROCK_BOUNDARY_DEFAULT_OFFSET_M
|
||||||
|
assert r["without"][key]["cut_soil_area_m2"] == 0 # 기본값이 안 오면 옛 그대로(전부 암)
|
||||||
|
assert any(r["with"][key]["cut_rock_area_m2"] > 0 for key in rock)
|
||||||
Reference in New Issue
Block a user