From 28e34dc6916f8e7096ac4c4155e0cc4aa1db9aa2 Mon Sep 17 00:00:00 2001 From: julian Date: Tue, 19 Jan 2021 18:00:53 +1100 Subject: [PATCH 01/13] The initial commit of the cuvi work. New stuff includes: - Variable vector dirichlet boundary conditions, `CurvilinearDirichletCondition`, driven by uw.fn. - A system wide rotation matrix that can be used for stokes or rotating a vector mesh variable - Meshes with non cartesian geometries. New examples under, docs/examples/curvi_examples. --- .../curvi_examples/Annulus_PoissonEq.py | 112 ++++++ .../curvi_examples/Annulus_StokesAdvection.py | 155 +++++++++ underworld/conditions/__init__.py | 2 +- underworld/conditions/_conditions.py | 84 +++++ underworld/function/math.py | 36 ++ .../src/BSSCR/auglag-driver-DGTGD.c | 48 +++ .../StokesFlow/src/Stokes_SLE.h | 1 + .../StgFEM/SLE/SystemSetup/src/ForceVector.c | 86 ++++- .../StgFEM/SLE/SystemSetup/src/ForceVector.h | 9 +- .../SLE/SystemSetup/src/SolutionVector.c | 30 ++ .../SLE/SystemSetup/src/SolutionVector.h | 4 +- .../SLE/SystemSetup/src/StiffnessMatrix.c | 329 ++++++++---------- .../SLE/SystemSetup/src/StiffnessMatrix.h | 7 +- .../Underworld/Function/src/Binary.cpp | 29 ++ .../Underworld/Function/src/Binary.hpp | 8 + .../libUnderworld/Underworld/Utils/src/Init.c | 4 + .../src/MatrixAssemblyTerm_RotationDof.cpp | 240 +++++++++++++ .../src/MatrixAssemblyTerm_RotationDof.h | 96 +++++ .../Underworld/Utils/src/Utils.h | 1 + .../Underworld/Utils/src/types.h | 2 + .../libUnderworldPy/Underworld.i | 1 + underworld/mesh/__init__.py | 2 +- underworld/mesh/_mesh.py | 268 ++++++++++++++ underworld/systems/_stokes.py | 66 +++- underworld/systems/sle/__init__.py | 2 +- underworld/systems/sle/_assemblyterm.py | 33 ++ 26 files changed, 1441 insertions(+), 214 deletions(-) create mode 100644 docs/examples/curvi_examples/Annulus_PoissonEq.py create mode 100644 docs/examples/curvi_examples/Annulus_StokesAdvection.py create mode 100644 underworld/libUnderworld/Underworld/Utils/src/MatrixAssemblyTerm_RotationDof.cpp create mode 100644 underworld/libUnderworld/Underworld/Utils/src/MatrixAssemblyTerm_RotationDof.h diff --git a/docs/examples/curvi_examples/Annulus_PoissonEq.py b/docs/examples/curvi_examples/Annulus_PoissonEq.py new file mode 100644 index 000000000..3a43c4aaf --- /dev/null +++ b/docs/examples/curvi_examples/Annulus_PoissonEq.py @@ -0,0 +1,112 @@ +# --- +# jupyter: +# jupytext: +# formats: ipynb,py:percent +# text_representation: +# extension: .py +# format_name: percent +# format_version: '1.3' +# jupytext_version: 1.4.2 +# kernelspec: +# display_name: Python 3 +# language: python +# name: python3 +# --- + +# %% [markdown] +# ## Demo of Temperature diffusion (PoissonEq) in an annulus +# ### Check against an simple analytic solution and plot radial temperature + +# %% +import underworld as uw +import underworld.visualisation as vis +import numpy as np +import underworld.function as fn + +# %% +## Parameters + +# radius of the bottom and top +rb = 3. +rt = 6. + +# surface temperature at the bottom and top +tb = 10. +tt = 5. + +# analytic solution assuming diffusivity is 1 and 0 heating, +# ie. T_ii = 0, T(rb) = tb, T(rt) = tt +fac = (tt-tb) / np.log(rt/rb) +def np_analytic(r): + return np.log( (r)/rb) * fac + tb + + +# %% +annulus = uw.mesh.FeMesh_Annulus(elementRes=(20,100), radialLengths=(rb,rt), angularExtent=(0.0,360.0)) +tField = annulus.add_variable(nodeDofCount=1) + +# analytic function description, only possible one we have the fn_radial +fn_r = annulus.fn_radial +fn_analytic = fn.math.log( fn_r/rb ) * fac + tb + +# %% +fig = vis.Figure() +fig.append(vis.objects.Mesh(annulus)) +fig.append(vis.objects.Surface(annulus, tField, onMesh=True)) +fig.show() + +# %% +outer = annulus.specialSets["outer"] +inner = annulus.specialSets["inner"] + +tField.data[inner.data] = tb +tField.data[outer.data] = tt + +# %% +tBC = uw.conditions.DirichletCondition( variable=tField, indexSetsPerDof=(inner+outer)) +ssSLE = uw.systems.SteadyStateHeat(tField,fn_diffusivity=1.0, conditions=tBC) +ssSolver = uw.systems.Solver(ssSLE) + +ssSolver.solve() + +# %% +# error measurement - l2 norm +fn_e = fn.math.pow(fn_analytic - tField, 2.) + +error = annulus.integrate(fn_e)[0] +tolerance = 3.6e-4 +if error > tolerance: + es = "Model error greater the test tolerance. {:.4e} > {:.4e}".format(error, tolerance) + raise RuntimeError(es) + +# %% +fig.show() + +# %% +# if serial plot the +if uw.mpi.size == 1: + uw.utils.matplotlib_inline() + import matplotlib.pyplot as plt + plt.ion() + + # Build a (Swarm) line of points along a constant angle + p_x = np.ndarray((20,annulus.dim)) + theta = 0. + p_x[:,0] = np.linspace(rb, rt, 20) # first build radial position + p_x[:,1] = np.sin(theta)*p_x[:,0] + p_x[:,0] = np.cos(theta)*p_x[:,0] + + # Build a swarm + swarm = uw.swarm.Swarm(annulus) + swarm.add_particles_with_coordinates(p_x) + + # evaluate numerical and analytic fields + measure_temp = tField.evaluate(swarm) + analytic_temp = np_analytic(p_x[:,0]) + + ## Plot radial temperature and check against an analytic solution + plt.plot(p_x[:,0],analytic_temp,label="Analytic") + plt.scatter(p_x[:,0],measure_temp[:,0],label="Numerical") + plt.xlabel("Radial Distance") + plt.ylabel("Temperature") + plt.legend() diff --git a/docs/examples/curvi_examples/Annulus_StokesAdvection.py b/docs/examples/curvi_examples/Annulus_StokesAdvection.py new file mode 100644 index 000000000..e547de448 --- /dev/null +++ b/docs/examples/curvi_examples/Annulus_StokesAdvection.py @@ -0,0 +1,155 @@ +# --- +# jupyter: +# jupytext: +# formats: ipynb,py:percent +# text_representation: +# extension: .py +# format_name: percent +# format_version: '1.3' +# jupytext_version: 1.4.2 +# kernelspec: +# display_name: Python 3 +# language: python +# name: python3 +# --- + +# %% [markdown] +# ## Swarm advection with lid Driven free slip boundary conditions in annulus geometry + +# %% +import underworld as uw +from underworld import function as fn +import underworld.visualisation as vis +import math, os +import numpy as np +from mpi4py import MPI + +# %% +# Set simulation box size. +boxHeight = 1.0 +boxLength = 2.0 +# Set the resolution. +res = 2 +# Set min/max temperatures. +tempMin = 0.0 +tempMax = 1.0 + +comm = MPI.COMM_WORLD +outputDir = 'outputWithSwarm/' + +if uw.mpi.rank == 0: + step = 1 + while os.path.exists(outputDir): + outputDir = outputDir.split("_")[0]+"_"+str(step).zfill(3)+'/' + step += 1 + + os.makedirs(outputDir) + outF = open(outputDir+'/output.dat', 'w') + +store = vis.Store(outputDir+'/viz') + +# build annulus mesh - handles deforming a recangular mesh and applying periodic dofs +mesh = uw.mesh.FeMesh_Annulus(elementRes=(10,60), radialLengths=(4.,6.)) +velocityField = mesh.add_variable( nodeDofCount=2 ) +pressureField = mesh.subMesh.add_variable( nodeDofCount=1 ) +vmag = fn.math.sqrt(fn.math.dot( velocityField, velocityField )) + +# %% +# Set viscosity to be a constant. +viscosity = 1. +buoyancyFn = (0.,0.0) + +# %% +# TODO: reuse only the vertex sets corresponding to the boundaries. +lower = mesh.specialSets["MinI_VertexSet"] +upper = mesh.specialSets["MaxI_VertexSet"] + +# (vx,vy) -> (vn,vt) (normal, tangential) +velocityField.data[ upper.data ] = [0.0,10.0] +velBC = uw.conditions.CurvilinearDirichletCondition( variable = velocityField, + indexSetsPerDof = (lower+upper, upper), + basis_vectors = (mesh.bnd_vec_normal, mesh.bnd_vec_tangent)) + + +# %% +swarm = uw.swarm.Swarm(mesh, particleEscape=True) +tvar = swarm.add_variable(dataType="double", count=1) # theta position +layout = uw.swarm.layouts.PerCellSpaceFillerLayout(swarm, particlesPerCell=10) + +swarm.populate_using_layout(layout) +# get initial theta coordinates and save into tvar +x,y = np.split(swarm.particleCoordinates.data, indices_or_sections=2,axis=1) +tvar.data[:] = 180 / np.pi * np.arctan2(y,x) + +# add an advector +advector = uw.systems.SwarmAdvector(velocityField=velocityField, swarm=swarm) + +# %% +fig = vis.Figure(store=store) +fig.append( vis.objects.Mesh( mesh )) +fig.append(vis.objects.Points(swarm, fn_colour=tvar, fn_size=4, colours="blue red")) +# fig.append( vis.objects.VectorArrows(mesh, velocityField)) +fig.show() + +# %% +stokesSLE = uw.systems.Stokes( velocityField = velocityField, + pressureField = pressureField, + conditions = velBC, + fn_viscosity = viscosity, + fn_bodyforce = buoyancyFn, + _removeBCs = False ) # _removeBC is required + +# %% +solver = uw.systems.Solver(stokesSLE) +# using a direct method here reports in petsc warnings, because the solution is a nullspace I think +solver.solve() # results in velocity solution being mixed +# re-rotate and unmix +ierr = uw.libUnderworld.Underworld.AXequalsX( stokesSLE._rot._cself, stokesSLE._velocitySol._cself, False) + +# %% +fig.show() + +# %% +i=0 +t0 = MPI.Wtime() +t_adv = 0.; +t_save = 0.; +while i < 30: + t_adv = MPI.Wtime() + # advect particles and count + advector.integrate(advector.get_max_dt()) + t_adv = MPI.Wtime() - t_adv + globalCount = swarm.particleGlobalCount + + # update + i += 1 + store.step = i + t_save = MPI.Wtime() + fig.save() + t_save = MPI.Wtime() - t_save + + # print diagnostics + if uw.mpi.rank == 0: + outF.write("{0}, {1}, {2:.3e}, {3:.3e}\n".format(i, globalCount, t_adv, t_save)) + swarm.save(outputDir+'swarm.'+(str(i).zfill(5))+'.h5') + +if uw.mpi.rank == 0: + outF.close() + +# %% +if uw.utils.is_kernel(): + vis = vis.lavavu.Viewer(database=store.filename) +# vis["pointsize"]=3. + vis.control.Panel() + vis.control.ObjectList() + vis.control.TimeStepper() + vis.control.show() + +# %% +# import os +# filename = "v.h5" +# mH = mesh.save("./annulus.h5") +# if os.path.exists(filename): +# os.remove(filename) +# fH = velocityField.save(filename, mH) +# velocityField.xdmf('v.xdmf', fH, 'velocity', mH, 'mesh', 0) diff --git a/underworld/conditions/__init__.py b/underworld/conditions/__init__.py index 8ab541f3c..512134bc0 100644 --- a/underworld/conditions/__init__.py +++ b/underworld/conditions/__init__.py @@ -12,4 +12,4 @@ """ -from ._conditions import NeumannCondition, DirichletCondition, SystemCondition +from ._conditions import NeumannCondition, DirichletCondition, SystemCondition, CurvilinearDirichletCondition diff --git a/underworld/conditions/_conditions.py b/underworld/conditions/_conditions.py index 69a902212..9d55fc7cb 100755 --- a/underworld/conditions/_conditions.py +++ b/underworld/conditions/_conditions.py @@ -161,3 +161,87 @@ def fn_flux(self, fn): if not isinstance( _fn, uw.function.Function): raise ValueError( "Provided '_fn' must be of or convertible to 'Function' class." ) self._fn_flux = _fn + +class CurvilinearDirichletCondition(DirichletCondition): + """ + The CuvilinearDirichletCondition class provides the functionality to apply + variably defined vector dirichlet boundary conditions to a system. + + This is required for when the domain boundaries don't align to the numerical + coordinate system of the problem. For example, solving a problem in an annulus geometry + using cartesian coordinate system. The normal and tangential vectors change at every point + along surface. + + Users are required to provide the `basis_vectors` for the rotation to be applied to each node/DOF. + The basis_vectors must be orthogonal and can be provided as underworld.functions. + See examples (TODO: link to examples) for details. + + Parameters + ---------- + variable : underworld.mesh.MeshVariable + This is the variable for which the Dirichlet condition applies. + indexSetsPerDof : list, tuple, IndexSet + The index set(s) which flag nodes/DOFs as Dirichlet conditions. + Note that the user must provide an index set for each degree of + freedom of the variable. So for a vector variable of rank 2 (say Vx & Vy), + two index sets must be provided (say VxDofSet, VyDofSet). + basis_vectors : list or tuple of underworld.function.functions + A list (or tuple) of underworld.function.functions to define the local coodinates per + nodal vector dirichlet condition. + For a mesh of type uw.mesh.FeMesh_Annulus,..., the underlying basis_vectors are + automatically extracted from the mesh and the users doesn't need to pass in + the `basis_vectors`. To check if automatic basis_vectors can be extracted on a mesh + check the attributes ._e1, ._e2, etc. on a mesh. + + Notes + ----- + Note that it is necessary for the user to set the required value on the variable, possibly + via the numpy interface. + + Constructor must be called collectively all processes. + + Example + ------- + TODO + + + """ + + _objectsDict = { "_pyvc": "PythonVC" } + _selfObjectName = "_pyvc" + + def __init__(self, variable, indexSetsPerDof, basis_vectors=None): + super(CurvilinearDirichletCondition,self).__init__(variable, indexSetsPerDof) + + + mesh = variable.mesh + + if basis_vectors is not None: + self.basis_vectors=basis_vectors + else: + try: + if mesh.dim == 2: + self.basis_vectors=( mesh._e1, mesh._e2) + else: + self.basis_vectors=( mesh._e1, mesh._e2, mesh._e3) + + except: + raise("Problem with the basis vectors in CurvilinearDirichletCondition") + + @property + def basis_vectors(self): + """ Get the basis_vectors """ + return self._basis_vectors + + @basis_vectors.setter + def basis_vectors(self, basis_vectors): + """ Set the basis_vectors, ensure they're valid """ + if not isinstance(basis_vectors, (list,tuple)): + raise TypeError("'basis_vectors' must be of type list or tuple") + if len(basis_vectors) != self.variable.nodeDofCount: + raise ValueError("'variable' number of components must equal the number of 'basis_vectors'") + for vec in basis_vectors: + if not isinstance( vec, uw.function.Function): + raise TypeError("'basis_vectors', must consist of 'uw.function.Function' types") + + self._basis_vectors=basis_vectors diff --git a/underworld/function/math.py b/underworld/function/math.py index faca78daf..29f87c4b0 100644 --- a/underworld/function/math.py +++ b/underworld/function/math.py @@ -214,6 +214,42 @@ def __init__(self, fn=None, *args, **kwargs): # build parent super(atan,self).__init__(argument_fns=[fn,],**kwargs) +class atan2(_Function): + """ + arctan2 function. Returns the arc tangent of y/x, expressed in radians. + + Parameters + ---------- + fn1: underworld.function.Function (or convertible). + The function to compute y values. + fn2: underworld.function.Function (or convertible). + The function to compute x values. + + Example + ------- + >>> import numpy as np + >>> func = atan2(_uw.function.input(),3.) + >>> np.allclose( func.evaluate(3.), np.pi/4 ) # TODO:think it should fail. + True + + """ + def __init__(self, fn1, fn2, **kwargs): + # lets convert integer powers to floats + fn1fn = _Function.convert( fn1 ) + if not isinstance( fn1fn, _Function ): + raise TypeError("Functions must be of type (or convertible to) 'Function'.") + fn2fn = _Function.convert( fn2 ) + if not isinstance( fn2fn, _Function ): + raise TypeError("Functions must be of type (or convertible to) 'Function'.") + + self._fn1 = fn1fn + self._fn2 = fn2fn + # ok finally lets create the fn + self._fncself = _cfn.Atan2(self._fn1._fncself, self._fn2._fncself ) + # build parent + super(atan2,self).__init__(argument_fns=[fn1fn,fn2fn],**kwargs) + + class cosh(_Function): """ Computes the hyperbolic cosine of its argument function. diff --git a/underworld/libUnderworld/Solvers/KSPSolvers/src/BSSCR/auglag-driver-DGTGD.c b/underworld/libUnderworld/Solvers/KSPSolvers/src/BSSCR/auglag-driver-DGTGD.c index 3468585cb..966b704db 100644 --- a/underworld/libUnderworld/Solvers/KSPSolvers/src/BSSCR/auglag-driver-DGTGD.c +++ b/underworld/libUnderworld/Solvers/KSPSolvers/src/BSSCR/auglag-driver-DGTGD.c @@ -88,6 +88,17 @@ PetscErrorCode BSSCR_DRIVER_auglag( KSP ksp, Mat stokes_A, Vec stokes_x, Vec sto PetscTruth flg, extractMats; PetscLogDouble flopsA,flopsB; + // if a rotation matrix exists, use it + Mat R; + + // all the rotated versions + Mat Kr, Gr, Dr; + Vec fr; + + if (bsscrp_self->st_sle->rMat != NULL ) { + R = bsscrp_self->st_sle->rMat->matrix; + } else { R = NULL; } + /***************************************************************************************************************/ /***************************************************************************************************************/ //if( bsscrp_self->solver->st_sle->context->loadFromCheckPoint ){ @@ -107,6 +118,35 @@ PetscErrorCode BSSCR_DRIVER_auglag( KSP ksp, Mat stokes_A, Vec stokes_x, Vec sto // Try this ... // MatMPIAIJSetPreallocation(K, 375 ,PETSC_NULL, 375, PETSC_NULL); + + if(R) { + int rows, cols; + + MatGetSize( K, &rows, &cols); + PetscPrintf( PETSC_COMM_WORLD, "K's rows %d and cols %d", rows, cols); + + MatGetSize( G, &rows, &cols); + PetscPrintf( PETSC_COMM_WORLD, "G's rows %d and cols %d", rows, cols); + + MatGetSize( R, &rows, &cols); + PetscPrintf( PETSC_COMM_WORLD, "R's rows %d and cols %d", rows, cols); + + VecGetSize( f, &rows); + PetscPrintf( PETSC_COMM_WORLD, "F's rows %d", rows ); + + VecDuplicate(f, &fr); + MatMult( R, f, fr ); + + MatMatMult( R, G, MAT_INITIAL_MATRIX, PETSC_DEFAULT, &Gr ); + MatMatTransposeMult( D, R, MAT_INITIAL_MATRIX, PETSC_DEFAULT, &Dr ); + MatRARt( K, R, MAT_INITIAL_MATRIX, 1.0, &Kr ); + + // use rotated versions + K = Kr; + G = Gr; + D = Dr; + f = fr; + } PetscPrintf( PETSC_COMM_WORLD, "AUGMENTED LAGRANGIAN K2 METHOD " ); PetscPrintf( PETSC_COMM_WORLD, "- Penalty = %f\n\n", bsscrp_self->solver->penaltyNumber ); @@ -565,6 +605,14 @@ PetscErrorCode BSSCR_DRIVER_auglag( KSP ksp, Mat stokes_A, Vec stokes_x, Vec sto /***************************************************************************************************************/ /***************************************************************************************************************/ + + if (R) { + MatDestroy( &Kr ); + MatDestroy( &Gr ); + MatDestroy( &Dr ); + VecDestroy( &fr ); + } + Stg_VecDestroy(&t ); Stg_KSPDestroy(&ksp_S ); Stg_VecDestroy(&h_hat ); diff --git a/underworld/libUnderworld/StgFEM/SLE/ProvidedSystems/StokesFlow/src/Stokes_SLE.h b/underworld/libUnderworld/StgFEM/SLE/ProvidedSystems/StokesFlow/src/Stokes_SLE.h index 6e9452336..9b2e78d41 100644 --- a/underworld/libUnderworld/StgFEM/SLE/ProvidedSystems/StokesFlow/src/Stokes_SLE.h +++ b/underworld/libUnderworld/StgFEM/SLE/ProvidedSystems/StokesFlow/src/Stokes_SLE.h @@ -32,6 +32,7 @@ SolutionVector* pSolnVec; /** pressure vector */\ ForceVector* fForceVec; /** forcing term vector */\ ForceVector* hForceVec; /** continuity force vector */\ + StiffnessMatrix* rMat; /** special rotation matrix */ \ /* the following are to help choose a "fudge" factor to remove null-space from Jacobian in rheology */\ double fnorm; /* current residual of rhs of Jacobian system J*dx=-F */\ double knorm; /* current norm of stiffness matrix from Jacobian */ diff --git a/underworld/libUnderworld/StgFEM/SLE/SystemSetup/src/ForceVector.c b/underworld/libUnderworld/StgFEM/SLE/SystemSetup/src/ForceVector.c index 75d2fed10..ec9124294 100644 --- a/underworld/libUnderworld/StgFEM/SLE/SystemSetup/src/ForceVector.c +++ b/underworld/libUnderworld/StgFEM/SLE/SystemSetup/src/ForceVector.c @@ -10,19 +10,15 @@ #include #include #include -#include "StgFEM/Discretisation/Discretisation.h" +#include #include "types.h" -#include "SolutionVector.h" -#include "ForceVector.h" -#include "ForceTerm.h" - #include #include #include #include -#include "EntryPoint.h" +#include /* Textual name of this class */ const Type ForceVector_Type = "ForceVector"; @@ -98,6 +94,14 @@ void _ForceVector_Init( void* forceVector, Dimension_Index dim, EntryPoint_Regis self->inc = IArray_New(); + self->rotMat = NULL; + self->tmpMat = NULL; + self->rotMatTerm = NULL; +} + +void ForceVector_SetRotationTerm( void* _self, void* rotTerm ) { + ForceVector *self = (ForceVector*)_self; + self->rotMatTerm = (StiffnessMatrixTerm*)Stg_CheckType( rotTerm, StiffnessMatrixTerm ); } void _ForceVector_Delete( void* forceVector ) { @@ -139,6 +143,10 @@ void _ForceVector_Build( void* forceVector, void* data ) { Journal_DPrintfL( self->debug, 2, "Allocating the L.A. Force Vector with %d local entries.\n", self->localSize ); Stream_UnIndentBranch( StgFEM_Debug ); + // alloc some memeory to rotation matrix + int defaultChunk = 24*24; + self->rotMat = ReallocArray( self->rotMat, double, defaultChunk ); + self->tmpMat = ReallocArray( self->tmpMat, double, defaultChunk ); } @@ -156,6 +164,9 @@ void _ForceVector_Destroy( void* forceVector, void* data ) { Memory_Free( self->_assembleForceVectorEPName ); + FreeArray( self->rotMat ); + FreeArray( self->tmpMat ); + /* Don't delete entry point: E.P register will delete it automatically */ Stg_Class_Delete( self->forceTermList ); @@ -166,7 +177,6 @@ void _ForceVector_Destroy( void* forceVector, void* data ) { void ForceVector_Assemble( void* forceVector ) { ForceVector* self = (ForceVector*)forceVector; - int ii; ((FeEntryPoint_AssembleForceVector_CallFunction*)EntryPoint_GetRun( self->assembleForceVector ))( self->assembleForceVector, @@ -272,7 +282,7 @@ void ForceVector_GlobalAssembly_General( void* forceVector ) { /* work out number of dofs at the node, using LM */ /* Since: Number of entries in LM table for this element = (by defn.) Number of dofs this element */ dofCountLastNode = feVar->dofLayout->dofCounts[nodeIdsInCurrElement[nodeCountCurrElement-1]]; - totalDofsThisElement = &elementLM[nodeCountCurrElement-1][dofCountLastNode-1] - &elementLM[0][0] + 1; + totalDofsThisElement = self->totalDofsThisElement = &elementLM[nodeCountCurrElement-1][dofCountLastNode-1] - &elementLM[0][0] + 1; if ( totalDofsThisElement > totalDofsPrevElement ) { if (elForceVecToAdd) Memory_Free( elForceVecToAdd ); @@ -363,16 +373,60 @@ void ForceVector_GlobalAssembly_General( void* forceVector ) { } void ForceVector_AssembleElement( void* forceVector, Element_LocalIndex element_lI, double* elForceVecToAdd ) { - ForceVector* self = (ForceVector*) forceVector; - Index forceTermCount = Stg_ObjectList_Count( self->forceTermList ); - Index forceTerm_I; - ForceTerm* forceTerm; + ForceVector* self = (ForceVector*) forceVector; + Index forceTermCount = Stg_ObjectList_Count( self->forceTermList ); + Index forceTerm_I; + ForceTerm* forceTerm; - for ( forceTerm_I = 0 ; forceTerm_I < forceTermCount ; forceTerm_I++ ) { - forceTerm = (ForceTerm*) Stg_ObjectList_At( self->forceTermList, forceTerm_I ); + for ( forceTerm_I = 0 ; forceTerm_I < forceTermCount ; forceTerm_I++ ) { + forceTerm = (ForceTerm*) Stg_ObjectList_At( self->forceTermList, forceTerm_I ); - ForceTerm_AssembleElement( forceTerm, self, element_lI, elForceVecToAdd ); - } + ForceTerm_AssembleElement( forceTerm, self, element_lI, elForceVecToAdd ); + } + + if( self->feVariable->nonAABCs ) { + /* + Perform [tmp] = [R]^T * [elVecToAdd] + but do it with BLAS (fortran column major ordered) memory layout + therefore compute: [tmp]^T = [elVecToAdd]^T * [[R]^T]^T + */ + + double* R = self->rotMat; + double* tmp = self->tmpMat; + + int rowA = self->totalDofsThisElement; // rows in [R] + int colA = rowA; // cols in [R] + int colB = 1; // cols in [elStiffMatToAdd] + + PetscScalar one=1.0; + PetscScalar zero=0.0; + char t='T'; + char n='N'; + + double *rubbish[27]; + int ii; + + // set up 2D ptr for StiffnessMatrixTerm_AssembleElement + for( ii=0; iirotMatTerm, self->rotMatTerm->stiffnessMatrix, element_lI, NULL, NULL, rubbish ); + + //blasMatrixMult( RT, elForceVecToAdd, 24, 1, 24, tmp ); + // [tmp]^T = [elStiffMatToAdd]^T * [R] + BLASgemm_( &n, &t, + &colB, &rowA, &colA, + &one, elForceVecToAdd, &colB, + R, &rowA, + &zero, tmp, &colB ); + // copy result into returned memory segment + memcpy( elForceVecToAdd, tmp, rowA*colB*sizeof(double) ); + + } } void ForceVector_AddForceTerm( void* forceVector, void* forceTerm ) { diff --git a/underworld/libUnderworld/StgFEM/SLE/SystemSetup/src/ForceVector.h b/underworld/libUnderworld/StgFEM/SLE/SystemSetup/src/ForceVector.h index 8d05de7d7..b6a56982c 100644 --- a/underworld/libUnderworld/StgFEM/SLE/SystemSetup/src/ForceVector.h +++ b/underworld/libUnderworld/StgFEM/SLE/SystemSetup/src/ForceVector.h @@ -31,7 +31,11 @@ Stg_ObjectList* forceTermList; \ Stg_Component* applicationDepExtraInfo; /**< Default is NULL: passed to elForceVec during assembly */\ IArray* inc; \ - + /* rotation matrix data structures */ \ + int totalDofsThisElement; /* assumes constant dofs per element */ \ + double *rotMat; \ + double *tmpMat; \ + StiffnessMatrixTerm* rotMatTerm; struct ForceVector { __ForceVector }; @@ -89,6 +93,9 @@ void _ForceVector_Destroy( void* forceVector, void* data ); + /** set the rotation matrix term to act on this ForceVector */ + void ForceVector_SetRotationTerm( void* _self, void* rotTerm ); + /** Interface to assemble this Force Vector. Calls an entry point, meaning the user can specify if, and then how, it should be assembled. */ void ForceVector_Assemble( void* forceVector ); diff --git a/underworld/libUnderworld/StgFEM/SLE/SystemSetup/src/SolutionVector.c b/underworld/libUnderworld/StgFEM/SLE/SystemSetup/src/SolutionVector.c index c3256769a..58247bf2a 100644 --- a/underworld/libUnderworld/StgFEM/SLE/SystemSetup/src/SolutionVector.c +++ b/underworld/libUnderworld/StgFEM/SLE/SystemSetup/src/SolutionVector.c @@ -106,6 +106,36 @@ void* _SolutionVector_Copy( void* solutionVector, void* dest, Bool deep, Name na return 0; } +double SolutionVector_RemoveVectorSpace( SolutionVector* xVec, SolutionVector* nVec ) { + /* + Remove a vector from another vector. + Assumes vectors are the same size and decomp. + + Returns the scalar projection of xVec in the direction of nVec + + *Is this a useful algorithm or should we just do element-wise subtraction + */ + + PetscScalar dotp, norm, a; + Vec x, n; + + // get PETSC data structures + x = xVec->vector; + n = nVec->vector; + + VecDot( x, n, &dotp); + VecDot( n, n, &norm); + + a = dotp/norm; // scalar projection of vec x in the direction of vec n + VecAXPY( x, -a, n); + + /* DEBUG + { PetscScalar a2 ; VecDot(x,n, &a2); + printf("The scalar projection removed was %g. DotProduct is now %g\n", a, a2 ); } + */ + + return a; +} void _SolutionVector_AssignFromXML( void* solutionVector, Stg_ComponentFactory* cf, void* data ) { SolutionVector* self = (SolutionVector*)solutionVector; diff --git a/underworld/libUnderworld/StgFEM/SLE/SystemSetup/src/SolutionVector.h b/underworld/libUnderworld/StgFEM/SLE/SystemSetup/src/SolutionVector.h index 625273085..34746ac32 100644 --- a/underworld/libUnderworld/StgFEM/SLE/SystemSetup/src/SolutionVector.h +++ b/underworld/libUnderworld/StgFEM/SLE/SystemSetup/src/SolutionVector.h @@ -71,7 +71,7 @@ void* _SolutionVector_Copy( void* solutionVector, void* dest, Bool deep, Name nameExt, PtrMap* ptrMap ); - void _SolutionVector_Build( void* solutionVector, void* data ); + void _SolutionVector_Build( void* solutionVector, void* data ); void _SolutionVector_AssignFromXML( void* solutionVector, Stg_ComponentFactory* cf, void* data ); @@ -87,6 +87,8 @@ void SolutionVector_UpdateSolutionOntoNodes( void* solutionVector ); + /** Remove a vector, nVec, from the vector, xVec. */ + double SolutionVector_RemoveVectorSpace( SolutionVector* xVec, SolutionVector* nVec ); /** Loads the current value at each dof of the feVariable related to this solution vector onto the vector itself */ void SolutionVector_LoadCurrentFeVariableValuesOntoVector( void* solutionVector ); diff --git a/underworld/libUnderworld/StgFEM/SLE/SystemSetup/src/StiffnessMatrix.c b/underworld/libUnderworld/StgFEM/SLE/SystemSetup/src/StiffnessMatrix.c index 37c943549..181023053 100644 --- a/underworld/libUnderworld/StgFEM/SLE/SystemSetup/src/StiffnessMatrix.c +++ b/underworld/libUnderworld/StgFEM/SLE/SystemSetup/src/StiffnessMatrix.c @@ -28,6 +28,9 @@ #include "SolutionVector.h" #include "ForceVector.h" +#include + + void __StiffnessMatrix_NewAssemble( void* stiffnessMatrix,void* _sle, void* _context ); /* Textual name of this class */ @@ -178,10 +181,7 @@ void _StiffnessMatrix_Init( self->stiffnessMatrixTermList = Stg_ObjectList_New(); /* Set default function for Global Stiffness Matrix Assembly */ - if ( self->assembleOnNodes ) - self->_assemblyFunction = __StiffnessMatrix_NewAssembleNodeWise; - else - self->_assemblyFunction = __StiffnessMatrix_NewAssemble; + self->_assemblyFunction = __StiffnessMatrix_NewAssemble; self->elStiffMat = NULL; self->bcVals = NULL; @@ -192,6 +192,11 @@ void _StiffnessMatrix_Init( self->rowInc = IArray_New(); self->colInc = IArray_New(); + self->rotMat = NULL; + self->tmpMat = NULL; + + self->rotMatTerm = NULL; + self->matrix = PETSC_NULL; } @@ -315,6 +320,10 @@ void _StiffnessMatrix_Build( void* stiffnessMatrix, void* data ) { StiffnessMatrix_RefreshMatrix( self ); + // alloc some memory to these guys + self->rotMat = ReallocArray( self->rotMat, double, (24*24) ); + self->tmpMat = ReallocArray( self->tmpMat, double, (24*24) ); + } @@ -347,6 +356,9 @@ void _StiffnessMatrix_Destroy( void* stiffnessMatrix, void* data ) { FreeArray( self->diagonalNonZeroIndices ); FreeArray( self->offDiagonalNonZeroIndices ); + FreeArray( self->rotMat ); + FreeArray( self->tmpMat ); + /* Don't delete entry points: E.P. register will delete them automatically */ Stg_Class_Delete( self->rowInc ); Stg_Class_Delete( self->colInc ); @@ -364,190 +376,12 @@ void StiffnessMatrix_Assemble( void* stiffnessMatrix, void* _sle, void* _context } - -void __StiffnessMatrix_NewAssembleNodeWise( void* stiffnessMatrix, void* _sle, void* _context ) { -#if 0 - const double one = 1.0; - StiffnessMatrix* self = (StiffnessMatrix*)stiffnessMatrix; - SystemLinearEquations* sle = (SystemLinearEquations*)_sle; - FeVariable *rowVar, *colVar; - FeMesh *rowMesh, *colMesh; - FeEquationNumber *rowEqNum, *colEqNum; - DofLayout *rowDofs, *colDofs; - unsigned nRowEls; - int nRowNodes, *rowNodes; - int nColNodes, *colNodes; - unsigned maxDofs, maxRCDofs, nDofs, nRowDofs, nColDofs; - double** nStiffMat; - double* bcVals; - Mat matrix = self->matrix; - Vec vector, transVector; - int nRowNodeDofs, nColNodeDofs; - int rowInd, colInd; - double bc; - unsigned e_i, n_i, dof_i, n_j, dof_j; - - assert( self && Stg_CheckType( self, StiffnessMatrix ) ); - - rowVar = self->rowVariable; - colVar = self->columnVariable ? self->columnVariable : rowVar; - rowEqNum = rowVar->eqNum; colEqNum = colVar->eqNum; - rowMesh = rowVar->feMesh; colMesh = colVar->feMesh; - rowDofs = rowVar->dofLayout; colDofs = colVar->dofLayout; - - nRowNodes = FeMesh_GetNodeLocalSize( rowMesh ); - assert( (rowVar == colVar) ? !self->transRHS : 1 ); - - vector = self->rhs ? self->rhs->vector : NULL; - transVector = self->transRHS ? self->transRHS->vector : NULL; - nStiffMat = NULL; - bcVals = NULL; - maxDofs = 0; - - nStiffMat = ReallocArray2D( nStiffMat, double, nRowDofs, nColDofs ); - - /* Begin assembling each element. */ - for( n_i = 0; n_i < nRowNodes; n_i++ ) { - /* Do we need more space to assemble this element? */ - nRowDofs = rowDofs->dofCounts[n_i]; - nColDofs = colDofs->dofCounts[n_i]; - nDofs = nRowDofs * nColDofs; - - if( nDofs > maxDofs ) { - maxRCDofs = (nRowDofs > nColDofs) ? nRowDofs : nColDofs; - nStiffMat = ReallocArray2D( nStiffMat, double, nRowDofs, nColDofs ); - bcVals = ReallocArray( bcVals, double, maxRCDofs ); - maxDofs = nDofs; - self->elStiffMat = nStiffMat; - self->bcVals = bcVals; - } - - /* Assemble the element. */ - memset( nStiffMat[0], 0, nDofs * sizeof(double) ); - StiffnessMatrix_AssembleElement( self, n_i, sle, _context, nStiffMat ); - - MatSetValues( self->matrix, - nRowDofs, rowEqNum->mapNodeDof2Eq[n_i], - nColDofs, colEqNum->mapNodeDof2Eq[n_i], - nStiffMat[0], INSERT_VALUES ); - /* Correct for BCs providing I'm not keeping them in. */ - // if( vector ) { - // memset( bcVals, 0, nRowDofs * sizeof(double) ); - // - // rowInd = 0; - // for( n_i = 0; n_i < nRowNodes; n_i++ ) { - // nRowNodeDofs = rowDofs->dofCounts[rowNodes[n_i]]; - // for( dof_i = 0; dof_i < nRowNodeDofs; dof_i++ ) { - // if( !FeVariable_IsBC( rowVar, rowNodes[n_i], dof_i ) ) { - // colInd = 0; - // for( n_j = 0; n_j < nColNodes; n_j++ ) { - // nColNodeDofs = colDofs->dofCounts[colNodes[n_j]]; - // for( dof_j = 0; dof_j < nColNodeDofs; dof_j++ ) { - // if( FeVariable_IsBC( colVar, colNodes[n_j], dof_j ) ) { - // bc = DofLayout_GetValueDouble( colDofs, colNodes[n_j], dof_j ); - // bcVals[rowInd] -= bc * elStiffMat[rowInd][colInd]; - // } - // colInd++; - // } - // } - // } - // rowInd++; - // } - // } - // - // VecSetValues( vector, nRowDofs, (int*)rowEqNum->locationMatrix[e_i][0], bcVals, ADD_VALUES ); - // } - // if( transVector ) { - // memset( bcVals, 0, nColDofs * sizeof(double) ); - // - // colInd = 0; - // for( n_i = 0; n_i < nColNodes; n_i++ ) { - // nColNodeDofs = colDofs->dofCounts[colNodes[n_i]]; - // for( dof_i = 0; dof_i < nColNodeDofs; dof_i++ ) { - // if( !FeVariable_IsBC( colVar, colNodes[n_i], dof_i ) ) { - // rowInd = 0; - // for( n_j = 0; n_j < nRowNodes; n_j++ ) { - // nRowNodeDofs = rowDofs->dofCounts[rowNodes[n_j]]; - // for( dof_j = 0; dof_j < nRowNodeDofs; dof_j++ ) { - // if( FeVariable_IsBC( rowVar, rowNodes[n_j], dof_j ) ) { - // bc = DofLayout_GetValueDouble( rowDofs, rowNodes[n_j], dof_j ); - // bcVals[colInd] -= bc * elStiffMat[rowInd][colInd]; - // } - // rowInd++; - // } - // } - // } - // colInd++; - // } - // } - // - // VecSetValues( transVector, nColDofs, (int*)colEqNum->locationMatrix[e_i][0], bcVals, ADD_VALUES ); - // } - - /* If keeping BCs in, zero corresponding entries in the element stiffness matrix. */ - // if( !rowEqNum->removeBCs || !colEqNum->removeBCs ) { - // rowInd = 0; - // for( n_i = 0; n_i < nRowNodes; n_i++ ) { - // nRowNodeDofs = rowDofs->dofCounts[rowNodes[n_i]]; - // for( dof_i = 0; dof_i < nRowNodeDofs; dof_i++ ) { - // if( FeVariable_IsBC( rowVar, rowNodes[n_i], dof_i ) ) { - // memset( nStiffMat[rowInd], 0, nColDofs * sizeof(double) ); - // } - // else { - // colInd = 0; - // for( n_j = 0; n_j < nColNodes; n_j++ ) { - // nColNodeDofs = colDofs->dofCounts[colNodes[n_j]]; - // for( dof_j = 0; dof_j < nColNodeDofs; dof_j++ ) { - // if( FeVariable_IsBC( colVar, colNodes[n_j], dof_j ) ) - // nStiffMat[rowInd][colInd] = 0.0; - // colInd++; - // } - // } - // } - // rowInd++; - // } - // } - // } - - /* Add to stiffness matrix. */ - // MatSetValues( matrix, - // nRowDofs, (int*)rowEqNum->locationMatrix[e_i][0], - // nColDofs, (int*)colEqNum->locationMatrix[e_i][0], - // nStiffMat[0], ADD_VALUES ); - } - - FreeArray( nStiffMat ); - FreeArray( bcVals ); - - /* If keeping BCs in and rows and columnns use the same variable, put ones in all BC'd diagonals. */ - // if( !colEqNum->removeBCs && rowVar == colVar ) { - // for( n_i = 0; n_i < FeMesh_GetNodeLocalSize( colMesh ); n_i++ ) { - // nColNodeDofs = colDofs->dofCounts[n_i]; - // for( dof_i = 0; dof_i < nColNodeDofs; dof_i++ ) { - // if( FeVariable_IsBC( colVar, n_i, dof_i ) ) { - // MatSetValues( self->matrix, - // 1, colEqNum->mapNodeDof2Eq[n_i] + dof_i, - // 1, colEqNum->mapNodeDof2Eq[n_i] + dof_i, - // (double*)&one, ADD_VALUES ); - // } - // } - // } - // } - - /* Reassemble the matrix and vectors. */ - MatAssemblyBegin( matrix, MAT_FINAL_ASSEMBLY ); - MatAssemblyEnd( matrix, MAT_FINAL_ASSEMBLY ); - if( vector ) { - VecAssemblyBegin( vector ); - VecAssemblyEnd( vector ); - } - if( transVector) { - VecAssemblyBegin( transVector ); - VecAssemblyEnd( transVector ); - } - -#endif +void StiffnessMatrix_SetRotationTerm( void* mat, void* rotTerm ) { + StiffnessMatrix* self = (StiffnessMatrix*)mat; + // set the ptr manually + self->rotMatTerm = (StiffnessMatrixTerm*)rotTerm; } + /* Callback version */ void __StiffnessMatrix_NewAssemble( void* stiffnessMatrix, void* _sle, void* _context ) { const double one = 1.0; @@ -703,10 +537,18 @@ void __StiffnessMatrix_NewAssemble( void* stiffnessMatrix, void* _sle, void* _co } /* Add to stiffness matrix. */ - MatSetValues( matrix, - nRowDofs, (int*)rowEqNum->locationMatrix[e_i][0], - nColDofs, (int*)colEqNum->locationMatrix[e_i][0], - elStiffMat[0], ADD_VALUES ); + if( self->assembleOnNodes ) { + MatSetValues( matrix, + nRowDofs, (int*)rowEqNum->locationMatrix[e_i][0], + nColDofs, (int*)colEqNum->locationMatrix[e_i][0], + elStiffMat[0], INSERT_VALUES ); + } else { + MatSetValues( matrix, + nRowDofs, (int*)rowEqNum->locationMatrix[e_i][0], + nColDofs, (int*)colEqNum->locationMatrix[e_i][0], + elStiffMat[0], ADD_VALUES ); + + } } FreeArray( elStiffMat ); @@ -795,6 +637,28 @@ void _StiffnessMatrix_PrintElementStiffnessMatrix( } } +void blasMatrixMult( double A[], double B[], int rowA, int colB, int colA, double *C ) +{ + /*@ + Performs [C] = [A]*[B], + + where [A], [B], [C] are single arrays, with rowMajor ordering, so they can be vectors. + [C] must be pre allocated + @*/ + char n='N'; + PetscScalar zero=0.0; + PetscScalar alpha=1.0; + + /* BLASgemm expects fortan memory chunks, so the problem is formed using + the matrix transpose formula [C]^T = [B]^T * [A]^T */ + + BLASgemm_( &n, &n, + &colB, &rowA, &colA, + &alpha, B, &colB, + A, &colA, &zero, + C, &colB ); +} + void StiffnessMatrix_AssembleElement( void* stiffnessMatrix, Element_LocalIndex element_lI, @@ -807,10 +671,93 @@ void StiffnessMatrix_AssembleElement( Index stiffnessMatrixTerm_I; StiffnessMatrixTerm* stiffnessMatrixTerm; + FeVariable* rowVar=self->rowVariable; + FeVariable* colVar=self->columnVariable; + + double* R = self->rotMat; + double* tmp = self->tmpMat; + for ( stiffnessMatrixTerm_I = 0 ; stiffnessMatrixTerm_I < stiffnessMatrixTermCount ; stiffnessMatrixTerm_I++ ) { stiffnessMatrixTerm = (StiffnessMatrixTerm*) Stg_ObjectList_At( self->stiffnessMatrixTermList, stiffnessMatrixTerm_I ); StiffnessMatrixTerm_AssembleElement( stiffnessMatrixTerm, self, element_lI, sle, context, elStiffMatToAdd ); } + + // if no rotation matrix term finish + if( !self->rotMatTerm ) return; + + /* check if we need to PRE MULTIPLY with rotation matrix, [R] - + only if rowVariable has non axis aligned BCs and the element is on the mesh boundary */ + if( rowVar->nonAABCs ) + { + + /* Perform [tmp] = [R]^T * [elStiffMatToAdd] + but do it with BLAS (fortran column major ordered) memory layout + therefore compute: [tmp]^T = [elStiffMatToAdd]^T * [[R]^T]^T + */ + + int rowA = (self->nRowDofs > self->nColDofs) ? self->nRowDofs : self->nColDofs; // rows in [R] + int colA = rowA; // cols in [R] + int colB = self->nColDofs; // cols in [elStiffMatToAdd] + + PetscScalar one=1.0; + PetscScalar zero=0.0; + char n='N'; + char t='T'; + double *rubbish[27]; + int ii; + + // initialise [R] and [tmp] memory + memset(R,0,rowA*rowA*sizeof(double)); + // set up 2D ptr for StiffnessMatrixTerm_AssembleElement + for( ii=0; iirotMatTerm, self, element_lI, sle, context, rubbish ); + + // [tmp]^T = [elStiffMatToAdd]^T * [R] + // only using BLASgemm_ [not blasMatrixMult()] because transposed matrices are used + BLASgemm_( &n, &t, &colB, &rowA, &colA, &one,elStiffMatToAdd[0], &colB, R, &colA, &zero, tmp, &colB ); + + // copy result into returned memory segment + memcpy( elStiffMatToAdd[0], tmp, rowA*colB*sizeof(double) ); + } + + + /* check if we need to POST MULTIPLY with rotation matrix - + only if columnVariable has non axis aligned BCs and the element is on the mesh boundary */ + if( colVar->nonAABCs ) + { + /* + Perform [tmp] = [elStiffMatToAdd] * [R] + but do it with BLAS (fortran column major ordered) memory layout + therefore compute: [tmp]^T = [R]^T * [elStiffMatToAdd]^T + */ + + int rowA = self->nRowDofs; // rows in [elStiffMatToAdd] + int colB = (self->nRowDofs > self->nColDofs) ? self->nRowDofs : self->nColDofs; // colB in [R] + int colA = self->nColDofs; // cols in [R] + + double *rubbish[27]; + int ii; + + // initialise [R] and [tmp] memory + memset(R,0,rowA*rowA*sizeof(double)); + // set up 2D ptr for StiffnessMatrixTerm_AssembleElement + for( ii=0; iirotMatTerm, self, element_lI, sle, context, rubbish ); + memset(tmp,0,rowA*rowA*sizeof(double)); + + // tmp = [elStiffMatToAdd] * [R] + blasMatrixMult( elStiffMatToAdd[0], R, rowA, colB, colA, tmp ); + + memcpy( elStiffMatToAdd[0], tmp, rowA*colB*sizeof(double) ); + } } void StiffnessMatrix_AddStiffnessMatrixTerm( void* stiffnessMatrix, void* stiffnessMatrixTerm ) { diff --git a/underworld/libUnderworld/StgFEM/SLE/SystemSetup/src/StiffnessMatrix.h b/underworld/libUnderworld/StgFEM/SLE/SystemSetup/src/StiffnessMatrix.h index 76a41394e..3e81eb1be 100644 --- a/underworld/libUnderworld/StgFEM/SLE/SystemSetup/src/StiffnessMatrix.h +++ b/underworld/libUnderworld/StgFEM/SLE/SystemSetup/src/StiffnessMatrix.h @@ -66,6 +66,9 @@ \ IArray* rowInc; \ IArray* colInc; \ + double* rotMat; \ + double* tmpMat; \ + StiffnessMatrixTerm* rotMatTerm; struct StiffnessMatrix { __StiffnessMatrix }; @@ -165,7 +168,6 @@ /** Interface to Build stiffness matrix. Calls an entry point, allowing user to specialise exactly what should be assembled at run-time. */ void StiffnessMatrix_Assemble( void* stiffnessMatrix, void* _sle, void* _context ); - void __StiffnessMatrix_NewAssembleNodeWise( void* stiffnessMatrix, void* _sle, void* _context ); /* +++ Public Functions +++ */ @@ -194,4 +196,7 @@ void StiffnessMatrix_CalcNonZeros( void* stiffnessMatrix ); + void StiffnessMatrix_SetRotationTerm(void* self, void* rotTerm); + void blasMatrixMult( double A[], double B[], int rowA, int colB, int colA, double *C ); + #endif /* __StgFEM_SLE_SystemSetup_StiffnessMatrix_h__ */ diff --git a/underworld/libUnderworld/Underworld/Function/src/Binary.cpp b/underworld/libUnderworld/Underworld/Function/src/Binary.cpp index dd76df1f3..e554ab154 100644 --- a/underworld/libUnderworld/Underworld/Function/src/Binary.cpp +++ b/underworld/libUnderworld/Underworld/Function/src/Binary.cpp @@ -181,6 +181,35 @@ Fn::Divide::func Fn::Divide::getFunction( IOsptr sample_input ) }; } +Fn::Atan2::func Fn::Atan2::getFunction( IOsptr sample_input ) +{ + const IO_double* doubleio[2]; + func _func[2]; + initGetFunction( sample_input, doubleio, _func ); + + bool equalSize = doubleio[0]->size() == doubleio[1]->size(); + + if( !equalSize or (doubleio[0]->size()!=1) ) + throw std::invalid_argument("atan2 must have scalar objects or equal size."); + + // allocate memory for our output + auto _output_sp = std::make_shared(doubleio[0]->size(), doubleio[0]->iotype()); + // get the raw pointer from the shared pointer + auto _output = _output_sp.get(); + + // create and return the lambda + unsigned size = doubleio[1]->size(); + return [_func, _output, _output_sp, size](IOsptr input)->IOsptr { + const IO_double* io1 = debug_dynamic_cast( _func[0](input) ) ; + const IO_double* io2 = debug_dynamic_cast( _func[1](input) ) ; + + // perform sum + for (unsigned ii=0; iiat(ii) = atan2(io1->at(ii),io2->at(ii)); + } + return debug_dynamic_cast(_output); + }; +} Fn::Dot::func Fn::Dot::getFunction( IOsptr sample_input ) { const IO_double* doubleio[2]; diff --git a/underworld/libUnderworld/Underworld/Function/src/Binary.hpp b/underworld/libUnderworld/Underworld/Function/src/Binary.hpp index 898b1a62f..b1c003e9f 100644 --- a/underworld/libUnderworld/Underworld/Function/src/Binary.hpp +++ b/underworld/libUnderworld/Underworld/Function/src/Binary.hpp @@ -93,6 +93,14 @@ namespace Fn { virtual ~Max(){}; }; + class Atan2: public Binary + { + public: + Atan2( Function *fn1, Function *fn2 ) : Binary( fn1, fn2) {}; + virtual func getFunction( IOsptr sample_input ); + virtual ~Atan2(){}; + }; + } #endif /* __Underworld_Function_Binary_hpp__ */ diff --git a/underworld/libUnderworld/Underworld/Utils/src/Init.c b/underworld/libUnderworld/Underworld/Utils/src/Init.c index ce2a5c5ce..329c50adf 100644 --- a/underworld/libUnderworld/Underworld/Utils/src/Init.c +++ b/underworld/libUnderworld/Underworld/Utils/src/Init.c @@ -43,6 +43,10 @@ Bool Underworld_Utils_Init( int* argc, char** argv[] ) { Stg_ComponentRegister_Add( componentRegister, VectorAssemblyTerm_NA_j__Fn_ij_Type, (Name)"0", _VectorAssemblyTerm_NA_j__Fn_ij_DefaultNew ); RegisterParent( VectorAssemblyTerm_NA_j__Fn_ij_Type, ForceTerm_Type ); + + Stg_ComponentRegister_Add( Stg_ComponentRegister_Get_ComponentRegister(), MatrixAssemblyTerm_RotationDof_Type, (Name)"0", _MatrixAssemblyTerm_RotationDof_DefaultNew ); + RegisterParent(MatrixAssemblyTerm_RotationDof_Type, StiffnessMatrixTerm_Type); + return True; } diff --git a/underworld/libUnderworld/Underworld/Utils/src/MatrixAssemblyTerm_RotationDof.cpp b/underworld/libUnderworld/Underworld/Utils/src/MatrixAssemblyTerm_RotationDof.cpp new file mode 100644 index 000000000..185294647 --- /dev/null +++ b/underworld/libUnderworld/Underworld/Utils/src/MatrixAssemblyTerm_RotationDof.cpp @@ -0,0 +1,240 @@ +/*~*~*~*~*~*~*~*~*~*~*~*~*~*~*~*~*~*~*~*~*~*~*~*~*~*~*~*~*~*~*~*~*~*~*~*~*~*~*~*~*~*~* +** ** +** 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. ** +** ** +**~*~*~*~*~*~*~*~*~*~*~*~*~*~*~*~*~*~*~*~*~*~*~*~*~*~*~*~*~*~*~*~*~*~*~*~*~*~*~*~*~*~*/ +#include +#include +#include + +#include +#include + +#include +#include +#include +#include +#include + +#include "MatrixAssemblyTerm_RotationDof.h" + + +/* Textual name of this class */ +const Type MatrixAssemblyTerm_RotationDof_Type = (char*)"MatrixAssemblyTerm_RotationDof"; + +/* Creation implementation / Virtual constructor */ +MatrixAssemblyTerm_RotationDof* _MatrixAssemblyTerm_RotationDof_New( MatrixAssemblyTerm_RotationDof_DEFARGS ) { + MatrixAssemblyTerm_RotationDof* self; + /* Allocate memory */ + assert( _sizeOfSelf >= sizeof(MatrixAssemblyTerm_RotationDof) ); + self = (MatrixAssemblyTerm_RotationDof*) _StiffnessMatrixTerm_New( STIFFNESSMATRIXTERM_PASSARGS ); + +/* Virtual info */ + self->cppdata = (void*) new MatrixAssemblyTerm_RotationDof_cppdata; + self->max_nElNodes_col = 0; + self->max_nElNodes_row = 0; + self->Ni = NULL; + self->Mi = NULL; + self->geometryMesh = NULL; + + return self; +} + +void MatrixAssemblyTerm_RotationDof_SetEFn( void* _self, int i, Fn::Function* fn ){ + /* + * Set the underworld functions which define the new basis vectors for the rotations + */ + MatrixAssemblyTerm_RotationDof* self = (MatrixAssemblyTerm_RotationDof*)_self; + FeMesh *mesh = (FeMesh*)self->geometryMesh; + unsigned dim = Mesh_GetDimSize(mesh); + + MatrixAssemblyTerm_RotationDof_cppdata* cppdata = (MatrixAssemblyTerm_RotationDof_cppdata*) self->cppdata; + // record fn to struct + cppdata->input = std::make_shared((void*)mesh ); + cppdata->eVecFn[i] = fn->getFunction(cppdata->input.get()); + + // check output conforms + const IO_double *testOutput = dynamic_cast(cppdata->eVecFn[i](cppdata->input.get())); + if( !testOutput || testOutput->size() != dim ) { + std::stringstream ss; + ss << "Expected 'fn' for " << __func__ << " must be of length " << dim; + throw std::invalid_argument(ss.str()); + } +} + +void _MatrixAssemblyTerm_RotationDof_Delete( void* matrixTerm ) { + MatrixAssemblyTerm_RotationDof* self = (MatrixAssemblyTerm_RotationDof*)matrixTerm; + + _StiffnessMatrixTerm_Delete( self ); +} + +void* _MatrixAssemblyTerm_RotationDof_DefaultNew( Name name ) { + /* Variables set in this function */ + SizeT _sizeOfSelf = sizeof(MatrixAssemblyTerm_RotationDof); + Type type = MatrixAssemblyTerm_RotationDof_Type; + Stg_Class_DeleteFunction* _delete = _MatrixAssemblyTerm_RotationDof_Delete; + Stg_Class_PrintFunction* _print = NULL; + Stg_Class_CopyFunction* _copy = NULL; + Stg_Component_DefaultConstructorFunction* _defaultConstructor = _MatrixAssemblyTerm_RotationDof_DefaultNew; + Stg_Component_ConstructFunction* _construct = _MatrixAssemblyTerm_RotationDof_AssignFromXML; + Stg_Component_BuildFunction* _build = _MatrixAssemblyTerm_RotationDof_Build; + Stg_Component_InitialiseFunction* _initialise = _MatrixAssemblyTerm_RotationDof_Initialise; + Stg_Component_ExecuteFunction* _execute = _MatrixAssemblyTerm_RotationDof_Execute; + Stg_Component_DestroyFunction* _destroy = _MatrixAssemblyTerm_RotationDof_Destroy; + StiffnessMatrixTerm_AssembleElementFunction* _assembleElement = _MatrixAssemblyTerm_RotationDof_AssembleElement; + AllocationType nameAllocationType = NON_GLOBAL /* default value NON_GLOBAL */; + + return (void*)_MatrixAssemblyTerm_RotationDof_New( MatrixAssemblyTerm_RotationDof_PASSARGS ); +} + +void _MatrixAssemblyTerm_RotationDof_AssignFromXML( void* matrixTerm, Stg_ComponentFactory* cf, void* data ) { + MatrixAssemblyTerm_RotationDof* self = (MatrixAssemblyTerm_RotationDof*)matrixTerm; + /* Construct Parent */ + _StiffnessMatrixTerm_AssignFromXML( self, cf, data ); + +} + +void _MatrixAssemblyTerm_RotationDof_Build( void* matrixTerm, void* data ) { + MatrixAssemblyTerm_RotationDof* self = (MatrixAssemblyTerm_RotationDof*)matrixTerm; + _StiffnessMatrixTerm_Build( self, data ); + + self->Ni = new double[27]; + self->Mi = new double[27]; + self->inc = IArray_New( ); +} + +void _MatrixAssemblyTerm_RotationDof_Initialise( void* matrixTerm, void* data ) { + MatrixAssemblyTerm_RotationDof* self = (MatrixAssemblyTerm_RotationDof*)matrixTerm; + _StiffnessMatrixTerm_Initialise( self, data ); +} + +void _MatrixAssemblyTerm_RotationDof_Execute( void* matrixTerm, void* data ) { + _StiffnessMatrixTerm_Execute( matrixTerm, data ); +} + +void _MatrixAssemblyTerm_RotationDof_Destroy( void* matrixTerm, void* data ) { + MatrixAssemblyTerm_RotationDof* self = (MatrixAssemblyTerm_RotationDof*)matrixTerm; + + Stg_Class_Delete( self->inc ); + + delete self->Ni; + delete self->Mi; + delete (MatrixAssemblyTerm_RotationDof_cppdata*)self->cppdata; + + _StiffnessMatrixTerm_Destroy( matrixTerm, data ); +} + + +void AXequalsY( StiffnessMatrix* a, SolutionVector* x, SolutionVector* y, Bool transpose ) { + Mat Amat; + Vec X, Y; + PetscInt m,n,vecSize; + PetscScalar *ptr; + + Amat = a->matrix; + X = x->vector; + Y = y->vector; + + MatGetSize(Amat,&m,&n); + VecGetSize(X, &vecSize); + VecGetArray(X,&ptr); + VecRestoreArray(X, &ptr); + + if (transpose) + MatMultTranspose(Amat,X,Y); + else + MatMult(Amat,X,Y); + + SolutionVector_UpdateSolutionOntoNodes( y ); +} + +PetscErrorCode AXequalsX( StiffnessMatrix* a, SolutionVector* x, Bool transpose ) { + Mat Amat; + Vec X, Y; + PetscErrorCode ierr; + + PetscFunctionBeginUser; + Amat = a->matrix; + X = x->vector; + // create Y, duplicate vector of X + VecDuplicate(X, &Y); + VecCopy(X,Y); + + if (transpose) { + ierr = MatMultTranspose(Amat,X,Y); + } else { + ierr = MatMult(Amat,X,Y); + } + // check for non-zero error code manually + Journal_Firewall(ierr==0, NULL, (char *)"Error in AXequalsX(), see terminal command line\n"); + + VecCopy(Y, X); + VecDestroy(&Y); + + SolutionVector_UpdateSolutionOntoNodes( x ); + PetscFunctionReturn(0); +} + +void _MatrixAssemblyTerm_RotationDof_AssembleElement( + void* matrixTerm, + StiffnessMatrix* stiffnessMatrix, + Element_LocalIndex lElement_I, + SystemLinearEquations* sle, + FiniteElementContext* context, + double** elStiffMat ) +{ + MatrixAssemblyTerm_RotationDof* self = (MatrixAssemblyTerm_RotationDof*)matrixTerm; + MatrixAssemblyTerm_RotationDof_cppdata* cppdata = (MatrixAssemblyTerm_RotationDof_cppdata*)self->cppdata; + + FeVariable* variable_row = stiffnessMatrix->rowVariable; + FeMesh* mesh = ( self->geometryMesh ? self->geometryMesh : variable_row->feMesh ); + int dim = variable_row->dim; + + const IO_double *e1_fnout, *e2_fnout, *e3_fnout; // the unit vector uw function ptrs + const double *e1ptr, *e2ptr, *e3ptr; + IArray *inc = self->inc; + int nNbr, *nbr, n_i, row_i, col_i; + + // get this element's nodes, using IArray + Mesh_GetIncidence( mesh, (MeshTopology_Dim)dim, (unsigned)lElement_I, MT_VERTEX, inc ); + nNbr = IArray_GetSize( inc ); + nbr = IArray_GetPtr( inc ); + + // loop over element's nodes + for( n_i=0 ; n_iinput->index() = nbr[n_i]; // get id of node + // indexing into local stiffness matrix + row_i = n_i*dim; + col_i = n_i*dim; + + auto rawPtr = cppdata->input.get(); + // get the 'e1' units vector for the vertex + e1_fnout = debug_dynamic_cast(cppdata->eVecFn[0](rawPtr)); + e1ptr = e1_fnout->data(); + + // get the e2 unit vector + e2_fnout = debug_dynamic_cast(cppdata->eVecFn[1](rawPtr)); + e2ptr = e2_fnout->data(); + + if (dim == 2) { + + elStiffMat[row_i ][col_i ] = e1ptr[0]; elStiffMat[row_i ][col_i+1] = e2ptr[0]; + elStiffMat[row_i+1][col_i ] = e1ptr[1]; elStiffMat[row_i+1][col_i+1] = e2ptr[1]; + + } else { + // assume we can always calulate the 3rd basis vector from the cross product of e1 & e2 + + // get the e2 unit vector + e3_fnout = debug_dynamic_cast(cppdata->eVecFn[2](rawPtr)); + e3ptr = e3_fnout->data(); + + elStiffMat[row_i ][col_i ] = e1ptr[0]; elStiffMat[row_i ][col_i+1] = e2ptr[0]; elStiffMat[row_i ][col_i+2] = e3ptr[0]; + elStiffMat[row_i+1][col_i ] = e1ptr[1]; elStiffMat[row_i+1][col_i+1] = e2ptr[1]; elStiffMat[row_i+1][col_i+2] = e3ptr[1]; + elStiffMat[row_i+2][col_i ] = e1ptr[2]; elStiffMat[row_i+2][col_i+1] = e2ptr[2]; elStiffMat[row_i+2][col_i+2] = e3ptr[2]; + } + + } +} diff --git a/underworld/libUnderworld/Underworld/Utils/src/MatrixAssemblyTerm_RotationDof.h b/underworld/libUnderworld/Underworld/Utils/src/MatrixAssemblyTerm_RotationDof.h new file mode 100644 index 000000000..5ffac67ea --- /dev/null +++ b/underworld/libUnderworld/Underworld/Utils/src/MatrixAssemblyTerm_RotationDof.h @@ -0,0 +1,96 @@ +/*~*~*~*~*~*~*~*~*~*~*~*~*~*~*~*~*~*~*~*~*~*~*~*~*~*~*~*~*~*~*~*~*~*~*~*~*~*~*~*~*~*~* +** ** +** 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. ** +** ** +**~*~*~*~*~*~*~*~*~*~*~*~*~*~*~*~*~*~*~*~*~*~*~*~*~*~*~*~*~*~*~*~*~*~*~*~*~*~*~*~*~*~*/ + + +#ifndef __Underworld_Utils_MatrixAssemblyTerm_RotationDof_h__ +#define __Underworld_Utils_MatrixAssemblyTerm_RotationDof_h__ + +#ifdef __cplusplus + + extern "C++" { + + #include + #include + + struct MatrixAssemblyTerm_RotationDof_cppdata + { + Fn::Function::func eVecFn[3]; + std::shared_ptr input; + }; + + void MatrixAssemblyTerm_RotationDof_SetEFn( void* _self, int i, Fn::Function* fn ); + + } + + extern "C" { +#endif + +#include +#include +#include +#include +#include + /** Textual name of this class */ + extern const Type MatrixAssemblyTerm_RotationDof_Type; + +/** MatrixAssemblyTerm_RotationDof class contents */ +#define __MatrixAssemblyTerm_RotationDof \ + __StiffnessMatrixTerm \ + void* cppdata; \ + IArray* inc; \ + FeMesh* geometryMesh; \ + int max_nElNodes_col; \ + int max_nElNodes_row; \ + double *Ni; \ + double *Mi; \ + + struct MatrixAssemblyTerm_RotationDof { __MatrixAssemblyTerm_RotationDof }; + + +#ifndef ZERO + #define ZERO 0 +#endif + + #define MatrixAssemblyTerm_RotationDof_DEFARGS \ + STIFFNESSMATRIXTERM_DEFARGS + + #define MatrixAssemblyTerm_RotationDof_PASSARGS \ + STIFFNESSMATRIXTERM_PASSARGS + + MatrixAssemblyTerm_RotationDof* _MatrixAssemblyTerm_RotationDof_New( MatrixAssemblyTerm_RotationDof_DEFARGS ); + + void _MatrixAssemblyTerm_RotationDof_Delete( void* matrixTerm ); + + void AXequalsY( StiffnessMatrix* Amat, SolutionVector* x, SolutionVector* y, Bool transpose ); + PetscErrorCode AXequalsX( StiffnessMatrix* Amat, SolutionVector* x, Bool transpose ); + + void* _MatrixAssemblyTerm_RotationDof_DefaultNew( Name name ); + + void _MatrixAssemblyTerm_RotationDof_AssignFromXML( void* matrixTerm, Stg_ComponentFactory* cf, void* data ); + + void _MatrixAssemblyTerm_RotationDof_Build( void* matrixTerm, void* data ); + + void _MatrixAssemblyTerm_RotationDof_Initialise( void* matrixTerm, void* data ); + + void _MatrixAssemblyTerm_RotationDof_Execute( void* matrixTerm, void* data ); + + void _MatrixAssemblyTerm_RotationDof_Destroy( void* matrixTerm, void* data ); + + void _MatrixAssemblyTerm_RotationDof_AssembleElement( + void* matrixTerm, + StiffnessMatrix* stiffnessMatrix, + Element_LocalIndex lElement_I, + SystemLinearEquations* sle, + FiniteElementContext* context, + double** elStiffMat ) ; + +#ifdef __cplusplus + } + #endif +#endif diff --git a/underworld/libUnderworld/Underworld/Utils/src/Utils.h b/underworld/libUnderworld/Underworld/Utils/src/Utils.h index 960359914..2472d731f 100644 --- a/underworld/libUnderworld/Underworld/Utils/src/Utils.h +++ b/underworld/libUnderworld/Underworld/Utils/src/Utils.h @@ -19,6 +19,7 @@ #include "VectorAssemblyTerm_NA_j__Fn_ij.h" #include "MatrixAssemblyTerm_NA_i__NB_i__Fn.h" #include "MatrixAssemblyTerm_NA__NB__Fn.h" + #include "MatrixAssemblyTerm_RotationDof.h" #include "Fn_Integrate.h" #include "Exceptions.h" diff --git a/underworld/libUnderworld/Underworld/Utils/src/types.h b/underworld/libUnderworld/Underworld/Utils/src/types.h index 721dab92b..1003d2946 100644 --- a/underworld/libUnderworld/Underworld/Utils/src/types.h +++ b/underworld/libUnderworld/Underworld/Utils/src/types.h @@ -17,6 +17,8 @@ typedef struct VectorAssemblyTerm_NA_j__Fn_ij VectorAssemblyTerm_NA_j__Fn_ij; typedef struct MatrixAssemblyTerm_NA_i__NB_i__Fn MatrixAssemblyTerm_NA_i__NB_i__Fn; typedef struct MatrixAssemblyTerm_NA__NB__Fn MatrixAssemblyTerm_NA__NB__Fn; + typedef struct MatrixAssemblyTerm_RotationDof MatrixAssemblyTerm_RotationDof; + typedef struct Fn_Integrate Fn_Integrate; #endif diff --git a/underworld/libUnderworld/libUnderworldPy/Underworld.i b/underworld/libUnderworld/libUnderworldPy/Underworld.i index 578ea43fa..f27bab0e6 100644 --- a/underworld/libUnderworld/libUnderworldPy/Underworld.i +++ b/underworld/libUnderworld/libUnderworldPy/Underworld.i @@ -78,3 +78,4 @@ extern "C" { %include "Utils/VectorAssemblyTerm_NA_j__Fn_ij.h" %include "Utils/MatrixAssemblyTerm_NA_i__NB_i__Fn.h" %include "Utils/MatrixAssemblyTerm_NA__NB__Fn.h" +%include "Utils/MatrixAssemblyTerm_RotationDof.h" diff --git a/underworld/mesh/__init__.py b/underworld/mesh/__init__.py index 44c8decca..aa33272af 100644 --- a/underworld/mesh/__init__.py +++ b/underworld/mesh/__init__.py @@ -12,5 +12,5 @@ """ -from ._mesh import FeMesh, FeMesh_Cartesian, FeMesh_IndexSet, _FeMesh_Regional +from ._mesh import FeMesh, FeMesh_Cartesian, FeMesh_IndexSet, _FeMesh_Regional, FeMesh_Annulus from ._meshvariable import MeshVariable diff --git a/underworld/mesh/_mesh.py b/underworld/mesh/_mesh.py index 9b0f62561..483a10ae8 100644 --- a/underworld/mesh/_mesh.py +++ b/underworld/mesh/_mesh.py @@ -1335,3 +1335,271 @@ def _setup(self): # (x,y,r) = (np.tan(np.pi*coord[0]/180.0), np.tan(np.pi*coord[1]/180.0), 1) # d = coord[2]/np.sqrt( x**2 + y**2 + 1) # mesh.data[index] = ( d*x, d*y, d) + +class FeMesh_Annulus(FeMesh_Cartesian): + def __init__(self, elementRes=(10,16), radialLengths=(3.0,6.0), angularExtent=[0.0,360.0], centroid=[0.0,0.0], periodic=[False, True], **kwargs): + """ + This class generates a 2D finite element mesh which is topologically cartesian + and in an annulus geometry. It is possible to directly build a dual mesh by + passing a pair of element types to the constructor. Warning only 'elementTypes' Q1/dQ0 are tested, + use other types at your own risk. + + Class initialiser for Annulus mesh, centered on the 'centroid'. + + MinI_VertexSet / MaxI_VertexSet -> radial walls : [min/max] = [inner/outer] + MinJ_VertexSet / MaxJ_VertexSet -> angular walls : [min/max] = [right/left] + + Parameter + --------- + elementRes : 3-tuple + 1st element - Number of elements across the radial length of the domain + 2nd element - Number of elements along the circumfrance + + radialLengths : 2-tuple, default (3.0,6.0) + The radial position of the inner and outer surfaces respectively. + (inner radialLengths, outer radialLengths) + + angularExtent : 2-tuple, default (0.0,360.0) + The angular extent of the domain, i.e. [15,75], starts at 15 degrees until 75 degrees. + 0 degrees represents the x-axis, i.e. 3 o'clock. + + radialData : Return the mesh node locations in polar form. + (radial length, angle in degrees) + + periodic : 2-tuple, default [False,True] + Sets the periodic boundary conditions along the radial and angular walls, respectively + + See parent classes for further required/optional parameters. + + >>> (radMin, radMax) = (4.0,8.0) + >>> mesh = uw.mesh.FeMesh_Annulus( elementRes=(14, 36), radialLengths=(radMin, radMax), angularExtent=[0.0,180.0] ) + >>> integral = uw.utils.Integral( 1.0, mesh).evaluate()[0] + >>> exact = np.pi*(radMax**2 - radMin**2)/2. + >>> np.fabs(integral-exact)/exact < 1e-1 + True + + """ + + self.has_velocity_null_space = True + + self._centroid = centroid + + self.natural_coords=('r','theta') + self.maskFn = function.misc.constant(1.0) + + self.unitvec_r_Fn = self._fn_unitvec_radial() + self.unitvec_theta_Fn = self._fn_unitvec_tangent() + + self.unitvec_v_Fn = self.unitvec_r_Fn + self.unitvec_h1_Fn = self.unitvec_theta_Fn + + self.radiusFn, self.thetaFn = self._fn_r_theta() + self.v_coord_Fn = self.radiusFn + self.h1_coord_Fn = self.thetaFn + self.h2_coord_Fn = function.misc.constant(0.0) + + + dR = (radialLengths[1]-radialLengths[0]) + iR = (radialLengths[0]) + self.unit_heightFn = (self.radiusFn-iR) / dR + + + errmsg = "Provided 'angularExtent' must be a tuple/list of 2 floats between values [0,360]" + if not isinstance( angularExtent, (tuple,list)): + raise TypeError(errmsg) + if len(angularExtent) != 2: + raise ValueError(errmsg) + for el in angularExtent: + if not isinstance( el, (float,int)) or (el < 0.0 or el > 360.0): + raise TypeError(errmsg) + self._angularExtent = angularExtent + + errmsg = "Provided 'radialLengths' must be a tuple/list of 2 floats" + if not isinstance( radialLengths, (tuple,list)): + raise TypeError(errmsg) + if len(radialLengths) != 2: + raise ValueError(errmsg) + for el in radialLengths: + if not isinstance( el, (float,int)) : + raise TypeError(errmsg) + self._radialLengths = radialLengths + + # build 3D mesh cartesian mesh centred on (0.0,0.0,0.0) - in _setup() we deform the mesh + super(FeMesh_Annulus,self).__init__(elementRes=elementRes, + minCoord=(radialLengths[0],angularExtent[0]), maxCoord=(radialLengths[1],angularExtent[1]), periodic=periodic, **kwargs) + + # define new specialSets, TODO, remove labels that don't make sense for the annulus + + self.specialSets["inner"] = _specialSets_Cartesian.MinI_VertexSet + self.specialSets["outer"] = _specialSets_Cartesian.MaxI_VertexSet + + # These are to provide consistency with other meshes (what of side walls if not 2pi mesh) + self.specialSets["lower_surface_VertexSet"] = _specialSets_Cartesian.MinI_VertexSet + self.specialSets["upper_surface_VertexSet"] = _specialSets_Cartesian.MaxI_VertexSet + + + return + + + + @property + def radialLengths(self): + """ + Returns: + Annulus min/max radius + """ + return self._radialLengths + + @property + def angularExtent(self): + """ + Returns: + Annulus min/max angular extents + """ + return self._angularExtent + + def _fn_unitvec_radial(self): + # returns the radial position + pos = function.coord() + centre = self._centroid + r_vec = pos - centre + mag = function.math.sqrt(function.math.dot( r_vec, r_vec )) + r_vec = r_vec / mag + return r_vec + + def _fn_unitvec_tangent(self): + # returns the radial position + pos = function.coord() + centre = self._centroid + r_vec = pos - centre + theta = [-1.0*r_vec[1], r_vec[0]] + mag = function.math.sqrt(function.math.dot( theta, theta )) + theta = theta / mag + return theta + + @property + def radialData(self): + # returns data in polar form + r = np.sqrt((self.data ** 2).sum(1)) + theta = (180/np.pi)*np.arctan2(self.data[:,1],self.data[:,0]) + return np.array([r,theta]).T + + def _fn_r_theta(self): + pos = function.coord() - self._centroid + rFn = function.math.sqrt(pos[0]**2 + pos[1]**2) + thetaFn = function.math.atan2(pos[1],pos[0]) + return rFn, thetaFn + + + # a function for radial coordinates + @property + def fn_radial(self): + pos = function.coord() + centre = self._centroid + r_vec = pos - centre + return function.math.sqrt(function.math.dot( r_vec, r_vec )) + + def _setup(self): + from underworld import function as fn + + with self.deform_mesh(): + # basic polar coordinate -> cartesian map, i.e. r,t -> x,y + r = self.data[:,0] + t = self.data[:,1] * np.pi/180.0 + + offset_x = self._centroid[0] + offset_y = self._centroid[1] + + (self.data[:,0], self.data[:,1]) = offset_x + r*np.cos(t), offset_y + r*np.sin(t) + + # add a boundary MeshVariable - 1 if nodes is on the boundary(ie 'AllWalls_VertexSet'), 0 if node is internal + self.bndMeshVariable = uw.mesh.MeshVariable(self, 1) + + # set a value 1.0 on provided vertices + self.bndMeshVariable.data[:] = 0. + self.bndMeshVariable.data[self.specialSets["AllWalls_VertexSet"]] = 1.0 + + # note we use this condition to only capture border quadrature points + # on the surface. For points not on the surface the bndMeshVariable will evaluate + # <1.0, so we need to remove those from the integration as well. + self.bnd_vec_normal = function.branching.conditional( + [ ( self.bndMeshVariable > 0.9, self._fn_unitvec_radial() ), + ( True, function.misc.constant(1.0)*(1.0,0.0) ) ] ) + + self.bnd_vec_tangent = function.branching.conditional( + [ ( self.bndMeshVariable > 0.9, self._fn_unitvec_tangent() ), + ( True, function.misc.constant(1.0)*(0.0,1.0) ) ] ) + + # define solid body rotation function for the annulus + + r = function.math.sqrt(function.math.pow(function.coord()[0],2.) + function.math.pow(function.coord()[1],2.)) + self.sbr_fn = r*self._fn_unitvec_tangent() # solid body rotation function + + self._e1 = self.add_variable(nodeDofCount=2) + self._e2 = self.add_variable(nodeDofCount=2) + + self._e1.data[:] = self.bnd_vec_normal.evaluate(self) + self._e2.data[:] = self.bnd_vec_tangent.evaluate(self) + + self.area = uw.utils.Integral(self.maskFn, self).evaluate()[0] + self.full_area = uw.utils.Integral(1.0, self).evaluate()[0] + + ## moments of weight functions used to compute mean / radial gradients in the shell + ## calculate this once at setup time. + self._c0 = uw.utils.Integral(self.unit_heightFn, self).evaluate()[0] / self.area + self._c1 = uw.utils.Integral(self.maskFn*(self.unit_heightFn-self._c0)**2, self).evaluate()[0] + + # Walls by relevant normal ... + + surfaces_e1i_normal_VertexSet = _specialSets_Cartesian.MaxI_VertexSet(self) + _specialSets_Cartesian.MinI_VertexSet(self) + self.specialSets["surfaces_e1_normal_VertexSet"] = lambda selfobject: surfaces_e1i_normal_VertexSet + + surfaces_e2_normal_VertexSet = self.specialSets["Empty"] + self.specialSets["surfaces_e2_normal_VertexSet"] = lambda selfobject: surfaces_e2_normal_VertexSet + + surfaces_e3_normal_VertexSet = self.specialSets["Empty"] + self.specialSets["surfaces_e3_normal_VertexSet"] = lambda selfobject: surfaces_e3_normal_VertexSet + + return + + def mean_value(self, fn=None): + """Returns mean value on the shell of scalar uw function""" + + ## Need to check here that the supplied function is + ## valid and is a scalar. + + mean_value = uw.utils.Integral(fn * self.maskFn, self).evaluate()[0] / self.area + + return mean_value + + def vertical_gradient_value(self, fn=None): + """Returns vertical gradient within the shell of scalar uw function""" + + ## Need to check here that the supplied function is + ## valid and is a scalar. Mask function is to eliminate the + ## effect of the values in the core. + + rGrad = uw.utils.Integral(fn * (self.unit_heightFn-self._c0)*self.maskFn, self).evaluate()[0] / self._c1 + + return rGrad + + + def remove_velocity_null_space(self, vField): + + # This is a sort-of null space ! + vField.data[:,:] *= self.maskFn.evaluate(self) + + # Value of the null space + null_space_v = uw.utils.Integral(function.math.dot( vField, self.unitvec_theta_Fn ) * self.radiusFn * self.maskFn, self).evaluate()[0] + null_space_v /= uw.utils.Integral( (self.radiusFn * self.maskFn)**2, self).evaluate()[0] + + print("1: Null Space Velocity: {}".format(null_space_v)) + + + + # Clean up the solution + vField.data[:,:] -= null_space_v * (self.unitvec_theta_Fn * self.radiusFn * self.maskFn).evaluate(self)[:,:] + + return null_space_v + + diff --git a/underworld/systems/_stokes.py b/underworld/systems/_stokes.py index 23913044f..3f182c6b6 100644 --- a/underworld/systems/_stokes.py +++ b/underworld/systems/_stokes.py @@ -189,7 +189,7 @@ def __init__(self, velocityField, pressureField, fn_viscosity, fn_bodyforce=None # set the bcs on here if not isinstance( cond, uw.conditions.SystemCondition ): raise TypeError( "Provided 'conditions' must be 'SystemCondition' objects." ) - elif type(cond) == uw.conditions.DirichletCondition: + elif isinstance(cond, uw.conditions.DirichletCondition): if cond.variable == self._velocityField: libUnderworld.StgFEM.FeVariable_SetBC( self._velocityField._cself, cond._cself ) elif cond.variable == self._pressureField: @@ -302,6 +302,70 @@ def avtop_pressure_nullspace_removal(): super(Stokes, self).__init__(**kwargs) + for cond in self._conditions: + if isinstance( cond, uw.conditions.CurvilinearDirichletCondition): + self.redefineVelocityDirichletBC(cond.basis_vectors) + + def redefineVelocityDirichletBC(self, basis_vectors): + ''' + Function to hide the implementation of rotating dirichlet boundary conditions. + Here we build a global rotation matrix and a local assembly term for it for 2 reasons. + 1) The assembly term rotates local element contributions immediately after their local evaluation + for the stokes system. This supports the Engelman & Sani idea in, + THE IMPLEMENTATION OF NORMAL AND/OR TANGENTIAL BOUNDARY CONDITIONS IN FINITE + ELEMENT CODES FOR INCOMPRESSIBLE FLUID FLOW, 1982 + 2) The global rotation matrix allows us to rotate the whole velocity field when we like. + Advantagous for this development phase whilst we are designing the workflow of rotated BCS. + + ''' + from underworld import function as fn + + if len(basis_vectors) != self._velocityField.nodeDofCount: + raise ValueError("Inconsistent number of 'basis_vectors' for the velocity field dimensionality") + # does rotMatTerm already exist + if self._kmatrix._cself.rotMatTerm is not None: + return + + mesh = self._velocityField.mesh + + # build 'vns' (the velocity null space) MeshVariable and SolutionVector + vnsField = self._vnsField = self._velocityField.copy() + vnsEqNum = uw.systems.sle.EqNumber( vnsField, False ) + self._vnsVec = uw.systems.sle.SolutionVector(vnsField, vnsEqNum) # store on class + + # evaluate vnsField, check compatibility + if isinstance(mesh, uw.mesh.FeMesh_Annulus): # with FeMesh_Annulus we have the sbr_fn + self._vnsVec.meshVariable.data[:] = mesh.sbr_fn.evaluate(mesh) + + uw.libUnderworld.StgFEM.SolutionVector_LoadCurrentFeVariableValuesOntoVector(self._vnsVec._cself) # store in petsc vec + + # must be done after vnsField creation + self._velocityField._cself.nonAABCs = 1 + + # build a global 're-rotate' matrix + # self._rot = uw.systems.sle.AssembledMatrix( self._velocitySol, self._velocitySol, rhs=None ) + self._rot = uw.systems.sle.AssembledMatrix( self._vnsVec, self._vnsVec, rhs=None ) + gaussSwarm = self._constitMatTerm._integrationSwarm + self._rot._cself.assembleOnNodes = 1 # important doesn't perform FEM integral + + term = self._term = uw.systems.sle.MatrixAssemblyTerm_RotationDof(integrationSwarm=gaussSwarm, + assembledObject = self._rot, + fn_basis=basis_vectors, + mesh=mesh) + + # self._eqNums[self._velocityField]._cself.removeBCs=True + vnsEqNum._cself.removeBCs = True + uw.libUnderworld.StgFEM.StiffnessMatrix_Assemble( + self._rot._cself, + None, None ); + vnsEqNum._cself.removeBCs = False + # self._eqNums[self._velocityField]._cself.removeBCs=False + + # add rotation matrix element terms using the following + uw.libUnderworld.StgFEM.StiffnessMatrix_SetRotationTerm(self._kmatrix._cself, term._cself) + uw.libUnderworld.StgFEM.StiffnessMatrix_SetRotationTerm(self._gmatrix._cself, term._cself) + uw.libUnderworld.StgFEM.ForceVector_SetRotationTerm(self._fvector._cself, term._cself) + def _add_to_stg_dict(self,componentDictionary): # call parents method diff --git a/underworld/systems/sle/__init__.py b/underworld/systems/sle/__init__.py index ea6ffdf46..e71e2e7d2 100644 --- a/underworld/systems/sle/__init__.py +++ b/underworld/systems/sle/__init__.py @@ -17,4 +17,4 @@ ConstitutiveMatrixTerm, AdvDiffResidualVectorTerm, LumpedMassMatrixVectorTerm, \ MatrixAssemblyTerm_NA_i__NB_i__Fn, VectorSurfaceAssemblyTerm_NA__Fn__ni, \ AdvDiffResidualVectorTerm, VectorAssemblyTerm_NA_j__Fn_ij, MatrixAssemblyTerm_NA__NB__Fn, \ - VectorAssemblyTerm_NA_i__Fn_i + MatrixAssemblyTerm_RotationDof, VectorAssemblyTerm_NA_i__Fn_i diff --git a/underworld/systems/sle/_assemblyterm.py b/underworld/systems/sle/_assemblyterm.py index 18addb65c..e12f0352d 100644 --- a/underworld/systems/sle/_assemblyterm.py +++ b/underworld/systems/sle/_assemblyterm.py @@ -344,6 +344,39 @@ def _add_to_stg_dict(self,componentDictionary): # call parents method super(MatrixAssemblyTerm_NA__NB__Fn,self)._add_to_stg_dict(componentDictionary) +class MatrixAssemblyTerm_RotationDof(MatrixAssemblyTerm): + _objectsDict = { "_assemblyterm": " MatrixAssemblyTerm_RotationDof" } + + def __init__(self, fn_basis, mesh, **kwargs): + """ + This assembly term rotates an elemental portion of an SLE. + The 2 (or 3) fn_e1 unit vectors + """ + # build parent + super(MatrixAssemblyTerm_RotationDof,self).__init__(**kwargs) + + dim = mesh.dim + + # we disable these parent attributes + self._set_fn_function = None + self._fn = None + + # save the rotated basis vectors + self._fn_basis = fn_basis + + # set mesh directly + self._cself.geometryMesh = mesh._cself + self._geometryMesh = mesh + + # attach fn_basis vectors to the cpp implementation + for i, fn_X in enumerate(fn_basis): + uw.libUnderworld.Underworld.MatrixAssemblyTerm_RotationDof_SetEFn(self._cself, i, fn_X._fncself) + + def _add_to_stg_dict(self,componentDictionary): + # call parents method + super(MatrixAssemblyTerm_RotationDof,self)._add_to_stg_dict(componentDictionary) + + class LumpedMassMatrixVectorTerm(VectorAssemblyTerm): _objectsDict = { "_assemblyterm": "LumpedMassMatrixForceTerm" } pass From e4cd71c49c64749c5aa1020ff5d8c0f7d44c38f7 Mon Sep 17 00:00:00 2001 From: julian Date: Wed, 27 Jan 2021 14:32:19 +1100 Subject: [PATCH 02/13] Some changes for gadi, testing and pypi package management --- docs/development/run_tests.py | 4 ++-- setup.py | 4 ++-- test_basic.sh | 3 --- test_long.sh | 3 --- 4 files changed, 4 insertions(+), 10 deletions(-) mode change 100644 => 100755 test_long.sh diff --git a/docs/development/run_tests.py b/docs/development/run_tests.py index 42d8495f4..d5b5f3df1 100755 --- a/docs/development/run_tests.py +++ b/docs/development/run_tests.py @@ -178,8 +178,8 @@ def run_file(fname, dir, exe, job): cleanup=False # if prefix args found and it's an ipynb, use 'jupyter nbconvert --execute' if is_ipynb and not args.prepend: - exe = ['jupyter', 'nbconvert', '--ExecutePreprocessor.kernel_name="python3"', - '--ExecutePreprocessor.timeout=10000','--execute', '--stdout'] + exe = ['jupyter', 'nbconvert', '--stdout', '--ExecutePreprocessor.kernel_name=\'python3\'', + '--ExecutePreprocessor.timeout=10000','--to=script', '--execute'] elif is_ipynb and args.prepend: # convert ipynb to py and run with python print("Converting test {} to .py".format(fname)); diff --git a/setup.py b/setup.py index a04e8c995..37d31ecc6 100755 --- a/setup.py +++ b/setup.py @@ -46,7 +46,7 @@ metadata = { 'provides' : ['underworld'], 'zip_safe' : False, - 'install_requires' : ['scons','numpy','mpi4py>=1.2.2', 'h5py'] + 'install_requires' : ['scons==3.1.1','numpy','mpi4py>=1.2.2', 'h5py==2.10'] } def config(prefix, dry_run=False): @@ -197,4 +197,4 @@ def get_outputs(self): packages = ['underworld'], # package_dir = {'': 'config/pypi'}, cmdclass={'install': cmd_install}, - **metadata) \ No newline at end of file + **metadata) diff --git a/test_basic.sh b/test_basic.sh index ac1b613d1..746305939 100755 --- a/test_basic.sh +++ b/test_basic.sh @@ -3,7 +3,4 @@ # ensure we bail on errors set -e -# update the PYTHONPATH -. ./updatePyPath.sh - ./docs/development/run_tests.py ./docs/examples/*.ipynb ./docs/user_guide/*.ipynb ./docs/test/* diff --git a/test_long.sh b/test_long.sh old mode 100644 new mode 100755 index 6ea18bacb..44649c435 --- a/test_long.sh +++ b/test_long.sh @@ -3,9 +3,6 @@ # ensure we bail on errors set -e -# update the PYTHONPATH -. ./updatePyPath.sh - export UW_LONGTEST=1 ./docs/development/run_tests.py ./docs/examples/* ./docs/user_guide/* docs/test/* ./docs/development/run_tests.py --prepend="mpirun -np 2" ./docs/examples/* From 21720aa0c38174a77836142144eae3901da5c8f5 Mon Sep 17 00:00:00 2001 From: julian Date: Wed, 27 Jan 2021 14:35:54 +1100 Subject: [PATCH 03/13] adding SRegion mesh --- underworld/mesh/__init__.py | 2 +- underworld/mesh/_mesh.py | 191 ++++++++++++++++++++++++++++++++++-- 2 files changed, 185 insertions(+), 8 deletions(-) diff --git a/underworld/mesh/__init__.py b/underworld/mesh/__init__.py index aa33272af..fa4e9ade6 100644 --- a/underworld/mesh/__init__.py +++ b/underworld/mesh/__init__.py @@ -12,5 +12,5 @@ """ -from ._mesh import FeMesh, FeMesh_Cartesian, FeMesh_IndexSet, _FeMesh_Regional, FeMesh_Annulus +from ._mesh import FeMesh, FeMesh_Cartesian, FeMesh_IndexSet, _FeMesh_Regional, FeMesh_Annulus, FeMesh_SRegion from ._meshvariable import MeshVariable diff --git a/underworld/mesh/_mesh.py b/underworld/mesh/_mesh.py index 483a10ae8..59ead474d 100644 --- a/underworld/mesh/_mesh.py +++ b/underworld/mesh/_mesh.py @@ -1438,10 +1438,6 @@ def __init__(self, elementRes=(10,16), radialLengths=(3.0,6.0), angularExtent=[0 self.specialSets["upper_surface_VertexSet"] = _specialSets_Cartesian.MaxI_VertexSet - return - - - @property def radialLengths(self): """ @@ -1552,13 +1548,13 @@ def _setup(self): # Walls by relevant normal ... surfaces_e1i_normal_VertexSet = _specialSets_Cartesian.MaxI_VertexSet(self) + _specialSets_Cartesian.MinI_VertexSet(self) - self.specialSets["surfaces_e1_normal_VertexSet"] = lambda selfobject: surfaces_e1i_normal_VertexSet + self.specialSets["surfaces_e1_normal_VertexSet"] = surfaces_e1i_normal_VertexSet surfaces_e2_normal_VertexSet = self.specialSets["Empty"] - self.specialSets["surfaces_e2_normal_VertexSet"] = lambda selfobject: surfaces_e2_normal_VertexSet + self.specialSets["surfaces_e2_normal_VertexSet"] = surfaces_e2_normal_VertexSet surfaces_e3_normal_VertexSet = self.specialSets["Empty"] - self.specialSets["surfaces_e3_normal_VertexSet"] = lambda selfobject: surfaces_e3_normal_VertexSet + self.specialSets["surfaces_e3_normal_VertexSet"] = surfaces_e3_normal_VertexSet return @@ -1602,4 +1598,185 @@ def remove_velocity_null_space(self, vField): return null_space_v +class FeMesh_SRegion(FeMesh_Cartesian): + def __init__(self, elementRes=(16,16,10), radialLengths=(3.0,6.0), latExtent=90.0, longExtent=90.0, centroid=[0.0,0.0,0.0], **kwargs): + """ + Create a Cubed-sphere sixth, centered on the 'centroid'. + + + MinI_VertexSet / MaxI_VertexSet -> longitudinal walls : [min/max] = [west/east] + MinJ_VertexSet / MaxJ_VertexSet -> latitudinal walls : [min/max] = [south/north] + MinK_VertexSet / MaxK_VertexSet -> radial walls : [min/max] = [inner/outer] + + Refer to parent classes for parameters beyond those below. + + Parameter + --------- + elementRes : tuple + Tuple determining number of elements (longitudinally, latitudinally, radially). + radialLengths : tuple + Tuple determining the (inner radialLengths, outer radialLengths). + longExtent : float + The angular extent of the domain between great circles of longitude. + latExtent : float + The angular extent of the domain between great circles of latitude. + + + Example + ------- + + >>> (radMin, radMax) = (4.0,8.0) + >>> mesh = uw.mesh.FeMesh_SRegion( elementRes=(20,20,14), radialLengths=(radMin, radMax) ) + >>> integral = uw.utils.Integral( 1.0, mesh).evaluate()[0] + >>> exact = (4./3.)*np.pi*(radMax**3 - radMin**3) / 6.0 + >>> np.isclose(integral, exact, rtol=0.02) + True + """ + + if not isinstance( latExtent, (float,int) ): + raise TypeError("Provided 'latExtent' must be a float or integer") + self._latExtent = latExtent + if not isinstance( longExtent, (float,int) ): + raise TypeError("Provided 'longExtent' must be a float or integer") + self._longExtent = longExtent + if not isinstance( radialLengths, (tuple,list)): + raise TypeError("Provided 'radialLengths' must be a tuple/list of 2 floats") + if len(radialLengths) != 2: + raise ValueError("Provided 'radialLengths' must be a tuple/list of 2 floats") + for el in radialLengths: + if not isinstance( el, (float,int)) : + raise TypeError("Provided 'radialLengths' must be a tuple/list of 2 floats") + self._radialLengths = radialLengths + + lat_half = latExtent/2.0 + long_half = longExtent/2.0 + + # build 3D mesh cartesian mesh centred on (0.0,0.0,0.0) - in _setup() we deform the mesh + # elementType="Q1/dQ0", + super(FeMesh_SRegion,self).__init__(elementRes=elementRes, + minCoord=(radialLengths[0],-long_half,-lat_half), + maxCoord=(radialLengths[1],long_half,lat_half), periodic=None, **kwargs) + + self.specialSets["innerWall_VertexSet"] = _specialSets_Cartesian.MinI_VertexSet + self.specialSets["outerWall_VertexSet"] = _specialSets_Cartesian.MaxI_VertexSet + self.specialSets["northWall_VertexSet"] = _specialSets_Cartesian.MaxK_VertexSet + self.specialSets["southWall_VertexSet"] = _specialSets_Cartesian.MinK_VertexSet + self.specialSets["eastWall_VertexSet"] = _specialSets_Cartesian.MaxJ_VertexSet + self.specialSets["westWall_VertexSet"] = _specialSets_Cartesian.MinJ_VertexSet + + self._centroid = centroid + + def _setup(self): + + with self.deform_mesh(): + # perform Cubed-sphere projection on coordinates + # fac = np.pi/180.0 + old = self.data + (x,y) = (np.tan(self.data[:,1]*np.pi/180.0), np.tan(self.data[:,2]*np.pi/180.0)) + d = self.data[:,0] / np.sqrt( x**2 + y**2 + 1) + self.data[:,0] = self._centroid[0] + d*x + self.data[:,1] = self._centroid[1] + d*y + self.data[:,2] = self._centroid[2] + d + + # add a boundary MeshVariable - 1 if nodes is on the boundary(ie 'AllWalls_VertexSet'), 0 if node is internal + self.bndMeshVariable = uw.mesh.MeshVariable(self, 1) + self.bndMeshVariable.data[:] = 0. + self.bndMeshVariable.data[self.specialSets["AllWalls_VertexSet"].data] = 1.0 + + + # ASSUME the parent class builds the _boundaryNodeFn + # self.bndMeshVariable = uw.mesh.MeshVariable(self, 1) + # self.bndMeshVariable.data[:] = 0. + # # set a value 1.0 on provided vertices + # self.bndMeshVariable.data[self.specialSets["AllWalls_VertexSet"].data] = 1.0 + # # note we use this condition to only capture border swarm particles + # # on the surface itself. for those directly adjacent, the deltaMeshVariable will evaluate + # # to non-zero (but less than 1.), so we need to remove those from the integration as well. + self._boundaryNodeFn = function.branching.conditional( + [ ( self.bndMeshVariable > 0.999, 1. ), + ( True, 0. ) ] ) + + """ + Rotation documentation. + We will create 3 basis vectors that will rotate the (x,y,z) problem to be a + (r,n,t) [radial, normal to cut, tangential to cut] problem. + + The rotations are performed on the local element level using the existing machinery + provided by UW2. As such only elements on the domain boundary need rotation, all internal + elements can continue with the (x,y,z) representation. + + This is implemented with rotations on all dofs such that: + 1. If on the domain boundary - rotated to (r,n,t) and + 2. If not on the domain boundary - rotated by identity matrix i.e. (NO rotation). + """ + + # initialiase bases vectors as meshVariables + self._e1 = self.add_variable(nodeDofCount=3) + self._e2 = self.add_variable(nodeDofCount=3) + self._e3 = self.add_variable(nodeDofCount=3) + + # _x_or_radial, y_or_east, z_or_north functions + self._fn_x_or_radial = function.branching.conditional( + [ ( self.bndMeshVariable > 0.9, self.fn_unitvec_radial() ), + ( True, function.misc.constant(1.0)*(1.,0.,0.) ) ] ) + self._fn_y_or_east = function.branching.conditional( + [ ( self.bndMeshVariable > 0.9, self._getEWFn() ), + ( True, function.misc.constant(1.0)*(0.,1.,0.) ) ] ) + self._fn_z_or_north = function.branching.conditional( + [ ( self.bndMeshVariable > 0.9, self._getNSFn() ), + ( True, function.misc.constant(1.0)*(0.,0.,1.) ) ] ) + + # shorthand variables for the walls + inner = self.specialSets["innerWall_VertexSet"] + outer = self.specialSets["outerWall_VertexSet"] + W = self.specialSets["westWall_VertexSet"] + E = self.specialSets["eastWall_VertexSet"] + S = self.specialSets["southWall_VertexSet"] + N = self.specialSets["northWall_VertexSet"] + + # evaluate the new bases + self._e1.data[:] = self._fn_x_or_radial.evaluate(self) + self._e2.data[:] = self._fn_y_or_east.evaluate(self) # only good on EW walls + self._e3.data[:] = self._fn_z_or_north.evaluate(self) # only good on NS walls + + # build the correct e3 on EW, with e3 = e1 cross e2 + walls = E+W + a = self._e1.data[walls.data] + b = self._e2.data[walls.data] + self._e3.data[walls.data] = np.cross(a,b) + + # build the correct e2 on NS-EW + # note, at the side edges of the sixth a choice for the basis must be made + # as two non orthogonal side walls meet. We let the basis of the EW walls + # define the rotations required and don't correct them below. + walls = N+S - walls + a = self._e3.data[walls.data] + b = self._e1.data[walls.data] + self._e2.data[walls.data] = np.cross(a,b) + + def fn_unitvec_radial(self): + + pos = function.coord() + centre = self._centroid + r_vec = pos - centre + mag = function.math.sqrt(function.math.dot( r_vec, r_vec )) + r_vec = r_vec / mag + return r_vec + + def _getEWFn(self): + pos = function.coord() - self._centroid + xi = function.math.atan(pos[0]/pos[2]) + # vec = [ cos(xi), 0.0, -sin(xi) ] + vec = function.math.cos(xi) * (1.,0.,0.) + vec = vec + function.math.sin(xi) * (0.,0.,-1.) + return vec + + def _getNSFn(self): + pos = function.coord() - self._centroid + xi = function.math.atan(pos[1]/pos[2]) + # vec = [ 0.0, cos(xi), -sin(xi) ] + vec = function.math.cos(xi) * (0.,1.,0.) + vec = vec + function.math.sin(xi) * (0.,0.,-1.) + return vec + From 407ddae726dcfea6fc1429a3d8262efed3724ecc Mon Sep 17 00:00:00 2001 From: julian Date: Wed, 27 Jan 2021 14:38:22 +1100 Subject: [PATCH 04/13] Adding a PoissonEq in a annulus --- docs/examples/curvi_examples/Annulus_PoissonEq.py | 15 +++++++++++++++ 1 file changed, 15 insertions(+) diff --git a/docs/examples/curvi_examples/Annulus_PoissonEq.py b/docs/examples/curvi_examples/Annulus_PoissonEq.py index 3a43c4aaf..1e9ec005b 100644 --- a/docs/examples/curvi_examples/Annulus_PoissonEq.py +++ b/docs/examples/curvi_examples/Annulus_PoissonEq.py @@ -49,6 +49,21 @@ def np_analytic(r): fn_r = annulus.fn_radial fn_analytic = fn.math.log( fn_r/rb ) * fac + tb +# %% +from underworld.mesh import _specialSets_Cartesian + +# %% +xx = _specialSets_Cartesian.MaxI_VertexSet + +# %% +xx + +# %% +xx(annulus) + +# %% +annulus.specialSets["surfaces_e1_normal_VertexSet"] + # %% fig = vis.Figure() fig.append(vis.objects.Mesh(annulus)) From 5821f41631215fa5bd66353ed3a772613867570a Mon Sep 17 00:00:00 2001 From: julian Date: Wed, 27 Jan 2021 15:30:41 +1100 Subject: [PATCH 05/13] Adding a regional spherical mesh implementation and example --- .../examples/curvi_examples/sregion_stokes.py | 204 ++++++++++++++++++ underworld/mesh/__init__.py | 2 +- underworld/mesh/_mesh.py | 78 ------- 3 files changed, 205 insertions(+), 79 deletions(-) create mode 100644 docs/examples/curvi_examples/sregion_stokes.py diff --git a/docs/examples/curvi_examples/sregion_stokes.py b/docs/examples/curvi_examples/sregion_stokes.py new file mode 100644 index 000000000..ab3027be9 --- /dev/null +++ b/docs/examples/curvi_examples/sregion_stokes.py @@ -0,0 +1,204 @@ +# --- +# jupyter: +# jupytext: +# formats: ipynb,py:percent +# text_representation: +# extension: .py +# format_name: percent +# format_version: '1.3' +# jupytext_version: 1.4.2 +# kernelspec: +# display_name: Python 3 +# language: python +# name: python3 +# --- + +# %% [markdown] +# ## Demo of buoyancy driven stokes flow in an Spherical Region + +# %% +import underworld as uw +import underworld.visualisation as vis +import numpy as np +from underworld import function as fn + +# %% +# boundary conditions available "BC_FREESLIP, "BC_NOSLIP, "BC_LIDDRIVEN" +bc_wanted = 'BC_FREESLIP' + +# %% +mesh = uw.mesh.FeMesh_SRegion(elementRes=(6,12,12), + radialLengths=(3.0,6.)) + +dField = mesh.add_variable(nodeDofCount=1) +vField = mesh.add_variable(nodeDofCount=mesh.dim) +pField = mesh.subMesh.add_variable(nodeDofCount=1) + +inner = mesh.specialSets["innerWall_VertexSet"] +outer = mesh.specialSets["outerWall_VertexSet"] +W = mesh.specialSets["westWall_VertexSet"] +E = mesh.specialSets["eastWall_VertexSet"] +S = mesh.specialSets["southWall_VertexSet"] +N = mesh.specialSets["northWall_VertexSet"] + +allWalls = mesh.specialSets["AllWalls_VertexSet"] +NS0 = N+S-(E+W) +# build corner edges node indexset +cEdge = (N&W)+(N&E)+(S&E)+(S&W) + + +# %% +# create checkpoint function +def checkpoint( mesh, fieldDict, swarm, swarmDict, index, + meshName='mesh', swarmName='swarm', + prefix='./', enable_xdmf=True): + import os + # Check the prefix is valid + if prefix is not None: + if not prefix.endswith('/'): prefix += '/' # add a backslash + if not os.path.exists(prefix) and uw.mpi.rank==0: + print("Creating directory: ",prefix) + os.makedirs(prefix) + uw.mpi.barrier() + + if not isinstance(index, int): + raise TypeError("'index' is not of type int") + ii = str(index) + + if mesh is not None: + + # Error check the mesh and fields + if not isinstance(mesh, uw.mesh.FeMesh): + raise TypeError("'mesh' is not of type uw.mesh.FeMesh") + if not isinstance(fieldDict, dict): + raise TypeError("'fieldDict' is not of type dict") + for key, value in fieldDict.items(): + if not isinstance( value, uw.mesh.MeshVariable ): + raise TypeError("'fieldDict' must contain uw.mesh.MeshVariable elements") + + + # see if we have already saved the mesh. It only needs to be saved once + if not hasattr( checkpoint, 'mH' ): + checkpoint.mH = mesh.save(prefix+meshName+".h5") + mh = checkpoint.mH + + for key,value in fieldDict.items(): + filename = prefix+key+'-'+ii + handle = value.save(filename+'.h5') + if enable_xdmf: value.xdmf(filename, handle, key, mh, meshName) + + # is there a swarm + if swarm is not None: + + # Error check the swarms + if not isinstance(swarm, uw.swarm.Swarm): + raise TypeError("'swarm' is not of type uw.swarm.Swarm") + if not isinstance(swarmDict, dict): + raise TypeError("'swarmDict' is not of type dict") + for key, value in swarmDict.items(): + if not isinstance( value, uw.swarm.SwarmVariable ): + raise TypeError("'fieldDict' must contain uw.swarm.SwarmVariable elements") + + sH = swarm.save(prefix+swarmName+"-"+ii+".h5") + for key,value in swarmDict.items(): + filename = prefix+key+'-'+ii + handle = value.save(filename+'.h5') + if enable_xdmf: value.xdmf(filename, handle, key, sH, swarmName) + + +# %% +# xdmf output +fieldDict = {'velocity':vField, + 'normal':mesh._e2, + 'radial':mesh._e1, + 'tangent':mesh._e3, + 'temperature':dField} +checkpoint(mesh, fieldDict, None, None, index=0, prefix='output') + +# %% +fig = vis.Figure() +fig.append(vis.objects.Mesh(mesh, segmentsPerEdge=1)) +fig.append(vis.objects.Surface(mesh, dField, onMesh=True )) +fig.show() + +# %% +# zero all dofs of vField +vField.data[...] = 0. + +if bc_wanted == "BC_NOSLIP": + # no slip + vBC = uw.conditions.CurvilinearDirichletCondition( variable=vField, indexSetsPerDof=(allWalls,allWalls,allWalls)) + +elif bc_wanted == "BC_FREESLIP": + # free-slip + + vField.data[cEdge.data] = (0.,0.,0.) + vBC = uw.conditions.CurvilinearDirichletCondition( variable=vField, + indexSetsPerDof=(inner+outer,E+W+cEdge,NS0+cEdge), + basis_vectors = (mesh._e1, mesh._e2, mesh._e3) ) +elif bc_wanted == "BC_LIDDRIVEN": + # lid-driven case + + # build driving node indexset & apply velocities with zero radial component + drivers = outer - (N+S+E+W) + vField.data[drivers.data] = (0.,1.,1.) + + # build corner edges node indexset and apply velocities with zero non-radial components + cEdge = (N&W)+(N&E)+(S&E)+(S&W) + vField.data[cEdge.data] = (0.,0.,0.) + + # apply altogether. + NS0 = N+S - (E+W) + vBC = uw.conditions.CurvilinearDirichletCondition( variable=vField, + indexSetsPerDof=(inner+outer,drivers+E+W+cEdge,drivers+NS0+cEdge), # optional, can include cEdge on the 3rd component + basis_vectors = (mesh._e1, mesh._e2, mesh._e3) ) +else: + raise ValueError("Can't find an option for the 'bc_wanted' = {}".format(bc_wanted)) + +# %% +z_hat = -1.0*mesh.fn_unitvec_radial() + +# %% +inds = (mesh.data[:,0]**2 + mesh.data[:,1]**2 + (mesh.data[:,2]-4.5)**2) < 1.5**2 +dField.data[inds] = 1. + +# %% +bodyForceFn = dField * z_hat + +# %% +stokesSLE = uw.systems.Stokes( vField, pField, + fn_viscosity=1.0, fn_bodyforce=bodyForceFn, + conditions=vBC, _removeBCs=False) + +# %% +stokesSolver = uw.systems.Solver(stokesSLE) +if uw.mpi.size == 1: + stokesSolver.set_inner_method("mumps") + +# %% +stokesSolver.solve() +# must re-orient boundary vectors +ignore = uw.libUnderworld.Underworld.AXequalsX( stokesSLE._rot._cself, stokesSLE._velocitySol._cself, False) + +# %% +vdotv = fn.math.dot(vField,vField) +vrms = np.sqrt( mesh.integrate(vdotv)[0] / mesh.integrate(1.)[0] ) +if uw.mpi.rank == 0: + print("The vrms = {:.5e}\n".format(vrms)) + +# %% +figV = vis.Figure() +figV.append(vis.objects.Mesh(mesh, segmentsPerEdge=1)) +figV.append(vis.objects.Surface(mesh, vdotv, onMesh=True)) +# figV.append(vis.objects.VectorArrows(mesh, vField, autoscale=True, onMesh=True)) +# figV.append(vis.objects.VectorArrows(mesh, vField, onMesh=True)) + +figV.window() + +# %% +# xdmf output +fieldDict = {'velocity':vField, + 'density':dField} +checkpoint(mesh, fieldDict, None, None, index=1, prefix='output') + +# %% diff --git a/underworld/mesh/__init__.py b/underworld/mesh/__init__.py index fa4e9ade6..59824dfef 100644 --- a/underworld/mesh/__init__.py +++ b/underworld/mesh/__init__.py @@ -12,5 +12,5 @@ """ -from ._mesh import FeMesh, FeMesh_Cartesian, FeMesh_IndexSet, _FeMesh_Regional, FeMesh_Annulus, FeMesh_SRegion +from ._mesh import FeMesh, FeMesh_Cartesian, FeMesh_IndexSet, FeMesh_Annulus, FeMesh_SRegion from ._meshvariable import MeshVariable diff --git a/underworld/mesh/_mesh.py b/underworld/mesh/_mesh.py index 59ead474d..71a736d2b 100644 --- a/underworld/mesh/_mesh.py +++ b/underworld/mesh/_mesh.py @@ -1258,84 +1258,6 @@ def _checkCompatWith(self,other): def _get_iterator(self): return libUnderworld.Function.MeshIndexSet(self._cself, self.object._cself) -class _FeMesh_Regional(FeMesh_Cartesian): - """ - Regional mesh class. - - MinI_VertexSet / MaxI_VertexSet -> longitudinal walls : [min/max] = [west/east] - MinJ_VertexSet / MaxJ_VertexSet -> latitudinal walls : [min/max] = [south/north] - MinK_VertexSet / MaxK_VertexSet -> radial walls : [min/max] = [inner/outer] - - Refer to parent classes for parameters beyond those below. - - Parameter - --------- - elementRes : tuple - Tuple determining number of elements (longitudinally, latitudinally, radially). - radius : tuple - Tuple determining the (inner radius, outer radius). - longExtent : float - The angular extent of the domain between great circles of longitude. - latExtent : float - The angular extent of the domain between great circles of latitude. - - - Example - ------- - - >>> (radMin, radMax) = (4.0,8.0) - >>> mesh = uw.mesh._FeMesh_Regional( elementRes=(20,20,14), radius=(radMin, radMax) ) - >>> integral = uw.utils.Integral( 1.0, mesh).evaluate()[0] - >>> exact = 4/3.0*np.pi*(radMax**3 - radMin**2) / 6.0 - >>> np.fabs(integral-exact)/exact < 1e-1 - True - - - """ - def __new__(cls, **kwargs): - return super(_FeMesh_Regional,cls).__new__(cls, **kwargs) - - def __init__(self, elementRes=(16,16,10), radius=(3.0,6.0), latExtent=90.0, longExtent=90.0, **kwargs): - - if not isinstance( latExtent, (float,int) ): - raise TypeError("Provided 'latExtent' must be a float or integer") - self._latExtent = latExtent - if not isinstance( longExtent, (float,int) ): - raise TypeError("Provided 'longExtent' must be a float or integer") - self._longExtent = longExtent - if not isinstance( radius, (tuple,list)): - raise TypeError("Provided 'radius' must be a tuple/list of 2 floats") - if len(radius) != 2: - raise ValueError("Provided 'radius' must be a tuple/list of 2 floats") - for el in radius: - if not isinstance( el, (float,int)) : - raise TypeError("Provided 'radius' must be a tuple/list of 2 floats") - self._radius = radius - - lat_half = latExtent/2.0 - long_half = longExtent/2.0 - - # call parent cartesian mesh - # build 3D mesh centred on (0.0,0.0,0.0) - in _setup() we deform the mesh - super(_FeMesh_Regional,self).__init__(elementType="Q1/dQ0", elementRes=elementRes, - minCoord=(-long_half,-lat_half,radius[0]), maxCoord=(long_half,lat_half,radius[1]), periodic=None, **kwargs) - - def _setup(self): - - with self.deform_mesh(): - # perform Cubed-sphere projection on coordinates - (x,y) = (np.tan(np.pi*self.data[:,0]/180.0), np.tan(np.pi*self.data[:,1]/180.0)) - d = self.data[:,2] / np.sqrt( x**2 + y**2 + 1) - self.data[:,0] = d*x - self.data[:,1] = d*y - self.data[:,2] = d - # - # for index, coord in enumerate(mesh.data): - # # perform Cubed-sphere projection on coordinates - # (x,y,r) = (np.tan(np.pi*coord[0]/180.0), np.tan(np.pi*coord[1]/180.0), 1) - # d = coord[2]/np.sqrt( x**2 + y**2 + 1) - # mesh.data[index] = ( d*x, d*y, d) - class FeMesh_Annulus(FeMesh_Cartesian): def __init__(self, elementRes=(10,16), radialLengths=(3.0,6.0), angularExtent=[0.0,360.0], centroid=[0.0,0.0], periodic=[False, True], **kwargs): """ From fba77bf5ce31d49a958c88aa0eb05558dcbd7c3d Mon Sep 17 00:00:00 2001 From: julian Date: Tue, 16 Feb 2021 14:03:48 +1100 Subject: [PATCH 06/13] Parallel safe --- .../curvi_examples/Annulus_PoissonEq.py | 32 +++++++------------ .../curvi_examples/Annulus_StokesAdvection.py | 5 ++- .../examples/curvi_examples/sregion_stokes.py | 11 +++++-- 3 files changed, 24 insertions(+), 24 deletions(-) diff --git a/docs/examples/curvi_examples/Annulus_PoissonEq.py b/docs/examples/curvi_examples/Annulus_PoissonEq.py index 1e9ec005b..2a48e2d6e 100644 --- a/docs/examples/curvi_examples/Annulus_PoissonEq.py +++ b/docs/examples/curvi_examples/Annulus_PoissonEq.py @@ -49,21 +49,6 @@ def np_analytic(r): fn_r = annulus.fn_radial fn_analytic = fn.math.log( fn_r/rb ) * fac + tb -# %% -from underworld.mesh import _specialSets_Cartesian - -# %% -xx = _specialSets_Cartesian.MaxI_VertexSet - -# %% -xx - -# %% -xx(annulus) - -# %% -annulus.specialSets["surfaces_e1_normal_VertexSet"] - # %% fig = vis.Figure() fig.append(vis.objects.Mesh(annulus)) @@ -86,13 +71,18 @@ def np_analytic(r): # %% # error measurement - l2 norm -fn_e = fn.math.pow(fn_analytic - tField, 2.) +fn_l2 = fn.math.pow((fn_analytic - tField), 2.) +fn_sol = fn.math.pow(fn_analytic, 2.) + +norms = np.sqrt(annulus.integrate((fn_l2,fn_sol))) +rerr = norms[0]/norms[1] * 100 +tolerance = 1e-1 +if uw.mpi.rank == 0: + print("Relative error norm: {:.3}%".format(rerr)) + if rerr > tolerance: + es = "Model error greater the test tolerance. {:.4e} > {:.4e}".format(error, tolerance) + raise RuntimeError(es) -error = annulus.integrate(fn_e)[0] -tolerance = 3.6e-4 -if error > tolerance: - es = "Model error greater the test tolerance. {:.4e} > {:.4e}".format(error, tolerance) - raise RuntimeError(es) # %% fig.show() diff --git a/docs/examples/curvi_examples/Annulus_StokesAdvection.py b/docs/examples/curvi_examples/Annulus_StokesAdvection.py index e547de448..d0089bfdb 100644 --- a/docs/examples/curvi_examples/Annulus_StokesAdvection.py +++ b/docs/examples/curvi_examples/Annulus_StokesAdvection.py @@ -79,7 +79,10 @@ swarm.populate_using_layout(layout) # get initial theta coordinates and save into tvar x,y = np.split(swarm.particleCoordinates.data, indices_or_sections=2,axis=1) -tvar.data[:] = 180 / np.pi * np.arctan2(y,x) +if uw.mpi.size == 1: + tvar.data[:] = 180 / np.pi * np.arctan2(y,x) +else: + tvar.data[:] = uw.mpi.rank # add an advector advector = uw.systems.SwarmAdvector(velocityField=velocityField, swarm=swarm) diff --git a/docs/examples/curvi_examples/sregion_stokes.py b/docs/examples/curvi_examples/sregion_stokes.py index ab3027be9..b0a7f2cef 100644 --- a/docs/examples/curvi_examples/sregion_stokes.py +++ b/docs/examples/curvi_examples/sregion_stokes.py @@ -184,7 +184,14 @@ def checkpoint( mesh, fieldDict, swarm, swarmDict, index, vdotv = fn.math.dot(vField,vField) vrms = np.sqrt( mesh.integrate(vdotv)[0] / mesh.integrate(1.)[0] ) if uw.mpi.rank == 0: - print("The vrms = {:.5e}\n".format(vrms)) + rtol = 1e-3 + expected = 6.89257e-02 + error = np.abs(vrms - expected) + rerr = error / expected + print("Model vrms / Expected vrms: {:.5e} / {:.5e}".format(vrms,expected)) + if rerr > rtol: + es = "Model vrms greater the test tolerance. {:.4e} > {:.4e}".format(error, rtol) + raise RuntimeError(es) # %% figV = vis.Figure() @@ -193,7 +200,7 @@ def checkpoint( mesh, fieldDict, swarm, swarmDict, index, # figV.append(vis.objects.VectorArrows(mesh, vField, autoscale=True, onMesh=True)) # figV.append(vis.objects.VectorArrows(mesh, vField, onMesh=True)) -figV.window() +if uw.mpi.size == 1: figV.window() # %% # xdmf output From 403ae31ec3e5c47b49c412340651519c09806d41 Mon Sep 17 00:00:00 2001 From: julian Date: Tue, 16 Feb 2021 14:20:04 +1100 Subject: [PATCH 07/13] Adding paraview viewing file --- .../examples/curvi_examples/sregion_view.pvsm | 12035 ++++++++++++++++ 1 file changed, 12035 insertions(+) create mode 100644 docs/examples/curvi_examples/sregion_view.pvsm diff --git a/docs/examples/curvi_examples/sregion_view.pvsm b/docs/examples/curvi_examples/sregion_view.pvsm new file mode 100644 index 000000000..6306e5014 --- /dev/null +++ b/docs/examples/curvi_examples/sregion_view.pvsm @@ -0,0 +1,12035 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + From 548dfd295bcf7498c069fde636a26667925ce58b Mon Sep 17 00:00:00 2001 From: julian Date: Wed, 24 Feb 2021 13:54:35 +1100 Subject: [PATCH 08/13] Update to the stokes models for parallel testing --- .../examples/curvi_examples/sregion_stokes.py | 68 +++-- .../curvi_examples/sregion_stokes_pic.py | 259 ++++++++++++++++++ 2 files changed, 298 insertions(+), 29 deletions(-) create mode 100644 docs/examples/curvi_examples/sregion_stokes_pic.py diff --git a/docs/examples/curvi_examples/sregion_stokes.py b/docs/examples/curvi_examples/sregion_stokes.py index b0a7f2cef..e50f09ea6 100644 --- a/docs/examples/curvi_examples/sregion_stokes.py +++ b/docs/examples/curvi_examples/sregion_stokes.py @@ -27,13 +27,22 @@ bc_wanted = 'BC_FREESLIP' # %% -mesh = uw.mesh.FeMesh_SRegion(elementRes=(6,12,12), +ITOL=1e-6 +OTOL=1e-4 + +# elementRes=(10,18,18) +elementRes = (10,12,12) +mesh = uw.mesh.FeMesh_SRegion(elementRes=elementRes, radialLengths=(3.0,6.)) -dField = mesh.add_variable(nodeDofCount=1) +# fields for stokes vField = mesh.add_variable(nodeDofCount=mesh.dim) pField = mesh.subMesh.add_variable(nodeDofCount=1) +# fields for density and rank +dField = mesh.add_variable(nodeDofCount=1) +rField = mesh.subMesh.add_variable(nodeDofCount=1) + inner = mesh.specialSets["innerWall_VertexSet"] outer = mesh.specialSets["outerWall_VertexSet"] W = mesh.specialSets["westWall_VertexSet"] @@ -106,20 +115,29 @@ def checkpoint( mesh, fieldDict, swarm, swarmDict, index, if enable_xdmf: value.xdmf(filename, handle, key, sH, swarmName) +# %% +# setup dField and rField +rField.data[:] = uw.mpi.rank + +z_hat = -1.0*mesh.fn_unitvec_radial() + +inds = (mesh.data[:,0]**2 + mesh.data[:,1]**2 + (mesh.data[:,2]-4.5)**2) < 1.5**2 +dField.data[inds] = 1. + +bodyForceFn = dField * z_hat + # %% # xdmf output fieldDict = {'velocity':vField, - 'normal':mesh._e2, - 'radial':mesh._e1, - 'tangent':mesh._e3, - 'temperature':dField} + 'density':dField, + 'rank':rField} checkpoint(mesh, fieldDict, None, None, index=0, prefix='output') # %% fig = vis.Figure() fig.append(vis.objects.Mesh(mesh, segmentsPerEdge=1)) fig.append(vis.objects.Surface(mesh, dField, onMesh=True )) -fig.show() +if uw.utils.is_kernel(): fig.show() # %% # zero all dofs of vField @@ -155,37 +173,35 @@ def checkpoint( mesh, fieldDict, swarm, swarmDict, index, else: raise ValueError("Can't find an option for the 'bc_wanted' = {}".format(bc_wanted)) -# %% -z_hat = -1.0*mesh.fn_unitvec_radial() - -# %% -inds = (mesh.data[:,0]**2 + mesh.data[:,1]**2 + (mesh.data[:,2]-4.5)**2) < 1.5**2 -dField.data[inds] = 1. - -# %% -bodyForceFn = dField * z_hat - # %% stokesSLE = uw.systems.Stokes( vField, pField, fn_viscosity=1.0, fn_bodyforce=bodyForceFn, conditions=vBC, _removeBCs=False) # %% -stokesSolver = uw.systems.Solver(stokesSLE) -if uw.mpi.size == 1: - stokesSolver.set_inner_method("mumps") +solver = uw.systems.Solver(stokesSLE) +# monitor src iteration tolerance +solver.options.scr.ksp_monitor='' +solver.set_inner_rtol(ITOL) +solver.set_outer_rtol(OTOL) +# if uw.mpi.size == 1: +# solver.set_inner_method("mumps") # %% -stokesSolver.solve() +solver.solve() # must re-orient boundary vectors ignore = uw.libUnderworld.Underworld.AXequalsX( stokesSLE._rot._cself, stokesSLE._velocitySol._cself, False) +# %% +# xdmf output +checkpoint(mesh, fieldDict, None, None, index=1, prefix='output') + # %% vdotv = fn.math.dot(vField,vField) vrms = np.sqrt( mesh.integrate(vdotv)[0] / mesh.integrate(1.)[0] ) -if uw.mpi.rank == 0: +if uw.mpi.rank == 0 and elementRes == (10,12,12): rtol = 1e-3 - expected = 6.89257e-02 + expected = 6.91805e-02 error = np.abs(vrms - expected) rerr = error / expected print("Model vrms / Expected vrms: {:.5e} / {:.5e}".format(vrms,expected)) @@ -203,9 +219,3 @@ def checkpoint( mesh, fieldDict, swarm, swarmDict, index, if uw.mpi.size == 1: figV.window() # %% -# xdmf output -fieldDict = {'velocity':vField, - 'density':dField} -checkpoint(mesh, fieldDict, None, None, index=1, prefix='output') - -# %% diff --git a/docs/examples/curvi_examples/sregion_stokes_pic.py b/docs/examples/curvi_examples/sregion_stokes_pic.py new file mode 100644 index 000000000..e6e077d75 --- /dev/null +++ b/docs/examples/curvi_examples/sregion_stokes_pic.py @@ -0,0 +1,259 @@ +# --- +# jupyter: +# jupytext: +# formats: ipynb,py:percent +# text_representation: +# extension: .py +# format_name: percent +# format_version: '1.3' +# jupytext_version: 1.4.2 +# kernelspec: +# display_name: Python 3 +# language: python +# name: python3 +# --- + +# %% [markdown] +# ## Demo of buoyancy driven stokes flow in an Spherical Region + +# %% +import underworld as uw +import underworld.visualisation as vis +import numpy as np +from underworld import function as fn + +# %% +# boundary conditions available "BC_FREESLIP, "BC_NOSLIP, "BC_LIDDRIVEN" +bc_wanted = 'BC_FREESLIP' + +# %% +ITOL=1e-6 +OTOL=1e-4 + +# elementRes=(10,18,18) +elementRes = (8,20,20) +mesh = uw.mesh.FeMesh_SRegion(elementRes=elementRes, + radialLengths=(3.0,6.)) + +# fields for stokes +vField = mesh.add_variable(nodeDofCount=mesh.dim) +pField = mesh.subMesh.add_variable(nodeDofCount=1) + +# auxiliary fields rank +rField = mesh.subMesh.add_variable(nodeDofCount=1) + +swarm = uw.swarm.Swarm(mesh, particleEscape=True) +rvar = swarm.add_variable(dataType="double", count=1) # original rank +dvar = swarm.add_variable(dataType="double", count=1) # density + +layout = uw.swarm.layouts.PerCellSpaceFillerLayout(swarm, particlesPerCell=10) +swarm.populate_using_layout(layout) +advector = uw.systems.SwarmAdvector(velocityField=vField, swarm=swarm) + +inner = mesh.specialSets["innerWall_VertexSet"] +outer = mesh.specialSets["outerWall_VertexSet"] +W = mesh.specialSets["westWall_VertexSet"] +E = mesh.specialSets["eastWall_VertexSet"] +S = mesh.specialSets["southWall_VertexSet"] +N = mesh.specialSets["northWall_VertexSet"] + +allWalls = mesh.specialSets["AllWalls_VertexSet"] +NS0 = N+S-(E+W) +# build corner edges node indexset +cEdge = (N&W)+(N&E)+(S&E)+(S&W) + + +# %% +# create checkpoint function +def checkpoint( mesh, fieldDict, swarm, swarmDict, index, + meshName='mesh', swarmName='swarm', time=None, + prefix='./', enable_xdmf=True): + import os + # Check the prefix is valid + if prefix is not None: + if not prefix.endswith('/'): prefix += '/' # add a backslash + if not os.path.exists(prefix) and uw.mpi.rank==0: + print("Creating directory: ",prefix) + os.makedirs(prefix) + uw.mpi.barrier() + + # initialise internal time + if time is None and not hasattr(checkpoint, '_time'): + checkpoint.time = 0 + # use internal time + if time is None: + time = checkpoint.time + 1 + + if not isinstance(index, int): + raise TypeError("'index' is not of type int") + ii = str(index) + + if mesh is not None: + + # Error check the mesh and fields + if not isinstance(mesh, uw.mesh.FeMesh): + raise TypeError("'mesh' is not of type uw.mesh.FeMesh") + if not isinstance(fieldDict, dict): + raise TypeError("'fieldDict' is not of type dict") + for key, value in fieldDict.items(): + if not isinstance( value, uw.mesh.MeshVariable ): + raise TypeError("'fieldDict' must contain uw.mesh.MeshVariable elements") + + + # see if we have already saved the mesh. It only needs to be saved once + if not hasattr( checkpoint, 'mH' ): + checkpoint.mH = mesh.save(prefix+meshName+".h5") + mh = checkpoint.mH + + for key,value in fieldDict.items(): + filename = prefix+key+'-'+ii + handle = value.save(filename+'.h5') + if enable_xdmf: value.xdmf(filename, handle, key, mh, meshName, modeltime=time) + + # is there a swarm + if swarm is not None: + + # Error check the swarms + if not isinstance(swarm, uw.swarm.Swarm): + raise TypeError("'swarm' is not of type uw.swarm.Swarm") + if not isinstance(swarmDict, dict): + raise TypeError("'swarmDict' is not of type dict") + for key, value in swarmDict.items(): + if not isinstance( value, uw.swarm.SwarmVariable ): + raise TypeError("'fieldDict' must contain uw.swarm.SwarmVariable elements") + + sH = swarm.save(prefix+swarmName+"-"+ii+".h5") + for key,value in swarmDict.items(): + filename = prefix+key+'-'+ii + handle = value.save(filename+'.h5') + if enable_xdmf: value.xdmf(filename, handle, key, sH, swarmName, modeltime=time) + + +# %% +# setup dField and rField +rField.data[:] = uw.mpi.rank +rvar.data[:] = uw.mpi.rank + +z_hat = -1.0*mesh.fn_unitvec_radial() + +inds = (swarm.data[:,0]**2 + swarm.data[:,1]**2 + (swarm.data[:,2]-4.5)**2) < 0.75**2 +if inds.size != 0: + dvar.data[inds] = 1. +# inds = (mesh.data[:,0]**2 + mesh.data[:,1]**2 + (mesh.data[:,2]-4.5)**2) < 1.5**2 +# dField.data[inds] = 1. + +bodyForceFn = dvar * z_hat + +fvar = swarm.add_variable(dataType="double", count=3) +fvar.data[:] = bodyForceFn.evaluate(swarm) + +# %% +# xdmf output +fieldDict = {'velocity':vField, + 'pressure':pField, + 'rank':rField} + +swarmDict = {'orank':rvar, + 'density':dvar, + 'force':fvar} + +# checkpoint(mesh, fieldDict, swarm, swarmDict, index=0, prefix='output') + +# %% +fig = vis.Figure() +fig.append(vis.objects.Mesh(mesh, segmentsPerEdge=1)) +fig.append(vis.objects.Surface(mesh, pField, onMesh=True )) +if uw.utils.is_kernel(): fig.show() + +# %% +# zero all dofs of vField +vField.data[...] = 0. + +if bc_wanted == "BC_NOSLIP": + # no slip + vBC = uw.conditions.CurvilinearDirichletCondition( variable=vField, indexSetsPerDof=(allWalls,allWalls,allWalls)) + +elif bc_wanted == "BC_FREESLIP": + # free-slip + + vField.data[cEdge.data] = (0.,0.,0.) + vBC = uw.conditions.CurvilinearDirichletCondition( variable=vField, + indexSetsPerDof=(inner+outer,E+W+cEdge,NS0+cEdge), + basis_vectors = (mesh._e1, mesh._e2, mesh._e3) ) +elif bc_wanted == "BC_LIDDRIVEN": + # lid-driven case + + # build driving node indexset & apply velocities with zero radial component + drivers = outer - (N+S+E+W) + vField.data[drivers.data] = (0.,1.,1.) + + # build corner edges node indexset and apply velocities with zero non-radial components + cEdge = (N&W)+(N&E)+(S&E)+(S&W) + vField.data[cEdge.data] = (0.,0.,0.) + + # apply altogether. + NS0 = N+S - (E+W) + vBC = uw.conditions.CurvilinearDirichletCondition( variable=vField, + indexSetsPerDof=(inner+outer,drivers+E+W+cEdge,drivers+NS0+cEdge), # optional, can include cEdge on the 3rd component + basis_vectors = (mesh._e1, mesh._e2, mesh._e3) ) +else: + raise ValueError("Can't find an option for the 'bc_wanted' = {}".format(bc_wanted)) + +# %% +stokesSLE = uw.systems.Stokes( vField, pField, + fn_viscosity=1.0, fn_bodyforce=bodyForceFn, + conditions=vBC, _removeBCs=False) + +# %% +solver = uw.systems.Solver(stokesSLE) +solver.options.scr.ksp_monitor='' +solver.set_inner_rtol(ITOL) +solver.set_outer_rtol(OTOL) +if uw.mpi.size == 1: + solver.set_inner_method("mumps") + +# %% +solver.solve() +# must re-orient boundary vectors +ignore = uw.libUnderworld.Underworld.AXequalsX( stokesSLE._rot._cself, stokesSLE._velocitySol._cself, False) + +# %% +maxsteps=1 +time = 0 +# xdmf output +checkpoint(mesh, fieldDict, swarm, swarmDict, index=0, prefix='output', time=0) + +# %% +for step in range(maxsteps): + # advect particles + dt = advector.get_max_dt() + time = time + dt + advector.integrate(dt) + solver.solve() + # must re-orient boundary vectors + ignore = uw.libUnderworld.Underworld.AXequalsX( stokesSLE._rot._cself, stokesSLE._velocitySol._cself, False) + checkpoint(mesh, fieldDict, swarm, swarmDict, index=step, time=time, prefix='output') + +# %% +vdotv = fn.math.dot(vField,vField) +vrms = np.sqrt( mesh.integrate(vdotv)[0] / mesh.integrate(1.)[0] ) +if uw.mpi.rank == 0: + rtol = 1e-3 + expected = 6.89257e-02 + error = np.abs(vrms - expected) + rerr = error / expected + print("Model vrms / Expected vrms: {:.5e} / {:.5e}".format(vrms,expected)) +# if rerr > rtol: +# es = "Model vrms greater the test tolerance. {:.4e} > {:.4e}".format(error, rtol) +# raise RuntimeError(es) + +# %% +figV = vis.Figure() +figV.append(vis.objects.Mesh(mesh, segmentsPerEdge=1)) +figV.append(vis.objects.Surface(mesh, vdotv, onMesh=True)) +# figV.append(vis.objects.VectorArrows(mesh, vField, autoscale=True, onMesh=True)) +# figV.append(vis.objects.VectorArrows(mesh, vField, onMesh=True)) + +if uw.mpi.size == 1: figV.window() + +# %% From 41cbd5ff2f049714cd410044978cdbbc6c485a0d Mon Sep 17 00:00:00 2001 From: julian Date: Thu, 11 Mar 2021 12:01:57 +1100 Subject: [PATCH 09/13] update sregion_stokes.py --- docs/examples/curvi_examples/sregion_stokes.py | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/docs/examples/curvi_examples/sregion_stokes.py b/docs/examples/curvi_examples/sregion_stokes.py index e50f09ea6..8e9692016 100644 --- a/docs/examples/curvi_examples/sregion_stokes.py +++ b/docs/examples/curvi_examples/sregion_stokes.py @@ -31,7 +31,8 @@ OTOL=1e-4 # elementRes=(10,18,18) -elementRes = (10,12,12) +elementRes=(16,32,32) +#elementRes = (10,12,12) mesh = uw.mesh.FeMesh_SRegion(elementRes=elementRes, radialLengths=(3.0,6.)) @@ -130,6 +131,7 @@ def checkpoint( mesh, fieldDict, swarm, swarmDict, index, # xdmf output fieldDict = {'velocity':vField, 'density':dField, + 'pressure':pField, 'rank':rField} checkpoint(mesh, fieldDict, None, None, index=0, prefix='output') @@ -180,10 +182,10 @@ def checkpoint( mesh, fieldDict, swarm, swarmDict, index, # %% solver = uw.systems.Solver(stokesSLE) -# monitor src iteration tolerance solver.options.scr.ksp_monitor='' solver.set_inner_rtol(ITOL) solver.set_outer_rtol(OTOL) +#solver.options.src.(1e-8) # if uw.mpi.size == 1: # solver.set_inner_method("mumps") From 1e81d1125c9a957694c69d4cfde8c2f5de38ce5f Mon Sep 17 00:00:00 2001 From: julian Date: Fri, 26 Mar 2021 14:35:26 +1100 Subject: [PATCH 10/13] Updating models --- docs/examples/08_Uplift_TractionBCs.ipynb | 2 +- .../curvi_examples/Annulus_StokesAdvection.py | 36 +-- .../examples/curvi_examples/sregion_stokes.py | 223 ------------------ .../curvi_examples/sregion_stokes_pic.py | 69 +++--- docs/user_guide/08_StokesSolver.ipynb | 2 +- underworld/systems/_stokes.py | 14 +- 6 files changed, 62 insertions(+), 284 deletions(-) delete mode 100644 docs/examples/curvi_examples/sregion_stokes.py diff --git a/docs/examples/08_Uplift_TractionBCs.ipynb b/docs/examples/08_Uplift_TractionBCs.ipynb index a2dba5063..f9fcfdee9 100644 --- a/docs/examples/08_Uplift_TractionBCs.ipynb +++ b/docs/examples/08_Uplift_TractionBCs.ipynb @@ -431,7 +431,7 @@ "name": "python", "nbconvert_exporter": "python", "pygments_lexer": "ipython3", - "version": "3.5.3" + "version": "3.8.5" } }, "nbformat": 4, diff --git a/docs/examples/curvi_examples/Annulus_StokesAdvection.py b/docs/examples/curvi_examples/Annulus_StokesAdvection.py index d0089bfdb..ee54a042f 100644 --- a/docs/examples/curvi_examples/Annulus_StokesAdvection.py +++ b/docs/examples/curvi_examples/Annulus_StokesAdvection.py @@ -23,20 +23,14 @@ import math, os import numpy as np from mpi4py import MPI +comm = MPI.COMM_WORLD # %% -# Set simulation box size. -boxHeight = 1.0 -boxLength = 2.0 -# Set the resolution. -res = 2 -# Set min/max temperatures. -tempMin = 0.0 -tempMax = 1.0 - -comm = MPI.COMM_WORLD outputDir = 'outputWithSwarm/' - +nEls = (10,60) # radial / tangential +radialLengths = (4.,6.) # inner / outer radius lengths + +# %% if uw.mpi.rank == 0: step = 1 while os.path.exists(outputDir): @@ -49,7 +43,7 @@ store = vis.Store(outputDir+'/viz') # build annulus mesh - handles deforming a recangular mesh and applying periodic dofs -mesh = uw.mesh.FeMesh_Annulus(elementRes=(10,60), radialLengths=(4.,6.)) +mesh = uw.mesh.FeMesh_Annulus(elementRes=nEls, radialLengths=radialLengths) velocityField = mesh.add_variable( nodeDofCount=2 ) pressureField = mesh.subMesh.add_variable( nodeDofCount=1 ) vmag = fn.math.sqrt(fn.math.dot( velocityField, velocityField )) @@ -65,9 +59,10 @@ upper = mesh.specialSets["MaxI_VertexSet"] # (vx,vy) -> (vn,vt) (normal, tangential) -velocityField.data[ upper.data ] = [0.0,10.0] +velocityField.data[ lower ] = [0.0, 0.0] +velocityField.data[ upper ] = [0.0,10.0] velBC = uw.conditions.CurvilinearDirichletCondition( variable = velocityField, - indexSetsPerDof = (lower+upper, upper), + indexSetsPerDof = (lower+upper, upper+lower), basis_vectors = (mesh.bnd_vec_normal, mesh.bnd_vec_tangent)) @@ -91,7 +86,7 @@ fig = vis.Figure(store=store) fig.append( vis.objects.Mesh( mesh )) fig.append(vis.objects.Points(swarm, fn_colour=tvar, fn_size=4, colours="blue red")) -# fig.append( vis.objects.VectorArrows(mesh, velocityField)) +fig.append( vis.objects.VectorArrows(mesh, velocityField)) fig.show() # %% @@ -109,15 +104,24 @@ # re-rotate and unmix ierr = uw.libUnderworld.Underworld.AXequalsX( stokesSLE._rot._cself, stokesSLE._velocitySol._cself, False) +# %% +# pressure +figp = vis.Figure() +figp.append(vis.objects.Mesh(mesh)) +figp.append(vis.objects.Surface(mesh, pressureField, onMesh=True)) +figp.show() + # %% fig.show() # %% i=0 +maxSteps = 30 t0 = MPI.Wtime() t_adv = 0.; t_save = 0.; -while i < 30: +while i < maxSteps: + t_adv = MPI.Wtime() # advect particles and count advector.integrate(advector.get_max_dt()) diff --git a/docs/examples/curvi_examples/sregion_stokes.py b/docs/examples/curvi_examples/sregion_stokes.py deleted file mode 100644 index 8e9692016..000000000 --- a/docs/examples/curvi_examples/sregion_stokes.py +++ /dev/null @@ -1,223 +0,0 @@ -# --- -# jupyter: -# jupytext: -# formats: ipynb,py:percent -# text_representation: -# extension: .py -# format_name: percent -# format_version: '1.3' -# jupytext_version: 1.4.2 -# kernelspec: -# display_name: Python 3 -# language: python -# name: python3 -# --- - -# %% [markdown] -# ## Demo of buoyancy driven stokes flow in an Spherical Region - -# %% -import underworld as uw -import underworld.visualisation as vis -import numpy as np -from underworld import function as fn - -# %% -# boundary conditions available "BC_FREESLIP, "BC_NOSLIP, "BC_LIDDRIVEN" -bc_wanted = 'BC_FREESLIP' - -# %% -ITOL=1e-6 -OTOL=1e-4 - -# elementRes=(10,18,18) -elementRes=(16,32,32) -#elementRes = (10,12,12) -mesh = uw.mesh.FeMesh_SRegion(elementRes=elementRes, - radialLengths=(3.0,6.)) - -# fields for stokes -vField = mesh.add_variable(nodeDofCount=mesh.dim) -pField = mesh.subMesh.add_variable(nodeDofCount=1) - -# fields for density and rank -dField = mesh.add_variable(nodeDofCount=1) -rField = mesh.subMesh.add_variable(nodeDofCount=1) - -inner = mesh.specialSets["innerWall_VertexSet"] -outer = mesh.specialSets["outerWall_VertexSet"] -W = mesh.specialSets["westWall_VertexSet"] -E = mesh.specialSets["eastWall_VertexSet"] -S = mesh.specialSets["southWall_VertexSet"] -N = mesh.specialSets["northWall_VertexSet"] - -allWalls = mesh.specialSets["AllWalls_VertexSet"] -NS0 = N+S-(E+W) -# build corner edges node indexset -cEdge = (N&W)+(N&E)+(S&E)+(S&W) - - -# %% -# create checkpoint function -def checkpoint( mesh, fieldDict, swarm, swarmDict, index, - meshName='mesh', swarmName='swarm', - prefix='./', enable_xdmf=True): - import os - # Check the prefix is valid - if prefix is not None: - if not prefix.endswith('/'): prefix += '/' # add a backslash - if not os.path.exists(prefix) and uw.mpi.rank==0: - print("Creating directory: ",prefix) - os.makedirs(prefix) - uw.mpi.barrier() - - if not isinstance(index, int): - raise TypeError("'index' is not of type int") - ii = str(index) - - if mesh is not None: - - # Error check the mesh and fields - if not isinstance(mesh, uw.mesh.FeMesh): - raise TypeError("'mesh' is not of type uw.mesh.FeMesh") - if not isinstance(fieldDict, dict): - raise TypeError("'fieldDict' is not of type dict") - for key, value in fieldDict.items(): - if not isinstance( value, uw.mesh.MeshVariable ): - raise TypeError("'fieldDict' must contain uw.mesh.MeshVariable elements") - - - # see if we have already saved the mesh. It only needs to be saved once - if not hasattr( checkpoint, 'mH' ): - checkpoint.mH = mesh.save(prefix+meshName+".h5") - mh = checkpoint.mH - - for key,value in fieldDict.items(): - filename = prefix+key+'-'+ii - handle = value.save(filename+'.h5') - if enable_xdmf: value.xdmf(filename, handle, key, mh, meshName) - - # is there a swarm - if swarm is not None: - - # Error check the swarms - if not isinstance(swarm, uw.swarm.Swarm): - raise TypeError("'swarm' is not of type uw.swarm.Swarm") - if not isinstance(swarmDict, dict): - raise TypeError("'swarmDict' is not of type dict") - for key, value in swarmDict.items(): - if not isinstance( value, uw.swarm.SwarmVariable ): - raise TypeError("'fieldDict' must contain uw.swarm.SwarmVariable elements") - - sH = swarm.save(prefix+swarmName+"-"+ii+".h5") - for key,value in swarmDict.items(): - filename = prefix+key+'-'+ii - handle = value.save(filename+'.h5') - if enable_xdmf: value.xdmf(filename, handle, key, sH, swarmName) - - -# %% -# setup dField and rField -rField.data[:] = uw.mpi.rank - -z_hat = -1.0*mesh.fn_unitvec_radial() - -inds = (mesh.data[:,0]**2 + mesh.data[:,1]**2 + (mesh.data[:,2]-4.5)**2) < 1.5**2 -dField.data[inds] = 1. - -bodyForceFn = dField * z_hat - -# %% -# xdmf output -fieldDict = {'velocity':vField, - 'density':dField, - 'pressure':pField, - 'rank':rField} -checkpoint(mesh, fieldDict, None, None, index=0, prefix='output') - -# %% -fig = vis.Figure() -fig.append(vis.objects.Mesh(mesh, segmentsPerEdge=1)) -fig.append(vis.objects.Surface(mesh, dField, onMesh=True )) -if uw.utils.is_kernel(): fig.show() - -# %% -# zero all dofs of vField -vField.data[...] = 0. - -if bc_wanted == "BC_NOSLIP": - # no slip - vBC = uw.conditions.CurvilinearDirichletCondition( variable=vField, indexSetsPerDof=(allWalls,allWalls,allWalls)) - -elif bc_wanted == "BC_FREESLIP": - # free-slip - - vField.data[cEdge.data] = (0.,0.,0.) - vBC = uw.conditions.CurvilinearDirichletCondition( variable=vField, - indexSetsPerDof=(inner+outer,E+W+cEdge,NS0+cEdge), - basis_vectors = (mesh._e1, mesh._e2, mesh._e3) ) -elif bc_wanted == "BC_LIDDRIVEN": - # lid-driven case - - # build driving node indexset & apply velocities with zero radial component - drivers = outer - (N+S+E+W) - vField.data[drivers.data] = (0.,1.,1.) - - # build corner edges node indexset and apply velocities with zero non-radial components - cEdge = (N&W)+(N&E)+(S&E)+(S&W) - vField.data[cEdge.data] = (0.,0.,0.) - - # apply altogether. - NS0 = N+S - (E+W) - vBC = uw.conditions.CurvilinearDirichletCondition( variable=vField, - indexSetsPerDof=(inner+outer,drivers+E+W+cEdge,drivers+NS0+cEdge), # optional, can include cEdge on the 3rd component - basis_vectors = (mesh._e1, mesh._e2, mesh._e3) ) -else: - raise ValueError("Can't find an option for the 'bc_wanted' = {}".format(bc_wanted)) - -# %% -stokesSLE = uw.systems.Stokes( vField, pField, - fn_viscosity=1.0, fn_bodyforce=bodyForceFn, - conditions=vBC, _removeBCs=False) - -# %% -solver = uw.systems.Solver(stokesSLE) -solver.options.scr.ksp_monitor='' -solver.set_inner_rtol(ITOL) -solver.set_outer_rtol(OTOL) -#solver.options.src.(1e-8) -# if uw.mpi.size == 1: -# solver.set_inner_method("mumps") - -# %% -solver.solve() -# must re-orient boundary vectors -ignore = uw.libUnderworld.Underworld.AXequalsX( stokesSLE._rot._cself, stokesSLE._velocitySol._cself, False) - -# %% -# xdmf output -checkpoint(mesh, fieldDict, None, None, index=1, prefix='output') - -# %% -vdotv = fn.math.dot(vField,vField) -vrms = np.sqrt( mesh.integrate(vdotv)[0] / mesh.integrate(1.)[0] ) -if uw.mpi.rank == 0 and elementRes == (10,12,12): - rtol = 1e-3 - expected = 6.91805e-02 - error = np.abs(vrms - expected) - rerr = error / expected - print("Model vrms / Expected vrms: {:.5e} / {:.5e}".format(vrms,expected)) - if rerr > rtol: - es = "Model vrms greater the test tolerance. {:.4e} > {:.4e}".format(error, rtol) - raise RuntimeError(es) - -# %% -figV = vis.Figure() -figV.append(vis.objects.Mesh(mesh, segmentsPerEdge=1)) -figV.append(vis.objects.Surface(mesh, vdotv, onMesh=True)) -# figV.append(vis.objects.VectorArrows(mesh, vField, autoscale=True, onMesh=True)) -# figV.append(vis.objects.VectorArrows(mesh, vField, onMesh=True)) - -if uw.mpi.size == 1: figV.window() - -# %% diff --git a/docs/examples/curvi_examples/sregion_stokes_pic.py b/docs/examples/curvi_examples/sregion_stokes_pic.py index e6e077d75..ed2f35dc4 100644 --- a/docs/examples/curvi_examples/sregion_stokes_pic.py +++ b/docs/examples/curvi_examples/sregion_stokes_pic.py @@ -25,13 +25,17 @@ # %% # boundary conditions available "BC_FREESLIP, "BC_NOSLIP, "BC_LIDDRIVEN" bc_wanted = 'BC_FREESLIP' +output_path = 'pic_output' -# %% -ITOL=1e-6 -OTOL=1e-4 +maxsteps = 5 +elementRes=(12,20,20) +#elementRes = (16,24,24) + +# inner / outer stokes solve tolerances +ITOL=1e-4 +OTOL=1e-3 -# elementRes=(10,18,18) -elementRes = (8,20,20) +# %% mesh = uw.mesh.FeMesh_SRegion(elementRes=elementRes, radialLengths=(3.0,6.)) @@ -57,7 +61,7 @@ S = mesh.specialSets["southWall_VertexSet"] N = mesh.specialSets["northWall_VertexSet"] -allWalls = mesh.specialSets["AllWalls_VertexSet"] +all_boundary_set = mesh.specialSets["AllWalls_VertexSet"] NS0 = N+S-(E+W) # build corner edges node indexset cEdge = (N&W)+(N&E)+(S&E)+(S&W) @@ -134,18 +138,16 @@ def checkpoint( mesh, fieldDict, swarm, swarmDict, index, rField.data[:] = uw.mpi.rank rvar.data[:] = uw.mpi.rank +# z_hat, the gravity unit vector z_hat = -1.0*mesh.fn_unitvec_radial() +# dvar is the density anomaly inds = (swarm.data[:,0]**2 + swarm.data[:,1]**2 + (swarm.data[:,2]-4.5)**2) < 0.75**2 if inds.size != 0: dvar.data[inds] = 1. -# inds = (mesh.data[:,0]**2 + mesh.data[:,1]**2 + (mesh.data[:,2]-4.5)**2) < 1.5**2 -# dField.data[inds] = 1. bodyForceFn = dvar * z_hat -fvar = swarm.add_variable(dataType="double", count=3) -fvar.data[:] = bodyForceFn.evaluate(swarm) # %% # xdmf output @@ -154,10 +156,7 @@ def checkpoint( mesh, fieldDict, swarm, swarmDict, index, 'rank':rField} swarmDict = {'orank':rvar, - 'density':dvar, - 'force':fvar} - -# checkpoint(mesh, fieldDict, swarm, swarmDict, index=0, prefix='output') + 'density':dvar} # %% fig = vis.Figure() @@ -171,7 +170,7 @@ def checkpoint( mesh, fieldDict, swarm, swarmDict, index, if bc_wanted == "BC_NOSLIP": # no slip - vBC = uw.conditions.CurvilinearDirichletCondition( variable=vField, indexSetsPerDof=(allWalls,allWalls,allWalls)) + vBC = uw.conditions.CurvilinearDirichletCondition( variable=vField, indexSetsPerDof=(all_boundary_set,all_boundary_set,all_boundary_set)) elif bc_wanted == "BC_FREESLIP": # free-slip @@ -209,8 +208,6 @@ def checkpoint( mesh, fieldDict, swarm, swarmDict, index, solver.options.scr.ksp_monitor='' solver.set_inner_rtol(ITOL) solver.set_outer_rtol(OTOL) -if uw.mpi.size == 1: - solver.set_inner_method("mumps") # %% solver.solve() @@ -218,42 +215,42 @@ def checkpoint( mesh, fieldDict, swarm, swarmDict, index, ignore = uw.libUnderworld.Underworld.AXequalsX( stokesSLE._rot._cself, stokesSLE._velocitySol._cself, False) # %% -maxsteps=1 -time = 0 # xdmf output -checkpoint(mesh, fieldDict, swarm, swarmDict, index=0, prefix='output', time=0) - +checkpoint(mesh, fieldDict, swarm, swarmDict, index=0, prefix=output_path, time=0) # %% -for step in range(maxsteps): +step = 1 +time = 0 +while step < maxsteps: # advect particles dt = advector.get_max_dt() time = time + dt advector.integrate(dt) + vField.data[all_boundary_set] = (0.,0.,0.) solver.solve() # must re-orient boundary vectors ignore = uw.libUnderworld.Underworld.AXequalsX( stokesSLE._rot._cself, stokesSLE._velocitySol._cself, False) - checkpoint(mesh, fieldDict, swarm, swarmDict, index=step, time=time, prefix='output') + checkpoint(mesh, fieldDict, swarm, swarmDict, index=step, time=time, prefix=output_path) + + step += 1 # %% vdotv = fn.math.dot(vField,vField) vrms = np.sqrt( mesh.integrate(vdotv)[0] / mesh.integrate(1.)[0] ) -if uw.mpi.rank == 0: +if uw.mpi.rank == 0 and step == 5: rtol = 1e-3 - expected = 6.89257e-02 + expected = 1.67223e-2 error = np.abs(vrms - expected) rerr = error / expected print("Model vrms / Expected vrms: {:.5e} / {:.5e}".format(vrms,expected)) -# if rerr > rtol: -# es = "Model vrms greater the test tolerance. {:.4e} > {:.4e}".format(error, rtol) -# raise RuntimeError(es) + if rerr > rtol: + es = "Model vrms greater the test tolerance. {:.4e} > {:.4e}".format(rerr, rtol) + raise RuntimeError(es) # %% -figV = vis.Figure() -figV.append(vis.objects.Mesh(mesh, segmentsPerEdge=1)) -figV.append(vis.objects.Surface(mesh, vdotv, onMesh=True)) -# figV.append(vis.objects.VectorArrows(mesh, vField, autoscale=True, onMesh=True)) -# figV.append(vis.objects.VectorArrows(mesh, vField, onMesh=True)) +# figV = vis.Figure() +# figV.append(vis.objects.Mesh(mesh, segmentsPerEdge=1)) +# figV.append(vis.objects.Surface(mesh, vdotv, onMesh=True)) +# # figV.append(vis.objects.VectorArrows(mesh, vField, autoscale=True, onMesh=True)) +# # figV.append(vis.objects.VectorArrows(mesh, vField, onMesh=True)) -if uw.mpi.size == 1: figV.window() - -# %% +# if uw.mpi.size == 1: figV.window() diff --git a/docs/user_guide/08_StokesSolver.ipynb b/docs/user_guide/08_StokesSolver.ipynb index 28a29f379..2aa0df1a6 100644 --- a/docs/user_guide/08_StokesSolver.ipynb +++ b/docs/user_guide/08_StokesSolver.ipynb @@ -317,7 +317,7 @@ "name": "python", "nbconvert_exporter": "python", "pygments_lexer": "ipython3", - "version": "3.5.3" + "version": "3.8.5" } }, "nbformat": 4, diff --git a/underworld/systems/_stokes.py b/underworld/systems/_stokes.py index 3f182c6b6..49e7901b6 100644 --- a/underworld/systems/_stokes.py +++ b/underworld/systems/_stokes.py @@ -309,8 +309,8 @@ def avtop_pressure_nullspace_removal(): def redefineVelocityDirichletBC(self, basis_vectors): ''' Function to hide the implementation of rotating dirichlet boundary conditions. - Here we build a global rotation matrix and a local assembly term for it for 2 reasons. - 1) The assembly term rotates local element contributions immediately after their local evaluation + Here we build a global rotation matrix (and required assembly term) for 2 reasons. + 1) The assembly term rotates element contributions immediately after their element evaluation for the stokes system. This supports the Engelman & Sani idea in, THE IMPLEMENTATION OF NORMAL AND/OR TANGENTIAL BOUNDARY CONDITIONS IN FINITE ELEMENT CODES FOR INCOMPRESSIBLE FLUID FLOW, 1982 @@ -348,7 +348,7 @@ def redefineVelocityDirichletBC(self, basis_vectors): gaussSwarm = self._constitMatTerm._integrationSwarm self._rot._cself.assembleOnNodes = 1 # important doesn't perform FEM integral - term = self._term = uw.systems.sle.MatrixAssemblyTerm_RotationDof(integrationSwarm=gaussSwarm, + rterm = self._term = uw.systems.sle.MatrixAssemblyTerm_RotationDof(integrationSwarm=gaussSwarm, assembledObject = self._rot, fn_basis=basis_vectors, mesh=mesh) @@ -357,14 +357,14 @@ def redefineVelocityDirichletBC(self, basis_vectors): vnsEqNum._cself.removeBCs = True uw.libUnderworld.StgFEM.StiffnessMatrix_Assemble( self._rot._cself, - None, None ); + None, None ) vnsEqNum._cself.removeBCs = False # self._eqNums[self._velocityField]._cself.removeBCs=False # add rotation matrix element terms using the following - uw.libUnderworld.StgFEM.StiffnessMatrix_SetRotationTerm(self._kmatrix._cself, term._cself) - uw.libUnderworld.StgFEM.StiffnessMatrix_SetRotationTerm(self._gmatrix._cself, term._cself) - uw.libUnderworld.StgFEM.ForceVector_SetRotationTerm(self._fvector._cself, term._cself) + uw.libUnderworld.StgFEM.StiffnessMatrix_SetRotationTerm(self._kmatrix._cself, rterm._cself) + uw.libUnderworld.StgFEM.StiffnessMatrix_SetRotationTerm(self._gmatrix._cself, rterm._cself) + uw.libUnderworld.StgFEM.ForceVector_SetRotationTerm(self._fvector._cself, rterm._cself) def _add_to_stg_dict(self,componentDictionary): From 07740ddd9c0e1a347295adbfb29f57dbe7c93547 Mon Sep 17 00:00:00 2001 From: Julian Giordani Date: Tue, 17 Aug 2021 14:22:18 +1000 Subject: [PATCH 11/13] Update curvi-bits with 2.11 (#565) * Fix issue 537 - vrms typo * Making sure the factor is an appropriate numpy type * Updates for docker image generation: (#550) * Switch deprecated `MAINTAINER` tag for new `LABEL` construct. * Set `/opt` to ugo+rwx to allow users to install new pip packages. * Remove `tini` which no longer appears to be required (and wasn't being used previously in any case). * Remove extras for notebook MPI usage, as this wasn't being used and is easy to reinstall. * Update UW image to point to newest base file image. * Update to CHANGES.md * Renaming guidelines.md -> development_guidelines.md * * Fixes h5 file save mode (underworldcode/underworld2/#533) * Minor documentation updates. * Minor redundant code removal. * Adding a pull request template! * Update `CHANGES.md` * Update to fix broken ReadTheDocs generation. * * Import scaling into uw. (#554) * Fix docstrings & doctests. * Rename pull request template as Github doesn't appear to register it with upper case naming. * Fix test in config which Py3.8 is complaining about. * * Added a check to ensure that the selected solver is available at runtime. (#553) The previous behaviour resulted in solves continuing unaware of solve difficulties, and failing elsewhere, or worse. * Added test for the solver check. * Solver check (#555) * * Added a check to ensure that the selected solver is available at runtime. The previous behaviour resulted in solves continuing unaware of solve difficulties, and failing elsewhere, or worse. * Added test for the solver check. * I've updated these models to run with "mumps" if available, and fallback to the default solver otherwise. * Using a relative path to reload swarm files * * For sequential IO operations, need to ensure that non-root procs open file with `append` mode instead of `write` mode (otherwise a new file is created by each proc, effectively deleting all data previously written). * Update mesh file mode to `write` mode. * Making test_long.sh executable * Adding a EBA example to the tests for now. Documentation will come next release when it's moved into examples. The 2 extra terms in the heat equation, viscous dissipation and adiabatic heating, should have equal volume integrals in the model, as per King et al. * Update CHANGES * github actions docker images * * Documentation, version numbers, and docker file updates. (#559) * File creation mode should be "w" here too. Interesting, "append" mode causes a test failure in the Docker container, but not baremetal Ubuntu. * Update docker_build_push.yml * Update docker_build_push.yml * Update docker_build_push.yml * Adding symlink to `LICENSE.md` for conda. Co-authored-by: John Mansour --- .github/pull_request_template.md | 8 + .github/workflows/docker_build_push.yml | 28 + CHANGES.md | 16 +- CONTRIBUTING.rst | 21 +- Dockerfile | 2 +- LICENSE.md | 6 +- README.md | 15 +- conda/LICENSE.md | 1 + ...uidelines.md => development_guidelines.md} | 0 docs/development/docker/base/Dockerfile | 31 +- docs/development/docker/lavavu/Dockerfile | 27 +- docs/development/docker/petsc/Dockerfile | 39 +- docs/development/docker/stampede2/Dockerfile | 2 +- .../development/docker/underworld2/Dockerfile | 7 +- docs/development/docs_generator/conf.py | 2 +- docs/development/release_guidelines.md | 4 +- docs/examples/04_StokesSinker.ipynb | 52 +- docs/examples/06_SlabSubduction.ipynb | 30 +- docs/examples/07_ShearBandsPureShear.ipynb | 10 +- .../install_guides/pawsey_magnus_container.md | 6 +- docs/test/14_Convection_EBA.ipynb | 639 ++++++++++++++++++ docs/test/solver_non_existent.py | 82 +++ docs/user_guide/06_Utilities.ipynb | 6 +- docs/user_guide/08_StokesSolver.ipynb | 16 +- underworld/__init__.py | 1 + underworld/_version.py | 2 +- .../src/BSSCR/auglag-driver-DGTGD.c | 13 +- .../KSPSolvers/src/StokesBlockKSPInterface.c | 6 +- .../Energy/src/Energy_SLE_Solver.c | 7 +- .../libUnderworld/config/utils/options.py | 2 +- underworld/mesh/_mesh.py | 2 +- underworld/mesh/_meshvariable.py | 2 +- underworld/mpi.py | 1 - underworld/scaling/_scaling.py | 11 +- underworld/swarm/_swarmvariable.py | 5 +- underworld/systems/_energy_solver.py | 2 +- underworld/timing.py | 2 +- underworld/utils/_io.py | 4 + 38 files changed, 966 insertions(+), 144 deletions(-) create mode 100644 .github/pull_request_template.md create mode 100644 .github/workflows/docker_build_push.yml create mode 120000 conda/LICENSE.md rename docs/development/{guidelines.md => development_guidelines.md} (100%) create mode 100644 docs/test/14_Convection_EBA.ipynb create mode 100644 docs/test/solver_non_existent.py diff --git a/.github/pull_request_template.md b/.github/pull_request_template.md new file mode 100644 index 000000000..b6e436eb5 --- /dev/null +++ b/.github/pull_request_template.md @@ -0,0 +1,8 @@ +# PR Checklist + +- [ ] I have read the `CONTRIBUTING.rst` document. +- [ ] I have updated the docstrings accordingly. +- [ ] I have updated `CHANGES.md`. +- [ ] I have added tests which give complete coverage for my changes. +- [ ] I have provided a usage example for my changes. +- [ ] All new and existing tests pass. diff --git a/.github/workflows/docker_build_push.yml b/.github/workflows/docker_build_push.yml new file mode 100644 index 000000000..86dd75b66 --- /dev/null +++ b/.github/workflows/docker_build_push.yml @@ -0,0 +1,28 @@ +name: docker image build and push to dockerhub + +on: + push: + branches: + - 'jgiordani/curvi-bits' + +jobs: + docker: + runs-on: ubuntu-latest + steps: + - + name: Checkout + uses: actions/checkout@v2 + - + name: Login to DockerHub + uses: docker/login-action@v1 + with: + username: ${{ secrets.DOCKERHUB_USERNAME }} + password: ${{ secrets.DOCKERHUB_TOKEN }} + - + name: Build and push + uses: docker/build-push-action@v2 + with: + context: . + file: ./docs/development/docker/underworld2/Dockerfile + push: true + tags: julesg/curvi_bits:latest diff --git a/CHANGES.md b/CHANGES.md index aa1bef6fd..572464884 100644 --- a/CHANGES.md +++ b/CHANGES.md @@ -3,13 +3,21 @@ CHANGES: Underworld2 Release 2.11.0 [] ----------------- -Changes +Changes: * Enabled user defined Gauss integration swarms for all systems. * Update docker base images: switch to ubuntu(20.04), update petsc(3.1.4) & mpich(3.3.2), other tweaks. * Cleaner Python compile time configuration. +* Add runtime check for solver availability (for example, 'mumps'). Also add test for check. -New: +Docker image changes: +* Update base image. +* Updated to PETSc 3.15.1 +* Updated to MPICH 3.4.2 +* Removed petsc4py. +* Switched `/opt` to ugo+rwx to allow users to install Python packages. +New: +* Model for EBA convection, based on King et al. (2010) benchmarks. See `docs/test/14_Convection_EBA.ipynb` * Mesh/MeshVariable/Swarm/SwarmVariable objects now support loading and saving of units information, as well as additional attributes. For usage examples, see `docs/test/mesh_aux.py` and `docs/test/swarm_aux.py`. @@ -17,6 +25,7 @@ New: * Conda binaries available via underworldcode conda channel `conda install -c underworldcode underworld2` * Added `underworld.function.count()` method, which counts function calls. * Added GADI install/run scripts @ ./docs/install_guides/nci_gadi/ +* Updated pull request related documentation and added template. Fixes: * Updates for SCons4.1. @@ -25,6 +34,9 @@ Fixes: structures to accomodate. * Tester uses `jupyter-nbconvert` which no longer defaults to Python. Update to explicitly select Python. +* Switched h5 file save to mode "w" instead of "a" as append mode resulted + in data from previous datasets (with identical name) not being removed from + file, and file sizes therefore growing unnecessarily. Release 2.10.1 [2020-08-28] --------------------------- diff --git a/CONTRIBUTING.rst b/CONTRIBUTING.rst index aaa70a81b..80d3eea25 100644 --- a/CONTRIBUTING.rst +++ b/CONTRIBUTING.rst @@ -15,23 +15,24 @@ For Bug reports and Suggestions / Requests please submit an Issue on the Underwo Click here to submit an Issue https://github.com/underworldcode/underworld2/issues - -For Code / Documentation changes please submit a GitHub Pull Request (PR). This allows us to review and discuss the contributions before merging it into our ``development`` branch. -As part of the PR please provide a documentation change summary (for our ``Changes.md``) and a test where applicable. - -For creating Pull Request (PR) we recommend following the workflow outlined https://guides.github.com/activities/forking/. +For Code / Documentation changes please submit a GitHub Pull Request (PR). This allows us to review and discuss the contributions before merging it into our ``development`` branch. For creating Pull Request (PR) we recommend following the workflow outlined https://guides.github.com/activities/forking/. More specifically: 1. Fork Underworld via GitHub and clone it to your machine. -2. Add the original Underworld repository as `upstream` and branch your contribution off its development branch. + .. code-block:: + + git clone https://github.com/YOUR_GITHUB_ACCOUNT/underworld2 + +2. Add the master Underworld repository as an additional remote source (named `uwmaster`) for your local repo and pull down its latest changesets. Checkout to the master/development repo state, and then create a new local branch which will contain your forthcoming changes. .. code-block:: - git remote add upstream https://github.com/underworldcode/underworld2 - git checkout upstream/development ; git checkout -b newFeature + git remote add uwmaster https://github.com/underworldcode/underworld2 + git pull uwmaster + git checkout uwmaster/development + git checkout -b newFeature -3. Make your changes! Remember to write comments, a test if applicable and follow the code style of the project (see ``./docs/development/guidelines.md`` for details). - We ask that a short description be made in the commit message that is appropriate for our ``Changes.md`` summary. If the change is sizeable or adds news functionality we ask for an associated blog post which describes the contribution in its full glory. Your time to shine! Details on how to write a blog post are coming soon. +3. Make your changes! Remember to write comments, a test if applicable and follow the code style of the project (see ``./docs/development/guidelines.md`` for details). 4. Push your changes to your GitHub fork and then submit a PR to the ``development`` branch of Underworld via Github. diff --git a/Dockerfile b/Dockerfile index 22f742fcd..ba015a92a 100644 --- a/Dockerfile +++ b/Dockerfile @@ -1,4 +1,4 @@ -FROM underworldcode/underworld2:dev +FROM underworldcode/underworld2:2.11.0b # Set the UW_MACHINE env variable for metrics ENV UW_MACHINE binder diff --git a/LICENSE.md b/LICENSE.md index bfd23876e..4303a571a 100644 --- a/LICENSE.md +++ b/LICENSE.md @@ -16,9 +16,9 @@ Underworld has been in development since 2003. It has always been released under ### Copyright holders -Copyright Australian National University, 2020 -Copyright Melbourne University, 2014-2020 -Copyright Monash University, 2003-2020 +Copyright Australian National University, 2020-2021 +Copyright Melbourne University, 2014-2021 +Copyright Monash University, 2003-2021 Copyright VPAC, 2003-2009 ### References diff --git a/README.md b/README.md index 8921ba42a..f7749b9bb 100644 --- a/README.md +++ b/README.md @@ -29,18 +29,15 @@ In particular, the *Getting Started* section of the User Guide might be useful p Trying out Underworld2 ---------------------- -You can try out the code immediately via a Jupyter Binder cloud instance. Be aware that it can take a little while for the site to fire up and that it will time-out after 30 minutes of inactivity and reset if you log back in. +You can try out the code immediately via a Jupyter Binder cloud instance. The Binder environment is identical to that obtained through running an Underworld Docker image locally. +Note that it can take a while for the site to fire up and that it will time-out after 30 minutes of inactivity and reset if you log back in. | | | |-|-| -| [![Binder](https://mybinder.org/badge.svg)](https://mybinder.org/v2/gh/underworldcode/underworld2/v2.10.0b) | v2.10.0b (Py3) | -| [![Binder](https://mybinder.org/badge.svg)](https://mybinder.org/v2/gh/underworldcode/underworld2/v2.9.2b) | v2.9.2b (Py3) | -| [![Binder](https://mybinder.org/badge.svg)](https://mybinder.org/v2/gh/underworldcode/underworld2/v2.8.2b) | v2.8.2b (Py3) | -| [![Binder](https://mybinder.org/badge.svg)](https://mybinder.org/v2/gh/underworldcode/underworld2/v2.7.1b) | v2.7.1b (Py2) | -| [![Binder](https://mybinder.org/badge.svg)](https://mybinder.org/v2/gh/underworldcode/underworld2/development) | dev (Py3) | - - -Note that the Binder environment is identical to that obtained through running an Underworld Docker image locally. +| [![Binder](https://mybinder.org/badge.svg)](https://mybinder.org/v2/gh/underworldcode/underworld2/v2.11.0b) | v2.11.0b | +| [![Binder](https://mybinder.org/badge.svg)](https://mybinder.org/v2/gh/underworldcode/underworld2/v2.10.0b) | v2.10.0b | +| [![Binder](https://mybinder.org/badge.svg)](https://mybinder.org/v2/gh/underworldcode/underworld2/v2.9.2b) | v2.9.2b | +| [![Binder](https://mybinder.org/badge.svg)](https://mybinder.org/v2/gh/underworldcode/underworld2/development) | dev | Getting Underworld2 diff --git a/conda/LICENSE.md b/conda/LICENSE.md new file mode 120000 index 000000000..7eabdb1c2 --- /dev/null +++ b/conda/LICENSE.md @@ -0,0 +1 @@ +../LICENSE.md \ No newline at end of file diff --git a/docs/development/guidelines.md b/docs/development/development_guidelines.md similarity index 100% rename from docs/development/guidelines.md rename to docs/development/development_guidelines.md diff --git a/docs/development/docker/base/Dockerfile b/docs/development/docker/base/Dockerfile index a7db9eb9c..b6492db05 100644 --- a/docs/development/docker/base/Dockerfile +++ b/docs/development/docker/base/Dockerfile @@ -1,5 +1,5 @@ FROM ubuntu:20.04 as base_runtime -MAINTAINER https://github.com/underworldcode/ +LABEL maintainer="https://github.com/underworldcode/" ENV LANG=C.UTF-8 ENV PYVER=3.8 # Setup some things in anticipation of virtualenvs @@ -9,6 +9,19 @@ ENV PATH=${VIRTUAL_ENV}/bin:$PATH # The following ensures venv packages are available when using system python (such as from jupyter) ENV PYTHONPATH=${PYTHONPATH}:${VIRTUAL_ENV}/lib/python${PYVER}/site-packages +# add joyvan user, volume mount and expose port 8888 +EXPOSE 8888 +ENV NB_USER jovyan +ENV NB_WORK /home/$NB_USER +RUN useradd -m -s /bin/bash -N $NB_USER -g users \ +&& mkdir -p /$NB_WORK/workspace \ +&& chown -R $NB_USER:users $NB_WORK +VOLUME $NB_WORK/workspace + +# make virtualenv directory and set permissions +RUN mkdir ${VIRTUAL_ENV} \ +&& chmod ugo+rwx ${VIRTUAL_ENV} + # install runtime requirements RUN apt-get update -qq \ && DEBIAN_FRONTEND=noninteractive apt-get install -yq --no-install-recommends \ @@ -62,21 +75,9 @@ RUN apt-get update \ && rm -rf /var/lib/apt/lists/* RUN PYTHONPATH= /usr/bin/pip3 install -r /opt/requirements.txt -# add tini, joyvan user, volume mount and expose port 8888 -EXPOSE 8888 -ENV NB_USER jovyan -ENV NB_WORK /home/$NB_USER -ADD https://github.com/krallin/tini/releases/download/v0.18.0/tini /tini -RUN ipcluster nbextension enable \ -&& chmod +x /tini \ -&& useradd -m -s /bin/bash -N $NB_USER -g users \ -&& mkdir -p /$NB_WORK/workspace \ -&& chown -R $NB_USER:users $NB_WORK -VOLUME $NB_WORK/workspace - # jovyan user, finalise jupyter env USER $NB_USER -RUN ipython profile create --parallel --profile=mpi \ -&& echo "c.IPClusterEngines.engine_launcher_class = 'MPIEngineSetLauncher'" >> $NB_WORK/.ipython/profile_mpi/ipcluster_config.py +# RUN ipython profile create --parallel --profile=mpi \ +# && echo "c.IPClusterEngines.engine_launcher_class = 'MPIEngineSetLauncher'" >> $NB_WORK/.ipython/profile_mpi/ipcluster_config.py WORKDIR $NB_WORK CMD ["jupyter", "notebook", "--no-browser", "--ip='0.0.0.0'"] diff --git a/docs/development/docker/lavavu/Dockerfile b/docs/development/docker/lavavu/Dockerfile index c91a74ef4..3f1a92f7d 100644 --- a/docs/development/docker/lavavu/Dockerfile +++ b/docs/development/docker/lavavu/Dockerfile @@ -1,5 +1,5 @@ FROM ubuntu:20.04 as base_runtime -MAINTAINER https://github.com/underworldcode/ +LABEL maintainer="https://github.com/underworldcode/" ENV LANG=C.UTF-8 ENV PYVER=3.8 # Setup some things in anticipation of virtualenvs @@ -9,6 +9,19 @@ ENV PATH=${VIRTUAL_ENV}/bin:$PATH # The following ensures venv packages are available when using system python (such as from jupyter) ENV PYTHONPATH=${PYTHONPATH}:${VIRTUAL_ENV}/lib/python${PYVER}/site-packages +# add joyvan user, volume mount and expose port 8888 +EXPOSE 8888 +ENV NB_USER jovyan +ENV NB_WORK /home/$NB_USER +RUN useradd -m -s /bin/bash -N $NB_USER -g users \ +&& mkdir -p /$NB_WORK/workspace \ +&& chown -R $NB_USER:users $NB_WORK +VOLUME $NB_WORK/workspace + +# make virtualenv directory and set permissions +RUN mkdir ${VIRTUAL_ENV} \ +&& chmod ugo+rwx ${VIRTUAL_ENV} + # install runtime requirements RUN apt-get update -qq \ && DEBIAN_FRONTEND=noninteractive apt-get install -yq --no-install-recommends \ @@ -45,8 +58,8 @@ RUN apt-get update -qq RUN DEBIAN_FRONTEND=noninteractive apt-get install -yq --no-install-recommends \ build-essential \ python3-setuptools \ - libpython${PYVER}-dev \ -sudo apt-get install libpng-dev \ + libpython${PYVER}-dev \ + libpng-dev \ libjpeg-dev \ libtiff-dev \ mesa-utils \ @@ -63,6 +76,7 @@ sudo apt-get install libpng-dev \ # lavavu # create a virtualenv to put new python modules +USER $NB_USER RUN /usr/bin/python3 -m virtualenv --system-site-packages --python=/usr/bin/python3 ${VIRTUAL_ENV} RUN LV_OSMESA=1 pip3 install --no-cache-dir --no-binary=lavavu lavavu @@ -78,13 +92,6 @@ RUN apt-mark showmanual >/opt/installed.txt # Add user environment FROM minimal -EXPOSE 8888 -ADD https://github.com/krallin/tini/releases/download/v0.18.0/tini /tini -RUN chmod +x /tini -ENV NB_USER jovyan -RUN useradd -m -s /bin/bash -N jovyan USER $NB_USER -ENV NB_WORK /home/$NB_USER -VOLUME $NB_WORK/workspace WORKDIR $NB_WORK CMD ["jupyter", "notebook", "--no-browser", "--ip='0.0.0.0'"] \ No newline at end of file diff --git a/docs/development/docker/petsc/Dockerfile b/docs/development/docker/petsc/Dockerfile index d778e4a0d..cbfb2b495 100644 --- a/docs/development/docker/petsc/Dockerfile +++ b/docs/development/docker/petsc/Dockerfile @@ -1,5 +1,5 @@ FROM ubuntu:20.04 as base_runtime -MAINTAINER https://github.com/underworldcode/ +LABEL maintainer="https://github.com/underworldcode/" ENV LANG=C.UTF-8 ENV PYVER=3.8 # Setup some things in anticipation of virtualenvs @@ -9,6 +9,19 @@ ENV PATH=${VIRTUAL_ENV}/bin:$PATH # The following ensures venv packages are available when using system python (such as from jupyter) ENV PYTHONPATH=${PYTHONPATH}:${VIRTUAL_ENV}/lib/python${PYVER}/site-packages +# add joyvan user, volume mount and expose port 8888 +EXPOSE 8888 +ENV NB_USER jovyan +ENV NB_WORK /home/$NB_USER +RUN useradd -m -s /bin/bash -N $NB_USER -g users \ +&& mkdir -p /$NB_WORK/workspace \ +&& chown -R $NB_USER:users $NB_WORK +VOLUME $NB_WORK/workspace + +# make virtualenv directory and set permissions +RUN mkdir ${VIRTUAL_ENV} \ +&& chmod ugo+rwx ${VIRTUAL_ENV} + # install runtime requirements RUN apt-get update -qq \ && DEBIAN_FRONTEND=noninteractive apt-get install -yq --no-install-recommends \ @@ -43,23 +56,26 @@ RUN DEBIAN_FRONTEND=noninteractive apt-get install -yq --no-install-recommends \ git # build mpi WORKDIR /tmp/mpich-build -ARG MPICH_VERSION="3.3.2" +ARG MPICH_VERSION="3.4.2" RUN wget http://www.mpich.org/static/downloads/${MPICH_VERSION}/mpich-${MPICH_VERSION}.tar.gz RUN tar xvzf mpich-${MPICH_VERSION}.tar.gz WORKDIR /tmp/mpich-build/mpich-${MPICH_VERSION} -ARG MPICH_CONFIGURE_OPTIONS="--prefix=/usr/local --enable-g=option=none --disable-debuginfo --enable-fast=O3,ndebug --without-timing --without-mpit-pvars" +ARG MPICH_CONFIGURE_OPTIONS="--prefix=/usr/local --enable-g=option=none --disable-debuginfo --enable-fast=O3,ndebug --without-timing --without-mpit-pvars --with-device=ch3" ARG MPICH_MAKE_OPTIONS="-j8" RUN ./configure ${MPICH_CONFIGURE_OPTIONS} RUN make ${MPICH_MAKE_OPTIONS} RUN make install RUN ldconfig + # create venv now for forthcoming python packages +USER $NB_USER RUN /usr/bin/python3 -m virtualenv --system-site-packages --python=/usr/bin/python3 ${VIRTUAL_ENV} - RUN pip3 install --no-cache-dir mpi4py + +USER root # build petsc WORKDIR /tmp/petsc-build -ARG PETSC_VERSION="3.14.0" +ARG PETSC_VERSION="3.15.1" RUN wget http://ftp.mcs.anl.gov/pub/petsc/release-snapshots/petsc-lite-${PETSC_VERSION}.tar.gz RUN tar zxf petsc-lite-${PETSC_VERSION}.tar.gz WORKDIR /tmp/petsc-build/petsc-${PETSC_VERSION} @@ -85,8 +101,7 @@ RUN make PETSC_DIR=/tmp/petsc-build/petsc-${PETSC_VERSION} PETSC_ARCH=arch-linux RUN rm -fr /usr/local/share/petsc # build petsc4py ENV PETSC_DIR=/usr/local -ENV PETSC_ARCH=arch-linux-c-opt -RUN pip3 install --no-cache-dir petsc4py +USER $NB_USER RUN CC=h5pcc HDF5_MPI="ON" HDF5_DIR=${PETSC_DIR} pip3 install --no-cache-dir --no-binary=h5py git+https://github.com/h5py/h5py@master FROM base_runtime AS minimal @@ -100,16 +115,6 @@ RUN apt-mark showmanual >/opt/installed.txt # Add user environment FROM minimal -EXPOSE 8888 -RUN ipcluster nbextension enable -ADD https://github.com/krallin/tini/releases/download/v0.18.0/tini /tini -RUN chmod +x /tini -ENV NB_USER jovyan -RUN useradd -m -s /bin/bash -N jovyan USER $NB_USER -ENV NB_WORK /home/$NB_USER -RUN ipython profile create --parallel --profile=mpi && \ - echo "c.IPClusterEngines.engine_launcher_class = 'MPIEngineSetLauncher'" >> $NB_WORK/.ipython/profile_mpi/ipcluster_config.py -VOLUME $NB_WORK/workspace WORKDIR $NB_WORK CMD ["jupyter", "notebook", "--no-browser", "--ip='0.0.0.0'"] \ No newline at end of file diff --git a/docs/development/docker/stampede2/Dockerfile b/docs/development/docker/stampede2/Dockerfile index 13adfc837..ee8ca0c02 100644 --- a/docs/development/docker/stampede2/Dockerfile +++ b/docs/development/docker/stampede2/Dockerfile @@ -1,5 +1,5 @@ FROM ubuntu:bionic -MAINTAINER https://github.com/underworldcode/ +LABEL maintainer="https://github.com/underworldcode/" RUN mkdir /home1 /work /scratch /gpfs /corral-repl /corral-tacc /data diff --git a/docs/development/docker/underworld2/Dockerfile b/docs/development/docker/underworld2/Dockerfile index 2e377ac96..89c1bff74 100644 --- a/docs/development/docker/underworld2/Dockerfile +++ b/docs/development/docker/underworld2/Dockerfile @@ -2,8 +2,8 @@ # Typically: # $ docker build -f docs/development/docker/underworld2/Dockerfile . -FROM underworldcode/base@sha256:f194ce40ea602305141a6a95fad640c90dd7ae5fae7532cd164dc566ec465edb as base_runtime -MAINTAINER https://github.com/underworldcode/ +FROM underworldcode/base@sha256:27198726bedbe747889d4db096d5e15bf0c7cec5c9dc07c483b1d02acdfff543 as base_runtime +LABEL maintainer="https://github.com/underworldcode/" # install runtime requirements USER root ENV PYVER=3.8 @@ -22,6 +22,7 @@ RUN DEBIAN_FRONTEND=noninteractive apt-get install -yq --no-install-recommends \ libxml2-dev RUN PYTHONPATH= /usr/bin/pip3 install --no-cache-dir setuptools scons # setup further virtualenv to avoid double copying back previous packages (h5py,mpi4py,etc) +USER $NB_USER RUN /usr/bin/python3 -m virtualenv --system-site-packages --python=/usr/bin/python3 ${VIRTUAL_ENV} WORKDIR /tmp COPY --chown=jovyan:users . /tmp/underworld2 @@ -47,7 +48,7 @@ COPY --chown=jovyan:users ./docs/test $NB_WORK/Underworld/test COPY --from=build_base --chown=jovyan:users /tmp/UWGeodynamics/docs/examples $NB_WORK/UWGeodynamics/examples COPY --from=build_base --chown=jovyan:users /tmp/UWGeodynamics/docs/tutorials $NB_WORK/UWGeodynamics/tutorials COPY --from=build_base --chown=jovyan:users /tmp/UWGeodynamics/docs/benchmarks $NB_WORK/UWGeodynamics/benchmarks -RUN chown jovyan:users /home/jovyan/workspace /home/jovyan/UWGeodynamics +RUN chown jovyan:users /home/jovyan/workspace /home/jovyan/UWGeodynamics RUN jupyter serverextension enable --sys-prefix jupyter_server_proxy USER $NB_USER WORKDIR $NB_WORK diff --git a/docs/development/docs_generator/conf.py b/docs/development/docs_generator/conf.py index 81727a256..0e18ef6b3 100644 --- a/docs/development/docs_generator/conf.py +++ b/docs/development/docs_generator/conf.py @@ -472,7 +472,7 @@ class Mock(MagicMock): @classmethod def __getattr__(cls, name): return MagicMock() - MOCK_MODULES = ['pint', 'numpy', '_StGermain', 'StGermain', '_StgDomain', '_PICellerator', '_StgFEM', '_Solvers', + MOCK_MODULES = ['pint', 'pint.errors', 'numpy', '_StGermain', 'StGermain', '_StgDomain', '_PICellerator', '_StgFEM', '_Solvers', '_Underworld', 'h5py', '_Function', '_gLucifer', '_c_arrays', '_c_pointers', '_StGermain_Tools', '_petsc', 'mpi4py', 'underworld.libUnderworld.libUnderworldPy.StGermain', 'underworld.libUnderworld.libUnderworldPy.StgDomain', 'underworld.libUnderworld.libUnderworldPy.StgFEM', 'underworld.libUnderworld.libUnderworldPy.PICellerator', 'underworld.libUnderworld.libUnderworldPy.Underworld', 'underworld.libUnderworld.libUnderworldPy.Solvers', 'underworld.libUnderworld.libUnderworldPy.gLucifer', 'underworld.libUnderworld.libUnderworldPy.c_arrays', 'underworld.libUnderworld.libUnderworldPy.c_pointers', diff --git a/docs/development/release_guidelines.md b/docs/development/release_guidelines.md index 07f624812..beb6990ae 100644 --- a/docs/development/release_guidelines.md +++ b/docs/development/release_guidelines.md @@ -23,7 +23,7 @@ Review issue tracker Documentation review ==================== * Review this document. -* Review `guidelines.md`. +* Review `development_guidelines.md`. * Review docstrings updates for deprecation warnings. * Check for other DEPRECATE flags in the code. * Check autocomplete to ensure no garbage has slipped in. Non @@ -43,7 +43,7 @@ Documentation review * Generate/update change log (`CHANGES.md`). * Review cheat sheet contents. * Increment version number within ``underworld/_version.py`` - (check `guidelines.md` for details on version numbering). + (check `development_guidelines.md` for details on version numbering). * Update `FROM` tag in top level (binder) Dockerfile. Testing diff --git a/docs/examples/04_StokesSinker.ipynb b/docs/examples/04_StokesSinker.ipynb index 5f608aedb..02b19d2f0 100644 --- a/docs/examples/04_StokesSinker.ipynb +++ b/docs/examples/04_StokesSinker.ipynb @@ -281,7 +281,7 @@ { "data": { "text/html": [ - "" + "" ], "text/plain": [ "" @@ -357,8 +357,7 @@ " voronoi_swarm = swarm, \n", " conditions = freeslipBC,\n", " fn_viscosity = viscosityMapFn, \n", - " fn_bodyforce = buoyancyFn )\n", - "solver = uw.systems.Solver( stokes )" + " fn_bodyforce = buoyancyFn )" ] }, { @@ -367,10 +366,17 @@ "metadata": {}, "outputs": [], "source": [ - "solver.set_inner_method(\"mumps\")\n", - "inner_rtol = 1e-7\n", - "solver.set_inner_rtol(inner_rtol)\n", - "solver.set_outer_rtol(10*inner_rtol)" + "try:\n", + " solver = uw.systems.Solver( stokes )\n", + " solver.set_inner_method(\"mumps\")\n", + " inner_rtol = 1e-7\n", + " solver.set_inner_rtol(inner_rtol)\n", + " solver.set_outer_rtol(10*inner_rtol)\n", + " solver.solve()\n", + "except RuntimeError:\n", + " # If the above failed, most likely \"mumps\" isn't \n", + " # installed. Fallback to default solver. \n", + " solver = uw.systems.Solver( stokes )" ] }, { @@ -450,25 +456,25 @@ "name": "stdout", "output_type": "stream", "text": [ - "Zeroing pressure using mean upper surface pressure -0.5963865446963474\n", + "Zeroing pressure using mean upper surface pressure -0.5963863976830965\n", "step = 0; time = 0.000e+00; v_rms = 9.022e-03; height = 6.000e-01\n", - "Zeroing pressure using mean upper surface pressure -0.5950600430107463\n", + "Zeroing pressure using mean upper surface pressure -0.5950600562722963\n", "step = 1; time = 2.996e-01; v_rms = 9.164e-03; height = 5.925e-01\n", - "Zeroing pressure using mean upper surface pressure -0.5938679921131681\n", + "Zeroing pressure using mean upper surface pressure -0.5938677131209827\n", "step = 2; time = 5.948e-01; v_rms = 9.316e-03; height = 5.849e-01\n", - "Zeroing pressure using mean upper surface pressure -0.5926535037924707\n", + "Zeroing pressure using mean upper surface pressure -0.5926534525201359\n", "step = 3; time = 8.859e-01; v_rms = 9.454e-03; height = 5.774e-01\n", - "Zeroing pressure using mean upper surface pressure -0.5912588507700052\n", + "Zeroing pressure using mean upper surface pressure -0.5912657573592722\n", "step = 4; time = 1.173e+00; v_rms = 9.571e-03; height = 5.698e-01\n", - "Zeroing pressure using mean upper surface pressure -0.5900388239149108\n", + "Zeroing pressure using mean upper surface pressure -0.5900389835377373\n", "step = 5; time = 1.457e+00; v_rms = 9.696e-03; height = 5.623e-01\n", - "Zeroing pressure using mean upper surface pressure -0.5886540412905718\n", + "Zeroing pressure using mean upper surface pressure -0.5886541507651466\n", "step = 6; time = 1.738e+00; v_rms = 9.797e-03; height = 5.548e-01\n", - "Zeroing pressure using mean upper surface pressure -0.5874559408488842\n", + "Zeroing pressure using mean upper surface pressure -0.5874560154524617\n", "step = 7; time = 2.017e+00; v_rms = 9.921e-03; height = 5.472e-01\n", - "Zeroing pressure using mean upper surface pressure -0.586002447443974\n", + "Zeroing pressure using mean upper surface pressure -0.5860024955119623\n", "step = 8; time = 2.292e+00; v_rms = 1.001e-02; height = 5.397e-01\n", - "Zeroing pressure using mean upper surface pressure -0.5849503118787854\n", + "Zeroing pressure using mean upper surface pressure -0.5849501369904566\n", "step = 9; time = 2.566e+00; v_rms = 1.013e-02; height = 5.321e-01\n" ] } @@ -532,7 +538,7 @@ "cell_type": "code", "execution_count": 19, "metadata": { - "scrolled": true + "scrolled": false }, "outputs": [ { @@ -545,7 +551,7 @@ }, { "data": { - "image/png": "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\n", + "image/png": "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\n", "text/plain": [ "
" ] @@ -586,7 +592,7 @@ { "data": { "text/html": [ - "" + "" ], "text/plain": [ "" @@ -617,7 +623,7 @@ { "data": { "text/html": [ - "" + "" ], "text/plain": [ "" @@ -629,7 +635,7 @@ { "data": { "text/html": [ - "" + "" ], "text/plain": [ "" @@ -670,7 +676,7 @@ "name": "python", "nbconvert_exporter": "python", "pygments_lexer": "ipython3", - "version": "3.7.3" + "version": "3.8.10" } }, "nbformat": 4, diff --git a/docs/examples/06_SlabSubduction.ipynb b/docs/examples/06_SlabSubduction.ipynb index 0b0a038bd..8ee0191d2 100644 --- a/docs/examples/06_SlabSubduction.ipynb +++ b/docs/examples/06_SlabSubduction.ipynb @@ -170,7 +170,7 @@ { "data": { "text/html": [ - "" + "" ], "text/plain": [ "" @@ -319,9 +319,7 @@ " pressureField = pressureField,\n", " conditions = periodicBC,\n", " fn_viscosity = viscosityMapFn, \n", - " fn_bodyforce = buoyancyFn )\n", - "# Create solver & solve\n", - "solver = uw.systems.Solver(stokes)" + " fn_bodyforce = buoyancyFn )" ] }, { @@ -330,8 +328,16 @@ "metadata": {}, "outputs": [], "source": [ - "# use \"mumps\" direct solve, best for 2D models.\n", - "solver.set_inner_method(\"mumps\")" + "# Create solver & solve\n", + "try:\n", + " # Try using \"mumps\" direct solve, best for 2D models.\n", + " solver = uw.systems.Solver(stokes)\n", + " solver.set_inner_method(\"mumps\")\n", + " solver.solve()\n", + "except RuntimeError:\n", + " # If the above failed, most likely \"mumps\" isn't \n", + " # installed. Fallback to default solver. \n", + " solver = uw.systems.Solver( stokes )" ] }, { @@ -526,7 +532,7 @@ { "data": { "text/html": [ - "" + "" ], "text/plain": [ "" @@ -538,7 +544,7 @@ { "data": { "text/html": [ - "" + "" ], "text/plain": [ "" @@ -550,7 +556,7 @@ { "data": { "text/html": [ - "" + "" ], "text/plain": [ "" @@ -562,7 +568,7 @@ { "data": { "text/html": [ - "" + "" ], "text/plain": [ "" @@ -574,7 +580,7 @@ { "data": { "text/html": [ - "" + "" ], "text/plain": [ "" @@ -611,7 +617,7 @@ "name": "python", "nbconvert_exporter": "python", "pygments_lexer": "ipython3", - "version": "3.7.3" + "version": "3.8.10" } }, "nbformat": 4, diff --git a/docs/examples/07_ShearBandsPureShear.ipynb b/docs/examples/07_ShearBandsPureShear.ipynb index b2befb3c6..cd210ba94 100644 --- a/docs/examples/07_ShearBandsPureShear.ipynb +++ b/docs/examples/07_ShearBandsPureShear.ipynb @@ -175,8 +175,10 @@ " swarmLayout = uw.swarm.layouts.PerCellSpaceFillerLayout( swarm=swarm, particlesPerCell=parts_per_cell )\n", " swarm.populate_using_layout( layout=swarmLayout )\n", "else:\n", + " import os\n", + " dirname = os.path.dirname(__file__)\n", " # load in parallel for deterministic results\n", - " swarm.load(\"input/07_ShearBandsPureShear/particle_coords.h5\")\n", + " swarm.load(dirname+\"/input/07_ShearBandsPureShear/particle_coords.h5\")\n", "# create pop control object\n", "pop_control = uw.swarm.PopulationControl(swarm, aggressive=True, aggressiveThreshold=0.8, particlesPerCell=parts_per_cell)\n", "\n", @@ -277,8 +279,10 @@ " plasticStrain.data[:,0] *= gaussian(swarm.particleCoordinates.data[:,1], 0.0, 0.025) \n", " plasticStrain.data[:,0] *= boundary(swarm.particleCoordinates.data[:,0], minX, maxX, 10.0, 2) \n", "else:\n", + " import os\n", + " dirname = os.path.dirname(__file__)\n", " # load in parallel for deterministic results\n", - " plasticStrain.load(\"input/07_ShearBandsPureShear/plastic_strain.h5\")\n", + " plasticStrain.load(dirname+\"/input/07_ShearBandsPureShear/plastic_strain.h5\")\n", "\n", "deformationVariable.data[:,0] = deformationSwarm.particleCoordinates.data[:,1]" ] @@ -1053,7 +1057,7 @@ "name": "python", "nbconvert_exporter": "python", "pygments_lexer": "ipython3", - "version": "3.5.3" + "version": "3.8.10" } }, "nbformat": 4, diff --git a/docs/install_guides/pawsey_magnus_container.md b/docs/install_guides/pawsey_magnus_container.md index 23596382b..a29aa1151 100644 --- a/docs/install_guides/pawsey_magnus_container.md +++ b/docs/install_guides/pawsey_magnus_container.md @@ -19,17 +19,14 @@ user@magnus-1:~$ module load singularity 2. Pull down the required image. Note that images are generally around 1GB, so you might consider storing them somewhere in `/group` or `/scratch`. ```shell -user@magnus-1:~$ singularity pull /WHERE/TO/STORE/YOUR/IMAGE/IMAGENAME.sif docker://underworldcode/underworld2 +user@magnus-1:~$ singularity pull /WHERE/TO/STORE/YOUR/IMAGE/IMAGENAME.sif docker://underworldcode/underworld2:latest ``` 3. Use the image. Note that this will normally be a command line in your queue submission script. Don't forget to also load the Singularity module. Note also the setting of the environment variable `UW_VIS_PORT` which is required for correct vis operation. - Note also that we currently need to set the `SINGULARITYENV_LD_LIBRARY_PATH` - variable which prevents unwanted host libraries being loaded at runtime. ```shell -user@magnus-1:~$ export SINGULARITYENV_LD_LIBRARY_PATH=/opt/cray/pe/mpt/7.7.0/gni/mpich-gnu-abi/4.9/lib:/opt/cray/xpmem/2.2.15-6.0.7.1_5.16__g7549d06.ari/lib64:/opt/cray/ugni/6.0.14.0-6.0.7.1_3.18__gea11d3d.ari/lib64:/opt/cray/udreg/2.3.2-6.0.7.1_5.18__g5196236.ari/lib64:/opt/cray/pe/pmi/5.0.13/lib64:/opt/cray/alps/6.6.43-6.0.7.1_5.54__ga796da32.ari/lib64:/opt/cray/wlm_detect/1.3.3-6.0.7.1_5.8__g7109084.ari/lib64:/usr/lib64 user@magnus-1:~$ export UW_VIS_PORT=0 user@magnus-1:~$ srun singularity exec /WHERE/TO/STORE/YOUR/IMAGE/IMAGENAME.sif python3 YOURSCRIPT.py ``` @@ -48,7 +45,6 @@ user@magnus-1:~$ module load singularity 3. Launch the Singularity shell. Note the `--pty` required for pseudo terminal mode. ```shell -user@magnus-1:~$ export SINGULARITYENV_LD_LIBRARY_PATH=/opt/cray/pe/mpt/7.7.0/gni/mpich-gnu-abi/4.9/lib:/opt/cray/xpmem/2.2.15-6.0.7.1_5.16__g7549d06.ari/lib64:/opt/cray/ugni/6.0.14.0-6.0.7.1_3.18__gea11d3d.ari/lib64:/opt/cray/udreg/2.3.2-6.0.7.1_5.18__g5196236.ari/lib64:/opt/cray/pe/pmi/5.0.13/lib64:/opt/cray/alps/6.6.43-6.0.7.1_5.54__ga796da32.ari/lib64:/opt/cray/wlm_detect/1.3.3-6.0.7.1_5.8__g7109084.ari/lib64:/usr/lib64 user@magnus-1:~$ export UW_VIS_PORT=0 user@magnus-1:~$ srun --pty singularity shell /WHERE/TO/STORE/YOUR/IMAGE/IMAGENAME.sif ``` diff --git a/docs/test/14_Convection_EBA.ipynb b/docs/test/14_Convection_EBA.ipynb new file mode 100644 index 000000000..9b580935e --- /dev/null +++ b/docs/test/14_Convection_EBA.ipynb @@ -0,0 +1,639 @@ +{ + "cells": [ + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "King / Blankenbach Benchmark Case 1\n", + "======\n", + "\n", + "Isoviscous thermal convection using EBA formulation.\n", + "----\n", + "\n", + "Two-dimensional, incompressible, bottom heated, steady isoviscous thermal convection in a 1 x 1 box, see case 1 of King *et al.* 2009 / Blankenbach *et al.* 1989 for details.\n", + "\n", + "\n", + "**This example introduces:**\n", + "1. Extended Boussinesq Approximation, EBA, formulation for Stokes Flow.\n", + "\n", + "**Keywords:** Stokes system, EBA, advective diffusive systems, analysis tools\n", + "\n", + "**References**\n", + "\n", + "Scott D. King, Changyeol Lee, Peter E. Van Keken, Wei Leng, Shijie Zhong, Eh Tan, Nicola Tosi, Masanori C. Kameyama, A community benchmark for 2-D Cartesian compressible convection in the Earth's mantle, Geophysical Journal International, Volume 180, Issue 1, January 2010, Pages 73–87, https://doi.org/10.1111/j.1365-246X.2009.04413.x\n", + "\n", + "B. Blankenbach, F. Busse, U. Christensen, L. Cserepes, D. Gunkel, U. Hansen, H. Harder, G. Jarvis, M. Koch, G. Marquart, D. Moore, P. Olson, H. Schmeling and T. Schnaubelt. A benchmark comparison for mantle convection codes. Geophysical Journal International, 98, 1, 23–38, 1989\n", + "http://onlinelibrary.wiley.com/doi/10.1111/j.1365-246X.1989.tb05511.x/abstract\n" + ] + }, + { + "cell_type": "code", + "execution_count": 1, + "metadata": {}, + "outputs": [], + "source": [ + "import underworld as uw\n", + "from underworld import function as fn\n", + "import underworld.visualisation as vis\n", + "import math\n", + "import numpy as np" + ] + }, + { + "cell_type": "code", + "execution_count": 2, + "metadata": {}, + "outputs": [], + "source": [ + "from underworld.scaling import units as u" + ] + }, + { + "cell_type": "code", + "execution_count": 3, + "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "10000.000000000002 dimensionless\n" + ] + } + ], + "source": [ + "# The physical S.I. units from the Blankenbach paper\n", + "# Sanity check for the Rayleigh number.\n", + "# In this implementation the equations are non-dimensionalised with Ra\n", + "\n", + "# Ra = a*g*dT*h**3 / (eta0*dif)\n", + "h = 1e6 * u.m\n", + "dT = 1e3 * u.degK\n", + "a = 2.5e-5 * u.degK**-1\n", + "g = 10 * u.m * u.s**-2\n", + "diff = 1e-6 * u.m**2 * u.s**-1\n", + "eta = 1e23 * u.kg * u.s**-1 * u.m**-1\n", + "rho = 4000 * u.kg * u.m**-3 # reference density, only for units\n", + "\n", + "Ra = (a*g*dT*h**3)/(eta/rho*diff)\n", + "print(Ra.to_compact())" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "Setup parameters\n", + "-----" + ] + }, + { + "cell_type": "code", + "execution_count": 4, + "metadata": {}, + "outputs": [], + "source": [ + "boxHeight = 1.0\n", + "boxLength = 1.0\n", + "# Set grid resolution.\n", + "res = 64\n", + "# Set max & min temperautres\n", + "tempMin = 0.0\n", + "tempMax = 1.0" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "Choose which Rayleigh number, see case 1 of Blankenbach *et al.* 1989 for details." + ] + }, + { + "cell_type": "code", + "execution_count": 5, + "metadata": {}, + "outputs": [], + "source": [ + "Di = 0.5\n", + "Ra = 1.e4\n", + "eta0 = 1.e23" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "Set input and output file directory " + ] + }, + { + "cell_type": "code", + "execution_count": 6, + "metadata": {}, + "outputs": [], + "source": [ + "outputPath = 'EBA/'\n", + "# Make output directory if necessary.\n", + "if uw.mpi.rank==0:\n", + " import os\n", + " if not os.path.exists(outputPath):\n", + " os.makedirs(outputPath)" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "Create mesh and variables\n", + "------" + ] + }, + { + "cell_type": "code", + "execution_count": 7, + "metadata": {}, + "outputs": [], + "source": [ + "mesh = uw.mesh.FeMesh_Cartesian( elementType = (\"Q1/dQ0\"), \n", + " elementRes = (res, res), \n", + " minCoord = (0., 0.), \n", + " maxCoord = (boxLength, boxHeight))\n", + "\n", + "velocityField = mesh.add_variable( nodeDofCount=2 )\n", + "pressureField = mesh.subMesh.add_variable( nodeDofCount=1 )\n", + "temperatureField = mesh.add_variable( nodeDofCount=1 )\n", + "temperatureDotField = mesh.add_variable( nodeDofCount=1 )\n", + "\n", + "# initialise velocity, pressure and temperatureDot field\n", + "velocityField.data[:] = [0.,0.]\n", + "pressureField.data[:] = 0.\n", + "temperatureField.data[:] = 0.\n", + "temperatureDotField.data[:] = 0." + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "Set up material parameters and functions\n", + "-----\n", + "\n", + "Set values and functions for viscosity, density and buoyancy force." + ] + }, + { + "cell_type": "code", + "execution_count": 8, + "metadata": {}, + "outputs": [], + "source": [ + "# Set a constant viscosity.\n", + "viscosity = 1.\n", + "\n", + "# Create our density function.\n", + "densityFn = Ra * temperatureField\n", + "\n", + "# Define our vertical unit vector using a python tuple (this will be automatically converted to a function).\n", + "z_hat = ( 0.0, 1.0 )\n", + "\n", + "# A buoyancy function.\n", + "buoyancyFn = densityFn * z_hat" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "Set initial temperature field\n", + "-----\n" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "**Use a sinusodial perturbation**" + ] + }, + { + "cell_type": "code", + "execution_count": 9, + "metadata": {}, + "outputs": [], + "source": [ + "temperatureField.data[:] = 0.\n", + "pertStrength = 0.1\n", + "deltaTemp = tempMax - tempMin\n", + "for index, coord in enumerate(mesh.data):\n", + " pertCoeff = math.cos( math.pi * coord[0]/boxLength ) * math.sin( math.pi * coord[1]/boxLength )\n", + " temperatureField.data[index] = tempMin + deltaTemp*(boxHeight - coord[1]) + pertStrength * pertCoeff\n", + " temperatureField.data[index] = max(tempMin, min(tempMax, temperatureField.data[index]))" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "**Show initial temperature field**\n" + ] + }, + { + "cell_type": "code", + "execution_count": 10, + "metadata": { + "scrolled": false + }, + "outputs": [], + "source": [ + "# fig = vis.Figure()\n", + "# fig.append( vis.objects.Surface(mesh, temperatureField) )\n", + "# fig.show()" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "Create boundary conditions\n", + "----------\n", + "\n", + "Set temperature boundary conditions on the bottom ( ``MinJ`` ) and top ( ``MaxJ`` )." + ] + }, + { + "cell_type": "code", + "execution_count": 11, + "metadata": {}, + "outputs": [], + "source": [ + "for index in mesh.specialSets[\"MinJ_VertexSet\"]:\n", + " temperatureField.data[index] = tempMax\n", + "for index in mesh.specialSets[\"MaxJ_VertexSet\"]:\n", + " temperatureField.data[index] = tempMin" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "Construct sets for the both horizontal and vertical walls. Combine the sets of vertices to make the ``I`` (left and right side walls) and ``J`` (top and bottom walls) sets." + ] + }, + { + "cell_type": "code", + "execution_count": 12, + "metadata": {}, + "outputs": [], + "source": [ + "iWalls = mesh.specialSets[\"MinI_VertexSet\"] + mesh.specialSets[\"MaxI_VertexSet\"]\n", + "jWalls = mesh.specialSets[\"MinJ_VertexSet\"] + mesh.specialSets[\"MaxJ_VertexSet\"]\n", + "\n", + "freeslipBC = uw.conditions.DirichletCondition( variable = velocityField, \n", + " indexSetsPerDof = (iWalls, jWalls) )\n", + "tempBC = uw.conditions.DirichletCondition( variable = temperatureField, \n", + " indexSetsPerDof = (jWalls,) )" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "System setup\n", + "-----\n", + "\n", + "**Setup a Stokes system**\n" + ] + }, + { + "cell_type": "code", + "execution_count": 13, + "metadata": {}, + "outputs": [], + "source": [ + "stokes = uw.systems.Stokes( velocityField = velocityField, \n", + " pressureField = pressureField,\n", + " conditions = [freeslipBC,],\n", + " fn_viscosity = viscosity, \n", + " fn_bodyforce = buoyancyFn )\n", + "# get the default stokes equation solver\n", + "solver = uw.systems.Solver( stokes )" + ] + }, + { + "cell_type": "code", + "execution_count": 14, + "metadata": {}, + "outputs": [], + "source": [ + "# a function for the 2nd invariant strain rate tensor\n", + "fn_sr2Inv = fn.tensor.second_invariant(fn.tensor.symmetric( velocityField.fn_gradient ))\n", + "\n", + "# a function for viscous dissipation, i.e.\n", + "# the contraction of dev. stress tensor with strain rate tensor.\n", + "vd = 2 * viscosity * 2 * fn_sr2Inv**2\n", + "\n", + "# function for adiabatic heating\n", + "adiabatic_heating = Di * velocityField[1]*(temperatureField)\n", + "\n", + "# combine viscous dissipation and adiabatic heating\n", + "# terms to the energy equation, via the argument 'fn_source'\n", + "fn_source = Di/Ra * vd - adiabatic_heating\n", + "\n", + "### As discussed by King et al. (JI09) the volume integral of the viscous dissipation and \n", + "### the adiabatic heating should balance.\n", + "\n", + "int_vd = uw.utils.Integral([Di/Ra*vd,adiabatic_heating], mesh)" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "**Create an advection diffusion system**\n" + ] + }, + { + "cell_type": "code", + "execution_count": 15, + "metadata": {}, + "outputs": [], + "source": [ + "advDiff = uw.systems.AdvectionDiffusion( phiField = temperatureField, \n", + " phiDotField = temperatureDotField, \n", + " velocityField = velocityField, \n", + " fn_diffusivity = 1.0,\n", + " fn_sourceTerm = fn_source,\n", + " conditions = [tempBC,] )" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "Analysis tools\n", + "-----" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "collapsed": true + }, + "source": [ + "**Nusselt number**\n", + "\n", + "The Nusselt number is the ratio between convective and conductive heat transfer\n", + "\n", + "\\\\[\n", + "Nu = -h \\frac{ \\int_0^l \\partial_z T (x, z=h) dx}{ \\int_0^l T (x, z=0) dx}\n", + "\\\\]\n", + "\n", + "\n", + "\n" + ] + }, + { + "cell_type": "code", + "execution_count": 16, + "metadata": {}, + "outputs": [], + "source": [ + "nuTop = uw.utils.Integral( fn=temperatureField.fn_gradient[1], \n", + " mesh=mesh, integrationType='Surface', \n", + " surfaceIndexSet=mesh.specialSets[\"MaxJ_VertexSet\"])\n", + "\n", + "nuBottom = uw.utils.Integral( fn=temperatureField, \n", + " mesh=mesh, integrationType='Surface', \n", + " surfaceIndexSet=mesh.specialSets[\"MinJ_VertexSet\"])" + ] + }, + { + "cell_type": "code", + "execution_count": 17, + "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "Nusselt number = 1.000000\n" + ] + } + ], + "source": [ + "nu = - nuTop.evaluate()[0]/nuBottom.evaluate()[0]\n", + "if uw.mpi.rank == 0 : print('Nusselt number = {0:.6f}'.format(nu))" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "**RMS velocity**\n", + "\n", + "The root mean squared velocity is defined by intergrating over the entire simulation domain via\n", + "\n", + "\\\\[\n", + "\\begin{aligned}\n", + "v_{rms} = \\sqrt{ \\frac{ \\int_V (\\mathbf{v}.\\mathbf{v}) dV } {\\int_V dV} }\n", + "\\end{aligned}\n", + "\\\\]\n", + "\n", + "where $V$ denotes the volume of the box." + ] + }, + { + "cell_type": "code", + "execution_count": 18, + "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "Initial vrms = 0.000\n" + ] + } + ], + "source": [ + "vrms = stokes.velocity_rms()\n", + "if uw.mpi.rank == 0 : print('Initial vrms = {0:.3f}'.format(vrms))" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "Main simulation loop\n", + "-----" + ] + }, + { + "cell_type": "code", + "execution_count": 19, + "metadata": {}, + "outputs": [], + "source": [ + "#initialise time, step, output arrays\n", + "time = 0.\n", + "step = 0\n", + "timeVal = []\n", + "vrmsVal = []\n", + "step_end = 30\n", + "\n", + "# output frequency\n", + "step_output = max(1,min(100, step_end/10)) # reasonable automatic choice\n", + "epsilon = 1.e-8\n", + "\n", + "velplotmax = 0.0\n", + "nuLast = -1.0\n", + "rerr = 1." + ] + }, + { + "cell_type": "code", + "execution_count": 20, + "metadata": {}, + "outputs": [], + "source": [ + "# define an update function\n", + "def update():\n", + " # Determining the maximum timestep for advancing the a-d system.\n", + " dt = advDiff.get_max_dt()\n", + " # Advect using this timestep size. \n", + " advDiff.integrate(dt)\n", + " return time+dt, step+1" + ] + }, + { + "cell_type": "code", + "execution_count": 21, + "metadata": {}, + "outputs": [], + "source": [ + "v_old = velocityField.copy()" + ] + }, + { + "cell_type": "code", + "execution_count": 22, + "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "steps = 0; time = 0.000e+00; v_rms = 17.899; Nu = 1.000; Rel change = 2.000e+00 vChange = 1.000e+00\n", + "steps = 10; time = 1.221e-03; v_rms = 21.672; Nu = 1.059; Rel change = 6.079e-03 vChange = 2.010e-02\n", + "steps = 20; time = 2.441e-03; v_rms = 26.347; Nu = 1.137; Rel change = 8.114e-03 vChange = 1.939e-02\n", + "steps = 30; time = 3.662e-03; v_rms = 31.759; Nu = 1.252; Rel change = 1.083e-02 vChange = 1.838e-02\n", + "velocity relative tolerance is: 0.018\n" + ] + } + ], + "source": [ + "tol = 1e-8\n", + "# Perform steps.\n", + "while step<=step_end and rerr > tol:\n", + " \n", + " # copy to previous\n", + " v_old.data[:] = velocityField.data[:]\n", + " \n", + " # Solving the Stokes system.\n", + " solver.solve()\n", + " \n", + " aerr = uw.utils._nps_2norm(v_old.data-velocityField.data)\n", + " magV = uw.utils._nps_2norm(v_old.data)\n", + " rerr = ( aerr/magV if magV>1e-8 else 1) # calculate relative variation\n", + "\n", + " # Calculate & store the RMS velocity and Nusselt number.\n", + " vrms = stokes.velocity_rms()\n", + " nu = - nuTop.evaluate()[0]/nuBottom.evaluate()[0]\n", + " vrmsVal.append(vrms)\n", + " timeVal.append(time)\n", + " velplotmax = max(vrms, velplotmax)\n", + "\n", + " # print output statistics \n", + " if step%(step_end/step_output) == 0:\n", + "\n", + "# mH = mesh.save(outputPath+\"mesh-{}.h5\".format(step))\n", + "# tH = temperatureField.save(outputPath+\"t-{}.h5\".format(step), mH)\n", + "# vH = velocityField.save(outputPath+\"v-{}.h5\".format(step), mH)\n", + "# velocityField.xdmf(outputPath+\"v-{}.xdmf\".format(step), vH, \"velocity\", mH, \"mesh\")\n", + "# temperatureField.xdmf(outputPath+\"t-{}.xdmf\".format(step), tH, \"temperature\", mH, \"mesh\" )\n", + "\n", + " if(uw.mpi.rank==0):\n", + " print('steps = {0:6d}; time = {1:.3e}; v_rms = {2:.3f}; Nu = {3:.3f}; Rel change = {4:.3e} vChange = {5:.3e}'\n", + " .format(step, time, vrms, nu, abs((nu - nuLast)/nu), rerr))\n", + " \n", + "# # Check loop break conditions.\n", + "# if(abs((nu - nuLast)/nu) < epsilon):\n", + "# if(uw.mpi.rank==0):\n", + "# print('steps = {0:6d}; time = {1:.3e}; v_rms = {2:.3f}; Nu = {3:.3f}; Rel change = {4:.3e}'\n", + "# .format(step, time, vrms, nu, abs((nu - nuLast)/nu)))\n", + "# break\n", + " nuLast = nu\n", + " \n", + " # update\n", + " time, step = update()\n", + " \n", + "print(\"velocity relative tolerance is: {:.3f}\".format(rerr))" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "Post analysis\n", + "-----\n", + "\n", + "**Benchmark values**\n", + "\n", + "We can check the volume integral of viscous dissipation and adibatic heating are equal\n" + ] + }, + { + "cell_type": "code", + "execution_count": 23, + "metadata": {}, + "outputs": [], + "source": [ + "vd, ad = int_vd.evaluate()" + ] + }, + { + "cell_type": "code", + "execution_count": 24, + "metadata": {}, + "outputs": [], + "source": [ + "# error if >2% difference in vd and ad\n", + "if not np.isclose(vd,ad, rtol=2e-2):\n", + " if uw.mpi.rank == 0: print('vd = {0:.3e}, ad = {1:.3e}'.format(vd,ad))\n", + " raise RuntimeError(\"The volume integral of viscous dissipation and adiabatic heating should be approximately equal\")" + ] + } + ], + "metadata": { + "kernelspec": { + "display_name": "Python 3", + "language": "python", + "name": "python3" + }, + "language_info": { + "codemirror_mode": { + "name": "ipython", + "version": 3 + }, + "file_extension": ".py", + "mimetype": "text/x-python", + "name": "python", + "nbconvert_exporter": "python", + "pygments_lexer": "ipython3", + "version": "3.8.10" + } + }, + "nbformat": 4, + "nbformat_minor": 1 +} diff --git a/docs/test/solver_non_existent.py b/docs/test/solver_non_existent.py new file mode 100644 index 000000000..fa6171220 --- /dev/null +++ b/docs/test/solver_non_existent.py @@ -0,0 +1,82 @@ +''' +This test simply configures the solver class for an underlying +solver type that does not exist, and ensures that the appropriate +error message is flagged to the user. + +This simulates the behaviour that a user might encounter if they +attmpted to use `mumps` (for example) on a system where `mumps` +isn't installed. +''' + +expectedmsg1 = "Error encountered. Full restart recommended as exception safety not guaranteed. Error message:\n" \ + "An error was encountered during the PETSc solve. You should refer to the PETSc\n" \ + "error message for details. Note that if you are running within Jupyter, this error\n" \ + "message will only be visible in the console window." +expectedmsg2 = "Error encountered. Full restart recommended as exception safety not guaranteed. Error message:\n" \ + "An error was encountered during the PETSc solver setup. You should refer to the PETSc\n" \ + "error message for details. Note that if you are running within Jupyter, this error\n" \ + "message will only be visible in the console window." + +import underworld as uw +mesh = uw.mesh.FeMesh_Cartesian( elementType = ("Q1/dQ0") ) + + +# Heat solve test + +temperatureField = mesh.add_variable( nodeDofCount=1 ) +temperatureField.data[:] = 0. +botWalls = mesh.specialSets["Bottom_VertexSet"] +topWalls = mesh.specialSets[ "Top_VertexSet"] +bcWalls = botWalls + topWalls +tempBC = uw.conditions.DirichletCondition( variable=temperatureField, indexSetsPerDof=(bcWalls,) ) +temperatureField.data[botWalls] = 1.0 +temperatureField.data[topWalls] = 0.0 +heatequation = uw.systems.SteadyStateHeat(temperatureField = temperatureField, + fn_diffusivity = 1.0, + conditions = tempBC) +heatsolver = uw.systems.Solver(heatequation) +heatsolver.options.EnergySolver.pc_factor_mat_solver_type="NON_EXISTENT_SOLVER" +flagged_error=True +try: + heatsolver.solve() + flagged_error=False +except RuntimeError as e: + if str(e)!=expectedmsg1: + raise RuntimeError(f"Wrong error message encountered.\n\nExpected:\n\n{expectedmsg1}\n\nEncountered:\n\n{e}\n\n") + +if not flagged_error: + raise RuntimeError("Heat solver didn't flag error for unknown solver.") + + +# Stokes solve test + +velocityField = mesh.add_variable( nodeDofCount=2 ) +pressureField = mesh.subMesh.add_variable( nodeDofCount=1 ) +velocityField.data[:] = [0.,0.] +pressureField.data[:] = 0. +iWalls = mesh.specialSets["MinI_VertexSet"] + mesh.specialSets["MaxI_VertexSet"] +jWalls = mesh.specialSets["MinJ_VertexSet"] + mesh.specialSets["MaxJ_VertexSet"] +freeslipBC = uw.conditions.DirichletCondition( variable = velocityField, + indexSetsPerDof = (iWalls, jWalls) ) +stokes = uw.systems.Stokes( velocityField = velocityField, + pressureField = pressureField, + conditions = freeslipBC, + fn_viscosity = 1., + fn_bodyforce = (0.,-1.) ) +stokessolver = uw.systems.Solver( stokes ) +stokessolver.options.A11.__dict__.clear() +stokessolver.options.A11.ksp_type="preonly" +stokessolver.options.A11.pc_type="lu" +stokessolver.options.A11._mg_active=False +stokessolver.options.A11.pc_factor_mat_solver_type="NON_EXISTENT_SOLVER" + +flagged_error=True +try: + stokessolver.solve() + flagged_error=False +except RuntimeError as e: + if str(e)!=expectedmsg2: + raise RuntimeError(f"Wrong error message encountered.\n\nExpected:\n\n{expectedmsg2}\n\nEncountered:\n\n{e}\n\n") + +if not flagged_error: + raise RuntimeError("Heat solver didn't flag error for unknown solver.") diff --git a/docs/user_guide/06_Utilities.ipynb b/docs/user_guide/06_Utilities.ipynb index eaeb9d4ff..5c481d55b 100644 --- a/docs/user_guide/06_Utilities.ipynb +++ b/docs/user_guide/06_Utilities.ipynb @@ -127,7 +127,7 @@ "volume = mesh.integrate( 1.0 )\n", "\n", "# finally, calculate RMS\n", - "v_rms = math.sqrt( v2sum[0] )/volume[0]\n", + "v_rms = math.sqrt( v2sum[0] /volume[0] )\n", "print('RMS velocity = {0:.3f}'.format(v_rms))\n", "\n", "# option 3\n", @@ -140,7 +140,7 @@ "volume = volume_integral.evaluate()\n", "\n", "# finally, calculate RMS\n", - "v_rms = math.sqrt( v2sum[0] )/volume[0]\n", + "v_rms = math.sqrt( v2sum[0] /volume[0] )\n", "print('RMS velocity = {0:.3f}'.format(v_rms))" ] }, @@ -469,7 +469,7 @@ "name": "python", "nbconvert_exporter": "python", "pygments_lexer": "ipython3", - "version": "3.5.3" + "version": "3.8.5" } }, "nbformat": 4, diff --git a/docs/user_guide/08_StokesSolver.ipynb b/docs/user_guide/08_StokesSolver.ipynb index 2aa0df1a6..2d5326eb9 100644 --- a/docs/user_guide/08_StokesSolver.ipynb +++ b/docs/user_guide/08_StokesSolver.ipynb @@ -247,11 +247,15 @@ } ], "source": [ - "solver.set_inner_method(\"mumps\")\n", - "solver.options.scr.ksp_type=\"cg\"\n", - "solver.set_penalty(1.0e7)\n", - "solver.solve()\n", - "solver.print_stats()" + "try:\n", + " solver.set_inner_method(\"mumps\")\n", + " solver.options.scr.ksp_type=\"cg\"\n", + " solver.set_penalty(1.0e7)\n", + " solver.solve()\n", + " solver.print_stats()\n", + "except RuntimeError:\n", + " # If the above fails, \"mumps\" probably isn't installed\n", + " pass" ] }, { @@ -317,7 +321,7 @@ "name": "python", "nbconvert_exporter": "python", "pygments_lexer": "ipython3", - "version": "3.8.5" + "version": "3.8.10" } }, "nbformat": 4, diff --git a/underworld/__init__.py b/underworld/__init__.py index 3068db79e..c8c7c76a3 100644 --- a/underworld/__init__.py +++ b/underworld/__init__.py @@ -159,6 +159,7 @@ def _set_init_sig_as_sig(mod): _set_init_sig_as_sig(underworld.utils) timing._add_timing_to_mod(underworld.utils) +import underworld.scaling # to allow our legacy doctest formats try: diff --git a/underworld/_version.py b/underworld/_version.py index ee4b26580..f7d5795e7 100644 --- a/underworld/_version.py +++ b/underworld/_version.py @@ -1 +1 @@ -__version__ = "2.11.0-dev" +__version__ = "2.11.0b" diff --git a/underworld/libUnderworld/Solvers/KSPSolvers/src/BSSCR/auglag-driver-DGTGD.c b/underworld/libUnderworld/Solvers/KSPSolvers/src/BSSCR/auglag-driver-DGTGD.c index 966b704db..37fa85dd1 100644 --- a/underworld/libUnderworld/Solvers/KSPSolvers/src/BSSCR/auglag-driver-DGTGD.c +++ b/underworld/libUnderworld/Solvers/KSPSolvers/src/BSSCR/auglag-driver-DGTGD.c @@ -322,12 +322,21 @@ PetscErrorCode BSSCR_DRIVER_auglag( KSP ksp, Mat stokes_A, Vec stokes_x, Vec sto RHSSetupTime = MPI_Wtime(); - KSPSetUp(ksp_inner); + ierr = KSPSetUp(ksp_inner); + + Journal_Firewall( (ierr == 0), NULL, "An error was encountered during the PETSc solver setup. You should refer to the PETSc\n" + "error message for details. Note that if you are running within Jupyter, this error\n" + "message will only be visible in the console window." ); + + RHSSetupTime = MPI_Wtime() - RHSSetupTime; bsscrp_self->solver->stats.velocity_presolve_setup_time = RHSSetupTime; RHSSolveTime = MPI_Wtime(); - KSPSolve(ksp_inner, f, f_tmp); + ierr = KSPSolve(ksp_inner, f, f_tmp); + Journal_Firewall( (ierr == 0), NULL, "An error was encountered during the PETSc solve. You should refer to the PETSc\n" + "error message for details. Note that if you are running within Jupyter, this error\n" + "message will only be visible in the console window." ); KSPGetConvergedReason( ksp_inner, &reason ); {if (reason < 0) bsscrp_self->solver->fhat_reason=(int)reason; } KSPGetIterationNumber( ksp_inner, &bsscrp_self->solver->stats.velocity_presolve_its ); RHSSolveTime = MPI_Wtime() - RHSSolveTime; diff --git a/underworld/libUnderworld/Solvers/KSPSolvers/src/StokesBlockKSPInterface.c b/underworld/libUnderworld/Solvers/KSPSolvers/src/StokesBlockKSPInterface.c index 233e08a65..8c3a967d7 100644 --- a/underworld/libUnderworld/Solvers/KSPSolvers/src/StokesBlockKSPInterface.c +++ b/underworld/libUnderworld/Solvers/KSPSolvers/src/StokesBlockKSPInterface.c @@ -425,7 +425,11 @@ PetscErrorCode _BlockSolve( void* solver, void* _stokesSLE ) { } } - ierr = KSPSolve( stokes_ksp, stokes_b, stokes_x );CHKERRQ(ierr); + ierr = KSPSolve( stokes_ksp, stokes_b, stokes_x ); + + Journal_Firewall( (ierr == 0), NULL, "An error was encountered during the PETSc solve. You should refer to the PETSc\n" + "error message for details. Note that if you are running within Jupyter, this error\n" + "message will only be visible in the console window." ); Stg_KSPDestroy(&stokes_ksp ); //if( ((StokesBlockKSPInterface*)stokesSLE->solver)->preconditioner ) diff --git a/underworld/libUnderworld/StgFEM/SLE/ProvidedSystems/Energy/src/Energy_SLE_Solver.c b/underworld/libUnderworld/StgFEM/SLE/ProvidedSystems/Energy/src/Energy_SLE_Solver.c index b3bbf34a5..286bee28e 100644 --- a/underworld/libUnderworld/StgFEM/SLE/ProvidedSystems/Energy/src/Energy_SLE_Solver.c +++ b/underworld/libUnderworld/StgFEM/SLE/ProvidedSystems/Energy/src/Energy_SLE_Solver.c @@ -226,9 +226,14 @@ void _Energy_SLE_Solver_Solve( void* sleSolver, void* standardSLE ) { #endif } /*** Solve ***/ - KSPSolve( self->ksp, + PetscErrorCode ierr; + ierr = KSPSolve( self->ksp, ((ForceVector*) sle->forceVectors->data[0])->vector, ((SolutionVector*) sle->solutionVectors->data[0])->vector ); + Journal_Firewall( (ierr == 0), NULL, "An error was encountered during the PETSc solve. You should refer to the PETSc\n" + "error message for details. Note that if you are running within Jupyter, this error\n" + "message will only be visible in the console window." ); + KSPGetIterationNumber( self->ksp, &iterations ); Journal_DPrintf( self->debug, "Solved after %u iterations.\n", iterations ); diff --git a/underworld/libUnderworld/config/utils/options.py b/underworld/libUnderworld/config/utils/options.py index 94738993c..75ef684ca 100644 --- a/underworld/libUnderworld/config/utils/options.py +++ b/underworld/libUnderworld/config/utils/options.py @@ -233,7 +233,7 @@ def make_help_string(self): elif v.type == "enum": help += v._pref_sep + "<%s>" % "|".join(v.enum.values()) elif v.type == "bool": - if v._pref_true_flag is "": + if v._pref_true_flag == "": help = "[" + help + "]" else: help += v._pref_sep + "<" + v._pref_true_flag + \ diff --git a/underworld/mesh/_mesh.py b/underworld/mesh/_mesh.py index 78a790ef5..7c4da66da 100644 --- a/underworld/mesh/_mesh.py +++ b/underworld/mesh/_mesh.py @@ -561,7 +561,7 @@ def save(self, filename, units=None, **kwargs): raise TypeError("'filename', must be of type 'str'") - with h5File(name=filename, mode="a") as h5f: + with h5File(name=filename, mode="w") as h5f: # Save attributes and simple data. # This must be done in collectively for mpio driver. # Also, for sequential, this is performed redundantly. diff --git a/underworld/mesh/_meshvariable.py b/underworld/mesh/_meshvariable.py index f452461e3..b80ffefa9 100644 --- a/underworld/mesh/_meshvariable.py +++ b/underworld/mesh/_meshvariable.py @@ -407,7 +407,7 @@ def save( self, filename, meshHandle=None, units=None, **kwargs): raise TypeError("Expected 'filename' to be provided as a string") mesh = self.mesh - with h5File(name=filename, mode="a") as h5f: + with h5File(name=filename, mode="w") as h5f: # ugly global shape def globalShape = ( mesh.nodesGlobal, self.data.shape[1] ) diff --git a/underworld/mpi.py b/underworld/mpi.py index 4ff9cae9e..df0636e5d 100644 --- a/underworld/mpi.py +++ b/underworld/mpi.py @@ -21,7 +21,6 @@ """ -import underworld as _uw from mpi4py import MPI as _MPI comm = _MPI.COMM_WORLD size = comm.size diff --git a/underworld/scaling/_scaling.py b/underworld/scaling/_scaling.py index b16de866d..628b45a19 100644 --- a/underworld/scaling/_scaling.py +++ b/underworld/scaling/_scaling.py @@ -41,14 +41,17 @@ def non_dimensionalise(dimValue): Parameters ---------- - dimValue : pint quantity + dimValue : pint.Quantity + A pint quantity. Returns ------- - float: The scaled value. + float + The scaled value. + + Example + ------- - Example: - -------- >>> import underworld as uw >>> u = uw.scaling.units diff --git a/underworld/swarm/_swarmvariable.py b/underworld/swarm/_swarmvariable.py index 80a6e5053..2c8e6e9ed 100644 --- a/underworld/swarm/_swarmvariable.py +++ b/underworld/swarm/_swarmvariable.py @@ -440,13 +440,12 @@ def save( self, filename, collective=False, swarmHandle=None, units=None, **kwar for i in range(comm.rank): offset += procCount[i] - # open parallel hdf5 file - with h5File(name=filename, mode="a") as h5f: + with h5File(name=filename, mode="w") as h5f: # write the entire local swarm to the appropriate offset position globalShape = (particleGlobalCount, self.data.shape[1]) dset = h5_require_dataset(h5f, "data", shape=globalShape, dtype=self.data.dtype) - fact=1.0 + fact = np.array(1.0, dtype=self.data.dtype) if units: fact = dimensionalise(1.0, units=units).magnitude diff --git a/underworld/systems/_energy_solver.py b/underworld/systems/_energy_solver.py index 5b3f991d7..cadc9f10c 100644 --- a/underworld/systems/_energy_solver.py +++ b/underworld/systems/_energy_solver.py @@ -138,7 +138,7 @@ def _check_linearity(self, nonLinearIterate): def configure(self,solve_type=""): """ - Configure velocity/inner solver (A11 PETSc prefix). + Configure solver. solve_type can be one of: diff --git a/underworld/timing.py b/underworld/timing.py index 2b0bdf0bf..11c6ca7f1 100644 --- a/underworld/timing.py +++ b/underworld/timing.py @@ -34,7 +34,7 @@ >>> with someMesh.deform_mesh(): ... someMesh.data[0] = [0.1,0.1] >>> uw.timing.stop() ->>> # uw.print_table() # This will print the data. +>>> # uw.timing.print_table() # This will print the data. >>> # Commented out as not doctest friendly. >>> del os.environ["UW_ENABLE_TIMING"] # remove to prevent timing for future doctests diff --git a/underworld/utils/_io.py b/underworld/utils/_io.py index 010df1e8f..8fb8ae56b 100644 --- a/underworld/utils/_io.py +++ b/underworld/utils/_io.py @@ -90,6 +90,10 @@ def __enter__(self): h5py_mpi = h5py.get_config().mpi and (self.pattern=="collective") if h5py_mpi: self.kwargs.update( {"driver": 'mpio', "comm": MPI.COMM_WORLD} ) + else: + # If writing sequential, non-root procs should use `append` mode. + if uw.mpi.rank != 0: + self.kwargs.update( {"mode": 'a'} ) self.h5f = h5py.File(*self.args, **self.kwargs) From ad0a37136d686b2141b3771236e5fff7ca62ff89 Mon Sep 17 00:00:00 2001 From: Julian Giordani Date: Tue, 17 Aug 2021 14:33:10 +1000 Subject: [PATCH 12/13] Update docker_build_push.yml --- .github/workflows/docker_build_push.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/docker_build_push.yml b/.github/workflows/docker_build_push.yml index 86dd75b66..06178cfa2 100644 --- a/.github/workflows/docker_build_push.yml +++ b/.github/workflows/docker_build_push.yml @@ -25,4 +25,4 @@ jobs: context: . file: ./docs/development/docker/underworld2/Dockerfile push: true - tags: julesg/curvi_bits:latest + tags: underworldcode/underworld:curvi-bits From bc0a59722714c92e22939465c25cc1a1235c4cf5 Mon Sep 17 00:00:00 2001 From: Julian Giordani Date: Tue, 17 Aug 2021 14:38:18 +1000 Subject: [PATCH 13/13] typo fix --- .github/workflows/docker_build_push.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/docker_build_push.yml b/.github/workflows/docker_build_push.yml index 06178cfa2..3249cbffa 100644 --- a/.github/workflows/docker_build_push.yml +++ b/.github/workflows/docker_build_push.yml @@ -25,4 +25,4 @@ jobs: context: . file: ./docs/development/docker/underworld2/Dockerfile push: true - tags: underworldcode/underworld:curvi-bits + tags: underworldcode/underworld2:curvi-bits