전처리 파이프라인#
CFD 해석 결과를 학습용 h5 로 바꾸는 과정이다. grid 모델과 mesh 모델은 거치는 단계가 달라서 따로 본다.
grid 모델#
해석 결과 점군(.ip) → RBF 보간 → 정규 격자 fp32 → O(1) clip → uint16 양자화 → h5
| 단계 | 코드 | 하는 일 |
|---|---|---|
| 보간 | scripts/preprocess/rbfi.py, pp_utils.py |
점군을 격자로 옮긴다. 국소 RBF, 이웃 150개. 좌표는 격자 단위로 넘긴다 (이유) |
| 마스크 | section_utils.py 또는 preprocess.py |
fluid 영역을 정한다 |
| clip | compressor.py |
물리 원리 기준 크기를 기준으로 임계값을 정해 극단값만 자른다 |
| 양자화 | compressor.py |
필드별 고정 범위로 uint16 변환 |
pixel space 모델(aicfd-grid-building-rgb, aicfd-grid-nasal-rgb)은 이 경로를 타지 않는다. 해석 결과를 이미지로 렌더해서 바이트를 그대로 저장하기 때문에 clip 도, 양자화 범위를 정하는 일도 없다.
보간 안전장치#
국소 RBF 는 도메인 경계에서 물리적으로 말이 안 되는 overshoot 를 만들 수 있다. 보간 단계가 이걸 직접 잡는데, 필드마다 따로 판정해서 따로 되돌린다.
| 필드 | 판정 | 되돌림 |
|---|---|---|
| velocity | 예측 벡터 크기가 이웃 벡터 크기 최댓값의 2배 초과 | velocity 채널만 최근접값 |
| 스칼라(pressure, k, temperature) | 예측 절댓값이 이웃 절댓값 최댓값의 2배 초과 | 해당 채널만 최근접값 |
| NaN·∞ | 값이 NaN 이거나 ∞ | 행 전체 최근접값 |
여기서 중요한 것은 판정을 필드마다 따로, 그리고 값의 크기로 한다는 점이다. 한 필드에서 이상값이 나와도 다른 필드는 그대로 두고, 값이 어느 구간에 있는지가 아니라 절댓값이나 벡터 크기가 얼마인지를 본다. 왜 그렇게 했는지는 보간 안전장치 설계 에 있다.
마지막에 NaN·∞ 검사를 한 번 더 하기 때문에, 깨진 h5 가 완료로 기록되는 일은 없다.
O(1) clip#
보간 안전장치를 지나고도 남아 있는 극단값은 대부분 실제로 일어난 현상이다 — 고압부, 고전단, 제트. 그래서 자르면 그만큼 정보가 없어진다. 그래도 임계값을 두는 이유는, 그런 값이 몇 개만 있어도 L2 계열 지표와 정규화에 쓰는 표준편차가 크게 달라지기 때문이다. 임계값을 어디에 두느냐는 두 모델이 다르다.
aicfd-grid-building-physical — 양자화 범위가 곧 임계값
이 모델은 데이터가 커서 uint16 양자화 가 선택 사항이 아니다. 양자화는 저장 범위를 정해야 하고, 범위를 정하면 그 바깥은 자를 수밖에 없다. 그래서 여기서 clip 은 별도의 정제 단계가 아니라 양자화의 일부이고, 임계값과 저장 범위가 같은 숫자다.
범위는 필드별 최소·최대와 평균, quantile 을 놓고 사람이 정한다. velocity 는 성분별 ±150 m/s, pressure 는 −12500 ~ 3500 Pa, k 는 0 ~ 1500 m²/s² 다. 최대 풍속이 44 m/s 이므로 velocity 임계값은 그 3.4배, pressure 임계값은 같은 풍속의 동압(1186 Pa)의 열 배쯤 된다. 저장 범위를 넉넉히 잡아 두고 그 바깥으로 나가는 값만 걸러낸다.
aicfd-grid-indoor-physical — 데이터 분포에서 뽑은 임계값
풍속이 조건 입력이 아니므로 절대값을 기준으로 자른다. 임계값도 사람이 눈대중으로 정한 숫자가 아니라 데이터 분포에서 계산한다. 규칙은 두 필드 공통으로 이다. 판정값의 p99.9 를 구한 다음 거기에 1.6 을 곱한 값을 임계값으로 쓴다. 아래 표의 숫자들은 이 규칙으로 한 번 정해서 코드에 고정해 둔 값이고, 실제 전처리는 샘플마다 이 고정 임계값과 비교한다. 이쪽은 잘려 나가는 값이 훨씬 적다.
| 필드 | 판정값 | clip | 상수 |
|---|---|---|---|
| velocity | 성분 절댓값 | m/s (0.0110% 화소) | |
| temperature | °C (0.0335% 화소) |
제대로 돌아가고 있다면 잘려 나가는 화소는 필드당 0.01~0.05% 다. 이보다 많이 잘린다면 임계값이 잘못된 것이 아니라 보간 단계에서 아티팩트가 남았다고 보고 그쪽을 확인한다.
clip 은 압축 직전에 샘플 단위로 판정하고, 저장되는 값은 여전히 물리 단위다. velocity 는 성분별로 자른다. 벡터 크기를 잘라 버리면 방향까지 같이 바뀌기 때문이다.
mesh 모델은 어느 단계에서도 자르지 않는다. 저장된 WSS 도 학습 target 도 해석 결과 그대로다. 분포의 꼬리를 확인해 보니 잘라낼 만한 뾰족한 값이 없었기 때문이다 (WSS nondimensionalization — 꼬리를 자르지 않는 이유).
양자화 범위#
clip 을 지난 값은 uint16 으로 양자화해 저장한다. 이때 필드마다 저장 범위를 하나씩 정해 두는데, 이 범위가 나중에 그대로 dequantization 계수가 된다.
| 모델 | 필드 | 범위 |
|---|---|---|
| aicfd-grid-indoor-physical | velocity | m/s |
| temperature | °C | |
| aicfd-grid-building-physical | velocity | m/s |
| pressure | Pa | |
| k |
이 범위가 있어야 uint16 저장값을 물리값으로 되돌릴 수 있다. 같은 uint16 값이라도 범위가 다르면 다른 물리값으로 읽힌다. 그래서 범위는 데이터셋, 그리고 그 데이터로 학습한 checkpoint 와 늘 같이 다녀야 하고, 학습에 쓴 범위는 각 run 의 얼려둔 설정에 남아 있다.
aicfd-grid-building-physical 의 범위는 보간 안전장치가 없던 시절 기준이라 실제 분포보다 넉넉하다. 범위를 좁히면 uint16 한 단계가 나타내는 물리값 간격이 작아져서 양자화 오차가 줄어든다. 다만 범위를 바꾸면 재학습까지 따라오므로, 절차는 운영 절차 를 따른다.
fluid 영역 정의#
fluid 영역을 정하는 방식은 모델마다 다르다. 형상과 해석 영역의 포함 관계가 서로 반대라서 그렇다. building 은 해석 영역인 상자 안에 건물이 장애물로 들어 있고, indoor 는 반대로 벽이 해석 영역을 바깥에서 둘러싼다.
| 모델 | 정의 | 포함 관계 |
|---|---|---|
| aicfd-grid-building-physical, aicfd-grid-building-rgb | domain_mask = 255 − geo_mask, geo_mask 는 z 슬라이스별 obj 단면 래스터 |
obj ⊂ 해석 영역 (풍동 상자 안의 장애물) |
| aicfd-grid-indoor-physical | obj 그룹별 단면 래스터의 여집합 ∩ CFD 점군 bounding box | obj ⊃ 해석 영역 (벽 두께를 가진 방 형상이 유체를 감싼다) |
indoor 는 단면 면적이 ROOM_FACE_AREA(3.0 m²)를 넘는 면을 방의 벽으로 보고 제외한다. 가구는 커야 1.3 m² 이고
방 단면은 12.03 m² 다. 두 값이 열 배 가까이 떨어져 있어서 그 사이 어디에 임계값을 두어도 결과가 같다.
설계 근거와 검토된 대안은 fluid 영역 정의 참고.
fluid 판정 기준#
grid 모델에서 fluid 판정은 domain_mask != 0 이다. 반투명(안티에일리어싱) 경계 화소도 해석 영역에 포함된다.
metric 의 dom_flag 정의(.bool())와 같은 기준이다.
예외는 nondim 통계 집계로, 일부 스크립트가 ==255 를 쓴다. 두 기준의 통계 차이는 무시할 수준이다 —
안티에일리어싱은 값을 0 쪽으로만 당기므로 상단 꼬리를 건드리지 않는다.
안티에일리어싱#
두 building 계열과 indoor 계열 모두 마스크가 안티에일리어싱돼 있다. 다만 슬라이스 하나만 보고 판단하면 안 된다.
렌더러가 축정렬 경계를 화소에 딱 맞춰 스냅하기 때문이다. 그래서 형상이 없는 z 는 값이 한 종류뿐이고, 축정렬 벽만 있는 z 는 0 과 255 두 종류만 나온다. 중간값은 사선이나 곡면이 있는 z 에서만 생긴다.
경계 화소의 값은 커버리지만큼 감쇠돼 있어 물리값이 아니다. 자세한 결과는 모델별 차이 — aicfd-grid-indoor-physical 의 경고를 참고.
mesh 모델#
aicfd-mesh-nasal-physical 은 격자화 단계가 없다. 해석 결과의 삼각형 표면 mesh 와 면 중심 WSS 를 그대로 h5 에 담고,
전처리가 벽면 mesh 만으로 cavity 부피를 계산해 volume 으로 함께 저장한다(divergence theorem, 경계 루프 캡).
담기 전에 좌표가 같은 노드를 합친다. CGNS 는 표면을 한 덩어리로 내보내면서도 패치 이음매의 노드를 두 번 적어 두는데(케이스당 1.7% 남짓), 그대로 쓰면 벽면이 위상적으로 여러 조각이 된다. 그러면 Laplacian 이 조각마다 끊겨 확산이 이음매를 넘지 못하고, 부피 계산도 이음매를 개구부로 보고 캡을 씌운다. 합치고 나면 전 케이스가 한 덩어리가 되고 경계 루프는 콧구멍과 인두만 남는다(3~4개). 면 데이터는 손대지 않으므로 WSS·면적은 그대로다.
vertex 마다의 단면적#
WSS 는 국소 유속이 정하고, 국소 유속은 유량을 통로 단면적으로 나눈 값이다. 그런데 단면적은 한 점에서 볼 수 있는 값이 아니다. 벽 건너편까지 봐야 나오기 때문에, 벽면을 따라 확산하는 net 은 이 값을 스스로 만들지 못한다. 그래서 전처리가 미리 계산해 vertex 마다 스칼라 하나씩 붙여 준다.
문제는 어느 면으로 자르느냐다. 유량을 재려면 자르는 면이 유속을 가로질러야 하는데, 비강은 콧구멍에서 위로 올라갔다가 비인두에서 뒤로 눕고 다시 아래로 꺾인다. 콧구멍과 하인두의 평균 유속 방향이 157도 벌어져 있어서, 축 하나를 고정해 놓고 자르면 한쪽에서 맞을 때 다른 쪽에서 어긋난다.
중심선을 긋고 그 접선에 수직인 평면으로 자르는 방법도 통하지 않는다. 그 평면이 유속에 수직인 곳은 중심선이 지나는 한 점뿐이고, 통로가 휘면 그 점에서 멀어질수록 면이 유속과 비스듬해진다. 비스듬하게 자른 단면은 실제보다 넓게 나온다. 를 통로 진행방향과 절단면 법선 사이의 각이라 하면
이므로, 면을 길이방향에 가깝게 눕힐수록 값이 커진다.
그래서 평면으로 자르는 대신 cavity 안에서 라플라스 방정식을 푼다.
콧구멍에서 를 0, 인두에서 1 로 두고, 벽으로는 유량이 빠져나가지 않게 한다. 퍼텐셜 유동에서 속도는 이고 등위면 는 정의상 에 수직이므로, 등위면은 한 점이 아니라 면 위 모든 점에서 유속을 가로지른다. 평면에서 생기던 각도 오차가 아예 생기지 않는다.
cavity 는 벽면만으로 만든다. 벽면에 뚫린 곳이 콧구멍 둘과 인두 하나뿐이라, 셋을 centroid fan 으로 막으면 닫힌 표면이 되고 그 안쪽을 채우면 공기가 차지하는 영역이 나온다.
넓이는 복셀 수를 세서 구하지 않는다. 격자에 비스듬히 누운 단면은 실제 넓이보다 복셀이 많이 걸리기 때문이다. 대신 얇은 구간에서 성립하는 coarea 관계를 쓴다.
구간 폭으로 나누면 그 구간에 든 단면의 넓이가 나온다. 식에 격자 방향이 들어가지 않으므로 단면이 어떻게 누워 있든 같은 기준으로 재진다.
한 등위면이 통로 하나만 지나는 것은 아니다. 같은 에 좌비강과 우비강이 함께 걸리고, 갑개가 가른 비도들과 그 자리에 열린 부비동 입구도 같이 걸린다. 이 조각들의 넓이를 더하면 어느 통로의 단면적도 아닌 값이 되므로, 조각마다 따로 재서 그 조각에 닿는 vertex 에만 준다.
조각은 등위면 자신의 연결성으로 가른다. 구간으로 가르면 안 되는데, 구간에는 두께가 있어서 한 단면이 그 안에서 끊기면 여러 조각으로 쪼개지기 때문이다. 그래서 같은 조각에 닿은 vertex 들은 같은 값을 받고, 같은 라도 비중격 건너편은 다른 조각이라 다른 값을 받는다.
나머지는 학습할 때 Dataset 이 처리한다.
- 좌표 정규화
- 기하 연산자 계산 — 라플라시안, vertex 질량, eigenpair, gradient 연산자 (
(verts, faces)해시 키로 디스크 캐시) - HKS 입력 특징 계산 (디스크 캐시)
physical_to_nondim으로 면 WSS 를 학습 공간으로 — 물리 유도 지수 둘과 해석 설정값에서 나온 크기 계수를 쓴다 (nondim)- 면적가중으로 vertex 에 투영 (꼬리는 자르지 않는다). 물리 단위 WSS 도 같은 방식으로 투영해
target_wss로 함께 들고 간다