Module conduction.solver.conduction3d
Expand source code
try: range = xrange
except: pass
import numpy as np
from petsc4py import PETSc
from mpi4py import MPI
comm = MPI.COMM_WORLD
class Conduction3D(object):
"""
Implicit 3D steady-state heat equation solver over a structured grid using PETSc
"""
def __init__(self, minCoord, maxCoord, res):
minX, minY, minZ = tuple(minCoord)
maxX, maxY, maxZ = tuple(maxCoord)
resI, resJ, resK = tuple(res)
dm = PETSc.DMDA().create(dim=3, sizes=[resI, resJ, resK], stencil_width=1, comm=comm)
dm.setUniformCoordinates(minX, maxX, minY, maxY, minZ, maxZ)
self.dm = dm
self.lvec = dm.createLocalVector()
self.gvec = dm.createGlobalVector()
self.rhs = dm.createGlobalVector()
self.lgmap = dm.getLGMap()
# Setup matrix sizes
self.sizes = self.gvec.getSizes()
Nx, Ny, Nz = dm.getSizes()
N = Nx*Ny*Nz
dx = (maxX - minX)/(Nx - 1)
dy = (maxY - minY)/(Ny - 1)
dz = (maxZ - minZ)/(Nz - 1)
# include ghost nodes in local domain
(minI, maxI), (minJ, maxJ), (minK, maxK) = dm.getGhostRanges()
nx = maxI - minI
ny = maxJ - minJ
nz = maxK - minK
self.dx, self.dy, self.dz = dx, dy, dz
self.nx, self.ny, self.nz = nx, ny, nz
# local numbering
self.nodes = np.arange(0, nx*ny*nz, dtype=PETSc.IntType)
self._initialise_mesh_variables()
self._initialise_boundary_dictionary()
self._initialise_matrix()
def _initialise_mesh_variables(self):
# local coordinates
self.coords = self.dm.getCoordinatesLocal().array.reshape(-1,3)
minX, minY, minZ = self.coords.min(axis=0)
maxX, maxY, maxZ = self.coords.max(axis=0)
self.minX, self.maxX = minX, maxX
self.minY, self.maxY = minY, maxY
self.minZ, self.maxZ = minZ, maxZ
# thermal properties
self.diffusivity = None
self.heat_sources = None
def _initialise_boundary_dictionary(self):
# Setup boundary dictionary
self.bc = dict()
self.bc["maxY"] = {"val": 0.0, "delta": self.dy, "flux": True, "mask": self.coords[:,1]==self.maxY}
self.bc["minY"] = {"val": 0.0, "delta": self.dy, "flux": True, "mask": self.coords[:,1]==self.minY}
self.bc["minX"] = {"val": 0.0, "delta": self.dx, "flux": True, "mask": self.coords[:,0]==self.minX}
self.bc["maxX"] = {"val": 0.0, "delta": self.dx, "flux": True, "mask": self.coords[:,0]==self.maxX}
self.bc["minZ"] = {"val": 0.0, "delta": self.dz, "flux": True, "mask": self.coords[:,2]==self.minZ}
self.bc["maxZ"] = {"val": 0.0, "delta": self.dz, "flux": True, "mask": self.coords[:,2]==self.maxZ}
self.dirichlet_mask = np.zeros(self.nx*self.ny*self.nz, dtype=bool)
def _initialise_matrix(self):
# read into matrix
self.mat = PETSc.Mat().create(comm=comm)
self.mat.setType('aij')
self.mat.setSizes(self.sizes)
self.mat.setLGMap(self.lgmap)
self.mat.setFromOptions()
self.mat.setPreallocationNNZ((7,6))
def update_properties(self, diffusivity, heat_sources):
"""
Update diffusivity and heat sources
"""
self.diffusivity = np.asarray(diffusivity)
self.heat_sources = np.asarray(heat_sources)
def boundary_condition(self, wall, val, flux=True):
"""
Set the boundary conditions on each wall of the domain.
By default each wall is a Neumann (flux) condition.
If flux=True, positive val indicates a flux vector towards the centre
of the domain.
val can be a vector with the same number of elements as the wall
"""
# (minX, maxX), (minY, maxY), (minZ, maxZ)
if self.bc.has_key(str(wall)):
self.bc[str(wall)]["val"] = val
self.bc[str(wall)]["flux"] = flux
d = self.bc[str(wall)]
mask = d['mask']
if flux:
self.dirichlet_mask[mask] = False
self.bc[str(wall)]["val"] /= -d['delta']
else:
self.dirichlet_mask[mask] = True
else:
raise ValueError("Wall should be one of 'top', 'bottom', 'left', 'right'")
def construct_matrix(self, in_place=True, derivative=False):
"""
Construct the coefficient matrix
i.e. matrix A in Ax = b
We vectorise the 7-point stencil for fast matrix insertion.
An extra border of dummy values around the domain allows for automatic
Neumann (flux) boundary creation.
These are stomped on if there are any Dirichlet conditions.
"""
if in_place:
mat = self.mat
else:
mat = PETSc.Mat().create(comm=comm)
mat.setType('aij')
mat.setSizes(self.sizes)
mat.setLGMap(self.lgmap)
mat.setFromOptions()
mat.setPreallocationNNZ((7,comm.size-1))
nodes = self.nodes
nx, ny, nz = self.nx, self.ny, self.nz
n = nx*ny*nz
adx = 1.0/(2*self.dx**2)
ady = 1.0/(2*self.dy**2)
adz = 1.0/(2*self.dz**2)
u = self.diffusivity.reshape(nz,ny,nx)
k = np.zeros((nz+2, ny+2, nx+2))
k[1:-1,1:-1,1:-1] = u
index = np.empty((nz+2, ny+2, nx+2), dtype=PETSc.IntType)
index.fill(-1)
index[1:-1,1:-1,1:-1] = nodes.reshape(nz,ny,nx)
rows = np.empty((7,n), dtype=PETSc.IntType)
cols = np.empty((7,n), dtype=PETSc.IntType)
vals = np.empty((7,n))
dirichlet_mask = self.dirichlet_mask
closure = [(0,-2), (1,-1), (1,-1), (2,0), (1,-1), (1,-1), (1,-1)]
# N W F S E B C
delta = [ady, adx, adz, ady, adx, adz, 0.0]
for i in range(7):
rs, re = closure[i]
cs, ce = closure[-1+i]
ds, de = closure[-2+i]
rows[i] = nodes
cols[i] = index[ds:nz+de+2,rs:ny+re+2,cs:nx+ce+2].ravel()
vals[i] = delta[i]*(k[ds:nz+de+2,rs:ny+re+2,cs:nx+ce+2] + u).ravel()
# Dirichlet boundary conditions (duplicates are summed)
cols[:,dirichlet_mask] = nodes[dirichlet_mask]
vals[:,dirichlet_mask] = 0.0
# zero off-grid coordinates
vals[cols < 0] = 0.0
# centre point
vals[-1] = 0.0
if derivative:
vals[-1][dirichlet_mask] = 0.
else:
vals[-1][dirichlet_mask] = -1.0
row = rows.ravel()
col = cols.ravel()
val = vals.ravel()
# mask off-grid entries and sum duplicates
mask = col > -1
row, col, val = sum_duplicates(row[mask], col[mask], val[mask])
# indptr, col, val = coo_tocsr(row, col, val)
nnz = np.bincount(row)
indptr = np.insert(np.cumsum(nnz),0,0)
mat.assemblyBegin()
mat.setValuesLocalCSR(indptr.astype(PETSc.IntType), col, val)
mat.assemblyEnd()
# set diagonal vector
diag = mat.getRowSum()
diag.scale(-1.0)
mat.setDiagonal(diag)
return mat
def construct_rhs(self, in_place=True):
"""
Construct the right-hand-side vector
i.e. vector b in Ax = b
Boundary conditions are grabbed from the dictionary and
summed to the rhs.
Be careful of duplicate entries on the corners!!
"""
if in_place:
rhs = self.rhs
else:
rhs = self.gvec.duplicate()
vec = -1.0*self.heat_sources.copy()
for wall in self.bc:
val = self.bc[wall]['val']
flux = self.bc[wall]['flux']
mask = self.bc[wall]['mask']
if flux:
vec[mask] += val
else:
vec[mask] = val
self.lvec.setArray(vec)
self.dm.localToGlobal(self.lvec, rhs)
return rhs
def solve(self, solver='gmres'):
"""
Construct the matrix A and vector b in Ax = b
and solve for x
GMRES method is default
"""
matrix = self.construct_matrix()
rhs = self.construct_rhs()
res = self.dm.createGlobalVector()
ksp = PETSc.KSP().create(comm=comm)
ksp.setType(solver)
ksp.setOperators(matrix)
ksp.setFromOptions()
ksp.setTolerances(1e-10, 1e-50)
ksp.solve(rhs, res)
return res.array
def csr_tocoo(indptr, indices, data):
""" Convert from CSR to COO sparse matrix format """
d = np.diff(indptr)
I = np.repeat(np.arange(0,d.size,dtype='int32'), d)
return I, indices, data
def coo_tocsr(I, J, V):
""" Convert from COO to CSR sparse matrix format """
nnz = np.bincount(I)
indptr = np.insert(np.cumsum(nnz),0,0)
return indptr, J, V
def sum_duplicates(I, J, V):
"""
Sum all duplicate entries in the matrix
"""
order = np.lexsort((J, I))
I, J, V = I[order], J[order], V[order]
unique_mask = ((I[1:] != I[:-1]) |
(J[1:] != J[:-1]))
unique_mask = np.append(True, unique_mask)
unique_inds, = np.nonzero(unique_mask)
return I[unique_mask], J[unique_mask], np.add.reduceat(V, unique_inds)
Functions
def coo_tocsr(I, J, V)
-
Convert from COO to CSR sparse matrix format
Expand source code
def coo_tocsr(I, J, V): """ Convert from COO to CSR sparse matrix format """ nnz = np.bincount(I) indptr = np.insert(np.cumsum(nnz),0,0) return indptr, J, V
def csr_tocoo(indptr, indices, data)
-
Convert from CSR to COO sparse matrix format
Expand source code
def csr_tocoo(indptr, indices, data): """ Convert from CSR to COO sparse matrix format """ d = np.diff(indptr) I = np.repeat(np.arange(0,d.size,dtype='int32'), d) return I, indices, data
def sum_duplicates(I, J, V)
-
Sum all duplicate entries in the matrix
Expand source code
def sum_duplicates(I, J, V): """ Sum all duplicate entries in the matrix """ order = np.lexsort((J, I)) I, J, V = I[order], J[order], V[order] unique_mask = ((I[1:] != I[:-1]) | (J[1:] != J[:-1])) unique_mask = np.append(True, unique_mask) unique_inds, = np.nonzero(unique_mask) return I[unique_mask], J[unique_mask], np.add.reduceat(V, unique_inds)
Classes
class Conduction3D (minCoord, maxCoord, res)
-
Implicit 3D steady-state heat equation solver over a structured grid using PETSc
Expand source code
class Conduction3D(object): """ Implicit 3D steady-state heat equation solver over a structured grid using PETSc """ def __init__(self, minCoord, maxCoord, res): minX, minY, minZ = tuple(minCoord) maxX, maxY, maxZ = tuple(maxCoord) resI, resJ, resK = tuple(res) dm = PETSc.DMDA().create(dim=3, sizes=[resI, resJ, resK], stencil_width=1, comm=comm) dm.setUniformCoordinates(minX, maxX, minY, maxY, minZ, maxZ) self.dm = dm self.lvec = dm.createLocalVector() self.gvec = dm.createGlobalVector() self.rhs = dm.createGlobalVector() self.lgmap = dm.getLGMap() # Setup matrix sizes self.sizes = self.gvec.getSizes() Nx, Ny, Nz = dm.getSizes() N = Nx*Ny*Nz dx = (maxX - minX)/(Nx - 1) dy = (maxY - minY)/(Ny - 1) dz = (maxZ - minZ)/(Nz - 1) # include ghost nodes in local domain (minI, maxI), (minJ, maxJ), (minK, maxK) = dm.getGhostRanges() nx = maxI - minI ny = maxJ - minJ nz = maxK - minK self.dx, self.dy, self.dz = dx, dy, dz self.nx, self.ny, self.nz = nx, ny, nz # local numbering self.nodes = np.arange(0, nx*ny*nz, dtype=PETSc.IntType) self._initialise_mesh_variables() self._initialise_boundary_dictionary() self._initialise_matrix() def _initialise_mesh_variables(self): # local coordinates self.coords = self.dm.getCoordinatesLocal().array.reshape(-1,3) minX, minY, minZ = self.coords.min(axis=0) maxX, maxY, maxZ = self.coords.max(axis=0) self.minX, self.maxX = minX, maxX self.minY, self.maxY = minY, maxY self.minZ, self.maxZ = minZ, maxZ # thermal properties self.diffusivity = None self.heat_sources = None def _initialise_boundary_dictionary(self): # Setup boundary dictionary self.bc = dict() self.bc["maxY"] = {"val": 0.0, "delta": self.dy, "flux": True, "mask": self.coords[:,1]==self.maxY} self.bc["minY"] = {"val": 0.0, "delta": self.dy, "flux": True, "mask": self.coords[:,1]==self.minY} self.bc["minX"] = {"val": 0.0, "delta": self.dx, "flux": True, "mask": self.coords[:,0]==self.minX} self.bc["maxX"] = {"val": 0.0, "delta": self.dx, "flux": True, "mask": self.coords[:,0]==self.maxX} self.bc["minZ"] = {"val": 0.0, "delta": self.dz, "flux": True, "mask": self.coords[:,2]==self.minZ} self.bc["maxZ"] = {"val": 0.0, "delta": self.dz, "flux": True, "mask": self.coords[:,2]==self.maxZ} self.dirichlet_mask = np.zeros(self.nx*self.ny*self.nz, dtype=bool) def _initialise_matrix(self): # read into matrix self.mat = PETSc.Mat().create(comm=comm) self.mat.setType('aij') self.mat.setSizes(self.sizes) self.mat.setLGMap(self.lgmap) self.mat.setFromOptions() self.mat.setPreallocationNNZ((7,6)) def update_properties(self, diffusivity, heat_sources): """ Update diffusivity and heat sources """ self.diffusivity = np.asarray(diffusivity) self.heat_sources = np.asarray(heat_sources) def boundary_condition(self, wall, val, flux=True): """ Set the boundary conditions on each wall of the domain. By default each wall is a Neumann (flux) condition. If flux=True, positive val indicates a flux vector towards the centre of the domain. val can be a vector with the same number of elements as the wall """ # (minX, maxX), (minY, maxY), (minZ, maxZ) if self.bc.has_key(str(wall)): self.bc[str(wall)]["val"] = val self.bc[str(wall)]["flux"] = flux d = self.bc[str(wall)] mask = d['mask'] if flux: self.dirichlet_mask[mask] = False self.bc[str(wall)]["val"] /= -d['delta'] else: self.dirichlet_mask[mask] = True else: raise ValueError("Wall should be one of 'top', 'bottom', 'left', 'right'") def construct_matrix(self, in_place=True, derivative=False): """ Construct the coefficient matrix i.e. matrix A in Ax = b We vectorise the 7-point stencil for fast matrix insertion. An extra border of dummy values around the domain allows for automatic Neumann (flux) boundary creation. These are stomped on if there are any Dirichlet conditions. """ if in_place: mat = self.mat else: mat = PETSc.Mat().create(comm=comm) mat.setType('aij') mat.setSizes(self.sizes) mat.setLGMap(self.lgmap) mat.setFromOptions() mat.setPreallocationNNZ((7,comm.size-1)) nodes = self.nodes nx, ny, nz = self.nx, self.ny, self.nz n = nx*ny*nz adx = 1.0/(2*self.dx**2) ady = 1.0/(2*self.dy**2) adz = 1.0/(2*self.dz**2) u = self.diffusivity.reshape(nz,ny,nx) k = np.zeros((nz+2, ny+2, nx+2)) k[1:-1,1:-1,1:-1] = u index = np.empty((nz+2, ny+2, nx+2), dtype=PETSc.IntType) index.fill(-1) index[1:-1,1:-1,1:-1] = nodes.reshape(nz,ny,nx) rows = np.empty((7,n), dtype=PETSc.IntType) cols = np.empty((7,n), dtype=PETSc.IntType) vals = np.empty((7,n)) dirichlet_mask = self.dirichlet_mask closure = [(0,-2), (1,-1), (1,-1), (2,0), (1,-1), (1,-1), (1,-1)] # N W F S E B C delta = [ady, adx, adz, ady, adx, adz, 0.0] for i in range(7): rs, re = closure[i] cs, ce = closure[-1+i] ds, de = closure[-2+i] rows[i] = nodes cols[i] = index[ds:nz+de+2,rs:ny+re+2,cs:nx+ce+2].ravel() vals[i] = delta[i]*(k[ds:nz+de+2,rs:ny+re+2,cs:nx+ce+2] + u).ravel() # Dirichlet boundary conditions (duplicates are summed) cols[:,dirichlet_mask] = nodes[dirichlet_mask] vals[:,dirichlet_mask] = 0.0 # zero off-grid coordinates vals[cols < 0] = 0.0 # centre point vals[-1] = 0.0 if derivative: vals[-1][dirichlet_mask] = 0. else: vals[-1][dirichlet_mask] = -1.0 row = rows.ravel() col = cols.ravel() val = vals.ravel() # mask off-grid entries and sum duplicates mask = col > -1 row, col, val = sum_duplicates(row[mask], col[mask], val[mask]) # indptr, col, val = coo_tocsr(row, col, val) nnz = np.bincount(row) indptr = np.insert(np.cumsum(nnz),0,0) mat.assemblyBegin() mat.setValuesLocalCSR(indptr.astype(PETSc.IntType), col, val) mat.assemblyEnd() # set diagonal vector diag = mat.getRowSum() diag.scale(-1.0) mat.setDiagonal(diag) return mat def construct_rhs(self, in_place=True): """ Construct the right-hand-side vector i.e. vector b in Ax = b Boundary conditions are grabbed from the dictionary and summed to the rhs. Be careful of duplicate entries on the corners!! """ if in_place: rhs = self.rhs else: rhs = self.gvec.duplicate() vec = -1.0*self.heat_sources.copy() for wall in self.bc: val = self.bc[wall]['val'] flux = self.bc[wall]['flux'] mask = self.bc[wall]['mask'] if flux: vec[mask] += val else: vec[mask] = val self.lvec.setArray(vec) self.dm.localToGlobal(self.lvec, rhs) return rhs def solve(self, solver='gmres'): """ Construct the matrix A and vector b in Ax = b and solve for x GMRES method is default """ matrix = self.construct_matrix() rhs = self.construct_rhs() res = self.dm.createGlobalVector() ksp = PETSc.KSP().create(comm=comm) ksp.setType(solver) ksp.setOperators(matrix) ksp.setFromOptions() ksp.setTolerances(1e-10, 1e-50) ksp.solve(rhs, res) return res.array
Methods
def boundary_condition(self, wall, val, flux=True)
-
Set the boundary conditions on each wall of the domain. By default each wall is a Neumann (flux) condition. If flux=True, positive val indicates a flux vector towards the centre of the domain.
val can be a vector with the same number of elements as the wall
Expand source code
def boundary_condition(self, wall, val, flux=True): """ Set the boundary conditions on each wall of the domain. By default each wall is a Neumann (flux) condition. If flux=True, positive val indicates a flux vector towards the centre of the domain. val can be a vector with the same number of elements as the wall """ # (minX, maxX), (minY, maxY), (minZ, maxZ) if self.bc.has_key(str(wall)): self.bc[str(wall)]["val"] = val self.bc[str(wall)]["flux"] = flux d = self.bc[str(wall)] mask = d['mask'] if flux: self.dirichlet_mask[mask] = False self.bc[str(wall)]["val"] /= -d['delta'] else: self.dirichlet_mask[mask] = True else: raise ValueError("Wall should be one of 'top', 'bottom', 'left', 'right'")
def construct_matrix(self, in_place=True, derivative=False)
-
Construct the coefficient matrix i.e. matrix A in Ax = b
We vectorise the 7-point stencil for fast matrix insertion. An extra border of dummy values around the domain allows for automatic Neumann (flux) boundary creation. These are stomped on if there are any Dirichlet conditions.
Expand source code
def construct_matrix(self, in_place=True, derivative=False): """ Construct the coefficient matrix i.e. matrix A in Ax = b We vectorise the 7-point stencil for fast matrix insertion. An extra border of dummy values around the domain allows for automatic Neumann (flux) boundary creation. These are stomped on if there are any Dirichlet conditions. """ if in_place: mat = self.mat else: mat = PETSc.Mat().create(comm=comm) mat.setType('aij') mat.setSizes(self.sizes) mat.setLGMap(self.lgmap) mat.setFromOptions() mat.setPreallocationNNZ((7,comm.size-1)) nodes = self.nodes nx, ny, nz = self.nx, self.ny, self.nz n = nx*ny*nz adx = 1.0/(2*self.dx**2) ady = 1.0/(2*self.dy**2) adz = 1.0/(2*self.dz**2) u = self.diffusivity.reshape(nz,ny,nx) k = np.zeros((nz+2, ny+2, nx+2)) k[1:-1,1:-1,1:-1] = u index = np.empty((nz+2, ny+2, nx+2), dtype=PETSc.IntType) index.fill(-1) index[1:-1,1:-1,1:-1] = nodes.reshape(nz,ny,nx) rows = np.empty((7,n), dtype=PETSc.IntType) cols = np.empty((7,n), dtype=PETSc.IntType) vals = np.empty((7,n)) dirichlet_mask = self.dirichlet_mask closure = [(0,-2), (1,-1), (1,-1), (2,0), (1,-1), (1,-1), (1,-1)] # N W F S E B C delta = [ady, adx, adz, ady, adx, adz, 0.0] for i in range(7): rs, re = closure[i] cs, ce = closure[-1+i] ds, de = closure[-2+i] rows[i] = nodes cols[i] = index[ds:nz+de+2,rs:ny+re+2,cs:nx+ce+2].ravel() vals[i] = delta[i]*(k[ds:nz+de+2,rs:ny+re+2,cs:nx+ce+2] + u).ravel() # Dirichlet boundary conditions (duplicates are summed) cols[:,dirichlet_mask] = nodes[dirichlet_mask] vals[:,dirichlet_mask] = 0.0 # zero off-grid coordinates vals[cols < 0] = 0.0 # centre point vals[-1] = 0.0 if derivative: vals[-1][dirichlet_mask] = 0. else: vals[-1][dirichlet_mask] = -1.0 row = rows.ravel() col = cols.ravel() val = vals.ravel() # mask off-grid entries and sum duplicates mask = col > -1 row, col, val = sum_duplicates(row[mask], col[mask], val[mask]) # indptr, col, val = coo_tocsr(row, col, val) nnz = np.bincount(row) indptr = np.insert(np.cumsum(nnz),0,0) mat.assemblyBegin() mat.setValuesLocalCSR(indptr.astype(PETSc.IntType), col, val) mat.assemblyEnd() # set diagonal vector diag = mat.getRowSum() diag.scale(-1.0) mat.setDiagonal(diag) return mat
def construct_rhs(self, in_place=True)
-
Construct the right-hand-side vector i.e. vector b in Ax = b
Boundary conditions are grabbed from the dictionary and summed to the rhs. Be careful of duplicate entries on the corners!!
Expand source code
def construct_rhs(self, in_place=True): """ Construct the right-hand-side vector i.e. vector b in Ax = b Boundary conditions are grabbed from the dictionary and summed to the rhs. Be careful of duplicate entries on the corners!! """ if in_place: rhs = self.rhs else: rhs = self.gvec.duplicate() vec = -1.0*self.heat_sources.copy() for wall in self.bc: val = self.bc[wall]['val'] flux = self.bc[wall]['flux'] mask = self.bc[wall]['mask'] if flux: vec[mask] += val else: vec[mask] = val self.lvec.setArray(vec) self.dm.localToGlobal(self.lvec, rhs) return rhs
def solve(self, solver='gmres')
-
Construct the matrix A and vector b in Ax = b and solve for x
GMRES method is default
Expand source code
def solve(self, solver='gmres'): """ Construct the matrix A and vector b in Ax = b and solve for x GMRES method is default """ matrix = self.construct_matrix() rhs = self.construct_rhs() res = self.dm.createGlobalVector() ksp = PETSc.KSP().create(comm=comm) ksp.setType(solver) ksp.setOperators(matrix) ksp.setFromOptions() ksp.setTolerances(1e-10, 1e-50) ksp.solve(rhs, res) return res.array
def update_properties(self, diffusivity, heat_sources)
-
Update diffusivity and heat sources
Expand source code
def update_properties(self, diffusivity, heat_sources): """ Update diffusivity and heat sources """ self.diffusivity = np.asarray(diffusivity) self.heat_sources = np.asarray(heat_sources)