Data structures

Author(s): Jukka Aho

Abstract: Description of basis data structures. In this notebook the concepts of Increment, TimeStep, DiscreteField, ContinuousField, FieldSet, SpatialBasis and TemporalBasis are intoduced. By combining these atomic structures one is able to form finite elements and interpolate it's fields in time and spatial domain.

  • Increment is the most atomic structure. It's a vector-like object with 1 dimension. Each element in Increment can be scalar, vector or tensor (2 or 4 order). It's easy to extend Increment to have other data types too.
  • TimeStep is container for increments in certain time $t$.
  • DefaultDiscreteField is container for timesteps for a single field.
  • FieldSet is container for all fields.
  • DefaultContinuousField can have continuous field variables.

Revision history

2015-06-14

  • Initial version.

2015-09-25

  • Complete rewrite. The main ideas proposed in earlier version didn't work.

2015-10-29

  • Third iteration.

2015-11-03

  • Starts to be ready. Introduced symbolic fields.

Data fields on elements

Typical element structure so far:

type MyElement <: Element
    connectivity :: Array{Int, 1}  # describes how dofs of this element is connected to another elements in global level
    basis :: Basis  # describes how to interpolate fields
    fields :: ???
end

  • Fields must be interpolable, in space $\mathbb{C}^n \times \mathbb{R}$, i.e. $f(\boldsymbol{\xi}, t) = \sum_i \phi_i(\boldsymbol{\xi}) f_i(t) = \sum_i \phi_i(\boldsymbol{\xi}) \sum_j \varphi_j(t) f_{ij}$, where $f_{ij}$ is scalar, tensor or vector defined in element area $e$ by some basis functions $\phi(\boldsymbol{\xi})$ and $\varphi(t)$. Parameter $t$ is normally considered as "time" and $\xi$ is dimensionless coordinate. Parameter $t$ has not necessarily to be time, it could be for example angle $\alpha \in [-2\pi, 2\pi]$ or similar.
  • We store mainly three fields, scalar field, vector field, tensor field. Field may or may not be dependent from parameters $\xi$ or $t$.
  • $t$ is discretized to several steps $\{t_0, t_1, \ldots, t_n\}$. Each discrete time $t_i$ may contain several iterations until convergence. We want to save and get access to all of this data if needed.
  • So in practice we have a set of fields $f(\boldsymbol{\xi})$ over time domain $t$. Typically some fields, like Geometry, is introduced only in time $t_0$. Some other fields, like boundary load, may be "active" only on some time $\hat{t} \subset t$. Some care must be taken of how to extrapolate field variables.
  • In the simplest case (simple nonlinear quasistatic analysis), we have for instance $t \in [0, 1]$ where boundary conditions are set in $t_0$ and load is set in $t_1$. We may use adaptive strategies to shorten time if convergence issues araises.

Creating discrete fields

Here's the strategy in short: each non-linear iteration is Increment, what is a vector-like object containing data. Elements of Increment can be scalars, vectors or tensors. Increment belongs to TimeStep. TimeStep contains one or more increments. Then we have Field which contains all timesteps. And finally we have FieldSet which contains all fields.

FieldSet -> Field -> TimeStep -> Increment -> data

for example,

FieldSet -> Field -> TimeStep -> Increment -> data
FieldSet -> "temperature" -> 0.0 -> 1 -> [1,2,3,4]
                                    2 -> [2,3,4,5]
FieldSet -> "temperature" -> 1.0 -> 1 -> [3,4,5,6]
                                    2 -> [5,6,7,8]

and so on. Here we create a FieldSet containing one field temperature which contains two TimeSteps, two Increments in both of them.


In [1]:
using JuliaFEM: Increment, TimeStep, Field, FieldSet, DefaultDiscreteField

fs = FieldSet()
i1 = Increment([1, 2, 3])
i2 = Increment([2, 3, 4])
t1 = TimeStep(1.0, Increment[i1, i2])
i3 = Increment([2, 3, 4])
i4 = Increment([3, 4, 5])
t2 = TimeStep(2.0, Increment[i3, i4])
f1 = DefaultDiscreteField(TimeStep[t1, t2])
fs["temperature"] = f1
fs


Out[1]:
Dict{ASCIIString,JuliaFEM.Field} with 1 entry:
  "temperature" => JuliaFEM.DefaultDiscreteField([JuliaFEM.TimeStep(1.0,JuliaFE…

Accessing last increment of last timestep of temperature can be done in the following way:


In [2]:
fs["temperature"][2][2]


Out[2]:
3-element JuliaFEM.Increment{Int64}:
 3
 4
 5

Because the model is deep and quite heavy to type some "shortcuts" are provided, but keep in mind that everything is there in place. Here's the shortened version:


In [3]:
fs = FieldSet()  # create empty fieldset
fs["temperature"] = [1, 2, 3, 4]  # create temperature field with time t=0
first(fs["temperature"]), last(fs["temperature"])  # pick first and last timestep of temperature


Out[3]:
([1,2,3,4],[1,2,3,4])

This way we can easily create discrete scalar, vector and tensor fields.


In [4]:
fs2 = FieldSet()
fs2["constant scalar field"] = 1
fs2["scalar field"] = [1, 2, 3, 4]
fs2["vector field"] = reshape(collect(1:8), 2, 4)
fs2["second order tensor field"] = reshape(collect(1:3*3*4), 3, 3, 4)
fs2["fourth order tensor field"] = reshape(collect(1:3*3*3*3*4), 3, 3, 3, 3, 4)
fs2


Out[4]:
Dict{ASCIIString,JuliaFEM.Field} with 5 entries:
  "fourth order tensor fi… => JuliaFEM.DefaultDiscreteField([JuliaFEM.TimeStep(…
  "constant scalar field"  => JuliaFEM.DefaultDiscreteField([JuliaFEM.TimeStep(…
  "vector field"           => JuliaFEM.DefaultDiscreteField([JuliaFEM.TimeStep(…
  "scalar field"           => JuliaFEM.DefaultDiscreteField([JuliaFEM.TimeStep(…
  "second order tensor fi… => JuliaFEM.DefaultDiscreteField([JuliaFEM.TimeStep(…

Or even more compactly:


In [5]:
FieldSet("geometry" => [1, 2, 3, 4], "temperature" => [0, 0, 0, 0], "density" => 7850)


Out[5]:
Dict{ASCIIString,JuliaFEM.Field} with 3 entries:
  "density"     => JuliaFEM.DefaultDiscreteField([JuliaFEM.TimeStep(0.0,JuliaFE…
  "geometry"    => JuliaFEM.DefaultDiscreteField([JuliaFEM.TimeStep(0.0,JuliaFE…
  "temperature" => JuliaFEM.DefaultDiscreteField([JuliaFEM.TimeStep(0.0,JuliaFE…

By default using this "fast typing" fields are defined at time $t=0.0$.


In [6]:
fs2["vector field"][end].time  # pick last timestep of field "vector field"


Out[6]:
0.0

Creating new field with several time steps defined can also be done compactly. Each tuple has time and increment data.


In [7]:
t0 = TimeStep(0.0, Increment([1, 2, 3, 4]))
t1 = TimeStep(0.5, Increment([2, 3, 4, 5]))
t2 = TimeStep(1.0, Increment([1, 1, 1, 1]))
fs2["time series"] = [t0, t1, t2]

info("time on last timestep: $(fs2["time series"][end].time)")


INFO: time on last timestep: 1.0

Or even without explicitly expressing time. In that case time step size is 1 second starting from 0.


In [8]:
fs2["time series 2"] = [1, 2], [2, 3], [3, 4]
fs2


LoadError: MethodError: `convert` has no method matching convert(::Type{Float64}, ::Array{Int64,1})
This may have arisen from a call to the constructor Float64(...),
since type constructors fall back to convert methods.
Closest candidates are:
  call{T}(::Type{T}, ::Any)
  convert(::Type{Float64}, !Matched::Int8)
  convert(::Type{Float64}, !Matched::Int16)
  ...
while loading In[8], in expression starting on line 1

 in convert at /home/jukka/.julia/v0.4/JuliaFEM/src/fields.jl:233
 in call at essentials.jl:56
 in setindex! at dict.jl:641

Adding another field, with different time.


In [9]:
T0 = first(fs["temperature"])  # pick first timestep (or to be spesific, last increment of first timestep)
T1 = Increment(T0 + 1)  # create new increment from old one
timestep = TimeStep(1.0, T1)  # create new timestep at t=1.0
push!(fs["temperature"], timestep)  # push to field


Out[9]:
2-element Array{JuliaFEM.TimeStep,1}:
 JuliaFEM.TimeStep(0.0,JuliaFEM.Increment[[1,2,3,4]])
 JuliaFEM.TimeStep(1.0,JuliaFEM.Increment[[2,3,4,5]])

Normal stuff like dot product works:


In [10]:
S1 = Increment([1, 2, 3])
S2 = Increment([2, 3, 4])
dot(S1, S2), dot([1, 2, 3], S2), dot(S1, [2, 3, 4])


Out[10]:
(20,20,20)

In [11]:
1/2*(S1 + S2)


Out[11]:
3-element Array{Float64,1}:
 1.5
 2.5
 3.5

In [12]:
dot([1, 2], Increment[S1, S2])


Out[12]:
3-element Array{Int64,1}:
  5
  8
 11

Creating empty Increment:


In [13]:
f = zeros(Increment, 2, 4)


Out[13]:
4-element JuliaFEM.Increment{Array{Float64,1}}:
 [0.0,0.0]
 [0.0,0.0]
 [0.0,0.0]
 [0.0,0.0]

Create similar increment with new data:


In [14]:
g = similar(f, ones(8))


Out[14]:
4-element JuliaFEM.Increment{Array{Float64,1}}:
 [1.0,1.0]
 [1.0,1.0]
 [1.0,1.0]
 [1.0,1.0]

Creating continuous (and discrete) fields

In previous section the concept of discrete fields was demonstrated. DefaultDiscreteField is subtype of DiscreteField which is subtype of Field:


In [15]:
using JuliaFEM: DefaultDiscreteField, DiscreteField, Field
DefaultDiscreteField <: DiscreteField <: Field


Out[15]:
true

There exists another type of fields too: ContinuousFields. Like the name already suggests it stores continuous time and spatial domain and it can be used to write custom fields. It needs to be callable. In this example ContinuousField is created which returns 1x4 dimensional array defined in $\boldsymbol\xi \in [-1,1]^2, t\in[0,1]$:


In [16]:
using JuliaFEM: FieldSet, DefaultContinuousField, ContinuousField

In [17]:
type MyContinuousField <: ContinuousField
end
function Base.call(f::MyContinuousField, xi::Vector, time::Number)
    time/4*[(1-xi[1])*(1-xi[2]) (1+xi[1])*(1-xi[2]) (1+xi[1])*(1+xi[2]) (1-xi[1])*(1+xi[2])]
end
f = MyContinuousField()


Out[17]:
MyContinuousField()

In [18]:
f([0.0, 0.0], 1.0)


Out[18]:
1x4 Array{Float64,2}:
 0.25  0.25  0.25  0.25

In [19]:
fs = FieldSet()
fs["basis"] = MyContinuousField()
fs


Out[19]:
Dict{ASCIIString,JuliaFEM.Field} with 1 entry:
  "basis" => MyContinuousField()

In [20]:
fs["basis"]([0.0, 0.0], 1.0)


Out[20]:
1x4 Array{Float64,2}:
 0.25  0.25  0.25  0.25

Again we have a shortcut to quickly define continuous fields:


In [21]:
fs["continuous field"] = (xi, t) -> xi[1]*xi[2]*t
fs["continuous field"]([1.0, 2.0], 3.0)


Out[21]:
6.0

Naturally we can pass another fields or even fieldsets to continuous field to make fields depend from each other. Here's another example, where ContinuousField takes another field and operates it with some function.


In [22]:
type MyContinuousField2 <: ContinuousField
    basis :: Function
    discrete_field :: DiscreteField
end
function Base.call(field::MyContinuousField2, xi::Vector, time::Number=1.0)
    data = last(field.discrete_field) # get the last timestep last increment
    basis = field.basis(xi) # evaluate basis at point ξ.
    sum([basis[i]*data[i] for i=1:length(data)]) # sum results
end


Out[22]:
call (generic function with 1264 methods)

Now we create two fields, one is discrete and another is continuous taking discrete field as input argument:


In [23]:
fs = FieldSet()
fs["discrete field"] = [1, 2, 3, 4]
basis(xi) = 1/4*[
    (1-xi[1])*(1-xi[2]),
    (1+xi[1])*(1-xi[2]),
    (1+xi[1])*(1+xi[2]),
    (1-xi[1])*(1+xi[2])]
fs["continuous field"] = MyContinuousField2(basis, fs["discrete field"])


Out[23]:
MyContinuousField2(basis,JuliaFEM.DefaultDiscreteField([JuliaFEM.TimeStep(0.0,JuliaFEM.Increment[[1,2,3,4]])]))

Results:


In [24]:
fs["continuous field"]([0.0, 0.0], 1.0), last(fs["discrete field"])


Out[24]:
(2.5,[1,2,3,4])

Next we add another discrete field and see that continuous field "connecting" to discrete field updates accordingly:


In [25]:
T0 = last(fs["discrete field"])
push!(fs["discrete field"], TimeStep(1.0, Increment(T0 + 1.0)))  # push to field
fs


Out[25]:
Dict{ASCIIString,JuliaFEM.Field} with 2 entries:
  "continuous field" => MyContinuousField2(basis,JuliaFEM.DefaultDiscreteField(…
  "discrete field"   => JuliaFEM.DefaultDiscreteField([JuliaFEM.TimeStep(0.0,Ju…

Updated results, notice how the value of continuous field changes according to the update of discrete field.


In [26]:
fs["continuous field"]([0.0, 0.0], 1.0), last(fs["discrete field"])


Out[26]:
(3.5,[2.0,3.0,4.0,5.0])

What we just did is that we actually interpolated discrete field using continuous functions. We evaluated discrete field using bilinear basis at midpoint of "element":


In [27]:
1/4*(1+2+3+4), 1/4*(2+3+4+5)


Out[27]:
(2.5,3.5)

From continuous field we can also go back to discrete fields, if needed. Here we evaluate continuous field in four discrete points, let's call them to Gauss quadrature points.


In [28]:
using JuliaFEM: DiscreteField
type MyDiscreteField <: DiscreteField
    discrete_points :: Vector
    continuous_field :: ContinuousField
end
Base.length(field::MyDiscreteField) = length(field.discrete_points)
Base.endof(field::MyDiscreteField) = endof(field.discrete_points)
Base.last(field::MyDiscreteField) = Float64[field[i] for i=1:length(field)]
function Base.getindex(field::MyDiscreteField, idx::Int64)
    field.continuous_field(field.discrete_points[idx])
end


Out[28]:
getindex (generic function with 126 methods)

In [29]:
discrete_points = 1.0/sqrt(3.0)*Vector[[-1, -1], [1, -1], [1, 1], [-1, 1]]
fs["discrete field 2"] = MyDiscreteField(discrete_points, fs["continuous field"])
last(fs["discrete field 2"])


Out[29]:
4-element Array{Float64,1}:
 2.75598
 3.08932
 3.91068
 4.24402

Basically summing the above values together we have (almost) done numerical integration over element area. By using these two simple concepts we are able to construct rest of the results.

Interpolation

In earlier discussion the concepts of DiscreteField and ContinuousField were introduced, so that now we can define discrete set of values and continuous functions. It has also been shown how fields can depend from each other such a way that we can interpolate continuous field from discrete field and vice versa.

This motivates us to create continuous fields that are interpolated from discrete fields using some polynomial basis. By thinking this way interpolation is nothing more than just an application of the earlier results already shown.

Some fields has special meaning in JuliaFEM. Typically one needs to define at least discrete field geometry and continuous field basis so that basic interpolation is working on element.

Interpolation of fields works of course both in time and spatial dimension. Here's a simple example showing the main concept.


In [30]:
using JuliaFEM: Basis

Yep, it's just continuous field:


In [31]:
Basis <: ContinuousField


Out[31]:
true

Basis + DiscreteField = Interpolation:


In [32]:
basis(xi) = 1/4*[(1-xi[1])*(1-xi[2])   (1+xi[1])*(1-xi[2])   (1+xi[1])*(1+xi[2])   (1-xi[1])*(1+xi[2])]
dbasis(xi) = 1/4*[
    -(1-xi[2])    (1-xi[2])   (1+xi[2])  -(1+xi[2])
    -(1-xi[1])   -(1+xi[1])   (1+xi[1])   (1-xi[1])]

N = Basis(basis, dbasis)


Out[32]:
JuliaFEM.Basis(basis,dbasis)

In [33]:
N([0.0, 0.0])


Out[33]:
1x4 Array{Float64,2}:
 0.25  0.25  0.25  0.25

Interpolation in time domain

To interpolate in time domain, call DiscreteField given time. Result is an Increment interpolated to that time. Here we interpolate the position of particle moving $y = \frac{1}{2}t^2$ at time $t=1.0$.


In [34]:
t = linspace(0, 2, 5)
y = 1/2*t.^2
t, y


Out[34]:
(linspace(0.0,2.0,5),[0.0,0.125,0.5,1.125,2.0])

In [35]:
timesteps = TimeStep[]
for (ti, yi) in zip(t, y)
    increment = Increment(yi)
    push!(timesteps, TimeStep(ti, increment))
end

#fs["particle position"] = t, 1/2*t.^2
pos = Field(timesteps)
pos(1.0)


Out[35]:
1-element JuliaFEM.Increment{Float64}:
 0.5

It's also possible to take time derivatives using finite difference approximation. To do so, call Field with time and additional argument Val{:diff}. Again, same example:


In [36]:
velocity = pos(1.0, Val{:diff})


Out[36]:
1-element Array{Float64,1}:
 1.0

Interpolation in spatial domain

To interpolate in spatial domain, call Basis with Increment and coordinate $\boldsymbol\xi$. Increments to interpolate are the latest ones in each time step by default. Result depends from the content of the field. If it is scalar field, result will be scalar, if it's vector the result will be vector and so on.

Let's have a $\Omega = \left[0,1\right]\times\left[0,1\right] \in \mathbb{R}^2$ domain with a displacement field \begin{equation} \mathbf{u}\left(X_1, X_2\right)=t\begin{bmatrix}X_{1}\left(X_{2}+1\right)\\ X_{1}\left(4X_{2}-1\right) \end{bmatrix}. \end{equation}

Gradient is \begin{equation} \mbox{grad}\left(\mathbf{u}\right)=t\begin{bmatrix}X_{2}+1 & X_{1}\\ 4X_{2}-1 & 4X_{1} \end{bmatrix} \end{equation}

We look for a displacement in center point of the domain at time $t=1.0$:


In [37]:
X = Field(Vector{Float64}[[0.0,0.0], [1.0,0.0], [1.0,1.0], [0.0,1.0]])
u = Field(
    (0.5, Vector{Float64}[[0.0, 0.0], [0.5, -0.5], [1.0, 1.5], [0.0, 0.0]]),
    (1.5, Vector{Float64}[[0.0, 0.0], [1.5, -1.5], [3.0, 4.5], [0.0, 0.0]]));

In [38]:
# evaluate fields in time t=1.0 -> Increments
X_increment = X(1.0)
u_increment = u(1.0)
X_increment, u_increment


Out[38]:
([[0.0,0.0],[1.0,0.0],[1.0,1.0],[0.0,1.0]],Any[[0.0,0.0],[1.0,-1.0],[2.0,3.0],[0.0,0.0]])

In [39]:
# evaluate geometry + displacement at midpoint, basically x = X + u
N(X_increment, [0.0, 0.0]) + N(u_increment, [0.0, 0.0])


Out[39]:
2-element Array{Float64,1}:
 1.25
 1.0 

Calculating gradients is also possible, for scalar or vector fields, if passing additional argument Val{:grad}. Here's an example of gradient of vector field, i.e, $u_{i,j} = \frac{\partial u_i}{\partial X_j}$:


In [40]:
N = Basis(basis, dbasis)
gradu = N(X_increment, u_increment, [0.0, 0.0], Val{:grad})


Out[40]:
2x2 Array{Float64,2}:
 1.5  0.5
 1.0  2.0

One can of course combine the above results. In this case field must be first interpolated in time domain and after that in spatial domain. So calculating time derivative of small strain tensor $\epsilon$ is basically:


In [41]:
du = u(1.0, Val{:diff})
grad_du = N(X_increment, du, [0.0, 0.0], Val{:grad})
strain_rate = 1/2*(grad_du + grad_du')


Out[41]:
2x2 Array{Float64,2}:
 1.5   0.75
 0.75  2.0 

Symbolic fields

There's still one kind of field not yet introduced, namely symbolic fields:


In [42]:
u = Field("displacement")


Out[42]:
JuliaFEM.SymbolicField("displacement")

The idea is to write equation in expression form using tiny symbolic interface and evaluate equations afterwards. Main benefits:

  • possibility to simplify equations symbolically
  • prettier syntax
  • "lazy evaluation" gives possibilities to optimize performance.

Here's a preliminary example how this could work:


In [43]:
# in future this will be something like this:
# u = Field("displacement")
# eps = 1/2*(grad(u) + grad(u)')
# strain_rate = diff(eps)

In [44]:
# now this is
expr = JuliaFEM.Expression("1/2*(grad(diff(displacement)) + grad(diff(displacement))')")
print(expr.expr)


(1 / 2) * (grad(diff(displacement)) + grad(diff(displacement))')

Evaluation of expression: call Basis with FieldSet, $\boldsymbol\xi$ and time as arguments:


In [45]:
X = Field(Vector[[0.0, 0.0], [1.0, 0.0], [1.0, 1.0], [0.0, 1.0]])
u = Field(
    (0.5, Vector[[0.0, 0.0], [0.5, -0.5], [1.0, 1.5], [0.0, 0.0]]),
    (1.5, Vector[[0.0, 0.0], [1.5, -1.5], [3.0, 4.5], [0.0, 0.0]]))
fs = FieldSet()
fs["geometry"] = X
fs["displacement"] = u
xi = [0.0, 0.0]
time = 1.0
result = N(expr, fs, xi, time);


INFO: expression = (1 / 2) * (grad(diff(displacement)) + grad(diff(displacement))')
INFO: expression = 1 / 2
INFO: new expression: 1 / 2
INFO: expression = grad(diff(displacement)) + grad(diff(displacement))'
INFO: expression = grad(diff(displacement))
INFO: inside gradient operator
INFO: gradient args: Any[:grad,:(diff(displacement))]
INFO: expression inside gradient
INFO: grad: evaluate field diff(displacement)
INFO: taking time derivative of displacement
INFO: expression = diff(displacement)
INFO: inside diff operator
INFO: data = Any[[0.0,0.0],[1.0,-1.0],[2.0,3.0],[0.0,0.0]]
INFO: evaluate geometry
INFO: evaluate gradient
INFO: expression = grad(diff(displacement))'
INFO: transpose
INFO: expression = transpose(grad(diff(displacement)))
INFO: expression = grad(diff(displacement))
INFO: inside gradient operator
INFO: gradient args: Any[:grad,:(diff(displacement))]
INFO: expression inside gradient
INFO: evaluate gradient
INFO: new expression: transpose([1.5 0.5
 1.0 2.0])
INFO: new expression: [1.5 0.5
 1.0 2.0] + transpose([1.5 0.5
 1.0 2.0])
INFO: new expression: (1 / 2) * ([1.5 0.5
 1.0 2.0] + transpose([1.5 0.5
 1.0 2.0]))

In [46]:
result


Out[46]:
:((1 / 2) * (2x2 Array{Float64,2}:
 1.5  0.5
 1.0  2.0 + transpose(2x2 Array{Float64,2}:
 1.5  0.5
 1.0  2.0)))

What happened is that fields were automatically interpolated and substituted to the equation. The result is Expr and can be evaluated like usual:


In [47]:
strain_rate = eval(result)


Out[47]:
2x2 Array{Float64,2}:
 1.5   0.75
 0.75  2.0