kawin.diffusion.mesh
On this page
- kawin.kawin.diffusion.mesh.MeshBase
- kawin.kawin.diffusion.mesh.FVM1D
- PeriodicBoundary1D(BoundaryCondition)
- MixedBoundary1D(BoundaryCondition)
- StepProfile1D(ProfileFunction)
- LinearProfile1D(ProfileFunction)
- ExperimentalProfile1D(ProfileFunction)
- FiniteVolumeMidPointCalculator(ABC)
- FVM1DMidpoint(FiniteVolumeMidPointCalculator)
- FVM1DEdge(FiniteVolumeMidPointCalculator)
- FiniteVolume1D(FiniteVolumeGrid)
- Cartesian1D(FiniteVolume1D)
- Cylindrical1D(FiniteVolume1D)
- Spherical1D(FiniteVolume1D)
- kawin.kawin.diffusion.mesh.FVM2D
kawin.kawin.diffusion.mesh.MeshBase
def noChangeAtNode(Ds):
Default function for at node diffusivity
def arithmeticMean(Ds):
Arithmetic mean - D_avg = 1/2*(D_i + D_j)
Ds should be in the shape of (m x N x y)
m - number of items to average over
N - number of nodes
y - number of responses
Average is taken along m
def geometricMean(Ds):
Geometric mean - D_avg = sqrt(D_i * D_j)
Ds should be in the shape of (m x N x y)
m - number of items to average over
N - number of nodes
y - number of responses
Average is taken along m
Note: this is only valid when D_i and D_j are the same sign
def logMean(Ds):
Log mean - D_avg = exp(1/2*(log(D_i)+log(D_j)))
Ds should be in the shape of (m x N x y)
m - number of items to average over
N - number of nodes
y - number of responses
Average is taken along m
Note: this is only valid for positive D
def harmonicMean(Ds):
Harmonic mean - D_avg = (1/2 * (D_i^-1 + D_j^-1))^-1
Ds should be in the shape of (m x N x y)
m - number of items to average over
N - number of nodes
y - number of responses
Average is taken along m
Note: in the limit of D_i or D_j -> 0, the mean is 0. In practice, this is undefined, so we
convert all inf/nan to 0
def _getResponseVariables(responses: Union[int, list[str]]) -> tuple[int, list[str]]:
This returns the number of response variables and names for each variable
Parameters
----------
responses: int | list[str]
If int, then this is the number of responses and the names will be R{i}
If list[str], then theses are the response names and the number of responses will be the list length
def _getResponseIndex(responseVar: Union[int, str], responses):
Given the response variable, return the corresponding index
def _formatSpatial(z):
BoundaryCondition(ABC)
def BoundaryCondition.setInitialResponse(self, y):
def BoundaryCondition.adjustFluxes(self, fluxes):
def BoundaryCondition.adjustdXdt(self, dXdt):
ProfileFunction(Protocol)
ProfileBuilder
class ProfileBuilder(self, steps = []):
Stores build steps to construct a response profile in a mesh
Note that all steps are additive to the response profile
TODO: consider removing this since it doesn't really do anything other
than store a list that the user can create themselves
Parameters
----------
steps: list[tuple(callable, int|str | list[int|str])] (optional)
List of build steps to initialize
def ProfileBuilder.addBuildStep(self, func: ProfileFunction, responseVar: int|str|list[int|str] = 0):
Adds a build step to construct the initial profile
Parameters
----------
func: ProfileFunction
Function that takes in a set of coordinates and returns the profile values
at coordinates
responseVar: int or str or list of int or str (optional)
Response variable(s) that func will return
This does not have to match exactly to the mesh variables, but only needs
to be a subset
def ProfileBuilder.clearBuildSteps(self):
Removes all build steps
ConstantProfile(ProfileFunction)
class ConstantProfile(self, value):
Constant value across profile
def ConstantProfile.call(self, z):
DiracDeltaProfile(ProfileFunction)
class DiracDeltaProfile(self, z, value):
Zero at all values except at z
Note: if a mesh does not have a node exactly at z, it will choose the closest node
def DiracDeltaProfile.call(self, z):
GaussianProfile(ProfileFunction)
class GaussianProfile(self, z, sigma, maxValue):
Gaussian function centered around z with standard deviation of sigma
Scaled to maxValue
def GaussianProfile.call(self, z):
BoundedRectangleProfile(ProfileFunction)
class BoundedRectangleProfile(self, lowerZ, upperZ, innerValue, outerValue = 0):
Defines rectangle with lower and upper corners
innerValue corresponds to value inside the rectangle while
outerValue corresponds to value outside the rectangle (defaults to 0)
def BoundedRectangleProfile.call(self, z):
BoundedEllipseProfile(ProfileFunction)
class BoundedEllipseProfile(self, z, r, innerValue, outerValue = 0):
Defines ellipse centered at z with radii r
innerValue corresponds to value inside the rectangle while
outerValue corresponds to value outside the rectangle (defaults to 0)
def BoundedEllipseProfile.call(self, z):
AbstractMesh (ABC)
class AbstractMesh (self, responses):
Abstract mesh class that defines basic methods to be used in a diffusion model
Parameters
----------
responses: int | list[str]
If int, then this is the number of responses and the names will be R{i}
If list[str], then theses are the response names and the number of responses will be the list length
Attributes
----------
numResponses: int
Number of response variables
responses: list[str]
Names of response variables
N: int
Total number of nodes
dims: int
Number of dimensions in z
y: np.ndarray
Initial values of response variables
This is intended not to change in a model. Time evolution
of response variables should be stored in MeshData
z: np.ndarray
Values of spatial coordinates corresponding to y
dz: float
In general, the smallest distance between two nodes
Default assumptions in this mesh:
y has shape [N,e] - e is number of responses
z has shape [N,d] - d is number of dimensiosn
def AbstractMesh .setResponseProfile(self, profileBuilder: ProfileBuilder, boundaryConditions: BoundaryCondition = None):
Creates initial profile on mesh based off a series of build steps
def AbstractMesh .computeFluxes(self, pairs):
def AbstractMesh .computedXdt(self, pairs: list[DiffusionPair]):
Given list of diffusivity and response pairs, compute dX/dt from diffusion fluxes
Parameters
----------
pairs: list[DiffusionPair]
Tuple will contain (diffusivity, response, averaging function, at node function)
Note that the diffusivity and response will be in the form of [N,e] (corresponding
to getDiffusivityCoordinates and getResponseCoordinates), which may not be the
shape of self.y. This function is responsible for accounting for this difference
and the shape of dxdt and y should be the same
Returns
-------
dXdt: np.ndarray
Must be same shape as y
def AbstractMesh .flattenResponse(self, y, numResponses = None):
Converts y [internal shape, ...] to yFlat [N, e, ...]
Parameters
----------
y: np.ndarray
Input y response in internal mesh shape with 1 dimension corresponding to responses
numResponses: int (optional)
Number of responses in y, defaults to total number of responses in mesh
Returns
-------
yFlat: np.ndarray
Shape of [N, e] with N being the total number of nodes and e being the number of responses
def AbstractMesh .flattenSpatial(self, z):
Converts z [internal shape, ...] to zFlat [N, d, ...]
Parameters
----------
z: np.ndarray
Input z response in internal mesh shape with 1 dimension
corresponding to the number of spatial dimensions
Returns
-------
zFlat: np.ndarray
Shape of [N, d] with N being the total number of nodes and d being the number of spatial dimensions
def AbstractMesh .unflattenResponse(self, yFlat, numResponses = None):
Converts yFlat [N, e, ...] to y [internal shape, ...]
Parameters
----------
yFlat: np.ndarray
Shape of [N, e] with N being the total number of nodes and e being the number of responses
numResponses: int (optional)
Number of responses in y, defaults to total number of responses in mesh
Returns
-------
y: np.ndarray
Input y response in internal mesh shape with 1 dimension corresponding to responses
def AbstractMesh .unflattenSpatial(self, zFlat):
Converts zFlat [N, d, ...] to z [internal shape, ...]
Parameters
----------
zFlat: np.ndarray
Shape of [N, d] with N being the total number of nodes and d being the number of spatial dimensions
Returns
-------
z: np.ndarray
Input z response in internal mesh shape with 1 dimension
corresponding to the number of spatial dimensions
def AbstractMesh .getResponseCoordinates(self, y):
Returns y as [N,e] and z as [N,D] arrays to compute response terms
def AbstractMesh .getDiffusivityCoordinates(self, y):
Returns y as [N,e] and z as [N,D] arrays to compute diffusivity terms
We decouple this from the response coordinates to allow for different ways
of computing D^(1/2)
a) By computing D^0 and D^1 and averaging
b) By computing D^(1/2) at (y^1/2, z^1/2)
By default, this will use option a since its an easier implementation, but the user can override this to use option b
def AbstractMesh ._diffusiveFlux(self, D, rHigh, rLow, dz):
Flux = -D (r1 - r0) / dz
FiniteVolumeGrid(AbstractMesh)
class FiniteVolumeGrid(self, responses, zlims, Ns, dims):
Functions for the finite volume method on a rectangular grid
def FiniteVolumeGrid.defineZCoordinates(self):
def FiniteVolumeGrid.flattenResponse(self, y, numResponses = None):
Converts y [n x m x ..., e, ...] to yFlat [N, e, ...]
def FiniteVolumeGrid.flattenSpatial(self, z):
Converts z [n x m x ..., d, ...] to zFlat [N, d, ...]
def FiniteVolumeGrid.unflattenResponse(self, yFlat, numResponses = None):
Converts yFlat [N, e, ...] to y [n x m x ..., e, ...]
def FiniteVolumeGrid.unflattenSpatial(self, zFlat):
Converts zFlat [N, e, ...] to z [n x m x ..., e, ...]
MeshData
class MeshData(self, mesh: AbstractMesh, record: bool | int = False):
Stores time and response variables for a mesh
Parameters
----------
mesh : AbstractMesh
Mesh to take dimensions of response variables from
record : bool | int
Record interval or whether to record
If False, only the current state of the mesh will be stored
If int, then every n iterations will be stored
def MeshData.reset(self):
Resets arrays
def MeshData.record(self, time, y, force = False):
Stores current state of time and response variables
Response variables should be in the flattened state
This is the same format as used in GenericModel
But for the mesh, we must call flattenResponse
def MeshData.setResponseAtN(self, y, N = -1):
Sets response profile at index N
If N is not supplied, then this sets at the current index
def MeshData.finalize(self):
Removes extra padding
def MeshData.y(self, time = None):
Returns reponse variable at time
If recording is disabled, then this will return the current state
kawin.kawin.diffusion.mesh.FVM1D
PeriodicBoundary1D(BoundaryCondition)
This doesn't do anything, but rather is used in the Mesh to compute the fluxes accordingly
Unfortunately, we can't just wrap the fluxes around and instead have to wrap the response variable
around. Which means that we would have to have knowledge on how to compute the flux (which is done in the mesh)
def PeriodicBoundary1D.setInitialResponse(self, y):
def PeriodicBoundary1D.adjustFluxes(self, fluxes):
def PeriodicBoundary1D.adjustdXdt(self, dXdt):
MixedBoundary1D(BoundaryCondition)
class MixedBoundary1D(self, responses: Union[int, list[str]]):
Currently, left and right boundary conditions both are defined here
We'll need a way to define boundary conditions at any edge, which will
especially be the case if we add a 2D mesh
Parameters
----------
responses: int | list[str]
If int, then this is the number of responses and the names will be R{i}
If list[str], then theses are the response names and the number of responses will be the list length
def MixedBoundary1D._setBC(self, responseVar, bcType, value, bcTypeArray, bcValueArray):
Sets boundary condition to given side (left or right)
def MixedBoundary1D.setLBC(self, responseVar, bcType, value):
Left boundary condition
Parameters
----------
responseVar: int | str
Response variable name or index
bcType: str
For flux/Neumann condition, it could be [flux, neumman, MixedBoundary1D.NEUMANN]
For composition/dirichlet condition, it could be [composition, dirichlet, MixedBoundary1D.DIRICHLET]
value: float
Value of boundary condition
def MixedBoundary1D.setRBC(self, responseVar, bcType, value):
Left boundary condition
Parameters
----------
responseVar: int | str
Response variable name or index
bcType: str
For flux/Neumann condition, it could be [flux, neumman, MixedBoundary1D.NEUMANN]
For composition/dirichlet condition, it could be [composition, dirichlet, MixedBoundary1D.DIRICHLET]
value: float
Value of boundary condition
def MixedBoundary1D.setInitialResponse(self, y):
Sets dirichlet boundary conditions onto y
def MixedBoundary1D.adjustFluxes(self, fluxes):
Adjust flux values using boundary conditions
For Neumann boundary conditions, this overrides the flux with the BC value
def MixedBoundary1D.adjustdXdt(self, dXdt):
Adjust dx/dt using boundary conditions
For Dirichlet boundary conditions, this sets dx/dt to 0
StepProfile1D(ProfileFunction)
class StepProfile1D(self, z, leftValue, rightValue):
Step function at z
def StepProfile1D.call(self, z):
LinearProfile1D(ProfileFunction)
class LinearProfile1D(self, leftZ, leftValue, rightZ, rightValue, lowerLeftValue = None, upperRightValue = None):
Linear function from (leftZ, leftValue) to (rightZ, rightValue)
lowerLeftValue and upperRightValue corresponds to the value outside the bounds of
leftZ and rightZ. By default they will use leftValue and rightValue
def LinearProfile1D.call(self, z):
ExperimentalProfile1D(ProfileFunction)
class ExperimentalProfile1D(self, z, values, left=None, right=None):
Given experimental values at z, this will interpolate in between
left and right are the values used if the mesh goes beyond z. By default, they
will use the left-most and right-most values
def ExperimentalProfile1D.call(self, z):
FiniteVolumeMidPointCalculator(ABC)
In the finite volume method, we need to compute diffusivity at the
edges of out nodes, but the response variables are always at the cell centers
This class is intended to provide functionality of how to we approach computing
diffusivities at the edges
def FiniteVolumeMidPointCalculator.getDiffusivityCoordinates(self, *args, **kwargs):
def FiniteVolumeMidPointCalculator.getDMid(self, *args, **kwargs):
FVM1DMidpoint(FiniteVolumeMidPointCalculator)
def FVM1DMidpoint.getDiffusivityCoordinates(y, z, zEdge, isPeriodic = False, *args, **kwargs):
This returns the mid point of y and z (which is zEdge)
Note: first element in ymid is second element of zEdge
For periodic conditions, we also include y value between the first and last node
def FVM1DMidpoint.getDMid(D, isPeriodic = False, atNodeFunc = noChangeAtNode, *args, **kwargs):
For periodic conditions, the last D will correspond to between the first and last node
FVM1DEdge(FiniteVolumeMidPointCalculator)
def FVM1DEdge.getDiffusivityCoordinates(y, z, zEdge, *args, **kwargs):
Return y and z since we compute diffusivity at these coordinates
def FVM1DEdge.getDMid(D, isPeriodic = False, avgFunc = arithmeticMean, *args, **kwargs):
For periodic conditions, we average the first and last D
FiniteVolume1D(FiniteVolumeGrid)
class FiniteVolume1D(self, responses, zlim, N, computeMidpoint = False):
1D finite volume mesh
Cell will be a volume with thickness dz
y - value at node center
z - spatial coordinate at node center
zEdge - spatial coordinate at node edge
Parameters
----------
zlim: list[float]
Left and right boundary position of mesh
N: int
Number of cells
responses: int | list[str]
If int, then this is the number of responses and the names will be R{i}
If list[str], then theses are the response names and the number of responses will be the list length
boundaryConditions: BoundaryCondition1D
Boundary conditions on mesh
computeMidpoint: bool
Whether to compute diffusivity at average midpoint composition (True) or
average diffusivities computed on nearby cell centers (False)
def FiniteVolume1D.defineZCoordinates(self):
def FiniteVolume1D.setResponseProfile(self, profileBuilder, boundaryConditions = None):
def FiniteVolume1D.computeAtMidpoint(self, compute: bool):
Sets midpoint calculator
Parameters
----------
compute: bool
If True, diffusivity is computed at average response at node edge
If False, diffusivity is the average diffusivity computed at the neighboring node centers
def FiniteVolume1D._fluxTodXdt(self, fluxes):
Given fluxes, compute dx/dt. This depends on coordinate system
def FiniteVolume1D.computeFluxes(self, pairs: list[DiffusionPair]):
Compute fluxes from (diffusivity, response) pairs on a 1D FVM mesh
def FiniteVolume1D.computedXdt(self, pairs: list[DiffusionPair]):
Computes fluxes, correct fluxes for boundary conditions, compute dx/dt and correct dx/dt for boundary conditions
def FiniteVolume1D.getDiffusivityCoordinates(self, y):
Return y and z to compute diffusivities at
Cartesian1D(FiniteVolume1D)
1D finite volume mesh in cartesian coordinates
Cell will be a volume with thickness dz
y - value at node center
z - spatial coordinate at node center
zEdge - spatial coordinate at node edge
Parameters
----------
zlim: list[float]
Left and right boundary position of mesh
N: int
Number of cells
responses: int | list[str]
If int, then this is the number of responses and the names will be R{i}
If list[str], then theses are the response names and the number of responses will be the list length
boundaryConditions: BoundaryCondition1D
Boundary conditions on mesh
computeMidpoint: bool
Whether to compute diffusivity at average midpoint composition (True) or
average diffusivities computed on nearby cell centers (False)
def Cartesian1D._fluxTodXdt(self, fluxes):
For cartesian: dx/dt = -dJ/dz
Cylindrical1D(FiniteVolume1D)
class Cylindrical1D(self, responses, rlim, N, computeMidpoint = False):
1D finite volume mesh in cylindrical coordinates
Cell will be a volume with thickness dz
y - value at node center
z - spatial coordinate at node center
zEdge - spatial coordinate at node edge
Parameters
----------
rlim: list[float]
Left and right boundary position of mesh in terms of radius
Both min and max radius must be above 0
N: int
Number of cells
responses: int | list[str]
If int, then this is the number of responses and the names will be R{i}
If list[str], then theses are the response names and the number of responses will be the list length
boundaryConditions: BoundaryCondition1D
Boundary conditions on mesh
computeMidpoint: bool
Whether to compute diffusivity at average midpoint composition (True) or
average diffusivities computed on nearby cell centers (False)
def Cylindrical1D.setResponseProfile(self, profileBuilder, boundaryConditions=None):
def Cylindrical1D._fluxTodXdt(self, fluxes):
For cylindrical: dx/dt = -1/r dJ/dz
Spherical1D(FiniteVolume1D)
class Spherical1D(self, responses, rlim, N, computeMidpoint = False):
1D finite volume mesh in spherical coordinates
Cell will be a volume with thickness dz
y - value at node center
z - spatial coordinate at node center
zEdge - spatial coordinate at node edge
Parameters
----------
rlim: list[float]
Left and right boundary position of mesh in terms of radius
Both min and max radius must be above 0
N: int
Number of cells
responses: int | list[str]
If int, then this is the number of responses and the names will be R{i}
If list[str], then theses are the response names and the number of responses will be the list length
boundaryConditions: BoundaryCondition1D
Boundary conditions on mesh
computeMidpoint: bool
Whether to compute diffusivity at average midpoint composition (True) or
average diffusivities computed on nearby cell centers (False)
def Spherical1D.setResponseProfile(self, profileBuilder, boundaryConditions=None):
def Spherical1D._fluxTodXdt(self, fluxes):
For spherical: dx/dt = -1/r^2 dJ/dz
kawin.kawin.diffusion.mesh.FVM2D
Cartesian2D(FiniteVolumeGrid)
class Cartesian2D(self, responses, zx, Nx, zy, Ny):
2D finite volume mesh
y - value at node center (Nx, Ny, dims)
z - spatial coordinate at node center (Nx, Ny, 2)
zCorner - spatial coordinate at node corners (Nx+1, Ny+1, 2)
dz - thickness of node in both dimensions
TODO: boundary conditions not supported yet, so only no flux conditions
Parameters
----------
responses: int | list[str]
If int, then this is the number of responses and the names will be R{i}
If list[str], then theses are the response names and the number of responses will be the list length
zx: list[float]
Left and right boundary position of mesh
Nx: int
Number of cells along x
zy: list[float]
Top and bottom boundary position of mesh
Ny: int
Number of cells along y
def Cartesian2D.defineZCoordinates(self):
def Cartesian2D.computeFluxes(self, pairs: list[DiffusionPair]):
Compute fluxes from (diffusivity, response) pairs on a 2D FVM mesh
def Cartesian2D.computedXdt(self, pairs: list[DiffusionPair]):
dx/dt = -dJx/dz + -dJy/dz