feat(511-B B-4 [저장] 횡단 한 바퀴 · 형상 얹기 Node): [저장] · [확정] 서버 다시 셈의 횡단 한 바퀴(설계선 다시 냄 → 미교차 넓힘 → 포장 구간 → 세월교 하강 · G2 C3) = DTM 이면 늘 떠 있는 Node cross.enforce 한 부름(logic/B06_Section_Cross_Save.ts · 파이썬 차례 · 승계 · 입력 그대로 · 격자 = npz 곁 이진 · 넓힌 표본 = 횡단 파일) · DTM 아닌 지표면 · Node 못 부름 = 파이썬 길 · GET 상세 · 재생성 형상 얹기(F4 F5 F6 C6) = Node facility.attach(측점 자리만 보냄 · 구조물 정의 M02 반영 넘김 · 못 부르면 파이썬 짝) · 구조물 정본 못 읽음 = 소단 저장분 그대로 · 등록부 G2 C3 C6 F4 node: · 거울 test_511_b4_cross_save_mirror 4(가짜 지형 넓힘 · 지표면 끝 · 포장 · 기울임 · 세월교 · 소단 · 파이썬 길 = Node 길 0 차) · 검증_sub2 [저장] · [확정] 값 차 0 · 다시 셈 0.7 → 0.5 s · [확정] 2.2 → 2.1 s · GET 형상 얹기 +5.6 ms — 요구 320-4
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
@@ -16,6 +16,7 @@
|
||||
* chainage_m? }] · facilities?: { pipes · structures }(시설 구간 · 511-B B-4) } ·
|
||||
* berm?: { structures: 구조물 정본 목록 · chainages: [측점 m] } ·
|
||||
* rebase? · carry?(노선 바뀐 [저장] · `design_calc_node_carry.ts`) ·
|
||||
* definition? · attach? · cross_enforce?(형상 얹기 · [저장] 횡단 · `design_calc_node_cross.ts` · 511-B B-4) ·
|
||||
* stages?(묶음 단계 · `design_calc_node_stages.ts` · 511-B) } }
|
||||
* 출력 { results: { [id]: 값 }, available: [id…] }
|
||||
* — 차례는 요청 차례가 아니라 기댐 차례(누가토량 앞에 구조물 면적을 상세에 얹음 ·
|
||||
@@ -49,13 +50,14 @@ import {
|
||||
structureAreaRows,
|
||||
} from "../../../B06_Section/B06_Section_Structure_Layouts";
|
||||
import { carryRunners, type CarryInputs } from "./design_calc_node_carry";
|
||||
import { crossRunners, useDefinition, type CrossInputs } from "./design_calc_node_cross";
|
||||
import { followRunners, type FollowInputs } from "./design_calc_node_follow";
|
||||
import { STAGE_IDS, stageRunners, type StagesInputs } from "./design_calc_node_stages";
|
||||
import { TRIAL_IDS, trialRunners, type TrialInputs } from "./design_calc_node_trial";
|
||||
|
||||
type HaulOptions = NonNullable<Parameters<typeof computeHaulPlan>[2]>;
|
||||
|
||||
interface CalcInputs extends FollowInputs, CarryInputs, TrialInputs, StagesInputs {
|
||||
interface CalcInputs extends FollowInputs, CarryInputs, CrossInputs, TrialInputs, StagesInputs {
|
||||
detail?: SectionDetailResponse;
|
||||
route_points?: RoutePoint[];
|
||||
context?: Partial<
|
||||
@@ -94,7 +96,7 @@ interface CalcInputs extends FollowInputs, CarryInputs, TrialInputs, StagesInput
|
||||
chainage_m?: number;
|
||||
}>;
|
||||
/** 관 지점 · 구조물 정본 → 측점마다 포장 · 기울임 · 세월교 하강(`facilityRanges` · C3) */
|
||||
facilities?: { pipes: CulvertPipePoint[]; structures: FacilityStructure[] };
|
||||
facilities?: { pipes: CulvertPipePoint[]; structures: FacilityStructure[] | null };
|
||||
};
|
||||
}
|
||||
|
||||
@@ -110,6 +112,7 @@ async function runDesignCalc(run: readonly string[], inputs: CalcInputs) {
|
||||
...carryRunners(inputs), // 노선 바뀐 [저장] 다시 얹기 — 관 · 구조물 초안 · 횡단 고침(472-6 6-B)
|
||||
...trialRunners(inputs), // 계산 자리 「서버」 시험 길(511-A)
|
||||
...stageRunners(inputs), // 묶음 단계 노선 ~ 횡단(511-B B-1)
|
||||
...crossRunners(inputs), // 형상 얹기 · [저장] 횡단 한 바퀴(511-B B-4)
|
||||
// 구조물 측점 면적 · 관 길이를 상세에 얹음(자리에서) — 뒤 누가토량이 고친 면적 위에서 쌓임.
|
||||
"structure.layouts": () => {
|
||||
const areas = structureAreaRows(sections);
|
||||
@@ -167,8 +170,9 @@ async function runDesignCalc(run: readonly string[], inputs: CalcInputs) {
|
||||
// 측점마다 TS 짝(`computeCrossDesign`) — 못 세우면 그 자리 { error }(파이썬은 ValueError 로 되던짐).
|
||||
"cross.design": () => {
|
||||
const batch = inputs.cross_design;
|
||||
if (batch?.facilities) useDefinition(inputs.definition);
|
||||
const ranges = batch?.facilities
|
||||
? facilityRanges(batch.facilities.pipes, batch.facilities.structures)
|
||||
? facilityRanges(batch.facilities.pipes, batch.facilities.structures ?? [])
|
||||
: null;
|
||||
return (batch?.stations ?? []).map((station) => {
|
||||
try {
|
||||
|
||||
@@ -0,0 +1,72 @@
|
||||
/* =============================================================================
|
||||
* A00_Common/design/logic/design_calc_node_cross.ts
|
||||
* 계산 Node 입구(`design_calc_node.ts`)의 횡단 몫(PLAN 511-B B-4) — 서버 GET · [저장] 이 부르는 횡단 셈을
|
||||
* 브라우저와 같은 TS 로(파이썬 짝을 걷는 자리).
|
||||
*
|
||||
* facility.attach 측점 그림 자료(관 세트 · 벽 · 포장 · 맹암거 · 빈칸 경고 · `attachShapes` · F4 F5 F6 C6)
|
||||
* 입력 attach { sections: [{ chainage_m · center_x · center_y }] · pipes · structures · types }
|
||||
* 출력 { shapes: 측점마다 그림 자료 칸만 · misses: { outside · far } }
|
||||
* cross.enforce [저장] 횡단 한 바퀴(`enforceSavedCross` · 설계선 → 미교차 넓힘 → 포장 → 세월교 하강 · G2 C3)
|
||||
* 입력 cross_enforce = `SaveCrossInput` + grid(DTM 격자 이진 경로 · 넓힘 때만 읽음)
|
||||
* 출력 측점마다 { design · samples?(넓힘) · extended? } | null
|
||||
* 구조물 표 = 서버 정의(`definition` · M02 반영) — 받으면 갈아 끼움(파이썬 `catalog_row` 와 같은 표).
|
||||
* ========================================================================== */
|
||||
|
||||
import { attachShapes, SHAPE_KEYS, type AttachType } from "@util/common_util_structure_attach";
|
||||
import { useCatalogDefinition, type CatalogDefinition } from "@util/common_util_structure_catalog";
|
||||
import type { PlanPipe, PlanStructure } from "@util/common_util_structure_station";
|
||||
import {
|
||||
enforceSavedCross,
|
||||
type SaveCrossInput,
|
||||
} from "../../../B06_Section/logic/B06_Section_Cross_Save";
|
||||
import { gridAt } from "./design_calc_node_stages";
|
||||
|
||||
export interface CrossInputs {
|
||||
definition?: CatalogDefinition | null;
|
||||
attach?: {
|
||||
sections: Array<{ chainage_m: number; center_x?: number | null; center_y?: number | null }>;
|
||||
pipes: PlanPipe[];
|
||||
structures: PlanStructure[];
|
||||
types: AttachType[];
|
||||
};
|
||||
cross_enforce?: SaveCrossInput & { grid?: string | null };
|
||||
}
|
||||
|
||||
let defined = "";
|
||||
|
||||
/** 서버 정의로 구조물 표를 갈아 끼움 — 같은 정의면 그대로(늘 떠 있는 Node). */
|
||||
export function useDefinition(definition: CatalogDefinition | null | undefined): void {
|
||||
if (!definition) return;
|
||||
const key = JSON.stringify(definition);
|
||||
if (key === defined) return;
|
||||
useCatalogDefinition(definition);
|
||||
defined = key;
|
||||
}
|
||||
|
||||
const SHAPE_FIELDS = [...SHAPE_KEYS, "blank"];
|
||||
|
||||
export function crossRunners(inputs: CrossInputs): Record<string, () => unknown> {
|
||||
return {
|
||||
"facility.attach": () => {
|
||||
useDefinition(inputs.definition);
|
||||
const attach = inputs.attach;
|
||||
if (!attach) throw new Error("형상 얹기 입력(attach) 없음");
|
||||
const sections = attach.sections.map((section) => ({ ...section })) as Array<
|
||||
Record<string, unknown>
|
||||
>;
|
||||
const misses = attachShapes(sections, attach.pipes, attach.structures, attach.types);
|
||||
const shapes = sections.map((section) =>
|
||||
Object.fromEntries(
|
||||
SHAPE_FIELDS.filter((key) => key in section).map((k) => [k, section[k]]),
|
||||
),
|
||||
);
|
||||
return { shapes, misses };
|
||||
},
|
||||
"cross.enforce": () => {
|
||||
useDefinition(inputs.definition);
|
||||
const input = inputs.cross_enforce;
|
||||
if (!input) throw new Error("횡단 한 바퀴 입력(cross_enforce) 없음");
|
||||
return enforceSavedCross(input, () => (input.grid ? gridAt(input.grid) : null));
|
||||
},
|
||||
};
|
||||
}
|
||||
@@ -77,7 +77,7 @@ export const STAGE_IDS = STAGE_NAMES.map((name) => `stage.${name}`);
|
||||
let gridMemo: { path: string; mtime: number; grid: DtmGrid } | null = null;
|
||||
|
||||
/** DTM 격자 이진 — 같은 경로 · 같은 수정 시각이면 앞에 읽은 것(늘 떠 있는 Node). */
|
||||
function gridAt(path: string): DtmGrid {
|
||||
export function gridAt(path: string): DtmGrid {
|
||||
const mtime = statSync(path).mtimeMs;
|
||||
if (gridMemo?.path !== path || gridMemo.mtime !== mtime) {
|
||||
const bytes = readFileSync(path);
|
||||
|
||||
@@ -217,10 +217,10 @@ export const CALC_REGISTRY: readonly CalcEntry[] = [
|
||||
site: "browser_now",
|
||||
stage: "cross",
|
||||
ts: `${B05L}B05_Profile_Sections_Ground.ts#extendUnclosed`,
|
||||
runner: `py:${B06}B06_Section_Engine_SampleExtend.py#extend_unclosed`,
|
||||
until: "472-4",
|
||||
runner: "node:cross.enforce",
|
||||
pair: `${B06}B06_Section_Engine_SampleExtend.py#extend_unclosed`,
|
||||
mirror: ["test_344_1_sections_ground_mirror"],
|
||||
mirror: ["test_344_1_sections_ground_mirror", "test_511_b4_cross_save_mirror"],
|
||||
note: "[저장] = Node cross.enforce 안(DTM 격자 · 511-B B-4) · DTM 아닌 지표면 = 파이썬 짝(표본기)",
|
||||
},
|
||||
{
|
||||
id: "ground.widen",
|
||||
@@ -408,11 +408,10 @@ export const CALC_REGISTRY: readonly CalcEntry[] = [
|
||||
site: "browser_now",
|
||||
stage: "cross",
|
||||
ts: `${B06L}B06_Section_Cross_Enforce.ts#facilityRanges`,
|
||||
runner: `py:${B06}B06_Section_Router_Design.py#enforce_design_lines`,
|
||||
until: "472-4",
|
||||
runner: "node:cross.enforce",
|
||||
pair: `${B06}B06_Section_Router_Design.py#enforce_design_lines`,
|
||||
note: "구간 판정 = 파이썬 짝(Router_Design_Ranges) · 다시 세운 설계 = Node cross.design",
|
||||
mirror: ["test_349_0n_cross_enforce_mirror"],
|
||||
note: "[저장] 횡단 한 바퀴 = Node cross.enforce · 기본 설계 구간 = cross.design facilities(511-B B-4) · 파이썬 짝 = DTM 아닌 지표면 · Node 못 부를 때",
|
||||
mirror: ["test_349_0n_cross_enforce_mirror", "test_511_b4_cross_save_mirror"],
|
||||
},
|
||||
{
|
||||
id: "cross.berm_spans",
|
||||
@@ -451,11 +450,10 @@ export const CALC_REGISTRY: readonly CalcEntry[] = [
|
||||
site: "browser_now",
|
||||
stage: "cross",
|
||||
ts: `${U}common_util_structure_underdrain.ts#attachUnderdrainSpecs`,
|
||||
runner: `py:${B06}B06_Section_Engine_Tilt.py#attach_underdrains`,
|
||||
until: "472-4",
|
||||
runner: "node:facility.attach",
|
||||
pair: `${B06}B06_Section_Engine_Tilt.py#attach_underdrains`,
|
||||
mirror: ["test_472_4_underdrain_mirror"],
|
||||
note: "얹기 = GET 상세(파이썬) · 알려진 차 = 옛 underdrain 걷음 TS 만 · 기울임은 cross.design 안",
|
||||
note: "얹기 = GET 상세 Node facility.attach 안(511-B B-4) · 알려진 차 = 옛 underdrain 걷음 TS 만 · 기울임은 cross.design 안",
|
||||
},
|
||||
{
|
||||
id: "cross.fit",
|
||||
|
||||
@@ -85,10 +85,10 @@ export const FACILITY_ROWS: readonly CalcEntry[] = [
|
||||
site: "browser_now",
|
||||
stage: "cross",
|
||||
ts: `${U}common_util_structure_attach.ts#attachShapes`,
|
||||
runner: `py:${B06}B06_Section_Engine_Attach.py#attach_shapes`,
|
||||
until: "472-4",
|
||||
runner: "node:facility.attach",
|
||||
pair: `${B06}B06_Section_Engine_Attach.py#attach_shapes`,
|
||||
mirror: ["test_323_4_attach_mirror"],
|
||||
note: "GET 상세 · 재생성 = Node facility.attach(511-B B-4) · 파이썬 짝 = Node 못 부를 때",
|
||||
},
|
||||
{
|
||||
id: "facility.culvert_sets",
|
||||
|
||||
@@ -2,6 +2,8 @@
|
||||
|
||||
구조물 열쇠(`SHAPE_KEYS` · 빈칸 경고 `blank`)를 걷고 차례대로 다시 얹음 — 관 세트 → 벽 → 포장 → 맹암거 →
|
||||
빈칸 경고. 고르기 = 표 형상(322-1). 서버 상세 읽기(`_read_section_detail`) · 재생성 응답이 이것 하나를 씀.
|
||||
셈 = 늘 떠 있는 Node `facility.attach`(화면과 같은 TS · 511-B B-4) ·
|
||||
파이썬 몫(`_attach_python`)은 Node 를 못 부를 때만.
|
||||
TS 짝 = `common_util/common_util_structure_attach.ts` `attachShapes`(화면 = 초안 ?? 저장분 현황) ·
|
||||
거울 시험 = `resources/tester/test_323_4_attach_mirror.py`.
|
||||
"""
|
||||
@@ -106,14 +108,10 @@ def _outside_route(pipe: Any, sections: list[dict[str, Any]]) -> bool:
|
||||
return False
|
||||
|
||||
|
||||
def attach_shapes(
|
||||
def _attach_python(
|
||||
sections: list[dict[str, Any]], pipes: list[Any], structures: list[Any]
|
||||
) -> dict[str, list[float]]:
|
||||
"""측점마다 그림 자료를 걷고 현황으로 다시 얹음(TS `attachShapes` 짝 · 제자리 고침) ·
|
||||
시종점 밖 관 · 주인 측점 먼 관은 안 얹고 돌려줌(458 · `{"outside", "far"}`)."""
|
||||
for section in sections:
|
||||
for key in (*SHAPE_KEYS, "blank"):
|
||||
section.pop(key, None)
|
||||
"""파이썬 짝(TS `attachShapes` · Node 를 못 부를 때만)."""
|
||||
misses: dict[str, list[float]] = {"outside": [], "far": []}
|
||||
kept = []
|
||||
for pipe in pipes:
|
||||
@@ -131,6 +129,55 @@ def attach_shapes(
|
||||
)
|
||||
attach_underdrain_specs(sections, underdrains_from(structures))
|
||||
_attach_blanks(sections, pipes, structures)
|
||||
return misses
|
||||
|
||||
|
||||
def _attach_node(
|
||||
sections: list[dict[str, Any]], pipes: list[Any], structures: list[Any]
|
||||
) -> dict[str, list[float]] | None:
|
||||
"""늘 떠 있는 Node `facility.attach`(511-B B-4) — 측점 자리만 보내고 그림 자료 칸만 받음 ·
|
||||
못 부르면 None."""
|
||||
from dataclasses import asdict
|
||||
|
||||
from B05_Profile.B05_Profile_Structures_Schema import load_definition, load_structure_types
|
||||
from common_util.common_util_calc_runner import run_calc
|
||||
|
||||
stubs = [
|
||||
{key: section.get(key) for key in ("chainage_m", "center_x", "center_y")}
|
||||
for section in sections
|
||||
]
|
||||
results = run_calc(
|
||||
["facility.attach"],
|
||||
{
|
||||
"definition": load_definition(),
|
||||
"attach": {
|
||||
"sections": stubs,
|
||||
"pipes": [asdict(pipe) for pipe in pipes],
|
||||
"structures": [item.model_dump(mode="json") for item in structures],
|
||||
"types": [item.model_dump(mode="json") for item in load_structure_types()],
|
||||
},
|
||||
},
|
||||
)
|
||||
if results is None:
|
||||
return None
|
||||
out = results["facility.attach"]
|
||||
for section, shape in zip(sections, out["shapes"], strict=True):
|
||||
section.update(shape)
|
||||
return out["misses"]
|
||||
|
||||
|
||||
def attach_shapes(
|
||||
sections: list[dict[str, Any]], pipes: list[Any], structures: list[Any]
|
||||
) -> dict[str, list[float]]:
|
||||
"""측점마다 그림 자료를 걷고 현황으로 다시 얹음(TS `attachShapes` 짝 · 제자리 고침) ·
|
||||
시종점 밖 관 · 주인 측점 먼 관은 안 얹고 돌려줌(458 · `{"outside", "far"}`)."""
|
||||
for section in sections:
|
||||
for key in (*SHAPE_KEYS, "blank"):
|
||||
section.pop(key, None)
|
||||
misses = _attach_node(sections, pipes, structures)
|
||||
if misses is None:
|
||||
logger.warning("Node 형상 얹기 실패 — 파이썬 짝으로 얹음(%d 측점)", len(sections))
|
||||
misses = _attach_python(sections, pipes, structures)
|
||||
if misses["outside"] or misses["far"]:
|
||||
logger.warning("B06 시설 그림 안 얹음 — 노선 밖 %s · 측점 멂 %s", *misses.values())
|
||||
return misses
|
||||
|
||||
@@ -56,10 +56,11 @@ def _python(job: DesignJob, standard: dict[str, Any] | None) -> dict[str, Any] |
|
||||
Facilities = tuple[Path, list[float]]
|
||||
|
||||
|
||||
def facility_inputs(project_root: Path) -> dict[str, list[dict[str, Any]]]:
|
||||
def facility_inputs(project_root: Path) -> dict[str, list[dict[str, Any]] | None]:
|
||||
"""TS `facilityRanges` 입력 — 관 = 지금 노선에 얹은 관 지점(`route_pipe_points`) · 구조물 정본.
|
||||
|
||||
못 읽은 쪽은 빈 목록(파이썬 `pavement_ranges` · `ford_surface_drops` 의 비치명 태도 그대로)."""
|
||||
못 읽은 관 = 빈 목록 · 구조물 = None(포장 · 맹암거 구간 없음 · 소단 = 저장분 그대로 · 파이썬
|
||||
`pavement_ranges` · `berm_specs_at` 의 비치명 태도 그대로)."""
|
||||
from dataclasses import asdict
|
||||
|
||||
from B05_Profile.B05_Profile_Engine_Sections import route_pipe_points
|
||||
@@ -70,11 +71,12 @@ def facility_inputs(project_root: Path) -> dict[str, list[dict[str, Any]]]:
|
||||
except Exception: # noqa: BLE001 — 정본을 못 읽어도 설계 계산은 이어 간다
|
||||
logger.exception("B06 관 지점을 읽지 못했습니다 (시설 구간 없음으로 본다)")
|
||||
pipes = []
|
||||
structures: list[dict[str, Any]] | None
|
||||
try:
|
||||
structures = [s.model_dump(mode="json") for s in load_structures(str(project_root))[1]]
|
||||
except Exception: # noqa: BLE001
|
||||
logger.exception("B06 구조물 정본을 읽지 못했습니다 (포장 · 맹암거 구간 없음으로 본다)")
|
||||
structures = []
|
||||
structures = None
|
||||
return {"pipes": pipes, "structures": structures}
|
||||
|
||||
|
||||
|
||||
@@ -57,6 +57,8 @@ logger = logging.getLogger(__name__)
|
||||
|
||||
ROOT = Path(__file__).resolve().parents[1]
|
||||
BUNDLE = ROOT / "config" / "server_calc_node" / "B06_Section_Server_Calc_Node.js"
|
||||
# 곡선부 확폭 입력 칸(파이썬 `curve_widening_args` · TS `SaveSection`)
|
||||
CURVE_KEYS = ("plan_radius_m", "curve_outer_side", "curve_widening_m")
|
||||
_NPM_SCRIPT = "build:server-calc"
|
||||
# 정본에 얹는 값만 받는다 — Node 가 다른 키를 내도 설계 데이터에 흘리지 않는다.
|
||||
# ⚠ **TS 쪽 `STRUCTURE_ROW_KEYS` 와 짝이다.** 한쪽만 늘리면 Node 가 값을 내도 여기서 조용히
|
||||
@@ -229,6 +231,105 @@ def _enforce_stored_designs(
|
||||
enforce_ford_surface_drops(longitudinal, sections, project_root, standard)
|
||||
|
||||
|
||||
def design_grid_file(project_root: Path, params: dict[str, Any] | None) -> Path | None:
|
||||
"""설계 지형이 DTM 이면 격자 이진(`dtm_grid_file` · 표본기 `build_surface_sampler` 와
|
||||
같은 npz) · DTM 아닌 지표면(tin · nurbs · …) · 못 엶 = None(파이썬 길)."""
|
||||
from common_util.common_util_surface_sampler import dtm_grid_file
|
||||
|
||||
params = params or {}
|
||||
if params.get("method") != "dtm" or not params.get("source_filter"):
|
||||
return None
|
||||
models = project_root / "B04_PreProcess" / "models"
|
||||
smooth = models / f"dtm_{params['source_filter']}_smooth.npz"
|
||||
npz = (
|
||||
smooth
|
||||
if params.get("smooth") and smooth.exists()
|
||||
else smooth.with_name(f"dtm_{params['source_filter']}.npz")
|
||||
)
|
||||
try:
|
||||
return dtm_grid_file(npz) if npz.is_file() else None
|
||||
except (OSError, ValueError, KeyError) as exc:
|
||||
logger.warning("서버 재계산: DTM 격자 이진을 못 만듦 — 파이썬 길 · %s", exc)
|
||||
return None
|
||||
|
||||
|
||||
def _enforce_on_python(
|
||||
longitudinal: dict[str, Any],
|
||||
sections: list[dict[str, Any]],
|
||||
project_root: Path,
|
||||
standard: dict[str, Any] | None,
|
||||
surface: dict[str, Any] | None,
|
||||
) -> None:
|
||||
"""파이썬 길(DTM 아닌 지표면 · Node 못 부름) — 설계선 → (미교차면 표본기) → 넓힘 ·
|
||||
포장 · 하강."""
|
||||
from B06_Section.B06_Section_Router_Design import enforce_design_lines
|
||||
|
||||
# 모든 측점 설계선을 저장 입력으로 먼저 다시 냄(139-6 · 노견 확장 · 측구 · 지반유형) — 미교차
|
||||
# 판정도 새 설계로 해야 넓힘 지표면을 연다.
|
||||
enforce_design_lines(longitudinal, sections, project_root, standard)
|
||||
# 확정 지표면 — 미교차 측점이 있을 때만 연다(샘플 넓힘 ㉳ (가) · 없으면 여는 비용을 안 씀).
|
||||
unclosed = any((item.get("design") or {}).get("slope_unclosed") for item in sections)
|
||||
sampler = _surface_sampler(project_root, surface) if unclosed else None
|
||||
_enforce_stored_designs(longitudinal, sections, project_root, standard, sampler)
|
||||
|
||||
|
||||
def _enforce_on_node(
|
||||
longitudinal: dict[str, Any],
|
||||
sections: list[dict[str, Any]],
|
||||
project_root: Path,
|
||||
standard: dict[str, Any] | None,
|
||||
grid: Path,
|
||||
) -> bool:
|
||||
"""Node `cross.enforce` 한 부름(TS `B06_Section_Cross_Save.enforceSavedCross`) — 측점 설계 ·
|
||||
넓힌 표본을 자리에서 갈고 넓힌 표본은 횡단 파일에 남김. 못 부르면 False(파이썬 길)."""
|
||||
from B05_Profile.B05_Profile_Engine_Sections import find_cross_file
|
||||
from B05_Profile.B05_Profile_Structures_Schema import load_definition
|
||||
from B06_Section.B06_Section_Engine_Design_Node import facility_inputs, full_standard
|
||||
from common_util.common_util_calc_runner import run_calc
|
||||
from common_util.common_util_json import atomic_write_json
|
||||
from config.config_system import STANDARD_ROCK_BOUNDARY_DEFAULT_OFFSET_M
|
||||
|
||||
keys = ("chainage_m", "design", "samples", "frame", *CURVE_KEYS)
|
||||
profiles = longitudinal.get("design_profiles") or []
|
||||
results = run_calc(
|
||||
["cross.enforce"],
|
||||
{
|
||||
"definition": load_definition(),
|
||||
"cross_enforce": {
|
||||
"profile": (profiles[0] or {}).get("samples") or [] if profiles else [],
|
||||
"sections": [{k: s.get(k) for k in keys if k in s} for s in sections],
|
||||
"standard": full_standard(standard),
|
||||
"rockDefaultM": STANDARD_ROCK_BOUNDARY_DEFAULT_OFFSET_M,
|
||||
**facility_inputs(project_root),
|
||||
"grid": str(grid),
|
||||
},
|
||||
},
|
||||
)
|
||||
if results is None:
|
||||
logger.warning("서버 재계산: Node 횡단 한 바퀴 실패 — 파이썬 길")
|
||||
return False
|
||||
cross_dir = project_root / "B06_Section" / "cross_sections"
|
||||
for section, row in zip(sections, results["cross.enforce"], strict=True):
|
||||
if not row:
|
||||
continue
|
||||
section["design"] = row["design"]
|
||||
if row.get("samples"):
|
||||
section["samples"] = row["samples"]
|
||||
cross_path = find_cross_file(cross_dir, float(section.get("chainage_m") or 0.0))
|
||||
if cross_path.is_file():
|
||||
stored = json.loads(cross_path.read_text(encoding="utf-8"))
|
||||
atomic_write_json(cross_path, {**stored, "samples": row["samples"]})
|
||||
info = row.get("extended") or {}
|
||||
logger.info(
|
||||
"미교차 샘플 넓힘: 측점 %.3f 좌 +%sm 우 +%sm 지표면 끝 %s",
|
||||
float(section.get("chainage_m") or 0.0),
|
||||
info.get("left_m"),
|
||||
info.get("right_m"),
|
||||
info.get("surface_end"),
|
||||
)
|
||||
return True
|
||||
|
||||
|
||||
def refresh_alignment(longitudinal: dict[str, Any]) -> dict[str, Any] | None:
|
||||
"""종단 경고를 저장 입력(base_pvi · 편집 · 정책 · 기준)으로 지금 코드가 다시 셈 — 옛 코드가
|
||||
쓴 경고가 남지 않게(472 덧 · 요구 272). 기하(계획고 · 곡선 · 표본 · 위반)는 저장값 그대로 —
|
||||
@@ -324,24 +425,26 @@ async def _recompute(project_id: UUID | str, route_id: int) -> int:
|
||||
standard = stored_standard_cross_section(longitudinal_row)
|
||||
# 포장 구간·세월교 보정 — 고쳐진 설계 위에서 면적·유토곡선이 나와야 한다.
|
||||
before = [json.dumps(item.get("design"), sort_keys=True, default=str) for item in sections]
|
||||
# 모든 측점 설계선을 저장 입력으로 먼저 다시 냄(139-6 · 노견 확장 · 측구 · 지반유형) — 미교차
|
||||
# 판정도 새 설계로 해야 넓힘 지표면을 연다.
|
||||
from B06_Section.B06_Section_Router_Design import enforce_design_lines
|
||||
|
||||
await asyncio.to_thread(
|
||||
enforce_design_lines, detail.get("longitudinal") or {}, sections, project_root, standard
|
||||
)
|
||||
# 확정 지표면 — 미교차 측점이 있을 때만 연다(샘플 넓힘 ㉳ (가) · 없으면 여는 비용을 안 씀).
|
||||
unclosed = any((item.get("design") or {}).get("slope_unclosed") for item in sections)
|
||||
sampler = await asyncio.to_thread(_surface_sampler, project_root, surface) if unclosed else None
|
||||
await asyncio.to_thread(
|
||||
_enforce_stored_designs,
|
||||
# 횡단 한 바퀴(설계선 → 미교차 넓힘 → 포장 → 세월교 하강) — DTM 이면 Node 한 부름
|
||||
# (화면과 같은 TS · 511-B B-4) · DTM 아닌 지표면 · Node 못 부름 = 파이썬 길
|
||||
grid = await asyncio.to_thread(design_grid_file, project_root, surface)
|
||||
on_node = grid is not None and await asyncio.to_thread(
|
||||
_enforce_on_node,
|
||||
detail.get("longitudinal") or {},
|
||||
sections,
|
||||
project_root,
|
||||
standard,
|
||||
sampler,
|
||||
grid,
|
||||
)
|
||||
if not on_node:
|
||||
await asyncio.to_thread(
|
||||
_enforce_on_python,
|
||||
detail.get("longitudinal") or {},
|
||||
sections,
|
||||
project_root,
|
||||
standard,
|
||||
surface,
|
||||
)
|
||||
fixed = [
|
||||
item
|
||||
for index, item in enumerate(sections)
|
||||
|
||||
@@ -44,6 +44,8 @@ NODE_IDS = frozenset(
|
||||
"cross.berm_spans",
|
||||
"facility.rebase",
|
||||
"stations.carry",
|
||||
"facility.attach",
|
||||
"cross.enforce",
|
||||
}
|
||||
)
|
||||
# 시험 길(`design_calc_node_trial.ts` `TRIAL` 표와 같음) — 조작 중 브라우저 함수를 서버가 같은 TS 로
|
||||
|
||||
@@ -69,4 +69,5 @@ def test_서버_재계산이_먼저_돌고_미교차_판정은_그_뒤() -> None
|
||||
src = (PROJECT_ROOT / "B06_Section" / "B06_Section_Server_Calc_Prebuild.py").read_text(
|
||||
encoding="utf-8"
|
||||
)
|
||||
assert src.index("enforce_design_lines, detail.get") < src.index("unclosed = any(")
|
||||
# 파이썬 길(DTM 아닌 지표면) — 설계선 먼저 · Node 길 차례 = `test_511_b4_cross_save_mirror`
|
||||
assert src.index("enforce_design_lines(longitudinal, sections") < src.index("unclosed = any(")
|
||||
|
||||
@@ -48,6 +48,8 @@ NODE_IDS = {
|
||||
"cross.berm_spans",
|
||||
"facility.rebase",
|
||||
"stations.carry",
|
||||
"facility.attach", # 511-B B-4 — GET 형상 얹기
|
||||
"cross.enforce", # 511-B B-4 — [저장] 횡단 한 바퀴
|
||||
}
|
||||
|
||||
pytestmark = pytest.mark.skipif(
|
||||
|
||||
@@ -0,0 +1,251 @@
|
||||
"""PLAN 511-B B-4 (요구 320-4) — [저장] 횡단 한 바퀴 파이썬 길 = Node 길(`cross.enforce`) · 형상 얹기 Node.
|
||||
|
||||
못박는 것
|
||||
- 가짜 지형(DTM · 빈칸 = 지표면 끝) · 관 지점(세월교 · 물넘이포장 둘 · 배관) · 구조물(포장 · 맹암거 · 소단 ·
|
||||
사토장)로 파이썬 길(`_enforce_on_python` = 설계선 → 표본기 넓힘 → 포장 → 하강)과 Node 길
|
||||
(`_enforce_on_node` · TS `B06_Section_Cross_Save.enforceSavedCross`)의 측점 설계 · 넓힌 표본 0 차.
|
||||
- 그 사이 넓힘 · 지표면 끝 · 포장 켬 · 끔 · 기울임 · 세월교 하강 · 소단 · 사용자 값 승계가 실제로 일어남.
|
||||
- 구조물 정본을 못 읽으면 두 길 다 소단 = 저장분 그대로 · 포장 · 맹암거 구간 없음.
|
||||
- DTM 아닌 지표면(tin) = 격자 없음(파이썬 길) · 형상 얹기 = Node(`facility.attach`) 부름.
|
||||
|
||||
./venv/Scripts/python.exe -m pytest resources/tester/test_511_b4_cross_save_mirror.py -q
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import copy
|
||||
import json
|
||||
import math
|
||||
import sys
|
||||
from pathlib import Path
|
||||
|
||||
import numpy as np
|
||||
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 PIPES # noqa: E402
|
||||
|
||||
import B05_Profile.B05_Profile_Engine_Sections as sections_mod # 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_Ranges as ranges_mod # noqa: E402
|
||||
from B05_Profile.B05_Profile_Structures_Schema import StructureInstance # noqa: E402
|
||||
from B06_Section import B06_Section_Server_Calc_Prebuild as prebuild # 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 common_util.common_util_surface_sampler import DtmGridSampler # noqa: E402
|
||||
from config.config_system_design import STANDARD_CROSS_SECTION # noqa: E402
|
||||
|
||||
pytestmark = pytest.mark.skipif(
|
||||
not (ROOT / "config" / "node_modules" / "vite").is_dir(), reason="프론트엔드 의존성 미설치"
|
||||
)
|
||||
|
||||
SURFACE = {"method": "dtm", "source_filter": "classification", "smooth": False}
|
||||
STRUCTURES = [
|
||||
StructureInstance(
|
||||
structure_id="pv1", type_id="pavement_concrete", placement="interval", start_m=40, end_m=80
|
||||
),
|
||||
StructureInstance(
|
||||
structure_id="ud1",
|
||||
type_id="cross_underdrain",
|
||||
placement="point",
|
||||
chainage_m=160,
|
||||
options={"culvert_slope_pct": 2, "culvert_height_m": 0.5},
|
||||
),
|
||||
StructureInstance(
|
||||
structure_id="bm1",
|
||||
type_id="berm",
|
||||
placement="interval",
|
||||
start_m=180,
|
||||
end_m=230,
|
||||
options={"width_m": 1.0, "interval_m": 3.0, "slope_deg": 0.0, "depth_m": 1.0},
|
||||
),
|
||||
StructureInstance(
|
||||
structure_id="sp1",
|
||||
type_id="spoil_bank",
|
||||
placement="site",
|
||||
chainage_m=335,
|
||||
options={"capacity_m3": 300, "side": "자동(성토 쪽)"},
|
||||
),
|
||||
]
|
||||
OFFSETS = [round(-20 + 0.5 * i, 1) for i in range(81)]
|
||||
|
||||
|
||||
def _plan(chainage: float) -> float:
|
||||
return round(100.5 + 0.6 * math.sin(chainage / 53) + 0.002 * chainage, 4)
|
||||
|
||||
|
||||
def _npz(root: Path) -> None:
|
||||
"""DTM 격자 — y = 노선 왼쪽(+) · −42 m 아래 빈칸(지표면 끝) · x 180~240 은 −30 m 아래도."""
|
||||
models = root / "B04_PreProcess" / "models"
|
||||
models.mkdir(parents=True)
|
||||
x = np.arange(-10.0, 431.0, 1.0)
|
||||
y = np.arange(-80.0, 81.0, 1.0)
|
||||
xx, yy = np.meshgrid(x, y)
|
||||
# 가로 기울기 0.68 ~ 0.78(반폭 20 m 에서 안 닫히는 측점 · 닫히는 거리 갈림 · 지표면 끝)
|
||||
z = 100 + (0.73 + 0.05 * np.sin(xx / 40)) * yy + 0.8 * np.sin(xx / 37) + 0.3 * np.cos(yy / 3)
|
||||
valid = ~((yy < -42) | ((xx >= 180) & (xx <= 240) & (yy < -30)))
|
||||
np.savez(models / f"dtm_{SURFACE['source_filter']}.npz", x=x, y=y, z=z, valid_mask=valid)
|
||||
|
||||
|
||||
def _sections(root: Path) -> tuple[dict, list[dict]]:
|
||||
sampler = DtmGridSampler.from_npz(
|
||||
root / "B04_PreProcess" / "models" / f"dtm_{SURFACE['source_filter']}.npz"
|
||||
)
|
||||
chainages = sorted({20.0 * i for i in range(21)} | {p["chainage_m"] for p in PIPES})
|
||||
longitudinal = {
|
||||
"design_profiles": [
|
||||
{"samples": [{"chainage_m": c, "elevation_m": _plan(c)} for c in chainages]}
|
||||
]
|
||||
}
|
||||
out = []
|
||||
for index, c in enumerate(chainages):
|
||||
xy = np.array([[c, o] for o in OFFSETS])
|
||||
z, _valid = sampler.sample_xy(xy)
|
||||
samples = [
|
||||
{
|
||||
"offset_m": o,
|
||||
"x": c,
|
||||
"y": o,
|
||||
"z": round(float(e), 6),
|
||||
"elevation_m": round(float(e), 6),
|
||||
"valid": True,
|
||||
}
|
||||
for o, e in zip(OFFSETS, z)
|
||||
]
|
||||
# 저장분 = 옛 입력(포장 · 기울임 · 하강은 지금 시설과 다르게) + 사용자 값
|
||||
options = dict(
|
||||
ground_type="soil" if index % 3 == 0 else "ripping_rock",
|
||||
section_mode="left_cut" if index % 2 else "right_cut",
|
||||
ditch_type="standard",
|
||||
paved=index % 4 == 0,
|
||||
standard=STANDARD_CROSS_SECTION,
|
||||
surface_drop_m=0.0,
|
||||
shoulder_extend_left_m=0.3 if index % 5 == 0 else 0.0,
|
||||
)
|
||||
design = compute_cross_design(samples, _plan(c), **options)
|
||||
design.update(status="provisional", pavement_suggested=False, structure_note="메모")
|
||||
if index % 5 == 0:
|
||||
design["shoulder_extend_left_m"] = 0.3
|
||||
if index % 7 == 0:
|
||||
design["cut_slope_ratio_user"] = 0.8
|
||||
if index % 6 == 0:
|
||||
design["berm"] = {"width_m": 1.5, "interval_m": 5.0, "slope_deg": 0.0, "depth_m": 2.0}
|
||||
design["berm_depth_left_m"] = 0.5
|
||||
out.append(
|
||||
{
|
||||
"chainage_m": c,
|
||||
"center_x": c,
|
||||
"center_y": 0.0,
|
||||
"frame": {"origin": {"x": c, "y": 0.0}, "left_xy": [0.0, 1.0]},
|
||||
"plan_radius_m": 60.0 if index == 4 else None,
|
||||
"curve_outer_side": "left" if index == 4 else None,
|
||||
"curve_widening_m": 0.5 if index == 4 else None,
|
||||
"samples": samples,
|
||||
"design": design,
|
||||
}
|
||||
)
|
||||
return longitudinal, out
|
||||
|
||||
|
||||
def _facilities(monkeypatch, readable: bool = True) -> None:
|
||||
pipes = parse_pipe_points(PIPES)
|
||||
|
||||
def structures(_root):
|
||||
if not readable:
|
||||
raise OSError("구조물 정본 못 읽음(시험)")
|
||||
return 0, STRUCTURES
|
||||
|
||||
monkeypatch.setattr(sections_mod, "route_pipe_points", lambda *_a, **_k: pipes)
|
||||
for module in (structures_repo, ranges_mod, tilt_mod):
|
||||
monkeypatch.setattr(module, "load_structures", structures)
|
||||
|
||||
|
||||
def _both(tmp_path: Path, monkeypatch, readable: bool = True):
|
||||
root = tmp_path / "p"
|
||||
_npz(root)
|
||||
_facilities(monkeypatch, readable)
|
||||
longitudinal, sections = _sections(root)
|
||||
py, node = copy.deepcopy(sections), copy.deepcopy(sections)
|
||||
prebuild._enforce_on_python(longitudinal, py, root, None, SURFACE)
|
||||
grid = prebuild.design_grid_file(root, SURFACE)
|
||||
assert grid is not None and grid.is_file()
|
||||
assert prebuild._enforce_on_node(longitudinal, node, root, None, grid)
|
||||
return sections, py, node
|
||||
|
||||
|
||||
def _same(py: list[dict], node: list[dict]) -> None:
|
||||
"""값 0 차(수는 값으로 — Node 를 거친 저장분 칸은 5.0 이 5 로 옴)."""
|
||||
for left, right in zip(py, node, strict=True):
|
||||
assert left["samples"] == right["samples"], left["chainage_m"]
|
||||
assert left["design"] == right["design"], left["chainage_m"]
|
||||
|
||||
|
||||
def test_저장_횡단_한_바퀴_파이썬_길_Node_길_0차(tmp_path, monkeypatch):
|
||||
before, py, node = _both(tmp_path, monkeypatch)
|
||||
_same(py, node)
|
||||
by = {s["chainage_m"]: s for s in node}
|
||||
widened = [s["chainage_m"] for s in node if len(s["samples"]) > len(OFFSETS)]
|
||||
assert len(widened) >= 5, widened # 넓힘이 실제로 돎
|
||||
assert any(s["design"].get("slope_unclosed") for s in node) # 지표면 끝까지 넓혀도 안 닫힘
|
||||
assert (
|
||||
by[60.0]["design"]["paved"] is True and by[100.0]["design"]["paved"] is False
|
||||
) # 포장 켬 · 끔
|
||||
assert by[160.0]["design"]["tilted"] is True # 맹암거 자리 기울임
|
||||
assert by[250.0]["design"]["paved"] is True and by[250.0]["design"]["tilted"] is True # 물넘이
|
||||
assert by[110.0]["design"]["surface_drop_m"] > 0 # 세월교 하강
|
||||
assert by[200.0]["design"]["berm"]["width_m"] == 1.0 # 소단 = 구조물 정본 구간
|
||||
assert (
|
||||
"berm" not in by[120.0]["design"] or by[120.0]["design"]["berm"] is None
|
||||
) # 구간 밖 = 없음
|
||||
assert all(s["design"]["structure_note"] == "메모" for s in node) # 사용자 값 승계
|
||||
assert by[0.0]["design"]["shoulder_extend_left_m"] == 0.3
|
||||
assert any(a["design"] != b["design"] for a, b in zip(before, node))
|
||||
|
||||
|
||||
def test_구조물_정본_못_읽음_소단_저장분_그대로(tmp_path, monkeypatch):
|
||||
before, py, node = _both(tmp_path, monkeypatch, readable=False)
|
||||
_same(py, node)
|
||||
by = {s["chainage_m"]: s for s in node}
|
||||
saved = {s["chainage_m"]: s for s in before}
|
||||
assert by[0.0]["design"]["berm"] == saved[0.0]["design"]["berm"] # 저장분 소단
|
||||
assert by[60.0]["design"]["paved"] is False # 포장 구간 없음(물넘이 밖)
|
||||
assert not by[160.0]["design"].get("tilted") # 맹암거 구간 없음(칸 없음)
|
||||
|
||||
|
||||
def test_DTM_아닌_지표면은_격자_없음(tmp_path):
|
||||
root = tmp_path / "p"
|
||||
_npz(root)
|
||||
assert prebuild.design_grid_file(root, {**SURFACE, "method": "tin"}) is None
|
||||
assert prebuild.design_grid_file(root, {**SURFACE, "source_filter": "없음"}) is None
|
||||
|
||||
|
||||
def test_형상_얹기는_Node(monkeypatch):
|
||||
from B06_Section import B06_Section_Engine_Attach as attach_mod
|
||||
from common_util import common_util_calc_runner as runner
|
||||
|
||||
calls = []
|
||||
real = runner.run_calc
|
||||
monkeypatch.setattr(
|
||||
runner, "run_calc", lambda ids, inputs: calls.append(ids) or real(ids, inputs)
|
||||
)
|
||||
sections = [
|
||||
{"chainage_m": float(c), "center_x": float(c), "center_y": 0.0} for c in range(0, 401, 10)
|
||||
]
|
||||
misses = attach_mod.attach_shapes(sections, parse_pipe_points(PIPES), STRUCTURES)
|
||||
assert calls == [["facility.attach"]]
|
||||
assert misses == {"outside": [], "far": []}
|
||||
keys = {k for s in sections for k in s} - {"chainage_m", "center_x", "center_y"}
|
||||
assert {"ford", "ford_pavement", "culvert", "pavement", "underdrain"} <= keys
|
||||
# 파이썬 짝(Node 못 부를 때)과 같음
|
||||
py = [
|
||||
{"chainage_m": s["chainage_m"], "center_x": s["center_x"], "center_y": 0.0}
|
||||
for s in sections
|
||||
]
|
||||
attach_mod._attach_python(py, parse_pipe_points(PIPES), STRUCTURES)
|
||||
assert json.loads(json.dumps(py)) == json.loads(json.dumps(sections))
|
||||
Reference in New Issue
Block a user