518 lines
19 KiB
Python
518 lines
19 KiB
Python
'''
|
|
Copyright (c) 2015, Harsh Bhatia (bhatia4@llnl.gov)
|
|
All rights reserved.
|
|
|
|
Redistribution and use in source and binary forms, with or without
|
|
modification, are permitted provided that the following conditions are met:
|
|
1. Redistributions of source code must retain the above copyright notice, this
|
|
list of conditions and the following disclaimer.
|
|
2. Redistributions in binary form must reproduce the above copyright notice,
|
|
this list of conditions and the following disclaimer in the documentation
|
|
and/or other materials provided with the distribution.
|
|
THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND
|
|
ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
|
|
WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
|
|
DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE
|
|
FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
|
|
DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
|
|
SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
|
|
CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,
|
|
OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
|
|
OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
|
|
'''
|
|
|
|
import numpy
|
|
from scipy import signal, spatial
|
|
from .timer import Timer
|
|
|
|
# Try to import CuPy for GPU acceleration
|
|
try:
|
|
import cupy as cp
|
|
import cupyx.scipy.signal as cp_signal
|
|
CUPY_AVAILABLE = True
|
|
except ImportError:
|
|
cp = None
|
|
cp_signal = None
|
|
CUPY_AVAILABLE = False
|
|
|
|
|
|
'''
|
|
# ------------------------------------------------------------------------------
|
|
Poisson Solver for 1D, 2D, and 3D scalar fields.
|
|
Data may be defined on a rectlinear grid or unstructured points.
|
|
|
|
1) create an object
|
|
PoissonSolver(args)
|
|
'solver' : type of solver
|
|
'S' for spatial convolution
|
|
only option for unstructured points
|
|
'F' for convolution in frequency space
|
|
default option for regular grids
|
|
|
|
--> for regular grid
|
|
'grid' : shape of the rectlinear grid
|
|
tuple of dimensions (Z,Y,X)
|
|
'spacings' : spacings of the rectlinear grid
|
|
tuple of spacings (dz,dy,dx)
|
|
|
|
--> for unstrucutred points
|
|
'points' : unstructured points
|
|
numpy array of shape (N, dim)
|
|
'pvolumes' : point volumes for each point
|
|
numpy array of shape (N, 1)
|
|
|
|
2) call prepare()
|
|
this initializes internal datastructures depending upon the mesh.
|
|
|
|
Once initialized, solve() can be called any number of times to
|
|
solve the Poisson equation on the same domain. If the domain
|
|
changes (e.g., a Lagrangian mesh), create a new object.
|
|
|
|
3) call solve(f), where, f is a numpy array defining a scalar field
|
|
|
|
for rectlinear grid, f.shape = grid.shape
|
|
for unstructured points, f.shape = (points.shape[0], 1)
|
|
|
|
# ------------------------------------------------------------------------------
|
|
'''
|
|
|
|
class PoissonSolver(object):
|
|
|
|
# constructor
|
|
def __init__(self, **kwargs):
|
|
|
|
args = kwargs.keys()
|
|
|
|
if ('grid' in args) == ('points' in args):
|
|
raise SyntaxError("Poisson Solver needs either shape of a regular grid or the points in an rectilinear/unstructured grid")
|
|
|
|
if ('grid' in args) != ('spacings' in args):
|
|
raise SyntaxError("Poisson Solver needs spacings for regular grid")
|
|
|
|
if ('points' in args) != ('pvolumes' in args):
|
|
raise SyntaxError("Poisson Solver needs point volumes for unstructured points")
|
|
|
|
# (default) data type
|
|
self.dtype = numpy.float32
|
|
if 'dtype' in args:
|
|
self.dtype = kwargs['dtype']
|
|
|
|
if 'grid' in args:
|
|
self.ptype = 'G'
|
|
self.stype = 'F' # default
|
|
self.gdims = kwargs['grid']
|
|
self.gdx = kwargs['spacings']
|
|
self.dim = len(self.gdims)
|
|
if self.dim != len(self.gdx):
|
|
raise ValueError("Dimensions of spacings should match that of the grid")
|
|
|
|
elif 'points' in args:
|
|
self.ptype = 'P'
|
|
self.stype = 'S' # only option
|
|
self.points = kwargs['points']
|
|
self.pvolumes = kwargs['pvolumes']
|
|
self.dim = len(self.points.shape)
|
|
if self.pvolumes.shape[0] != self.points.shape[0]:
|
|
raise ValueError("Number of pvolumes should match that of the points")
|
|
|
|
if 'solver' in args:
|
|
if (kwargs['solver'] != 'S' and kwargs['solver'] != 'F'):
|
|
raise SyntaxError('Solver can be only of type S(patial) or F(requency)')
|
|
|
|
if (kwargs['solver'] == 'F') and ('points' in args):
|
|
raise SyntaxError('Only Spatial solver supported for unstructured points')
|
|
|
|
if (kwargs['solver'] == 'S') and ('grid' in args):
|
|
raise SyntaxError('Frequncy solver strongly suggested for structured grids')
|
|
|
|
self.stype = kwargs['solver']
|
|
|
|
if (self.dim > 3):
|
|
raise ValueError("Poisson Solver works only for 1, 2, or 3 dimensions")
|
|
|
|
self.ready = False
|
|
self._gpu_cache = None # Cache for GPU arrays
|
|
# if (self.ptype == 'G'):
|
|
# print( 'PoissonSolver:', self.dim, 'D grid =', self.gdims, 'with spacings', self.gdx)
|
|
# elif (self.ptype == 'P'):
|
|
# print( 'PoissonSolver', self.dim, 'D points =', self.points.shape)
|
|
|
|
# --------------------------------------------------------------------------
|
|
# create the Green's function in appropriate representation
|
|
# --------------------------------------------------------------------------
|
|
|
|
# generate a grid -(N-1) to (N-1) to define distance function
|
|
# where N is the size of the original grid
|
|
def _create_radialGrid(self, verbose):
|
|
|
|
mtimer = Timer()
|
|
if verbose:
|
|
print( ' - creating distance kernel:', end ='')
|
|
|
|
rdims = tuple(2*d - 1 for d in self.gdims)
|
|
|
|
R = numpy.indices(rdims, dtype=self.dtype)
|
|
for d in range(self.dim):
|
|
R[d] += 1-self.gdims[d]
|
|
R[d] *= self.gdx[d]
|
|
|
|
if self.dim == 1:
|
|
numpy.absolute(R[0], R[0])
|
|
R = R[0]
|
|
|
|
elif self.dim == 2:
|
|
numpy.hypot(R[1], R[0], R[0])
|
|
R = R[0]
|
|
|
|
# half of the average grid spacing
|
|
zval = 0.5 * (self.gdx[0]+self.gdx[1]) / 2.0
|
|
R[self.gdims[0]-1, self.gdims[1]-1] = zval
|
|
|
|
elif self.dim == 3:
|
|
numpy.hypot(R[2], R[1], R[1])
|
|
numpy.hypot(R[1], R[0], R[0])
|
|
R = R[0]
|
|
|
|
# half of the average grid spacing
|
|
zval = 0.5 * (self.gdx[0]+self.gdx[1]+self.gdx[2]) / 3.0
|
|
R[self.gdims[0]-1, self.gdims[1]-1, self.gdims[2]-1] = zval
|
|
|
|
if verbose:
|
|
print( R.shape, R.min(), R.max())
|
|
mtimer.end()
|
|
|
|
return R
|
|
|
|
# --------------------------------------------------------------------------
|
|
# scale a function by point volumes for correct integration
|
|
def _scale(self, func):
|
|
|
|
if self.ptype == 'P':
|
|
return numpy.multiply(func, self.pvolumes)
|
|
|
|
elif self.ptype == 'G':
|
|
sfunc = func * numpy.prod(self.gdx)
|
|
|
|
'''
|
|
# volume of the boundary elements is only half the voxel
|
|
if self.dim == 1:
|
|
sfunc[ 0] *= 0.5
|
|
sfunc[-1] *= 0.5
|
|
elif self.dim == 2:
|
|
sfunc[ 0, :] *= 0.5
|
|
sfunc[-1, :] *= 0.5
|
|
sfunc[ :, 0] *= 0.5
|
|
sfunc[ :,-1] *= 0.5
|
|
elif self.dim == 3:
|
|
sfunc[ 0, :, :] *= 0.5
|
|
sfunc[-1, :, :] *= 0.5
|
|
sfunc[ :, 0, :] *= 0.5
|
|
sfunc[ :,-1, :] *= 0.5
|
|
sfunc[ :, :, 0] *= 0.5
|
|
sfunc[ :, :,-1] *= 0.5
|
|
'''
|
|
return sfunc
|
|
|
|
def prepare(self, verbose=False):
|
|
|
|
# ----------------------------------------------------------------------
|
|
gtimer = Timer()
|
|
if verbose:
|
|
print( '\nInitializing Poisson solver, type =', self.stype)
|
|
|
|
# ----------------------------------------------------------------------
|
|
# generate a grid to store pairwise distances to store Green's function
|
|
self.G = None # definition depends upon solver type
|
|
|
|
# ----------------------------------------------------------------------
|
|
# for Fourier solution,
|
|
if self.stype == 'F':
|
|
|
|
if self.ptype == 'G':
|
|
self.G = self._create_radialGrid(verbose)
|
|
|
|
elif self.ptype == 'P':
|
|
raise SyntaxError('Frequncy solver not supported for unstructured points')
|
|
#self.G = create_distArray()
|
|
|
|
# ----------------------------------------------------------------------
|
|
# for spatial solution,
|
|
# use the pairwise distance matrix, N x N
|
|
# where N is the number of points
|
|
elif self.stype == 'S':
|
|
|
|
if self.ptype == 'G':
|
|
raise SyntaxError('Spatial solver not suggested for structured grids')
|
|
#self.G = self.create_points(verbose)
|
|
|
|
elif self.ptype == 'P':
|
|
# for 1D points, add an extra axis
|
|
if (self.dim == 1) and (len(self.points.shape) == 1):
|
|
self.points = self.points[:,numpy.newaxis]
|
|
|
|
# create pairwise distance matrix now
|
|
ltimer = Timer()
|
|
self.G = spatial.distance.cdist(self.points, self.points)
|
|
|
|
# TODO: use a good approximation here
|
|
zval = numpy.power(10.0, -10.0)
|
|
self.G += zval * numpy.identity(self.G.shape[0])
|
|
|
|
if verbose:
|
|
print(' - created pairwise distance matrix:', self.G.shape, self.G.min(), self.G.max(), end = "")
|
|
ltimer.end()
|
|
|
|
# ----------------------------------------------------------------------
|
|
# compute the Green's function
|
|
ltimer = Timer()
|
|
if verbose:
|
|
print( ' - computing the Green\'s function:')
|
|
|
|
if self.dim == 1:
|
|
numpy.multiply(self.G, 0.5, self.G)
|
|
|
|
elif self.dim == 2:
|
|
numpy.log(self.G, self.G)
|
|
numpy.multiply(self.G, (0.5 / numpy.pi), self.G)
|
|
|
|
elif self.dim == 3:
|
|
numpy.reciprocal(self.G, self.G)
|
|
numpy.multiply(self.G, (-0.25 / numpy.pi), self.G)
|
|
|
|
if verbose:
|
|
print( self.G.shape, self.G.min(), self.G.max(), end = "")
|
|
ltimer.end()
|
|
|
|
# ----------------------------------------------------------------------
|
|
self.ready = True
|
|
|
|
if verbose:
|
|
print( 'Poisson solver initialized', end ='')
|
|
gtimer.end()
|
|
|
|
# --------------------------------------------------------------------------
|
|
# compute the integral solution
|
|
# --------------------------------------------------------------------------
|
|
def solve(self, f, verbose=False):
|
|
|
|
if not self.ready:
|
|
init(verbose)
|
|
|
|
fshape = f.shape
|
|
|
|
gtimer = Timer()
|
|
if verbose:
|
|
print( '\nSolving Poisson Eq.')
|
|
|
|
# ----------------------------------------------------------------------
|
|
# regular grid
|
|
if self.ptype == 'G':
|
|
|
|
#if (self.gdims - fshape).any():
|
|
if self.gdims != fshape:
|
|
print( self.gdims, fshape)
|
|
raise ValueError("Shape of function should match shape of grid")
|
|
|
|
# convolution in frequency domain
|
|
if self.stype == 'F':
|
|
p = signal.fftconvolve(f, self.G, mode='same')
|
|
numpy.multiply(p, numpy.prod(self.gdx), p)
|
|
|
|
# convolution in spatial domain
|
|
elif self.stype == 'S':
|
|
raise SyntaxError('Spatial solver not suggested for structured grids')
|
|
#p = (self.G).dot(f.reshape(-1)).reshape(fshape)
|
|
|
|
# ----------------------------------------------------------------------
|
|
# unstructured points
|
|
elif self.ptype == 'P':
|
|
|
|
if (self.points.shape[0] != fshape[0]) or (1 != len(fshape)):
|
|
raise ValueError("Shape of function should be (N,1) with N as the number of points")
|
|
|
|
# convolution in spatial domain
|
|
# Dec 13, 2017 -- pre scaling works for unstructured grids!
|
|
if self.stype == 'S':
|
|
p = numpy.multiply(f, self.pvolumes)
|
|
numpy.dot(self.G, p, p)
|
|
#numpy.multiply(p, self.pvolumes, p)
|
|
|
|
elif self.stype == 'F':
|
|
raise SyntaxError('Frequncy solver not supported for unstructured points')
|
|
|
|
# ----------------------------------------------------------------------
|
|
if verbose:
|
|
print( 'Poisson solver finished')
|
|
gtimer.end()
|
|
|
|
return p
|
|
|
|
# --------------------------------------------------------------------------
|
|
# GPU-accelerated solve using CuPy
|
|
# --------------------------------------------------------------------------
|
|
def solve_gpu(self, f, verbose=False):
|
|
"""
|
|
GPU-accelerated Poisson solver using CuPy.
|
|
Falls back to CPU if CuPy is not available.
|
|
|
|
Args:
|
|
f: Input field (numpy array)
|
|
verbose: Print timing information
|
|
|
|
Returns:
|
|
p: Solution (numpy array on CPU)
|
|
"""
|
|
if not CUPY_AVAILABLE:
|
|
if verbose:
|
|
print("CuPy not available, falling back to CPU solver")
|
|
return self.solve(f, verbose)
|
|
|
|
if not self.ready:
|
|
self.prepare(verbose)
|
|
|
|
fshape = f.shape
|
|
|
|
gtimer = Timer()
|
|
if verbose:
|
|
print('\nSolving Poisson Eq. on GPU')
|
|
|
|
# ----------------------------------------------------------------------
|
|
# regular grid
|
|
if self.ptype == 'G':
|
|
if self.gdims != fshape:
|
|
print(self.gdims, fshape)
|
|
raise ValueError("Shape of function should match shape of grid")
|
|
|
|
# convolution in frequency domain using GPU
|
|
if self.stype == 'F':
|
|
# Initialize GPU cache if needed
|
|
if self._gpu_cache is None:
|
|
if verbose:
|
|
print(' - Transferring Green\'s function to GPU...')
|
|
self._gpu_cache = {
|
|
'G_gpu': cp.asarray(self.G, dtype=cp.float32),
|
|
'gdx_prod': numpy.prod(self.gdx)
|
|
}
|
|
|
|
# Transfer input to GPU
|
|
f_gpu = cp.asarray(f, dtype=cp.float32)
|
|
|
|
# Perform FFT convolution on GPU
|
|
# CuPy's fftconvolve is GPU-accelerated
|
|
p_gpu = cp_signal.fftconvolve(f_gpu, self._gpu_cache['G_gpu'], mode='same')
|
|
|
|
# Scale by grid spacing (on GPU)
|
|
p_gpu *= self._gpu_cache['gdx_prod']
|
|
|
|
# Transfer result back to CPU
|
|
p = cp.asnumpy(p_gpu)
|
|
|
|
elif self.stype == 'S':
|
|
raise SyntaxError('Spatial solver not suggested for structured grids')
|
|
|
|
# ----------------------------------------------------------------------
|
|
# unstructured points - not optimized for GPU
|
|
elif self.ptype == 'P':
|
|
if verbose:
|
|
print(' - GPU solver not optimized for unstructured points, using CPU')
|
|
return self.solve(f, verbose)
|
|
|
|
# ----------------------------------------------------------------------
|
|
if verbose:
|
|
print('Poisson solver (GPU) finished')
|
|
gtimer.end()
|
|
|
|
return p
|
|
|
|
def solve_gpu_batch(self, f_list, verbose=False):
|
|
"""
|
|
Batch GPU-accelerated Poisson solver for multiple inputs.
|
|
Solves multiple Poisson equations in a single GPU call.
|
|
|
|
Args:
|
|
f_list: List of input fields (numpy arrays)
|
|
verbose: Print timing information
|
|
|
|
Returns:
|
|
List of solutions (numpy arrays on CPU)
|
|
"""
|
|
if not CUPY_AVAILABLE:
|
|
if verbose:
|
|
print("CuPy not available, falling back to CPU solver")
|
|
return [self.solve(f, verbose) for f in f_list]
|
|
|
|
if not self.ready:
|
|
self.prepare(verbose)
|
|
|
|
gtimer = Timer()
|
|
if verbose:
|
|
print(f'\nSolving {len(f_list)} Poisson Eq. on GPU (batched)')
|
|
|
|
# Verify all inputs have same shape
|
|
fshape = f_list[0].shape
|
|
for f in f_list:
|
|
if f.shape != fshape:
|
|
raise ValueError("All input fields must have the same shape for batch processing")
|
|
|
|
# ----------------------------------------------------------------------
|
|
# regular grid
|
|
if self.ptype == 'G':
|
|
if self.gdims != fshape:
|
|
raise ValueError("Shape of function should match shape of grid")
|
|
|
|
# convolution in frequency domain using GPU
|
|
if self.stype == 'F':
|
|
# Initialize GPU cache if needed
|
|
if self._gpu_cache is None:
|
|
if verbose:
|
|
print(' - Transferring Green\'s function to GPU...')
|
|
self._gpu_cache = {
|
|
'G_gpu': cp.asarray(self.G, dtype=cp.float32),
|
|
'gdx_prod': numpy.prod(self.gdx)
|
|
}
|
|
|
|
results = []
|
|
|
|
# Process all inputs in batch
|
|
for i, f in enumerate(f_list):
|
|
# Transfer input to GPU
|
|
f_gpu = cp.asarray(f, dtype=cp.float32)
|
|
|
|
# Perform FFT convolution on GPU
|
|
p_gpu = cp_signal.fftconvolve(f_gpu, self._gpu_cache['G_gpu'], mode='same')
|
|
|
|
# Scale by grid spacing (on GPU)
|
|
p_gpu *= self._gpu_cache['gdx_prod']
|
|
|
|
# Transfer result back to CPU
|
|
results.append(cp.asnumpy(p_gpu))
|
|
|
|
if verbose:
|
|
print(f' - Solved {i+1}/{len(f_list)}')
|
|
|
|
if verbose:
|
|
print('Poisson solver (GPU batch) finished')
|
|
gtimer.end()
|
|
|
|
return results
|
|
|
|
elif self.stype == 'S':
|
|
raise SyntaxError('Spatial solver not suggested for structured grids')
|
|
|
|
# ----------------------------------------------------------------------
|
|
# unstructured points - not optimized for GPU
|
|
elif self.ptype == 'P':
|
|
if verbose:
|
|
print(' - GPU solver not optimized for unstructured points, using CPU')
|
|
return [self.solve(f, verbose) for f in f_list]
|
|
|
|
return results
|
|
|
|
def clear_gpu_cache(self):
|
|
"""Clear GPU cache to free GPU memory"""
|
|
if self._gpu_cache is not None:
|
|
self._gpu_cache = None
|
|
if CUPY_AVAILABLE:
|
|
cp.get_default_memory_pool().free_all_blocks()
|
|
|
|
|