Skip to content
cfd-lab:~/ko/posts/2026-08-03-immersed-boun…● online
NOTE #121DAY MON CFD기법DATE 2026.08.03UPDATED 2026.10.19READ 7 min read#IBM#Direct-Forcing#Delta-Kernel#Lagrangian-Marker#Incompressible

격자가 모르는 벽 — IBM 델타 커널과 multi-direct forcing

마커 위 슬립만 보면 간격을 키우게 된다. Δs/h가 3이면 사이 누수가 0.287이고, 0.25면 조건수가 10^11로 오른다.

벽은 격자 위에 없다. 그런데 유동은 벽을 느낀다. Immersed boundary method(IBM·물체를 격자에 맞추지 않고 강제항으로 표현하는 기법)는 이 모순을 운동량 방정식의 소스항 하나로 처리한다. 격자는 직교 상태 그대로 두고, 물체는 그 위에 떠 있는 마커 점들의 집합으로만 존재한다. 오늘은 그 강제항이 실제로 어떻게 계산되는지, 왜 한 번 계산해서는 no-slip이 지켜지지 않는지, 그리고 마커 간격 하나를 잘못 잡으면 어느 방향으로 무너지는지를 코드로 확인한다.

커널을 바꿔 가며 slide를 켜 두면, ∑φ2\sum\varphi^2가 출렁이거나 한 값에 고정된다.

kernel
drag on the canvas to move the marker

2점 hat은 0.5와 1.0 사이를 오가고, Roma 3점은 0.5, Peskin 4점은 0.375에 붙는다. 모멘트 조건 두 개는 세 커널이 같이 만족한다.

강제항 하나로 벽을 세운다#

Peskin은 1972년에 심장 판막 주위 혈류를 풀려고 이 기법을 만들었다. 판막은 얇고 휘어진다. 그 형상에 맞춰 격자를 다시 짜는 일은 매 시간스텝 반복해야 한다. Peskin은 격자를 건드리는 대신 방정식에 항을 하나 더 넣었다.

∂u∂t+(u⋅∇)u=−1ρ∇p+ν∇2u+f\frac{\partial \mathbf{u}}{\partial t} + (\mathbf{u}\cdot\nabla)\mathbf{u} = -\frac{1}{\rho}\nabla p + \nu\nabla^2\mathbf{u} + \mathbf{f}

u\mathbf{u}는 속도장, pp는 압력, ν\nu는 동점성계수, f\mathbf{f}는 물체가 유체에 가하는 체적력이다. 벽 정보는 전부 f\mathbf{f} 안에 들어간다.

계산은 두 개의 격자를 오간다. 유체는 고정된 직교 격자(Eulerian) 위에, 물체는 표면을 따라 늘어선 마커 점(Lagrangian) 위에 산다. 둘을 잇는 것이 이산 델타 함수 δh\delta_h다. 방향은 두 갈래다.

Ul=∑xu(x) δh(x−Xl) hd\mathbf{U}_l = \sum_{\mathbf{x}} \mathbf{u}(\mathbf{x})\,\delta_h(\mathbf{x}-\mathbf{X}_l)\,h^d

격자 속도를 마커 위치 Xl\mathbf{X}_l로 끌어오는 보간(interpolation)이다. hh는 격자 간격, dd는 차원이다. 반대 방향은

f(x)=∑lFl δh(x−Xl) Δsl\mathbf{f}(\mathbf{x}) = \sum_{l} \mathbf{F}_l\,\delta_h(\mathbf{x}-\mathbf{X}_l)\,\Delta s_l

마커에서 계산한 힘 Fl\mathbf{F}_l을 격자로 뿌리는 확산(spreading)이다. Δsl\Delta s_l은 마커가 대표하는 표면 조각의 길이다. 두 연산자를 각각 I\mathcal{I}와 S\mathcal{S}로 쓰겠다.

연속 강제와 이산 강제 — 무엇을 포기하는가#

강제항을 만드는 방식은 크게 두 갈래로 갈린다.

연속 강제 (continuous)이산 강제 (ghost-cell / cut-cell)direct forcing + MDF
계면 표현델타 커널로 4h4h 정도 번짐sharp4h4h 정도 번짐
공간 정확도1차2차 이상 가능1차
물체 내부같이 푼다계산에서 제외같이 푼다
이동·변형 물체그대로 됨fresh cell 처리 필요그대로 됨
주 사용처탄성 막, 낮은 Re높은 Re 강체IB-LBM, 강체·이동체

연속 강제는 델타 함수를 쓴다. 그래서 계면이 번지고, 정확도는 1차에 묶이며, 물체 내부까지 전부 푼다. 높은 Reynolds 수(관성력/점성력 비) 계산에서는 이 낭비가 크다.

이산 강제는 델타 함수를 쓰지 않는다. 고스트 셀 IBM은 고체 쪽 셀에 가상값을 채우고, 표면 수직선을 따라 이미지 포인트에서 보간해 경계조건을 건다. 델타 함수가 없으니 2차 이상으로 올릴 수 있다. 대신 셀 분류(유체/고체/고스트)와 fresh point 처리가 붙는다. 물체가 움직이면 어제 고체였던 셀이 오늘 유체가 되고, 그 셀의 값은 어디에도 없다.

오늘 다루는 direct forcing은 첫 번째 갈래에 속한다. 구현이 짧고 이동 물체에 강해서 IB-LBM에서 특히 많이 쓰인다. 출발점은 Peskin이 판막을 격자에 박지 않은 구성이다.

델타 커널이 지켜야 하는 세 번째 조건#

δh\delta_h는 아무 함수나 되지 않는다. 1차원 커널 φ\varphi를 곱으로 쌓아 만드는데(δh(x)=h−d∏φ(xk/h)\delta_h(\mathbf{x}) = h^{-d}\prod \varphi(x_k/h)), φ\varphi는 조건을 만족해야 한다. 교과서가 늘 적는 두 개는 모멘트 조건이다.

∑iφ(r−i)=1,∑i(i−r) φ(r−i)=0\sum_i \varphi(r-i) = 1, \qquad \sum_i (i-r)\,\varphi(r-i) = 0

rr은 마커의 격자 좌표, ii는 정수 노드 번호다. 앞의 것은 뿌린 힘의 총합이 보존된다는 뜻이고, 뒤의 것은 그 힘의 무게중심이 마커 위치에 정확히 놓인다는 뜻이다.

문제는 잘 언급되지 않는 세 번째 조건이다.

∑iφ(r−i)2=const(∀r)\sum_i \varphi(r-i)^2 = \text{const} \quad (\forall r)

이 값은 IS\mathcal{I}\mathcal{S}의 대각 성분, 즉 마커가 자기 자신에게 되돌려받는 양이다. 이것이 rr에 따라 변하면, 물체가 격자를 가로질러 미끄러질 때 같은 속도 보정에 대해 다른 크기의 힘이 나온다. 힘이 격자 주기로 진동한다. 실린더를 균일하게 끌어당겼는데 항력 곡선에 톱니가 생기는 현상이 여기서 온다.

세 커널이 모멘트 두 개를 같이 만족해도, 갈리는 자리는 세 번째 조건이다. 위 화면의 아래 곡선이 그 차이다.

Peskin 4점 커널의 형태는 다음과 같다.

φ(r)={18(3−2∣r∣+1+4∣r∣−4r2),∣r∣≤118(5−2∣r∣−−7+12∣r∣−4r2),1<∣r∣≤20,그 외\varphi(r)=\begin{cases} \frac{1}{8}\left(3-2|r|+\sqrt{1+4|r|-4r^2}\right), & |r|\le 1 \\[4pt] \frac{1}{8}\left(5-2|r|-\sqrt{-7+12|r|-4r^2}\right), & 1<|r|\le 2 \\[4pt] 0, & \text{그 외} \end{cases}

지지 폭이 4h4h로 넓어 계면이 번지는 대가를 치른다. 그 대가로 이동 물체에서 힘이 떨지 않는다.

한 번의 direct forcing으로는 no-slip이 안 잡힌다#

Direct forcing의 발상은 단순하다. 강제항 없이 한 스텝 전진시킨 임시 속도장 u∗\mathbf{u}^*를 마커로 보간하고, 목표 속도 Ud\mathbf{U}^d와의 차이를 Δt\Delta t로 나눠 힘으로 삼는다.

Fl=Uld−I[u∗]lΔt\mathbf{F}_l = \frac{\mathbf{U}^{d}_l - \mathcal{I}[\mathbf{u}^*]_l}{\Delta t}

고정 강체면 Ud=0\mathbf{U}^d = \mathbf{0}, 움직이면 그 물체의 속도다. 이 힘을 격자로 뿌리고 속도를 갱신하면 끝 — 이어야 하는데, 끝이 아니다.

이유는 한 줄이다. IS≠E\mathcal{I}\mathcal{S} \ne \mathbf{E}. 보간했다가 다시 뿌리는 왕복이 항등 연산이 아니다. 마커 하나에 힘을 뿌리면 그 힘은 4h4h 폭으로 퍼지고, 그중 일부만 원래 마커로 되돌아온다. 나머지는 이웃 마커에게 가고, 격자 노드에 남는다. 그래서 보정 후 다시 보간해보면 슬립이 남아 있다.

64×64 격자에 반지름 0.180.18 원기둥, 균일류 u=1u=1로 시험한 결과 한 번의 보정 뒤 마커 위 슬립은 자유류의 **64%**였다. 벽이 서긴 섰는데 유체는 여전히 벽 속도의 3분의 2로 지나간다.

Multi-direct forcing — 반복이 메우는 간극#

Wang 등(2008)이 낸 답은 반복이다. 보간 → 힘 계산 → 확산을 한 번으로 끝내지 말고, 남은 슬립을 다시 입력으로 넣는다.

um+1=um+S[Ud−I[um]]\mathbf{u}^{m+1} = \mathbf{u}^{m} + \mathcal{S}\left[\mathbf{U}^{d} - \mathcal{I}[\mathbf{u}^{m}]\right]

이것은 A≡IS\mathbf{A} \equiv \mathcal{I}\mathcal{S}에 대한 Richardson 반복이다. 오차는 매 회 (E−A)(\mathbf{E}-\mathbf{A})가 곱해진다. 수렴 속도는 A\mathbf{A}의 고유값이 정한다.

암시적으로 한 번에 풀 수도 있다.

A ΔU=Ud−I[u∗]\mathbf{A}\,\Delta\mathbf{U} = \mathbf{U}^{d} - \mathcal{I}[\mathbf{u}^*]

마커 수만큼의 조밀 행렬을 매 시간스텝 푸는 비용이 든다. 물체가 움직이거나 변형되면 A\mathbf{A}도 매 스텝 다시 만들어야 한다. MDF는 이 행렬을 만들지 않고 곱셈만으로 같은 지점에 접근하는 방법이다.

여기에 함정이 하나 숨어 있다. 측정해보면 A\mathbf{A}의 최대 고유값은 0.374 근처이고 최소 고유값은 0에 붙어 있다. (E−A)(\mathbf{E}-\mathbf{A})의 스펙트럼 반경은 따라서 1이다. 지배 모드는 매 회 0.64배로 줄지만, 고유값이 0에 가까운 모드는 전혀 줄지 않는다. 그래서 MDF의 잔류 슬립은 0으로 수렴하지 않고 몇 % 수준에서 평평해진다. 반복 횟수를 20으로 올려도 5회일 때와 큰 차이가 없다. 3~5회면 얻을 것을 다 얻는다.

Python으로 세어보는 잔류 슬립#

원기둥 하나에 균일류를 걸고, 마커 간격을 바꿔가며 MDF를 돌린다. 측정하는 것은 두 가지다. 마커 위에서의 슬립, 그리고 마커 사이(중점)에서의 슬립.

import numpy as np
 
N, H = 64, 1.0 / 64            # Eulerian 격자: 64x64 균일 셀
R, CX, CY = 0.18, 0.5, 0.5     # 균일류 u = 1 속에 놓인 원형 물체
 
def peskin_kernel(r):
    """4점 Peskin 커널. 마커 위치와 무관하게 모멘트 조건이 유지된다."""
    a = np.abs(r)
    out = np.zeros_like(a)
    m1, m2 = a <= 1.0, (a > 1.0) & (a <= 2.0)
    out[m1] = (3 - 2 * a[m1] + np.sqrt(1 + 4 * a[m1] - 4 * a[m1] ** 2)) / 8
    out[m2] = (5 - 2 * a[m2] - np.sqrt(-7 + 12 * a[m2] - 4 * a[m2] ** 2)) / 8
    return out
 
def make_marker_ring(ratio, offset=0.0):
    """원 위의 Lagrangian 마커. 간격은 ds = ratio * h."""
    n = max(8, int(round(2 * np.pi * R / (ratio * H))))
    th = np.linspace(0, 2 * np.pi, n, endpoint=False) + offset * np.pi / n
    return CX + R * np.cos(th), CY + R * np.sin(th), 2 * np.pi * R / n
 
def marker_stencil(xm, ym):
    """마커마다 4x4 지지영역의 인덱스와 분리형 가중치."""
    ii = np.floor(xm / H - 1.5).astype(int)[:, None] + np.arange(4)
    jj = np.floor(ym / H - 1.5).astype(int)[:, None] + np.arange(4)
    return ii % N, jj % N, peskin_kernel(xm[:, None] / H - ii), peskin_kernel(ym[:, None] / H - jj)
 
def interp_to_markers(u, st):
    """Eulerian -> Lagrangian:  U_l = sum_x u(x) delta_h(x - X_l) h^2"""
    ii, jj, wx, wy = st
    out = np.zeros(ii.shape[0])
    for a in range(4):
        for b in range(4):
            out += u[ii[:, a], jj[:, b]] * wx[:, a] * wy[:, b]
    return out
 
def spread_to_grid(dU, st, ds):
    """Lagrangian -> Eulerian:  du(x) = sum_l dU_l delta_h(x - X_l) ds"""
    ii, jj, wx, wy = st
    out = np.zeros((N, N))
    for a in range(4):
        for b in range(4):
            np.add.at(out, (ii[:, a], jj[:, b]), dU * wx[:, a] * wy[:, b] * ds / H)
    return out
 
def influence_matrix(st, ds):
    """A = I S. 암시적 IB 해법이 역행렬을 구해야 하는 바로 그 행렬."""
    n = st[0].shape[0]
    A = np.zeros((n, n))
    for l in range(n):
        e = np.zeros(n)
        e[l] = 1.0
        A[:, l] = interp_to_markers(spread_to_grid(e, st, ds), st)
    return A
 
def slip_after_mdf(ratio, n_iter):
    """MDF를 n_iter회 돌린 뒤, 마커 위와 마커 사이의 슬립을 측정한다."""
    xm, ym, ds = make_marker_ring(ratio)
    st = marker_stencil(xm, ym)
    gap = marker_stencil(*make_marker_ring(ratio, offset=1.0)[:2])   # 마커 사이 중점
    u = np.ones((N, N))                                             # 자유류. 아직 물체를 모른다
    history = []
    for _ in range(n_iter):
        slip = 0.0 - interp_to_markers(u, st)                       # 목표 속도는 0
        history.append(np.max(np.abs(slip)))
        u += spread_to_grid(slip, st, ds)
    A = influence_matrix(st, ds)
    return dict(n=len(xm), history=history,
                on=np.max(np.abs(interp_to_markers(u, st))),
                between=np.max(np.abs(interp_to_markers(u, gap))),
                cond=np.linalg.cond(A), lam=np.linalg.eigvals(A).real.max())
 
print(f"{'ds/h':>5}{'markers':>9}{'slip@marker':>13}{'slip@gap':>10}{'cond(A)':>11}{'lam_max':>9}")
for ratio in (0.25, 0.5, 1.0, 1.5, 2.0, 3.0):
    r = slip_after_mdf(ratio, n_iter=10)
    print(f"{ratio:5.2f}{r['n']:9d}{r['on']:13.4f}{r['between']:10.4f}{r['cond']:11.1e}{r['lam']:9.3f}")

출력은 다음과 같다.

 ds/h  markers  slip@marker  slip@gap    cond(A)  lam_max
 0.25      290       0.0407    0.0442    1.3e+11    0.369
 0.50      145       0.0352    0.0439    8.8e+05    0.371
 1.00       72       0.0425    0.0455    6.8e+02    0.374
 1.50       48       0.0368    0.0630    6.4e+00    0.374
 2.00       36       0.0114    0.0519    2.0e+00    0.376
 3.00       24       0.0041    0.2865    1.1e+00    0.434

ds/h를 2.5 위로 올리면 마커 틈으로 흰 점이 빨갛게 바뀌며 들어가고, 0.4 아래로 내리면 cos θ가 1에 붙는다.

통과한 입자는 물체 안이다. MDF passes는 0에서 3까지 슬립이 크게 줄고, 그 뒤는 거의 그대로다.

slip@marker 열만 보면 마커를 성기게 둘수록 좋아 보인다. Δs/h=3\Delta s/h = 3에서 0.004로 가장 작다. 함정이다.

원주 2πR=2π×0.18≈1.1312\pi R = 2\pi\times 0.18 \approx 1.131, h=1/64=0.015625h=1/64=0.015625다. Δs/h=1\Delta s/h=1이면 마커 수는 1.131/0.015625≈72.41.131/0.015625\approx 72.4를 반올림한 72다. 표의 72와 같다. Δs/h=0.25\Delta s/h=0.25는 290개, 3.0은 24개다. 한 번 보정 뒤 슬립은 자유류의 64%, 즉 0.64다. 10회 MDF 뒤 Δs/h=1\Delta s/h=1의 마커 위 슬립은 0.0425다. 0.64의 약 15분의 1이다. 고유값 0.374면 한 회 수축은 1−0.374=0.6261-0.374=0.626이고, 앞에서 말한 0.64와 맞다. 0.645≈0.1070.64^5\approx 0.107이라 다섯 번이면 지배 모드는 처음의 약 11%다. 0 근처 모드는 그대로라, 10회 뒤 0.04 언저리가 바닥이다.

Δs/h=3\Delta s/h=3에서 사이 슬립 0.2865는 마커 위 0.0041의 약 70배다. Δs/h=1\Delta s/h=1에서는 0.0455와 0.0425로 거의 같다. 조건수는 0.25에서 1.3×10111.3\times 10^{11}, 1.0에서 6.8×1026.8\times 10^{2}, 3.0에서 1.1이다. λmax⁡\lambda_{\max}는 2.0까지 0.376이다가 3.0에서 0.434로 튄다. 마커가 성기면 대각도 같이 움직인다.

Δs/h가 만드는 두 개의 낭떠러지#

같은 표의 slip@gap 열을 보자. Δs/h=3\Delta s/h = 3에서 0.287이다. 마커가 앉은 자리에서는 no-slip이 거의 완벽한데, 마커와 마커 사이에서는 자유류의 29%가 그대로 지나간다. 강제한 지점만 조용하고 그 사이로는 물이 샌다.

반대쪽 낭떠러지는 cond(A) 열에 있다. Δs/h=0.25\Delta s/h = 0.25에서 조건수가 101110^{11}이다. 마커가 너무 촘촘하면 이웃한 두 마커가 거의 같은 격자 노드를 보게 되고, A\mathbf{A}의 행이 서로 평행해진다. 명시적 MDF는 그래도 돌아간다. 암시적 해법은 이 지점에서 죽는다.

실무에서 Δs≈h\Delta s \approx h를 권하는 이유가 이 두 열 사이에 있다. 표면 격자를 유체 격자보다 조금 조밀하게(Δs≈0.7h∼1.0h\Delta s \approx 0.7h \sim 1.0h) 잡는 것이 안전한 구간이다.

마커 위만 조용하면 벽을 세운 게 아니다#

slip@marker가 0.004까지 내려도 Δs/h=3\Delta s/h=3의 사이 슬립은 0.2865다. 강제한 점만 조용하다. 반대쪽 Δs/h=0.25\Delta s/h=0.25는 누수가 0.0442로 작지만 조건수가 1.3×10111.3\times10^{11}이라 암시적 역행렬이 죽는다. 표에서 두 열이 같이 작은 구간은 Δs/h\Delta s/h가 0.5에서 1.5 사이다.

커널을 모멘트 두 개로만 고르면 2점 hat의 ∑φ2\sum\varphi^2가 0.5와 1.0 사이를 오가며 항력에 격자 톱니를 남긴다. 한 번의 direct forcing은 슬립의 64%를 남긴다. 3에서 5회면 지배 모드는 줄고, 영공간 쪽은 남는다. 속도를 먼저 전진시키는 자리는 Chorin의 투영 스텝과 같다.

도움이 됐다면 공유해주세요.