Skip to content

Commit 25b9b5f

Browse files
Merge pull request #14 from kirk0830/speedup-poisson-jac
Perf: parallelize the construction of Jacobian and RHS-vector in Poisson solving problem
2 parents 55ba23c + eba14c2 commit 25b9b5f

File tree

5 files changed

+1085
-128
lines changed

5 files changed

+1085
-128
lines changed
Lines changed: 251 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,251 @@
1+
import logging
2+
from typing import Optional, Tuple, List, Callable
3+
4+
import numpy as np
5+
from scipy.sparse import lil_matrix
6+
7+
log = logging.getLogger(__name__)
8+
9+
def _impose_j_bound(inout, nx, ny, nz, typ, val, mask):
10+
'''impose the special mask for boundary points'''
11+
# Neumann boundary types
12+
neumann_types_ = ['xmin', 'xmax', 'ymin', 'ymax', 'zmin', 'zmax']
13+
displ_ = [+1, -1, +nx, -nx, +nx*ny, -nx*ny]
14+
for t, d in zip(neumann_types_, displ_):
15+
i = np.where(np.array(typ) == t)[0]
16+
if len(i) == 0:
17+
log.warning(f'no grid point with type {t} found')
18+
# scipy sparse matrix also supports the parallelized assignment
19+
inout[i, i] = val
20+
inout[i, i + d] = mask
21+
22+
# Dirichlet boundary
23+
i = np.where(typ == 'Dirichlet')[0]
24+
inout[i, i] = val
25+
26+
def _impose_b_bound(inout, nx, ny, nz, typ, phi, dirichlet_pot):
27+
'''impose the special mask for boundary points in b vector'''
28+
# Neumann boundary types
29+
neumann_types_ = ['xmin', 'xmax', 'ymin', 'ymax', 'zmin', 'zmax']
30+
displ_ = [+1, -1, +nx, -nx, +nx*ny, -nx*ny]
31+
for t, d in zip(neumann_types_, displ_):
32+
i = np.where(typ == t)[0]
33+
if len(i) == 0:
34+
log.warning(f'no grid point with type {t} found')
35+
inout[i] = phi[i] - phi[i + d]
36+
37+
# Dirichlet boundary
38+
i = np.where(typ == 'Dirichlet')[0]
39+
inout[i] = phi[i] - dirichlet_pot[i]
40+
41+
def coulomb(chr: float|np.ndarray) -> float|np.ndarray:
42+
'''convert the charge to unit of Coulomb'''
43+
assert isinstance(chr, (float, np.ndarray)), "chr must be a float or numpy array"
44+
from dpnegf.utils.constants import elementary_charge as e
45+
return chr * e
46+
47+
def _bflux_impl(jflux, i, nx, ny, nz, phi, full_size=True) -> np.ndarray:
48+
'''calculate the b vector from the jflux and phi'''
49+
disp_ = [-1, +1, -nx, +nx, -nx*ny, +nx*ny]
50+
if full_size:
51+
assert jflux.shape == (6, len(phi))
52+
bflux = np.zeros((6, len(phi)), dtype=float)
53+
for j, d in enumerate(disp_):
54+
bflux[j, i] = jflux[j, i] * (phi[i + d] - phi[i])
55+
else:
56+
assert jflux.shape == (6, len(i))
57+
bflux = np.array([jf * (phi[i + d] - phi[i]) for jf, d in zip(jflux, disp_)])
58+
assert bflux.shape == (6, len(i))
59+
return bflux
60+
61+
def _jflux_impl(nx, ny, nz, r, typ, sigma, eps, eps0, avgeps, with_index=False,
62+
full_size=True) -> Tuple[np.ndarray, Optional[np.ndarray]]:
63+
'''kernal implementation of the Jacobian calculation.'''
64+
65+
# get all indices of the grid points of type "in"
66+
i = np.where(typ == "in")[0]
67+
# j stands for flux, <listcomp> faster than for loop
68+
disp_ = [-1, +1, -nx, +nx, -nx*ny, +nx*ny]
69+
if not full_size:
70+
jflux = np.array([avgeps(eps[i + d], eps[i]) * eps0 * sigma[i, j // 2] \
71+
/ np.abs(r[i + d, j // 2] - r[i, j // 2])
72+
for j, d in enumerate(disp_)]).reshape((6, -1))
73+
assert jflux.shape == (6, len(i))
74+
else:
75+
jflux = np.zeros((6, len(typ)), dtype=float)
76+
for j, d in enumerate(disp_):
77+
jflux[j, i] = avgeps(eps[i + d], eps[i]) * eps0 * sigma[i, j // 2] \
78+
/ np.abs(r[i + d, j // 2] - r[i, j // 2])
79+
80+
return (jflux, i if with_index else None)
81+
82+
def _jacobian(inout, flux, i, nx, ny, nz, typ, zfree, phi, phi_, beta):
83+
'''calculate the Jacobian matrix for the Poisson equation'''
84+
disp_ = [-1, +1, -nx, +nx, -nx*ny, +nx*ny]
85+
# diagonal elements
86+
assert flux.shape == (6, len(i))
87+
inout[i, i] = -np.sum(flux, axis=0)
88+
inout[i, i] -= beta * \
89+
np.abs(coulomb(zfree[i])) * np.exp(-beta * np.sign(zfree[i]) * (phi[i] - phi_[i]))
90+
# non-diagonal elements
91+
for jf, d in zip(flux, disp_):
92+
inout[i, i + d] = jf
93+
# impose the boundary conditions
94+
_impose_j_bound(inout, nx, ny, nz, typ, val=1.0, mask=-1.0)
95+
96+
def _rhsvec(inout, flux, i, nx, ny, nz, typ, zfree, zfixed, phi, phi_, dirichlet_pot, beta):
97+
'''calculate the right-hand side vector for the Poisson equation'''
98+
# calculate the b vector
99+
assert flux.shape == (6, len(i))
100+
inout[i] = np.sum(flux, axis=0)
101+
inout[i] += coulomb(zfree[i]) * \
102+
np.exp(-beta * np.sign(zfree[i]) * (phi[i] - phi_[i]))
103+
inout[i] += coulomb(zfixed[i])
104+
# impose the boundary conditions
105+
_impose_b_bound(inout, nx, ny, nz, typ, phi, dirichlet_pot)
106+
107+
inout *= -1 # for convenience change the sign of B in later NR iteration
108+
109+
def nr_construct(jinout: Optional[lil_matrix],
110+
binout: Optional[np.ndarray],
111+
grid_dim: Tuple[int, int, int]|List[int],
112+
gridpoint_coords: np.ndarray,
113+
gridpoint_typ: List[str]|np.ndarray,
114+
gridpoint_surfarea: np.ndarray,
115+
eps: np.ndarray,
116+
phi: np.ndarray,
117+
phi_: np.ndarray,
118+
free_chr: np.ndarray,
119+
fixed_chr: Optional[np.ndarray] = None,
120+
dirichlet_pot: Optional[np.ndarray] = None,
121+
eps0: float = 8.854187817e-12,
122+
beta: float = 1/(1.380649e-23 * 300),
123+
feps: Callable[[float, float], float] = lambda eps1, eps2: (eps1 + eps2) / 2.0) -> None:
124+
'''
125+
refactored version of the implementation to calculate the Jacobian for the Poisson equation
126+
using the Newton-Raphson method (finit difference method).
127+
128+
NOTE: the input would be modified in-place
129+
130+
Parameters
131+
----------
132+
jinout : scipy.sparse.lil_matrix
133+
The Jacobian matrix to be calculated. It is a sparse matrix in LIL format. If is None,
134+
then the Jacobian will not be calculated.
135+
binout : np.ndarray
136+
The b vector to be calculated, which is the right-hand side of the linear system.
137+
It is a dense numpy array. If is None, then the b vector will not be calculated.
138+
grid_dim : tuple or list of int
139+
The dimensions of the grid as (nx, ny, nz), where nx, ny, and nz are the number of grid
140+
points in the x, y, and z directions, respectively.
141+
gridpoint_coords : np.ndarray
142+
The coordinates of the grid points, shape (nr, 3), where nr is the total number of grid
143+
points.
144+
gridpoint_typ : list of str
145+
The type of grid point, with length nr
146+
gridpoint_surfarea : np.ndarray
147+
The area of Voronoi surface in three dimension, shape (nr, 3). For example the gridpoint_
148+
area[ir, 0] indicates the area of surface whose norm vector is along x-axis
149+
eps : np.ndarray
150+
The relative permittivity of each grid point
151+
phi : np.ndarray
152+
The electrostatic potential mapped on real-space grid point
153+
phi\_ : np.ndarray
154+
The "old" electrostatic potential mapped on real-space grid point
155+
free_chr : np.ndarray
156+
The free charge density mapped on real-space grid point
157+
fixed_chr : np.ndarray, optional
158+
The fixed charge density mapped on real-space grid point, shape (nr,).
159+
dirichlet_pot : np.ndarray, optional
160+
The Dirichlet potential mapped on real-space grid point, shape (nr,).
161+
eps0 : float, optional
162+
The vacuum permittivity, a constant value. Default is 8.854187817e-12 F/m.
163+
beta : float, optional
164+
The inverse temperature (1/kBT), used in the Boltzmann factor for charge density.
165+
Default is 1/(1.380649e-23 * 300), which corresponds to room temperature (300 K).
166+
feps : callable, optional
167+
the functor to calculate the average permittivity on a pair of grid points.
168+
It should take two arguments, eps1 and eps2, and return the average permittivity.
169+
If not provided, it defaults to the arithmetic mean.
170+
'''
171+
# shortcut: nothing to do
172+
if jinout is None and binout is None:
173+
return
174+
175+
# check dimensions
176+
if not isinstance(grid_dim, (tuple, list)) or len(grid_dim) != 3:
177+
raise ValueError("grid_dim must be a tuple or list of three integers")
178+
if any(not isinstance(d, int) or d <= 0 for d in grid_dim):
179+
raise ValueError("grid_dim must contain positive integers")
180+
nx, ny, nz = grid_dim; nr = nx * ny * nz
181+
182+
# simple sanity check
183+
if jinout is not None:
184+
if not isinstance(jinout, lil_matrix):
185+
raise TypeError("jinout (jacobian) must be a scipy.sparse.lil_matrix")
186+
if jinout.shape != (nr, nr):
187+
raise ValueError(f"jinout (jacobian) must have shape ({nr}, {nr}), but got {jinout.shape}")
188+
189+
if binout is not None:
190+
if not isinstance(binout, np.ndarray):
191+
raise TypeError("binout must be a numpy array")
192+
if binout.shape != (nr,):
193+
raise ValueError(f"binout must have shape ({nr},), but got {binout.shape}")
194+
195+
# check the common input parameters
196+
if not isinstance(gridpoint_coords, np.ndarray) or gridpoint_coords.shape != (nr, 3):
197+
raise ValueError(f"gridpoint_coords must be a numpy array of shape ({nr}, 3)")
198+
gridpoint_typ = np.array(list(gridpoint_typ.values()), dtype=str) \
199+
if isinstance(gridpoint_typ, dict) else np.array(gridpoint_typ, dtype=str)
200+
if not isinstance(gridpoint_typ, np.ndarray) or len(gridpoint_typ) != nr:
201+
raise ValueError(f"gridpoint_typ must be a list of length {nr}")
202+
if not isinstance(gridpoint_surfarea, np.ndarray) or gridpoint_surfarea.shape != (nr, 3):
203+
raise ValueError(f"gridpoint_surfarea must be a numpy array of shape ({nr}, 3)")
204+
if not isinstance(eps, np.ndarray) or eps.shape != (nr,):
205+
raise ValueError(f"eps must be a numpy array of shape ({nr},)")
206+
if not isinstance(phi, np.ndarray) or phi.shape != (nr,):
207+
raise ValueError(f"phi must be a numpy array of shape ({nr},)")
208+
if not isinstance(phi_, np.ndarray) or phi_.shape != (nr,):
209+
raise ValueError(f"phi_ must be a numpy array of shape ({nr},)")
210+
if not isinstance(free_chr, np.ndarray) or free_chr.shape != (nr,):
211+
raise ValueError(f"free_chr must be a numpy array of shape ({nr},)")
212+
213+
if binout is not None:
214+
if not isinstance(fixed_chr, np.ndarray) or fixed_chr.shape != (nr,):
215+
raise ValueError(f"fixed_chr must be a numpy array of shape ({nr},)")
216+
if not isinstance(dirichlet_pot, np.ndarray) or dirichlet_pot.shape != (nr,):
217+
raise ValueError(f"dirichlet_pot must be a numpy array of shape ({nr},)")
218+
219+
if not isinstance(eps0, (float, int)):
220+
raise TypeError("eps0 must be a float or int")
221+
if not isinstance(beta, (float, int)):
222+
raise TypeError("beta must be a float or int")
223+
if not callable(feps):
224+
raise TypeError("feps must be a callable function with strictly two arguments: "
225+
"eps1 and eps2 to calculate the average permittivity")
226+
227+
# ================================ implementation =======================================
228+
# making alias for more succint code
229+
r = gridpoint_coords
230+
typ = np.array(gridpoint_typ, dtype=str)
231+
sigma = gridpoint_surfarea
232+
zfree = free_chr
233+
zfixed = fixed_chr
234+
235+
# calculate the flux of jacobian for all grid points of type "in"
236+
jflux, ind = _jflux_impl(nx, ny, nz, r, typ, sigma, eps, eps0, feps,
237+
with_index=True, full_size=False)
238+
# the "ind" is the indices of the grid points of type "in"
239+
assert jflux.shape == (6, len(ind)) # ensure the full_size is disabled
240+
241+
if jinout is not None: # only calculate jacobian when jinout is provided (allocated)
242+
_jacobian(jinout, jflux, ind, nx, ny, nz, typ, zfree, phi, phi_, beta)
243+
assert jinout.shape == (nr, nr)
244+
245+
if binout is not None: # only calculate b vector when binout is provided (allocated)
246+
# calculate bflux with jflux
247+
bflux = _bflux_impl(jflux, ind, nx, ny, nz, phi, full_size=False)
248+
assert bflux.shape == (6, len(ind))
249+
_rhsvec(binout, bflux, ind, nx, ny, nz, typ, zfree, zfixed, phi, phi_,
250+
dirichlet_pot, beta)
251+
assert binout.shape == (nr,)

0 commit comments

Comments
 (0)