chore: import 3d-drawing history from local Code/3d-drawing (subfolder, history preserved)

This commit is contained in:
SeanJo committed 2026-10-27 13:30:09 +09:00
commit 7b754e810b
51 files changed
+20300

No files matched your search

+21
View File
@@ -0,0 +1,21 @@
#!/usr/bin/env python3
"""Task 5 (user decision 2026-10-21): no pattern-station data exists. Create a
PLACEHOLDER pattern table at 2 m segments over the modelled range 4+745..5+480,
cycling the six pattern names. Every row is marked '임의 배정 (placeholder)'.
The table is an editable CSV: replace the 'pattern' column later and rebuild.
Output: data/pattern_zones_placeholder.csv (created only if absent; never overwritten)."""
import csv
import os
import sys
ROOT = sys.argv[1]
OUT = f"{ROOT}/data/pattern_zones_placeholder.csv"
PATTERNS = ["NP-4AD-1-D-2", "NP-4BD-1-D-2", "NP-5AE-1-WL-4", "NP-5BE-1-WL-4", "NP-6A", "NP-6B"]
S0, S1, SEG = 4745, 5480, 2
if not os.path.exists(OUT):
with open(OUT, "w", newline="", encoding="utf-8") as fh:
w = csv.writer(fh)
w.writerow(["seg_from", "seg_to", "pattern", "status"])
for k, s in enumerate(range(S0, S1, SEG)):
w.writerow([s, min(s + SEG, S1), PATTERNS[k % len(PATTERNS)], "임의 배정 (placeholder, 실제 구간 아님)"])
print("pattern table:", OUT)
+206
View File
@@ -0,0 +1,206 @@
#!/usr/bin/env python3
"""Task 7: axis + standard-section sweep skeleton of the main line (본선), 4+745..5+480.
Inputs (all confirmed by the user, 2026-10-21):
- centreline nodes data/alignment_plan_001_centre.csv (midpoint of side ticks)
- profile data/profile_gl_002_draft.csv (A = 지반고 GL, B = 계획고 = 노면)
- section table DOC/03_설계/설계_표준단면치수표_2026-10-21_v0.1.00.md
R1 6,982 mm (lining inner), lining 300 mm -> outer R 7,282, excavation R 7,402
crown A = 6.615 m above road (F = 0.000 = road reference)
- cross slope: 4+745~5+105 -2 %, 5+105~5+165 linear -2 -> +2 %, 5+165~5+480 +2 %
Model (skeleton, not as-built):
- Section = arch (concentric centre c = 6.615 - 6.982) + vertical walls down to the road.
Walls and invert are APPROXIMATIONS and are flagged in the metadata. No invert.
- Superelevation: z += (S/100) * x across the section (sign convention ASSUMED, flagged).
- Coordinates: plan XY in m (station spacing 100 m matches plan units), origin at 4+745.
Outputs: model/tunnel_skeleton.glb, model/tunnel_skeleton_meta.json, data/verify_skeleton.json
"""
import csv
import json
import math
import sys
import numpy as np
import trimesh
ROOT = sys.argv[1] # Code/3d-drawing
C_CSV = f"{ROOT}/data/alignment_plan_001_centre.csv"
P_CSV = f"{ROOT}/data/profile_gl_002_draft.csv"
R1, R_OUT, R_EX = 6.982, 7.282, 7.402 # m (mm / 1000)
CROWN_A = 6.615 # m above road
CENTRE_C = CROWN_A - R1 # arch centre height above road (m)
S0, S1 = 4745, 5480 # modelled range (fully determined)
DS = 1.0 # station step for rings (m); pattern segments are 2 m
ANG = np.radians(np.linspace(40.0, 140.0, 41)) # arch arc, right springline -> left
def read_centre():
rows = [r for r in csv.DictReader(open(C_CSV, encoding="utf-8"))]
return np.array([[int(r["station_m"]), float(r["centre_x"]), float(r["centre_y"])] for r in rows])
def read_profile():
gl, dg = {}, {}
for r in csv.DictReader(open(P_CSV, encoding="utf-8")):
sta = int(r["station_anchor"])
(gl if float(r["across_m"]) < -25 else dg)[sta] = float(r["value"])
return gl, dg
def slope_pct(s):
if s < 5105: return -2.0
if s < 5165: return -2.0 + 4.0 * (s - 5105) / 60.0
return 2.0
cn = read_centre()
gl_d, dg_d = read_profile()
gl_s = np.array(sorted(gl_d)); gl_v = np.array([gl_d[k] for k in gl_s])
dg_s = np.array(sorted(dg_d)); dg_v = np.array([dg_d[k] for k in dg_s])
cn_s = cn[:, 0]
def axis_xy(s):
x = np.interp(s, cn_s, cn[:, 1]); y = np.interp(s, cn_s, cn[:, 2])
i = min(max(np.searchsorted(cn_s, s) - 1, 0), len(cn_s) - 2)
dx, dy = cn[i + 1, 1] - cn[i, 1], cn[i + 1, 2] - cn[i, 2]
L = math.hypot(dx, dy)
return x, y, dx / L, dy / L
ORIGIN = np.array(axis_xy(S0)[:2])
stations = np.arange(S0, S1 + 0.001, DS)
def section(R, s, slope):
"""ring (x lateral m, z above road m) for radius R, including superelevation."""
xs = np.concatenate([[R * math.cos(ANG[0])], R * np.cos(ANG), [R * math.cos(ANG[-1])]])
zs = np.concatenate([[0.0], CENTRE_C + R * np.sin(ANG), [0.0]])
zs = zs + slope * xs
return xs, zs
def ring_world(R, s):
x, y, tx, ty = axis_xy(s)
nx, ny = -ty, tx # left normal (horizontal)
zr = np.interp(s, dg_s, dg_v) # road (계획고 = 노면)
xs, zs = section(R, s, slope_pct(s) / 100.0)
P = np.column_stack([x - ORIGIN[0] + xs * nx, y - ORIGIN[1] + xs * ny, zr + zs])
return P
PAL = {"A": (42, 157, 143, 255), "B-1": (231, 111, 81, 255), "B-2": (244, 162, 97, 255),
"C-1": (233, 196, 106, 255), "C-2": (138, 177, 125, 255), "D-1": (38, 70, 83, 255),
"D-2": (100, 130, 160, 255), "P-2": (190, 110, 140, 255), "P-3": (150, 90, 60, 255),
"RP-1": (80, 170, 200, 255), "WL-1": (200, 200, 120, 255), "WL-2": (120, 90, 170, 255),
"개착부": (170, 170, 170, 255)}
ZONES = [(int(float(r["sta_from"])), int(float(r["sta_to"])), r["lining_pattern"], "엑셀 지보패턴표(당진방향)")
for r in csv.DictReader(open(f"{ROOT}/data/pattern_stations_dangjin.csv", encoding="utf-8"))]
def pattern_of(sta):
for a, b, pat, _ in ZONES:
if a <= sta < b:
return pat
return ZONES[-1][2]
def make_band_pattern(R, name):
rings = [ring_world(R, s) for s in stations]
n = len(rings[0]); m = len(rings)
V = np.vstack(rings)
F, C = [], []
for i in range(m - 1):
col = PAL[pattern_of(stations[i])]
for j in range(n - 1):
a = i * n + j; b = a + 1; c = a + n; d = c + 1
F += [[a, b, c], [b, d, c]]
C += [col, col]
mesh = trimesh.Trimesh(vertices=V, faces=np.array(F), process=False)
mesh.visual.face_colors = np.array(C, dtype=np.uint8)
mesh.metadata["name"] = name
mesh.metadata["ring_points"] = n
return mesh
def make_ribbon(z_fn, half_w, color, name):
V, F = [], []
for k, s in enumerate(stations):
x, y, tx, ty = axis_xy(s)
nx, ny = -ty, tx
zr = z_fn(s)
V.append([x - ORIGIN[0] - half_w * nx, y - ORIGIN[1] - half_w * ny, zr])
V.append([x - ORIGIN[0] + half_w * nx, y - ORIGIN[1] + half_w * ny, zr])
V = np.array(V)
for k in range(len(stations) - 1):
a = 2 * k; b = a + 1; c = a + 2; d = a + 3
F += [[a, c, b], [b, c, d]]
mesh = trimesh.Trimesh(vertices=V, faces=np.array(F), process=False)
mesh.visual.face_colors = np.tile(np.array(color, dtype=np.uint8), (len(F), 1))
mesh.metadata["name"] = name
return mesh
def make_band(R, color, name):
"""uniform-colour band (excavation outline)."""
rings = [ring_world(R, s) for s in stations]
n = len(rings[0]); m = len(rings)
V = np.vstack(rings)
F = []
for i in range(m - 1):
for j in range(n - 1):
a = i * n + j; b = a + 1; c = a + n; d = c + 1
F += [[a, b, c], [b, d, c]]
mesh = trimesh.Trimesh(vertices=V, faces=np.array(F), process=False)
mesh.visual.face_colors = np.tile(np.array(color, dtype=np.uint8), (len(F), 1))
mesh.metadata["name"] = name
return mesh
parts = [
make_band_pattern(R_OUT, "lining_outer_R7282"),
make_band_pattern(R1, "lining_inner_R6982"),
make_band(R_EX, (150, 120, 90, 110), "excavation_R7402"),
make_ribbon(lambda s: float(np.interp(s, dg_s, dg_v)), 5.0, (70, 70, 75, 255), "road_surface_계획고"),
make_ribbon(lambda s: float(np.interp(s, gl_s, gl_v)), 20.0, (110, 150, 90, 200), "ground_GL_지반고"),
]
scene = trimesh.Scene()
for p in parts:
scene.add_geometry(p, geom_name=p.metadata["name"])
glb = f"{ROOT}/model/tunnel_skeleton.glb"
scene.export(glb)
# ---- verification (assertions recorded, not forced) ----
checks = {}
# radius check in section plane: distance from arch centre (lateral=0, z=centre_c)
xs, zs = section(R1, S0, 0.0)
rad = np.hypot(xs[1:-1], zs[1:-1] - CENTRE_C)
checks["inner_radius_max_dev_m"] = float(np.max(np.abs(rad - R1)))
checks["lining_thickness_crown_m"] = round(R_OUT - R1, 3)
crown_z = ring_world(R1, 5000)[:, 2].max() - np.interp(5000, dg_s, dg_v)
checks["crown_above_road_m (expect 6.615 at S=0 station 5000)"] = round(float(crown_z), 4)
checks["road_z_4745_m"] = round(float(np.interp(S0, dg_s, dg_v)), 3)
checks["road_z_5480_m"] = round(float(np.interp(S1, dg_s, dg_v)), 3)
L_chord = float(np.sum([math.dist(cn[i, 1:], cn[i + 1, 1:]) for i in range(len(cn) - 1)]))
checks["plan_chord_length_total_m (4+700..5+500)"] = round(L_chord, 3)
checks["station_range_modelled"] = f"{S0} .. {S1} (ring step {DS} m, {len(stations)} rings)"
checks["pattern_zones (지보패턴표 당진방향)"] = len(ZONES)
checks["pattern_coverage_all_stations"] = bool(all(pattern_of(x) in PAL for x in stations))
checks["pattern_counts"] = {k: sum(1 for z in ZONES if z[2] == k) for k in PAL}
checks["glb_bytes"] = int(__import__("os").path.getsize(glb))
reload = trimesh.load(glb)
checks["glb_reload_geometries"] = len(reload.geometry)
json.dump(checks, open(f"{ROOT}/data/verify_skeleton.json", "w", encoding="utf-8"), ensure_ascii=False, indent=2)
meta = {
"model": "tunnel_skeleton.glb", "scope": f"{S0}~{S1} (4+745~5+480)",
"units": "m", "origin_plan_xy": [round(ORIGIN[0], 3), round(ORIGIN[1], 3)],
"sources": {"centreline": "C0060302-001 CA-ALGN-STSM (side-tick midpoint)",
"road_grade_B": "C0060302-002 CR-TABL-TEX2 (계획고=노면)",
"GL_A": "C0060302-002 CR-TABL-TEX2 (지반고)",
"section": "C0060303-001 CS-TABL-TEX1/2, CS-CONC-MAJR, CS-EXCV"},
"approximations": ["walls vertical down to road; invert not modelled",
"lining band thickness at walls is radial-consistent but not as-built",
"centreline and grade linearly interpolated between nodes",
"superelevation sign convention assumed (z += S/100 * x, x = left of tangent)"],
"layers": [p.metadata["name"] for p in parts],
"ring_step_m": DS, "ring_points": int(parts[0].metadata["ring_points"]), "first_station": S0,
"pattern_status": "지보패턴: 엑셀 참고_지보패턴현황 당진방향 표(4+740~5+500, 42행, 빈틈 없음) 적용. 출처 data/pattern_stations_dangjin.csv (사용자 결정 2026-10-21)",
"patterns": [{"from": a, "to": b, "pattern": pat} for a, b, pat, _ in ZONES],
"open": ["4+700~4+745 and 5+480~5+500 excluded ([미확정])", "지보패턴 측점표 적용(당진). 도면 E-1~2, RP-2, WL-3~4 는 당진 표에 없어 [미확정]. 대산 표 WL-4(4+758.85~4+768.85)는 참고만 [확인 필요]",
"lining reference recorded (meta.lining_reference, 04-004~009); sheet-to-station mapping [미확정]",
"[확정 2026-10-21] R1 6,982 mm 유지 (사용자 결정). 라이닝 도면 6장의 R1 6.892 m 표기는 확인 대상 오기로 기록", "pattern-group to excavation-pattern mapping: user explanation conflicts with filename (WL-4) - re-confirm [확인 필요]"]
}
_ls = json.load(open(f"{ROOT}/data/lining_spec.json", encoding="utf-8"))
meta["lining_reference"] = {
"source": _ls["source"], "concrete": "fck 27 MPa", "rebar": "SD400 (fy 400 MPa)", "aggregate_mm": 25,
"summary": "C0060304-004~009 | fck 27 MPa | SD400 | 입출구부 50 m 취약구간 철근 상세 (측점 대응 미확정)",
"sheets": [{"sheet": r["file"][:14], "groups": [[g["group"], g["kind"]] for g in r["pattern_details"]]} for r in _ls["sheets"]]
}
json.dump(meta, open(f"{ROOT}/model/tunnel_skeleton_meta.json", "w", encoding="utf-8"), ensure_ascii=False, indent=2)
print(json.dumps(checks, ensure_ascii=False, indent=1))
+329
View File
@@ -0,0 +1,329 @@
#!/usr/bin/env python3
"""Task 8: B-rep solids of the main line (본선) 4+745..5+480 -> STEP (Rhino/Autodesk) + GLB (web viewer).
Section geometry: standard section C0060303-001 (drawing 03-001), revision rev2 approved 2026-10-21.
- lining (라이닝): arch R6982 (inner) / R7282 (outer) about centre C; haunch arcs R3712/R3910 (inner),
R4454/R4699 (outer) run below road level as drawn; inner walls and inner chord (road level) as drawn.
Lining is symmetric (no superelevation). H/I boxes removed from lining (2D).
- excavation band (굴착 여유대): R7402 / R4574 / R4819 minus lining outer, above road level. Symmetric.
- road surface / 계획고 (노면, 포장 280 mm): cross slope S per station (superelevation). Only this part tilts.
- base / invert (기층·인버트, ASSUMED: not in drawing): region between lining outer and inner chord minus
pavement and side drains. Its top follows the tilted pavement bottom.
- side drains (측구): box 300 x 836 mm minus pipe r150 (drawing circles).
- H/I boxes (공동구, ASSUMED by user decision 2026-10-21).
- 시설한계 50 mm: REFERENCE polylines only (not solids), as decided by the user.
Coordinates: local section x = lateral along the LEFT normal of the alignment (sign convention flagged),
z = height above road centre. Superelevation: z += (S/100)*x (sign flagged).
Outputs: model/tunnel_solids.step, model/tunnel_solids.glb, model/tunnel_solids_meta.json, data/verify_solids.json
Usage: python3 scripts/build_solids.py "$PWD"
"""
import csv, json, math, os, sys, tempfile
import numpy as np
from shapely.geometry import Polygon, box
from build123d import Vector, Wire, Solid, Compound, Color, export_step, export_stl, import_step
import trimesh
ROOT = sys.argv[1]
MODE = sys.argv[2] if len(sys.argv) > 2 else 'all' # parts_a | parts_b | assemble (4 GB RAM: run in stages)
PARTS = f"{ROOT}/model/parts"
RANGE = (int(sys.argv[3]), int(sys.argv[4])) if len(sys.argv) > 4 else (0, 10**6) # run index range for parts_a
os.makedirs(PARTS, exist_ok=True)
C_CSV = f"{ROOT}/data/alignment_plan_001_centre.csv"
P_CSV = f"{ROOT}/data/profile_gl_002_draft.csv"
PAT_CSV = f"{ROOT}/data/pattern_stations_dangjin.csv"
S0, S1 = 4745, 5480
STEP_M = 10.0 # section spacing (m): ruled loft between sections (STEP size vs curvature)
CHUNK_M = 100
PIPE_SEG = 24
CLEAR_OFFSET = 0.05
CLEAR_LINES = 6
CLEAR_RIB_W = 0.02
# ---------- drawing geometry (mm) -> local m ----------
CX, CY, Y0 = 14126.29, 15305.02, 15672.0
def P(x, y): return ((x - CX) / 1000.0, (y - Y0) / 1000.0)
def arc(cx, cy, r, a0, a1, n=120):
return [(cx + r * math.cos(math.radians(a0 + (a1 - a0) * i / n)),
cy + r * math.sin(math.radians(a0 + (a1 - a0) * i / n))) for i in range(n + 1)]
def loc(pts): return [P(x, y) for x, y in pts]
WALL_L_INNER = [(8771, 15751), (8771, 15826), (8651, 16001), (8621, 16411), (8471, 16411),
(8171, 16411), (8045, 16411), (8034, 16446)]
WALL_R_INNER = [(19481, 15567), (19481, 15642), (19601, 15817), (19631, 16227), (19781, 16227),
(20181, 16227), (20251, 16239)]
INNER_MM = (WALL_L_INNER + arc(11621.3, 17406.9, 3712, 195.6, 140) + arc(CX, CY, 6982, 140, 40)
+ arc(16479.6, 17279.7, 3910, 40, -15.6) + WALL_R_INNER[::-1])
OUTER_MM = (arc(11959.9, 17122.8, 4454, 204.7, 140) + arc(CX, CY, 7282, 140, 40)
+ arc(16105.0, 16965.3, 4699, 40, -23.7))
EX_MM = (arc(11959.9, 17122.8, 4574, 204.0, 140) + arc(CX, CY, 7402, 140, 40)
+ arc(16105.0, 16965.3, 4819, 40, -23.0))
inner_poly = Polygon(loc(INNER_MM)).buffer(0)
outer_poly = Polygon(loc(OUTER_MM)).buffer(0)
ex_poly = Polygon(loc(EX_MM)).buffer(0)
def _fill_slivers(g, tol=1e-3):
if g.geom_type != 'Polygon': return g
return Polygon(g.exterior, [h for h in g.interiors if Polygon(h).area > tol])
ring = _fill_slivers(outer_poly.difference(inner_poly)) # 37 mm wall/haunch join leaves 1e-5 m2 slivers
CHORD_A, CHORD_B = P(8771, 15751), P(19481, 15567)
_k = (CHORD_B[1] - CHORD_A[1]) / (CHORD_B[0] - CHORD_A[0])
def chord_z(x): return CHORD_A[1] + _k * (x - CHORD_A[0])
below_chord = Polygon([(-9, chord_z(-9)), (9, chord_z(9)), (9, -5), (-9, -5)])
above_chord = Polygon([(-9, chord_z(-9)), (9, chord_z(9)), (9, 9), (-9, 9)])
XL = P(9526, 15764)[0]; XR = P(18726, 15580)[0]
T_FIX = -0.02
PAV_T = 0.28
def pav_corners(t):
return [(XL, t * XL), (XR, t * XR), (XR, t * XR - PAV_T), (XL, t * XL - PAV_T)]
pav_fix = Polygon(pav_corners(T_FIX))
def rect_local(x0_mm, x1_mm, y0_mm, y1_mm):
(a, b), (c, d) = P(x0_mm, y0_mm), P(x1_mm, y1_mm)
return box(min(a, c), min(b, d), max(a, c), max(b, d))
DRAIN_L = rect_local(9101, 9401, 14864, 15700)
DRAIN_R = rect_local(18851, 19151, 14680, 15520)
PIPE_L = (P(9251.3, 15064)[0], P(9251.3, 15064)[1], 0.150)
PIPE_R = (P(19001.3, 14880)[0], P(19001.3, 14880)[1], 0.150)
HBOX = rect_local(8171, 8471, 16051, 16411)
IBOX = rect_local(19781, 20181, 15867, 16227)
lin_full = _fill_slivers(ring.intersection(above_chord))
assert lin_full.geom_type == 'Polygon' and len(lin_full.interiors) == 0, f"lining topology: {lin_full.geom_type}"
LINING_POLY = lin_full.simplify(0.01, preserve_topology=True) # 10 mm: removes 30 um edges (loft invalid) and cuts ~700 -> ~60 section vertices (STEP size)
EXCAV_POLY = ex_poly.difference(outer_poly).intersection(box(-9, 0.0, 9, 9)).simplify(0.01, preserve_topology=True)
assert EXCAV_POLY.geom_type == 'Polygon' and len(EXCAV_POLY.interiors) == 0, f"excavation topology: {EXCAV_POLY.geom_type}"
base_fix = ring.intersection(below_chord).difference(pav_fix).difference(DRAIN_L).difference(DRAIN_R)
assert base_fix.geom_type == 'Polygon' and len(base_fix.interiors) == 0, f"base topology: {base_fix.geom_type}"
BASE_PTS_FIX = np.array(base_fix.exterior.coords)[:-1]
def _find(pt):
d = np.hypot(BASE_PTS_FIX[:, 0] - pt[0], BASE_PTS_FIX[:, 1] - pt[1])
i = int(np.argmin(d)); assert d[i] < 1e-6, f"pavement corner not on base boundary: {d[i]}"
return i
IDX_BL = _find((XL, T_FIX * XL - PAV_T)); IDX_BR = _find((XR, T_FIX * XR - PAV_T))
def base_pts(t):
pts = BASE_PTS_FIX.copy()
pts[IDX_BL, 1] = t * XL - PAV_T
pts[IDX_BR, 1] = t * XR - PAV_T
return pts
LIN_PTS = np.array(LINING_POLY.exterior.coords)[:-1]
EXC_PTS = np.array(EXCAV_POLY.exterior.coords)[:-1]
def PIPE_PTS(cx, cz, r):
return np.array([(cx + r * math.cos(2 * math.pi * k / PIPE_SEG), cz + r * math.sin(2 * math.pi * k / PIPE_SEG)) for k in range(PIPE_SEG)])
DRAIN_L_PTS = np.array(DRAIN_L.exterior.coords)[:-1]
DRAIN_R_PTS = np.array(DRAIN_R.exterior.coords)[:-1]
HBOX_PTS = np.array(HBOX.exterior.coords)[:-1]
IBOX_PTS = np.array(IBOX.exterior.coords)[:-1]
# ---------- alignment, profile ----------
cn = np.array([[int(r["station_m"]), float(r["centre_x"]), float(r["centre_y"])]
for r in csv.DictReader(open(C_CSV, encoding="utf-8"))])
dg_d = {}
for r in csv.DictReader(open(P_CSV, encoding="utf-8")):
if float(r["across_m"]) >= -25:
dg_d[int(r["station_anchor"])] = float(r["value"])
dg_s = np.array(sorted(dg_d)); dg_v = np.array([dg_d[k] for k in dg_s])
cn_s = cn[:, 0]
def axis(s):
x = np.interp(s, cn_s, cn[:, 1]); y = np.interp(s, cn_s, cn[:, 2])
i = min(max(np.searchsorted(cn_s, s) - 1, 0), len(cn_s) - 2)
dx, dy = cn[i + 1, 1] - cn[i, 1], cn[i + 1, 2] - cn[i, 2]
L = math.hypot(dx, dy)
return x, y, dx / L, dy / L
ORX, ORY = axis(S0)[0], axis(S0)[1]
def slope_pct(s):
if s < 5105: return -2.0
if s < 5165: return -2.0 + 4.0 * (s - 5105) / 60.0
return 2.0
def world(s, x, z):
ax, ay, tx, ty = axis(s)
nx, ny = -ty, tx
zr = float(np.interp(s, dg_s, dg_v))
return (ax - ORX + x * nx, ay - ORY + x * ny, zr + z)
# ---------- B-rep helpers ----------
def loft(stations, pts2_fn):
wires = []
for s in stations:
pts3 = [world(s, float(x), float(z)) for x, z in pts2_fn(s)]
wires.append(Wire.make_polygon([Vector(*p) for p in pts3], close=True))
sol = Solid.make_loft(wires, ruled=True)
if not sol.is_valid: # end caps of lofts with near-degenerate edges; ShapeFix repairs them
sol = sol.fix()
return sol
def stations_of(a, b, step=STEP_M):
n = max(1, int(round((b - a) / step)))
return [float(v) for v in np.linspace(a, b, n + 1)]
def chunks():
out = []; a = S0
while a < S1:
b = min(a + CHUNK_M, S1); out.append((a, b)); a = b
return out
PAL = {"A": (42, 157, 143), "B-1": (231, 111, 81), "B-2": (244, 162, 97), "C-1": (233, 196, 106),
"C-2": (138, 177, 125), "D-1": (38, 70, 83), "D-2": (100, 130, 160), "P-2": (190, 110, 140),
"P-3": (150, 90, 60), "RP-1": (80, 170, 200), "WL-1": (200, 200, 120), "WL-2": (120, 90, 170),
"개착부": (170, 170, 170)}
COL = {"base": (141, 110, 99), "pav": (96, 125, 139), "drain": (30, 136, 229), "hi": (253, 216, 53),
"clear": (255, 111, 0)}
runs = []
for r in csv.DictReader(open(PAT_CSV, encoding="utf-8")):
a, b = int(float(r["sta_from"])), int(float(r["sta_to"])); pat = r["lining_pattern"]
a2, b2 = max(a, S0), min(b, S1)
if b2 > a2: runs.append((a2, b2, pat))
runs.sort()
assert runs[0][0] == S0 and runs[-1][1] == S1, f"pattern coverage {runs[0][0]}..{runs[-1][1]}"
for (a1, b1, _), (a2, _, _) in zip(runs, runs[1:]):
assert b1 == a2, f"pattern gap/overlap at {b1} vs {a2}"
solids, meta_c, clear_wires = [], [], []
def add(sol, cid, label, group, color, pattern=None, a=None, b=None):
if not sol.is_valid:
sol = sol.fix() # booleans on lofts can leave cap faces invalid
assert sol.is_valid, f"invalid solid {cid}"
sol.label = cid; sol.color = Color(*[c / 255 for c in color])
solids.append(sol)
meta_c.append({"id": cid, "label": label, "group": group, "pattern": pattern,
"from_sta": a, "to_sta": b, "volume_m3": round(sol.volume, 4), "color": list(color)})
for i, (a, b, pat) in enumerate(runs):
if MODE != 'parts_a' or not (RANGE[0] <= i < RANGE[1]): continue
print('lining run', i + 1, pat, a, b, flush=True)
lin_run = loft(stations_of(a, b), lambda s: LIN_PTS)
lin_run = lin_run - loft(stations_of(a, b), lambda s: HBOX_PTS) - loft(stations_of(a, b), lambda s: IBOX_PTS)
add(lin_run, f"LIN_{i+1:02d}_{pat}", f"라이닝 {pat} {a/1000:.3f}~{b/1000:.3f}",
"라이닝", PAL[pat], pat, a, b)
for i, (a, b, pat) in enumerate(runs):
if MODE != 'parts_a' or not (RANGE[0] <= i < RANGE[1]): continue
add(loft(stations_of(a, b), lambda s: EXC_PTS), f"EXC_{i+1:02d}_{pat}", f"굴착 여유대 {pat} {a/1000:.3f}~{b/1000:.3f}",
"굴착면", (150, 120, 90), pat, a, b)
for i, (a, b, pat) in enumerate(runs): # 기타 구성요소도 지보패턴 구간(run) 경계에서 분할
if MODE != 'parts_b' or not (RANGE[0] <= i < RANGE[1]): continue
sts = stations_of(a, b)
rid = f"{i+1:02d}_{pat}"
rng = f"{a/1000:.3f}~{b/1000:.3f}"
add(loft(sts, lambda s: np.array(pav_corners(slope_pct(s) / 100.0))), f"PAV_{rid}", f"노면 계획고 포장 {pat} {rng}",
"노면(계획고)", COL["pav"], pat, a, b)
add(loft(sts, lambda s: base_pts(slope_pct(s) / 100.0)), f"BASE_{rid}", f"기층·인버트(가정) {pat} {rng}",
"기층·인버트(가정)", COL["base"], pat, a, b)
for side, dpts, pipe in (("L", DRAIN_L_PTS, PIPE_L), ("R", DRAIN_R_PTS, PIPE_R)):
bx = loft(sts, lambda s, d=dpts: d)
pp = loft(sts, lambda s, p=pipe: PIPE_PTS(p[0], p[1], p[2]))
add(bx - pp, f"DRN{side}_{rid}", f"측구 {side} {pat} {rng}", "배수(측구)", COL["drain"], pat, a, b)
add(loft(sts, lambda s: HBOX_PTS), f"HBOX_{rid}", f"공동구 H(가정) {pat} {rng}", "공동구(가정)", COL["hi"], pat, a, b)
add(loft(sts, lambda s: IBOX_PTS), f"IBOX_{rid}", f"공동구 I(가정) {pat} {rng}", "공동구(가정)", COL["hi"], pat, a, b)
# 시설한계 50 mm reference lines (wires, not solids)
clr = inner_poly.buffer(CLEAR_OFFSET).exterior
clr_pts = [(x, z) for x, z in list(clr.coords)[:-1] if z >= chord_z(x) - 1e-9 and abs(x) <= 5.4]
idxs = np.round(np.linspace(0, len(clr_pts) - 1, CLEAR_LINES)).astype(int)
clear_lines = []
sts_all = stations_of(S0, S1)
for k, ii in enumerate(idxs):
x0, z0 = clr_pts[ii]
pts3 = [world(s, x0, z0) for s in sts_all]
clear_lines.append(pts3)
if MODE != 'parts_b' or RANGE[0] != 0:
continue
w = Wire.make_polygon([Vector(*p) for p in pts3], close=False)
w.label = f"REF_CLEAR_{k+1}"; w.color = Color(1.0, 0.44, 0.0)
clear_wires.append(w)
meta_c.append({"id": f"REF_CLEAR_{k+1}", "label": f"시설한계 50 mm 참조선 {k+1}", "group": "시설한계(참조선)",
"pattern": None, "from_sta": S0, "to_sta": S1, "volume_m3": 0, "color": list(COL["clear"]), "reference": True})
if MODE in ('parts_a', 'parts_b'):
tag = f'lin_exc_{RANGE[0]:02d}' if MODE == 'parts_a' else f'oth_{RANGE[0]:02d}'
export_step(Compound(children=solids + clear_wires, label=tag), f"{PARTS}/{tag}.step")
json.dump(meta_c, open(f"{PARTS}/{tag}.json", 'w', encoding='utf-8'), ensure_ascii=False, indent=1)
print('parts written', tag, len(solids), 'solids', len(clear_wires), 'ref wires')
sys.exit(0)
# ---- post-processing modes (each call < 180 s, 4 GB RAM) ----
import glob
TAGS = sorted(os.path.basename(x)[:-5] for x in glob.glob(f"{PARTS}/*.json"))
from build123d import Solid as _Solid
if MODE == 'mesh': # python build_solids.py ROOT mesh <tag>
tag = sys.argv[3]
imp = import_step(f"{PARTS}/{tag}.step")
items = [x for x in (imp.children or list(imp.solids())) if isinstance(x, _Solid)]
pj = [m for m in json.load(open(f"{PARTS}/{tag}.json", encoding='utf-8')) if not m.get('reference')]
assert len(items) == len(pj), f"{tag}: {len(items)} solids vs {len(pj)} meta"
sc = trimesh.Scene(); tmp = tempfile.mkdtemp(prefix='stl_')
for k, (sol, mm) in enumerate(zip(items, pj)):
stl = os.path.join(tmp, f"s{k}.stl")
export_stl(sol, stl, tolerance=0.002, angular_tolerance=0.2)
m = trimesh.load(stl, force='mesh')
m.visual.face_colors = np.tile(np.array(list(mm['color']) + [255], dtype=np.uint8), (len(m.faces), 1))
sc.add_geometry(m, geom_name=mm['id'])
sc.export(f"{PARTS}/{tag}.glb")
print('mesh', tag, len(sc.geometry), 'geometries'); sys.exit(0)
if MODE == 'step': # python build_solids.py ROOT step
allitems = []
for tag in TAGS:
imp = import_step(f"{PARTS}/{tag}.step")
allitems += list(imp.children or list(imp.solids()))
os.makedirs(f"{ROOT}/model", exist_ok=True)
export_step(Compound(children=allitems, label="본선 4+745~5+480 표준단면 모델"), f"{ROOT}/model/tunnel_solids.step")
print('step merged', len(allitems), 'items', os.path.getsize(f"{ROOT}/model/tunnel_solids.step"), 'bytes'); sys.exit(0)
if MODE == 'final': # python build_solids.py ROOT final
meta_c = []
for tag in TAGS:
meta_c += json.load(open(f"{PARTS}/{tag}.json", encoding='utf-8'))
scene = trimesh.Scene()
for tag in TAGS:
g = trimesh.load(f"{PARTS}/{tag}.glb")
for name, geom in g.geometry.items():
scene.add_geometry(geom, geom_name=name)
glb_faces = {}
for k, pts3 in enumerate(clear_lines): # 시설한계 reference ribbons (visual only; exact wires in STEP)
P3 = np.array(pts3); V = []; F = []
for i in range(len(P3)):
d = P3[min(i + 1, len(P3) - 1)] - P3[max(i - 1, 0)]
d = d / (np.linalg.norm(d) + 1e-12)
w = np.cross(d, np.array([0, 0, 1.0])); w = w / (np.linalg.norm(w) + 1e-12) * CLEAR_RIB_W
V += [P3[i] - w, P3[i] + w]
for i in range(len(P3) - 1):
a0, b0, c0, d0 = 2 * i, 2 * i + 1, 2 * i + 2, 2 * i + 3
F += [[a0, c0, b0], [b0, c0, d0]]
m = trimesh.Trimesh(vertices=np.array(V), faces=np.array(F), process=False)
m.visual.face_colors = np.tile(np.array([255, 111, 0, 255], dtype=np.uint8), (len(m.faces), 1))
scene.add_geometry(m, geom_name=f"REF_CLEAR_{k+1}")
glb_path = f"{ROOT}/model/tunnel_solids.glb"
scene.export(glb_path)
step_path = f"{ROOT}/model/tunnel_solids.step"
solid_meta = [x for x in meta_c if not x.get('reference')]
meta = {
"model": ["tunnel_solids.step (STEP B-rep, Rhino/Autodesk)", "tunnel_solids.glb (web viewer, same B-rep tessellated)"],
"scope": f"{S0}~{S1} (4+745~5+480)", "units": "m",
"origin_plan_xy": [round(ORX, 3), round(ORY, 3)],
"sections": {"lining": "rev2: 라이닝 R6982/R7282, 하단 호 도면 끝점까지", "excavation": "굴착 여유대 R7402/R4574/R4819 (노면 위)",
"road": "계획고 노면 포장 280 mm, 측점별 편경사 S (노면만)", "base": "기층·인버트: 가정 (도면 미표현)",
"drains": "측구 300x836 mm, 관 r150", "hi": "H/I 박스: 공동구 가정(사용자 결정)", "clearance": "시설한계 50 mm 참조선 (솔리드 아님)"},
"assumptions": ["기층·인버트 형상·두께는 가정 (도면 미표현, 사용자 결정 '가정하여 생성')",
"H/I 박스 = 공동구 가정 (사용자 결정)",
"편경사 부호 z = (S/100)·x, x = 접선 좌측 법선 (부호 확인 필요)",
"라이닝·굴착 여유대는 대칭 (편경사 미적용)",
"측점 간 축선은 선형 보간, 단면은 10 m 간격 Ruled loft (곡선부 근사)",
"라이닝 H/I 박스 제외 부분은 3D 불리언 차감 (공동구 가정)"],
"pattern_runs": [{"id": f"LIN_{i+1:02d}_{p}", "pattern": p, "from": a, "to": b} for i, (a, b, p) in enumerate(runs)],
"components": meta_c,
"open": ["E-1, E-2, RP-2, WL-3 패턴 배치 [미확정] (당진 표 밖)", "NP-5BE-1-WL-4 해석 [확인 필요]",
"04 공동구·배수 도면 상세 미반영 (측구·공동구 치수는 03-001 단면 기준)", "편경사 부호(좌·우) 확인 필요"]
}
json.dump(meta, open(f"{ROOT}/model/tunnel_solids_meta.json", "w", encoding="utf-8"), ensure_ascii=False, indent=2)
ver = {"solid_count": len(solid_meta), "runs": len(runs), "pattern_coverage": f"{runs[0][0]}..{runs[-1][1]} contiguous",
"lining_runs": sum(1 for x in solid_meta if x['group'] == '라이닝'),
"excavation_runs": sum(1 for x in solid_meta if x['group'] == '굴착면'),
"volume_sum_m3": round(sum(x['volume_m3'] for x in solid_meta), 3),
"step_bytes": os.path.getsize(step_path), "glb_bytes": os.path.getsize(glb_path),
"glb_geometries": len(trimesh.load(glb_path).geometry)}
def _overlap(sample):
base = Polygon(base_pts(slope_pct(sample) / 100.0)); pav = Polygon(pav_corners(slope_pct(sample) / 100.0))
return {"lining_vs_excavation": round(LINING_POLY.intersection(EXCAV_POLY).area, 9),
"base_vs_pavement": round(base.intersection(pav).area, 9),
"lining_vs_base": round(LINING_POLY.intersection(base).area, 9),
"pavement_vs_lining": round(pav.intersection(LINING_POLY).area, 9),
"base_vs_drainL": round(base.intersection(DRAIN_L).area, 9),
"base_vs_drainR": round(base.intersection(DRAIN_R).area, 9)}
ver["section_overlap_m2_by_station"] = {str(st): _overlap(st) for st in [4745, 5105, 5165, 5480]}
json.dump(ver, open(f"{ROOT}/data/verify_solids.json", "w", encoding="utf-8"), ensure_ascii=False, indent=2)
print(json.dumps(ver, ensure_ascii=False, indent=1)); sys.exit(0)
+34
View File
@@ -0,0 +1,34 @@
#!/usr/bin/env python3
"""Compare two plan sheets layer-by-layer and list elevation-like numeric
texts (decimal values in 0..1000) per layer, to locate ground levels (지반고).
Read-only on the source DXF."""
import collections
import re
import sys
import ezdxf
num = re.compile(r"^-?\d{1,4}\.\d{1,3}$")
def scan(path):
msp = ezdxf.readfile(path).modelspace()
layers = collections.Counter(e.dxf.layer for e in msp)
el = collections.defaultdict(list)
for e in msp.query("TEXT"):
s = e.dxf.text.strip()
if num.match(s) and 0 <= float(s) <= 1000:
el[e.dxf.layer].append((round(e.dxf.insert.x, 1), round(e.dxf.insert.y, 1), s))
return layers, el
la, ea = scan(sys.argv[1])
lb, eb = scan(sys.argv[2])
print("layers only in 001:", sorted(set(la) - set(lb)))
print("layers only in 002:", sorted(set(lb) - set(la)))
for lay in sorted(set(la) | set(lb)):
if la[lay] != lb[lay]:
print(f" count diff {lay}: 001={la[lay]} 002={lb[lay]}")
print("== elevation-like numeric texts (layer: n, sample)")
for name, el in (("001", ea), ("002", eb)):
for lay, items in sorted(el.items(), key=lambda kv: -len(kv[1])):
print(f" {name} {lay}: n={len(items)} sample={sorted(items)[:6]}")
+24
View File
@@ -0,0 +1,24 @@
#!/usr/bin/env python3
"""Task 4: dump the variable table of the standard section (C0060303-001) as a
coordinate grid. Rows = cross slope (S, 편경사) per user answer 2026-10-21; the
first row = variable names. Every TEXT on the table layers is kept (no regex
filter), so that the S column and the header row can be checked."""
import csv
import sys
from collections import defaultdict
import ezdxf
SRC, OUT = sys.argv[1], sys.argv[2]
msp = ezdxf.readfile(SRC).modelspace()
cells = [(round(e.dxf.insert.x, 1), round(e.dxf.insert.y, 1), e.dxf.text.strip())
for e in msp.query("TEXT") if e.dxf.layer in ("CS-TABL-TEX1", "CS-TABL-TEX2")]
rows = defaultdict(list)
for x, y, s in cells:
rows[round(y / 50) * 50].append((x, s)) # bucket by ~50 mm
with open(OUT, "w", newline="", encoding="utf-8") as fh:
w = csv.writer(fh)
for y in sorted(rows, reverse=True):
line = [s for x, s in sorted(rows[y])]
w.writerow([y] + line)
print(f"Y{y:>7}: " + " | ".join(f"{x:.0f}:{s}" for x, s in sorted(rows[y])))
+43
View File
@@ -0,0 +1,43 @@
#!/usr/bin/env python3
"""Task 3 exploration: layer usage and long polylines / text clusters in a
plan-profile DXF, to locate the tunnel centreline (본선 축선) candidates."""
import collections
import json
import sys
import ezdxf
path = sys.argv[1]
doc = ezdxf.readfile(path)
msp = doc.modelspace()
by_layer = collections.Counter(e.dxf.layer for e in msp)
print("layers used:", len(by_layer))
for name, n in by_layer.most_common(25):
print(f" {n:6d} {name}")
# long polylines = candidate alignment / outline strings
polys = []
for e in msp:
if e.dxftype() == "LWPOLYLINE":
pts = list(e.get_points("xy"))
if len(pts) >= 20:
xs = [p[0] for p in pts]; ys = [p[1] for p in pts]
polys.append({"layer": e.dxf.layer, "n": len(pts),
"x": [round(min(xs), 1), round(max(xs), 1)],
"y": [round(min(ys), 1), round(max(ys), 1)]})
polys.sort(key=lambda d: -d["n"])
print("long LWPOLYLINE (>=20 vertices):", len(polys))
for p in polys[:12]:
print(" ", p)
# text that looks like station (STA) or level (EL) labels
import re
pat = re.compile(r"(STA|No\.?|EL|E\.L|\d+\+\d+|\+\d{2,3}\.\d{3}|\d{2,3}\.\d{2,3})", re.I)
hits = []
for e in msp:
if e.dxftype() in ("TEXT", "MTEXT"):
s = e.dxf.text if e.dxftype() == "TEXT" else e.plain_text()
if s and pat.search(s):
loc = e.dxf.insert if e.dxftype() == "TEXT" else e.dxf.insert
hits.append((round(loc.x, 1), round(loc.y, 1), s.strip()[:40]))
print("station/level-like texts:", len(hits))
for h in sorted(hits)[:25]:
print(" ", h)
+89
View File
@@ -0,0 +1,89 @@
#!/usr/bin/env python3
"""Explore the concrete-lining structure drawings (04-004 .. 04-009).
Read-only on the source RAG folder. For each DXF prints layers with entity
counts, INSERT block names, dimension measurements and every text string with
its position, and writes a JSON dump for later use.
Usage: explore_lining.py <RAG_04_DIR> <OUT_JSON> [max_texts_per_file]
"""
import collections
import glob
import json
import os
import sys
import ezdxf
from ezdxf import recover
RAG_04 = sys.argv[1]
OUT_JSON = sys.argv[2]
MAX_TEXT = int(sys.argv[3]) if len(sys.argv) > 3 else 400
def load(path):
try:
return ezdxf.readfile(path), None
except Exception as exc:
try:
doc, _ = recover.readfile(path)
return doc, f"recovered after strict error: {type(exc).__name__}"
except Exception as exc2:
return None, f"{type(exc).__name__} | recover: {type(exc2).__name__}: {exc2}"
def explore(path):
rec = {"file": os.path.basename(path)}
doc, note = load(path)
if doc is None:
rec["error"] = note
return rec
rec["note"] = note
msp = doc.modelspace()
layers = collections.Counter()
counts = collections.Counter()
for e in msp:
counts[e.dxftype()] += 1
layers[e.dxf.layer] += 1
rec["layers"] = dict(layers.most_common())
rec["entity_counts"] = dict(counts.most_common())
rec["inserts"] = dict(collections.Counter(e.dxf.name for e in msp if e.dxftype() == "INSERT").most_common(30))
dims, texts = [], []
for e in msp:
t = e.dxftype()
if t == "DIMENSION":
try:
val = round(float(e.get_measurement()), 3)
except Exception:
val = None
dims.append({"layer": e.dxf.layer, "value": val, "text": e.dxf.get("text", "")})
elif t in ("TEXT", "MTEXT"):
s = e.dxf.text if t == "TEXT" else e.plain_text()
if s and s.strip():
pos = e.dxf.insert
texts.append({"layer": e.dxf.layer, "x": round(pos.x, 2), "y": round(pos.y, 2),
"text": s.strip().replace("\n", " | ")[:200]})
rec["dimensions"] = dims[:200]
rec["texts_total"] = len(texts)
rec["texts"] = texts[:MAX_TEXT]
return rec
def main():
files = sorted(glob.glob(os.path.join(RAG_04, "C0060304-00[4-9]*.dxf")))
out = [explore(f) for f in files]
with open(OUT_JSON, "w", encoding="utf-8") as fh:
json.dump(out, fh, ensure_ascii=False, indent=1)
for r in out:
print("==", r["file"])
if "error" in r:
print(" ERROR", r["error"])
continue
print(" note:", r["note"])
print(" entities:", r["entity_counts"])
print(" layers:", dict(list(r["layers"].items())[:25]))
print(" inserts:", r["inserts"])
print(" dims:", len(r["dimensions"]), "texts:", r["texts_total"])
if __name__ == "__main__":
main()
+25
View File
@@ -0,0 +1,25 @@
#!/usr/bin/env python3
"""Task 4 exploration: layers, arcs (radius + centre) and dimension-like texts
of the standard section drawing (C0060303-001). Read-only."""
import collections
import re
import sys
import ezdxf
from ezdxf import bbox
msp = ezdxf.readfile(sys.argv[1]).modelspace()
print("layers:", collections.Counter(e.dxf.layer for e in msp).most_common(12))
ext = bbox.extents(msp)
print("extents:", [round(v, 1) for v in ext.extmin], [round(v, 1) for v in ext.extmax])
print("== ARCs (layer, centre, radius, start/end deg) sorted by radius")
arcs = sorted(msp.query("ARC"), key=lambda e: -e.dxf.radius)
for e in arcs[:14]:
c = e.dxf.center
print(f" {e.dxf.layer:14s} c=({c.x:9.2f},{c.y:9.2f}) R={e.dxf.radius:9.2f} {e.dxf.start_angle:7.1f}->{e.dxf.end_angle:7.1f}")
print("== R-labels and numeric dims")
pat = re.compile(r"^(R\d'?|\d{2,5}(\.\d+)?|\d+\.\d+%?|-?\d+\.\d+)$")
for e in msp.query("TEXT"):
s = e.dxf.text.strip()
if pat.match(s) or s.startswith("R"):
print(f" {e.dxf.layer:14s} ({e.dxf.insert.x:9.2f},{e.dxf.insert.y:9.2f}) {s}")
@@ -0,0 +1,81 @@
#!/usr/bin/env python3
"""Task 3 (HITL draft): main-line centreline station table from a plan sheet.
Method (layers CA-ALGN-TEXT labels and CA-ALGN-STSM station ticks):
1. Station labels: 'N+mmm' is explicit; bare numbers take the km of the next
explicit label when they precede it (700 before 5+000 -> 4+700), otherwise
the km of the previous explicit label.
2. Each station has two labels (one per side of the road). Each label is
paired with its nearest tick; the centreline point is the midpoint of the
two tick points.
3. Checks reported (not forced): side offset per station, and consecutive
centreline distance vs station difference (residual).
DRAFT for HITL review. No value is adjusted to fit."""
import csv
import math
import re
import sys
from collections import defaultdict
import ezdxf
SRC, OUT = sys.argv[1], sys.argv[2]
msp = ezdxf.readfile(SRC).modelspace()
explicit_re = re.compile(r"^(\d+)\+(\d{3})$")
labels = [(e.dxf.insert.x, e.dxf.insert.y, e.dxf.text.strip())
for e in msp.query("TEXT") if e.dxf.layer == "CA-ALGN-TEXT"]
ticks = []
for e in msp.query("LINE"):
if e.dxf.layer == "CA-ALGN-STSM":
a, b = e.dxf.start, e.dxf.end
ticks.append(((a.x + b.x) / 2, (a.y + b.y) / 2))
labels = [l for l in labels if explicit_re.match(l[2]) or l[2].isdigit()]
labels.sort(key=lambda t: -t[1]) # along the sheet, NNE -> SSW
explicit_idx = [i for i, l in enumerate(labels) if explicit_re.match(l[2])]
station_of = {}
for i, (x, y, s) in enumerate(labels):
if explicit_re.match(s):
m = explicit_re.match(s)
station_of[i] = int(m.group(1)) * 1000 + int(m.group(2))
continue
v = int(s)
nxt = next((j for j in explicit_idx if j > i), None)
prv = next((j for j in reversed(explicit_idx) if j < i), None)
if prv is not None:
km = int(explicit_re.match(labels[prv][2]).group(1))
else:
km = int(explicit_re.match(labels[nxt][2]).group(1)) - 1
station_of[i] = km * 1000 + v
groups = defaultdict(list)
for i, (x, y, s) in enumerate(labels):
groups[station_of[i]].append((x, y))
def nearest_tick(pt):
return min(ticks, key=lambda t: math.hypot(t[0] - pt[0], t[1] - pt[1]))
centre = []
for sta in sorted(groups):
pts = groups[sta]
if len(pts) != 2:
continue
t1, t2 = nearest_tick(pts[0]), nearest_tick(pts[1])
side_gap = math.dist(t1, t2)
mid = ((t1[0] + t2[0]) / 2, (t1[1] + t2[1]) / 2)
centre.append((sta, mid[0], mid[1], side_gap))
with open(OUT, "w", newline="", encoding="utf-8") as fh:
w = csv.writer(fh)
w.writerow(["station_m", "centre_x", "centre_y", "side_gap_m"])
for sta, x, y, g in centre:
w.writerow([sta, round(x, 3), round(y, 3), round(g, 3)])
print(f"stations={len(centre)} (labels={len(labels)}, ticks={len(ticks)})")
prev = None
for sta, x, y, g in centre:
step = "" if prev is None else f" d={math.dist(prev[1:3], (x, y)):8.3f} dSTA={sta - prev[0]:5d} res={math.dist(prev[1:3], (x, y)) - (sta - prev[0]):+.3f}"
print(f" STA {sta:>5} ({x:12.3f}, {y:12.3f}) side_gap={g:6.2f}{step}")
prev = (sta, x, y)
+98
View File
@@ -0,0 +1,98 @@
#!/usr/bin/env python3
"""Extract structured lining-reinforcement data from the concrete-lining
structure drawings 04-004..009 (read-only on RAG).
Per sheet: pattern groups and their detail kind (입출구부 50 m / 온도미고려),
note-5 scope sentence, concrete and rebar grade, rebar sizes present,
radius and lap dimensions as printed, EL datum texts.
Output: data/lining_spec.json (+ printed summary).
Usage: extract_lining_spec.py <RAG_04_DIR> <OUT_JSON>
"""
import glob
import json
import os
import re
import sys
import ezdxf
from ezdxf import recover
RAG_04, OUT = sys.argv[1], sys.argv[2]
BAR_RE = re.compile(r"^0?H(\d{2})$")
PAT_RE = re.compile(r"\(([^()]*?)\s*패턴\)")
GROUP_RE = re.compile(r"주철근 조립도\(([^()]*?)\s*패턴\)")
def load(path):
try:
return ezdxf.readfile(path)
except Exception:
doc, _ = recover.readfile(path)
return doc
def texts_of(doc):
out = []
for e in doc.modelspace():
if e.dxftype() == "TEXT":
s = e.dxf.text
elif e.dxftype() == "MTEXT":
s = e.plain_text()
else:
continue
if s and s.strip():
out.append((e.dxf.layer, round(e.dxf.insert.x, 1), round(e.dxf.insert.y, 1), s.strip()))
return out
def sheet_no(name):
return int(re.search(r"C0060304-(\d{3})", name).group(1))
def main():
files = sorted(glob.glob(os.path.join(RAG_04, "C0060304-00[4-9]*.dxf")))
sheets = []
for f in files:
doc = load(f)
tx = texts_of(doc)
rec = {"sheet": sheet_no(f), "file": os.path.basename(f)}
# pattern groups: detail kind from the text itself
groups = []
for layer, x, y, s in tx:
if layer != "CS-TABL-TEX1":
continue
if "패턴" in s:
kind = "입출구부 50m" if "입출구부" in s else ("온도미고려" if "온도미고려" in s else "기타")
m = PAT_RE.search(s)
groups.append({"group": m.group(1) if m else None, "kind": kind, "raw": s})
rec["pattern_details"] = groups
# scope note 5 (full line, joined with the continuation line)
note5 = [s for _, _, _, s in tx if s.startswith("5. 본 콘크리트 라이닝 구조도")]
cont = [s for _, _, _, s in tx if s.startswith("설계조건과 상이할 경우")]
rec["note5"] = (note5[0] + " " + (cont[0] if cont else "")).strip() if note5 else None
rec["concrete"] = sorted({s for _, _, _, s in tx if "fck" in s})
rec["rebar_grade"] = sorted({s for _, _, _, s in tx if "fy=" in s})
rec["aggregate"] = sorted({s for _, _, _, s in tx if "최대치수" in s})
bars = {}
for layer, x, y, s in tx:
m = BAR_RE.match(s)
if m:
bars[f"H{m.group(1)}"] = bars.get(f"H{m.group(1)}", 0) + 1
rec["rebar_sizes_label_count"] = dict(sorted(bars.items()))
rec["radius_dims"] = sorted({s for layer, _, _, s in tx if layer == "CS-DIMT" and re.match(r"R\d?'?=", s)})
rec["lap_dims"] = sorted({s for layer, _, _, s in tx if layer == "CS-DIMT" and "mm" in s and "이음장" not in s and "600" in s})
rec["el_texts"] = sorted({s for _, _, _, s in tx if s.startswith("EL")})
rec["entrance_exit_50m"] = any("입출구부 50m" in s for _, _, _, s in tx)
sheets.append(rec)
with open(OUT, "w", encoding="utf-8") as fh:
json.dump({"source": "C0060304-004..009 (콘크리트라이닝구조도)", "sheets": sheets},
fh, ensure_ascii=False, indent=1)
for r in sheets:
print("==", r["sheet"], "| groups:", [(g["group"], g["kind"]) for g in r["pattern_details"]])
print(" note5:", (r["note5"] or "")[:140])
print(" concrete:", r["concrete"], "| rebar:", r["rebar_grade"], "| agg:", r["aggregate"])
print(" bars:", r["rebar_sizes_label_count"], "| R:", r["radius_dims"], "| EL:", r["el_texts"])
if __name__ == "__main__":
main()
@@ -0,0 +1,28 @@
#!/usr/bin/env python3
"""Task 5: support-pattern (지보패턴) station ranges from drawings 03-003..008.
Collects every TEXT that names a station (STA. / N+mmm / N+mmm.mm) and every
pattern name (NP-...) with its layer and position, without interpreting ranges."""
import glob
import os
import re
import sys
import ezdxf
RAG = sys.argv[1]
pat_re = re.compile(r"NP-\d[A-Z0-9-]*|WL-\d")
sta_re = re.compile(r"(STA\.?\s*)?\d{1,2}\+\d{2,3}(\.\d{2})?")
files = sorted(glob.glob(os.path.join(RAG, "03_본선터널굴착", "C0060303-00[3-8]*.dxf")))
for f in files:
msp = ezdxf.readfile(f).modelspace()
stas, pats = [], set()
for e in msp.query("TEXT MTEXT"):
s = e.dxf.text if e.dxftype() == "TEXT" else e.plain_text()
for m in sta_re.finditer(s):
stas.append((round(e.dxf.insert.x), round(e.dxf.insert.y), m.group(0).strip()))
for m in pat_re.finditer(s):
pats.add(m.group(0))
print(os.path.basename(f)[:28], "| patterns:", sorted(pats))
for x, y, t in sorted(stas, key=lambda r: (r[0], r[1]))[:12]:
print(f" ({x},{y}) {t}")
print(f" station-like texts: {len(stas)}")
@@ -0,0 +1,48 @@
#!/usr/bin/env python3
"""Extract the 당진방향 station pattern table from 참고_지보패턴현황 (read-only).
Rows 55..96 of the sheet: C=start STA, G=end STA, J=length, M=lining pattern
(지보패턴), O=실시설계 excavation class (blank = continues the row above),
Q=굴착공법, T=비고. Also extracts the 대산방향 table (rows 13..49) for reference.
Usage: extract_pattern_table.py <xlsx> <out_dir>"""
import csv, os, sys
import openpyxl
SRC, OUT = sys.argv[1], sys.argv[2]
wb = openpyxl.load_workbook(SRC, read_only=True, data_only=True)
ws = wb["참고_지보패턴현황"]
rows = {r: row for r, row in enumerate(ws.iter_rows(min_row=1, max_row=114, max_col=33, values_only=True), start=1)}
def table(r0, r1, tag):
out, last_o = [], None
for r in range(r0, r1 + 1):
row = rows[r]
try:
a, b = float(row[2]), float(row[6])
except (TypeError, ValueError):
continue
m = (row[12] or "").strip() if isinstance(row[12], str) else row[12]
o_raw = row[14]
o = o_raw if o_raw not in (None, "") else last_o
last_o = o
remark = (row[19] or "") if isinstance(row[19], str) else ""
out.append({"direction": tag, "source_row": r, "sta_from": a, "sta_to": b,
"length_m": row[9], "lining_pattern": m or ("개착부" if "개착" in remark else ""),
"excavation": o or "", "excavation_inherited": o_raw in (None, ""),
"method": (row[16] or "") if isinstance(row[16], str) else "",
"remark": remark})
return out
south = table(55, 96, "당진")
north = table(13, 49, "대산")
os.makedirs(OUT, exist_ok=True)
cols = list(south[0].keys())
for name, data in [("pattern_stations_dangjin.csv", south), ("pattern_stations_daesan_ref.csv", north)]:
with open(os.path.join(OUT, name), "w", encoding="utf-8", newline="") as fh:
w = csv.DictWriter(fh, fieldnames=cols)
w.writeheader()
w.writerows(data)
print("dangjin rows", len(south), "range", south[0]["sta_from"], "->", south[-1]["sta_to"])
print("daesan rows", len(north), "range", north[0]["sta_from"], "->", north[-1]["sta_to"])
print("blank lining pattern rows (dangjin):", [(d["source_row"], d["remark"]) for d in south if not d["lining_pattern"]])
+68
View File
@@ -0,0 +1,68 @@
#!/usr/bin/env python3
"""Task 3 (user-confirmed reading): ground-level and design-grade values per
20 m station from sheet 02-002 (당진방향).
Confirmed by the user (2026-10-21): decimal texts in CR-TABL-TEX2 are 지반고 (GL),
sheet 002 values used; the ruler 740.00..980.00 is chainage 4+740..4+980, and
0.00 is 5+000 (ruler continues in 20 m steps).
Method: ruler texts give anchors (chainage = 4000+v for 740..980; 5000+v after 0.00).
Each decimal text is projected onto the ruler axis; its station is the nearest anchor.
Two decimal series appear at each station (|across| ~ 17-21 and ~ 32-39 m);
the series names are NOT assigned here (HITL). Checks are reported, not forced."""
import csv
import math
import re
import sys
import ezdxf
SRC, OUT = sys.argv[1], sys.argv[2]
msp = ezdxf.readfile(SRC).modelspace()
texts = [(e.dxf.insert.x, e.dxf.insert.y, e.dxf.text.strip())
for e in msp.query("TEXT") if e.dxf.layer == "CR-TABL-TEX2"]
ruler = {}
for x, y, s in texts:
if re.fullmatch(r"\d{1,3}\.00", s):
ruler[int(s.split(".")[0])] = (x, y)
anchors = {}
for v in range(740, 1000, 20):
if v in ruler:
anchors[4000 + v] = ruler[v]
for v in range(0, 1000, 20):
if v in ruler:
anchors[5000 + v] = ruler[v]
else:
break
stas = sorted(anchors)
print("anchors:", len(stas), "range", stas[0], "..", stas[-1])
gaps = [(stas[i + 1] - stas[i], round(math.dist(anchors[stas[i]], anchors[stas[i + 1]]), 2))
for i in range(len(stas) - 1)]
print("consecutive (dSTA, chord m):", gaps)
A0, A1 = anchors[stas[0]], anchors[stas[-1]]
L = math.dist(A0, A1)
u = ((A1[0] - A0[0]) / L, (A1[1] - A0[1]) / L)
v_ = (-u[1], u[0])
rows = []
for x, y, s in texts:
if not re.fullmatch(r"\d{1,3}\.\d{3}", s):
continue
dx, dy = x - A0[0], y - A0[1]
along = dx * u[0] + dy * u[1]
across = dx * v_[0] + dy * v_[1]
if not (-5.0 <= along <= L + 5.0 and abs(across) < 60):
continue # outside the ruler band (table noise)
sta_est = stas[0] + along * (stas[-1] - stas[0]) / L
near = min(stas, key=lambda k: abs(k - sta_est))
rows.append((near, round(sta_est, 1), round(across, 2), float(s), round(x, 2), round(y, 2)))
with open(OUT, "w", newline="", encoding="utf-8") as fh:
w = csv.writer(fh)
w.writerow(["station_anchor", "est_station", "across_m", "value", "x", "y"])
w.writerows(sorted(rows))
per = {}
for near, est, acr, val, _, _ in rows:
per.setdefault(near, []).append((acr, val))
for k in sorted(per):
print(f"STA {k}: " + " ".join(f"[{a:+.1f}m {val:.3f}]" for a, val in sorted(per[k])))
+85
View File
@@ -0,0 +1,85 @@
#!/usr/bin/env python3
"""Inventory the main-line (본선) DXF drawings: header, layers, entity counts,
extents, 3D flag and text samples. Read-only on the source RAG folder."""
import collections
import glob
import json
import os
import sys
import ezdxf
from ezdxf import bbox, recover
RAG_DIR = sys.argv[1]
OUT_JSON = sys.argv[2]
# 본선 scope (approved 2026-10-21): 02 평면·종단, 03 본선굴착, 04 본선기타
SUBDIRS = ["02_평면 및 종단면도", "03_본선터널굴착", "04_본선터널기타"]
def inventory(path):
rec = {"file": os.path.basename(path), "ok": False}
rec["recovered"] = False
try:
doc = ezdxf.readfile(path)
except Exception as exc: # strict load failed: retry in recover mode and record it
strict_error = f"{type(exc).__name__}: {exc}"
try:
doc, _auditor = recover.readfile(path)
rec["recovered"] = True
rec["strict_error"] = strict_error
except Exception as exc2:
rec["error"] = f"{strict_error} | recover: {type(exc2).__name__}: {exc2}"
return rec
hdr = doc.header
rec["dxfversion"] = doc.dxfversion
rec["insunits"] = hdr.get("$INSUNITS")
rec["codepage"] = hdr.get("$DWGCODEPAGE")
rec["layers"] = len(doc.layers)
msp = doc.modelspace()
counts = collections.Counter(e.dxftype() for e in msp)
rec["entity_counts"] = dict(counts.most_common())
rec["blocks"] = len([b for b in doc.blocks if not b.name.startswith("*")])
z_vals = []
texts = []
for e in msp:
t = e.dxftype()
if t == "LINE":
z_vals += [e.dxf.start.z, e.dxf.end.z]
elif t in ("TEXT", "MTEXT"):
s = e.dxf.text if t == "TEXT" else e.plain_text()
if s and len(texts) < 40:
texts.append(s.strip()[:80])
elif t in ("POINT", "INSERT", "CIRCLE", "ARC"):
z_vals.append(e.dxf.location.z if t == "POINT" else e.dxf.insert.z if t == "INSERT" else e.dxf.center.z)
rec["z_min"] = round(min(z_vals), 3) if z_vals else None
rec["z_max"] = round(max(z_vals), 3) if z_vals else None
rec["has_3d_geometry"] = bool(z_vals) and (max(z_vals) - min(z_vals)) > 1e-6
try:
ext = bbox.extents(msp)
rec["extents_min"] = [round(v, 2) for v in ext.extmin]
rec["extents_max"] = [round(v, 2) for v in ext.extmax]
except Exception as exc:
rec["extents_error"] = str(exc)
rec["text_sample"] = texts
rec["ok"] = True
return rec
def main():
paths = []
for sub in SUBDIRS:
paths += sorted(glob.glob(os.path.join(RAG_DIR, sub, "*.dxf")))
results = [inventory(p) for p in paths]
os.makedirs(os.path.dirname(OUT_JSON), exist_ok=True)
with open(OUT_JSON, "w", encoding="utf-8") as fh:
json.dump(results, fh, ensure_ascii=False, indent=2)
ok = sum(r["ok"] for r in results)
print(f"files={len(results)} ok={ok} failed={len(results) - ok}")
for r in results:
status = "OK " if r["ok"] else "ERR"
detail = r.get("error") or f"ents={sum(r['entity_counts'].values())} 3D={r['has_3d_geometry']} units={r['insunits']} cp={r['codepage']} recovered={r['recovered']}"
print(f"{status} {r['file'][:60]:60s} {detail}")
if __name__ == "__main__":
main()
+49
View File
@@ -0,0 +1,49 @@
#!/usr/bin/env python3
"""Render the Task 3 draft centreline report (DOC/03_설계) from the centreline
CSVs written by extract_alignment_plan.py. Numbers are read, not typed."""
import csv
import math
import sys
CSV_001, CSV_002, OUT_MD = sys.argv[1], sys.argv[2], sys.argv[3]
def load(path):
with open(path, encoding="utf-8") as fh:
return [(int(r["station_m"]), float(r["centre_x"]), float(r["centre_y"]), float(r["side_gap_m"]))
for r in csv.DictReader(fh)]
a, b = load(CSV_001), load(CSV_002)
same = a == b
lines = ["# 본선 중심선 측점 좌표 (초안)\n",
"| 버전 | 일자 | 변경 내용 |", "|---|---|---|",
"| v0.1.00 | 2026-10-21 | 최초 작성. 평면도 02-001 기준 측점 4+700~5+500 중심선 초안 (HITL 검토 대상) |\n",
"## 1. 추출 방법",
"- 소스: 레이어 `CA-ALGN-TEXT`(측점 라벨), `CA-ALGN-STSM`(측점 눈금) [출처: C0060302-001]",
"- 측점 라벨은 측점당 2개(좌우 측). 각 라벨의 최근접 눈금 2점의 **중점**을 중심선 점으로 채택 (정의 HITL 확인 대상)",
"- 좌표는 도면 좌표계 그대로 (단위: 도면 표기 따름, 사용자 승인 2026-10-21)\n",
"## 2. 측점별 중심선 좌표 (02-001)\n",
"| 측점(m) | 표기 | X | Y | 좌우 간격(m) | 인접 거리(m) | 측점차(m) | 잔차(m) |",
"|---|---|---|---|---|---|---|---|"]
prev = None
for sta, x, y, g in a:
label = f"{sta // 1000}+{sta % 1000:03d}"
if prev is None:
lines.append(f"| {sta} | {label} | {x:,.3f} | {y:,.3f} | {g:.2f} | — | — | — |")
else:
d = math.dist(prev[1:3], (x, y))
lines.append(f"| {sta} | {label} | {x:,.3f} | {y:,.3f} | {g:.2f} | {d:.3f} | {sta - prev[0]} | {d - (sta - prev[0]):+.3f} |")
prev = (sta, x, y)
resid = [math.dist(a[i][1:3], a[i + 1][1:3]) - (a[i + 1][0] - a[i][0]) for i in range(len(a) - 1)]
lines += ["",
f"- 검증: 인접 측점 거리와 측점차의 잔차 최대 {max(abs(r) for r in resid):.3f} m (측점 9개, 8구간). 잔차는 보정하지 않고 그대로 기재",
"- 범위: 4+700 ~ 5+500 (800m). **이 시트 밖의 연장은 확보된 도면이 없음**\n",
"## 3. 02-002 시트 대조",
f"- 02-002 추출 결과 {'02-001과 측점 좌표가 동일' if same else '02-001과 차이 있음'} ({'중복 시트 가능성' if same else '차이 확인 필요'}). 원본 파일 md5는 상이함 [출처: C0060302-002]",
"- 02-002가 종단(프로파일) 시트인지 중복 시트인지는 사용자 확인 필요\n",
"## 4. 미완 항목 (OPEN 등재 대상)",
"- 종단 EL 추출 미실시 (측점별 계획고·지반고). `CR-TABL-TEX2`의 740.00~980.00 라벨은 20m 간격으로 보이나 측점 체계와 대응 미확인 [추론]",
"- 허용오차 ±50mm 판정은 EL 추출 후 실시",
"- 측점 4+700 이전·5+500 이후 구간 평면 도면 미확보\n"]
open(OUT_MD, "w", encoding="utf-8").write("\n".join(lines) + "\n")
print("written", OUT_MD, "same_as_002=", same, "max_resid=", round(max(abs(r) for r in resid), 3))
+59
View File
@@ -0,0 +1,59 @@
#!/usr/bin/env python3
"""Generate the main-line drawing inventory report (DOC/02_분석) from the
inventory JSON so that every number in the report comes from the extraction."""
import collections
import json
import sys
SRC_JSON, OUT_MD = sys.argv[1], sys.argv[2]
rows = json.load(open(SRC_JSON, encoding="utf-8"))
ok = [r for r in rows if r["ok"]]
err = [r for r in rows if not r["ok"]]
units = collections.Counter(str(r.get("insunits")) for r in ok)
n3d = sum(1 for r in ok if r["has_3d_geometry"])
total_ents = sum(sum(r["entity_counts"].values()) for r in ok)
def top_types(r, k=3):
return ", ".join(f"{t} {n}" for t, n in list(r["entity_counts"].items())[:k])
lines = []
lines.append("# 대산2터널 본선 도면 인벤토리\n")
lines.append("| 버전 | 일자 | 변경 내용 |\n|---|---|---|")
lines.append("| v0.1.00 | 2026-10-21 | 최초 작성. 본선 21건(02 평면·종단 4, 03 본선굴착 8, 04 본선기타 9) DXF 인벤토리 |\n")
lines.append("## 1. 개요\n")
lines.append("- 입력: `RAG/03_터널명(대산2터널)/{02,03,04}` 하위 DXF 21건 (원본 DWG 변환본, 읽기 전용 사용)")
lines.append(f"- 추출 결과: 성공 {len(ok)}건 / 실패 {len(err)}건 / 복구 모드 적용 {sum(1 for r in ok if r['recovered'])}건")
lines.append(f"- 총 엔티티 {total_ents:,}개 (성공 파일 기준)")
lines.append(f"- 3D 형상(Z 편차 있음) 보유 파일: **{n3d}건** — 전 도면이 2D이므로 3D 형상은 도면 해석으로 재구성해야 함 [출처: 본 인벤토리 `has_3d_geometry`]")
lines.append(f"- `$INSUNITS` 분포: " + ", ".join(f"{k}={v}건" for k, v in units.items()) + " (0=미지정, 4=mm) — 단위는 도면 표기를 따름 (사용자 승인 2026-10-21)\n")
lines.append("## 2. 파일별 추출 결과\n")
lines.append("| # | 파일 | 상태 | 엔티티 | 주요 엔티티(상위 3) | 단위 | 코드페이지 |")
lines.append("|---|---|---|---|---|---|---|")
for i, r in enumerate(rows, 1):
if r["ok"]:
n = sum(r["entity_counts"].values())
lines.append(f"| {i} | {r['file']} | OK | {n:,} | {top_types(r)} | {r['insunits']} | {r['codepage']} |")
else:
lines.append(f"| {i} | {r['file']} | **ERR** | — | {r.get('error','')} | — | — |")
lines.append("")
lines.append("## 3. 도면 주석 관찰 (정성, 판정 어휘: 있음 / 일부 있음 / 없음 / 확인 못함)\n")
lines.append("| 항목 | 판정 | 근거 |\n|---|---|---|")
lines.append("| 표준단면 편경사 기준 | 있음 | 「편경사 -2.0%를 기준으로 작성」 [출처: C0060303-001, 주석 1] |")
lines.append("| 표준단면 방향 표기 범위 | 일부 있음 | 「당진방향 단면만 표기, 대산방향 단면은 생략」 [출처: C0060303-001, 주석 2] — 대산방향 형상은 이 도면으로 확정 불가 |")
lines.append("| 숏크리트·라이닝 두께 수치 | 없음 | 「표준지보패턴도 참조」 [출처: C0060303-001, 주석 5] — 두께 값은 다른 도면(표준지보패턴) 필요 |")
lines.append("| 굴착 순서 표기 | 있음 | N, N+1, N-1 단계 표기 및 1회 굴진장 [출처: C0060303-002] |")
lines.append("| 굴진 1회당 길이 | 있음 | 「굴진 (1회당 0.8m)」 [출처: C0060303-008] |")
lines.append("| 라이닝 구조도 적용 구간 | 일부 있음 | 「지보패턴 입출구부 A~C-2 구간의 구조적 취약구간에 적용」 [출처: C0060304-004, 주석 5] — 전 구간 적용 여부 확인 못함 |")
lines.append("| 지보패턴 명칭 | 있음 | NP-4AD-1-D-2, NP-4BD-1-D-2, NP-5AE-1-WL-4, NP-5BE-1-WL-4, NP-6A, NP-6B [출처: 03-003~008 파일명] |")
lines.append("| 평면·종단 지질·구배 표기 | 있음 | 구배 S=2.4913%, 지층명(붕적층·풍화토·풍화암·보통암) [출처: C0060302-001 텍스트] |")
lines.append("| 구간 측점(STA) 정합 | 확인 못함 | 평면·종단·굴착 도면 간 측점 대응표 없음 — Task 3에서 대조 필요 [추론] |")
lines.append("")
lines.append("## 4. 확인·보류 항목 (OPEN 등재 대상)\n")
lines.append("- **C0060304-003** (공동구 및 배수계획도 3): DXF 구조 오류(`Invalid sort handle code 331`), 복구 모드도 실패. 원본 DWG 재변환 필요. 본선 3D 골격에는 필수가 아니므로 배수 요소만 보류.")
lines.append("- 대산방향 단면 형상 (C0060303-001 주석 2): 3D 대산방향 재구성 여부 결정 필요 [CLARIFICATION NEEDED].")
lines.append("- 라이닝 두께 수치 출처 (표준지보패턴도 미보유): 두께 값 확정 전까지 `[미확정]`으로 표기.")
lines.append("")
lines.append("## 5. 다음 단계\n")
lines.append("Task 3(평면·종단 선형 추출, HITL) 착수 전 위 4항의 확인 응답을 받는다. 본 인벤토리의 수치는 `Code/3d-drawing/data/inventory_mainline.json`에서 자동 생성되었다.")
open(OUT_MD, "w", encoding="utf-8").write("\n".join(lines) + "\n")
print("written", OUT_MD, "lines", len(lines))
+60
View File
@@ -0,0 +1,60 @@
#!/usr/bin/env python3
"""Render the lining-reinforcement reference report (DOC/03_설계) from
data/lining_spec.json written by extract_lining_spec.py. Values are read from
the JSON, not typed. Usage: make_lining_doc.py <lining_spec.json> <out.md>"""
import json
import sys
SRC, OUT = sys.argv[1], sys.argv[2]
d = json.load(open(SRC, encoding="utf-8"))
S = d["sheets"]
note5 = S[0]["note5"]
conc = S[0]["concrete"][0].split("=")[-1]
rebar = S[0]["rebar_grade"][0].split("=")[-1]
agg = S[0]["aggregate"][0].split("Ø")[-1]
r1_drawn = next(x for x in S[0]["radius_dims"] if x.startswith("R1="))
r1_mm = float(r1_drawn.split("=")[1])
lines = ["# 콘크리트 라이닝 구조도 판독 및 재료 사양 (초안)\n",
"| 버전 | 일자 | 변경 내용 |", "|---|---|---|",
"| v0.1.00 | 2026-10-21 | 최초 작성. 콘크리트라이닝구조도 C0060304-004~009 판독 (HITL 검토 대상) |\n",
"## 1. 개요",
"- 6장은 **지보패턴 입출구부 취약구간**의 철근 상세와 공통 사양이다. 도면에 측점(STA) 표기가 없어 어느 측점 구간에 적용되는지는 판독되지 않았다 ([미확정]).",
"- 적용 범위(주기 5, 원문): " + note5 + "\n",
"## 2. 시트별 패턴군",
"| 도면 | 패턴군 | 상세 종류 | 철근 라벨(H13~H29 개수) |",
"|---|---|---|---|"]
for r in S:
bars = ", ".join(f"{k} {v}" for k, v in r["rebar_sizes_label_count"].items())
for i, g in enumerate(r["pattern_details"]):
lab = bars if i == 0 else ""
lines.append(f"| {r['file'][:14]} | {g['group']} | {g['kind']} | {lab} |")
lines += ["",
"- 철근 라벨 개수는 도면 시트 단위 합계다 (패턴군별 분리 불가). 첫 행에만 표기했다.\n",
"## 3. 공통 재료 사양 (6장 동일)",
f"- 콘크리트: {conc} ({S[0]['concrete'][0]})",
f"- 철근: {rebar}, 도면 표기 SD400 ({S[0]['rebar_grade'][0]})",
f"- 굵은골재 최대치수: Ø{agg} mm",
"- 설계방법: 강도설계법, 피복두께 6 cm 이상 확보 (주기 1)",
"- 이음장 600 mm (도면 치수), 겹침이음 1 cycle (도면 상세)",
"- 라이닝 뒷면 주입재 강도 1.0 MPa 기대 (주기 11)",
"- 천정부 방수쉬트 부착형 앵커, 철근처짐방지 앵커 적용 (주기 4, 12)\n",
"## 4. 기준선과 치수",
f"- 도면 표기 R1 = {r1_drawn} (6장 모두 동일 표기)",
"- 표준단면 치수표(설계_표준단면치수표) R1 = 6,982 mm, 골격 모델은 6,982 mm 사용",
f"- 라이닝 외면 R 7.282 m 에서 내면 R 6.982 m 를 빼면 두께 0.300 m, 표기 6.892 m 를 쓰면 0.390 m 이다. 표준단면 두께 300 mm 와 맞는 값은 6.982 m 이다.",
"- R2 = 3,712 mm, R2′ = 3,910 mm (도면 표기, 표준단면 치수표와 일치)",
"- EL. 0.000 = 노면 기준 (계획고 F = 0.000 과 일치 기록)",
"- EL. -0.367 = 아치 중심 높이 (골격의 중심 c = -0.367 m 와 일치)\n",
"## 5. 충돌·미확정",
"- [확정 2026-10-21, 사용자 결정] R1 = 6.982 m 유지. 도면의 6.892 m 표기는 확인 대상 오기로 기록한다.",
"- [미확정] 시트별 패턴군이 어느 측점 구간인지 (도면에 측점 없음)",
"- [확인 필요] 라이닝 패턴군(A~C-2, D-1~2, E-1~2, WL-1~4, RP-1~2, P-2~3)과 굴착 순서도 패턴명(NP-4AD-1-D-2 등)의 대응. 사용자는 파일명에 기록되어 있다고 답했다. 예시 풀이(NP-5BE-1-WL-4 = NP-5B 의 D-1·D-2)는 파일명 속 WL-4 및 시트 패턴군(WL-3~4)과 맞지 않아 재확인이 필요하다.",
"- 재료표 수량(총 중량)은 이번 판독에서 추출하지 않았다. 필요하면 재료표 추출을 별도 Task로 한다.",
"- 철근 배치는 골격 LOD 범위 밖이라 모델에 넣지 않았다 (사용자 승인 3D 상세 수준: 축선·표준단면 스윕 골격).",
"- 설계 조건과 상이하면 구조해석 검토 후 철근 보강 가능 (주기 5, 시공 중 판단 대상)\n",
"## 6. 확인 요청 (HITL)",
"1. NP-5BE-1-WL-4 의 D-1·D-2 풀이가 맞는지, 아니면 WL-4 가 어느 라이닝 패턴군인지",
"2. 측점별 지보패턴 구간표가 확보되면 그 값으로 placeholder 구간을 교체할지",
""]
open(OUT, "w", encoding="utf-8").write("\n".join(lines))
print("written", OUT, len(lines), "lines")
+56
View File
@@ -0,0 +1,56 @@
#!/usr/bin/env python3
"""Render the station-by-station lining pattern report (DOC/03_설계) from the
CSVs written by extract_pattern_table.py. Values are read, not typed.
Usage: make_pattern_doc.py <dangjin.csv> <daesan_ref.csv> <lining_spec.json> <out.md>"""
import csv
import json
import sys
DJ, DS, LS, OUT = sys.argv[1:5]
dj = list(csv.DictReader(open(DJ, encoding="utf-8")))
ds = list(csv.DictReader(open(DS, encoding="utf-8")))
lin = json.load(open(LS, encoding="utf-8"))["sheets"]
drawing_groups = sorted({g["group"] for s in lin for g in s["pattern_details"] if g["group"]})
dj_names = {r["lining_pattern"] for r in dj}
ds_names = {r["lining_pattern"] for r in ds}
# Drawing group labels (read from the sheet notes, 04-004..009) -> member patterns.
# Explicit table: the labels mix letter ranges (A~C-2) and number ranges (WL-1~2).
DRAWING_MEMBERS = {"A~C-2": ["A", "B-1", "B-2", "C-1", "C-2"], "D-1~2": ["D-1", "D-2"],
"E-1~2": ["E-1", "E-2"], "WL-1~2": ["WL-1", "WL-2"], "WL-1~2,RP-1~2": ["WL-1", "WL-2", "RP-1", "RP-2"],
"WL-3~4": ["WL-3", "WL-4"], "P-2~3": ["P-2", "P-3"], "RP-1~2": ["RP-1", "RP-2"]}
members = set()
for g in drawing_groups:
members |= set(DRAWING_MEMBERS.get(g, [g]))
missing_dj = sorted(m for m in members if m not in dj_names)
only_ds = sorted(m for m in missing_dj if m in ds_names)
none_any = sorted(m for m in missing_dj if m not in ds_names)
L = ["# 측점별 콘크리트 라이닝 지보패턴표 (당진방향) (초안)\n",
"| 버전 | 일자 | 변경 내용 |", "|---|---|---|",
"| v0.1.00 | 2026-10-21 | 최초 작성. 엑셀 `참고_지보패턴현황` 당진방향 표를 측점별로 정리 (사용자 결정: 모델 placeholder 대체) |\n",
"## 1. 출처와 범위",
"- 원본: `RAG/03_터널명(대산2터널)/지보패턴현황_20260918.xlsx`, 시트 `참고_지보패턴현황`, 당진방향 표 (원본 행 55~96)",
f"- 행 {len(dj)}개, 측점 {dj[0]['sta_from']} ~ {dj[-1]['sta_to']}, 빈틈 없음 (각 행 끝 = 다음 행 시작)",
"- 모델 적용 범위 4+745 ~ 5+480 은 이 표로 전 구간 덮임 (735 m)",
"- 지보패턴(M열) = 라이닝 패턴, 실시설계(O열) = 굴착 등급. 실시설계 칸이 빈 행은 위 행의 값을 이어 받음 (병합 셀 해석, [추론])\n",
"## 2. 측점별 표",
"| 측점 from | 측점 to | 연장(m) | 지보패턴 | 실시설계 | 굴착공법 | 비고 |",
"|---|---|---|---|---|---|---|"]
def sta(x):
v = int(float(x)); return f"{v // 1000}+{v % 1000:03d}"
for r in dj:
exc = r["excavation"] + (" (이어받음)" if r["excavation_inherited"] == "True" and r["excavation"] else "")
L.append(f"| {sta(r['sta_from'])} | {sta(r['sta_to'])} | {float(r['length_m']):g} | {r['lining_pattern']} | {exc} | {r['method'].replace(chr(10), ' ')} | {r['remark']} |")
L += ["", "## 3. 도면과 표의 대응",
f"- 라이닝 도면(04-004~009)의 패턴 {len(members)}개 중 당진 표에 있는 것: {len(members) - len(missing_dj)}개",
f"- 당진 표에 없고 대산 표에만 있는 것: {', '.join(only_ds) if only_ds else '없음'} (대산 표 값 참고만, 적용 안 함 — 아래 4항)",
f"- 두 표 모두 없는 것: {', '.join(none_any) if none_any else '없음'} → 측점 지정 불가, [미확정]\n",
"## 4. 확인 필요 (HITL)",
"- [확인 필요] 대산방향 표의 WL-4 (4+758.85~4+768.85, NP-5) 를 당진 측점에 적용할지. 두 방향이 별도 튜브라는 점은 [추론]이며 확인되지 않았다. 당진 표는 같은 측점에 WL-2/WL-1 을 보여 준다.",
"- [확인 필요] 말씀하신 `NP-5BE-1-WL-4` 의 풀이 (NP-5B 의 E-1 ~ WL-4). 당진 NP-5 구간은 WL-2·WL-1 뿐이고, E-1 은 두 표 어디에도 없다.",
"- [확인 필요] 엑셀 `개요2`·`1` 시트: 굴착 등급은 NP-4/5/6 (A·B 접미사 포함), 괄호 안은 지보 하위 패턴이라는 표기. 이 해석은 엑셀 표기와 일치하는 것으로 보이나 도면 명칭 대응은 확정하지 않았다.\n",
"## 5. 적용 결과 (골격 모델)",
"- placeholder 2 m 구간은 폐기하고 이 표로 대체 (사용자 결정 2026-10-21)",
"- 뷰어의 라이닝 클릭 값: 측점, 지보패턴(본 표), 라이닝 사양 (C0060304-004~009)",
""]
open(OUT, "w", encoding="utf-8").write("\n".join(L))
print("written", OUT, "rows", len(dj), "drawing groups", len(members), "missing in dangjin", missing_dj)
+55
View File
@@ -0,0 +1,55 @@
#!/usr/bin/env python3
"""Render the confirmed station table (지반고 = A열, 계획고 = B열, 20 m station
numbering per user decision 2026-10-21) into DOC/03_설계 and check the design
grade against the drawn label S=2.4913% (정상 구배 검산). No value is adjusted."""
import csv
import sys
SRC, OUT = sys.argv[1], sys.argv[2]
rows = []
with open(SRC, encoding="utf-8") as fh:
rd = list(csv.DictReader(fh))
gl, dg = {}, {}
for r in rd:
sta = int(r["station_anchor"])
if float(r["across_m"]) < -25:
gl[sta] = float(r["value"])
else:
dg[sta] = float(r["value"])
stas = sorted(set(gl) & set(dg))
n = len(stas)
xs = [s for s in stas]
mx = sum(xs) / n
my = sum(dg[s] for s in stas) / n
slope = sum((s - mx) * (dg[s] - my) for s in stas) / sum((s - mx) ** 2 for s in stas)
grade_pct = slope * 100
resid = [dg[s] - (my + slope * (s - mx)) for s in stas]
max_res = max(abs(r) for r in resid)
lines = ["# 본선 종단 측점값 — 지반고·계획고 (초안, 확정 해석 반영)\n",
"| 버전 | 일자 | 변경 내용 |", "|---|---|---|",
"| v0.1.00 | 2026-10-21 | 최초 작성. 02-002 종단 눈금 4+740~5+480, 지반고(A열)·계획고(B열) 확정 해석 반영 |\n",
"## 1. 확정 사항 (사용자 답변 2026-10-21)",
"- 소수 A열(중심선 측 약 -33m) = **지반고**, B열(약 -16m) = **계획고** [출처: C0060302-002, CR-TABL-TEX2]",
"- 측점은 **측점 번호 20m 기준**. 도면상 눈금 거리 21.27m는 사용하지 않음 (축척 1.0635배 불일치 기록)",
"- 공백 구간(4+700~4+740, 5+480~5+500)은 **[미확정]**, 확보 구간만 모델링\n",
"## 2. 측점별 지반고·계획고",
"| 측점(m) | 표기 | 지반고 GL(m) | 계획고(m) |", "|---|---|---|---|"]
for s in stas:
lines.append(f"| {s} | {s // 1000}+{s % 1000:03d} | {gl[s]:.3f} | {dg[s]:.3f} |")
lines += ["",
"## 3. 검산",
f"- 전구간 직선 회귀 구배 {grade_pct:.4f}%는 **단일 구배가 아님**을 뜻하므로 채택하지 않음 (잔차 최대 {max_res:.3f} m)",
"- 구간별 구배 (계획고 2점 기울기, 도면 표기 S=2.4913%와 비교):"]
for a, b in [(4740, 4800), (4800, 4900), (4900, 5000), (5000, 5100), (5100, 5200), (5200, 5300), (5300, 5400), (5400, 5480)]:
seg = [k for k in stas if a <= k <= b]
if len(seg) >= 2:
g = (dg[seg[-1]] - dg[seg[0]]) / (seg[-1] - seg[0]) * 100
lines.append(f" - {a // 1000}+{a % 1000:03d} ~ {b // 1000}+{b % 1000:03d}: {g:.4f}%")
lines += ["- 해석: 4+740~5+300은 2.491%로 S=2.4913%와 일치. 5+300 이후 구배 감소는 종곡선 구간으로 보이나 [추론], 종곡선 제원은 도면에서 확인 못함 [확인 못함]",
"- 지반고는 구배 검산 대상이 아님 (지형 값)\n",
"## 4. 미확정·후속 (OPEN 등재 대상)",
"- 4+700~4+740, 5+480~5+500 종단 값 없음 → [미확정]",
"- 허용오차 ±50mm 판정: 본 초안은 도면 표기 값 그대로이며, 3D 대조 판정은 Task 7 이후 수행",
"- 라이닝 두께 수치: 04-004~009 콘크리트라이닝 구조도 확인 예정 (사용자 지시 2026-10-21)\n"]
open(OUT, "w", encoding="utf-8").write("\n".join(lines) + "\n")
print(f"stations={n} grade={grade_pct:.4f}% max_resid={max_res:.3f}")
+42
View File
@@ -0,0 +1,42 @@
import sys, time, pickle
sys.path += ['/root/.local/lib/python3.13/site-packages','/usr/local/lib/python3.13/dist-packages','/usr/lib/python3/dist-packages']
from build123d import import_step, Face
import numpy as np
shape = import_step("/mnt/user-data/uploads/97.과제/Code/3d-drawing/model/tunnel_solids.step")
solids = shape.solids()
pickle_out = {}
import json
meta=json.load(open('/mnt/user-data/uploads/97.과제/Code/3d-drawing/model/tunnel_solids_meta.json'))
comps=[c for c in meta['components'] if c['group']!='시설한계(참조선)']
print("non-clearance comps", len(comps))
# match by volume greedily in meta order
used=set(); mapping={}
for c in comps:
best=None;bd=1e9
for j,s in enumerate(solids):
if j in used: continue
d=abs(s.volume-c['volume_m3'])
if d<bd: bd=d;best=j
if bd<0.02:
used.add(best); mapping[c['id']]=best
else:
mapping[c['id']]=None
print("matched", sum(v is not None for v in mapping.values()), "of", len(comps), "unused solids", len(solids)-len(used))
print("first 10 mapping", list(mapping.items())[:10])
pickle.dump({'mapping':mapping}, open('/tmp/verify_daesan2_run/mapping.pkl','wb'))
# faces of solid 0 (LIN_01)
for idx in [mapping['LIN_01_P-3']]:
s=solids[idx]
fs=s.faces()
print("solid",idx,"nfaces",len(fs), "vol", round(s.volume,3))
rows=[]
for f in fs:
gt=f.geom_type
c=f.center(); a=f.area
try:
n=f.normal_at(c)
except Exception as ex:
n=None
rows.append((gt, round(a,1), [round(x,3) for x in (c.X,c.Y,c.Z)], None if n is None else [round(x,4) for x in (n.X,n.Y,n.Z)]))
rows.sort(key=lambda r: -r[1])
for r in rows[:25]: print(r)
+69
View File
@@ -0,0 +1,69 @@
import sys
exec(open('/tmp/verify_daesan2_run/scripts/hd2.py').read().split('allres={}')[0])
from scipy.spatial import cKDTree
import numpy as np
extra={
'PAV_sideL': seg(9526.3,15764.0,9526.3,15484.0), 'PAV_sideR': seg(18726.3,15580.0,18726.3,15300.0),
'DRN_topL': seg(9026.3,15694.0,9476.3,15703.0), 'DRN_topR': seg(18776.3,15519.0,19226.3,15510.0),
}
REF.update(extra)
def dd(A,B):
d1,_=cKDTree(A).query(B); d2,_=cKDTree(B).query(A); return d1,d2
for st in ['4745.0005','5100.0','5469.9995']:
print("=== station", st, "sigma=+1 slope(dxf)", round(FIT[st]['slope_top'],5))
L=to_dxf(pts(st,lambda g,i:g=='라이닝'),st,1); E=to_dxf(pts(st,lambda g,i:g=='굴착면'),st,1)
P=to_dxf(pts(st,lambda g,i:g=='노면(계획고)'),st,1); B=to_dxf(pts(st,lambda g,i:g=='기층·인버트(가정)'),st,1)
D=to_dxf(pts(st,lambda g,i:g=='배수(측구)'),st,1)
# LIN full arcs (no clipping) and clipped to model extent
arcs=['LIN_crown_outer','LIN_crown_inner','LIN_haunchL_outer','LIN_haunchL_inner','LIN_haunchR_outer','LIN_haunchR_inner']
for k in arcs:
d,_=cKDTree(L).query(REF[k]); print(f" LIN {k:18s} max={d.max():7.1f} mm (at y_min_ref={REF[k][:,1].min():.0f})")
ymin_L=L[L[:,0]<C0[0]][:,1].min(); ymin_R=L[L[:,0]>C0[0]][:,1].min()
lr=np.vstack([REF[k] for k in arcs]); d,_=cKDTree(L).query(lr)
inside=((lr[:,0]<C0[0])&(lr[:,1]>=ymin_L))|((lr[:,0]>=C0[0])&(lr[:,1]>=ymin_R))
print(f" LIN within model extent (y>={ymin_L:.0f}/{ymin_R:.0f}) max={d[inside].max():.1f} mm; drawn part below model extent (not modelled) n={int((~inside).sum())} pts, y range {lr[~inside,1].min():.0f}-{lr[~inside,1].max():.0f}")
fl=np.vstack([REF['LIN_footL'],REF['LIN_footR']]); a,b=dd(L,fl); print(f" LIN foot lines (CS-CONC-MAJR wall/foot) ref->model max={a.max():.1f} model->ref(near band) ")
# EXC
ea=['EXC_crown','EXC_haunchL','EXC_haunchR']
eymL=E[E[:,0]<C0[0]][:,1].min(); eymR=E[E[:,0]>C0[0]][:,1].min()
er=np.vstack([REF[k] for k in ea]); d,_=cKDTree(E).query(er)
ins=((er[:,0]<C0[0])&(er[:,1]>=eymL))|((er[:,0]>=C0[0])&(er[:,1]>=eymR))
print(f" EXC arcs within model extent (y>={eymL:.0f}/{eymR:.0f}) max={d[ins].max():.1f} mm; crown max={cKDTree(E).query(REF['EXC_crown'])[0].max():.1f}; below model extent n={int((~ins).sum())} y {er[~ins,1].min():.0f}-{er[~ins,1].max():.0f}")
# PAV model vs drawn top/bottom/sides
pref=np.vstack([REF['PAV_top'],REF['PAV_bot'],REF['PAV_sideL'],REF['PAV_sideR']])
Pm=P[(P[:,0]>9526.3-150)&(P[:,0]<18726.3+150)]
a,b=dd(Pm,pref); print(f" PAV ref->model max={a.max():.1f} mm ; model->ref max={b.max():.1f} mm (top/bottom/sides)")
# BASE: top envelope vs base top line; bottom envelope vs base bottom line, within road span
bw=B[(B[:,0]>=9526.3)&(B[:,0]<=18726.3)]
xs=np.linspace(9526.3,18726.3,921)
top_line=15484.0+(xs-9526.3)*(15300-15484)/9200.0; bot_line=15334.0+(xs-9526.3)*(15150-15334)/9200.0
# model envelope per x-bin (1 m bins for robust): max y (top) and min y (bottom)
tops=[];bots=[]
for x0 in np.arange(9526.3,18726.3,200):
m=(bw[:,0]>=x0)&(bw[:,0]<x0+200)
if m.sum()==0: continue
xm=bw[m,0].mean(); tops.append((xm,bw[m,1].max())); bots.append((xm,bw[m,1].min()))
tops=np.array(tops); bots=np.array(bots)
et=np.abs(tops[:,1]-np.interp(tops[:,0],xs,top_line)); eb=np.abs(bots[:,1]-np.interp(bots[:,0],xs,bot_line))
print(f" BASE top envelope vs drawn top (15484->15300): max={et.max():.1f} mm, mean={et.mean():.1f}; bottom envelope vs drawn bottom (15334->15150): max={eb.max():.1f} mm, mean={eb.mean():.1f}")
print(f" BASE model bottom envelope at span ends: x={bots[0,0]:.0f} y={bots[0,1]:.1f} / x={bots[-1,0]:.0f} y={bots[-1,1]:.1f}; top envelope ends y={tops[0,1]:.1f}/{tops[-1,1]:.1f}")
print(f" BASE model extent outside road span: x {B[:,0].min():.0f}..{B[:,0].max():.0f}, y {B[:,1].min():.0f}..{B[:,1].max():.0f}")
# DRN
dref=np.vstack([REF[k] for k in ['DRN_wallL1','DRN_wallL2','DRN_botL','DRN_pipeL','DRN_wallR1','DRN_wallR2','DRN_botR','DRN_pipeR','DRN_topL','DRN_topR']])
dm=D[(D[:,0]>8900)&(D[:,0]<19300)]
a,b=dd(dm,dref); print(f" DRN ref->model max={a.max():.1f} mm ; model->ref max={b.max():.1f} mm (walls, bottoms, pipes, tops)")
# vertical datum check at tunnel centre and road centre
fit=FIT[st]; uc,vc,_=fit['outer']
top_c=fit['slope_top']*(0-uc)+0 ;
# top envelope at u for DXF tunnel centre (u=uc) and road centre (x=9776.3 -> u=uc-4.35)
uu=P[:,0];
def top_env(xdxf):
um=uc+(xdxf-C0[0])/1000.0
m=np.abs(pts(st,lambda g,i:g=='노면(계획고)')[:,0]-um)<0.05
return pts(st,lambda g,i:g=='노면(계획고)')[m,1].max()
Pm_raw=pts(st,lambda g,i:g=='노면(계획고)')
for lab,xdxf,ydxf in [('tunnel centre x=14126.3',14126.3,15672.0),('road centre x=9776.3',9776.3,15759.0)]:
um=uc+(xdxf-C0[0])/1000.0
m=np.abs(Pm_raw[:,0]-um)<0.05
yt=(Pm_raw[m,1].max()-vc)*1000; yb=(Pm_raw[m,1].min()-vc)*1000
print(f" datum {lab}: model top={yt:.1f} mm, bottom={yb:.1f} mm above lining centre | DXF top at this x = {ydxf-15305:.1f} mm above lining centre")
+23
View File
@@ -0,0 +1,23 @@
import sys
exec(open('/tmp/verify_daesan2_run/scripts/final.py').read().split("for st in ['4745.0005','5100.0','5469.9995']:")[0])
for st in ['4745.0005','5100.0']:
B=to_dxf(pts(st,lambda g,i:g=='기층·인버트(가정)'),st,1); D=to_dxf(pts(st,lambda g,i:g=='배수(측구)'),st,1)
bw=B[(B[:,0]>=9526.3+150)&(B[:,0]<=18726.3-150)]
xs=np.arange(9676.3,18576.3,50.0)
top_line=lambda x: 15484.0+(x-9526.3)*(15300-15484)/9200.0
bot_line=lambda x: 15334.0+(x-9526.3)*(15150-15334)/9200.0
tt=[];bb=[]
for x0 in xs:
m=(bw[:,0]>=x0-25)&(bw[:,0]<x0+25)
if m.sum()==0: continue
tt.append(bw[m,1].max()-top_line(x0)); bb.append(bw[m,1].min()-bot_line(x0))
tt=np.array(tt); bb=np.array(bb)
print(f"{st}: BASE top (pavement-bottom interface), interior 150mm from ends: model-drawn max|dev|={np.abs(tt).max():.1f} mm, mean={tt.mean():.1f} mm (signed mean)")
print(f"{st}: BASE bottom (drawn excavation floor 15334->15150), interior: max|dev|={np.abs(bb).max():.1f} mm, mean={bb.mean():.1f} mm (signed; negative=model below drawing)")
# drains body vs lid
body=np.vstack([REF[k] for k in ['DRN_wallL1','DRN_wallL2','DRN_botL','DRN_pipeL','DRN_wallR1','DRN_wallR2','DRN_botR','DRN_pipeR']])
lid=np.vstack([REF['DRN_topL'],REF['DRN_topR']])
dm=D[(D[:,0]>8900)&(D[:,0]<19300)]
a,_=cKDTree(dm).query(body); b,_=cKDTree(dm).query(lid)
print(f"{st}: DRN body (walls/bottom/pipe) ref->model max={a.max():.1f} mm; lid (450-wide top line) ref->model max={b.max():.1f} mm")
print(f"{st}: DRN model x-extent per ditch: left {dm[dm[:,0]<14126,0].min():.0f}-{dm[dm[:,0]<14126,0].max():.0f}, right {dm[dm[:,0]>14126,0].min():.0f}-{dm[dm[:,0]>14126,0].max():.0f}")
@@ -0,0 +1,102 @@
import sys, json, pickle, collections
sys.path += ['/root/.local/lib/python3.13/site-packages','/usr/local/lib/python3.13/dist-packages','/usr/lib/python3/dist-packages']
import numpy as np
from build123d import import_step, Shape
from OCP.BRepAlgoAPI import BRepAlgoAPI_Section
from OCP.gp import gp_Pln, gp_Pnt, gp_Dir
from OCP.BRepBuilderAPI import BRepBuilderAPI_MakeFace
from OCP.TopExp import TopExp_Explorer
from OCP.TopAbs import TopAbs_EDGE
solids = import_step("/mnt/user-data/uploads/97.과제/Code/3d-drawing/model/tunnel_solids.step").solids()
mp = pickle.load(open('/tmp/verify_daesan2_run/mapping.pkl','rb'))['mapping']
caps = json.load(open('/tmp/verify_daesan2_run/caps.json'))
ch = json.load(open('/tmp/verify_daesan2_run/chain.json'))
meta = json.load(open('/mnt/user-data/uploads/97.과제/Code/3d-drawing/model/tunnel_solids_meta.json'))
comps = {c['id']: c for c in meta['components'] if c['group']!='시설한계(참조선)'}
solid_comp = {v:k for k,v in mp.items()}
# 1) boundary points from lining chain
pts = []
for k in ch['lin']:
pts.append(np.array(ch['starts'][k])); pts.append(np.array(ch['ends'][k]))
P = np.array(pts)[:, :2]
uniq = []
for p in P:
if not any(np.linalg.norm(p-q) < 1e-3 for q in uniq): uniq.append(p)
uniq = np.array(uniq)
print("unique boundary points", len(uniq), "(expect 41)")
D = P[-1]-P[0] if False else (np.array(ch['ends'][ch['lin'][-1]])[:2]-np.array(ch['starts'][ch['lin'][0]])[:2])
proj = uniq@D/np.linalg.norm(D)
order = np.argsort(proj); B = uniq[order]
cum = np.concatenate([[0], np.cumsum(np.linalg.norm(np.diff(B,axis=0),axis=1))])
Sb = 4745 + cum
M = sorted(set([c['from_sta'] for c in comps.values()] + [5480]))
print("boundary count", len(Sb), "meta boundary count", len(M))
print("max |station_chain - meta_boundary| (m):", round(float(np.max(np.abs(Sb-np.array(M)))),3) if len(Sb)==len(M) else 'count mismatch')
# 2) station for each solid by centroid projection
def station_of(p):
best=None
for k in range(len(B)-1):
a,b=B[k],B[k+1]; t=b-a; L2=t@t
f=np.clip(((p-a)@t)/L2,0,1); q=a+f*t; d=np.linalg.norm(p-q)
if best is None or d<best[0]: best=(d, Sb[k]+f*(Sb[k+1]-Sb[k]), k, f)
return best
sol_info={}
for i,s in enumerate(solids):
c=s.center(); d,st,k,f=station_of(np.array([c.X,c.Y]))
cid=solid_comp[i]; sol_info[i]=dict(cid=cid, group=comps[cid]['group'], st=st, lat=d)
# 3) pair within group by sorted mid
assign={}
groups=collections.defaultdict(list)
for i,v in sol_info.items(): groups[v['group']].append(i)
maxres=0
for g,idxs in groups.items():
cs=[c for c in comps.values() if c['group']==g]
idxs_s=sorted(idxs, key=lambda i: sol_info[i]['st'])
cs_s=sorted(cs, key=lambda c: (c['from_sta']+c['to_sta'])/2)
if len(idxs_s)!=len(cs_s): print("COUNT MISMATCH", g, len(idxs_s), len(cs_s))
for i,c in zip(idxs_s, cs_s):
mid=(c['from_sta']+c['to_sta'])/2
res=abs(sol_info[i]['st']-mid); maxres=max(maxres,res)
assign[i]=dict(id=c['id'], group=g, frm=c['from_sta'], to=c['to_sta'], mid_chain=sol_info[i]['st'], lat=sol_info[i]['lat'])
print("max residual solid-centre station vs comp mid (m):", round(maxres,3))
# lateral offset sanity
print("max lateral offset of solid centroid from chain (m):", round(max(v['lat'] for v in assign.values()),3))
# 4) slice
def slice_solid(idx, Q, T):
nrm=np.array([T[0],T[1],0.0])
pl=gp_Pln(gp_Pnt(float(Q[0]),float(Q[1]),0.0), gp_Dir(float(nrm[0]),float(nrm[1]),0.0))
face=BRepBuilderAPI_MakeFace(pl,-80,80,-80,80).Face()
sec=BRepAlgoAPI_Section(solids[idx].wrapped, face); sec.Build()
polys=[]
exp=TopExp_Explorer(sec.Shape(), TopAbs_EDGE)
while exp.More():
e=Shape.cast(exp.Current()); L=e.length; N=max(2,int(L/0.02))
polys.append([[p.X,p.Y,p.Z] for p in (e.position_at(j/N) for j in range(N+1))])
exp.Next()
return polys
def plane_at(s):
k=int(np.searchsorted(Sb, s, side='right')-1); k=min(max(k,0),len(B)-2)
f=(s-Sb[k])/(Sb[k+1]-Sb[k])
Q=(1-f)*B[k]+f*B[k+1]; T=(B[k+1]-B[k]); T=T/np.linalg.norm(T)
return Q, T
out={}
for s in [4745.0005, 5100.0, 5469.9995]:
Q,T=plane_at(s); eh=np.array([-T[1],T[0]])
print(f"== station {s}: Q=({Q[0]:.3f},{Q[1]:.3f}) T=({T[0]:.5f},{T[1]:.5f})")
out[str(s)]={'Q':Q.tolist(),'T':T.tolist(),'eh':eh.tolist(),'solids':{}}
cnt=collections.Counter()
for i,a in assign.items():
if not (a['frm'] <= s <= a['to']): continue
polys=slice_solid(i,Q,T)
uv=[]
for pl_ in polys:
arr=np.array(pl_); u=(arr[:,:2]-Q)@eh; uv.append(np.stack([u,arr[:,2]],1).tolist())
out[str(s)]['solids'][str(i)]={'id':a['id'],'group':a['group'],'polys':uv}
cnt[a['group']]+=1
allp=np.vstack([np.array(x) for x in uv]) if uv else np.zeros((0,2))
if len(allp): print(f" {a['group']:<12} {a['id']:<16} nloops={len(uv)} u[{allp[:,0].min():.3f},{allp[:,0].max():.3f}] v[{allp[:,1].min():.3f},{allp[:,1].max():.3f}]")
else: print(" EMPTY", a['group'], a['id'])
print(" counts", dict(cnt))
json.dump(out, open('/tmp/verify_daesan2_run/sections_final.json','w'))
json.dump({str(i):a for i,a in assign.items()}, open('/tmp/verify_daesan2_run/assign.json','w'), ensure_ascii=False)
print("saved")
+110
View File
@@ -0,0 +1,110 @@
import sys, json, math
sys.path += ['/root/.local/lib/python3.13/site-packages','/usr/local/lib/python3.13/dist-packages','/usr/lib/python3/dist-packages']
import numpy as np
from scipy.spatial import cKDTree
SEC=json.load(open('/tmp/verify_daesan2_run/sections_final.json'))
FIT=json.load(open('/tmp/verify_daesan2_run/fits.json'))
C0=(14126.3,15305.0)
def pts(st, pred):
out=[]
for sid,v in SEC[st]['solids'].items():
if pred(v['group'], v['id']):
for poly in v['polys']: out.append(np.array(poly)[:,:2])
return np.vstack(out)
def arc(cx,cy,r,a0,a1,step=1.0):
if a1<a0: a1+=360
n=max(2,int(r*math.radians(a1-a0)/step)); th=np.radians(np.linspace(a0,a1,n))
return np.c_[cx+r*np.cos(th), cy+r*np.sin(th)]
def seg(x0,y0,x1,y1,step=1.0):
n=max(2,int(math.hypot(x1-x0,y1-y0)/step)); t=np.linspace(0,1,n); return np.c_[x0+(x1-x0)*t,y0+(y1-y0)*t]
def circ(cx,cy,r,step=1.0):
n=max(8,int(2*math.pi*r/step)); th=np.linspace(0,2*math.pi,n,endpoint=False); return np.c_[cx+r*np.cos(th),cy+r*np.sin(th)]
REF={
'LIN_crown_outer': arc(*C0,7282,40,140), 'LIN_crown_inner': arc(*C0,6982,40,140),
'LIN_haunchL_outer': arc(11959.9,17122.8,4454,140,204.67), 'LIN_haunchL_inner': arc(11621.3,17406.9,3712,140,195.56),
'LIN_haunchR_outer': arc(16105.0,16965.3,4699,336.35,40), 'LIN_haunchR_inner': arc(16479.6,17279.7,3910,344.38,40),
'LIN_footL': np.vstack([seg(8171.3,16410.9,8471.3,16410.9), seg(8471.3,16410.9,8621.3,16410.9), seg(8621.3,16410.9,8651.3,16000.9), seg(8651.3,16000.9,8771.3,15825.9), seg(8771.3,15825.9,8771.3,15750.9)]),
'LIN_footR': np.vstack([seg(20181.3,16226.9,19781.3,16226.9), seg(19781.3,16226.9,19631.3,16226.9), seg(19631.3,16226.9,19601.3,15816.9), seg(19601.3,15816.9,19481.3,15641.9), seg(19481.3,15641.9,19481.3,15566.9)]),
'EXC_crown': arc(*C0,7402,40,140), 'EXC_haunchL': arc(11959.9,17122.8,4574,140,203.98), 'EXC_haunchR': arc(16105.0,16965.3,4819,336.97,40),
'PAV_top': seg(9526.3,15764.0,18726.3,15580.0), 'PAV_bot': seg(9526.3,15484.0,18726.3,15300.0), 'PAV_sideL': seg(9526.3,15764.0,9526.3,15484.0), 'PAV_sideR': seg(18726.3,15580.0,18726.3,15300.0),
'BASE_top': seg(9526.3,15484.0,18726.3,15300.0), 'BASE_bot': seg(9526.3,15334.0,18726.3,15150.0),
'DRN_wallL1': seg(9101.3,14864.0,9101.3,15695.5), 'DRN_wallL2': seg(9401.3,14864.0,9401.3,15701.5), 'DRN_botL': seg(9101.3,14864.0,9401.3,14864.0), 'DRN_pipeL': circ(9251.3,15064.0,150),
'DRN_wallR1': seg(18851.3,14680.0,18851.3,15517.5), 'DRN_wallR2': seg(19151.3,14680.0,19151.3,15511.5), 'DRN_botR': seg(18851.3,14680.0,19151.3,14680.0), 'DRN_pipeR': circ(19001.3,14880.0,150),
}
def to_dxf(P, st, sig):
uc,vc,_=FIT[st]['outer']
return np.c_[C0[0]+sig*(P[:,0]-uc)*1000.0, C0[1]+(P[:,1]-vc)*1000.0]
def dist_stats(M, R, label):
"""M model pts (dxf frame), R reference pts. Returns d(ref->model) max, d(model->ref) max within band."""
if len(M)==0 or len(R)==0: return None
tM=cKDTree(M); tR=cKDTree(R)
dR,_=tM.query(R); dM,_=tR.query(M)
return dict(ref_to_model_max=float(dR.max()), ref_to_model_mean=float(dR.mean()), model_to_ref_max=float(dM.max()), model_to_ref_mean=float(dM.mean()), n_ref=len(R), n_model=len(M))
def run(sig, st, feats):
res={}
L=to_dxf(pts(st,lambda g,i:g=='라이닝'),st,sig); E=to_dxf(pts(st,lambda g,i:g=='굴착면'),st,sig)
P=to_dxf(pts(st,lambda g,i:g=='노면(계획고)'),st,sig); B=to_dxf(pts(st,lambda g,i:g=='기층·인버트(가정)'),st,sig)
D=to_dxf(pts(st,lambda g,i:g=='배수(측구)'),st,sig)
def side_clip(R, M, side):
# keep reference points on given side (L: x<C0x, R: x>C0x) and above model's lowest y on that side minus 50mm
sm = M[(M[:,0]<C0[0])] if side=='L' else M[(M[:,0]>C0[0])]
sr = R[(R[:,0]<C0[0])] if side=='L' else R[(R[:,0]>C0[0])]
if len(sm)==0: return sr, 0, None
ymin=sm[:,1].min()-0.05*1000
keep=sr[sr[:,1]>=ymin]
dropped=len(sr)-len(keep)
rng=None if dropped==0 else [round(float(sr[sr[:,1]<ymin,1].min())),round(float(sr[sr[:,1]<ymin,1].max()))]
return keep, dropped, rng
out={}
# Lining: arcs and foot lines; clip below model extent per side
lin_ref=np.vstack([REF[k] for k in ['LIN_crown_outer','LIN_crown_inner','LIN_haunchL_outer','LIN_haunchL_inner','LIN_haunchR_outer','LIN_haunchR_inner']])
keepL,dL,rL=side_clip(lin_ref[lin_ref[:,0]<C0[0]],L,'L'); keepR,dR_,rR=side_clip(lin_ref[lin_ref[:,0]>C0[0]],L,'R')
crown=REF['LIN_crown_outer'];
out['LIN_arcs']=dist_stats(L, np.vstack([crown, REF['LIN_crown_inner'], keepL, keepR]), 'LIN')
out['LIN_arcs']['clipped_below_model_extent']={'L':dL,'R':dR_,'yrange_L':rL,'yrange_R':rR}
out['LIN_footlines']=dist_stats(L, np.vstack([REF['LIN_footL'],REF['LIN_footR']]), 'LIN_foot')
# per-arc max deviations (ref->model), by arc name
perarc={}
for k in ['LIN_crown_outer','LIN_crown_inner','LIN_haunchL_outer','LIN_haunchL_inner','LIN_haunchR_outer','LIN_haunchR_inner']:
R=REF[k]; tM=cKDTree(L); d,_=tM.query(R); perarc[k]=round(float(d.max())*1,1)
out['LIN_perarc_ref2model_max_mm']=perarc
# Excavation
exc_ref=np.vstack([REF['EXC_crown'],REF['EXC_haunchL'],REF['EXC_haunchR']])
keepE_L,eL,reL=side_clip(exc_ref,E,'L'); keepE_R,eR,reR=side_clip(exc_ref,E,'R')
out['EXC_arcs']=dist_stats(E, np.vstack([REF['EXC_crown'],keepE_L,keepE_R]),'EXC')
out['EXC_arcs']['clipped_below_model_extent']={'L':eL,'R':eR,'yrange_L':reL,'yrange_R':reR}
tE=cKDTree(E); d,_=tE.query(REF['EXC_crown']); out['EXC_crown_ref2model_max_mm']=round(float(d.max()),1)
# Pavement: top/bottom/sides
pav_ref=np.vstack([REF['PAV_top'],REF['PAV_bot'],REF['PAV_sideL'],REF['PAV_sideR']])
out['PAV']=dist_stats(P[(P[:,0]>9526.3-150)&(P[:,0]<18726.3+150)], pav_ref,'PAV')
# Base: model points restricted to road width (drawn span)
Bw=B[(B[:,0]>=9526.3-1)&(B[:,0]<=18726.3+1)]
out['BASE_drawn_span']=dist_stats(Bw, np.vstack([REF['BASE_top'],REF['BASE_bot']]),'BASE')
out['BASE_model_extent_x']=[round(float(B[:,0].min())),round(float(B[:,0].max()))]
out['BASE_model_ymin_ymax']=[round(float(B[:,1].min())),round(float(B[:,1].max()))]
# Drains: both ditches vs both references
dref=np.vstack([REF[k] for k in ['DRN_wallL1','DRN_wallL2','DRN_botL','DRN_pipeL','DRN_wallR1','DRN_wallR2','DRN_botR','DRN_pipeR']])
dm=D[(D[:,0]>8900)&(D[:,0]<19300)]
out['DRN']=dist_stats(dm, dref,'DRN')
# per-ditch: ref walls/bottom/pipe to model
tD=cKDTree(dm)
out['DRN_perref_ref2model_max_mm']={k: round(float(tD.query(REF[k])[0].max()),1) for k in ['DRN_wallL1','DRN_wallL2','DRN_botL','DRN_pipeL','DRN_wallR1','DRN_wallR2','DRN_botR','DRN_pipeR']}
out['DRN_model_xrange']=[round(float(dm[:,0].min())),round(float(dm[:,0].max()))]
out['DRN_model_yrange']=[round(float(dm[:,1].min())),round(float(dm[:,1].max()))]
return out
allres={}
for sig in [1,-1]:
for st in ['4745.0005','5100.0','5469.9995']:
r=run(sig,st,None); allres[f"{sig}|{st}"]=r
print(f"=== sigma={sig} st={st} slope={FIT[st]['slope_top']*sig:.5f}")
for k in ['LIN_arcs','EXC_arcs','PAV','BASE_drawn_span','DRN']:
v=r[k];
if v is None: print(' ',k,'none'); continue
print(f" {k:16s} ref->model max={v['ref_to_model_max']*1000:7.1f}mm mean={v['ref_to_model_mean']*1000:6.1f} | model->ref max={v['model_to_ref_max']*1000:7.1f}mm mean={v['model_to_ref_mean']*1000:6.1f}")
if 'clipped_below_model_extent' in v: print(' clipped', v['clipped_below_model_extent'])
print(' LIN foot lines model->ref / ref->model:', None if r['LIN_footlines'] is None else (round(r['LIN_footlines']['model_to_ref_max']*1000,1), round(r['LIN_footlines']['ref_to_model_max']*1000,1)))
print(' LIN per-arc ref->model max mm:', r['LIN_perarc_ref2model_max_mm'])
print(' EXC crown ref->model max mm:', r['EXC_crown_ref2model_max_mm'])
print(' BASE model x-range', r['BASE_model_extent_x'], 'y-range', r['BASE_model_ymin_ymax'])
print(' DRN per-ref ref->model max mm:', r['DRN_perref_ref2model_max_mm'], 'model x', r['DRN_model_xrange'], 'y', r['DRN_model_yrange'])
json.dump(allres, open('/tmp/verify_daesan2_run/hd2_results.json','w'), indent=1, default=str)
+44
View File
@@ -0,0 +1,44 @@
import sys, json, math
sys.path += ['/root/.local/lib/python3.13/site-packages','/usr/local/lib/python3.13/dist-packages','/usr/lib/python3/dist-packages']
import numpy as np
from scipy.spatial import cKDTree
from scipy.optimize import least_squares
import matplotlib; matplotlib.use('Agg'); import matplotlib.pyplot as plt
SEC = json.load(open('/tmp/verify_daesan2_run/sections_final.json'))
def pts_of(st, pred):
out=[]; ids=[]
for sid,v in SEC[st]['solids'].items():
if pred(v['group'], v['id']):
for poly in v['polys']:
out.append(np.array(poly)[:, :2]); ids.append(v['id'])
return (np.vstack(out) if out else np.zeros((0,2))), ids
def fit_circle(P):
x,y=P[:,0],P[:,1]
A=np.c_[2*x,2*y,np.ones(len(x))]; b=x*x+y*y
c,_,_,_=np.linalg.lstsq(A,b,rcond=None)
xc,yc=c[0],c[1]; r=math.sqrt(c[2]+xc*xc+yc*yc)
return xc,yc,r
# ---- lining crown fit per station
fits={}
for st in SEC:
P,_=pts_of(st, lambda g,i: g=='라이닝')
u0=(P[:,0].min()+P[:,0].max())/2; v0=P[:,1].max()-7.282
d=np.hypot(P[:,0]-u0,P[:,1]-v0)
sel_o=P[(np.abs(d-7.282)<0.03)&(P[:,1]-v0>4.0)]
sel_i=P[(np.abs(d-6.982)<0.03)&(P[:,1]-v0>4.0)]
co=fit_circle(sel_o); ci=fit_circle(sel_i)
fits[st]={'outer':co,'inner':ci,'n_o':len(sel_o),'n_i':len(sel_i)}
print(f"st {st}: outer n={len(sel_o)} centre=({co[0]:.4f},{co[1]:.4f}) R={co[2]:.4f} | inner n={len(sel_i)} centre=({ci[0]:.4f},{ci[1]:.4f}) R={ci[2]:.4f}")
# ---- pavement slope (top/bottom envelopes) per station
for st in SEC:
P,_=pts_of(st, lambda g,i: g=='노면(계획고)')
bins=np.arange(P[:,0].min(),P[:,0].max()+0.25,0.25)
tops=[];bots=[]
for a,b in zip(bins[:-1],bins[1:]):
m=(P[:,0]>=a)&(P[:,0]<b)
if m.sum()>0: tops.append((P[m,0].mean(),P[m,1].max())); bots.append((P[m,0].mean(),P[m,1].min()))
tops=np.array(tops); bots=np.array(bots)
mt=np.polyfit(tops[:,0],tops[:,1],1); mb=np.polyfit(bots[:,0],bots[:,1],1)
fits[st]['slope_top']=mt[0]; fits[st]['slope_bot']=mb[0]
print(f"st {st}: PAV top slope dv/du={mt[0]:.5f} (S={mt[0]*100:.3f}%), bottom slope={mb[0]:.5f}; top@u=0 {mt[1]:.4f}, bottom@u=0 {mb[1]:.4f}; u-range [{P[:,0].min():.3f},{P[:,0].max():.3f}]")
json.dump(fits, open('/tmp/verify_daesan2_run/fits.json','w'))
+77
View File
@@ -0,0 +1,77 @@
import sys, json, pickle, math
sys.path += ['/root/.local/lib/python3.13/site-packages','/usr/local/lib/python3.13/dist-packages','/usr/lib/python3/dist-packages']
from build123d import import_step, Edge, Shape
from OCP.BRepAlgoAPI import BRepAlgoAPI_Section
from OCP.gp import gp_Pln, gp_Pnt, gp_Dir
from OCP.BRepBuilderAPI import BRepBuilderAPI_MakeFace
from OCP.TopExp import TopExp_Explorer
from OCP.TopAbs import TopAbs_EDGE
from OCP.TopoDS import TopoDS
import numpy as np
solids = import_step("/mnt/user-data/uploads/97.과제/Code/3d-drawing/model/tunnel_solids.step").solids()
mp = pickle.load(open('/tmp/verify_daesan2_run/mapping.pkl','rb'))['mapping']
caps = json.load(open('/tmp/verify_daesan2_run/caps.json'))
ch = json.load(open('/tmp/verify_daesan2_run/chain.json'))
lin = ch['lin']
starts = {k: np.array(v) for k,v in ch['starts'].items()}
ends = {k: np.array(v) for k,v in ch['ends'].items()}
# station knots
S = [caps[k]['from_sta'] for k in lin] + [caps[lin[-1]]['to_sta']]
P = [starts[k] for k in lin] + [ends[lin[-1]]]
def plane_at(s):
for i in range(len(lin)):
if S[i] <= s <= S[i+1]:
f=(s-S[i])/(S[i+1]-S[i])
Q=(1-f)*P[i]+f*P[i+1]
T=(P[i+1]-P[i])[:2]; T=T/np.linalg.norm(T)
return Q, T, lin[i]
raise ValueError(s)
def slice_solid(idx, Q, T):
nrm=np.array([T[0],T[1],0.0])
pl=gp_Pln(gp_Pnt(*Q), gp_Dir(*nrm))
face=BRepBuilderAPI_MakeFace(pl,-80,80,-80,80).Face()
sec=BRepAlgoAPI_Section(solids[idx].wrapped, face); sec.Build()
sh=sec.Shape()
polys=[]
exp=TopExp_Explorer(sh, TopAbs_EDGE)
while exp.More():
e=Shape.cast(exp.Current())
L=e.length; N=max(2,int(L/0.02))
pts=[e.position_at(i/N) for i in range(N+1)]
polys.append([[p.X,p.Y,p.Z] for p in pts])
exp.Next()
return polys
out={}
stations=[4745.0005, 5100.0, 5469.9995]
for s in stations:
Q,T,lk=plane_at(s)
eh=np.array([-T[1],T[0]])
print(f"== station {s}: comp {lk}, Q=({Q[0]:.4f},{Q[1]:.4f},{Q[2]:.4f}) T=({T[0]:.5f},{T[1]:.5f}) ")
out[str(s)]={'Q':Q.tolist(),'T':T.tolist(),'eh':eh.tolist(),'comps':{}}
for cid,info in caps.items():
if not (info['from_sta'] <= s <= info['to_sta']): continue
polys=slice_solid(mp[cid], Q, T)
uv=[]
for pl_ in polys:
arr=np.array(pl_)
dx=arr[:,:2]-Q[:2]
u=dx@eh; v=arr[:,2]
uv.append(np.stack([u,v],1).tolist())
out[str(s)]['comps'][cid]={'group':info['group'],'polys':uv}
allp=np.vstack([np.array(x) for x in uv]) if uv else np.zeros((0,2))
if info['group'] in ('라이닝','굴착면','노면(계획고)','기층·인버트(가정)','공동구(가정)') or cid.startswith('DRN') or 'DRN' in cid or 'HI' in cid:
pass
# summary per group
import collections
summ=collections.defaultdict(list)
for cid,v in out[str(s)]['comps'].items():
summ[v['group']].append(cid)
for g,cids in summ.items():
allp=[p for cid in cids for poly in out[str(s)]['comps'][cid]['polys'] for p in poly]
A=np.array(allp) if allp else np.zeros((0,2))
if len(A):
print(f" {g}: n_comp={len(cids)} n_pts={len(A)} u[{A[:,0].min():.3f},{A[:,0].max():.3f}] v[{A[:,1].min():.3f},{A[:,1].max():.3f}] ids={cids[:4]}")
else:
print(f" {g}: n_comp={len(cids)} EMPTY ids={cids[:4]}")
json.dump(out, open('/tmp/verify_daesan2_run/sections_raw.json','w'))
print("saved sections_raw.json")
+102
View File
@@ -0,0 +1,102 @@
#!/usr/bin/env python3
"""Step 3 cross-check of the lining deliverables (Task 6).
Re-reads the raw DXF texts (04-004..009 and 03-001) with a plain text search,
independent of extract_lining_spec.py, and compares them with the report and
the model meta. Prints PASS/FAIL per check with population counts.
Usage: verify_lining.py <RAG_ROOT> <DOC_LINING_MD> <DOC_STD_MD> <META_JSON>
"""
import glob
import json
import os
import re
import sys
import ezdxf
from ezdxf import recover
RAG, DOC_LIN, DOC_STD, META = sys.argv[1:5]
RESULTS = []
def check(name, ok, detail):
RESULTS.append((name, ok, detail))
print(f"[{'PASS' if ok else 'FAIL'}] {name}: {detail}")
def all_texts(path):
try:
doc = ezdxf.readfile(path)
except Exception:
doc, _ = recover.readfile(path)
out = []
for e in doc.modelspace():
if e.dxftype() == "TEXT":
out.append(e.dxf.text)
elif e.dxftype() == "MTEXT":
out.append(e.plain_text())
return out
sheets = sorted(glob.glob(os.path.join(RAG, "04_본선터널기타", "C0060304-00[4-9]*.dxf")))
std = glob.glob(os.path.join(RAG, "03_본선터널굴착", "C0060303-001*.dxf"))
raw = {os.path.basename(p)[:14]: all_texts(p) for p in sheets}
doc_lin = open(DOC_LIN, encoding="utf-8").read()
doc_std = open(DOC_STD, encoding="utf-8").read()
meta = json.load(open(META, encoding="utf-8"))
# C1 common values present in every sheet (population = 6 sheets)
common = {"fck=27MPa": "fck 27", "fy=400MPa": "SD400", "최대치수 Ø25": "Ø25",
"R1=6.892": "R1 6.892", "R2=3,712": "R2 3,712", "R2'=3,910": "R2' 3,910",
"EL.-0.367": "EL -0.367", "EL. 0.000": "EL 0.000"}
for token, label in common.items():
hits = [k for k, t in raw.items() if any(token in s for s in t)]
check(f"C1 {label} in all sheets", len(hits) == len(raw), f"{len(hits)}/{len(raw)} sheets (표본 아님, 전수)")
# C2 temperature-not-considered detail appears on 3 sheets (005, 006, 007)
temp = [k for k, t in raw.items() if any("온도미고려" in s for s in t)]
check("C2 온도미고려 detail sheets", sorted(temp) == sorted([k for k in raw if k[9:12] in ("005", "006", "007")]),
f"{len(temp)} sheets: {[k[9:12] for k in sorted(temp)]} (전수)")
# C3 pattern-group count in raw texts equals report rows (9)
groups = [s for t in raw.values() for s in t if "패턴" in s and "주철근 조립도" in s or (("패턴)" in s) and ("입출구부" in s or "온도미고려" in s))]
rows = len([l for l in doc_lin.splitlines() if l.startswith("| C0060304-")])
check("C3 pattern-detail count = report rows", len(groups) == rows,
f"raw {len(groups)} vs report {rows} (전수)")
# C4 R1 cross-check against the standard section drawing and the report table
std_txt = all_texts(std[0]) if std else []
std_has = any(("6.982" in s or "6982" in s or "6,982" in s) for s in std_txt)
check("C4 R1 6.982 in standard section (03-001 raw)", std_has, f"03-001 raw texts scanned: {len(std_txt)}")
check("C4b standard table DOC states R1 6,982", "6,982" in doc_std, "설계_표준단면치수표 본문 대조")
check("C4c report records decision", "6.982 m 유지" in doc_lin, "R1 결정 문구 존재")
# C5 thickness arithmetic: outer R 7.282 m minus inner R
t982 = round(7.282 - 6.982, 3); t892 = round(7.282 - 6.892, 3)
check("C5 thickness arithmetic", t982 == 0.300 and t892 == 0.390,
f"7.282-6.982={t982} m (matches 300 mm), 7.282-6.892={t892} m (does not match)")
# C6 V9 existence + name version + key content of OUTPUT
name = os.path.basename(DOC_LIN)
m = re.match(r"^설계_콘크리트라이닝상세_(\d{4}-\d{2}-\d{2})_v(\d+\.\d+\.\d{2})\.md$", name)
ver_hdr = re.search(r"\| (v\d+\.\d+\.\d{2}) \|", doc_lin)
check("C6 file name pattern and version", bool(m) and ver_hdr is not None and ver_hdr.group(1) == "v" + m.group(2),
f"name v{m.group(2) if m else '?'} vs header {ver_hdr.group(1) if ver_hdr else '?'}")
check("C6b OUTPUT key items present", all(k in doc_lin for k in ["fck", "SD400", "입출구부", "확정 2026-10-21"]),
"fck, SD400, 입출구부, 결정 문구")
# C7 meta: lining reference and open items
lr = meta.get("lining_reference", {})
check("C7 meta lining_reference 9 groups", sum(len(s["groups"]) for s in lr.get("sheets", [])) == 9,
f"{sum(len(s['groups']) for s in lr.get('sheets', []))} groups, status={lr.get('summary', '')[:40]}...")
check("C7b meta open records decision", any("확정 2026-10-21" in o for o in meta.get("open", [])), "open[] 결정 문구")
# C8 scripts never write to the RAG source tree
writes = []
for sc in glob.glob("scripts/*lining*.py"):
txt = open(sc, encoding="utf-8").read()
if re.search(r"open\([^)]*RAG[^)]*['\"]w", txt):
writes.append(sc)
check("C8 no script writes into RAG", not writes, f"scanned {len(glob.glob('scripts/*lining*.py'))} scripts")
n_fail = sum(1 for r in RESULTS if not r[1])
print(f"SUMMARY: {len(RESULTS) - n_fail} PASS / {n_fail} FAIL of {len(RESULTS)}")
+149
View File
@@ -0,0 +1,149 @@
#!/usr/bin/env python3
"""Step 3 deterministic checks for the solid model (Task 8, 대산2본선3D변환).
Usage: python3 verify_solids.py <project_dir> <pattern_csv>
project_dir : Code/3d-drawing folder (contains model/ and data/)
pattern_csv : data/pattern_stations_dangjin.csv
Checks (each prints PASS/FAIL with counts, method and population):
M1 meta counts per group M2 pattern runs contiguous 4745..5480
M3 runs == CSV rows (in range) M4 component ids and from/to/pattern match runs
M5 meta volume sum == verify json M6 pattern-run ids (PAV/BASE/DRN/HBOX/IBOX) present
S1 STEP re-import: solid count, validity, total volume vs meta (0.1 %)
S2 lining vs excavation overlap per run (volume, threshold 0.05 m2 x run length)
G1 GLB reload: geometry count and non-empty meshes
"""
import csv
import json
import os
import sys
from collections import Counter, defaultdict
PROJ, CSV = sys.argv[1], sys.argv[2]
S0, S1 = 4745, 5480
results = []
def rec(code, verdict, method, note=""):
results.append((code, verdict, method, note))
print(f"{code} {verdict} | {method} | {note}")
meta = json.load(open(os.path.join(PROJ, "model/tunnel_solids_meta.json"), encoding="utf-8"))
verify = json.load(open(os.path.join(PROJ, "data/verify_solids.json"), encoding="utf-8"))
comps = meta["components"]
runs = meta["pattern_runs"]
solids_meta = [c for c in comps if not c.get("reference")]
refs = [c for c in comps if c.get("reference")]
# M1 counts per group
cnt = Counter(c["group"] for c in solids_meta)
exp = {"라이닝": 40, "굴착면": 40, "노면(계획고)": 40, "기층·인버트(가정)": 40, "배수(측구)": 80, "공동구(가정)": 80}
bad = {g: (cnt.get(g, 0), n) for g, n in exp.items() if cnt.get(g, 0) != n}
rec("M1", "PASS" if not bad and len(solids_meta) == 320 and len(refs) == 6 else "FAIL",
f"meta components 전수 {len(comps)}", f"solids {len(solids_meta)} (expect 320), refs {len(refs)} (expect 6), group mismatch {bad}")
# M2 contiguity
runs_sorted = sorted(runs, key=lambda r: r["from"])
gaps = [(a["to"], b["from"]) for a, b in zip(runs_sorted, runs_sorted[1:]) if a["to"] != b["from"]]
ok2 = runs_sorted[0]["from"] == S0 and runs_sorted[-1]["to"] == S1 and not gaps
rec("M2", "PASS" if ok2 else "FAIL", f"pattern_runs 전수 {len(runs)}",
f"range {runs_sorted[0]['from']}..{runs_sorted[-1]['to']}, gaps/overlaps {gaps}")
# M3 runs vs CSV rows inside the model range
rows = list(csv.DictReader(open(CSV, encoding="utf-8")))
csv_in = [r for r in rows if float(r["sta_to"]) > S0 and float(r["sta_from"]) < S1]
# clip each CSV interval to the model range (row 95 runs 5468..5495 and is cut at 5480 by design)
csv_set = Counter((max(int(float(r["sta_from"])), S0), min(int(float(r["sta_to"])), S1), r["lining_pattern"]) for r in csv_in)
run_set = Counter((r["from"], r["to"], r["pattern"]) for r in runs)
mism = (csv_set - run_set) + (run_set - csv_set)
rec("M3", "PASS" if not mism else "FAIL",
f"CSV 전수 {len(rows)} 행 중 범위 내 {len(csv_in)} 행과 pattern_runs {len(runs)} 행 대조",
f"mismatch {dict(mism)}")
# M4 component from/to/pattern equal their run
run_by_id = {r["id"]: r for r in runs}
bad4 = []
for c in solids_meta:
if c["group"] == "라이닝":
r = run_by_id.get(c["id"])
if not r or (r["from"], r["to"], r["pattern"]) != (c["from_sta"], c["to_sta"], c["pattern"]):
bad4.append(c["id"])
rec("M4", "PASS" if not bad4 else "FAIL", f"라이닝 전수 {cnt['라이닝']} 대 run",
f"mismatch {bad4[:5]}")
# M5 volume sum
vol = sum(c["volume_m3"] for c in solids_meta)
ok5 = abs(vol - verify["volume_sum_m3"]) < 0.001
rec("M5", "PASS" if ok5 else "FAIL", f"meta 부피 합 전수 {len(solids_meta)} 개",
f"sum {vol:.3f} vs verify {verify['volume_sum_m3']}")
# M6 per-run component ids
need = ["LIN", "EXC", "PAV", "BASE", "DRNL", "DRNR", "HBOX", "IBOX"]
ids = {c["id"] for c in solids_meta}
miss = []
for i, r in enumerate(runs_sorted):
for pre in need:
cid = f"{pre}_{i+1:02d}_{r['pattern']}"
if pre in ("DRNL", "DRNR"):
cid = f"{pre}_{i+1:02d}_{r['pattern']}"
if cid not in ids and pre not in ("DRNL", "DRNR"):
miss.append(cid)
drn = [c for c in solids_meta if c["group"] == "배수(측구)"]
rec("M6", "PASS" if not miss and len(drn) == 80 else "FAIL",
f"run {len(runs_sorted)} x 부품 종류 {len(need)} 대조 (배수 L/R 포함 {len(drn)})",
f"missing {miss[:5]}")
# S1 STEP re-import
import build123d as bd # noqa: E402
step_path = os.path.join(PROJ, "model/tunnel_solids.step")
shape = bd.importers.import_step(step_path) if hasattr(bd, "importers") else bd.import_step(step_path)
solids = shape.solids()
n_valid = sum(1 for s in solids if s.is_valid)
vol_step = sum(s.volume for s in solids)
rel = abs(vol_step - vol) / vol
rec("S1", "PASS" if len(solids) == 320 and n_valid == len(solids) and rel < 0.001 else "FAIL",
f"STEP 재임포트 솔리드 전수 {len(solids)}, is_valid 전수 {n_valid}, 부피 재계산 합 vs meta",
f"count {len(solids)}, valid {n_valid}, vol_step {vol_step:.3f} vs meta {vol:.3f} (rel {rel:.2e})")
# S2 overlap lining vs excavation per run, using the STEP solids
def centroid(s):
return s.center()
lin = [s for s in solids]
# classify STEP solids by volume match to meta (only for overlap sampling)
meta_vol = sorted([(c["volume_m3"], c["group"], c["id"], c["from_sta"], c["to_sta"]) for c in solids_meta])
sample_ids = [r["id"] for r in runs_sorted[:: max(1, len(runs_sorted) // 4)]][:4]
lines = []
ok_overlap = True
for rid in sample_ids:
r = run_by_id[rid]
k = rid.split("_", 1)[1]
idx = [i for i, rr in enumerate(runs_sorted) if rr["id"] == rid][0] + 1
lin_c = next(c for c in solids_meta if c["id"] == f"LIN_{idx:02d}_{r['pattern']}")
exc_c = next(c for c in solids_meta if c["id"] == f"EXC_{idx:02d}_{r['pattern']}")
def find(vol_target):
best = min(solids, key=lambda s: abs(s.volume - vol_target))
assert abs(best.volume - vol_target) < 1e-3, "no STEP solid matches meta volume"
return best
sl = find(lin_c["volume_m3"]); se = find(exc_c["volume_m3"])
inter = sl.intersect(se)
iv = sum(x.volume for x in inter) if inter is not None else 0.0
length = r["to"] - r["from"]
thr = 0.05 * length
lines.append(f"{rid}: ovl {iv:.4f} m3 (thr {thr:.2f})")
if iv > thr:
ok_overlap = False
rec("S2", "PASS" if ok_overlap else "FAIL", f"샘플 run {len(sample_ids)}건 라이닝∩굴착 부피 (부피 매칭 솔리드)",
"; ".join(lines))
# G1 GLB reload
import trimesh # noqa: E402
glb = trimesh.load(os.path.join(PROJ, "model/tunnel_solids.glb"), force="scene")
geoms = list(glb.geometry.values())
empty = [g for g in geoms if len(g.faces) == 0]
rec("G1", "PASS" if len(geoms) == verify["glb_geometries"] and not empty else "FAIL",
f"GLB 로드 지오메트리 전수 {len(geoms)}", f"empty {len(empty)}, expect {verify['glb_geometries']}")
fails = [r for r in results if r[1] == "FAIL"]
print("SUMMARY", len(results), "checks, FAIL", len(fails))
@@ -0,0 +1,39 @@
#!/usr/bin/env python3
"""Headless browser check of the 3D viewer (Task 6, V11).
Needs Playwright + Chromium and the viewer/model served over HTTP, e.g.
python3 -m http.server 8765 (from Code/3d-drawing)
python3 verify_viewer_headless.py http://127.0.0.1:8765/viewer/index.html <chromium_path>
Checks: page loads without page errors, canvas exists, meta text is shown,
and a click on the lining (other layers hidden) returns station and pattern.
"""
import sys
from playwright.sync_api import sync_playwright
URL, CHROME = sys.argv[1], sys.argv[2]
with sync_playwright() as p:
b = p.chromium.launch(executable_path=CHROME, args=["--use-gl=swiftshader", "--enable-webgl", "--ignore-gpu-blocklist", "--no-sandbox"])
pg = b.new_page(viewport={"width": 1280, "height": 800})
errors = []
pg.on("pageerror", lambda e: errors.append(str(e)))
pg.goto(URL, wait_until="networkidle", timeout=90000)
pg.wait_for_timeout(6000)
canvas = pg.evaluate("() => !!document.querySelector('canvas')")
body = pg.evaluate("() => document.body.innerText")
print("page errors:", errors)
print("canvas present:", canvas, "| meta shown:", "패턴:" in body and "범위:" in body)
for label in ["excavation_R7402", "road_surface", "ground_GL"]:
pg.locator("label", has_text=label).locator("input").uncheck()
pg.wait_for_timeout(500)
readout = None
for fx in [0.5, 0.45, 0.55, 0.4, 0.6, 0.35, 0.65]:
for fy in [0.5, 0.45, 0.55, 0.4, 0.6]:
pg.mouse.click(1280 * fx, 800 * fy)
pg.wait_for_timeout(200)
t = pg.evaluate("() => { const els=[...document.querySelectorAll('div,pre,p')]; return els.map(e=>e.innerText).filter(s=>s && s.includes('교차점')).pop()||null }")
if t and "측점" in t:
readout = t
break
if readout:
break
print("lining readout:", (readout or "NO LINING HIT").replace("\n", " | ")[:700])
b.close()