refactor(472-4 ① 꼭짓점 세움): 서버 꼭짓점 세움도 TS seatBornNodes 한 벌 — Node 입구 profile.seat
- 새 design_calc_node_follow.ts(따라감 몫 runner) · 입구는 두 줄만(import · 펼침) · 등록부 runner node:profile.seat · 실행기 NODE_IDS - Router_Confirm._seat_born_nodes = run_calc 겉(세울 자리 없으면 Node 안 부름) · 파이썬 셈 · 상수 걷음 - 시험 test_472_4_follow_node(옛 코드 빨강 2) · 427_1 · 459 거울 통과 · 472-3 NODE_IDS 에 profile.seat(cross.design 빨강은 ② 몫) Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
@@ -35,10 +35,11 @@ import {
|
||||
extraWallRows,
|
||||
structureAreaRows,
|
||||
} from "../../../B06_Section/B06_Section_Structure_Layouts";
|
||||
import { followRunners, type FollowInputs } from "./design_calc_node_follow";
|
||||
|
||||
type HaulOptions = NonNullable<Parameters<typeof computeHaulPlan>[2]>;
|
||||
|
||||
interface CalcInputs {
|
||||
interface CalcInputs extends FollowInputs {
|
||||
detail?: SectionDetailResponse;
|
||||
route_points?: RoutePoint[];
|
||||
context?: Partial<
|
||||
@@ -130,6 +131,7 @@ function runDesignCalc(run: readonly string[], inputs: CalcInputs) {
|
||||
let massHaul: MassHaulResult | null = null;
|
||||
|
||||
const RUNNERS: Record<string, () => unknown> = {
|
||||
...followRunners(inputs), // 측점 열쇠 · 따라감 · 꼭짓점 세움(472-4 ①)
|
||||
// 구조물 측점 면적 · 관 길이를 상세에 얹음(자리에서) — 뒤 누가토량이 고친 면적 위에서 쌓임.
|
||||
"structure.layouts": () => {
|
||||
const areas = structureAreaRows(sections);
|
||||
|
||||
@@ -0,0 +1,43 @@
|
||||
/* =============================================================================
|
||||
* A00_Common/design/logic/design_calc_node_follow.ts
|
||||
* 계산 Node 입구(`design_calc_node.ts`)의 따라감 몫(PLAN 472-4 ①) — 측점 열쇠 · 시설 따라감 · 추가 측점 ·
|
||||
* 꼭짓점 세움. 브라우저 묶음이 쓰는 같은 TS 를 부름 · 파이썬은 `common_util_calc_runner.run_calc` 로.
|
||||
*
|
||||
* profile.seat 입력 seat { ground_s · ground_z(저장 선형 표본) · policy(파이썬 `AlignmentPolicy.as_dict`) ·
|
||||
* base_pvi(옮기기 전) · base(옮긴 꼭짓점) · born · held } → 꼭짓점 목록(`seatBornNodes`)
|
||||
* ========================================================================== */
|
||||
|
||||
import { seatBornNodes } from "../../../B05_Profile/logic/B05_Profile_Grade_Seat";
|
||||
import type {
|
||||
AlignmentNode,
|
||||
AlignmentPolicy,
|
||||
} from "../../../B05_Profile/logic/B05_Profile_UI_Profile_Alignment";
|
||||
|
||||
export interface FollowInputs {
|
||||
seat?: {
|
||||
ground_s: number[];
|
||||
ground_z: number[];
|
||||
policy: AlignmentPolicy;
|
||||
base_pvi: AlignmentNode[];
|
||||
base: AlignmentNode[];
|
||||
born: number[];
|
||||
held?: number[];
|
||||
};
|
||||
}
|
||||
|
||||
/** id → 셈(입구 `RUNNERS` 에 얹음). */
|
||||
export function followRunners(inputs: FollowInputs): Record<string, () => unknown> {
|
||||
return {
|
||||
"profile.seat": () => {
|
||||
const seat = inputs.seat;
|
||||
if (!seat) return null;
|
||||
const base = {
|
||||
chainage: seat.ground_s,
|
||||
ground: seat.ground_z,
|
||||
policy: seat.policy,
|
||||
basePvi: seat.base_pvi,
|
||||
};
|
||||
return seatBornNodes(base, seat.base, seat.born, seat.held ?? []);
|
||||
},
|
||||
};
|
||||
}
|
||||
@@ -320,11 +320,10 @@ export const CALC_REGISTRY: readonly CalcEntry[] = [
|
||||
site: "browser_now",
|
||||
stage: "follow",
|
||||
ts: `${B05L}B05_Profile_Grade_Seat.ts#seatBornNodes`,
|
||||
runner: `py:${B05}B05_Profile_Router_Confirm.py#_seat_born_nodes`,
|
||||
until: "472-4",
|
||||
pair: `${B05}B05_Profile_Router_Confirm.py#_seat_born_nodes`,
|
||||
mirror: ["test_427_1_follow_seat", "test_459_route_seat_mirror"],
|
||||
note: "Route_Save `_carry_profile` 도 같은 일",
|
||||
runner: "node:profile.seat",
|
||||
pair: null,
|
||||
mirror: ["test_427_1_follow_seat", "test_459_route_seat_mirror", "test_472_4_follow_node"],
|
||||
note: "파이썬 `Router_Confirm._seat_born_nodes` = Node 부름 겉(측점 병합 · Route_Save `_carry_profile`)",
|
||||
},
|
||||
{
|
||||
id: "profile.tools",
|
||||
|
||||
@@ -12,15 +12,12 @@ from typing import Any
|
||||
from uuid import UUID
|
||||
|
||||
import aiomysql
|
||||
import numpy as np
|
||||
|
||||
from B03_FileInput.B03_FileInput_Repository import get_project_storage_relative_path
|
||||
from B05_Profile.B05_Profile_Engine_Grade import GradeDesignOptions
|
||||
from B05_Profile.B05_Profile_Engine_Grade_Alignment import (
|
||||
AlignmentPolicy,
|
||||
build_curves,
|
||||
chainage_key,
|
||||
evaluate,
|
||||
)
|
||||
from B05_Profile.B05_Profile_Engine_Grade_Profile import rebuild_alignment_profile
|
||||
from B05_Profile.B05_Profile_Engine_Sections import (
|
||||
@@ -42,6 +39,7 @@ from B06_Section.B06_Section_Repository import (
|
||||
get_latest_section_options,
|
||||
get_longitudinal_section,
|
||||
)
|
||||
from common_util.common_util_calc_runner import run_calc
|
||||
from common_util.common_util_json import atomic_write_json
|
||||
from common_util.common_util_storage import resolve_stored_project_path
|
||||
|
||||
@@ -213,10 +211,6 @@ async def sync_uphill_overrides_into_designs(
|
||||
#: 반올림만 다른 같은 측점(m) — 서버가 관 측점을 0.01m 로 반올림.
|
||||
#: 화면 `applyIrregularStations` 0.006 과 짝.
|
||||
_SAME_STATION_M = 0.006
|
||||
#: 지반 종단으로 볼 꼭짓점 자리 계획선 · 지반 어긋남 가운데값(m) — 새 꼭짓점을 지반에 세울지(438).
|
||||
_SEAT_GROUND_M = 0.01
|
||||
#: 새 꼭짓점 이웃이 옮기기 전 계획고에서 뜨면 맞출 한도(m · 433-5).
|
||||
_SEAT_KEEP_M = 0.001
|
||||
|
||||
|
||||
def _seat_born_nodes(
|
||||
@@ -225,84 +219,36 @@ def _seat_born_nodes(
|
||||
born: list[float],
|
||||
held: list[float] = (),
|
||||
) -> list[dict[str, Any]]:
|
||||
"""새 측점(`born`)에 자동 계획선 규칙(원지반 지남 · `pass_through`)으로 꼭짓점을 세움(427).
|
||||
"""새 측점(`born`)에 자동 계획선 규칙(원지반 지남)으로 꼭짓점을 세움(427 · 433-5 · 438).
|
||||
|
||||
이웃 꼭짓점은 곡선 모양이 바뀌어 옮기기 전 계획고에서 1mm 넘게 뜨면 그 높이로 맞춤(433-5 ·
|
||||
바로 앞뒤부터 번지는 만큼) · 높이 고친 꼭짓점(`held` = 옮긴 뒤 `station_offsets` 측점)과 그
|
||||
밖 꼭짓점은 그대로 · 양 끝 밖 · 이미 꼭짓점 있는(0.006m 안) 자리는 건너뜀. 지반 = 저장
|
||||
선형 표본(`samples`) · 곡선 = 편집 없이(`pass_through` 와 같음). 옮기기 전 계획선이 꼭짓점
|
||||
대부분에서 지반을 안 지나면(균형 종단) 지반 대신 옮기기 전 계획선 높이(438). TS 짝 =
|
||||
`seatBornNodes`(B05_Profile/logic/B05_Profile_Grade_Seat.ts) · 거울 시험 =
|
||||
`test_427_1_follow_seat.py`.
|
||||
셈 = TS `seatBornNodes`(B05_Profile/logic/B05_Profile_Grade_Seat.ts) 한 벌 — 서버 Node 입구
|
||||
`profile.seat`(472-4 ①). 지반 = 저장 선형 표본(`samples`) · 정책 = 파이썬 기본값을 채운 한 벌.
|
||||
세울 자리가 없으면(양 끝 밖 · 이미 꼭짓점 0.006m 안) Node 를 안 부름. 거울 시험 =
|
||||
`test_427_1_follow_seat.py` · `test_472_4_follow_node.py`.
|
||||
"""
|
||||
samples = stored.get("samples") or []
|
||||
if not base or len(samples) < 2:
|
||||
return base
|
||||
first, last = float(base[0]["chainage_m"]), float(base[-1]["chainage_m"])
|
||||
fresh = [
|
||||
value
|
||||
for value in born
|
||||
if first < value < last
|
||||
if not any(
|
||||
first < value < last
|
||||
and all(abs(value - float(node["chainage_m"])) >= _SAME_STATION_M for node in base)
|
||||
]
|
||||
if not fresh:
|
||||
for value in born
|
||||
):
|
||||
return base
|
||||
ground_s = np.array([float(row["chainage_m"]) for row in samples], dtype=np.float64)
|
||||
ground_z = np.array([float(row["ground_elevation_m"]) for row in samples], dtype=np.float64)
|
||||
policy = AlignmentPolicy.from_dict(stored.get("policy") or {})
|
||||
pre_s = np.array([float(node["chainage_m"]) for node in stored["base_pvi"]], dtype=np.float64)
|
||||
pre_z = np.array([float(node["elevation_m"]) for node in stored["base_pvi"]], dtype=np.float64)
|
||||
pre_curves, _warnings = build_curves(pre_s, pre_z, policy)
|
||||
off = evaluate(pre_s, pre_z, pre_curves, pre_s) - np.interp(pre_s, ground_s, ground_z)
|
||||
on_ground = float(np.median(np.abs(off))) < _SEAT_GROUND_M
|
||||
|
||||
def seat_z(value: float) -> float:
|
||||
if on_ground:
|
||||
return float(np.interp(value, ground_s, ground_z))
|
||||
return float(evaluate(pre_s, pre_z, pre_curves, np.array([value]))[0])
|
||||
|
||||
rows = sorted(
|
||||
[(float(node["chainage_m"]), float(node["elevation_m"]), node) for node in base]
|
||||
+ [(value, seat_z(value), None) for value in fresh],
|
||||
key=lambda row: row[0],
|
||||
)
|
||||
base_s = np.array([row[0] for row in rows], dtype=np.float64)
|
||||
born_rows = np.array([row[2] is None for row in rows])
|
||||
# 다른 꼭짓점 목표 = 옮기기 전 계획선 그 자리 높이(433-5) · 맞출 수 있는 = 높이 안 고친 것
|
||||
target = np.where(
|
||||
born_rows,
|
||||
np.array([row[1] for row in rows], dtype=np.float64),
|
||||
evaluate(pre_s, pre_z, pre_curves, base_s),
|
||||
)
|
||||
can = np.array(
|
||||
[
|
||||
row[2] is not None and all(abs(row[0] - value) >= _SAME_STATION_M for value in held)
|
||||
for row in rows
|
||||
]
|
||||
)
|
||||
free = born_rows.copy()
|
||||
base_z = np.array([row[1] for row in rows], dtype=np.float64)
|
||||
for _ in range(len(rows)):
|
||||
for _ in range(20):
|
||||
curves, _warnings = build_curves(base_s, base_z, policy)
|
||||
error = np.where(free, target - evaluate(base_s, base_z, curves, base_s), 0.0)
|
||||
if float(np.max(np.abs(error))) < 1e-6:
|
||||
break
|
||||
base_z = base_z + error
|
||||
# 이웃 곡선 모양이 바뀌어 뜬 꼭짓점 — 그 꼭짓점도 맞춤(번지는 만큼)
|
||||
curves, _warnings = build_curves(base_s, base_z, policy)
|
||||
drift = (
|
||||
can & ~free & (np.abs(target - evaluate(base_s, base_z, curves, base_s)) > _SEAT_KEEP_M)
|
||||
)
|
||||
if not drift.any():
|
||||
break
|
||||
free |= drift
|
||||
return [
|
||||
node
|
||||
if node is not None and not keep
|
||||
else {**(node or {}), "chainage_m": round(float(s), 6), "elevation_m": round(float(z), 6)}
|
||||
for (s, _z, node), z, keep in zip(rows, base_z.tolist(), free.tolist())
|
||||
]
|
||||
seat = {
|
||||
"ground_s": [float(row["chainage_m"]) for row in samples],
|
||||
"ground_z": [float(row["ground_elevation_m"]) for row in samples],
|
||||
"policy": AlignmentPolicy.from_dict(stored.get("policy") or {}).as_dict(),
|
||||
"base_pvi": stored["base_pvi"],
|
||||
"base": base,
|
||||
"born": [float(value) for value in born],
|
||||
"held": [float(value) for value in held],
|
||||
}
|
||||
results = run_calc(["profile.seat"], {"seat": seat})
|
||||
if results is None:
|
||||
raise RuntimeError("꼭짓점 세움 실패 — 계산 Node 입구(profile.seat)")
|
||||
return results["profile.seat"]
|
||||
|
||||
|
||||
def _follow_moved_stations(
|
||||
|
||||
@@ -4,7 +4,7 @@
|
||||
* 바뀌어 옮기기 전 계획고에서 1mm 넘게 뜨면 그 높이로 맞춤(433-5 · 바로 앞뒤부터 번지는 만큼) · 높이 고친 꼭짓점은 그대로.
|
||||
* 옮기기 전 계획선이 꼭짓점 대부분에서 지반을 안 지나면(균형 종단) 지반 대신 옮기기 전 계획선 높이(438).
|
||||
*
|
||||
* 짝 = 파이썬 `B05_Profile_Router_Confirm._seat_born_nodes` · 세울 측점은 공용 `followMovedNodes`
|
||||
* 서버도 이 셈 — Node 입구 `profile.seat`(472-4 ① · 파이썬 `Router_Confirm._seat_born_nodes` 가 부름) · 세울 측점은 공용 `followMovedNodes`
|
||||
* (common_util_structure_station.ts)가 고름 · 거울 시험 = resources/tester/test_427_1_follow_seat.py.
|
||||
* ========================================================================== */
|
||||
|
||||
@@ -15,9 +15,9 @@ import {
|
||||
type AlignmentNode,
|
||||
} from "./B05_Profile_UI_Profile_Alignment";
|
||||
|
||||
/** 지반 종단으로 볼 꼭짓점 자리 계획선 · 지반 어긋남 가운데값(m) — 파이썬 `_SEAT_GROUND_M`. */
|
||||
/** 지반 종단으로 볼 꼭짓점 자리 계획선 · 지반 어긋남 가운데값(m) (438). */
|
||||
const SEAT_GROUND_M = 0.01;
|
||||
/** 새 꼭짓점 이웃이 옮기기 전 계획고에서 뜨면 맞출 한도(m · 433-5) — 파이썬 `_SEAT_KEEP_M`. */
|
||||
/** 새 꼭짓점 이웃이 옮기기 전 계획고에서 뜨면 맞출 한도(m · 433-5). */
|
||||
const SEAT_KEEP_M = 0.001;
|
||||
|
||||
/** `np.median` 짝. */
|
||||
|
||||
@@ -24,6 +24,7 @@ NODE_IDS = frozenset(
|
||||
"haul.plan",
|
||||
"excavation.trench",
|
||||
"corridor.mesh",
|
||||
"profile.seat",
|
||||
}
|
||||
)
|
||||
|
||||
|
||||
@@ -40,6 +40,7 @@ NODE_IDS = {
|
||||
"haul.plan",
|
||||
"excavation.trench",
|
||||
"corridor.mesh",
|
||||
"profile.seat",
|
||||
}
|
||||
|
||||
pytestmark = pytest.mark.skipif(
|
||||
|
||||
@@ -0,0 +1,59 @@
|
||||
"""PLAN 472-4 ① — 측점 열쇠 · 시설 따라감 · 추가 측점 · 꼭짓점 세움을 서버도 같은 TS(Node 입구)로 셈.
|
||||
|
||||
파이썬 짝을 걷고 서버 부름 자리는 `common_util_calc_runner.run_calc`(등록부 id) 한 길 —
|
||||
profile.seat `Router_Confirm._seat_born_nodes`(측점 병합 · 노선 저장 `_carry_profile`)
|
||||
고정 자료 = `fixtures/golden_472/detail.json`(472-1 · 읽기만).
|
||||
|
||||
./venv/Scripts/python.exe -m pytest resources/tester/test_472_4_follow_node.py -q
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import json
|
||||
import sys
|
||||
from pathlib import Path
|
||||
|
||||
import pytest
|
||||
|
||||
HERE = Path(__file__).resolve().parent
|
||||
ROOT = HERE.parents[1]
|
||||
sys.path.insert(0, str(ROOT))
|
||||
|
||||
from B05_Profile import B05_Profile_Router_Confirm as confirm # noqa: E402
|
||||
from common_util import common_util_calc_runner as runner # noqa: E402
|
||||
|
||||
pytestmark = pytest.mark.skipif(
|
||||
not (ROOT / "config" / "node_modules" / "vite").is_dir(),
|
||||
reason="프론트엔드 의존성 미설치",
|
||||
)
|
||||
|
||||
DETAIL = json.loads((HERE / "fixtures" / "golden_472" / "detail.json").read_text(encoding="utf-8"))
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def calls(monkeypatch) -> list[list[str]]:
|
||||
seen: list[list[str]] = []
|
||||
|
||||
def spy(ids, inputs):
|
||||
seen.append(list(ids))
|
||||
return runner.run_calc(ids, inputs)
|
||||
|
||||
monkeypatch.setattr(confirm, "run_calc", spy)
|
||||
return seen
|
||||
|
||||
|
||||
def test_꼭짓점_세움은_Node_입구(calls) -> None:
|
||||
stored = DETAIL["longitudinal"]["profile_alignment"]
|
||||
base = [node for node in stored["base_pvi"] if abs(node["chainage_m"] - 351.0) > 0.5]
|
||||
got = confirm._seat_born_nodes(stored, base, [351.0])
|
||||
assert calls == [["profile.seat"]]
|
||||
assert len(got) == len(base) + 1
|
||||
seated = next(node for node in got if abs(node["chainage_m"] - 351.0) < 1e-9)
|
||||
old = next(node for node in stored["base_pvi"] if abs(node["chainage_m"] - 351.0) < 1e-9)
|
||||
assert seated["elevation_m"] == pytest.approx(old["elevation_m"], abs=0.5)
|
||||
|
||||
|
||||
def test_세울_자리_없으면_Node_안_부름(calls) -> None:
|
||||
stored = DETAIL["longitudinal"]["profile_alignment"]
|
||||
assert confirm._seat_born_nodes(stored, stored["base_pvi"], [351.0]) == stored["base_pvi"]
|
||||
assert calls == []
|
||||
Reference in New Issue
Block a user