Skip to content

Instantly share code, notes, and snippets.

@jmansour
Created June 19, 2019 00:49
Show Gist options
  • Select an option

  • Save jmansour/d060afd9f4a5d30a7045d8000885356d to your computer and use it in GitHub Desktop.

Select an option

Save jmansour/d060afd9f4a5d30a7045d8000885356d to your computer and use it in GitHub Desktop.
##~#~#~#~#~#~#~#~#~#~#~#~#~#~#~#~#~#~#~#~#~#~#~#~#~#~#~#~#~#~#~#~#~#~#~#~#~#~#~#~#~#~##
## ##
## This file forms part of the Underworld geophysics modelling application. ##
## ##
## For full license and copyright information, please refer to the LICENSE.md file ##
## located at the project root, or contact the authors. ##
## ##
##~#~#~#~#~#~#~#~#~#~#~#~#~#~#~#~#~#~#~#~#~#~#~#~#~#~#~#~#~#~#~#~#~#~#~#~#~#~#~#~#~#~##
import underworld as uw
import underworld._stgermain as _stgermain
from . import sle
import libUnderworld
from mpi4py import MPI
class _SLCN_AdvectionDiffusion(object):
def __init__(self, phiField, velocityField, fn_diffusivity, fn_sourceTerm=None, conditions=[]):
# Implements the Spiegelman Semi-lagrangian Crank Nicholson algorithm
# unknown field and velocity field
self.phiField = phiField
self.vField = velocityField
# uw.functions for diffusivity and a source term
self.fn_diffusivity = uw.function.Function.convert(fn_diffusivity)
self.fn_sourceTerm = uw.function.Function.convert(fn_sourceTerm)
self.fn_dt = uw.function.misc.constant(1.0) # dummy value
# build a grid field, phiStar, for the information at departure points
self._phiStar = phiField.copy()
# check input 'conditions' list is valid
if not isinstance(conditions,(list,tuple)):
conditionslist = []
conditionslist.append(conditions)
conditions = conditionslist
nbc = None # storage for neumann conditions
for cond in conditions:
if not isinstance( cond, uw.conditions.SystemCondition ):
raise TypeError( "Provided 'conditions' must be a list 'SystemCondition' objects." )
# check type of condition
if type(cond) == uw.conditions.NeumannCondition:
if nbc != None:
# check only one nbc condition is given in 'conditions' list
RuntimeError( "Provided 'conditions' can only accept one NeumannConditions condition object.")
nbc = cond
elif type(cond) == uw.conditions.DirichletCondition:
if cond.variable == self.phiField:
libUnderworld.StgFEM.FeVariable_SetBC( self.phiField._cself, cond._cself )
else:
raise RuntimeError("Input condition type not recognised.")
self._conditions = conditions
# build matrices and vectors
phi_eqnums = uw.systems.sle.EqNumber(phiField)
solv = self._solv = uw.systems.sle.SolutionVector(phiField, phi_eqnums)
f = self._f = uw.systems.sle.AssembledVector(phiField, phi_eqnums)
K = self._K = uw.systems.sle.AssembledMatrix(solv, solv, f)
# create quadrature swarm
mesh = phiField.mesh
intSwarm = uw.swarm.GaussIntegrationSwarm(mesh,particleCount=2)
fn_dt = self.fn_dt
# take sourceTerm into account - implementation doesn't track from departure points so is less accurate in time
if fn_sourceTerm is not None:
rhs_term = self._phiStar + fn_dt*self.fn_sourceTerm
else:
rhs_term = self._phiStar
self._mv_term = uw.systems.sle.VectorAssemblyTerm_NA__Fn( integrationSwarm = intSwarm,
assembledObject = f,
mesh = mesh,
fn = 1.*rhs_term )
self._kv_term = uw.systems.sle.VectorAssemblyTerm_NA_i__Fn_i( integrationSwarm = intSwarm,
assembledObject = f,
mesh = mesh,
fn = -0.5 * fn_dt * self.fn_diffusivity * self._phiStar.fn_gradient )
if nbc is not None:
### -VE flux because of the FEM discretisation method of the initial equation
negativeCond = uw.conditions.NeumannCondition( fn_flux = fn_dt * nbc.fn_flux,
variable = nbc.variable,
indexSetsPerDof = nbc.indexSetsPerDof )
#NOTE many NeumannConditions can be used but the _sufaceFluxTerm only records the last
self._surfaceFluxTerm = sle.VectorSurfaceAssemblyTerm_NA__Fn__ni(
assembledObject = f,
surfaceGaussPoints = 2,
nbc = negativeCond )
self._k_term = uw.systems.sle.MatrixAssemblyTerm_NA_i__NB_i__Fn(assembledObject = K,
integrationSwarm = intSwarm,
fn = +0.5 * fn_dt * self.fn_diffusivity)
self._m_term = uw.systems.sle.MatrixAssemblyTerm_NA__NB__Fn(assembledObject = K,
integrationSwarm = intSwarm,
fn = 1.,
mesh = mesh)
# functions used to calculate the timestep, see function get_max_dt()
self._maxVsq = uw.function.view.min_max(velocityField, fn_norm = uw.function.math.dot(velocityField,velocityField) )
self._maxDiff = uw.function.view.min_max(self.fn_diffusivity)
# the required for the solve
self.sle = uw.utils.SolveLinearSystem(AMat=K, bVec=f, xVec=solv)
def integrate(self, dt):
# use the given timestep
self.fn_dt.value = dt
# apply conditions
uw.libUnderworld.StgFEM.SolutionVector_LoadCurrentFeVariableValuesOntoVector( self._solv._cself )
# update T* - temperature at departure points
uw.libUnderworld.StgFEM.SemiLagrangianIntegrator_SolveNew(
self.phiField._cself,
dt,
self.vField._cself,
self._phiStar._cself)
# solve T
self.sle.solve()
def get_max_dt(self):
"""
Returns a timestep size for the current system.
Returns
-------
float
The timestep size.
"""
mesh = self.phiField.mesh
vField = self.vField
# evaluate the global maximum velocity
ignore = self._maxVsq.evaluate(mesh)
vmax = uw._np.sqrt(self._maxVsq.max_global())
# evaluate the global maximum diffusivity
if type(self.fn_diffusivity) == uw.swarm.SwarmVariable:
maxDiffusion = self._maxDiff.evaluate(self.fn_diffusivity.swarm)
else:
maxDiffusion = self._maxDiff.evaluate(self.phiField.mesh)
maxDiffusion = self._maxDiff.max_global()
# evaluate the minimum separation of the mesh
dx = mesh._cself.minSep
diff_dt = dx*dx / maxDiffusion
adv_dt = dx / vmax
return min(adv_dt, diff_dt)
class _SUPG_AdvectionDiffusion(_stgermain.StgCompoundComponent):
"""
"""
_objectsDict = { "_system" : "AdvectionDiffusionSLE",
"_solver" : "AdvDiffMulticorrector" }
_selfObjectName = "_system"
def __init__(self, phiField, phiDotField, velocityField, fn_diffusivity, fn_sourceTerm=None, conditions=[], _allow_non_q1=False, **kwargs):
self._diffusivity = fn_diffusivity
self._source = fn_sourceTerm
if not isinstance( phiField, uw.mesh.MeshVariable):
raise TypeError( "Provided 'phiField' must be of 'MeshVariable' class." )
if phiField.data.shape[1] != 1:
raise TypeError( "Provided 'phiField' must be a scalar" )
self._phiField = phiField
if self._phiField.mesh.elementType != 'Q1':
import warnings
warnings.warn("The 'phiField' is discretised on a {} mesh. This 'uw.system.AdvectionDiffusion' implementation is ".format(self._phiField.mesh.elementType)+
"only stable for a phiField discretised with a Q1 mesh. Either create a Q1 mesh for the 'phiField' or, if you know "+
"what you're doing, override this error with the argument '_allow_non_q1=True' in the constructor", category=RuntimeWarning)
if _allow_non_q1 == False:
raise ValueError( "Error as provided 'phiField' discretisation is unstable")
if not isinstance( phiDotField, (uw.mesh.MeshVariable, type(None))):
raise TypeError( "Provided 'phiDotField' must be 'None' or of 'MeshVariable' class." )
if self._phiField.data.shape != phiDotField.data.shape:
raise TypeError( "Provided 'phiDotField' is not the same shape as the provided 'phiField'" )
self._phiDotField = phiDotField
# check compatibility of phiField and velocityField
if velocityField.data.shape[1] != self._phiField.mesh.dim:
raise TypeError( "Provided 'velocityField' must be the same dimensionality as the phiField's mesh" )
self._velocityField = velocityField
if not isinstance(conditions,(list,tuple)):
conditionslist = []
conditionslist.append(conditions)
conditions = conditionslist
# check input 'conditions' list is valid
nbc = None
for cond in conditions:
if not isinstance( cond, uw.conditions.SystemCondition ):
raise TypeError( "Provided 'conditions' must be a list 'SystemCondition' objects." )
# set the bcs on here
if type(cond) == uw.conditions.NeumannCondition:
if nbc != None:
# check only one nbc condition is given in 'conditions' list
RuntimeError( "Provided 'conditions' can only accept one NeumannConditions condition object.")
elif type(cond) == uw.conditions.DirichletCondition:
if cond.variable == self._phiField:
libUnderworld.StgFEM.FeVariable_SetBC( self._phiField._cself, cond._cself )
libUnderworld.StgFEM.FeVariable_SetBC( self._phiDotField._cself, cond._cself )
else:
raise RuntimeError("Input condition type not recognised.")
self._conditions = conditions
# force removal of BCs as SUPG cannot handle leaving them in
self._eqNumPhi = sle.EqNumber( phiField, removeBCs=True )
self._eqNumPhiDot = sle.EqNumber( phiDotField, removeBCs=True )
self._phiSolution = sle.SolutionVector( phiField, self._eqNumPhi )
self._phiDotSolution = sle.SolutionVector( phiDotField, self._eqNumPhiDot )
# create force vectors
self._residualVector = sle.AssembledVector(phiField, self._eqNumPhi )
self._massVector = sle.AssembledVector(phiField, self._eqNumPhi )
# create swarm
self._gaussSwarm = uw.swarm.GaussIntegrationSwarm(self._phiField.mesh)
super(AdvectionDiffusion, self).__init__(**kwargs)
self._cself.phiVector = self._phiSolution._cself
self._cself.phiDotVector = self._phiDotSolution._cself
def _add_to_stg_dict(self,componentDictionary):
# call parents method
super(AdvectionDiffusion,self)._add_to_stg_dict(componentDictionary)
componentDictionary[ self._cself.name ][ "SLE_Solver"] = self._solver.name
componentDictionary[ self._cself.name ][ "PhiField"] = self._phiField._cself.name
componentDictionary[ self._cself.name ][ "Residual"] = self._residualVector._cself.name
componentDictionary[ self._cself.name ][ "MassMatrix"] = self._massVector._cself.name
componentDictionary[ self._cself.name ][ "PhiDotField"] = self._phiDotField._cself.name
componentDictionary[ self._cself.name ][ "dim"] = self._phiField.mesh.dim
componentDictionary[ self._cself.name ]["courantFactor"] = 0.50
def _setup(self):
# create assembly terms here.
# in particular, the residualTerm requires and tries to build _system, so if created in __init__
# this causes a conflict.
self._lumpedMassTerm = sle.LumpedMassMatrixVectorTerm( integrationSwarm = self._gaussSwarm,
assembledObject = self._massVector )
self._residualTerm = sle.AdvDiffResidualVectorTerm( velocityField = self._velocityField,
diffusivity = self._diffusivity,
sourceTerm = self._source,
integrationSwarm = self._gaussSwarm,
assembledObject = self._residualVector,
extraInfo = self._cself.name )
for cond in self._conditions:
if isinstance( cond, uw.conditions.NeumannCondition ):
### -VE flux because of the FEM discretisation method of the initial equation
negativeCond = uw.conditions.NeumannCondition( fn_flux=cond.fn_flux,
variable=cond.variable,
indexSetsPerDof=cond.indexSetsPerDof )
#NOTE many NeumannConditions can be used but the _sufaceFluxTerm only records the last
self._surfaceFluxTerm = sle.VectorSurfaceAssemblyTerm_NA__Fn__ni(
assembledObject = self._residualVector,
surfaceGaussPoints = 2,
nbc = negativeCond )
self._cself.advDiffResidualForceTerm = self._residualTerm._cself
def integrate(self,dt):
"""
Integrates the advection diffusion system through time, dt
Must be called collectively by all processes.
Parameters
----------
dt : float
The timestep interval to use
"""
self._cself.currentDt = dt
libUnderworld.Underworld._AdvectionDiffusionSLE_Execute( self._cself, None )
def get_max_dt(self):
"""
Returns a numerically stable timestep size for the current system.
Note that as a default, this method returns a value one half the
size of the Courant timestep.
Returns
-------
float
The timestep size.
"""
return libUnderworld.Underworld.AdvectionDiffusionSLE_CalculateDt( self._cself, None )
class AdvectionDiffusion(object):
"""
This class provides functionality for a discrete representation
of an advection-diffusion equation.
The class uses the Streamline Upwind Petrov Galerkin SUPG method
to integrate through time.
.. math::
\\frac{\\partial\\phi}{\\partial t} + {\\bf u } \\cdot \\nabla \\phi= \\nabla { ( k \\nabla \\phi ) } + H
Parameters
----------
phiField : underworld.mesh.MeshVariable
The concentration field, typically the temperature field
velocityField : underworld.mesh.MeshVariable
The velocity field.
fn_diffusivity : underworld.function.Function
A function that defines the diffusivity within the domain.
fn_sourceTerm : underworld.function.Function
A function that defines the heating within the domain. Optional.
conditions : underworld.conditions.SystemCondition
Numerical conditions to impose on the system. This should be supplied as
the condition itself, or a list object containing the conditions.
type : string
Type of solver to use. Either "SLCN" or "SUPG".
phiDotField : underworld.mesh.MeshVariable
ONLY REQUIRED FOR SUPG type SOLVER.
A MeshVariable that defines the initial time derivative of the phiField.
Typically 0 at the beginning of a model, e.g. phiDotField.data[:]=0
When using a phiField loaded from disk one should also load the phiDotField to ensure
the solving method has the time derivative information for a smooth restart.
No dirichlet conditions are required for this field as the phiField degrees of freedom
map exactly to this field's dirichlet conditions, the value of which ought to be 0
for constant values of phi.
Notes
-----
Constructor must be called by collectively all processes.
"""
def __init__(self, phiField, velocityField, fn_diffusivity, fn_sourceTerm=None, conditions=[], type="SLCN", phiDotField=None, **kwargs):
if not isinstance( phiField, uw.mesh.MeshVariable):
raise TypeError( "Provided 'phiField' must be of 'MeshVariable' class." )
if phiField.data.shape[1] != 1:
raise TypeError( "Provided 'phiField' must be a scalar" )
if phiDotField and (type=="SLCN"):
import warning
warning.warn("'phiDotField' not requird by 'SLCN' solver. Ignoring.")
if not phiDotField and (type=="SUPG"):
raise ValueError("'phiDotField' required for 'SUPG' solver.")
# check compatibility of phiField and velocityField
if velocityField.data.shape[1] != self._phiField.mesh.dim:
raise TypeError( "Provided 'velocityField' must be the same dimensionality as the phiField's mesh" )
self._velocityField = velocityField
if type not in ("SLCN","SUPG"):
raise ValueError("'type parameter must be 'SLCN' or 'SUPG'.")
self.type = type
if self.type == "SLCN":
self.sysguy = _SLCN_AdvectionDiffusion(phiField, velocityField, fn_diffusivity, fn_sourceTerm=None, conditions=[],**kwargs)
else:
self.sysguy = _SUPG_AdvectionDiffusion(phiField, phiDotField, velocityField, fn_diffusivity, fn_sourceTerm=None, conditions=[],**kwargs)
super(AdvectionDiffusion, self).__init__(**kwargs)
def integrate(self,dt):
"""
Integrates the advection diffusion system through time, dt
Must be called collectively by all processes.
Parameters
----------
dt : float
The timestep interval to use
"""
self.sysguy.integrate()
def get_max_dt(self):
"""
Returns a numerically stable timestep size for the current system.
Note that as a default, this method returns a value one half the
size of the Courant timestep.
Returns
-------
float
The timestep size.
"""
return self.sysguy.get_max_dt()
@julesghub
Copy link

Thanks Johnny - this is exactly what I implemented. haha
The problem is the creation strings aren't available exactly as before, but I suppose people can just use the Public funcs if needed.

@jmansour
Copy link
Author

Creation strings?

@julesghub
Copy link

docstrings...opps

@jmansour
Copy link
Author

How are they not available?

@jmansour
Copy link
Author

Note I've moved everything into the public function.

@julesghub
Copy link

i know, as mentioned, when one alt-tabs the shell class docstring is not 100% the same as the original ones.

@jmansour
Copy link
Author

Really? What's different? It won't have the upper inheritance stuff, but we don't want that in any case.

@julesghub
Copy link

There are some rogue arguments for the class instansiation and the integrate() function. I think I'll just mention the documentation for the method can be found by seeking the docstrings for the _SLCN.... or _SUPG... classes.

@jmansour
Copy link
Author

jmansour commented Jun 19, 2019

You mean phiDotField? I think that's the only one. For any others, just capture all using **kwargs and pass through. I've slotted phiDotField into end of list, which is a potentially breaking API change (if the user hasn't used keywords for phiDotField.)

Shouldn't be rogue args for integrate() (although I forgot to pass through the dt above).

I think we need to sort it this way, or just drop it and use different classes. Directing users to private classes is a bit shit, and also will cause issues for API doc generation (which ignores privates).

@julesghub
Copy link

integrate() has many args for SLCN, that can be handled by **kwargs.
I think we use the wrapping idea and I'll just consolidate the docstrings of the AdvectionDiffusion class to reflect all options which are dependent on the type (i called method) selected

@jmansour
Copy link
Author

Ok, cool. I'm just looking at the version of SLCN that's already there. Romain committed it here 53315161960d62a385313b54c519e35aa7ec1787. Not sure why I didn't see it earlier when I look. So confused!

@julesghub
Copy link

julesghub commented Jun 19, 2019

oh dear me! I thought that was the case.... I'll clean up all and bring the (slow) bells and whistles stuff to dev.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment