aicfd#

CFD 해석 결과를 신경망으로 예측하는 모델 5종의 기술 문서다. 건물 외부 유동, 사무실 실내 공조, 비강 기류를 각각 다루는데, 다루는 대상은 달라도 학습 골격은 하나를 같이 쓴다.

모델마다 독립된 repo 로 관리하고 배포한다. 이 superproject 는 배포용이 아니라, 다섯을 나란히 두고 한자리에서 읽기 위한 것이다.

어디부터 읽나#

알고 싶은 것 문서
5종이 무엇이 같고 무엇이 다른가 이 페이지의 모델 목록 · 공통 아키텍처
값이 어떤 공간에서 어떤 단위로 흐르는가 데이터 공간
정확도를 무엇으로 재는가 Metric
학습 데이터가 어떻게 만들어지는가 전처리
값을 어떤 O(1) 공간으로 보내 학습하는가 nondim
왜 이 설계인가 (대안과 근거) 설계 근거
데이터 재생성·재학습 절차 운영 절차
학습이 이상할 때 장비·런타임 이슈

용어#

문서 전체가 아래 표기를 따른다. 코드 식별자는 등폭체(nondim), 개념어는 일반 서체다.

모델 이름#

repo 이름을 그대로 쓰고 줄이지 않는다. 이름은 aicfd-<표현>-<대상>-<target> 순서다. 세 자리 중 하나라도 빼면 이름이 겹친다. 비강을 다루는 repo 가 aicfd-grid-nasal-rgb 과 aicfd-mesh-nasal-physical 둘이라서, 대상만으로는 어느 쪽인지 알 수 없다.

대상이 target 보다 앞에 온다. 그래서 이름순으로 늘어놓으면 같은 대상을 다루는 repo 가 붙어 나온다. aicfd-grid-building-physical 바로 다음이 aicfd-grid-building-rgb 이고, 그 뒤에 aicfd-grid-indoor-physical 이 온다.

repo 대상 예측 대상
aicfd-grid-building-physical 건물 외부 유동 물리량 단면
aicfd-grid-indoor-physical 사무실 1개 실내 물리량 단면
aicfd-grid-building-rgb 건물 외부 유동 렌더 이미지 단면
aicfd-grid-nasal-rgb 비강 유동 렌더 이미지 단면
aicfd-mesh-nasal-physical 비강 표면 vertex 별 표면 flux (현재 WSS)

두 개의 분류 축#

축 값 뜻 해당 모델
표현 grid 필드를 정규 격자 이미지 (b,c,h,w) 로 다룬다 aicfd-grid-building-physical, aicfd-grid-indoor-physical, aicfd-grid-building-rgb, aicfd-grid-nasal-rgb
mesh 삼각형 표면 mesh 의 vertex 위에서 직접 다룬다 aicfd-mesh-nasal-physical
target 물리량 예측 대상이 실제 물리 단위 값(m/s, Pa, °C, WSS) aicfd-grid-building-physical, aicfd-grid-indoor-physical, aicfd-mesh-nasal-physical
렌더 이미지 예측 대상이 이미 색으로 렌더된 이미지 바이트 aicfd-grid-building-rgb, aicfd-grid-nasal-rgb

두 축의 교집합을 부를 때는 물리량-grid(aicfd-grid-building-physical, aicfd-grid-indoor-physical), 물리량-mesh(aicfd-mesh-nasal-physical), 렌더-grid(aicfd-grid-building-rgb, aicfd-grid-nasal-rgb) 로 쓴다.

'physical' 이라는 단어의 세 가지 쓰임

  • repo 이름의 셋째 자리 physical
  • 텐서 공간 이름 physical — dequantization 을 거친 물리 단위 텐서 (데이터 공간)
  • 위 표의 target 축 값 "물리량"

등폭체 physical 은 셋 중 공간을 가리킨다.

그 밖의 용어#

개념어 뜻 코드에서의 이름
예측 필드 pressure, velocity, temperature, k, WSS 같은 예측 대상 물리량 mode, pred_fields
정답(truth) 데이터셋이 제공하는 참값 truth, mesh 에서는 sample dict 의 target
contour 한 z 단면의 필드 값 격자 contour, ContourDict
fluid 화소/vertex 유체 영역에 속해 metric 에 포함되는 격자점 domain_mask != 0, mesh 는 전 vertex

모델 목록#

aicfd-grid-building-physical aicfd-grid-indoor-physical aicfd-grid-building-rgb aicfd-grid-nasal-rgb aicfd-mesh-nasal-physical
표현 grid grid grid grid mesh
target 물리량 물리량 렌더 이미지 렌더 이미지 물리량(WSS)
예측 필드 pressure, velocity, k velocity, temperature pressure, velocity pressure, velocity wss (표면 flux 중 현재 구현분)
지원 모델군 Reg, AE, EDM(2D/3D, Physical/Latent), EDMwithReg Reg Reg, Reg3D, AE, EDM(2D/3D, Pixel/Latent), EDMwithReg Reg 2D·3D DiffusionNet Reg
학습 공간 nondim nondim [-1,1] [-1,1] nondim
metric 단위 물리 단위 물리 단위 [0,255] 화소값 [0,255] 화소값 WSS 단위
Collator 있음 있음 있음 있음 없음
조건 입력 풍속·높이 센서 5점 온도·높이 풍속·높이 풍속·높이 유속·단면적
상태 운용 Reg 만 운용, AE/EDM 미배선 운용 운용 운용

aicfd-grid-building-rgb 과 aicfd-grid-nasal-rgb 의 AE/EDM 코드는 뿌리가 같고, aicfd-grid-nasal-rgb 은 거기서 회귀만 남긴 부분집합이다. aicfd-grid-indoor-physical 은 AE/EDM/3D 코드가 들어 있기는 하지만 실내 데이터에 맞게 배선되어 있지 않다.

표에 없는 차이는 대부분 도메인에서 온다. 격자 크기, 조건 입력 구성, 데이터 규모 같은 것들이다. 구조를 이해하는 데 필요한 차이는 위 표와 데이터 공간, 모델별 차이 에 모아 두었으니 그쪽을 보면 된다.


공통 아키텍처#

여기서부터는 다섯 모델이 함께 쓰는 골격이다. 각 repo 가 따로 배포되기 때문에 이 골격은 공용 패키지가 아니라 repo 마다 들어 있는 복사본이다. 그래서 한쪽을 고치면 나머지에도 같은 수정을 옮겨야 한다 (패키지 디렉터리는 다섯 repo 모두 aicfd/ 다).

학습 루프#

Workspace(fold 단위) → Trainer(epoch 단위) → finalize_fold

scripts/train.py 가 fold 를 순회하고, 각 fold 안에서 epoch 을 돌리며 검증 지표가 개선되지 않으면 조기 종료한다 (patience 초과 시 최고 성능 checkpoint 로 복원). fold 종료 시 post-hoc EMA 후보를 골라 test 를 돌리고 결과 csv 를 쓴다.

데이터 적재#

구성요소 단위 역할
Dataset (utils/dataset.py) 샘플 1개 h5 읽기, 윈도우/케이스 샘플링
Collator (utils/collator.py) 배치 1개 스택, crop 또는 pad, 증강, 공간 변환, 조건 입력 정규화

Collator 는 CPU 에서 float32 로만 동작하고, GPU 이동은 to_device 가 일괄 처리한다. Dataset 변종은 단일 슬라이스(DatasetImage), 깊이 윈도우(DatasetDepthSeq), 케이스 전체(DatasetDepthCase) 세 가지다.

aicfd-mesh-nasal-physical 은 Collator 가 없다. mesh 는 vertex 수가 가변이고 희소 연산자를 함께 들고 다녀 하나의 밀집 텐서로 묶을 수 없다. Dataset 이 기하 연산자 사전계산과 target 정규화까지 마친 샘플 dict 를 내보내고, 배치는 collate_fn=list 로 만든 파이썬 리스트다.

모델 기반 클래스#

models/base_model.py 의 BaseModel 이 train/val/eval 루프, 온라인 가중치와 EMA 가중치를 바꿔 끼우는 컨텍스트 매니저, checkpoint 생성·적재를 담당한다. 확산 계열은 BaseModelPostHocEMA 를 써서 여러 EMA 표준편차 후보를 사후 재구성해 최적을 고른다. 회귀 계열은 일반 SWA 형태의 EMA 를 쓴다.

Metric 컨테이너#

Metric 은 {예측 필드: {지표 이름: [값]}} 구조의 dict 서브클래스로, 배치 누적(.sum)·가중(.mul)·이름 병합(.extend) 만 제공한다. 지표의 정의와 해석은 Metric 문서 에 있다.

공유 범위#

파일 다섯 모델에서
utils/prefetch.py, utils/train_helper.py 동일
utils/scheduler.py, utils/ema.py grid 4종은 scheduler 가 동일하고, 그중 aicfd-grid-nasal-rgb 을 뺀 3종은 ema 까지 동일하다. aicfd-mesh-nasal-physical 은 scheduler 를 WarmupConst 로 축약하고, aicfd-grid-nasal-rgb 은 ema 의 eval_index 를 nn.Buffer 로 보관한다
utils/metric.py, utils/model_helper.py, models/base_model.py 절반만 공통. 나머지는 표현과 정규화 방식에서 달라진다

check_drift.sh 가 위 구분대로 검사한다.


데이터 공간#

디스크에 저장된 값이 그대로 신경망에 들어가지는 않는다. 어느 모델이든 저장값을 크기가 인 학습 공간으로 한 번 옮기고, 평가할 때 그 변환을 그대로 되돌린다. 변환은 전부 invertible affine 이라 되돌리는 데 오차가 없다. 변환의 모양은 다섯 모델이 다르지 않다. 그 affine 을 어떻게 정하느냐만 다르다.

공간 목록#

공간 뜻 배치 형상 쓰는 모델
uint16 h5 에 저장된 양자화 값 (b, …, c) 채널 마지막 전부
physical dequantization 을 거친 물리 단위 텐서 (b, c, …) 채널 앞 물리량-grid
nondim 학습·예측이 일어나는 정규화 공간 (b, c, …) / mesh 는 (N,) 물리량 3종
[-1,1] 화소 학습·예측이 일어나는 정규화 공간 (b, c, h, w) 렌더-grid
edm 확산 학습용 rescale 공간 (b, c, …) aicfd-grid-building-physical, aicfd-grid-building-rgb
latent → ldm AE 인코더 출력과 그 정규화 (b, c_lat, h', w') aicfd-grid-building-physical, aicfd-grid-building-rgb

dequantization (grid 공통)#

grid 모델은 저장값이 uint16 이라, 무엇을 하든 먼저 물리 단위로 풀어 놓는다.

amin/amax 는 필드별 저장 범위이고 cfg.dataset.maxima 로 넘어온다. velocity 는 풀고 나서 u/v/w 3채널로 나뉜다. 렌더-grid 는 저장값이 이미 이미지 바이트라 이 단계가 아예 없다.

nondim#

물리 단위로 풀어 놓은 값(렌더-grid 라면 저장 바이트)을 다시 신경망이 입력으로 받는 공간으로 옮긴다. 이것도 invertible affine 이다. 모델마다 다른 건 계수를 어떻게 잡느냐뿐이다.

모델 계수 결정 목표
aicfd-grid-building-rgb, aicfd-grid-nasal-rgb 고정 상수 값이
aicfd-grid-building-physical 기준 풍속별 표본 통계 표준편차 1
aicfd-grid-indoor-physical 채널별 전역 통계 + 기준온도 차감 표준편차 1
aicfd-mesh-nasal-physical 물리 유도 지수 + 해석 설정값에서 온 상수 면적가중 중앙값 32

식과 유도, 통계를 어디서 구하는지는 nondim 에 모아 두었다.

nondim 이라는 이름

nondim 은 특정 수식의 이름이 아니다. 물리량 3종이 각자의 학습 공간을 부를 때 쓰는 공통 인터페이스 이름에 가깝다. 세 모델 모두 physical_to_nondim / nondim_to_physical 을 갖고 있고, 안에 든 식만 서로 다르다. 렌더-grid 2종은 같은 일을 상수 한 줄로 끝내기 때문에 따로 이름을 두지 않았다.

조건 입력의 단위#

physical_info 는 같은 배열이라도 자리에 따라 단위가 다르다. 물리량-grid 에서 정규화 계수를 계산할 때 이 구분을 놓치면 계수가 틀어진다.

위치 단위
h5 원본, Dataset·Collator 의 raw_pinfos 원값 (풍속 26.0, 높이 mm, 센서 온도 °C)
모델 입력 batch_dict['input']['physical_info'] 정규화값 (building 은 풍속÷100·높이÷최대값, indoor 는 raw / PINFO_DIVISORS)
AE 의 최상위 batch_dict['physical_info'] 원값

모델은 정규화값에서 원값을 복원해 계수를 만든다 — building 은 풍속에 100 을 곱하고, indoor 는 PINFO_DIVISORS 를 되곱한다. _batch_pinfos() 가 위치 차이를 흡수한다. 렌더-grid 는 조건 입력을 정규화 계수에 쓰지 않으므로 이 구분이 필요 없다.


모델별 차이#

공통 구조는 위에서 다뤘으니, 여기서는 모델마다 다른 부분만 본다. 먼저 다섯을 나란히 놓고 무엇이 들어가서 무엇이 나오는지부터 확정하고, 그 다음에 모델별로 설명이 필요한 것만 따로 본다.

입출력 사양#

2D 회귀 기본 설정(reg-*-base, diffusionnet-base) 기준이다. 신경망이 실제로 받는 것은 기하 입력과 scalar 조건 둘뿐이고, 정답 필드는 어느 경로로도 입력에 들어가지 않는다 (예외는 깊이 경로).

aicfd-grid-building-physical aicfd-grid-indoor-physical aicfd-grid-building-rgb aicfd-grid-nasal-rgb aicfd-mesh-nasal-physical
기하 입력 3채널 — domain_mask 1 + proximity_mask 2. 전부 255 로 나눈다 vertex 좌표와 삼각형, 그리고 거기서 만든 기하 연산자
기하 입력의 형상 576² crop 256² pad (도메인 216×153) 576² 또는 256² crop 256² crop vertex 가변
scalar 조건 풍속, 슬라이스 높이 슬라이스 높이, 센서 5점 온도 T_S1..T_S5 풍속, 슬라이스 높이 유속, 슬라이스 높이 유속 라벨, vertex 별 기도 단면적
조건 개수·단위 2 — m/s, mm 6 — mm, °C ×5 2 — m/s, mm 2 — 라벨, mm 3 — 라벨(부호·크기 따로), cm²
조건 정규화 풍속÷100, 높이÷최대 높이 raw ÷ PINFO_DIVISORS = [2800, 40, 40, 40, 40, 40] 풍속÷100, 높이÷최대 높이 유속÷100, 높이÷최대 높이 라벨÷12, 단면적은 m² → cm² 환산만
조건을 넣는 방식 조건마다 ImgFiLM 하나씩(2개), 각각 기하 입력을 변조한 뒤 이어붙인다 → 6채널 조건마다 ImgFiLM 하나씩(6개), 변조 결과를 더한 뒤 원본 기하 입력을 이어붙인다 → 6채널 aicfd-grid-building-physical 과 동일 (2개 변조 후 이어붙이기) NasalFiLM 이 조건 3개를 MLP 에 통과시켜 DiffusionNet block 마다 를 만든다. vertex 마다 값이 다르다
예측 채널 5 — pressure 1, velocity 3, k 1 4 — velocity 3, temperature 1 6 — pressure 3, velocity 3 (렌더 RGB) 6 — pressure 3, velocity 3 (렌더 RGB) 1 — vertex 당 WSS 스칼라
예측이 나오는 공간 nondim nondim [-1,1] [-1,1] nondim
출력 머리 pressure 와 velocity·k 두 갈래로 나눠 디코딩하고 MMTM 으로 섞는다 velocity 와 temperature 두 갈래 단일 머리 6채널 단일 머리 1채널

proximity_mask 는 2D 슬라이스에 z 방향 맥락을 주는 채널이다. 각 화소에서 형상이 있는 가장 가까운 아래 슬라이스와 위 슬라이스까지의 거리를 재고 그 역수를 넣는다. 그래서 두 채널이다. 한 장짜리 단면만 보고는 알 수 없는 "이 화소가 건물 바로 위인가, 한참 위인가" 를 이것이 알려준다.

정답 필드는 왜 input_fields 에 들어 있나

설정의 input_fields 는 신경망 입력 목록이 아니라 h5 에서 읽어올 키 목록이다. pressure·velocity 같은 이름이 거기 있는 것은 정답으로 쓰기 위해 읽는다는 뜻이고, Collator 가 그것들을 truth 로 갈라낸다. 신경망의 forward(mask, physical_info) 는 기하 입력과 조건만 받는다.

깊이 경로 (3D)#

3D 회귀는 위 표와 두 군데가 다르다. 슬라이스를 장씩 묶어 로 다룬다.

2D 3D
기하 입력 domain_mask + proximity_mask = 3채널 domain_mask 1채널만. 깊이 축이 실제로 있으니 z 맥락을 대신 넣어 줄 이유가 없다
추가 입력 없음 context — 정답 슬라이스 일부를 그대로 넣는다. depth_mask 가 1 인 자리만 남기고 나머지는 0
푸는 문제 조건에서 단면 한 장을 만든다 주어진 슬라이스 사이를 메운다
metric 대상 fluid 화소 전부 depth_mask 가 0 인 슬라이스만. 주어진 슬라이스는 예측한 것이 아니라 제외한다

그래서 3D 회귀는 조건 생성기가 아니라 깊이 방향 보간기다. 학습할 때는 몇 장을 줄지 (depth_mask) 샘플마다 새로 정하고, 평가할 때는 창을 옮겨 가며 케이스 전체를 복원한다.

aicfd-grid-building-physical#

가장 많은 모델군을 갖는다. contour 는 공간을 함께 들고 다니는 ContourDict 로 다루고, physical ↔ nondim ↔ {edm, ldm} 을 샘플별 계수로 왕복한다.

모델군 학습 공간 loss
FBFRegDist, FBF2RegDist, FBF*RegNoProx nondim 필드별 L1
AutoencoderContour(VQ) nondim 재구성
AutoencoderDomain(VQ/Prox) 도메인 마스크 [-1,1] 재구성
PhysicalEDM, PhysicalEDM3D edm 확산 MSE
LatentEDM, LatentEDM3D ldm 확산 MSE
EDMwithReg, LDMwithReg 위와 동일 + 회귀 예측을 조건으로 융합

Latent 계열은 AE 두 개(도메인·contour)를 불러 nondim → latent → ldm 으로 인코딩하고, 평가 때만 디코더를 쓴다. 3D 모델은 contour 에 깊이 축을 더한 (b,c,D,h,w) 를 다룬다 — 깊이 경로 참고.

fluid 영역은 obj 단면을 z 슬라이스마다 래스터화해 만든다. 이 방법이 성립하는 이유는 외부 유동이기 때문이다 — 해석 영역이 풍동 상자이고 건물이 그 안의 장애물이라 "solid = obj 내부"가 유체의 여집합과 정확히 일치한다.

aicfd-grid-indoor-physical#

단일 사무실 형상이라 모델·각도 축이 없다. 예측 필드는 velocity 와 temperature 이며 온도는 섭씨로 저장한다.

항목 값
형상 data/geo/260602_officeroom_r0_S.obj 하나. 모든 케이스가 같은 bounding box
케이스 21개
슬라이스 케이스당 13장, z = 0.2 ~ 2.6 m (0.2 m 간격)
격자 216 × 153 화소, 화소당 0.02 m
fold aicfd/folds/21.yaml — 5-fold, 검증 2 고정, 학습 16~17 / 테스트 2~3
모델 설정 reg-256-base(FBFUNet2), reg-256-fbfunet1(FBFUNet1)

좌표계는 해석 결과의 로컬 프레임(최소 코너가 원점)을 따르고, 전처리가 obj 를 그 프레임으로 평행이동한다. z 는 양쪽이 같은 기준이라 그대로 쓴다. gt_max_height.txt 에 기록되는 216 은 contour 의 최대 변 길이(화소)이며, pad 캔버스 크기는 이 값보다 커야 한다.

fluid 영역은 obj 그룹별 단면 래스터와 CFD 점군 bounding box 의 교집합이다. 설계 근거는 fluid 영역 정의 참고.

경계 화소의 온도는 물리값이 아니다

마스크가 전처리(절대 온도 공간)와 Collator(ΔT 공간) 두 곳에서 곱해지고 두 공간의 원점이 다르다. 그래서 반투명 경계 화소를 inverse transform 한 값은 CFD 값도 저장값도 아니다. 예측과 정답이 같은 경로를 지나므로 metric 과 디지털 트윈은 서로 일관되지만, 그 화소 하나의 값 자체는 물리 온도가 아니다. velocity 는 bias 가 0 이고 벽면 값도 0 이라 영향이 없다.

온도 단위는 원본 .ip 만 켈빈이고, 전처리에서 섭씨로 바꾼 뒤로는 저장·metric·출력이 모두 섭씨다. nondim 공간은 온도차라서 켈빈과 섭씨가 같다.

aicfd-grid-building-rgb#

aicfd-grid-building-physical 과 같은 모델군을 갖되 target 이 렌더 이미지다. 물리 단위 공간이 없고 정규화가 상수이므로 ContourDict 도 샘플별 계수도 없다. 정규화 상태를 갖는 것은 EDM/LDM 뿐이고 그 계수는 checkpoint 에 저장된다.

EDMCollator 의 기본값 scale=2/255, bias=-1 은 추가 정규화가 필요 없는 경로(LDM, 3D 회귀)를 위한 것이라 결과가 그냥 [-1,1] 이다. 도메인 밖은 흰색(1)으로 채운다.

aicfd-grid-nasal-rgb#

aicfd-grid-building-rgb 에서 회귀 2D·3D 만 남긴 부분집합이다. 확산도 AE 도 없고 정규화 상태 자체가 없다 (3D 는 깊이 경로). 가장 작은 grid 모델이라 공통 코드 변경을 먼저 확인해 보기 좋다.

aicfd-mesh-nasal-physical#

유일한 mesh 모델이다. vertex 수가 가변인 삼각형 표면을 그대로 받아 vertex 별 스칼라 WSS 를 예측한다. 격자도, 화소 공간도, Collator 도, 확산·AE 도 없다.

h5 (verts/faces, 면 중심 WSS, 면적, 유속, cavity 부피, vertex 별 단면적)
  → normalize_positions → get_operators (Laplacian, 질량, eigenpair, gradient operator)   [디스크 캐시]
  → physical_to_nondim → 면 값을 면적가중으로 vertex 에 뿌림
  → net(features, 기하 연산자, FiLM(유속, 단면적)) → vertex 별 예측
  → nondim_to_physical → metric (WSS 단위)

평가 결과는 vertex 별 WSS 스칼라와 turbo 색을 담은 이진 PLY 로, 예측과 정답을 같은 색 범위로 나란히 저장한다.

표면 flux 확장#

이 모델의 대상은 벽면 flux 장 전반이고 WSS 는 그중 첫 필드다. 수증기 질량 flux 와 벽면 열 flux 가 추가될 예정이다. 현재 구현은 단일 필드로 고정돼 있어 확장 시 다음이 바뀐다.

지점 현재
h5 키 wss 하나를 직접 읽는다 (utils/dataset.py)
정규화 physical_to_nondim 이 WSS 전용 지수와 기준값을 쓴다
출력 신경망 C_out=1, vertex 당 스칼라 하나
결과 저장 PLY vertex 속성 이름이 wss 로 고정 (utils/model_helper.py)
metric Metric 은 필드 이름을 dict 키로 받으므로 구조 변경은 필요 없다

세 필드는 모두 벽면 법선 방향 기울기에 수송계수를 곱한 flux 이고, 면적분이 면 상수값 × 면적으로 정리된다. 구조가 같아서 면적가중 기준값과 면적가중 metric 이 그대로 적용된다.

필드 벽면 flux 면적분
WSS
벽면 열 flux
수증기 질량 flux

기준값의 유속 의존성만 필드별로 다르다 — WSS 의 지수는 WSS nondimensionalization 참고.


파일 지도#

grid 4종 공통 경로다. aicfd-mesh-nasal-physical 은 collator.py·edm_helper.py 가 없다.

경로 내용
scripts/train.py fold·epoch 루프, 조기 종료, fold 마무리
models/base_model.py 공통 학습·검증·평가 루프, EMA 교체, checkpoint
models/regression.py, models/reg_3d.py 결정론적 회귀
models/{autoencoder,edm,edm_3d,edm_with_reg}.py AE·확산 (building 2종만)
models/diffusionnet.py, models/modules/backbone.py DiffusionNet 회귀 (aicfd-mesh-nasal-physical)
utils/dataset.py h5 읽기, 샘플링, mesh 는 기하 연산자·정규화까지
utils/collator.py 배치 구성, 공간 변환, 조건 입력 정규화
utils/metric.py Metric, compute_submetric, ContourDict
utils/edm_helper.py 정규화 계수 계산, EDM 전처리 모듈
scripts/preprocess/ 원본 해석 결과 → h5 (전처리)