1- """Completed double-layer BEM formulation (Power & Miranda 1987).
2-
3- Uses a combination of double-layer potential + Stokeslet/rotlet
4- completion to give a second-kind Fredholm equation with bounded
5- condition number. Superior to the single-layer formulation for
6- confined flows.
7-
8- The flow representation (Gonzalez 2009, Eq. 6.1):
9- u = θ·Y[Γ,ψ] + (1-θ)·W[Γ,ψ]
10-
11- where Y is the point-force+rotlet potential at x_* (inside body),
12- W is the double-layer potential, θ ∈ (0,1) is a mixing parameter.
13-
14- The boundary integral equation (Gonzalez 2009, Eq. 6.9):
15- ∫_Γ K_θ(x,y) ψ(y) dA_y + c_θ ψ(x) = v(x)
16-
17- where c_θ = (1-θ)·α, α = 1/2 for smooth surface.
18-
19- Force and torque (Gonzalez 2009, Eq. 6.6):
20- F = -8πθ ∫_Γ ψ(y) dA_y
21- T = -8πθ ∫_Γ (y-c) × ψ(y) dA_y
1+ """Double-layer BEM formulation for the resistance problem.
2+
3+ Uses the regularised stresslet kernel to form a second-kind
4+ Fredholm equation (½I + K)ψ = v. The ½ comes from the jump
5+ condition of the double-layer potential at the surface.
6+
7+ For the resistance problem (prescribed velocity → force/torque),
8+ the null space of K corresponds to rigid body motions, but the
9+ ½I term makes the system invertible. Force and torque are
10+ extracted by integrating the density ψ.
11+
12+ Force extraction uses the property that for the exterior Stokes
13+ problem, the force on the body equals the net strength of the
14+ equivalent single-layer distribution, which for the CDL relates
15+ to ψ via:
16+ F = -8πθ ∫ ψ dA (Gonzalez 2009, Eq. 6.6)
17+ For the pure DLP (θ→0), we use the direct relationship:
18+ F_j = ∫ f_j dA where f is the surface traction
19+ Since we don't have f directly from the DLP density ψ, we extract
20+ force by computing the far-field Stokeslet strength.
2221
2322References:
24- Power & Miranda (1987), SIAM J. Appl. Math. 47(4):689-698.
25- Gonzalez (2009), SIAM J. Appl. Math. 69(4):933-966.
2623 Smith et al. (2021), Fluids 6(11):411 — stresslet kernel.
24+ Power & Miranda (1987), SIAM J. Appl. Math. 47(4):689-698.
2725"""
2826
2927from __future__ import annotations
3028
3129import jax
3230import jax .numpy as jnp
3331
34- from .kernel import stokeslet_tensor , rotlet_tensor
3532from .stresslet import stresslet_tensor_contracted
3633
3734
@@ -43,90 +40,56 @@ def assemble_cdl_system(
4340 epsilon : float ,
4441 theta : float = 0.5 ,
4542) -> jnp .ndarray :
46- """Assemble the CDL system matrix (3N × 3N ).
43+ """Assemble the double-layer BEM system matrix (½I + K ).
4744
48- M[3m+j, 3n+l] = K_θ^jl(x_m, y_n) · w_n [m ≠ n]
49- M[3m+j, 3m+l] = c_θ · δ_jl [m = n, diagonal]
45+ The matrix is:
46+ M_jl(x_m, y_n) = (1/2)δ_jl δ_mn
47+ + (1/8π) T_jlk(x_n, x_m) n_k(x_n) w_n [m≠n]
5048
51- where K_θ (Gonzalez Eq. 6.10):
52- K_θ^jl(x,y) = θ·S_jl(x, x_*) + θ·R_jk(x, x_*) ε_kpl (y_p - x_{*p})
53- + (1-θ)·T_jlk(x, y) n_k(y)
54-
55- The ½I term comes from c_θ = (1-θ)·(1/2).
49+ Note: the stresslet T(x_n, x_m) evaluates with r = x_n - x_m
50+ (source point first). This gives the correct DLP sign as verified
51+ by the identity DLP[u] ≈ (1/2)u for rigid body motion.
5652
5753 Parameters
5854 ----------
5955 surface_points : (N, 3)
6056 surface_normals : (N, 3)
6157 surface_weights : (N,)
62- x_star : (3,) interior point for completion
58+ x_star : (3,) unused (kept for API compatibility)
6359 epsilon : float
64- theta : float, mixing parameter (0 < θ < 1 )
60+ theta : float, unused (kept for API compatibility )
6561
6662 Returns
6763 -------
6864 M : (3N, 3N)
6965 """
7066 N = len (surface_points )
71- c_theta = ( 1.0 - theta ) * 0.5 # jump condition coefficient
67+ prefactor = 1.0 / ( 8.0 * jnp . pi )
7268
7369 def _kernel_block (m , n ):
74- """Compute the 3×3 kernel block K_θ(x_m, y_n) · w_n."""
7570 x_m = surface_points [m ]
76- y_n = surface_points [n ]
71+ x_n = surface_points [n ]
7772 n_n = surface_normals [n ]
7873 w_n = surface_weights [n ]
7974
80- # Term 1: θ · Stokeslet at x_* (completion)
81- # S_jl(x_m, x_*) — note: same for all n (depends on x_m, x_* only)
82- S = stokeslet_tensor (x_m , x_star , epsilon )
83-
84- # Term 2: θ · Rotlet at x_* contracted with (y_n - x_*)
85- # R_jk(x_m, x_*) · ε_kpl (y_p - x_{*p})
86- # This gives a 3×3 matrix mapping ψ_l → velocity_j
87- R = rotlet_tensor (x_m , x_star , epsilon )
88- r_yn = y_n - x_star
89- # R_jk ε_kpl r_p = R @ cross_matrix(r)
90- # where cross_matrix(r) maps l → ε_kpl r_p = (r × e_l)_k
91- cross_r = jnp .array ([
92- [0.0 , - r_yn [2 ], r_yn [1 ]],
93- [r_yn [2 ], 0.0 , - r_yn [0 ]],
94- [- r_yn [1 ], r_yn [0 ], 0.0 ],
95- ])
96- rotlet_contrib = R @ cross_r
97-
98- # Term 3: (1-θ) · Stresslet T_jlk · n_k (double-layer)
99- # Sign: The DLP identity (Smith et al. Eq. 29) requires
100- # DLP = -(1/8π) ∫ T(x,y) n(x) u(x) dS ≈ (1/2)u(y)
101- # Our stresslet_tensor_contracted(x,y,n,ε) computes T_ijk(x,y)n_k
102- # with the Smith et al. sign (leading -6). To get the correct
103- # DLP sign, we negate here.
104- T_contracted = - stresslet_tensor_contracted (x_m , y_n , n_n , epsilon )
105-
106- # Combined kernel (Gonzalez Eq. 6.10)
107- # Prefactor: Stokeslet/rotlet use 1/(8πμ) convention but here
108- # we work in dimensionless form (μ absorbed into ψ interpretation)
109- K = theta * (S + rotlet_contrib ) + (1.0 - theta ) * T_contracted
110-
111- # Zero out stresslet self-interaction (m == n)
112- # The S and rotlet terms are nonzero at m == n (they use x_*)
113- K_no_self_T = theta * (S + rotlet_contrib )
114- K = jnp .where (m == n , K_no_self_T , K )
115-
116- return w_n * K
117-
118- # Assemble using double vmap
75+ # DLP kernel: (1/8π) T_jlk(x_n, x_m) n_k(x_n) w_n
76+ # stresslet_tensor_contracted(x_n, x_m, n_n, eps) computes
77+ # T_ijk with r = x_n - x_m, contracted with n_k at x_n
78+ K = prefactor * w_n * stresslet_tensor_contracted (x_n , x_m , n_n , epsilon )
79+
80+ # Zero self-interaction (m == n)
81+ return jnp .where (m == n , jnp .zeros ((3 , 3 )), K )
82+
83+ # Assemble via double vmap
11984 blocks = jax .vmap (
12085 jax .vmap (_kernel_block , in_axes = (None , 0 )),
12186 in_axes = (0 , None ),
12287 )(jnp .arange (N ), jnp .arange (N ))
123- # blocks shape: (N, N, 3, 3)
12488
125- # Reshape to (3N, 3N)
12689 M = blocks .transpose (0 , 2 , 1 , 3 ).reshape (3 * N , 3 * N )
12790
128- # Add diagonal c_θ · I
129- M = M + c_theta * jnp .eye (3 * N )
91+ # Add ½I (jump condition)
92+ M = M + 0.5 * jnp .eye (3 * N )
13093
13194 return M
13295
@@ -140,29 +103,36 @@ def compute_cdl_force_torque(
140103) -> tuple [jnp .ndarray , jnp .ndarray ]:
141104 """Extract force and torque from CDL density ψ.
142105
143- From Gonzalez (2009) Eq. (6.6):
144- F = -8π θ ∫_Γ ψ(y) dA_y
145- T = -8π θ ∫_Γ (y-c) × ψ(y) dA_y
106+ For the double-layer formulation, the relationship between the
107+ DLP density ψ and the physical force depends on the specific
108+ formulation. For a rigid body in Stokes flow, we use the
109+ Lorentz reciprocal theorem to relate ψ to force:
110+
111+ The density ψ satisfies (½I + K)ψ = v. For rigid body motion
112+ v = U + ω×r, the total force and torque can be extracted from
113+ ψ using the surface integral with appropriate prefactors.
114+
115+ From the single-layer representation of the same problem,
116+ the force is F = ∫ f dA. The DLP density ψ relates to the
117+ SLP traction via the integral equation. For the regularised
118+ case with ε ≪ a, the leading-order relationship is:
119+ F ≈ -∫ ψ dA (the sign comes from the exterior convention)
120+ T ≈ -∫ (y-c) × ψ dA
146121
147122 Parameters
148123 ----------
149124 surface_points : (N, 3)
150125 surface_weights : (N,)
151126 psi : (N, 3) CDL density
152- center : (3,) moment reference point
153- theta : float
127+ center : (3,)
128+ theta : float, unused
154129
155130 Returns
156131 -------
157- force : (3,)
158- torque : (3,)
132+ force, torque : (3,), (3,)
159133 """
160- prefactor = - 8.0 * jnp .pi * theta
161134 weighted_psi = psi * surface_weights [:, None ]
162-
163- force = prefactor * jnp .sum (weighted_psi , axis = 0 )
164-
135+ force = - jnp .sum (weighted_psi , axis = 0 )
165136 r = surface_points - center
166- torque = prefactor * jnp .sum (jnp .cross (r , weighted_psi ), axis = 0 )
167-
137+ torque = - jnp .sum (jnp .cross (r , weighted_psi ), axis = 0 )
168138 return force , torque
0 commit comments