Integrated Engineering Development Environment

Oliver Rübenkönig
Zhenyu Liu
Jan G. Korvink

The IMTEK Mathematica Supplement (IMS) provides the basis for an Integrated Engineering Development Environment (IEDE). In this context we created an open source linear finite element modeling environment comprising the mechanics, fluidics, and a general differential operators domain. We split the operators from the geometry and shape functions, resulting in symbolic operators, which are well suited for finite element code generation, and fast numerical operators. In this article we show how we use the fast numerical operators for analyzing a microelectromechanical system device. A fast transient solution by means of model order reduction is presented. Finally, we conclude with a harmonic analysis.

Introduction

The IMTEK Mathematica Supplement (IMS) is a downloadable open source add-on package for Mathematica []. The supplement provides several hundred functions in about 40 packages. At IMTEK (the Institut für Mikrosystemtechnik) we encounter a variety of engineering tasks. The tasks range from modeling to analysis and culminate in the ultimate engineering goal—good design.

Unfortunately, the design process is often tedious for several reasons. It may be very difficult to extend existing Integrated Engineering Development Environments (IEDEs) due to their being closed source. Exporting some parts of the design process to controllable external tools and applying new algorithms is time-consuming. We try to remedy this shortcoming by providing the open source basis for an IEDE embedded in Mathematica.

In order to evaluate this notebook, IMS must be installed. Although IMS is a growing environment, we try to maintain the functionality between different versions. Sometimes, however, obsolete warning messages may appear. This means that a specific function has been replaced by a newer one. The code, however, should still work correctly.

Integrated Engineering Development Environment

An IEDE consists of several pieces that can be smoothly interchanged to reach a design. The ability to model and simulate several different physical domains (e.g., the electrical engineering and mechanical domains) must be combined with the ability to analyze.

This can be accomplished with an adequate data structure, which must be applicable to other—not yet conceived—domains. We will demonstrate this process with a Finite Element Method (FEM) analysis example and later show how this concept is extendable. The finite element code is based on [, , ].

We start from the observation that engineering systems are constructed by some “basic” components that are related to each other via some connectivity, to be further specified. This connectivity can then be transformed into a system of ordinary differential equations (ODEs) (Figure ).

A typical engineering application would be the components of an electrical circuit such as resistors, inductors, capacitances, and sources. These elements are connected to each other via “nodes” (Figure ). The elements and nodes reside in a graph. The elements are not restricted to circuit elements, but can be elements such as finite elements that the engineer might need.

The system of equations is a system of second-order ODEs. The coefficients are the mass, damping, and stiffness matrices. The load is a vector of a list of multiple load vectors.

In this article we show how we use the graph data structure, apply finite element operators in the elements in the graph, and obtain a system of equations (Figure ). The finite element operators are taken from the classical scalar partial differential equations (PDEs). The concept, however, is general, and for different engineering domains different operators can assemble a system of equations.

Heat Anemometer Application Example

Here we model a heat flow anemometer. Figure  shows a schematic of the device.

baseX = baseY = 0;
anemometerLength = 300;
anemometerHeight = 60;
anemometerBase = 60;
membraneThick = 20;
heatLength = 20;
heatHeight = 10;
flowHeight = 200;

alpha = ArcTan[ Sqrt[ 2 ] ];
myBeta = Pi/2 - alpha;
membraneHight = ( anemometerHeight - membraneThick );
l1 = membraneHight / Sin[ alpha ] * Sin[ myBeta ];

p1 = { baseX, baseY };
p2 = p1 + { anemometerBase, 0 };
p5 = p2 + { l1, membraneHight };
p4 = p1 + {anemometerLength, 0};
p3 = p4 - { anemometerBase, 0 };
p6 = p3 - { l1, -membraneHight };
p9 = p1 + { 0, anemometerHeight };
p12 = p4 + { 0, anemometerHeight };
heatBase = (p12 - p9 )/2 - { heatLength/2,-(anemometerHeight- heatHeight) };

p7 = heatBase;
p8 = heatBase + { heatLength, 0 };
p10 = heatBase + { 0, heatHeight };
p11 = heatBase + { heatLength, heatHeight };
p13 = p1 + { 0, anemometerHeight + flowHeight };
p14 = p13 + { anemometerLength, 0 };
p15 = p1 + { anemometerLength /2, (anemometerHeight - membraneThick)/2 };
p16 = p1 + { anemometerLength/2, anemometerHeight  -heatHeight -(membraneThick  - heatHeight)/2 };
p17 = p1 + { anemometerLength/2, anemometerHeight - heatHeight/2 };
p18 = p1 + { anemometerLength/2, anemometerHeight + flowHeight/2 };

silicon = { Thickness[ 0.01 ], Line[ { p1, p2, p5, p6, p3, p4, p12, p9, p1 } ] };
heater = { Hue[ 1 ], Polygon[ { p7, p8, p11, p10 } ] };
flowBox = Line[ { p12, p14, p13, p9, p12 } ];
freeCut = Line[ { p2, p3, p6, p5, p2 } ];
device = {  silicon, heater, flowBox, freeCut};

func[ y_ ] := { - 100 *(( (y-( anemometerHeight +  flowHeight/2 ) )/( flowHeight/2 ) )^2 -1. ), y }
res  = Table[ func[ x ],{ x, anemometerHeight, anemometerHeight + flowHeight }  ];
flowPlot = ListLinePlot[ res, Axes -> False, DisplayFunction -> Identity ];

flowFieldText = Style[Text[ "Flow Field", { anemometerLength/2,anemometerHeight + flowHeight/2 }, { 3, 0 }],FontFamily->"Helvetica",FontWeight->"Bold",FontSize->12];
flowFieldArrow = Arrow[ {{anemometerLength/2 -  2 * heatLength,anemometerHeight + flowHeight/2 }, { anemometerLength/2 + 2 * heatLength,anemometerHeight + flowHeight/2  }}];
siliconText = Style[Text[ "Silicon", { p3[[1]],anemometerHeight /2 }, { -1, 0 }],FontFamily->"Helvetica",FontWeight->"Bold",FontSize->12] ;
heaterText = Style[Text[ "Heater", heatBase + { heatLength/2, 2*heatHeight }],FontFamily->"Helvetica",FontWeight->"Bold",FontSize->12 ];
thermoSensorsText =Style[ Text[ "Thermo Sensors",  { anemometerLength/2, anemometerHeight + flowHeight/4 }, { 0, -1 }],FontFamily->"Helvetica",FontWeight->"Bold",FontSize->12];
sensorLeftArrow = Arrow[ {{anemometerLength/2, anemometerHeight + flowHeight/4 }, { p2[[1]], anemometerHeight }}];
sensorRightArrow = Arrow[ {{anemometerLength/2, anemometerHeight + flowHeight/4 }, { p3[[1]], anemometerHeight }}];

Show[ { Graphics[ device ], Graphics[ { flowFieldText, flowFieldArrow, siliconText, heaterText, thermoSensorsText, sensorLeftArrow, sensorRightArrow } ], flowPlot }, PlotRange -> All ]

scaleFactor = 1;
baseX = baseY = 0 * scaleFactor;
anemometerLength = 1200 * scaleFactor;
anemometerHeight = 80 * scaleFactor;
anemometerBase = 240 * scaleFactor;
membraneThick = 20 * scaleFactor;
heatLength = 20 * scaleFactor;
heatHeight = 10 * scaleFactor;
flowHeight = 300 * scaleFactor;

alpha = ArcTan[ Sqrt[ 2. ] ];
myBeta = Pi/2 - alpha;
membraneHight = ( anemometerHeight - membraneThick );
l1 = membraneHight / Sin[ alpha ] * Sin[ myBeta ];

p1 = { baseX, baseY };
p2 = p1 + { anemometerBase, 0 };
p5 = p2 + { l1, membraneHight };
p4 = p1 + {anemometerLength, 0};
p3 = p4 - { anemometerBase, 0 };
p6 = p3 - { l1, -membraneHight };
p9 = p1 + { 0, anemometerHeight };
p12 = p4 + { 0, anemometerHeight };
heatBase = (p12 - p9 )/2 - { heatLength/2,-(anemometerHeight- heatHeight) };

p7 = heatBase;
p8 = heatBase + { heatLength, 0 };
p10 = heatBase + { 0, heatHeight };
p11 = heatBase + { heatLength, heatHeight };
p13 = p1 + { 0, anemometerHeight + flowHeight };
p14 = p13 + { anemometerLength, 0 };
p15 = p1 + { anemometerLength /2, (anemometerHeight - membraneThick)/2 };
p16 = p1 + { anemometerLength/2, anemometerHeight  -heatHeight -(membraneThick  - heatHeight)/2 };
p17 = p1 + { anemometerLength/2, anemometerHeight - heatHeight/2 };
p18 = p1 + { anemometerLength/2, anemometerHeight + flowHeight/2 };

silicon = { Thickness[ 0.01 ], Line[ { p1, p2, p5, p6, p3, p4, p12, p9, p1 } ] };
heater = Line[ { p7, p8, p11, p10, p7 } ];
flowBox = Line[ { p12, p14, p13, p9, p12 } ];
freeCut = Line[ { p2, p3, p6, p5, p2 } ];
device = {  silicon, heater, flowBox, freeCut};

In a piece of bulk silicon we have embedded a small heater device. To the right and left are two temperature sensors. If the heater is switched on, both sensors will measure the same temperature. Once a flow field is flowing over the silicon device, the temperature distribution will shift and the left sensor will be cooler than the right sensor. From the temperature difference we can calculate the speed of the flow field. This is the principle of an anemometer. More about thermal measurements in fluids can be found in [].

In the next section we calculate the temperature distribution in the device and its surrounding area.

The Equation

In the most general assumption, we wish to model the following equation:

({{(Underscript[({{σ (∇^2)u, +, γ ∇u}, {Diffusion,  , Convection}}), ⏟]), (Underscript[({{=}, { }}),  ]), (Underscript[({{f}, {Load}}),  ])}, {Stiffness Matrix,  , Load Matrix}})

The heater element will be modeled with a load matrix and the airflow with the convection term. The general heat transfer is modeled with the diffusion term.

Loading Predefined Mesh

We begin by loading the Imtek`Interfaces`EasyMesh` package.

Needs["Imtek`Interfaces`EasyMesh`"]

Then we import the EasyMesh-generated example mesh.

{numberOfNodes,mesherNodes,numberOfElements,mesherElements,numberOfSides,sides}=imsReadEasyMesh[ ToFileName[ { "Imtek", "IMSData", "ApplicationExamples", "FEM"},"AnemometerMesh"],imsStartNodeNumbering->1 ];

Creating the Nodes

First we load the following packages.

Needs["Imtek`Graph`"]
Needs["Imtek`MeshElementLibrary`"]

Then we take the raw nodes and make IMS nodes from them. Each of the raw nodes is a list of a unique node identification number (id), the coordinates {x,y}, and a marker. This marker was inserted by the mesh generator and indicates whether the node belongs to one of the domain segments or not.

We also divide the nodes into boundary nodes and interior nodes. Boundary nodes are the nodes on the simulation domain boundary that specify the boundary conditions. In the mesh input file we have specified that markers 1, 2, and 3 are on the simulation boundary. All other nodes are interior nodes. Either they belong to interior domain segments (and carry the marker 7) or they do not belong to any segment at all (and carry the marker 0).

The IMS nodes we create also carry a unique identification number (id), the coordinates {x,y}, the marker, a value of the solution (initially set to 0), and the type of the boundary condition.

interiorNodes=Select[ mesherNodes,(#[[4]]==0||#[[4]]==7)&]/. { id_,x_,y_,marker_ }:>imsMakeNode[id,{x,y },marker ];

Here we select from the list of all mesh nodes those entries where the marker is 0 or 7.

This displays all the nodes that have a marker of 7.

Graphics[ Point[ # ]&/@(#[[ {2,3} ]]&/@Select[mesherNodes,(#[[4]]==7)&]), AspectRatio->0.25]

The boundary nodes are those nodes that have a fourth entry not equal to 0 or 7. Additionally we set boundary conditions in the nodes according to the markers for the specific node. The nodes now carry the id, the coordinates, the marker, the boundary condition value, and the boundary condition type.

boundaryNodes=Select[mesherNodes,(#[[4]]!=0&&#[[4]]!=7)&]/. {id_,x_,y_,marker_}:> 
Which[
marker==1,imsMakeNode[id,{x,y },marker,{{ 0.}},"Dirichlet" ],
marker==2,imsMakeNode[id,{ x,y },marker,{{ 0.}},"Dirichlet" ],
marker==3,imsMakeNode[id,{x,y },marker,{{ 0.}},"Dirichlet" ]
];

Creating the Elements

Now we set up the elements. Dirichlet boundary conditions are only set up in the nodes. Neumann boundary conditions also need an entry in the elements. Each element consisting of two Neumann boundary nodes will additionally carry the boundary values for the nodes involved.

First we join all nodes and obtain the Neumann boundary node ids.

allNodes=Sort[Join[interiorNodes,boundaryNodes], OrderedQ[{imsGetIds[#1],imsGetIds[#2]}]&];
neumannBoundaryNodesIds=imsGetIds[Select[boundaryNodes,imsGetDatas[#]=="Neumann"&]]

To construct the elements, we choose those elements that have a count of two Neumann boundary nodes and insert the Neumann values of the boundary nodes into the elements. All other elements are constructed in such a way that they carry their identification id, the incident ids (i.e., the ids of the nodes making up the specific element), and the element marker.

The element marker, in contrast to the node marker, states to which of the four parts of the simulation device the element belongs. Marker 1 is for the silicon device, marker 2 is the heater, marker 3 is the free cut, and marker 4 is the flow box.

allElements= mesherElements/. {id_,i_,j_,k_,ei_,ej_,ek_,si_,sj_,sk_,xV_,yV_,marker_ }:> 
If[Plus@@(Count[neumannBoundaryNodesIds,# ]&/@{i,j,k })==2,
imsMakeTriangleLinear1DOF[id,{ i,j,k },marker, imsGetValues[allNodes[[{ i,j,k }]]]],
imsMakeTriangleLinear1DOF[id,{i,j,k },marker ]
];

Each element has a marker similar to the markers in a node. The element markers can later be used to specify the behavior of finite element functions in different areas of the mesh.

Creating the Mesh

Now we generate the mesh.

femMesh=imsMakeNexus[boundaryNodes,interiorNodes,allElements ];

numberOfNodes

numberOfElements

Since each node belongs to several triangles, we have more triangles than nodes.

Displaying the Mesh

Here we display the mesh.

Graphics[{imsDrawElements[femMesh],device},PlotRange->All, AspectRatio->Automatic]

Here we display the mesh inside the silicon and those elements with marker 1.

siliconElementIds= imsGetIds[Select[imsGetElements[femMesh ],(imsGetMarkers[#]==1)&]];

Graphics[{imsDrawElements[femMesh,siliconElementIds]}, PlotRange->All,AspectRatio->0.25]

Physics Setup

The finite element operator functions need some further specifications. For example, the diffusion operators need some information about the isotropy of the material underneath, and each finite element operator needs a function to specify its behavior. Setting the function values to 0 will turn off the operator in that region. The functions obtain the marker of the element they are currently computing and the coordinates of the center of mass of this element. This information should be enough to specify complex functions in complex regions.

We have not spoken about the dimensions of the device. For meshing it was convenient to set the dimensions to (10^(-6)) (i.e., a micrometer scale). That implies, however, that we have to scale the material data accordingly. This is what the xyScale values do.

We have four markers for the elements. Depending on where an element is physically situated in the simulation domain, it may have different properties. We have four parts in our simulation domain. The silicon parts (silicon device and heater) have different material data than the air parts (free cut and flow box). The convection part of the equation is only active in the flow box part of the simulation domain. Thus, in the other parts we set the convection function to 0. The heater is similar. With this mechanism we can switch different parts of the PDE on and off in different parts of the simulation domain.

Anisotropy

We assume the heat conduction of silicon to be 148 W/(K m) and that of air to be 23.9*(10^(-3)) W/(K m).

anisotropyScale=10^6;
anisotropyFunction=Function[{marker,x,y},
Evaluate[
Which[
marker==1,Evaluate[anisotropyScale*148.*{{1.,0.},{0.,1.}}],
marker==2,Evaluate[anisotropyScale*148.*{{1.,0. },{0.,1.}}],
marker==3,Evaluate[anisotropyScale*23.9*10^-3* {{1.,0.},{0.,1.}}],
marker==4,Evaluate[anisotropyScale*23.9*10^-3* {{1.,0.},{0.,1.}}]
]
]
];

Convection

The specific heat of air is approximately 1000 J/(Kg K) and the density is 1.29 Kg/(m^3).

convectionScale=1.;
velocity=10^-1.;
convectionFunction=Function[{marker,x,y },
Evaluate[
Which[
marker==1,{{0.,0.}},
marker==2,{{0.,0.}},
marker==3,{{0.,0.}},
marker==4, Evaluate[-convectionScale* {{1000*1.29*velocity* (((y-(anemometerHeight+flowHeight/2))/(flowHeight/2))^2 -1),0.}}]
]
]
];

Load

We have a heat source of (10^10) Kg/((s^2)m).

loadScale=10^(-6);
loadFunction=Function[{marker,x,y},
Evaluate[
Which[
marker==1,0.,
marker==2,Evaluate[loadScale*10^10.],
marker==3,0.,
marker==4,0.
]
]
];

VectorPlot[Flatten[convectionFunction[4,x,y]],{x,baseX,anemometerLength},{y,anemometerHeight,anemometerHeight+flowHeight},PlotRange->All,AspectRatio->Automatic]

Steady-State Solution

Initialization

First we load the Imtek`ShowStatus` package.

Needs["Imtek`ShowStatus`"]

Matrix Assembly

Needs["Imtek`FEMOperators`"]
Needs["Imtek`Assembler`"]

The assembly of the global matrix is divided into several steps. First we need to initialize the required global matrices, the stiffness matrix, and the load vector. The element dimension specifies the size of the element stiffness matrix and the element right-hand side vector. We want to loop over all elements. First we get the element (let us call it i) from the mesh and get this element’s nodes. We also need the rows and columns that this element contributes to in the global matrix.

First we create two matrices with 0 entries—one for initializing each element stiffness matrix (elementSMEmpty) and one for initializing the element right-hand side (elementRHSEmpty).

In the main computation each operator is applied to each element i. The input to each operator is an element matrix and an element right-hand side matrix. Each operator also returns those two matrices, now occupied by the values computed for this element. The order in which the operators are applied does not matter. In the last step the element matrix is assembled into the global matrix.

stiffness=SparseArray[{},{numberOfNodes,numberOfNodes},0.]; 
load=Table[ 0.,{numberOfNodes},{1}];

elementDim=Length[imsGetIncidentsIds[imsGetElements[femMesh,1]]];

elementSMEmpty=Table[ 0.,{elementDim},{elementDim}];
elementRHSEmpty=Table[ 0.,{elementDim},{1}];

Do[
imsShowStatus[ "Computing Element Number:"<>ToString[i]<> " from "<>ToString[numberOfElements]];

(* element initialization *)
thisElement=imsGetElements[femMesh,i];
elementNodes=imsGetNodes[femMesh,imsGetIncidentsIds[thisElement]];
rows=cols=imsGetIds[elementNodes];

elementSM=imsMakeElementMatrix[elementSMEmpty,rows,cols];
elementRHS=imsMakeElementMatrix[elementRHSEmpty,rows,{1}];

(* main computation *)
{ elementSM,elementRHS}=
imsNFEMDiffusion[{elementSM,elementRHS},thisElement,elementNodes,anisotropyFunction ];

{ elementSM,elementRHS }=imsNFEMLoad[{elementSM,elementRHS},thisElement,elementNodes,loadFunction];

{elementSM,elementRHS}= 
imsNFEMConvection[{elementSM,elementRHS},thisElement,elementNodes,convectionFunction ];

{elementSM,elementRHS}= 
imsNFEMNeumann[{elementSM,elementRHS},thisElement,elementNodes];

(* build the global stiffness and load *)
imsAssemble[elementSM,stiffness];
imsAssemble[elementRHS,load ]; 

,{i,numberOfElements}
];

Dirichlet Boundary Conditions

The last step before solving the system of equations is to build in the Dirichlet boundary values. For this we load the Imtek`BoundaryConditions` package.

Needs["Imtek`BoundaryConditions`"]

Then we select the Dirichlet boundary nodes from the mesh.

diriNodes=Select[imsGetBoundaryNodes[femMesh], imsGetDatas[#]==="Dirichlet"&];

The Dirichlet operators get a matrix (stiffness), a vector (flattLoad), and the position and value of the Dirichlet boundary condition. We create a list of position (node id) and value pairs.

positionValuePairs= Transpose[{imsGetIds[diriNodes],Flatten[imsGetValues[diriNodes,1]]}];

This maps the Dirichlet operator on the list of position and value pairs.

flattLoad=Flatten[load];
Map[imsDirichlet[{stiffness,flattLoad },#[[1]],#[[2]]]&, positionValuePairs];

Solving

Needs["Imtek`System`"]

load=Partition[flattLoad,1];
stationarySystem=imsMakeSystem[load,{stiffness}]

To solve the system of equations we load the Imtek`SystemAnalysis` package.

Needs["Imtek`SystemAnalysis`"]

solution=imsStationarySolve[stationarySystem];

Initialize Post Process

Now we look at the result.

First we set the values of the solution into the nodes. Since a node may contain solution values at several time steps—which we so far do not have—we partition the solution into sublists of size 1 (see Nodes in the IMTEK documentation in the Help Browser).

newNodes=imsSetValues[allNodes,Partition[solution,1]];

We then sort them into boundary and interior nodes.

newBoundaryNodes=newNodes[[imsGetIds[imsGetBoundaryNodes[femMesh]]]];
newInteriorNodes=newNodes[[imsGetIds[imsGetInteriorNodes[femMesh]]]];

Here we create a new graph with the new nodes that carry the solution.

solutionMesh=imsMakeNexus[newBoundaryNodes,newInteriorNodes,allElements ];

Contour Plot

To visualize a contour plot of the solution, we sort the nodes and obtain their coordinates.

coordinates= imsGetCoords[Sort[imsGetNodes[solutionMesh], OrderedQ[imsGetIds[{#1,#2 }]]&]];

Next we obtain the incidents.

incidents = imsGetIncidentsIds[ imsGetElements[ solutionMesh ] ];

Then we load the Imtek`UnstructuredPlot` package.

Needs["Imtek`UnstructuredPlot`"]

To set the contours we find the minimum and maximum values.

{minSol,maxSol}={Min[solution],Max[solution]}

stepSize=(maxSol-minSol)/30.

Here is the result.

femP=imsUnstructuredContourPlot[coordinates[[Range[numberOfNodes]]], incidents,Flatten[solution],AspectRatio->Automatic,Contours->Range[minSol,maxSol,stepSize],Mesh->False, PlotRange->All];

Show[{femP,Graphics[device]}]

Transient Solution via Model Order Reduction

The Equation

Now we model the following equation:

({{(Underscript[({{ρ ((∂u)/(∂t))}, {Transient}}),  ]), (Underscript[({{+}, { }}),  ]), (Underscript[({{σ (∇^2)u, +, γ ∇u}, {Diffusion,  , Convection}}), ⏟]), (Underscript[({{=}, { }}),  ]), (Underscript[({{f}, {Load}}),  ])}, {Damping Matrix,  , Stiffness Matrix,  , Load Matrix}})

The new term is the partial time derivative.

The Physics

In order to scale the damping function correctly, we need to set the damping function. With the specific heat of approximately 700 J/(Kg K) for silicon and 1000 J(Kg JK) for air. The densities are 2330 Kg/(m^3) for silicon and 1.29 Kg/(m^3) for air.

dampingScale=10^-6.;

dampingFunction=Compile[{{marker,_Integer },x,y },
Evaluate[ Which[
marker==1,Evaluate[dampingScale*700*2330 ],
marker==2,Evaluate[dampingScale*700*2330 ],
marker==3,Evaluate[dampingScale*1000.*1.29 ],
marker==4,Evaluate[dampingScale*1000.*1.29 ]
]
]
];

The Damping Matrix Assembly

Since the stiffness matrix has already been assembled, we now devote our attention to the damping matrix matC. This approach is exactly the same as for the stiffness matrix. Now, however, we use the FEMTransient operator.

First we create an empty sparse array for the damping matrix.

damping=SparseArray[{},{numberOfNodes,numberOfNodes},0.];

Next we obtain the element dimension and create an empty element damping matrix and an empty right-hand side matrix.

elementDim=Length[imsGetIncidentsIds[imsGetElements[femMesh,1]]];

elementDMEmpty=Table[0.,{elementDim},{elementDim}];
elementRHSEmpty=Table[0.,{elementDim},{1}];

While assembling the stiffness matrix, we used a Do loop. Now we look at a more functional programming style. First we get all elements for the mesh.

allElements=imsGetElements[femMesh];

Next we obtain all element nodes, element rows, and columns.

allElementNodes=imsGetNodes[femMesh,#]&/@ imsGetIncidentsIds[allElements];
allRows=allCols=imsGetIds[allElementNodes];

We create all empty element matrices with the correct row and column entries.

allEmptyElements={
imsMakeElementMatrix[elementDMEmpty,#,#], 
imsMakeElementMatrix[elementRHSEmpty,#,{1}]}&/@allRows;

Now we thread the transient operator over all empty elements.

allFilledElements=
Thread[imsNFEMTransientMatrix[allEmptyElements,allElements,allElementNodes,dampingFunction]];

In the last step we assemble the filled local damping and right-hand side matrices into the global damping matrix and the global right-hand side vector.

Map[
(imsAssemble[#[[1]],damping];
imsAssemble[#[[2]],load ];)&,allFilledElements ];

To account for the Dirichlet values, we set the matrix entries to 0.

damping[[All,positionValuePairs[[All,1 ]]]]=damping[[positionValuePairs[[All,1]],All]]=0.;

Finally, we generate a transient system.

transientSystem=imsSetDamping[stationarySystem,{damping}]

Model Order Reduction

To perform a transient simulation, it is useful to reduce the order of the equation system and to thus accomplish the transient solution in a short time.

First we load the Imtek`Arnoldi` package.

Needs["Imtek`Arnoldi`"]

Then we reduce our original system to the dimension we want.

dimension=30;
{reducedSystem,matV}=imsArnoldiReduction[transientSystem,dimension];

Transient Solution

As an excitation we choose a unit step.

To compute the transient behavior we use TransientSolve.

tStart=0.;
tEnd=2*10^-3.

Timing[
{reducedSolution}= x/.imsTransientSolve[reducedSystem,x,{t,tStart,tEnd},UnitStep,SolveDelayed->True,StepMonitor:>imsShowStatus[t]]
]

Here we reverse the projection.

tStep=(tEnd-tStart)/5;
time=Range[tStart,tEnd,tStep];
solutionData=Transpose[matV.reducedSolution[#]&/@time ];

Post Process

The post processing is along the same lines as previously stated. This time, however, each node has several solution values, one for each time step.

transientNodes=imsSetValues[allNodes,Partition[#,1]&/@solutionData];

boundaryNodes=transientNodes[[imsGetIds[imsGetBoundaryNodes[femMesh]]]];
interiorNodes=transientNodes[[imsGetIds[imsGetInteriorNodes[femMesh]]]];

Then we create a new graph with the new nodes that carry the solution.

solutionMesh=imsMakeNexus[boundaryNodes,interiorNodes,allElements ];

Transient Contour Plot

The minimum of the solution value should be around zero.

{minSol, maxSol}={Min[solutionData], Max[solutionData]}

stepSize=(maxSol-minSol)/25.

Show[GraphicsGrid[Partition[(Show[{imsUnstructuredContourPlot[coordinates,incidents,Flatten[imsGetValues[transientNodes,Sequence[#1,1]]],Contours->Range[minSol,maxSol,stepSize],PlotRange->All,AspectRatio->Automatic,DisplayFunction->Identity],Graphics[device]}]&)/@Range[Length[time]],2]]]

Integrated Engineering Development Environment Future

A future application of this example might couple the computed model with an electrical circuit model. Generally more engineering domains need to be added, which implies an implementation of new operators. The finite element operators would be extended to arbitrary dimensions and elements.

Conclusion

We have shown that the concept of using graphs, operators, and system assembly can be used to create an Integrated Engineering Development Environment. Thus we have an environment in which to create different engineering domains as well as to extend and solve them. It is just a matter of plugging in new domains. We have added numerical capabilities to Mathematica in the sense that we can solve some classical PDEs on arbitrary formed domains. With model order reduction, a transient solution can be found in less time than many designated commercial software tools.

Acknowledgment

The authors would like to thank all the people who have contributed to IMS. The first author would like to especially thank his mentors, Professor Jan G. Korvink and Dr. Zhenyu Liu, as well as the many people who have contributed suggestions, packages, and motivation, in particular Steffen Kibbel, who made many suggestions for the anemometer example.

References

O. Rübenkönig and J. G. Korvink, IMTEK Mathematica Supplement (IMS), (2002–2005) portal.uni-freiburg.de/imteksimulation/downloads/ims.

H. R. Schwarz, Methode der Finiten Elemente, 3rd ed., Stuttgart: B. G. Teubner, 1991.

D. R. J. Owen and E. Hinton, A Simple Guide to Finite Elements, Swansea, UK: Pineridge Press, 1980.

V. N. Kaliakin, Introduction to Approximate Solution Techniques, Numerical Modeling, and Finite Element Methods, New York: Marcel Dekker, 2002.

H. Ernst, “High-Resolution Thermal Measurements in Fluids,” Ph.D. thesis, Albert-Ludwigs-Universität Freiburg, Freiburg, 2001.