Module conduction.solver.conductionNd_serial

Copyright 2017 Ben Mather

This file is part of Conduction https://git.dias.ie/itherc/conduction/

Conduction is free software: you can redistribute it and/or modify it under the terms of the GNU Lesser General Public License as published by the Free Software Foundation, either version 3 of the License, or any later version.

Conduction is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU Lesser General Public License for more details.

You should have received a copy of the GNU Lesser General Public License along with Conduction. If not, see http://www.gnu.org/licenses/.

Expand source code
"""
Copyright 2017 Ben Mather

This file is part of Conduction <https://git.dias.ie/itherc/conduction/>

Conduction is free software: you can redistribute it and/or modify
it under the terms of the GNU Lesser General Public License as published by
the Free Software Foundation, either version 3 of the License, or any later version.

Conduction is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
GNU Lesser General Public License for more details.

You should have received a copy of the GNU Lesser General Public License
along with Conduction.  If not, see <http://www.gnu.org/licenses/>.
"""

try: range = xrange
except: pass

import numpy as np
from scipy import sparse
from scipy.sparse import linalg

class ConductionND(object):
    """
    Implicit ND steady-state heat equation solver over a structured grid
    (Serial version)
    """

    def __init__(self, minCoord, maxCoord, res, **kwargs):

        dim = len(res)
        extent = np.zeros(dim*2)

        index = 0
        sizes = 1

        bbox  = list(range(dim))
        n = np.zeros(dim, dtype=np.int32)

        width = kwargs.pop('stencil_width', 1)

        for i in range(0, dim):
            extent[index]   = minCoord[i]
            extent[index+1] = maxCoord[i]
            index += 2
            sizes *= res[i]
            bbox[i] = (minCoord[i], maxCoord[i])
            n[i] = res[i]

        # Setup matrix sizes
        self.sizes = (sizes, sizes)
        self.dim = dim
        self.extent = extent
        self.bbox = bbox

        nn = sizes
        self.n = n[::-1]
        self.nn = nn
        self.npoints = nn

        # stencil size
        self.width = width
        self.stencil_width = 2*dim*width + 1

        # local numbering
        self.nodes = np.arange(0, nn, dtype=np.int32)

        # closure depends on dim
        if dim == 1:
            closure = [(0,-2),(2,0),(1,-1)]
        elif dim == 2:
            closure = [(0,-2), (1,-1), (2,0), (1,-1), (1,-1)]
        elif dim == 3:
            closure = [(0,-2), (1,-1), (1,-1), (2,0), (1,-1), (1,-1), (1,-1)]
        self.closure = self._create_closure_object(closure)


        # interior slices
        self.interior_slice = [None]*dim
        for i in range(0, dim):
            self.interior_slice[i] = slice(1, -1)


        self._initialise_mesh_variables()
        self._initialise_boundary_dictionary()
        self._initialise_COO_vectors()

        # thermal properties
        self.diffusivity  = np.zeros(nn)
        self.heat_sources = np.zeros(nn)
        self.temperature  = np.zeros(nn)

        # right hand side vector
        self.rhs = np.zeros(nn)


    def _initialise_COO_vectors(self):

        nn = self.nn
        n = self.n

        index = np.empty(n + 2, dtype=np.int32)
        index.fill(-1)
        index[self.interior_slice] = self.nodes.reshape(n)
        self.index = index

        self.rows = np.empty((self.stencil_width, nn), dtype=np.int32)
        self.cols = np.empty((self.stencil_width, nn), dtype=np.int32)
        self.vals = np.empty((self.stencil_width, nn))



    def _initialise_mesh_variables(self):

        dim = self.dim
        bbox = self.bbox
        n = self.n[::-1]

        extent = np.zeros(dim*2)

        index = 0
        for bs, be in bbox:
            extent[index]   = bs
            extent[index+1] = be
            index += 2

        self.extent = extent

        # local coordinates
        grid_coords = [None]*dim
        for i in range(0, dim):
            minI, maxI = bbox[i]
            size = n[i]
            grid_coords[i] = np.linspace(minI, maxI, size)


        coord_arrays = np.meshgrid(*grid_coords[::-1], indexing='ij')
        coords = np.empty((self.nn, dim))
        for i in range(0, dim):
            coords[:,i] = coord_arrays[::-1][i].ravel()

        self.grid_coords = grid_coords
        self.coords = coords


    def _initialise_boundary_dictionary(self):

        coords = self.coords
        grid_coords = self.grid_coords
        dim = self.dim

        minCoords = coords.min(axis=0)
        maxCoords = coords.max(axis=0)

        bbox = self.bbox
        n = self.n[::-1]

        # Setup boundary dictionary
        bc = dict()

        wall = [("minX", "maxX"), ("minY", "maxY"), ("minZ", "maxZ")]

        for i in range(0, dim):
            w0, w1 = wall[i]
            c0, c1 = bbox[i]
            m0, m1 = self.coords[:,i] == c0, self.coords[:,i] == c1
            d0 = d1 = (c1 - c0)/(n[i] - 1)

            bc[w0] = {"val": 0.0, "delta": d0, "flux": True, "mask": m0}
            bc[w1] = {"val": 0.0, "delta": d1, "flux": True, "mask": m1}

        self.bc = bc
        self.dirichlet_mask = np.zeros(self.nn, dtype=bool)


    def _create_closure_object(self, closure):

        n = self.n
        obj = [[0] * self.dim for i in range(self.stencil_width)]

        for i in range(0, self.stencil_width):
            # construct slicing object
            for j in range(0, self.dim):
                start, end = closure[i-j]
                obj[i][j] = slice(start, n[j]+end+2)

        return obj


    def update_properties(self, diffusivity, heat_sources):
        """
        Update diffusivity and heat sources
        """
        self.diffusivity = diffusivity
        self.heat_sources = 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
        """
        wall = str(wall)

        if wall in self.bc:
            self.bc[wall]["val"]  = np.array(val, copy=True)
            self.bc[wall]["flux"] = flux
            d = self.bc[wall]

            mask = d['mask']

            if flux:
                self.dirichlet_mask[mask] = False
                self.bc[wall]["val"] /= -d['delta']
            else:
                self.dirichlet_mask[mask] = True

        else:
            raise ValueError("Wall should be one of {}".format(self.bc.keys()))



    def construct_matrix(self, 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.

        """

        nodes = self.nodes
        nn = self.nn
        n = self.n
        dim = self.dim

        index = self.index

        rows = self.rows
        cols = self.cols
        vals = self.vals

        dirichlet_mask = self.dirichlet_mask

        u = self.diffusivity.reshape(n)

        k = np.zeros(n + 2)
        k[self.interior_slice] = u

        for i in range(0, self.stencil_width):
            obj = self.closure[i]

            rows[i] = nodes
            cols[i] = index[obj].ravel()

            distance = np.linalg.norm(self.coords[cols[i]] - self.coords, axis=1)
            distance[distance==0] = 1e-12 # protect against dividing by zero
            delta = 1.0/(2.0*distance**2)

            vals[i] = delta*(k[obj] + 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 >= 0
        row = row[mask]
        col = col[mask]
        val = val[mask]

        mat = sparse.coo_matrix((val, (row, col)), shape=self.sizes).tocsr()
        mat.sum_duplicates()
        diag = np.ravel(mat.sum(axis=1))
        diag *= -1
        mat.setdiag(diag)

        return mat


    def construct_rhs(self):
        """
        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!!
        """
        
        rhs = -1.0*self.heat_sources

        for wall in self.bc:
            val  = self.bc[wall]['val']
            flux = self.bc[wall]['flux']
            mask = self.bc[wall]['mask']
            if flux:
                rhs[mask] += val
            else:
                rhs[mask] = val

        return rhs


    def solve(self, matrix=None, rhs=None):
        """
        Construct the matrix A and vector b in Ax = b
        and solve for x

        GMRES method is default
        """
        if matrix is None:
            matrix = self.construct_matrix()
        if rhs is None:
            rhs = self.construct_rhs()
        res = self.temperature

        T = linalg.spsolve(matrix, rhs)
        self.temperature = T

        return T


    def gradient(self, vector, **kwargs):

        return np.gradient(vector.reshape(self.n), *self.grid_coords[::-1], **kwargs)


    def heatflux(self):

        T = self.temperature
        k = self.diffusivity * -1
        divT = self.gradient(T)
        for i in range(0, self.dim):
            div = k*divT[i].ravel()
            divT[i] = div

        return divT


    def isosurface(self, vector, isoval, axis=0, interp='nearest'):
        """
        Calculate an isosurface along a given axis
        (So far this is only working for axis=0)

        Parameters
        ----------
         vector : array, the same size as the mesh (n,)
         isoval : float, isosurface value
         axis   : int, axis to generate the isosurface
         interp : str, method can be either
            'nearest' - nearest neighbour interpolation
            'linear'  - linear interpolation
        
        Returns
        -------
         z_interp : isosurface the same size as the specified axis
        """
        Vcube = vector.reshape(self.n)
        Zcube = self.coords[:,::-1][:,axis].reshape(self.n)
        sort_idx = ((Vcube - isoval)**2).argsort(axis=axis)    
        i0 = sort_idx[0]
        # z0 = Zcube.take(i0)
        
        obj = []
        for d in range(0, self.dim):
            obj.append( slice(0, self.n[d]) )
        obj.pop(axis)
        
        idx = list(np.mgrid[obj])
        idx.insert(axis, i0)
        z0 = Zcube[idx]

        if interp == 'linear':
            v0 = Vcube[idx]
            
            # identify next nearest node
            i1 = sort_idx[1]
            idx[axis] = i1
            z1 = Zcube[idx]
            v1 = Vcube[idx]

            vmin = np.minimum(v0, v1)
            vmax = np.maximum(v0, v1)
            ratio = np.vstack([np.ones_like(vmax)*isoval, vmin, vmax])
            ratio -= ratio.min(axis=0)
            ratio /= ratio.max(axis=0)
            z_interp = ratio[0]*z1 + (1.0 - ratio[0])*z0
            return z_interp
        elif interp == 'nearest':
            return z0

Classes

class ConductionND (minCoord, maxCoord, res, **kwargs)

Implicit ND steady-state heat equation solver over a structured grid (Serial version)

Expand source code
class ConductionND(object):
    """
    Implicit ND steady-state heat equation solver over a structured grid
    (Serial version)
    """

    def __init__(self, minCoord, maxCoord, res, **kwargs):

        dim = len(res)
        extent = np.zeros(dim*2)

        index = 0
        sizes = 1

        bbox  = list(range(dim))
        n = np.zeros(dim, dtype=np.int32)

        width = kwargs.pop('stencil_width', 1)

        for i in range(0, dim):
            extent[index]   = minCoord[i]
            extent[index+1] = maxCoord[i]
            index += 2
            sizes *= res[i]
            bbox[i] = (minCoord[i], maxCoord[i])
            n[i] = res[i]

        # Setup matrix sizes
        self.sizes = (sizes, sizes)
        self.dim = dim
        self.extent = extent
        self.bbox = bbox

        nn = sizes
        self.n = n[::-1]
        self.nn = nn
        self.npoints = nn

        # stencil size
        self.width = width
        self.stencil_width = 2*dim*width + 1

        # local numbering
        self.nodes = np.arange(0, nn, dtype=np.int32)

        # closure depends on dim
        if dim == 1:
            closure = [(0,-2),(2,0),(1,-1)]
        elif dim == 2:
            closure = [(0,-2), (1,-1), (2,0), (1,-1), (1,-1)]
        elif dim == 3:
            closure = [(0,-2), (1,-1), (1,-1), (2,0), (1,-1), (1,-1), (1,-1)]
        self.closure = self._create_closure_object(closure)


        # interior slices
        self.interior_slice = [None]*dim
        for i in range(0, dim):
            self.interior_slice[i] = slice(1, -1)


        self._initialise_mesh_variables()
        self._initialise_boundary_dictionary()
        self._initialise_COO_vectors()

        # thermal properties
        self.diffusivity  = np.zeros(nn)
        self.heat_sources = np.zeros(nn)
        self.temperature  = np.zeros(nn)

        # right hand side vector
        self.rhs = np.zeros(nn)


    def _initialise_COO_vectors(self):

        nn = self.nn
        n = self.n

        index = np.empty(n + 2, dtype=np.int32)
        index.fill(-1)
        index[self.interior_slice] = self.nodes.reshape(n)
        self.index = index

        self.rows = np.empty((self.stencil_width, nn), dtype=np.int32)
        self.cols = np.empty((self.stencil_width, nn), dtype=np.int32)
        self.vals = np.empty((self.stencil_width, nn))



    def _initialise_mesh_variables(self):

        dim = self.dim
        bbox = self.bbox
        n = self.n[::-1]

        extent = np.zeros(dim*2)

        index = 0
        for bs, be in bbox:
            extent[index]   = bs
            extent[index+1] = be
            index += 2

        self.extent = extent

        # local coordinates
        grid_coords = [None]*dim
        for i in range(0, dim):
            minI, maxI = bbox[i]
            size = n[i]
            grid_coords[i] = np.linspace(minI, maxI, size)


        coord_arrays = np.meshgrid(*grid_coords[::-1], indexing='ij')
        coords = np.empty((self.nn, dim))
        for i in range(0, dim):
            coords[:,i] = coord_arrays[::-1][i].ravel()

        self.grid_coords = grid_coords
        self.coords = coords


    def _initialise_boundary_dictionary(self):

        coords = self.coords
        grid_coords = self.grid_coords
        dim = self.dim

        minCoords = coords.min(axis=0)
        maxCoords = coords.max(axis=0)

        bbox = self.bbox
        n = self.n[::-1]

        # Setup boundary dictionary
        bc = dict()

        wall = [("minX", "maxX"), ("minY", "maxY"), ("minZ", "maxZ")]

        for i in range(0, dim):
            w0, w1 = wall[i]
            c0, c1 = bbox[i]
            m0, m1 = self.coords[:,i] == c0, self.coords[:,i] == c1
            d0 = d1 = (c1 - c0)/(n[i] - 1)

            bc[w0] = {"val": 0.0, "delta": d0, "flux": True, "mask": m0}
            bc[w1] = {"val": 0.0, "delta": d1, "flux": True, "mask": m1}

        self.bc = bc
        self.dirichlet_mask = np.zeros(self.nn, dtype=bool)


    def _create_closure_object(self, closure):

        n = self.n
        obj = [[0] * self.dim for i in range(self.stencil_width)]

        for i in range(0, self.stencil_width):
            # construct slicing object
            for j in range(0, self.dim):
                start, end = closure[i-j]
                obj[i][j] = slice(start, n[j]+end+2)

        return obj


    def update_properties(self, diffusivity, heat_sources):
        """
        Update diffusivity and heat sources
        """
        self.diffusivity = diffusivity
        self.heat_sources = 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
        """
        wall = str(wall)

        if wall in self.bc:
            self.bc[wall]["val"]  = np.array(val, copy=True)
            self.bc[wall]["flux"] = flux
            d = self.bc[wall]

            mask = d['mask']

            if flux:
                self.dirichlet_mask[mask] = False
                self.bc[wall]["val"] /= -d['delta']
            else:
                self.dirichlet_mask[mask] = True

        else:
            raise ValueError("Wall should be one of {}".format(self.bc.keys()))



    def construct_matrix(self, 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.

        """

        nodes = self.nodes
        nn = self.nn
        n = self.n
        dim = self.dim

        index = self.index

        rows = self.rows
        cols = self.cols
        vals = self.vals

        dirichlet_mask = self.dirichlet_mask

        u = self.diffusivity.reshape(n)

        k = np.zeros(n + 2)
        k[self.interior_slice] = u

        for i in range(0, self.stencil_width):
            obj = self.closure[i]

            rows[i] = nodes
            cols[i] = index[obj].ravel()

            distance = np.linalg.norm(self.coords[cols[i]] - self.coords, axis=1)
            distance[distance==0] = 1e-12 # protect against dividing by zero
            delta = 1.0/(2.0*distance**2)

            vals[i] = delta*(k[obj] + 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 >= 0
        row = row[mask]
        col = col[mask]
        val = val[mask]

        mat = sparse.coo_matrix((val, (row, col)), shape=self.sizes).tocsr()
        mat.sum_duplicates()
        diag = np.ravel(mat.sum(axis=1))
        diag *= -1
        mat.setdiag(diag)

        return mat


    def construct_rhs(self):
        """
        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!!
        """
        
        rhs = -1.0*self.heat_sources

        for wall in self.bc:
            val  = self.bc[wall]['val']
            flux = self.bc[wall]['flux']
            mask = self.bc[wall]['mask']
            if flux:
                rhs[mask] += val
            else:
                rhs[mask] = val

        return rhs


    def solve(self, matrix=None, rhs=None):
        """
        Construct the matrix A and vector b in Ax = b
        and solve for x

        GMRES method is default
        """
        if matrix is None:
            matrix = self.construct_matrix()
        if rhs is None:
            rhs = self.construct_rhs()
        res = self.temperature

        T = linalg.spsolve(matrix, rhs)
        self.temperature = T

        return T


    def gradient(self, vector, **kwargs):

        return np.gradient(vector.reshape(self.n), *self.grid_coords[::-1], **kwargs)


    def heatflux(self):

        T = self.temperature
        k = self.diffusivity * -1
        divT = self.gradient(T)
        for i in range(0, self.dim):
            div = k*divT[i].ravel()
            divT[i] = div

        return divT


    def isosurface(self, vector, isoval, axis=0, interp='nearest'):
        """
        Calculate an isosurface along a given axis
        (So far this is only working for axis=0)

        Parameters
        ----------
         vector : array, the same size as the mesh (n,)
         isoval : float, isosurface value
         axis   : int, axis to generate the isosurface
         interp : str, method can be either
            'nearest' - nearest neighbour interpolation
            'linear'  - linear interpolation
        
        Returns
        -------
         z_interp : isosurface the same size as the specified axis
        """
        Vcube = vector.reshape(self.n)
        Zcube = self.coords[:,::-1][:,axis].reshape(self.n)
        sort_idx = ((Vcube - isoval)**2).argsort(axis=axis)    
        i0 = sort_idx[0]
        # z0 = Zcube.take(i0)
        
        obj = []
        for d in range(0, self.dim):
            obj.append( slice(0, self.n[d]) )
        obj.pop(axis)
        
        idx = list(np.mgrid[obj])
        idx.insert(axis, i0)
        z0 = Zcube[idx]

        if interp == 'linear':
            v0 = Vcube[idx]
            
            # identify next nearest node
            i1 = sort_idx[1]
            idx[axis] = i1
            z1 = Zcube[idx]
            v1 = Vcube[idx]

            vmin = np.minimum(v0, v1)
            vmax = np.maximum(v0, v1)
            ratio = np.vstack([np.ones_like(vmax)*isoval, vmin, vmax])
            ratio -= ratio.min(axis=0)
            ratio /= ratio.max(axis=0)
            z_interp = ratio[0]*z1 + (1.0 - ratio[0])*z0
            return z_interp
        elif interp == 'nearest':
            return z0

Subclasses

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
    """
    wall = str(wall)

    if wall in self.bc:
        self.bc[wall]["val"]  = np.array(val, copy=True)
        self.bc[wall]["flux"] = flux
        d = self.bc[wall]

        mask = d['mask']

        if flux:
            self.dirichlet_mask[mask] = False
            self.bc[wall]["val"] /= -d['delta']
        else:
            self.dirichlet_mask[mask] = True

    else:
        raise ValueError("Wall should be one of {}".format(self.bc.keys()))
def construct_matrix(self, 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, 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.

    """

    nodes = self.nodes
    nn = self.nn
    n = self.n
    dim = self.dim

    index = self.index

    rows = self.rows
    cols = self.cols
    vals = self.vals

    dirichlet_mask = self.dirichlet_mask

    u = self.diffusivity.reshape(n)

    k = np.zeros(n + 2)
    k[self.interior_slice] = u

    for i in range(0, self.stencil_width):
        obj = self.closure[i]

        rows[i] = nodes
        cols[i] = index[obj].ravel()

        distance = np.linalg.norm(self.coords[cols[i]] - self.coords, axis=1)
        distance[distance==0] = 1e-12 # protect against dividing by zero
        delta = 1.0/(2.0*distance**2)

        vals[i] = delta*(k[obj] + 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 >= 0
    row = row[mask]
    col = col[mask]
    val = val[mask]

    mat = sparse.coo_matrix((val, (row, col)), shape=self.sizes).tocsr()
    mat.sum_duplicates()
    diag = np.ravel(mat.sum(axis=1))
    diag *= -1
    mat.setdiag(diag)

    return mat
def construct_rhs(self)

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):
    """
    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!!
    """
    
    rhs = -1.0*self.heat_sources

    for wall in self.bc:
        val  = self.bc[wall]['val']
        flux = self.bc[wall]['flux']
        mask = self.bc[wall]['mask']
        if flux:
            rhs[mask] += val
        else:
            rhs[mask] = val

    return rhs
def gradient(self, vector, **kwargs)
Expand source code
def gradient(self, vector, **kwargs):

    return np.gradient(vector.reshape(self.n), *self.grid_coords[::-1], **kwargs)
def heatflux(self)
Expand source code
def heatflux(self):

    T = self.temperature
    k = self.diffusivity * -1
    divT = self.gradient(T)
    for i in range(0, self.dim):
        div = k*divT[i].ravel()
        divT[i] = div

    return divT
def isosurface(self, vector, isoval, axis=0, interp='nearest')

Calculate an isosurface along a given axis (So far this is only working for axis=0)

Parameters

vector : array, the same size as the mesh (n,) isoval : float, isosurface value axis : int, axis to generate the isosurface interp : str, method can be either 'nearest' - nearest neighbour interpolation 'linear' - linear interpolation

Returns

z_interp : isosurface the same size as the specified axis

Expand source code
def isosurface(self, vector, isoval, axis=0, interp='nearest'):
    """
    Calculate an isosurface along a given axis
    (So far this is only working for axis=0)

    Parameters
    ----------
     vector : array, the same size as the mesh (n,)
     isoval : float, isosurface value
     axis   : int, axis to generate the isosurface
     interp : str, method can be either
        'nearest' - nearest neighbour interpolation
        'linear'  - linear interpolation
    
    Returns
    -------
     z_interp : isosurface the same size as the specified axis
    """
    Vcube = vector.reshape(self.n)
    Zcube = self.coords[:,::-1][:,axis].reshape(self.n)
    sort_idx = ((Vcube - isoval)**2).argsort(axis=axis)    
    i0 = sort_idx[0]
    # z0 = Zcube.take(i0)
    
    obj = []
    for d in range(0, self.dim):
        obj.append( slice(0, self.n[d]) )
    obj.pop(axis)
    
    idx = list(np.mgrid[obj])
    idx.insert(axis, i0)
    z0 = Zcube[idx]

    if interp == 'linear':
        v0 = Vcube[idx]
        
        # identify next nearest node
        i1 = sort_idx[1]
        idx[axis] = i1
        z1 = Zcube[idx]
        v1 = Vcube[idx]

        vmin = np.minimum(v0, v1)
        vmax = np.maximum(v0, v1)
        ratio = np.vstack([np.ones_like(vmax)*isoval, vmin, vmax])
        ratio -= ratio.min(axis=0)
        ratio /= ratio.max(axis=0)
        z_interp = ratio[0]*z1 + (1.0 - ratio[0])*z0
        return z_interp
    elif interp == 'nearest':
        return z0
def solve(self, matrix=None, rhs=None)

Construct the matrix A and vector b in Ax = b and solve for x

GMRES method is default

Expand source code
def solve(self, matrix=None, rhs=None):
    """
    Construct the matrix A and vector b in Ax = b
    and solve for x

    GMRES method is default
    """
    if matrix is None:
        matrix = self.construct_matrix()
    if rhs is None:
        rhs = self.construct_rhs()
    res = self.temperature

    T = linalg.spsolve(matrix, rhs)
    self.temperature = T

    return T
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 = diffusivity
    self.heat_sources = heat_sources