Veamy: an extensible object-oriented C++ library for

0 downloads 0 Views 2MB Size Report
Aug 11, 2017 - This paper summarizes the development of an object-oriented C++ library for ... C++ library, named Veamy, that implements the virtual element ...
Veamy: an extensible object-oriented C++ library for the virtual element method A. Ortiz-Bernardina,∗, C. Alvareza,b , N. Hitschfeld-Kahlerb , A. Russoc,d , R. Silva-Valenzuelaa , E. Olate-Sanzanaa a

Department of Mechanical Engineering, Universidad de Chile, Av. Beauchef 851, Santiago 8370456, Chile. b Department of Computer Science (DCC), Universidad de Chile, Av. Beauchef 851, Santiago 8370456, Chile. c Dipartimento di Matematica e Applicazioni, Universit` a di Milano-Bicocca, 20153 Milano, Italy. d Istituto di Matematica Applicata e Tecnologie Informatiche del CNR, via Ferrata 1, 27100 Pavia, Italy.

Abstract This paper summarizes the development of an object-oriented C++ library for the virtual element method (VEM) named Veamy, whose modular design is focused on its extensibility. The two-dimensional linear elastostatic problem has been chosen as the starting stage for the development of this library. In contrast to the standard finite element method, the VEM in two dimensions uses polygonal finite element meshes. The theory of the VEM in which Veamy is based upon is presented using a notation and a terminology that is commonly found in the finite element literature, thereby allowing potential users that are familiar with finite elements to understand and implement the virtual element method under the object-oriented paradigm. A complete sample usage of Veamy is provided for a cantilever beam subjected to a parabolic end load. A displacement patch test is also solved using Veamy. A third example features the interaction between Veamy and the polygonal mesh generator ∗

Corresponding author. Tel: +56 (2) 297 846 64, Fax: +56 (2) 268 960 57, Email address: [email protected] (A. Ortiz-Bernardin)

August 11, 2017

PolyMesher. Step-by-step guidelines for the implementation of a problem that is currently not available in Veamy (the two-dimensional Poisson problem) are also provided. The source code is made freely available so that interested users can make free use of it, and possibly, extend Veamy to a wider class of problems. Keywords: virtual element method, polygonal finite elements, object-oriented programming, C++ 1. Introduction When a boundary-value problem, such as the linear elastostatic problem, is solved using the weak formulation, the trial and test displacements therein are replaced by their discrete values, which adopt the form of h

v (x) =

N X

φa (x)va ,

(1)

a=1

where φa (x) are nodal basis functions, va = [v1a

v2a ]T are nodal displacements in

the Cartesian coordinate system and N is the number of nodes that define an element. In this paper, we consider basis functions that reproduce linear fields, which is the lowest possible order in the Galerkin weak formulation of the linear elastostatic problem. Due to the nature of some basis functions, these discrete trial and test displacement fields may represent linear fields plus some additional non-polynomial or higher-order terms. Such additional terms cause inhomogeneous deformations, and when present they may introduce integration errors in the numerical integration of the stiffness matrix leading to stability issues that affect the convergence of the approximation method. This is the case of polygonal and polyhedral finite element methods [1–3], and meshfree Galerkin methods [4–14]. The virtual element method [15] (VEM) has been presented to deal with these integration issues. In short, the method consists in the construction of an algebraic 2

(exact) representation of the stiffness matrix without the explicit evaluation of basis functions (basis functions are virtual). In the VEM, the stiffness matrix is decomposed into two parts: a consistent matrix that guarantees the exact reproduction of a linear displacement field and a correction matrix that provides stability. Such a decomposition is formulated in the spirit of the Lax equivalence theorem (consistency + stability → convergence) for finite-difference schemes and is sufficient for the method to pass the patch test [16]. Recently, the virtual element framework has been used to correct integration errors in polygonal finite element methods [17–19] and in meshfree Galerkin methods [20]. In this paper, object-oriented programming concepts are adopted to develop a C++ library, named Veamy, that implements the virtual element method on polygonal meshes. This library uses Eigen [21] library for linear algebra and has a built-in polygonal mesher that is based on a Voronoi tessellation obtained from Triangle [22]. Moreover, Veamy is capable of interacting straightforwardly with PolyMesher [23], a polygonal mesh generator that is widely used in the VEM and polygonal finite elements communities. We have chosen the linear elastostatic problem as the starting stage for the development of this library. In presenting the theory of the VEM in which Veamy is built upon, we adopt a notation and a terminology that resemble the language of the finite element method in engineering analysis. The work of Gain et al. [17] is in line with this aim and has inspired most of the notation and terminology used in this paper. We try to complete the excellent work of Gain et al. by providing some additional details that are not explicitly given therein. To the best of our knowledge, Veamy is the first object-oriented C++ implementation of the virtual element method, and we make it publicly available so that interested users can make free use of it, and possibly, extend it to a wider class of problems. We believe that the understanding of the programming logic in Veamy is fundamental for its exten3

sion. With this in mind, we have included an appendix that provides step-by-step guidelines for the implementation of the two-dimensional Poisson problem, which is a problem that is currently not available in Veamy. The remainder of this paper is structured as follows. The governing equations and the weak formulation for the linear elastostatic problem are presented in Section 2. Section 3 presents the theoretical aspects of the VEM for the linear elastostatic problem and the construction of the VEM element matrices. The general structure of the object-oriented C++ VEM library, Veamy, is described and explained in Section 4. A complete sample usage of Veamy in linear elastostatics is provided in Section 5 along with a displacement patch test and a demonstration of the interaction between Veamy and PolyMesher. The paper ends with some concluding remarks in Section 6. 2. Model problem 2.1. Linear elastostatic boundary-value problem Consider an elastic body that occupies the open domain Ω ⊂ IR2 and is bounded by the one-dimensional surface Γ whose unit outward normal is n. The boundary is assumed to admit decompositions Γ = Γg ∪ Γh and ∅ = Γg ∩ Γh , where Γg is the Dirichlet boundary and Γh is the Neumann boundary. The closure of the domain is Ω ≡ Ω ∪ Γ . Let u(x) : Ω → IR2 be the displacement field at a point x of the elastic body when the body is subjected to external tractions h(x) : Γh → IR2 and body forces b(x) : Ω → IR2 . The imposed Dirichlet (essential) boundary conditions are g(x) : Γg → IR2 . The boundary-value problem for linear elastostatics is: find

4

u(x) : Ω → IR2 such that ∇ · σ + b = 0 ∀x ∈ Ω,

(2a)

∀x ∈ Γg ,

(2b)

σ · n = h ∀x ∈ Γh ,

(2c)

u=g

where σ is the Cauchy stress tensor. 2.2. Weak formulation When deriving the weak form of the linear elastostatic problem using the method of weighted residuals, with v being the arbitrary weighting function, the following expression for the bilinear form is obtained: Z a(u, v) = σ(u) : ∇v dx.

(3)



The gradient of the displacement field can be decomposed into its symmetric (∇S v) and skew-symmetric (∇AS v) parts, as follows: ∇(v) = ∇S v + ∇AS v = ε(v) + ω(v),

(4)

where  1 ∇v + ∇T v 2

∇S v = ε(v) = is known as the strain tensor, and ∇AS v = ω(v) =

 1 ∇v − ∇T v 2

(5)

(6)

is the skew-symmetric gradient tensor that represents rotations. However, because the stress tensor is symmetric its product with the skew-symmetric gradient tensor is zero, thereby simplifying the bilinear form to Z a(u, v) = σ(u) : ε(v) dx, Ω

5

(7)

which leads to the usual form of presenting the weak formulation: find u(x) ∈ U such that a(u, v) = ℓb (v) + ℓh (v) ∀v(x) ∈ V , Z a(u, v) = σ(u) : ε(v) dx, Ω Z Z b h ℓ (v) = b · v dx, ℓ (v) = h · v ds, Ω

(8a) (8b) (8c)

Γh

where U and V are the displacement trial and test spaces:  U := u(x) : u ∈ [W(Ω)]2 ⊆ [H 1 (Ω)]2 , u = g on Γg ,  V := v(x) : v ∈ [W(Ω)]2 ⊆ [H 1 (Ω)]2 , v = 0 on Γg ,

(9a) (9b)

where the space W(Ω) includes linear displacement fields. In order to fully understand the physical interpretations of VEM, we have to consider both expressions of the bilinear form. 3. The virtual element method In classical finite element methods, the domain of analysis is partitioned into non overlapping finite elements. In two dimensions, these elements are usually triangles and quadrilaterals. Here, we consider the possibility of using elements with arbitrary number of edges, that is, polygonal elements. So in essence, the partitioning of the domain will be achieved using a polygonal mesh generator. And for these polygonal elements, we will construct their stiffness matrices and nodal force vectors using the theory of the virtual element method. 3.1. The polygonal element Let the domain Ω be partitioned into non overlapping polygonal elements. A polygonal element is denoted by E and its area by |E|. The number of edges and 6

nodes (vertices) of a polygonal element are denoted by N. The boundary of the polygonal element is dS and the unit outward normal to this boundary is nE = [n1E

n2E ]T . Fig. 1 presents a schematic representation of a polygonal element for

N = 5. 11 00 00 11

1 0 0 1

a+1

ℓ11111 [nE ]a a 00000 00000 11111

E 1 0 0 1 0 1

11 00 00 11 00 a 11

ℓa−1 11 00 00 11

a−1

[nE ]a−1

Fig. 1: Schematic representation of a polygonal element of N = 5 edges.

3.2. Projection operators As in finite elements, for the numerical solution to converge monotonically it is required that the displacement approximation inside the polygonal element can represent rigid body modes and constant strain states. This requires that the displacement approximation inside the element is at least a linear polynomial [24]. We will define projection operators that allow the extraction of the rigid body modes, the constant strain modes and the linear polynomial part of the motion. In this exposition, the displacement approximation is assumed to be continuous within the element and across inter-element boundaries (i.e. no gaps or cracks open up), which is also a necessary condition for convergence [24]. 7

Consider the following three spaces: the space of rigid body motions (denoted by R), the space of constant strain states (denoted by C), and the space of linear displacements (denoted by P) that are able to represent rigid body motions and states of constant strains. Since polygonal elements can generally have more than three nodes, we have to consider the possibility that the displacement approximation within a polygonal element is composed of a linear polynomial part plus an additional1 non-polynomial or higher-order part, which implies that u(x) ∈ [W(E)]2 ⊇ [P(E)]2 . To extract the components of the displacement in the three aforementioned spaces, the following projections are defined: ΠR : [W(E)]2 → [R(E)]2 ,

ΠR r = r,

∀r ∈ [R(E)]2

(10)

ΠC c = c,

∀c ∈ [C(E)]2

(11)

∀p ∈ [P(E)]2

(12)

for extracting the rigid body motions, ΠC : [W(E)]2 → [C(E)]2 ,

for extracting the constant strain modes, and ΠP : [W(E)]2 → [P(E)]2 ,

ΠP p = p,

for extracting the linear polynomial part. These operators are required to satisfy the following orthogonality conditions:

1

ΠR c = 0,

∀c ∈ [C(E)]2

(13)

ΠC r = 0,

∀r ∈ [R(E)]2 ,

(14)

Just for the lowest order polygon, that is the three-node triangle, the approximation is composed

solely of the linear polynomial part. For a four-node quadrilateral finite element, a quadratic monomial exists, which is considered as the additional high-order term in the approximation.

8

so that elements of C have no rigid body motions and elements of R have no constant strain modes, which means ΠC ΠR = ΠR ΠC = 0 and ΠP = ΠR + ΠC .

(15)

In Appendix A, the orthogonality conditions (13) and (14) are verified. So, any u, v ∈ W(E) can be decomposed into three terms, as follows: u = ΠR u + ΠC u + (u − ΠP u),

(16a)

v = ΠR v + ΠC v + (v − ΠP v),

(16b)

that is, into a rigid body part, a constant strain part and the remaining nonpolynomial or higher-order terms. 3.3. Energy-orthogonality conditions The crucial point in the VEM is that the displacements (16) once replaced into the bilinear form (7) effectively leads to a split of the stiffness matrix that permits to isolate the non-polynomial terms and thus take control over their behavior. The following energy-orthogonality conditions are essentials to this aim. Consider the bilinear form (7) at the polygonal element level, which is denoted by aE (·, ·). The projection map ΠC is required to satisfy the following condition: aE (c, v − ΠC v) = 0 ∀c ∈ [C(E)]2 , v ∈ [W(E)]2 ,

(17)

which means that v − ΠC v is energetically orthogonal to C. In addition, the following energy-orthogonality condition extends from (17). The projection map ΠP satisfies aE (p, v − ΠP v) = 0 ∀p ∈ [P(E)]2 , v ∈ [W(E)]2 , 9

(18)

which means that v − ΠP v is energetically orthogonal to P. To prove this condition it is noted that rigid body motions have zero strain, that is, ε(r) = 0 ∀r ∈ [R(E)]2 and ε(ΠR v) = 0 (see Appendix A for the proof). So basically, any term involving rigid body modes r and ΠR v in the bilinear form is exactly zero. On using the foregoing observations, the energy-orthogonality condition (18) is proved as follows: Proof. aE (p, v − ΠP v) = aE (r + c, v − ΠR v − ΠC v) = aE (c, v − ΠC v) − aE (c, ΠR v) + aE (r, v − ΠR v − ΠC v) = aE (c, v − ΠC v) = 0, where (17) has been used in the last equality. A last observation is noted. Since p = r + c and aE (r, ·) = 0, the following energy-orthogonality condition emerges from (18): aE (c, v − ΠP v) = 0 ∀c ∈ [C(E)]2 , v ∈ [W(E)]2 .

(19)

3.4. The VEM bilinear and linear forms Substituting the VEM decomposition (16) into the bilinear form (7) leads to the following split of the bilinear form at element level: aE (v, v) = aE (ΠR v + ΠC v + (v − ΠP v), ΠR v + ΠC v + (v − ΠP v)) = aE (ΠC v, ΠC v) + aE (v − ΠP v, v − ΠP v),

(20)

where the symmetry of the bilinear form, the fact that ΠR v and ΠR u do not contribute in the bilinear form (both have zero strain), and the energy-orthogonality condition (19) have been invoked. 10

The first term on the right-hand side of the second line of (20) is the bilinear form associated with the constant strain modes that provides consistency (it leads to the consistency stiffness) and the second term is the bilinear form associated with the non-polynomial or higher-order terms that provides stability (it leads to the stability stiffness). The VEM linear form is obtained upon substitution of the VEM decomposition (16) into (8c) at element level, as follows: ℓE (v) = ℓbE (ΠR v + ΠC v + (v − ΠP v)) + ℓhE (ΠR v + ΠC v + (v − ΠP v)).

(21)

3.5. An explicit form of the VEM projection operators In the preceding section, the projection operators were required to satisfy certain energy-orthogonality conditions. In this section, we use the energy-orthogonality condition (18) to derive the explicit forms of the projection operators. In order to obtain an explicit form for ΠR , it is essential to have the rigid body modes present. That is, it is necessary to consider the bilinear form given in (3) as that expression has the skew-symmetric gradient tensor that represents the rotations of the rigid modes (see (4)). It is noted that σ(p) and ∇(ΠP v) are both constant fields since p, ΠP v ∈ [P(E)]2 and thus Z aE (p, v − ΠP v) = σ(p) : ∇(v − ΠP v) dx E  Z Z ∇v dx − ∇(ΠP v) dx = σ(p) : E E  Z = σ(p) : ∇v dx − ∇(ΠP v)|E| .

(22)

E

And since (22) is required to be exactly zero, it leads to Z 1 ∇(ΠP v) = ∇v dx. |E| E 11

(23)

On using (4), Eq. (23) can be further developed, as follows: Z Z 1 1 ε(v) dx + ω(v) dx, ∇(ΠP v) = |E| E |E| E

(24)

where it is shown that ∇(ΠP v) is equal to the sum of the volume-average of the strain tensor, which we denote as εb(v), and the volume-average of the skew-symmetric gra-

b dient tensor, which we denote as ω(v), respectively. The actual computation of these two volume-averaged tensors is performed on their surface integral representation, as follows:

and

Z

1 ε(v) dx = 2|E| E

Z

Z

1 ω(v) dx = 2|E| E

Z

1 εb(v) = |E| 1 b ω(v) = |E|

(v ⊗ nE + nE ⊗ v) ds

(25)

(v ⊗ nE − nE ⊗ v) ds.

(26)

∂E

∂E

Then from (24), ΠP v must have the following form: b ΠP v = εb(v) · x + ω(v) · x + a0 .

(27)

And since a0 is a constant, this means that the projection aE (p, v − ΠP v) = 0 defines ΠP v only up to a constant. Thus, to find a0 we need a projection operator onto constants such that Π0 (v − ΠP v) = 0 or Π0 (ΠP v) = Π0 v. In Ref. [25], a general expression for Π0 v is chosen as Z 1 b(x) = v(x) dx. Π0 v = v |E| E

(28)

(29)

If the centroid of the polygon is joined by line segments to the midpoints of the

edges of the polygon, a representative nodal area (denoted by |E|J ) results as the 12

region formed by a node (vertex), the midpoint of edges incident on the node, and the centroid of the polygon. With this, the volume integral in (29) can be computed approximately using nodal quadrature with |E|J as the nodal weight. In fact, this is a typical approach for linear fields in the VEM literature [15, 17, 18, 25, 26] and leads to Π0 v ≈

N N 1 X 1 X v(xJ )|E|J = v(xJ ) = v, |E| J=1 N J=1

(30)

where N is the number of nodes (vertices) of coordinates xJ that define the polygonal element. Therefore, Eq. (30) is the mean value of v over the vertices of the polygonal element. In Appendix A, it is demonstrated that (30) satisfies the orthogonality condition (28). Applying (28) to (27) gives b Π0 (ΠP v) = εb(v) · Π0 x + ω(v) · Π0 x + Π0 a0 = Π0 v.

(31)

Due to (30), Π0 a0 = a0 , Π0 v = v and Π0 x = x (x is equal to the centroid of the polygonal element), which after substituting them into (31) and solving for a0 gives b a0 = v − εb(v) · x − ω(v) · x.

(32)

b · (x − x) + v. ΠP v = εb(v) · (x − x) + ω(v)

(33)

Finally, substituting (32) into (27) yields

It is immediate from (33) that the projection of v onto the space of rigid body motions is given by the sum of the rotation mode and the translation mode, respectively, as follows: b (v) · (x − x) + v, ΠR v = ω

(34)

ΠC v = εb(v) · (x − x).

(35)

and that the projection of v onto the space of constant strain states is

13

In Appendix A, it is demonstrated that (35) satisfies (17). 3.6. Another form of the VEM projection operators At this point we have obtained three important VEM projection operators. A projection operator onto the space of constant strain modes, a projection operator onto the space of rigid body modes, and the sum of both that defines a third projection operator onto the space of linear displacements. In this section, an alternative form of the VEM projection operators are developed in terms of their space basis. These derivations are very important for the construction of the VEM element matrices. Thus, we need to find the basis for the space of rigid body motions and the basis for the space of constant strain states. These can be derived from the expressions given in (34) and (35). To this end, conb ≡ ω(v) b 2. sider the two-dimensional Cartesian space and the skew-symmetry of ω

The projection operator (34) can be written, as follows:   v 1 + (x2 − x2 )b ω12 ΠR v = v2 − (x1 − x1 )b ω12    v1  1 0 (x2 − x2 )  = v2  0 1 −(x1 − x1 ) ω b12       1 0 (x2 − x2 ) = ω b12 v1 + v2 + 0 −(x1 − x1 ) 1 = r1 v 1 + r2 v 2 + r3 ω b12 .

(36)

Thus, the basis for the space of rigid body modes is: r1 = 2



1 0

T

, r2 =



0 1

T

, r3 =

Note that ω b11 = ω b22 = 0 and ω b21 = −b ω12 .

14



(x2 − x2 ) −(x1 − x1 )

T

.

(37)

Similarly, on considering the symmetry of εb ≡ εb(v), the projection operator (35)

can be written as

ΠC v =



ε11 + (x2 − x2 )b ε12 (x1 − x1 )b (x1 − x1 )b ε12 + (x2 − x2 )b ε22



 εb11 0 (x2 − x2 )  (x1 − x1 ) εb22  = 0 (x2 − x2 ) (x1 − x1 ) εb12       (x2 − x2 ) (x1 − x1 ) 0 εb + εb = εb11 + (x2 − x2 ) 22 0 (x1 − x1 ) 12 





= c1 εb11 + c2 εb22 + c3 εb12 .

(38)

Thus, the basis for the space of constant strain states is: c1 =



(x1 − x1 ) 0

T

, c2 =



0 (x2 − x2 )

T

, c3 =



(x2 − x2 ) (x1 − x1 )

T

.

(39)

3.7. Projection matrices On each polygonal element of N edges, the discrete trial and test displacements are computed as uh (x) =

N X

φa (x)ua ,

v h (x) =

a=1

N X

φb (x)vb ,

b=1

where φa (x) and φb (x) are nodal basis functions, and ua = [u1a [v1b

(40) u2a ]T and vb =

v2b ]T are nodal displacements in the Cartesian coordinate system. The nodal

basis functions are also used for the discretization of the components of the basis for the space of rigid body motions: rαh (x) =

N X

φa (x)rα (xa ),

a=1

15

α = 1, . . . , 3

(41)

and the components of the basis for the space of constant strain modes: chβ (x)

=

N X

φa (x)cβ (xa ),

β = 1, . . . , 3.

(42)

a=1

The discrete version of the projection map to extract the rigid body motions is obtained by substituting (40) and (41) into (36), which yields          (x2a − x2 ) 0 1  φa φa ΠR v h ab = φa 1 0 −(x1a − x1 ) =



φa 0 0 φa





1 0 (x2a − x2 ) 0 1 −(x1a − x1 )



φb 0  0 φb q2b −q1b

 φb v1b  φb v2b q2b v1b − q1b v2b    v 1b  , (43) v2b

where

Z 1 φa niE ds, i = 1, 2 (44) qia = 2|E| ∂E appeared because of the discretization of ω b12 (see (26)). The matrix form of (43) is

obtained by expanding the nodal indexes, as follows: h

ΠR v =

N X N X

ΠR v h

a=1 b=1



ab

= NPR q,

where N = [(N)1

and

 q = v1T

···

(N)a

···

(N)N ] ; (N)a =

vaT

···

T vN

···

T

; va = [v1a

(45) 

φa 0 0 φa



,

v2a ]T

PR = HR W T R with



 (HR )1   ..   .    HR =   (HR )a  ,   ..   . (HR )N

T 1 0  (HR )a =  0 1 (x2a − x2 ) −(x1a − x1 ) 

16

(46) (47)

(48)

(49)

and



 (WR )1   ..   .    WR =  (WR )a  ,   ..   . (WR )N

T φa 0 (WR )a =  0 φa  . q2a −q1a 

(50)

Similarly, on substituting (40) and (42) into (38) leads to the following discrete version of the projection map to extract the constant strain modes:         (x2a − x2 ) (x1a − x1 ) 0 h φa ΠC v ab = φa φa (x2a − x2 ) 0 (x1a − x1 )   2q1b v1b  × 2q2b v2b q2b v1b + q1b v2b      2q1b 0 0 (x2a − x2 )  (x1a − x1 ) φa 0 = 0 2q2b  0 (x2a − x2 ) (x1a − x1 ) 0 φa q2b q1b   v1b . (51) × v2b The matrix form of (51) is obtained by expanding the nodal indexes, as follows: h

ΠC v =

N X N X

ΠC v h

a=1 b=1



ab

= NPC q,

(52)

where PC = HC WT C with

 (HC )1   ..   .    HC =   (HC )a  ,   ..   . (HC )N 



T 0 (x1a − x1 ) (HC )a =  0 (x2a − x2 )  (x2a − x2 ) (x1a − x1 )

17

(53)

(54)

and



 (WC )1    T ..   2q1a 0 .     0 2q2a  . WC =   (WC )a  , (WC )a =   .. q2a q1a   . (WC )N

(55)

The matrix form of the projection to extract the polynomial part of the displacement field is then PP = PR + PC . In order to develop the consistency stiffness matrix, it is useful to have the following alternative expression for the discrete projection map to extract the constant strain modes: h

ΠC v = c1

N X

2q1b v1b + c2

b=1

=

N X 



2q2b v2b + c3

b=1

N X

(q2b v1b + q1b v2b )

b=1

2q1b c1 + q2b c3 2q2b c2 + q1b c3

b=1

=

N X



v1b v2b



   2q 0 1b  v 1b  0 2q2b  c3 v2b b=1 q2b q1b N X

c1 c2





= c WT C q.

(56)

Finally, the discrete version of the projection map onto constants given in (30) is computed as follows: Π0 v

h



a

  N  1 X φa (xI ) v1a 0 = v2a 0 φa (xI ) N I=1    v1a φa 0 . = v2a 0 φa

(57)

The matrix form of (57) is obtained by expanding the nodal indexes as h

Π0 v =

N X

Π0 v h

a=1

18



a

= Nq,

(58)

where 

N = (N)1

···

(N)a

···



(N)N ; (N)a =



φa 0 0 φa



.

(59)

In the VEM, the basis functions are chosen such that they are piecewise linear on the edges of the polygon and have the Kronecker-delta property, which permits the following simpler computations: N 1 X 1 φa = φa (xJ ) = , N J=1 N

(60)

where (30) has been used, and the exact evaluation of the integral over the faces of the element (see (44)), as follows [18, 25]: Z 1 φa niE ds = (ℓa−1 [niE ]a−1 + ℓa [niE ]a ) , 2 ∂E

i = 1, 2,

(61)

where ℓa−1 and ℓa are the lengths of the two edges incident to node a (see Fig. 1). 3.8. VEM element stiffness matrix On using the above discrete operators to discretize the VEM bilinear form given in (20), yields T aE (uh , v h ) = aE (c WT C d, c WC q) + aE (N d − N PP d, N q − N PP q) T T T = qT WC aE (cT , c) WT C d + q (I2N − PP ) aE (N , N) (I2N − PP ) d T T = qT |E| WC D WT C d + q (I2N − PP ) KE (I2N − PP ) d,

(62)

where I2N is the identity (2N × 2N) matrix, d is the column vector of nodal displacements, and KE = aE (NT , N) is the standard stiffness matrix that arises from the discretization of (7) with the field approximation (40). In (62), we have used the identity aE (cT , c) = |E|D, which can be obtained if the strain is written in vector

19

format as ε(v) =



ε11 (v) ε22 (v) ε12 (v)

T

so that aE (u, v) =

R

E

εT (v)Dε(u) dx,

where D is the constitutive matrix for an isotropic linear elastic material given by   1−ν ν 0 E  ν  D= 1−ν 0 (63) (1 + ν)(1 − 2ν) 0 0 2(1 − 2ν) for plane strain condition, and 

 1 ν 0 E  ν 1 D= 0 (1 − ν 2 ) 0 0 2(1 − ν)

(64)

for plane stress condition, where E is the Young’s modulus and ν is the Poisson’s ratio. Now, it is easy to verify that ε(c) = I3 (the identity (3×3) matrix) leading to R aE (cT , c) = D E dx = |E|D. The standard stiffness KE is in practice replaced by an approximate stiffness

denoted by SE . Thus, the final expression for the stiffness matrix associated with the polygonal element can be written as the summation of the consistency stiffness and the stability stiffness, respectively, as follows: T KE = |E| WC D WT C + (I2N − PP ) SE (I2N − PP ).

(65)

There, any choice for SE that leads to a stability stiffness that is symmetric positive definite and scales like the exact bilinear form a(·, ·) is sufficient [15]. Herein, we adopt SE given as [17] SE = αE I2N ,

αE = γ

|E|trace(D) , trace(HT C HC )

(66)

where αE is the scaling parameter and γ is typically set to 1. Note that under the VEM framework, the polygonal finite element basis functions are known on the boundary of the element, which means that (60) and (61) are 20

used. These along with the nodal coordinates xa , the centroid x and area |E| of the polygonal element, and the material matrix D, permit the complete evaluation T of the matrices WC , PP = HR WT R + HC WC , and SE , and therefore of the VEM

stiffness (65). In this process, basis functions are not computed explicitly (they are virtual ) and quadrature errors are avoided since the only integral that appears in the formulation (Eq. (61)) is exactly computed. 3.9. VEM element body force vector The body force vector is given by the first term on the right-hand side of (21). For a constant body force vector, we write Z   b h h h h h ℓE (v ) = b · ΠR v + ΠC v + (v − ΠP v ) dx E Z  Z h h h =b· ΠP v dx + (v − ΠP v ) dx E E   = b · |E|Π0(ΠP v h ) + |E|Π0 (v h − ΠP v h ) = |E| b · Π0 v h T

= qT |E| N b,

(67)

where the projection Π0 has been used as given in (28)-(30). The element body force vector is then given by T

f bE = |E| N b.

(68)

3.10. VEM element traction force vector The traction force vector is given by the second term on the right-hand side of (21). It is similar to the element body force vector but the integral is one dimension lower. Therefore, on considering the polygonal edge as a one dimensional element,

21

the element traction force vector can be computed similarly to the element body force vector, as follows: T

f hE = |e| NΓ h, where |e| is the measure of the edge (e.g., its length) and    1    1 1 1 0 0 0 0 φ1 0 φ2 0 NΓ = = N 1 N 1 = 2 1 2 1 0 N 0 N 0 2 0 2 0 φ1 0 φ2

(69)

(70)

is a matrix related to the two nodes that define the loaded edge of the polygonal element. 4. Object-oriented implementation of VEM in C++ In this section, we introduce Veamy, a library that implements the VEM for the two-dimensional linear elastostatic problem using object-oriented programming in C++. In Veamy, entities such as elements, degrees of freedom, constraints, among others, are represented by C++ classes. A UML diagram showing the core design of Veamy is presented in Fig. 2. Veamy uses the following external libraries: • Triangle [22], which is used to generate a Delaunay triangulation that is later used to construct a Voronoi-based polygonal mesh. • Clipper [27], an open source freeware library for clipping and offsetting lines and polygons. • Eigen [21], a C++ template library for linear algebra. Veamy is free software and is available to be downloaded from its project website [28]. 22

Fig. 2: UML diagram for the Veamy library.

23

4.1. Overview of C++ classes in the Veamy library 4.1.1. Veamer The class named Veamer, a fantasy denomination stemming from the words VEM and beam, is in charge of receiving the problem conditions, including the polygonal mesh, the material parameters of the linear elastic solid, and the boundary conditions. Veamer initializes the Elements and DOFs structures, and keeps them as class members delegating to them most of the work. Veamer receives a polygonal mesh created by an internal mesh generator included in the library. This mesher constructs a Voronoi-based polygonal mesh from a Delaunay triangulation that is obtained from Triangle [22], and provides all the geometric entities necessary for VEM computations, such as polygon, vertex points3 and segments that define the sides of the polygon. The Veamer class definition is shown in Listing 1. Listing 1: Veamer Class. 1

class Veamer {

2

protected:

3

ProblemConditions conditions;

4

UniqueList points; virtual void createElement(Polygon p);

5 6

public:

7

DOFS DOFs;

8

std::vector elements;

9

void initProblem(PolygonalMesh m, ProblemConditions conditions); Eigen::VectorXd simulate(PolygonalMesh &mesh);

10 11

};

In the definition of the Veamer class, UniqueList is a custom data structure that contains a STL vector that keeps generic data types (in this case elements of 3

In this library, a vertex point is the equivalent to node in finite elements.

24

type Point) and in which a logarithmic time check is implemented to ensure that duplicated data types are not inserted on the list. 4.1.2. DOF DOF, is a class that represents the degrees of freedom associated with the nodal points. In the two-dimensional linear elastostatic problem, each nodal point of the mesh has two DOF instances associated that represent the components of the nodal displacement in the coordinate axes of the two-dimensional Cartesian system. Since a nodal point is typically shared by various elements, to avoid memory overuse each DOF instance is kept only ones in a container class called DOFS, where the DOF instance is associated with its corresponding nodal point index. In addition, to facilitate the assembly of element matrices into global ones, DOFS also assigns each DOF instance a global index that relates it to a specific row in the global stiffness matrix and global force vector. The DOF class definition is shown in Listing 2. Listing 2: DOF Class. 1

class DOF {

2

protected:

3

int index;

4

int point_index;

5

DOF::Axis d;

6

public:

7

enum Axis {x, y};

8

DOF(int index, int point_index, DOF::Axis a);

9

int globalIndex() const; int pointIndex();

10

DOF::Axis getAxis();

11 12

};

25

4.1.3. ProblemConditions ProblemConditions is a container structure that keeps the boundary conditions, the body force field, and the material properties of the two-dimensional linear elastic solid. ProblemConditions contains instances of three classes: Material, BodyForce and ConstraintsContainer. 4.1.4. Material The current object-oriented VEM implementation considers a unique material definition for the entire domain, hence, we choose to represent the material properties as a standalone class named Material. It contains the Young’s modulus (E) and the Poisson’s ratio (ν), which are used to define the material constitutive matrix. Material can be constructed either giving the material parameters E and ν, or selecting one of the predefined materials that come inside the library. 4.1.5. BodyForce BodyForce is an interface that represents the body forces that appear on the righthand side of the weak form equation. The BodyForce interface has three methods: applyX, applyY and computeVector. The first two are pure virtual functions that are meant to define a user-custom body force in both axis, and as such they must be implemented in a derived class. Both apply methods receive two real numbers representing the x- and y-coordinate of an evaluation point, and return a single real. On the other hand, computeVector method is also a pure virtual function but, differently from applyX and applyY, it is not expected to be implemented by the user, but rather is defined in two BodyForce subclasses: ConstantBodyForce and VariableBodyForce, which are the ones the user needs to inherit from. ConstantBodyForce is used for the computation of the body force vector when the body force is a constant field, which permits the use of the exact integration 26

presented in Section 3.9. VariableBodyForce is used when the body force is not constant, in which case it may be computed using a nodal quadrature like the one presented in Gain et al. [17]. The library includes one predefined zero body force implementation called None, which is used by default in absence of a user-custom body force function. Listing 3 presents the interface definition for BodyForce, ConstantBodyForce and VariableBodyForce. Listing 3: BodyForce Interface. 1

class BodyForce{

2

public:

3

virtual double applyX(double x, double y) = 0;

4

virtual double applyY(double x, double y) = 0;

5

virtual Eigen::VectorXd computeVector(Polygon p, std::vector points) = 0;

6

};

7 8

class ConstantBodyForce : public BodyForce{

9

public: Eigen::VectorXd computeVector(Polygon p, std::vector points) override {

10 11

auto* vector = new BodyForceVector;

12

Eigen::VectorXd value = vector->computeConstantForceVector(this, p, points);

13

delete vector;

14

return value; };

15 16

};

17 18

class VariableBodyForce: public BodyForce {

19

public: Eigen::VectorXd computeVector(Polygon p, std::vector points) override {

20 21

auto* vector = new BodyForceVector;

22

Eigen::VectorXd value = vector->computeVariableForceVector(this, p, points);

23

delete vector; return value;

24

};

25 26

};

27

4.1.6. EssentialConstraints and NaturalConstraints In the two-dimensional linear elastostatic problem, the essential (Dirichlet) and natural (Neumann) boundary conditions are applied on a number of boundary segments. The essential boundary conditions are then automatically applied to the corresponding nodes that define these segments. The boundary conditions are grouped according to two parameters: their value and their kind (essential or natural). The former group is represented by a class called Constraint and two subclasses, named SegmentConstraint and PointConstraint, that contain a single value. This single value can be the outcome of imposing a constant value, which is represented by an instance of a class named Constant, or the outcome of a function, which is represented by an instance of a class named Function. Also, the class Constraint stores the direction in the Cartesian axis, while SegmentConstraint and PointConstraint keep the segments and points where the constraint is applied, respectively. On the other hand, to assign a type to each constraint, two container classes called NaturalConstraints and EssentialConstraints are created as instances inside a container class named ConstraintsContainer. To avoid code duplication we also include an abstract class named Constraints, which implements the operations that are common to both NaturalConstraints and EssentialConstraints. Aside from providing a way of grouping the constraints, ConstraintContainer is also in charge of relating the constraints to the DOFs of the points defining the segments where the constraints are applied. For this purpose, when requested, the addConstrainedDOF method adds them to either the NaturalConstraints or EssentialConstraints. NaturalConstraints is also the responsible for creating the element traction force vector. This is done by its method boundaryVector.

28

4.1.7. BodyForceVector BodyForceVector is a small helper class that is in charge of computing the Body Force Vector in both the constant and non constant cases; to make the difference, this class has two methods, computeConstantForceVector and computeVariableForceVector, which are called by ConstantBodyForce and VariableBodyForce, respectively. 4.1.8. Element This class defines a single element. It contains as class field a list of the indexes of its DOFs, its element stiffness matrix, its element body force vector, its element traction force vector, and the polygon this element represents. Element is in charge of: 1. creating the DOFs associated with the element. 2. computing the element stiffness matrix using the computeK method 3. computing the external force vector, which is the sum of the element body force vector and the element traction force vector. This operation is performed by computeF method. 4. assembling the element stiffness matrix and the element force vectors into the global stiffness matrix and the global force vector, respectively. This operation is performed by assemble method. As an example, in Listing 4 it is shown the implementation of the VEM element stiffness matrix using the formulae presented in Section 3.8. Listing 4: Computation of element stiffness matrix (computeK method). 1

std::vector polygonPoints = p.getPoints();

2

int n = (int) polygonPoints.size();

29

3

Point average = p.getAverage(points.getList());

4

double area = p.getArea();

5 6

Eigen::MatrixXd Hr, Wr, Hc, Wc;

7

Hr = Eigen::MatrixXd::Zero(2*n, 3);

8

Wr = Eigen::MatrixXd::Zero(2*n, 3);

9

Hc = Eigen::MatrixXd::Zero(2*n, 3);

10

Wc = Eigen::MatrixXd::Zero(2*n, 3);

11 12

for(int vertex_id=0; vertex_idgetGamma()*alphaS*I;

62 63

this->K = area*Wc*D*Wc.transpose() + (I - Pp).transpose()*Se*(I - Pp);

4.2. Displacements computation Each simulation is represented by a single Veamer instance, which is in charge of guiding the VEM simulation through the simulate method shown in Listing 5. The resulting system of linear equations that leads to the vector of nodal displacements is solved using appropriate solvers available in the Eigen [21] library for linear algebra. Listing 5: simulate method in the Veamer class. 1

Eigen::MatrixXd K, Ks;

2

Eigen::VectorXd f;

3

int n = this->DOFs.size();

4

K = Eigen::MatrixXd::Zero(n,n);

5

Ks = Eigen::MatrixXd::Zero(n,n);

31

6

f = Eigen::VectorXd::Zero(n);

7 8

for(int i=0;ipoints, conditions);

10

elements[i].computeF(DOFs, this->points, conditions);

11

elements[i].assemble(DOFs, K, f);

12

}

13 14

EssentialConstraints essential = this->conditions.constraints.getEssentialConstraints ();

15

std::vector c = essential.getConstrainedDOF();

16 17

Eigen::VectorXd boundary_values = essential.getBoundaryValues(this->points.getList(), this->DOFs.

18

for (int j = 0; j < c.size(); ++j) {

getDOFS());

for (int i = 0; i < K.rows(); ++i) {

19 20

f(i) = f(i) - (K(i,c[j])*boundary_values(j));

21

K(c[j],i) = 0; K(i,c[j]) = 0;

22 23

}

24

K(c[j], c[j]) = 1;

25

f(c[j]) = boundary_values(j);

26

}

27 28

Eigen::VectorXd x = K.fullPivHouseholderQr().solve(f);

5. Sample usage 5.1. Cantilever beam subjected to a parabolic end load The VEM solution for the displacement field on a cantilever beam of unit thickness subjected to a parabolic end load P is computed using Veamy. Fig. 3 presents the geometry and boundary conditions. Plane strain condition is assumed. The essential boundary conditions on the clamped edge are applied according to the analytical

32

solution given by Timoshenko and Goodier [29]:   Py 3D 2 2 (6L − 3x)x + (2 + ν)y − (1 + ν) , ux = − 2 6EI  P 3νy 2(L − x) + (3L − x)x2 , uy = 6EI where E = E/ (1 − ν 2 ) with the Young’s modulus set to E = 1 × 107 psi, and ν = ν/ (1 − ν) with the Poisson’s ratio set to ν = 0.3; L = 8 in. is the length of the beam, D = 4 in. is the height of the beam, and I is the second-area moment of the beam section. The total load on the traction boundary is P = −1000 lbf.

y

D

P

x

L

Fig. 3: Model geometry and boundary conditions for the cantilever beam problem.

5.1.1. Setup file A main C++ file is written to setup the problem. This is the only file that needs to be written by the user in order to run the simulation in Veamy. As there are different aspects to consider, we divide the setup file in several blocks and explain each of them. Herein only the main parts of this setup file are described. 33

The complete setup file is provided in Appendix B. This setup file and some others are also available as part of a C++ test file located in the ‘test’ folder of Veamy’s source code [28]. Listing 6 shows the definition of the problem domain, the generation of base points for the Voronoi diagram, and the computation of the polygonal mesh. Listing 6: Geometry definition and mesh generation for the beam subjected to a parabolic end load. 1

std::vector rectangle4x8_points = {Point(0, -2), Point(8, -2), Point(8, 2), Point(0, 2)};

2

Region rectangle4x8(rectangle4x8_points);

3

rectangle4x8.generateSeedPoints(PointGenerator(functions::constantAlternating(), functions:: constant()), 24, 12);

4 5

std::vector seeds = rectangle4x8.getSeedPoints();

6

TriangleMeshGenerator meshGenerator = TriangleMeshGenerator (seeds, rectangle4x8);

7

PolygonalMesh mesh = meshGenerator.getMesh();

We proceed to initialize all the structures needed to represent the conditions of the problem at hand. In first place, we create a Constraint that represents the condition of the left side of the beam, including the segment it affects, the value taken by the DOFs and the direction in the Cartesian axis in which the constraint is applied. This implementation is shown in Listing 7. Listing 7: Definition of the essential boundary condition on the left side of the beam. 1

double uX(double x, double y){

2

double P = -1000;

3

double Ebar = 1e7/(1 - std::pow(0.3,2));

4

double vBar = 0.3/(1 - 0.3);

5

double D = 4; double L = 8; double I = std::pow(D,3)/12; return -P*y/(6*Ebar*I)*((6*L - 3*x)*x + (2+vBar)*std::pow(y,2) - 3*std::pow(D,2)/2*(1+vBar));

6 7

}

8 9

double uY(double x, double y){

34

10

double P = -1000;

11

double Ebar = 1e7/(1 - std::pow(0.3,2));

12

double vBar = 0.3/(1 - 0.3);

13

double D = 4; double L = 8; double I = std::pow(D,3)/12;

14

return P/(6*Ebar*I)*(3*vBar*std::pow(y,2)*(L-x) + (3*L-x)*std::pow(x,2));

15

}

16

EssentialConstraints essential;

17

Function* uXConstraint = new Function(uX);

18

Function* uYConstraint = new Function(uY);

19 20

PointSegment leftSide(Point(0,-2), Point(0,2));

21

SegmentConstraint const1 (leftSide, mesh.getPoints(), Constraint::Direction::Horizontal,

22

essential.addConstraint(const1, mesh.getPoints());

uXConstraint);

23 24

SegmentConstraint const2 (leftSide, mesh.getPoints(), Constraint::Direction::Vertical, uYConstraint);

25

essential.addConstraint(const2, mesh.getPoints());

Listing 8 presents the implementation of the natural boundary condition (the parabolic load) that is applied on the right side of the beam. The parabolic load is constructed using a function called tangential. Listing 8: Definition of the natural boundary condition on the right side of the beam. 1

double tangencial(double x, double y){

2

double P = -1000; double D = 4;

3

double I = std::pow(D,3)/12;

4

double value = std::pow(D,2)/4-std::pow(y,2); return P/(2*I)*value;

5 6

}

7

NaturalConstraints natural;

8

Function* tangencialLoad = new Function(tangencial);

9 10

PointSegment rightSide(Point(8,-2), Point(8,2));

11

SegmentConstraint const3 (rightSide, mesh.getPoints(), Constraint::Direction::Vertical,

12

natural.addConstraint(const3, mesh.getPoints());

tangencialLoad);

35

The NaturalConstraints and EssentialConstraints objects are inserted into an object of the ConstraintContainer class. An object of the Material class is created with the desired properties. The ConstraintContainer and Material objects are used to create a ProblemConditions object. The ProblemConditions object along with the mesh is used to create a Veamer instance that represents the system. Finally, to obtain the nodal displacements solution the simulate method is invoked. These instructions are presented in Listing 9. Listing 9: Computation of nodal displacements for the beam subjected to a parabolic end load. 1

ConstraintsContainer container;

2

container.addConstraints(essential, mesh);

3

container.addConstraints(natural, mesh);

4 5

Material m(1e7, 0.3);

6

ProblemConditions conditions(container, m);

7

v.initProblem(mesh, conditions);

8

Eigen::VectorXd x = v.simulate(mesh);

The output of the library is a vector that contains the nodal displacements solution. To plot the nodal displacements solution to an output file it suffices to include an extra line after the simulate call. This line is shown in Listing 10. This instruction generates a text file named as the string stored in displacementsFileName. The text file contains the computed displacements in the following format: vertex index, x-displacement and y-displacement. An extract of the output file generated for the beam subjected to a parabolic end load is shown in Listing 11. Listing 10: Plot nodal displacement solution to an output file. 1

v.writeDisplacements(displacementsFileName, x);

36

Listing 11: Extract of the output file for the beam subjected to a parabolic end load. x-displacement

y-displacement

2

0

9.38002e-005

-0.000100889

3

1

0.000137003

-0.000101589

4

2

9.30384e-005

-0.000115664

1

5

// Vertex index

...

The output file contains no information about the geometry of the problem. The geometry information is kept in the PolygonalMesh instance created at the beginning of this example. PolygonalMesh includes a way of printing its geometrical data to a text file with a single line of code, as shown in Listing 12. Listing 12: Print mesh to a text file. 1

mesh.printInFile(meshFileName);

The text file named as the string stored in meshFileName is arranged in the following format: • First line: Number of nodal points in the polygonal mesh. • Following lines: x-coordinate y-coordinate for each nodal point in the mesh. • One line: Number of segments in the polygonal mesh. • Following lines: index-of-start-point index-of-end-point for each segment in the polygonal mesh. • One line: Number of polygons in the polygonal mesh. • Following lines: number-of-vertices list-of-vertex-indexes centroid-x-coordinate centroid-y-coordinate for each polygon in the mesh. 37

5.1.2. Post processing Other than the output files mentioned in the preceding sections, Veamy does not provide a post processing interface. The user may opt to interface with a post processing interface of their choice. In the present example, we have chosen to visualize the displacement results using a Matlab function written for this purpose, which is also accessible from ‘lib/visualization’ folder of Veamy’s source code [28]. Fig. 4(a)-(c) presents the VEM solutions and Fig. 4(d) the norm of the exact solution. 5.1.3. Solution of the global system of equations The current implementation of the global VEM stiffness matrix in Veamy uses dense linear algebra solvers available in Eigen [21] library. The use of sparse solvers will be addressed in a future release of Veamy. The solution time in Veamy is dramatically affected by the solver selected. Herein, we compare the performance of various solvers that are offered by Eigen. As the computing time varies with the machine used to run the simulations, on a same machine we have normalized the computing time of different solvers with respect to the solver that performs the slowest (T /Tmax ). According to Eigen’s help resources, for a positive definite matrix a good choice is the solver based on the LDLT Cholesky decomposition. The performance is summarized in Table 1, where it is observed that the level of accuracy provided by the different solvers in this problem is the same and that in fact the LDLT is the best choice. 5.2. Patch test In this example, the polygonal mesh for the beam problem studied in Section 5.1 is used to test Veamy on a displacement patch test, which consists in the solution of the linear elastostatic problem with Dirichlet boundary conditions g = {x1

38

x1 + x2 }T

uhy

−4

x 10

4

8

4

3

6

3

2

4

2

1

2

1

0

0

−1

−2

−1

−2

−4

−2

−3

−6

−3

−4

−8

−4

0

2

4

x

6

y

y

uhx

−3

x 10 0

−0.5

−1

0

8

−1.5

−2

−2.5

0

2

4

x

(a)

6

8

(b)

h

||u ||

||u||

−3

x 10 3

4

4 3

−3

x 10 3

3

2.5

2.5

2

2 2

0

y

y

2

1

1 1.5

0

1.5

−1

−1 1

1 −2

−2 −3

−3

0.5

0.5

−4

−4 0

2

4

x

6

8

0

2

4

x

(c)

6

8

(d)

Fig. 4: Displacement field solution for the cantilever beam subjected to a parabolic end load. (a) VEM axial displacement, (b) VEM vertical displacement, (c) norm of the VEM displacement, and (d) norm of the exact displacement.

39

Table 1: Performance of various Eigen’s solvers in Veamy. T /Tmax

||uh −u|| ||u||

fullPivHouseholderQr()

1.0

0.0382651

fullPivLu()

0.8

0.0382651

partialPivLu()

0.2

0.0382651

ldlt()

0.1

0.0382651

Solver

applied along the entire boundary, and b = 0. Plane strain condition is assumed with the following material parameters: E = 1 × 107 psi and ν = 0.3. The complete setup file for this problem is provided in Appendix C. This setup file is also available as part of a C++ test file located in the ‘test’ folder of Veamy’s source code [28]. As predicted by the theory, the VEM solution coincides with the exact solution, which is given by g, within machine precision. The results are shown in Fig. 5, where the numerical error is shown in letter (d). 5.3. Using a PolyMesher mesh and boundary conditions in Veamy A polygonal mesher that is widely used in the VEM and polygonal finite element communities is PolyMesher [23]. PolyMesher delivers a polygonal mesh along with nodal displacement constraints and prescribed nodal loads. Veamy implements a function named initProblemFromFile that is able to read this mesh and boundary conditions and solve the problem straightforwardly. The reading of the PolyMesher mesh and boundary conditions is achieved by using a Matlab function named PolyMesher2Veamy that was especially designed to be 40

uhx

uhy

8

10

4

4 7

3

1

5

0

4

−1

2 6 1

y

2

y

8

3 6

0

4

−1

3

2 −2

−2

2

−3

−3

1

−4

0

−4 0

2

4

x

6

8

0

0

2

4

x

(a)

6

8

−2

(b)

h

h

||u ||

||u − u||

4

−13

x 10 1

4

12

0.9 3

3

0.8

10 2

2

y

0

y

8

1

6

−1 4

−2 −3

0.7

1

0.6

0

0.5

−1

0.4

−2

0.3 0.2

−3

2

−4

0.1

−4 0

2

4

x

6

8

0

(c)

2

4

x

6

8

0

(d)

Fig. 5: VEM solution for the displacement patch test using Veamy. (a) Axial displacement solution, (b) vertical displacement solution, (c) norm of the displacement solution, and (d) norm of the displacement error.

41

used within PolyMesher. Listing 13 presents the Matlab function PolyMesher2Veamy. Function PolyMesher2Veamy receives five PolyMesher’s data structures (Node, Element, NElem, Supp, Load) and writes a text file containing the mesh and boundary conditions that is readable in Veamy’s function initProblemFromFile. A main setup file consisting in few lines of code allows to run any simulation that uses a PolyMesher mesh and boundary conditions in Veamy. Listing 13:

Matlab function that allows the interaction between Veamy and

PolyMesher. 1

function PolyMesher2Veamy(Node,Element,NElem,Supp,Load)

2

fprintf(’Printing mesh to a Veamy mesh format\n’);

3

fid = fopen(’polymesher2veamy.txt’,’wt’);

4

NNode = size(Node,1);

5

fprintf(fid,’%d\n’,NNode);

6

for node_i = 1:NNode

7

fprintf(fid,’%d %d\n’,Node(node_i,1),Node(node_i,2));

8

end

9

fprintf(fid,’%d\n’,NElem);

10

NVertex = length(Element{el});

12

fprintf(fid,’%d ’,NVertex);

13

for vertex = 1:(NVertex-1) fprintf(fid,’%d ’,Element{el}(vertex));

14

16

end fprintf(fid,’%d\n’,Element{el}(NVertex));

17

end

18

NFix = size(Supp,1);

19

fprintf(fid,’%d\n’,NFix);

20

for fixnode = 1:NFix

21

%Nodal constraints ( 0 = fixed, 1 = free)

fprintf(fid,’%d %d %d\n’,Supp(fixnode,1),Supp(fixnode,2),Supp(fixnode,3));

22

end

23

NLoad = size(Load,1);

24

fprintf(fid,’%d\n’,NLoad);

25

for loadednode = 1:NLoad

26

%Element connectivity

for el = 1:NElem

11

15

%Nodal coordinates

%Nodal loads

fprintf(fid,’%d %d %d\n’,Load(loadednode,1),Load(loadednode,2),Load(loadednode,3));

42

27

end

As a demonstration of the potential that is offered to the simulation when Veamy interacts with PolyMesher, the MBB beam problem of Section 6.1 in Ref. [23] is considered. The MBB problem is shown in Fig. 6, where L = 3 in, D = 1 in and P = 0.5 lbf. The following material parameters are considered: E = 1 × 107 psi, ν = 0.3 and plane strain condition is assumed. The polygonal mesh and boundary conditions created in PolyMesher are shown in Fig. 7. The Matlab function PolyMesher2Veamy is used to retrieve the polygonal mesh and boundary conditions from PolyMesher and to write them to the text file ‘polymesher2veamy.txt.’ Listing 14 presents the only main setup file needed to run any simulation that uses a PolyMesher mesh and boundary conditions. Finally, the VEM solution for the MBB beam problem that is obtained from Veamy is presented in Fig. 8. Listing 14: Setup file to run a simulation in Veamy using a PolyMesher mesh and boundary conditions. 1

#include

2

#include

3

#include

4

#include

5

#include

6

#include

7

#include

8 9

int main(){

10

Veamer v;

11

Material m(1e7, 0.3);

12

PolygonalMesh mesh = v.initProblemFromFile("polymesher2veamy.txt", m);

13

mesh.printInFile("mesh.txt");

14

Eigen::VectorXd x = v.simulate(mesh);

15

std::string fileName = "displacements.txt";

16

v.writeDisplacements(fileName, x);

43

17

}

P

L D

Fig. 6: MBB beam problem definition as per Section 6.1 in Ref. [23].

Fig. 7: Polygonal mesh and boundary conditions generated in PolyMesher for the MBB beam problem.

6. Concluding remarks In this paper, an object-oriented programming of the virtual element method has been presented for two-dimensional linear elastostatic problems. As a result, a C++ VEM library named Veamy has been designed. A complete sample usage of the 44

uhx

−6

x 10

3

1

2 0.5

y

1 0 0

−1

−0.5

−2

−1 0

1

2

x

3

(a)

uhy

−6

x 10 0

3

−0.5 −1

2

−1.5 −2

1

y

−2.5 −3

0

−3.5 −4

−1

−4.5 −5

−2 0

1

2

x

3

(b)

Fig. 8: Solution for the MBB beam problem in Veamy. (a) VEM axial displacement solution and (b) VEM vertical displacement solution. 45

library, which consisted in the solution of the displacement field of a cantilever beam subjected to a parabolic end load, has been provided. Veamy was also tested on a displacement patch test and a demonstration of its interaction with PolyMesher was featured. In just a few lines of code, Veamy can straightforwardly solve any linear elastostatic problem using a PolyMesher mesh and boundary conditions. To the best of our knowledge, this is the first object-oriented C++ implementation of the virtual element method. Currently, new features are being planned for Veamy, which include the use of Eigen’s sparse solvers and an extension to three-dimensional linear elastostatic problems using polyhedral meshes. We have made Veamy freely available so that interested users can make free use of it, and possibly, extend it to a wider class of problems. Appendix A

Demonstration of some properties and relations used in the VEM derivations

It is instructive to demonstrate some crucial properties and relations that were used in the previous sections. In all of the demonstrations that follow, we consider the definitions for Π0 v, ΠP v, ΠR v and ΠC v given in (30), (33), (34) and (35), respectively. We start by demonstrating that (30) satisfies the orthogonality condition (28). Proof. Π0 (v − ΠP v) = v − ΠP v = v − (ΠR v + ΠC v) = v − (b ω (v) · (x − x) + v + εb(v) · (x − x))

= v − v = 0,

46

b where it has been used that ω(v) = εb(v) = 0 since v is constant.

The next demonstration is tailored to verify that the energy-orthogonality con-

dition (17) is satisfied for ΠC v given in (35). Proof. It is first noted that from (5) and (35), ε(ΠC v) = ∇S (ΠC v) =

 1 1 ∇ΠC v + ∇T ΠC v = (b ε(v) + εb(v)) = εb(v). 2 2

(A.1)

It is also noted that σ(c) and εb(v) are constant tensors, and using (25) and (A.1), we get

aE (c, v − ΠC v) = σ(c) :

Z

ε(v) dx −

E

Z

Z

ε(ΠC v) dx E





∇S (ΠC v) dx Z  Z = σ(c) : ε(v) dx − εb(v) dx E E  Z ε(v) dx − εb(v)|E| = σ(c) : E  Z Z = σ(c) : ε(v) dx − ε(v) dx = 0.

= σ(c) :

ε(v) dx −

Z

E

E

E

E

Another property that needs to be verified is (13). Proof. It is first noted that from (6) and (35), ω(c) = ∇AS (ΠC c) =

 1 1 ∇ΠC c − ∇T ΠC c = (b ε(c) − εb(c)) = 0, 2 2

b which means that ω(c) = 0 (see (26)). In addition, c = ΠC c = εb(c) · (x − x) = 0.

Therefore, from (34) and the preceding results,

b b ΠR c = ω(c) · (x − x) + c = ω(c) · (x − x) + ΠC c = 0. 47

We also need to verify (14). b b Proof. From (34), ∇ΠR r = ω(r). And because ω(r) is a skew-symmetric tensor, it follows that ∇T ΠR r = −b ω(r). These results lead to ∇S (ΠR r) =

 1 1 b b ∇ΠR r + ∇T ΠR r = (ω(r) − ω(r)) = 0. 2 2

(A.2)

Therefore, from (35) and (25), and the preceding result,   Z 1 ∇S (ΠR r) dx · (x − x) = 0. ΠC r = εb(r) · (x − x) = |E| E Finally, it was also stated that the rigid body modes have zero strain, which in fact follows from (A.2) as ε(r) = ∇S (ΠR r) = 0 and ε(ΠR v) = ∇S (ΠR v) = Appendix B

1 b b (ω(v) − ω(v)) = 0. 2

Setup file for the beam problem under parabolic end load

Listing B.1: Setup file for the cantilever beam under parabolic end load. 1

#include

2

#include

3

#include

4

#include

5

#include

6

#include

7

#include

8

#include

9

#include

10

48

11

double tangencial(double x, double y){

12

double P = -1000;

13

double D = 4;

14

double I = std::pow(D,3)/12;

15

double value = std::pow(D,2)/4-std::pow(y,2);

16

return P/(2*I)*value;

17

}

18 19

double uX(double x, double y){

20

double P = -1000;

21

double Ebar = 1e7/(1 - std::pow(0.3,2));

22

double vBar = 0.3/(1 - 0.3);

23

double D = 4;

24

double L = 8;

25

double I = std::pow(D,3)/12;

26

return -P*y/(6*Ebar*I)*((6*L - 3*x)*x + (2+vBar)*std::pow(y,2) - 3*std::pow(D,2)/2*(1+vBar));

27

}

28 29

double uY(double x, double y){

30

double P = -1000;

31

double Ebar = 1e7/(1 - std::pow(0.3,2));

32

double vBar = 0.3/(1 - 0.3);

33

double D = 4;

34

double L = 8;

35

double I = std::pow(D,3)/12;

36

return P/(6*Ebar*I)*(3*vBar*std::pow(y,2)*(L-x) + (3*L-x)*std::pow(x,2));

37

}

38 39

int main(){

40

std::vector rectangle4x8_points = {Point(0, -2), Point(8, -2), Point(8, 2), Point(0, 2)

41

Region rectangle4x8(rectangle4x8_points);

42

rectangle4x8.generateSeedPoints(PointGenerator(functions::constantAlternating(), functions::

};

constant()), 24, 12); 43 44

std::vector seeds = rectangle4x8.getSeedPoints();

45

TriangleMeshGenerator meshGenerator = TriangleMeshGenerator (seeds, rectangle4x8);

46

PolygonalMesh mesh = meshGenerator.getMesh();

47

mesh.printInFile("mesh24x12.txt");

49

48

Veamer v;

49 50 51

EssentialConstraints essential;

52

Function* uXConstraint = new Function(uX);

53

Function* uYConstraint = new Function(uY);

54 55

PointSegment leftSide(Point(0,-2), Point(0,2));

56

SegmentConstraint const1 (leftSide, mesh.getPoints(), Constraint::Direction::Horizontal, uXConstraint); essential.addConstraint(const1, mesh.getPoints());

57 58

SegmentConstraint const2 (leftSide, mesh.getPoints(), Constraint::Direction::Vertical,

59

uYConstraint); essential.addConstraint(const2, mesh.getPoints());

60 61

NaturalConstraints natural;

62 63 64

Function* tangencialLoad = new Function(tangencial);

65

PointSegment rightSide(Point(8,-2), Point(8,2));

66

SegmentConstraint const3 (rightSide, mesh.getPoints(), Constraint::Direction::Vertical,

67

tangencialLoad); natural.addConstraint(const3, mesh.getPoints());

68 69 70

ConstraintsContainer container;

71

container.addConstraints(essential, mesh);

72

container.addConstraints(natural, mesh);

73 74

Material m(1e7, 0.3);

75

ProblemConditions conditions(container, m);

76

v.initProblem(mesh, conditions);

77 78

Eigen::VectorXd x = v.simulate(mesh);

79

std::string fileName = "parabolic24x12.txt";

80

v.writeDisplacements(fileName, x);

81

}

50

Appendix C

Setup file for the patch test

Listing C.1: Setup file for the displacement patch test. 1

#include

2

#include

3

#include

4

#include

5

#include

6

#include

7

#include

8 9

double uXPatch(double x, double y){ return x;

10 11

}

12 13

double uYPatch(double x, double y){ return x + y;

14 15

}

16 17 18

int main(){ std::vector rectangle4x8_points = {Point(0, -2), Point(8, -2), Point(8, 2), Point(0, 2) };

19

Region rectangle4x8(rectangle4x8_points);

20

rectangle4x8.generateSeedPoints(PointGenerator(functions::constantAlternating(), functions:: constant()), 24, 12);

21 22

std::vector seeds = rectangle4x8.getSeedPoints();

23

TriangleMeshGenerator meshGenerator = TriangleMeshGenerator (seeds, rectangle4x8);

24

PolygonalMesh mesh = meshGenerator.getMesh();

25

mesh.printInFile("rectangle4x8ConstantAlternating .txt");

26 27

Veamer v;

28 29

EssentialConstraints essential;

30

Function* uXConstraint = new Function(uXPatch);

31

Function* uYConstraint = new Function(uYPatch);

32 33

PointSegment leftSide(Point(0,-2), Point(0,2));

51

34

SegmentConstraint leftX (leftSide, mesh.getPoints(), Constraint::Direction::Horizontal,

35

essential.addConstraint(leftX, mesh.getPoints());

36

SegmentConstraint leftY (leftSide, mesh.getPoints(), Constraint::Direction::Vertical,

uXConstraint);

uYConstraint); 37

essential.addConstraint(leftY, mesh.getPoints());

38 39

PointSegment downSide(Point(0,-2), Point(8,-2));

40

SegmentConstraint downX (downSide, mesh.getPoints(), Constraint::Direction::Horizontal, uXConstraint);

41

essential.addConstraint(downX, mesh.getPoints());

42

SegmentConstraint downY (downSide, mesh.getPoints(), Constraint::Direction::Vertical,

43

essential.addConstraint(downY, mesh.getPoints());

uYConstraint);

44 45

PointSegment rightSide(Point(8,-2), Point(8, 2));

46

SegmentConstraint rightX (rightSide, mesh.getPoints(), Constraint::Direction::Horizontal,

47

essential.addConstraint(rightX, mesh.getPoints());

48

SegmentConstraint rightY (rightSide, mesh.getPoints(), Constraint::Direction::Vertical,

uXConstraint);

uYConstraint); 49

essential.addConstraint(rightY, mesh.getPoints());

50 51

PointSegment topSide(Point(0, 2), Point(8, 2));

52

SegmentConstraint topX (topSide, mesh.getPoints(), Constraint::Direction::Horizontal, uXConstraint);

53

essential.addConstraint(topX, mesh.getPoints());

54

SegmentConstraint topY (topSide, mesh.getPoints(), Constraint::Direction::Vertical,

55

essential.addConstraint(topY, mesh.getPoints());

uYConstraint);

56 57

ConstraintsContainer container;

58

container.addConstraints(essential, mesh);

59 60

Material m(1e7, 0.3);

61

ProblemConditions conditions(container, m);

62 63

v.initProblem(mesh, conditions);

64

52

65

Eigen::VectorXd x = v.simulate(mesh);

66

std::string fileName = "displacement.txt";

67

v.writeDisplacements(fileName, x);

68

}

Appendix D

Using Veamy to model the two-dimensional Poisson problem

In this appendix, we list the changes that are required in order to use Veamy to model the Poisson problem in two dimensions. This requires some modifications in the C++ classes. It is also required to browse the source code [28] to follow some of these changes. Sometimes it also helps to look into the UML diagram (Fig. 2). Alternatively, it would be possible to extend the existing classes using inheritance. We choose to explain how to modify the existing classes as we believe that by doing this it permits a better understanding of the programming logic in Veamy. The changes to the classes are as follows: • Veamer: Remove the final deformation step of the simulate method as the Poisson problem does not involve deformations in the postprocessing — it only delivers scalars as nodal results. Also, writeDisplacements method must be slightly changed as each vertex now only has one degree of freedom, that is, only one nodal result associated. • Element: ◦ In the initialization step, create one degree of freedom per vertex, which implies several changes in the DOF and DOFS classes.

53

◦ Change the stiffness matrix computation in computeK method. That is, modify the code in Listing 4 accordingly. The expression for the VEM stiffness matrix can be found, for instance, in Ref. [25]. ◦ Change the computation of the element force vector considering that there is only one degree of freedom per node. • DOF: Remove the Axis enumeration and all references to it in the methods. This way, a DOF object now only keeps its own index and the one related to its assigned point. Alternatively, one could remove one of the members of the enumeration and keep all references to Axis. • DOFS: Create only one DOF instance when receiving a point in addDOF. Likewise, return the index of the single degree of freedom when required. • BodyForce: All BodyForce subclasses have only a single apply method, which represents the right-hand side function of the Poisson equation. • BodyForceVector: Remove the computation of the body force related to the second axis from the implementation of computeVariableBodyForceVector method. Note that the result vector is now of size n instead of 2n. On the other hand, change the implementation of computeConstantBodyForceVector method as it becomes a multiplication between a vector and a scalar. • Constraint: Remove all references to Direction since only one degree of freedom is involved. This includes all constructor methods and the Direction accessor. Alternatively, one could keep the Direction enumeration with only one member, Total.

54

• ConstraintsContainer: Remove all references to Axis and update the signature of all affected methods. • Constraints: Remove all references to Axis and update the signature of the affected class methods. In addConstrainedDOFBySegment method, remove the procedure that checks whether the degree of freedom should be constrained or not depending on its axis and the direction of the constraint, and simply constraint all degrees of freedom inside a constrained segment. • NaturalConstraints: Change the definition of boundaryVector method (the element traction force vector) similarly as explained for the BodyForceVector class, as it becomes a multiplication between a vector and a scalar. The resulting vector contains just two entries, one per endpoint of the segment. Acknowledgments AOB acknowledges the support provided by Universidad de Chile through the “Programa VID Ayuda de Viaje 2017.” The work of CA is supported by CONICYTPCHA/Mag´ıster Nacional/2016-22161437. NHK is grateful for the support provided by Proyecto Enlace VID 009/15. [1] C. Talischi, G. H. Paulino, Addressing integration error for polygonal finite elements through polynomial projections: A patch test connection, Mathematical Models and Methods in Applied Sciences 24 (08) (2014) 1701–1727. [2] C. Talischi, A. Pereira, I. Menezes, G. Paulino, Gradient correction for polygonal and polyhedral finite elements, International Journal for Numerical Methods in Engineering 102 (3–4) (2015) 728–747.

55

[3] A. Francis, A. Ortiz-Bernardin, S. Bordas, S. Natarajan, Linear smoothed polygonal and polyhedral finite elements, International Journal for Numerical Methods in Engineering 109 (9) (2017) 1263–1288. [4] J. Dolbow, T. Belytschko, Numerical integration of Galerkin weak form in meshfree methods, Computational Mechanics 23 (3) (1999) 219–230. [5] J. S. Chen, C. T. Wu, S. Yoon, Y. You, A stabilized conforming nodal integration for Galerkin mesh-free methods, International Journal for Numerical Methods in Engineering 50 (2) (2001) 435–466. [6] I. Babuˇska, U. Banerjee, J. E. Osborn, Q. L. Li, Quadrature for meshless methods, International Journal for Numerical Methods in Engineering 76 (9) (2008) 1434–1470. [7] I. Babuˇska, U. Banerjee, J. E. Osborn, Q. Zhang, Effect of numerical integration on meshless methods, Computer Methods in Applied Mechanics and Engineering 198 (37–40) (2009) 2886–2897. [8] A. Ortiz, M. A. Puso, N. Sukumar, Maximum-entropy meshfree method for compressible and near-incompressible elasticity, Computer Methods in Applied Mechanics and Engineering 199 (25–28) (2010) 1859–1871. [9] A. Ortiz, M. A. Puso, N. Sukumar, Maximum-entropy meshfree method for incompressible media problems, Finite Elements in Analysis and Design 47 (6) (2011) 572–585. [10] Q. Duan, X. Li, H. Zhang, T. Belytschko, Second-order accurate derivatives and integration schemes for meshfree methods, International Journal for Numerical Methods in Engineering 92 (4) (2012) 399–424. 56

[11] Q. Duan, X. Gao, B. Wang, , X. Li, H. Zhang, T. Belytschko, Y. Shao, Consistent element-free Galerkin method, International Journal for Numerical Methods in Engineering 99 (2) (2014) 79–101. [12] Q. Duan, X. Gao, B. Wang, X. Li, H. Zhang, A four-point integration scheme with quadratic exactness for three-dimensional element-free Galerkin method based on variationally consistent formulation, Computer Methods in Applied Mechanics and Engineering 280 (0) (2014) 84–116. [13] A. Ortiz-Bernardin, J. S. Hale, C. J. Cyron, Volume-averaged nodal projection method for nearly-incompressible elasticity using meshfree and bubble basis functions, Computer Methods in Applied Mechanics and Engineering 285 (2015) 427–451. [14] A. Ortiz-Bernardin, M. A. Puso, N. Sukumar, Improved robustness for nearlyincompressible large deformation meshfree simulations on Delaunay tessellations, Computer Methods in Applied Mechanics and Engineering 293 (2015) 348–374. [15] L. Beir˜ao da Veiga, F. Brezzi, A. Cangiani, G. Manzini, L. D. Marini, A. Russo, Basic principles of virtual element methods, Mathematical Models and Methods in Applied Sciences 23 (1) (2013) 199–214. [16] A. Cangiani, G. Manzini, A. Russo, N. Sukumar, Hourglass stabilization and the virtual element method, International Journal for Numerical Methods in Engineering 102 (3–4) (2015) 404–436. [17] A. L. Gain, C. Talischi, G. H. Paulino, On the virtual element method for three-

57

dimensional linear elasticity problems on arbitrary polyhedral meshes, Computer Methods in Applied Mechanics and Engineering 282 (0) (2014) 132–160. [18] G. Manzini, A. Russo, N. Sukumar, New perspectives on polygonal and polyhedral finite element methods, Mathematical Models and Methods in Applied Sciences 24 (08) (2014) 1665–1699. [19] L. Beir˜ao da Veiga, C. Lovadina, D. Mora, A virtual element method for elastic and inelastic problems on polytope meshes, Computer Methods in Applied Mechanics and Engineering 295 (2015) 327–346. [20] A. Ortiz-Bernardin, A. Russo, N. Sukumar, Consistent and stable meshfree Galerkin methods using the virtual element decomposition, International Journal for Numerical Methods in Engineeringdoi:10.1002/nme.5519. [21] G. Guennebaud, B. Jacob, et al., Eigen v3, http://eigen.tuxfamily.org (2010). [22] J. R. Shewchuk, Triangle: Engineering a 2D Quality Mesh Generator and Delaunay Triangulator, in: M. C. Lin, D. Manocha (Eds.), Applied Computational Geometry: Towards Geometric Engineering, Vol. 1148 of Lecture Notes in Computer Science, Springer-Verlag, 1996, pp. 203–222, from the First ACM Workshop on Applied Computational Geometry. [23] C. Talischi, G. H. Paulino, A. Pereira, I. F. M. Menezes, PolyMesher: a generalpurpose mesh generator for polygonal elements written in Matlab, Structural and Multidisciplinary Optimization 45 (3) (2012) 309–328. [24] G. Strang, G. Fix, An Analysis of the Finite Element Method, 2nd Edition, Wellesley-Cambridge Press, MA, 2008.

58

[25] L. Beir˜ao da Veiga, F. Brezzi, L. D. Marini, A. Russo, The Hitchhiker’s Guide to the Virtual Element Method, Mathematical Models and Methods in Applied Sciences 24 (08) (2014) 1541–1573. [26] L. Beir˜ao da Veiga, F. Brezzi, L. D. Marini, Virtual elements for linear elasticity problems, SIAM Journal on Numerical Analysis 51 (2) (2013) 794–812. [27] A. Johnson, Clipper - an open source freeware library for clipping and offsetting lines and polygons (version: 6.1.3), http://www.angusj.com/delphi/clipper.php (2014). [28] Veamy v1.0, http://camlab.cl/research/software/veamy/ (2017). [29] S. P. Timoshenko, J. N. Goodier, Theory of Elasticity, 3rd Edition, McGrawHill, NY, 1970.

59