knowledge(요구): 요구 504 분석 기록 — 미리 계산 시간 쪼개기 · D8 · 라플라스 · E 코어 · GPU 득실

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_015uTVHHDv583hbLQAFeBmd9
This commit is contained in:
2026-10-31 10:13:43 +09:00
co-authored by Claude Opus 5.5
parent 338a497e2a
commit 1b1d5daeda
@@ -0,0 +1,241 @@
# 분석 504 — 전국 미리 계산 단축 · GPU
실측 = 10-31 09:35~10:20 · 미리 계산(일꾼 7)이 도는 중 · 한 프로세스(보통 우선순위)로 잼 → 절대 초는 부하 낀 값 · 몫(%) · 배수가 판단 근거. 창고 · 코드 안 건드림(셈은 메모리 · 저장 시간은 임시 폴더).
## 결론
- 남은 시간(지금 그대로 · 모형) ≈ **61 h** = 목록 dem 19.5 + 0차곡 단계 이웃 dem 5.1 + **D8 36.8**. 지금 잡힌 zero 가운데 14 s 는 산지 100 장 값 · 남은 끝자락은 장당 D8 10~118 s(평균 63 s).
- 가장 큰 한 수 = **D8 평지 이어 감 고침**(같은 결과 · 6~30 배) → −34 h. dem 단계가 끝나기 전에 이 폴더에 들어가면 zero 일꾼 묶음이 새로 떠서 새 코드를 읽음 → 재시작 불필요.
- 둘째 = 라플라스 홀짝 덩이(4.5 배) + 폐합 채움 창(2~3 배) → dem −14 h(재시작 필요). 둘 다 비트 동일 실측(표본 8 장).
- 셋째 = **E 코어 8 개가 놂** — 전원 계획 셋 모두 SCHEDPOLICY = 1(P 코어에만) → 일꾼은 코어 몫 63~70 %. 정책 바꿈 + 일꾼 14 → ×1.7~1.9. 정책 그대로 일꾼만 올리면 +5 % 안팎.
- 다 하면 ≈ 7 h. GPU 는 라플라스 · EDT 에 맞으나 툴킷 · CuPy 없음 · EDT 비트 동일 미확인 · 만드는 데 1 일 → 이번엔 −3 h 뿐이라 손해.
## 1. 시간 쪼개기 (한 장)
### dem 단계 (`common_util_terrain_store.py:281 ensure_sheet_terrain` 길 그대로 재현 · 산지 도엽 결과 = 창고 npz 와 비트 동일)
| 구간 | 자리 | 산지 36901046 (721 km · 수위 88) | 끝자락 37612075 (191 km · 수위 33) | 평지 37616009 (99 km · 수위 15 · 목록 밖 이웃) |
|---|---|---|---|---|
| 도엽 안쪽 칸(좌표 변환) | store.py:172 | 0.32 s · 1 % | 0.32 · 1 % | 0.31 · 1 % |
| 전국 gpkg 읽기 | store.py:192 | 1.54 · 3 % | 0.16 · 0 % | 0.16 · 0 % |
| 등고선 굽기 | surface.py:48 | 0.35 · 1 % | 0.08 · 0 % | 0.05 · 0 % |
| 거리 비례 — EDT(수위마다 통격자) | B04 SheetMethods.py:78 | 13.18 · 24 % | 4.76 · 12 % | 2.17 · 6 % |
| 거리 비례 — np.where 6 번/수위 | SheetMethods.py:79-86 | 2.82 · 5 % | 1.02 · 3 % | 0.46 · 1 % |
| **라플라스 400 회** | SheetMethods.py:99 | **27.81 · 50 %** | **27.68 · 73 %** | **27.44 · 81 %** |
| 폐합 안쪽 채움 | surface.py:102 | 8.63 · 16 % | 3.53 · 9 % | 2.68 · 8 % |
| 저장(npz 압축 + fsync) | atomic.py:35 | 0.47 · 1 % | 0.42 · 1 % | 0.40 · 1 % |
| 합 | | 55.3 s | 38.1 s | 33.8 s |
- 격자 = 약 1,591 × 1,316 칸(도엽 + 여유 200 m · 2 m) · 라플라스는 도엽 모양과 무관하게 장당 약 27.5 s 고정.
- 남은 도엽 등고선 길이 가운데 417 km(끝난 것 703 km) → 남은 쪽은 라플라스 몫이 더 큼(70 % 안팎).
### zero 단계 (`D03_PlanAnalysis_ZeroOrder.py:56 d8_accumulation` 같은 코드 구간 시각 · 같은 크기 격자)
| 구간 | 자리 | 산지 36901046 | 끝자락 37612075 | 평지 37616009 |
|---|---|---|---|---|
| 웅덩이 메움(형태 재구성) | ZeroOrder.py:63-69 | 0.93 s · 6 % | 1.00 · 1 % | 1.07 · 3 % |
| 가장 가파른 이웃 | :73-78 | 0.34 · 2 % | 0.36 · 0 % | 0.38 · 1 % |
| **평지 이어 감(while)** | **:79-91** | **13.48 · 82 % (110 바퀴)** | **84.72 · 97 % (702 바퀴)** | **38.27 · 93 % (310 바퀴)** |
| 위→아래 더함(파이썬 스택) | :92-104 | 0.99 · 6 % | 1.04 · 1 % | 1.06 · 3 % |
| 저장(uint16 압축) | :197 | 0.48 · 3 % | 0.38 · 0 % | 0.35 · 1 % |
| 합 | | 16.5 s | 87.7 s | 41.3 s |
- 평지 이어 감 = 바퀴마다 8 방향 × 통격자 `np.pad` 두 번(그중 `filled` float64 = 바뀌지 않는데 매번 다시) → 바퀴 수 = 가장 긴 평지 길이(칸) · 평지가 많을수록 폭증.
- 지금 기록의 zero 가운데 14 s 는 첫 100 장(36901 산지)뿐 · 평균 37 s 는 목록 밖 이웃 dem 셈이 낀 값(run_step 이 이웃 9 장 `ensure_sheet_terrain` 부름 · precompute_terrain.py:217).
- 지형 읽기(`load_terrain` 이웃 9 장)는 0.3 s 안팎.
- 0차곡 단계가 새로 셀 목록 밖 이웃 = 3,191 장(등고선 있음 · 수위 가운데 20) + 등고선 없는 이웃 769 장(도엽 zip 받기 길 · 받기 실패면 그 가운데 도엽 zero 실패 → 다음에 다시).
### 남은 목록 자리별 표본 (dem 은 빠른 꼴 · 결과 동일 / D8 원본 대 고친 꼴)
| 차례 | 도엽 | 등고선 km · 높이 차 m | 수위 | dem 빠른 꼴 s | D8 원본 s | D8 고친 꼴 s | 같음 |
|---|---|---|---|---|---|---|---|
| 1,503(끝남) | 36901046 | 721 · 400 | 88 | ~27 | 16.0 | 2.5 | 참 |
| 4,500 | 37711003 | 579 · 420 | 85 | 26.0 | 35.9 | 2.8 | 참 |
| 6,500 | 37706012 | 496 · 535 | 118 | 30.5 | 10.1 | 3.2 | 참 |
| 8,500 | 37814094 | 407 · 240 | 50 | 20.5 | 77.0 | 3.8 | 참 |
| 10,500 | 35615054 | 302 · 190 | 47 | 15.4 | 49.8 | 3.3 | 참 |
| 12,000 | 37612075 | 191 · 160 | 33 | ~13 | 84.7 | 2.9 | 참 |
| 12,500 | 36704016 | 134 · 200 | 41 | 11.2 | 118.3 | 6.9 | 참 |
| 목록 밖 이웃 | 37616009 | 99 · 60 | 15 | ~10.5 | 37.9 | 3.5 | 참 |
- 남은 목록 표본 6 장 D8 원본 평균 62.6 s(10~118 · 평지 바퀴 수에 달림) · 고친 꼴 평균 3.8 s.
- dem 빠른 꼴 = 1.0 + 0.157 × 수위 + 6.0 + 0.4 + 0.049 × 수위(s) — 표본과 ±5 % 안 맞음 · 원본 = 1.0 + 0.175 × 수위 + 27.6 + 1.6 + 0.08 × 수위.
## 2. 일꾼 수
### 실측 처리량 (창고 `dem_*.key.json` 수정 시각 · 시간대별)
| 시각(10-31) | 일꾼 | 장/시 | 장당 평균 s |
|---|---|---|---|
| 01~05 시 | 4 | 312~330 | 43.7~46.4 |
| 07 시 | 7 | 435 | 58.0 |
| 08 시 | 7 | 402 | 62.9 |
| 09 시(36 분) | 7 | 220 → 시간당 약 367 | 70.7 |
- 4 → 7 일꾼 = 처리량 +25~36 %(이상값 +75 %) · 장당 시간 45 → 63~71 s. 남은 도엽이 더 가벼운데도 장당 시간이 늘어남 = CPU 몫 부족.
### CPU 실측 (8 s 표본)
| 항목 | 값 |
|---|---|
| 일꾼 7 개 | 각 63~70 % (BELOW_NORMAL) |
| 논리 CPU 0~11 (P 코어 6 × HT) | 92~100 % |
| 논리 CPU 12~19 (E 코어 8) | 0~14 % — 놂 |
| 다른 큰 손 | Code.exe 95 % · ipf_helper.exe 90 % · cloud-drive-daemon.exe(시놀로지 · 새 지형 파일 동기화) 90 % · Orca 31 % (모두 보통 우선순위 → 일꾼보다 먼저 P 코어 차지) |
| 전원 계획 | 균형 · Silent · Performance 모두 SCHEDPOLICY = 1 「성능 기준에 맞는 프로세서」(지금 Performance) |
| 메모리 | 일꾼 RSS 220~360 MB · 최대 작업 집합 710~930 MB · 남은 RAM 34.5 GB |
| 디스크 | dem npz 장당 4.3 MB · zero 1.3 MB → 끝까지 약 70 + 17 GB · C: 여유 334 GB · 쓰기 1 MB/s 아래(한계 아님) · 단 시놀로지가 87 GB 를 데스크탑에 옮김 |
### E 코어 실측 (내 프로세스만 E 코어 고정 · 라플라스 원본 100 회 · 끝자락 격자)
| 조건 | 장당 몫 s(100 회) |
|---|---|
| E 코어 혼자 | 6.68 |
| E 코어에 4 개 동시 | 8.37 |
| E 코어에 8 개 동시 | 10.50 (혼자 대비 −36 % · E 묶음 L2 · 메모리 겹침) |
| 고정 안 함(P 코어 붐빔 · 보통 우선순위) | 7.86 |
| E 코어 8 개 동시 · 홀짝 판 | 2.36 |
→ E 코어 8 개 = 지금 일꾼 7 개만큼의 일을 더 할 수 있음(E 일꾼 장당 약 60~70 s).
### 일꾼 수별 어림 (코드 그대로 · dem 단계)
| 일꾼 | 지금 정책(E 코어 막힘) | 정책 바꿈(E 코어 열림) |
|---|---|---|
| 7 | ~400 장/시(실측) · CPU 전체 ~63 % | ~480 (×1.2 · 일꾼 몫 66 → ~100 %) · ~65 % |
| 10 | ~420 (+5 %) · 장당 시간만 늘어남 | ~580 (×1.45) · ~75 % |
| 12 | ~430 | ~650 (×1.6) · ~85 % |
| 14 | ~430 | ~700~760 (×1.75~1.9) · ~95 % (E 일꾼 = 내 측정 프로세스의 ~0.65 · 메모리 대역 · E 묶음 L2 겹침 감안) |
- 어림 근거 = 08 시 실측 402 장/시 ↔ 한 프로세스 모형 장당 54.9 s → 일꾼 7 의 실효 병렬 6.1 · E 코어 8 개 동시 실측 6.68 → 10.50 s.
- 일꾼 14 × 최대 930 MB ≈ 13 GB → 메모리 여유 충분.
- 정책 바꿈 = 시스템 설정(사용자 몫): `powercfg /setacvalueindex SCHEME_CURRENT SUB_PROCESSOR SCHEDPOLICY 5`(자동) + `powercfg /setactive SCHEME_CURRENT` · 되돌림 = 값 1 · 도는 프로세스에 바로 듦(일꾼 수는 재시작해야 바뀜) · Armoury Crate 모드를 바꾸면 다시 덮일 수 있음.
- 정책 안 바꾸는 길 = 두 번째 미리 계산을 E 코어 고정(`start /low /affinity FF000`)으로 다른 도엽 묶음(`--sheets` 앞자리)에 — 잠금이 겹침은 막으나 진행 기록 파일을 서로 덮음(잃은 기록은 다음 실행에서 빠르게 건너뜀) → 버금 길.
## 3. GPU
### 환경
| 항목 | 값 |
|---|---|
| GPU | RTX 3060 Laptop 6 GB · 드라이버 592.00 · CUDA 13.1 · WDDM · 지금 665 MB 씀 |
| CUDA 툴킷 | 없음(CUDA_PATH · nvcc 없음) |
| venv | cupy · torch · numba · numexpr · cv2 없음 / numpy 1.26.4 · scipy 1.13.1 · skimage 0.26.0 있음 |
### 쓸 길
| 길 | 라이선스 | 설치(어림) | 맞는 몫 |
|---|---|---|---|
| CuPy `cupy-cuda12x` / `cupy-cuda13x` 14.2.0(PyPI 확인) | MIT | 바퀴 ~100 MB + CUDA 런타임(NVRTC 등) 툴킷 ~3 GB 또는 pip nvidia-* ~1 GB | 라플라스(ElementwiseKernel) · EDT(`cupyx.scipy.ndimage.distance_transform_edt`) — numpy 꼴 그대로 · 가장 짧음 |
| PyTorch CUDA | BSD-3 | ~2.5~3 GB | 라플라스(conv) · EDT 없음 |
| numba.cuda | BSD-2 | numba + llvmlite ~40 MB + CUDA 툴킷 | 라플라스 커널 손수 |
### 몫별 맞음
| 몫 | GPU | 어림 배수 |
|---|---|---|
| 라플라스 400 회 | 맞음 — 격자 스텐실 · 홀짝 칸 독립 | 장당 0.1~0.3 s(대역 336 GB/s 기준) · CPU 원본 27.5 s · CPU 홀짝 6 s |
| 거리 비례 EDT | 맞음 — cupyx EDT | 0.1~0.5 s · CPU 2~13 s |
| 폐합 안쪽 채움 | 안 맞음 — 작은 창 수백 개 · 차례 | — |
| D8 웅덩이 메움 · 위→아래 더함 | 안 맞음 — 차례 의존(큐 · 위상 정렬) | — |
| D8 평지 이어 감 | 맞으나 불필요 — CPU 고침으로 1 s 아래 | — |
### 같은 결과 문제
- 라플라스: float32 `((위+아래)+왼)+오른` → `×0.25`(2 의 거듭제곱 · 정확) → FMA 끼어들 자리 없음 → 빠른 수학 끄면 비트 동일 가능.
- EDT: 거리 = √(정수² × 4 합) → float64 정확 · √ 는 IEEE 정확 반올림 → 같을 공산 크나 cupyx 내부 경로(float32 여부) 미확인.
- 비트 동일 못 맞추면 `TERRAIN_ALGO`(surface.py:26) 올려야 → 이미 센 3,600 장 다시 셈 + GPU 없는 PC(데스크탑 · 서버)가 나중에 센 도엽과 값 어긋남 → 사실상 비트 동일 필수.
- 꼴: `common_util_terrain_surface.py` 에 공용 판 거리 비례 · 라플라스(B04 안 고침) → `cupy` 있으면 GPU 갈래 · 없으면 CPU 홀짝 판(같은 값) · 일꾼마다 CUDA 문맥 ~300 MB → 14 개면 ~4 GB(6 GB 중) 또는 GPU 전담 일꾼 하나.
### 득실 (남은 dem 9,258 + 이웃 3,191 · D8 12,804)
| 길 | 남은 계산 | 만들고 검증 | 판단 |
|---|---|---|---|
| ③④ CPU 고침 | ~13 h(일꾼 7) / ~7 h(①② 함께) | 3~4 h | 득 큼 |
| ⑤ GPU 더함 | ~4 h(①~④ 대비 −3 h · GPU 한 장치가 장당 ~0.6 s 몫으로 병목) | ~1 일(툴킷 · CuPy 설치 · 갈래 · EDT 비트 대조 · GPU 없는 PC 길 유지) | 이번엔 손해 |
| ⑤ 만(CPU 고침 없이) | 라플라스 · EDT 만 빠짐 · D8 36.8 h 그대로 | ~1 일 | 손해 |
→ GPU 는 다음 전국 재계산(`TERRAIN_ALGO` 올릴 때)이나 EDT 가 남은 몫 대부분이 될 때 검토.
## 4. CPU 쪽 단축 (같은 결과 · 시제품으로 비트 동일 실측)
| 고침 | 자리 | 전 → 후 (s) | 배수 | 같은 결과 |
|---|---|---|---|---|
| D8 평지 이어 감 — 같은 높이 이웃 표 8 장을 한 번만 · 아직 안 흐르는 후보 칸 목록만 돌림(방향 차례 · 갱신 차례 그대로) | ZeroOrder.py:79-91 | 16.0→2.5 · 84.7→2.9 · 37.9→3.5 · 남은 목록 표본 6 장 평균 62.6→3.8 | 6~30 | `array_equal` 참(표본 8 장 모두) |
| 라플라스 — 격자를 홀짝 네 덩이(연속 메모리)로 나눠 그 색 칸만 셈 · 덧셈 차례 같음 | 새 공용 판(SheetMethods.py:99 본뜸) | 27.2→6.1 · 짝 · 홀 크기 모두 | 4.5 | 참 |
|  (버금) `surface[colour]=` → `np.copyto(where=)` · 덧대기 한 번 | 〃 | 28.2→12.4 | 2.3 | 참 |
| 폐합 안쪽 채움 — 채움 · 묶음을 그 수위 상자(+1 칸) 안에서 · 묶음은 `find_objects` 창으로(통격자 `components == id` 없앰) | surface.py:117-131 | 9.0→4.7 · 3.2→1.1 | 1.9~2.8 | 참(채운 수 같음) |
| 거리 비례 — `np.where` 새 배열 → `np.copyto(where=)` | SheetMethods.py:79-86 본뜸 | 15.2→13.8 · 5.6→5.0 | 1.1 | 참 |
| npz 압축 수준 1 | atomic.py:47 | 0.45→0.24 | 장당 −0.2 s · 크기 +3~5 % | 값 같음 — 득 작아 안 함 |
| gpkg 읽기 묶기 | store.py:208 | 0.16~1.5 s(≤3 %) | — | 득 작아 안 함 |
| numba(설치 필요 · BSD-2) | D8 스택 · 메움 | 장당 ~2 s 몫 | — | 고친 뒤 zero 3 s → 불필요 |
- 고친 뒤 장당(한 프로세스 · 부하 낀 값): dem 산지 55.3 → ~27 s(2.0 배) · 끝자락 38.1 → ~13 s(2.9 배) · 평지 33.8 → ~10.5 s(3.2 배) · zero 16~88 → 2.5~3.5 s.
- 고친 뒤 dem 의 가장 큰 몫 = EDT(산지 48 % · 끝자락 37 % · 수위 100 넘는 중간 도엽 ~60 %) → GPU 후보는 그때 EDT 하나. EDT 를 CPU 에서 같은 값으로 줄일 쉬운 길은 못 찾음(수위별 창 자르기 = 둘째 가까운 거리 상한이 평지에서 커서 득 없음).
- 꼴: 라플라스 · 거리 비례는 `common_util_terrain_surface.py`(180 줄 → ~260 줄)에 공용 판을 두고 `build_contour_surface` 가 부름(B04 안 고침) · 비트 동일이라 `TERRAIN_ALGO` = 1 그대로 → 센 저장본 그대로 유효. D8 은 D03 파일 그대로 고침 · `ZERO_ORDER_VERSION` 그대로.
- 검증: 창고에 있는 도엽 30 장 안팎을 새 길로 셈 → 저장 npz 와 `array_equal` · zero 는 원본 d8 과 `array_equal` · 거울 시험 `resources/tester`.
- 시제품 파일은 지시대로 지움 — 핵심 두 가지는 부록에.
## 5. 권함 순서
### 지금 바로 (코드 안 고침)
| 할 것 | 줄어드는 시간 | 위험 | 드는 시간 |
|---|---|---|---|
| ① 전원 정책 SCHEDPOLICY 1 → 5(자동) — 사용자 승인 | dem ~400 → ~480 장/시(일꾼 7 그대로 · 재시작 없이 바로) | 낮음 · 다른 앱도 E 코어로 감 · 되돌림 = 값 1 | 1 분 |
| ② ① 뒤 일꾼 7 → 14 재시작(`--workers 14`) | → ~700~760 장/시(×1.75~1.9) · 남은 61 h → ~34 h | 낮음 · 도는 7 장(~1 분)만 다시 셈 · 메모리 ~13 GB | 2 분 |
| (①을 못 하면) 일꾼 수 올리기 | +5 % 안팎 | 장당 시간만 늚 | — |
| 시놀로지(cloud-drive-daemon) · ipf_helper · Code 가 P 코어 ~2.7 개 | 미리 계산 동안 시놀로지 동기화 쉼 → 일꾼 몫 회복 | 데스크탑 반영 늦음 · ipf_helper 90 % 는 이상 소모 | — |
### 코드 고침 뒤
| 할 것 | 줄어드는 시간(일꾼 7 기준) | 위험 | 드는 시간 |
|---|---|---|---|
| ③ D8 평지 이어 감 고침(D03 · 함수 하나) | 0차곡 D8 36.8 → 2.7 h(**−34 h**) · dem 단계 끝나기 전(지금 속도 ~19 h 뒤)에 들어가면 **재시작 불필요**(단계마다 일꾼 묶음을 새로 띄움 · precompute_terrain.py:324) | 낮음 — 비트 동일 표본 8 장 · 열쇠 그대로 · 파일이 반쯤 쓰인 때 묶음이 뜨면 그 단계 실패 → 커밋으로 한 번에 | 1~1.5 h |
| ④ 라플라스 홀짝 + 폐합 채움 창(+ 거리 copyto) 공용 판 | 목록 dem 19.5 → 8.6 h · 이웃 dem 5.1 → 1.6 h(**−14 h**) · 재시작 필요(도는 dem 일꾼은 옛 코드) | 낮음~중간 — 비트 동일 실측 · 공용 판이 B04 와 두 벌(짝 시험으로 묶음) | 2~3 h |
| ⑤ GPU(CuPy) 라플라스 · EDT | ①~④ 7 h → ~4 h(−3 h) | 중간~높음 — 툴킷 설치 · EDT 비트 동일 미확인 · GPU 없는 PC 갈래 | 1 일 안팎 |
### 남은 시간 (모형 · D8 표본 편차로 ±30 %)
| 길 | 목록 dem 9,258 | 0차곡 단계(이웃 dem 3,191 + D8 12,804) | 합 |
|---|---|---|---|
| 지금 그대로(일꾼 7) | 19.5 h | 5.1 + 36.8 h | ~61 h |
| ①② | ~11 h | ~23 h | ~34 h |
| ③ 만 | 19.5 h | 5.1 + 2.7 h | ~27 h |
| ③④ | 8.6 h | 1.6 + 2.7 h | ~13 h(+ 만들기 3~4 h · 그동안 지금 셈은 계속) |
| ①~④ | ~4.8 h | ~2.4 h | ~7 h |
| ①~⑤ | ~2 h | ~1.8 h | ~4 h(+ 만들기 1 일) |
## 부록 — 시제품 핵심 (비트 동일 확인한 꼴)
D8 평지 이어 감(`ZeroOrder.py:79-91` 대체 · 앞뒤 그대로):
```python
eq = [_shift(filled, dr, dc, np.nan) == filled for dr, dc in _D8] # 한 번만
cand = np.flatnonzero((~done & np.any(eq, axis=0)).ravel())
done_f, valid_f, rec_f = done.ravel(), valid.ravel(), receiver.ravel() # 보기
r, c = np.divmod(cand, cols)
nbr, ok = [], []
for k, (dr, dc) in enumerate(_D8):
inb = (r + dr >= 0) & (r + dr < rows) & (c + dc >= 0) & (c + dc < cols)
ok.append(inb & eq[k].ravel()[cand]); nbr.append(np.where(inb, cand + dr * cols + dc, 0))
while True:
grew = False
for k, (dr, dc) in enumerate(_D8): # 방향 차례 · 방향마다 done 갱신 = 원본과 같음
take = ok[k] & ~done_f[cand] & done_f[nbr[k]] & valid_f[nbr[k]]
if take.any():
hit = cand[take]; rec_f[hit] = hit + dr * cols + dc; done_f[hit] = True; grew = True
if not grew:
break
keep = ~done_f[cand]
cand, nbr, ok = cand[keep], [n[keep] for n in nbr], [o[keep] for o in ok]
```
라플라스 홀짝 덩이(`relax_laplace` 와 같은 값):
- 격자를 `S[p::2, q::2]` 네 덩이로 · 덩이마다 가장자리 한 칸 덧댄 float32 버퍼.
- 반 걸음 = 빨강 (0,0)·(1,1) → 검정 (0,1)·(1,0). (p,q) 칸의 위·아래 = (1−p,q) 버퍼 행을 한 칸 밀어 본 보기 · 왼·오른 = (p,1−q) 버퍼 열을 밀어 본 보기 · 격자 끝 = 자기 값(원본 edge 덧대기와 같음) → 셈 전 그 덧댄 한 줄만 자기 첫·끝 행·열로 채움.
- `np.add(위, 아래, out=t); t += 왼; t += 오른; t *= np.float32(0.25); np.copyto(x, t, where=free_pq)` — 덧셈 차례 원본과 같음.
- 끝에 `S[p::2, q::2] = 덩이`. 짝 · 홀 크기 모두 `array_equal` 참.
폐합 채움 창: 수위마다 그 수위 칸 상자 + 1 칸 0 덧댐 안에서 `binary_fill_holes` · `label` → `find_objects` 상자로 묶음 하나씩 · 창 = 상자 ± (`_BAND_CELLS` + 2) 를 격자 안으로 자름(원본 `_window` 와 같은 창) · 차례(수위 → 묶음 번호) 그대로.