|
| 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