Fortran 90: An Entry to Object-Oriented Programming for the Solution of Partial Differential Equations L. MACHIELS and M.O. DEVILLE LMF, DGM, Swiss Federal Institute of Technology, Lausanne
The aim of this work is to set up a programming model suitable for numerical computing while taking benefit of Fortran 90’s features. The use of concepts from object-oriented programming avoids the weaknesses of the traditional global data programming model of Fortran 77. This work supports the view that object-oriented concepts are not in contradiction with good Fortran 77 programming practices but complement them. These concepts can be embodied in a module-based programming style and result in an efficient and easy-to-maintain code (maintainability means code clarity, scope for further enhancements, and ease of debugging). After introducing the terminology associated with object-oriented programming, it is shown how these concepts are implemented in the framework of Fortran 90. Then, we present an object-oriented implementation of a spectral element solver for a Poisson equation. Categories and Subject Descriptors: D.1.5 [Programming Techniques]: Object-Oriented Programming; D.3.2 [Programming Languages]: Language Classifications—Fortran 90; G.1.8 [Numerical Analysis]: Partial Differential Equations General Terms: Languages Additional Key Words and Phrases: Spectral element method
1. INTRODUCTION During the past decades, many computer programs have been written for solving systems of partial differential equations for different types of applications in engineering. Nowadays, due to a combination of improved algorithms and faster computers, the range of problems that can be solved by computers has increased dramatically. Consequently, industrial codes have become intricate and hard to maintain, in the attempt to be as flexible as possible regarding the problems they solve, the methods they propose, and the geometrical configurations they treat. In this article, we identify The work of L. Machiels was partially supported by the Swiss National Foundation for Scientific Research. Authors’ address: LMF, DGM, Swiss Federal Institute of Technology, Ch-1015 Lausanne, Switzerland; email; {machiels; deville}@dgm.epfl.ch. Permission to make digital / hard copy of part or all of this work for personal or classroom use is granted without fee provided that the copies are not made or distributed for profit or commercial advantage, the copyright notice, the title of the publication, and its date appear, and notice is given that copying is by permission of the ACM, Inc. To copy otherwise, to republish, to post on servers, or to redistribute to lists, requires prior specific permission and / or a fee. © 1997 ACM 0098-3500/97/0300 –0032 $03.50 ACM Transactions on Mathematical Software, Vol. 23, No. 1, March 1997, Pages 32–49.
Fortran 90: An Entry to Object-Oriented Programming
•
33
the main drawback of these codes to be their global data representation model. We propose to address this drawback by extending the practices of Fortran 77 programmers with a set of concepts borrowed from objectoriented programming. Basically, writing a computational code consists in translating an algorithm into groups of statements called procedures (for the sake of simplicity, the term procedure will be used to denote either a function or a subroutine). The data set handled by the program can be viewed as a logical flow going through the statements in a fixed order. In the mind of a numerical analyst this sequential nature of the code seems inevitable. More precisely, the Fortran 77 programmer usually begins by choosing a global data structure (a set of arrays) and then he or she writes, tests, and progressively assembles the procedures. This programming style will be subsequently referred to as the global data model. If some design rules are followed [Metcalf 1985], the debugging is facilitated. The difficulties appear when a more complex data set is required: small changes can lead to extensive code revision, since data types have to be identically defined in all procedures. Furthermore, modifications are invariably followed by a fastidious series of checks in order to verify if the sequence of statements is preserved. The object-oriented model relies on a decomposition of the problem in autonomous data pieces with which some operations are associated. It is based on representations and manipulations of data that are not familiar to the Fortran 77 programmer. The number of concepts is small, and they will be defined in the next section with the terminology featured in Goldberg and Robson’s book about the SMALLTALK language [Goldberg and Robson 1983]. One of Fortran 77’s major strengths are the libraries of carefully implemented classical algorithms which have accumulated over the years. The impossibility of replacing or rewriting these libraries, coupled with the commonly held belief that object-oriented programming is the opposite of the procedural modularity of Fortran 77, has hindered object-oriented programming in scientific computing. Our work shows the fallacy of that view by making evident that object-oriented programming provides nothing more than a set of new concepts that enhances the traditional good programming practices of Fortran 77. In our approach, the calls to library subroutines are naturally included in the methods associated to an object. Moreover, in this work we show that the Fortran 90 standard supports most of the object-oriented concepts. Since the standard includes Fortran 77, it was the natural choice of programming language for our purpose. An example is provided with a spectral element solver for a Poisson equation in one and two dimensions. An object-oriented approach was used by Dubois-Pe`lerin et al. in a similar context (for a finite-element code) with other languages: SMALLTALK and C11 [Dubois-Pe`lerin and Zimmerman 1993; Dubois-Pe`lerin et al. 1992; Zimmerman et al. 1992]. This last work shows how object-oriented programming leads to enhanced modularity, ACM Transactions on Mathematical Software, Vol. 23, No. 1, March 1997.
34
•
L. Machiels and M. O. Deville
which helps to produce extendible, easy-to-maintain, and reusable software. Section 2 introduces the basic material required to understand the object-oriented programming paradigm. In Section 3, we present two important pieces of code design provided by Fortran 90: the module and the interface block. And we show how these features can be exploited to implement object-oriented concepts. Section 4 gives a presentation of the spectral element method, and within Section 5, we describe an implementation of the algorithm. Finally, in Section 6 some concluding remarks are drawn. 2. BASIC CONCEPTS The aim of this section is to define the concepts of object-oriented programming. The reader may find a more detailed discussion in Goldberg and Robson [1983]. Object-oriented programming focuses on information management. An object consists of some private memory and some operations, i.e., a set of variables (also referred to as the set of attributes) and an associated set of operations called the set of methods. An object can be a number or a character and the associated operations, but it can also be a more complex entity, such as a Poisson problem for instance. The information contained by the attributes at a given time is called the state of the object. The nature of a method depends on the type of variables it acts on (e.g., objects representing numbers compute arithmetic functions). One can imagine that the Poisson object defines and solves the Poisson equation on a specific domain. The methods are accompanied by messages. A message is a request for an object to carry out one or more of its operations. The set of messages to which an object can respond is called its protocol (or interface). A crucial property of messages is that they are the only way to act on the content of an object. A message specifies the desired operation, but it does not say anything about how this operation should be performed. This mechanism provides a complete modularity in the sense that the attributes of an object can only be modified by the methods of the object itself. The methods and the attributes are the object’s private entities, i.e., they are hidden from the outside world. Another useful concept is that of class. A class defines a kind of object. The individual objects belonging to the same class are called instances of that class. More precisely a class consists of (1) the definition of the attributes; (2) a set of methods which are the procedures implementing the operations applicable to the attributes contained in an object of the class; (3) a set of messages (the protocol)— each message initiates a specific method of the class. Each class contains at least two methods called constructor and destructor, respectively. The purpose of the constructor is to initialize an object. The ACM Transactions on Mathematical Software, Vol. 23, No. 1, March 1997.
Fortran 90: An Entry to Object-Oriented Programming
•
35
destructor frees the memory occupied by an object. An important part of object-oriented program design is to determine which classes should be defined and which messages provide a useful vocabulary of interaction among the objects of these classes. Finally, each class can be fully tested independently; therefore we avoid a lot of programming errors. In order to clarify the previous concepts, we give the example of the Poisson problem object mentioned earlier. The definition of Poisson_class can be stated as follows: (1) At least four attributes are required: the computational domain, a matrix representing the discretized Poisson operator, the right-hand side, and the solution. (2) The identification of the program tasks gives the methods. We have a method that allows the user to define the equation on a given domain, say new_Poisson. A method for deallocating the memory reserved for the object: dealloc_Poisson. One finds also build_Poisson which assembles the Poisson operator on the defined domain, factorize_Poisson which factorizes the Poisson matrix, and solve_Poisson which returns the solution. (3) The protocol contains three messages: new_Poisson, dealloc_Poisson, and solve_Poisson. Another concept to be introduced is the one related to the capability for objects of different classes to respond to exactly the same protocol. This is referred to as polymorphism. It enables us to send the same message to objects of different classes, and therefore it leads to a more uniform treatment of these objects. To illustrate polymorphism we extend the example of the previous paragraph defining a new class for solving Helmholtz equations instead of Poisson equations: the Helmholtz_class. The design of this new class is close to the one of Poisson_class. Its methods are new_Helmholtz, build_Helmholtz, factorize_Helmholtz, and solve_Helmholtz. The generality of the program is enhanced if solve_Poisson and solve_ Helmholtz could be called by the same message, say solve. In the program, wherever this message is found, one of the two methods is automatically chosen according to the object type. One of the major drawbacks of the global-data model is the sequential nature of the code: each of the procedures is expected to be called at a given foreseeable instant during the execution. When the code is modified for a particular reason, the programmer has to ensure that the order of the instruction sequence is preserved. This rapidly becomes a critical issue when the code has thousands of lines dispersed over several procedures: we often lose the control on the state of the data set, since procedures may act on variables that are not present in the calling sequence. The nonanticipation principle addresses this problem [Dubois-Pe`lerin 1992]. This principle states that a method, when applied to a given object, should not depend on whether or not a particular task has already been performed. It means that no assumption about the previous state of the object should be ACM Transactions on Mathematical Software, Vol. 23, No. 1, March 1997.
36
•
L. Machiels and M. O. Deville
made when sending a message and that each method is responsible for bringing the object into the correct state before performing any operation. Note that this programming principle does not belong exclusively to objectoriented programming; the implementation of many Fortran 77 libraries relies on a similar concept. The following example shows practical consequences of such a principle for programmers and users of modules for solving partial differential equations. For such problems, the sequence of operations is as follows: assemble the matrix for a given computational domain; factorize it; and compute the solution of the linear system. If a nonanticipation discipline is followed, the user does not have to pay any attention to the state of the linear system: when the message “solve the problem” is sent the method is responsible for checking if the matrix is already assembled and factorized; if not, the method itself sends a message to assemble and factorize the matrix and only afterward is the solution computed and returned. The last concept usually introduced in the context of object-oriented programming is the so-called class hierarchy. We will introduce here the concept of simple inheritance which is the simplest case of class hierarchy. Simple inheritance consists of reusing a class (or parts of a class) to define enriched classes where attributes and/or methods are added. This concept allows a very economic use of code lines, e.g., one can associate the classes that have attributes or methods in common in a general class. Then the definitions of the classes are achieved by the enrichment of the general class with particular attributes and methods. In summary, the class concept provides a complete encapsulation of the data structure. Moreover, the nonanticipation principle helps to produce operations that are independent of any execution context. It means that the objects are autonomous in both “space” (organization of variables) and “time” (sequence of operations). 3. MODULES AND INTERFACE BLOCKS In the present section, we show that the previous concepts (except class hierarchy) are naturally implemented in module-based Fortran 90 programs. The module is an important enhancement of Fortran 90 [Adams et al. 1992; Kerrigan 1993; Metcalf and Reid 1992]. This new program unit allows us to encapsulate definitions of types and variables with their associated procedures. Another innovation of Fortran 90 is the interface block. Interface blocks, among other features, can define a generic name for a set of procedures. A third important addition is the private/public statements, which give the ability to specify some entities (variables, constants, type definitions, procedures) to be hidden from outside of the module. In the light of the previous section, we notice that we are very close to the object-oriented concepts introduced. The classes are defined by the means of modules; the set of attributes is defined in a derived type; the protocol is ACM Transactions on Mathematical Software, Vol. 23, No. 1, March 1997.
Fortran 90: An Entry to Object-Oriented Programming
•
37
given by listing its procedures in a public statement; and the polymorphism is achieved using generic interfaces. The following piece of code shows the definition of the Poisson_class in a pseudolanguage close to Fortran 90. MODULE Poisson_class PRIVATE PUBLIC:: new_Poisson, dealloc_Poisson, solve_Poisson TYPE, PUBLIC:: Poisson PRIVATE TYPE(domain) :: dom TYPE(matrix) :: mtx TYPE(right_hand_side) :: rhs TYPE(solution) :: sol END TYPE CONTAINS FUNCTION new_Poisson(arguments) RESULT(p) TYPE(Poisson) :: p statements END FUNCTION new_Poisson SUBROUTINE dealloc_Poisson(p) TYPE(Poisson) :: p statements END SUBROUTINE dealloc_Poisson FUNCTION solve_Poisson(p) RESULT(s) TYPE(Poisson) :: p TYPE(solution) :: s statements END FUNCTION solve_Poisson SUBROUTINE assemble_Poisson(p) TYPE(Poisson) :: p statements END SUBROUTINE assemble_Poisson SUBROUTINE factorize_Poisson(p) TYPE(Poisson) :: p statements END SUBROUTINE factorize_Poisson END MODULE Poisson_class
The public/private statements specify the protocol. The declarations of the attributes are embodied in a type definition. The private statement in the derived type definition guarantees that attributes are hidden from outside of the module. The remainder of the module is devoted to method coding. The purpose of new_Poisson is to initialize and return an object of the class. It is important to notice that an object is not considered to be ACM Transactions on Mathematical Software, Vol. 23, No. 1, March 1997.
38
•
L. Machiels and M. O. Deville
defined until the initialization function new_Poisson has been called. This function takes the role of the C11 constructor. Destruction is achieved by the dealloc_Poisson subroutine. Poisson_class objects can be defined in program units that have a use Poisson_class statement. Now suppose that a second class is added: the Helmholtz_class with an implementation similar to Poisson_class. As noticed in the previous section, in order to improve the generality of the code, polymorphism should be used. More precisely, one would like to refer to the two distinct procedures solve_Poisson and solve_Helmholtz through a generic name, say solve. Fortran 90 allows us to define separately the two classes and to join them with an interface block as shown in the following program segment: INTERFACE solve MODULE PROCEDURE solve_Poisson, solve_Helmholtz END INTERFACE
Sending the message solve of the generic protocol to a given object automatically initiates the correct method. The only drawback of using this technique within the framework of Fortran 90 is the lack of an explicit mechanism to implement class hierarchy, which is a crucial property of an object-oriented approach with respect to the level of abstraction it provides. From a pragmatic point of view, complete modularity means that while composing a particular application it is possible to use previously developed code with no knowledge of implementation details. In that respect, we give the classical example of a situation for which hierarchy may help: when classes share a common set of attributes or methods, e.g., different kinds of iterative solvers may share attributes (the number of degrees of freedom, the right-hand side, the solution) and methods (computation of preconditioners, . . .). In that case, the hierarchy mechanism allows us to define a virtual class so that the general structure of the class is written only once and so that the particular classes are defined by enrichments of this virtual class. For this example the USE ONLY statement might be helpful to a certain extent, since it allows the use of a subset of a module in the definition of a new module. But it does not provide the flexibility to add attributes in the definition of an object and therefore cannot be interpreted as an inheritance mechanism. Finally, noting that object-oriented programming is based on complete data encapsulation provides interesting prospects, since this is a desirable feature for efficient implementation of algorithms on parallel computers with distributed memory. For example, if we consider domain decomposition techniques, the information about the domain can be carefully divided and stored in different objects. If these objects are distributed on different processors, object-oriented programming gives a new and elegant way implementing parallel algorithms. 4. THE SPECTRAL ELEMENT METHOD The spectral element method [Maday and Patera 1989] is a weighted residual technique for the solution of differential equations that combines ACM Transactions on Mathematical Software, Vol. 23, No. 1, March 1997.
Fortran 90: An Entry to Object-Oriented Programming
•
39
the geometric flexibility of low-order finite-element methods with the convergence rate of spectral methods. For a spectral element discretization, the computational domain is divided into macroelements, and within each of these subdomains the solution is approximated by a high-order polynomial interpolant. A Galerkin variational projection provides the discrete equations. Convergence may be achieved by increasing the degree N of the polynomial approximation while the number of elements K remains fixed. We consider here the two-dimensional Poisson equation on a rectilinear domain V with homogeneous Dirichlet boundary conditions on the domain boundary V,
2¹ 2u 5 f u50
in V,
(1)
on V.
(2)
The variational statement equivalent to (1, 2) is
find u [ H 10~ V ! such that
E
¹u z
V
¹v 5
E
v [ H 10~ V ! .
fv
(3)
V
H 10 (V) is the Sobolev space of functions and their first-order derivatives which are square integrable and vanish on the boundary. The spectral element discretization proceeds by breaking up the domain V into K rectilinear elements, K
# 5 V
ø
# k, V
V k 5 # a k , a9k@ 3 # b k ,b9k@ ,
k51
such that the intersection of two adjacent elements is either a whole edge or a vertex. We require that the variational statement (3) be satisfied for a polynomial subspace of H 10 . We define the space
P N,K~ V ! 5 $ f [ L 2~ V ! ; f uVk [ P N~ V k! , k 5 1, . . . , K % , where P N (V k ) denotes the tensor-product space of all polynomials of degree less than or equal to N in each space variable. The spectral element space X h consists of
X h 5 H 10~ V ! ù P N, K~ V ! . The discrete problem is given by
find u h [ X h such that
E
V
¹u h z
¹v h 5
E
fv h
v h [ X h.
V
ACM Transactions on Mathematical Software, Vol. 23, No. 1, March 1997.
40
•
L. Machiels and M. O. Deville
Finally, the discrete equations are obtained by using the Gauss-LobattoLegendre quadrature rule to compute the integrals of the discrete problem. We get the following linear system
Au 5 Bf, where A and B are respectively the stiffness and the mass matrix. In the sequel of the presentation and in the code these matrices will be denoted by a and b. 5. IMPLEMENTATION OF A POISSON EQUATION SOLVER A key issue in object-oriented analysis is the choice of a good set of classes in order to split the problem while keeping sufficient generality. This choice is mostly a matter of practice although some guidelines can be followed [Stroustrup 1991]. In the code given below, the classes are implemented more with a concern of clarity than efficiency. We will give the global structure of the program, a short presentation of each class, and a detailed description of the module implementing one of these classes (namely the grid_class).1 5.1 Global Description of the Program The code is composed of two simple tool modules (used_precision and mat_operations), five other modules implementing a class (grid_class, element_class, assembling_class, poisson_1d_class, poisson_2d_class), a main program, and some Fortran 77 subroutines. The module used_precision gives a good illustration of the use of portable mechanisms to set up computation accuracy. This module is written around the selected_real_kind intrinsic function and is used in all program units so that a specified numerical accuracy can be chosen regardless of the processor characteristics. Some useful constants are defined as well: MODULE used_precision ! This module specifies the numerical model IMPLICIT NONE INTEGER, PARAMETER :: float 5 selected_real_kind(13,99) REAL(float), PARAMETER :: zero 5 0.0_float REAL(float), PARAMETER :: one 5 1.0_float REAL(float), PARAMETER :: two 5 2.0_float REAL(float), PARAMETER :: pi 5 3.141592653589793_float END MODULE used_precision
Within the module mat_operations, useful matrix operations (e.g., the tensor product) are implemented, and new and overloaded operators are defined using interface blocks. 1
The program is available through the following anonymous FTP address: ftp.epfl.ch in the directory pub/doc/semo. The files do not include the Specmit and Lapack libraries, which can be obtained by other means.
ACM Transactions on Mathematical Software, Vol. 23, No. 1, March 1997.
Fortran 90: An Entry to Object-Oriented Programming
•
41
The classes are arranged such that the Poisson operator matrix is built progressively. A grid_class method computes the operator matrix on the canonical element (the canonical element is the specific element where the interpolant polynomials are defined on ] 2 1,1[). Within element_class this operator matrix is generalized for the size of each element. Then the different elemental contributions are assembled on the computational domain calling a method of the assembling_class. Finally, the one-dimensional Poisson problem (two-dimensional Poisson problem) is defined and solved in the poisson_1d_class method (poisson_2d_class method, respectively). The foundation of the numerical method lies in the definition of the interpolation basis and the integration rules. In the program, this information is stored in a grid type object. More precisely, such an object contains the integration nodes and weights as well as the evaluation at these nodes of the Lagrangian interpolant derivatives. This object also builds a discrete Poisson operator on the canonical element. The main tasks of grid are to compute and to return (nodes and weights) vectors and (derivative and Poisson) matrices if need be. The implementation of this class is given in the Appendix. An element type object is of geometrical nature. It contains the location and the size of an element, the number of nodes, the coordinates of the nodes, and the grid defined on this element: TYPE, PUBLIC :: element PRIVATE REAL(float) l ! left boundary REAL(float) es ! size of the element INTEGER n ! number of nodes REAL(float), dimension(:), pointer :: x ! coordinates of nodes TYPE(grid) :: g END TYPE element
Its main task is to build the local Poisson operator which is merely the canonical element matrix times a geometrical scale factor. The element_ class is designed in a way that new operator matrices can be easily generated to solve new problems by adding new computational methods. In the present version of the program only the Poisson operator matrix is available through the call to the function give_matrix_e, but other operator matrices can be easily added: FUNCTION give_matrix_e(e,which) RESULT(mat) TYPE(element), INTENT(in) :: e CHARACTER(LEN5p) which REAL(float), DIMENSION(e%n,e%n) :: mat SELECT CASE (which) CASE(’poisson’) mat 5 give_matrix_g(e%g, ’poisson’)ptwo/e%es ACM Transactions on Mathematical Software, Vol. 23, No. 1, March 1997.
42
•
L. Machiels and M. O. Deville
END SELECT END FUNCTION give_matrix_e
The function give_matrix_g is part of the protocol of grid_class and returns operators defined on the canonical element. The assembling_class is a more difficult case. An object of this class consists basically of the domain on which the computation is to be performed. Its attributes are a set of elements and the information about the interconnection between these elements. To understand how this information is stored, note that each discretization node can be numbered locally at the elemental level or globally as a node inside the full computational domain. The connection between elements is given by the mapping between this local and global node numbering. The mapping is represented by a two-dimensional array called index (index(i, j) holds the global index of the node i of element j). The assembling type is defined by TYPE, PUBLIC :: assembling PRIVATE INTEGER n_el ! number of elements INTEGER :: n ! number of global variables INTEGER, DIMENSION(:,:), POINTER :: index TYPE(element), DIMENSION(:), POINTER :: el ! set of elements END TYPE assembling
The assembling_class messages return vectors and discretized operators assembled on the computational domain. For instance, there is a function called assemble_matrix_a which returns operators on a set of elements. This function corresponds to give_matrix_e (respectively, give_matrix_g) which gives the Poisson operator at the elemental level (respectively, grid level): FUNCTION assemble_matrix_a(a,which) RESULT(mat) TYPE(assembling),INTENT(IN) :: a CHARACTER(LEN5p),INTENT(IN) :: which REAL(float),DIMENSION(a%n,a%n) :: mat REAL(float),DIMENSION(:,:),ALLOCATABLE :: m INTEGER n,i,j,iel mat 5 zero n 5 give_n_e(a%el(1)) ALLOCATE(m(n,n)) DO iel51,a%n_el ! assembling over the elements m 5 give_matrix_e(a%el(iel),which) DO j51,n DO i51,n mat(a%index(i,iel),a%index(j,iel)) 5 & & mat(a%index(i,iel),a%index(j,iel)) 1 & & m(i,j) ENDDO ACM Transactions on Mathematical Software, Vol. 23, No. 1, March 1997.
Fortran 90: An Entry to Object-Oriented Programming
•
43
ENDDO ENDDO END FUNCTION assemble_matrix_a
A poisson_1d_class object factorizes the matrix and sends back the solution of the equation. The main attributes of the class are the Poisson matrix, the right-hand side, and an assembling object (or domain): TYPE, PUBLIC :: poisson_1d PRIVATE REAL(float),DIMENSION(:,:),POINTER :: a ! a stands for the Poisson operator REAL(float),DIMENSION(:),POINTER :: f,b ! f is the right-hand side ! b is the diagonal mass matrix TYPE(assembling) :: assble END TYPE poisson_1d
For the one-dimensional problem, the matrix to be inverted is the one given by the assembling object, whereas for the two-dimensional problem, two assembling objects are required (one in each direction), and the full matrix is obtained by means of tensor products: SUBROUTINE assemble_abf_2d(p) TYPE(poisson_2d) :: p INTEGER nx,ny,info REAL(float),DIMENSION(:),ALLOCATABLE :: bx,by,xx,xy ! bx and by are the mass matrices in each direction ! xx and xy are the coordinates of the nodes in the domain REAL(float),DIMENSION(:,:),ALLOCATABLE :: ax,ay ! ax and ay are the Poisson matrices in each direction nx 5 give_n_a(p%assble_x) ny 5 give_n_a(p%assble_y) ALLOCATE(ax(nx,nx),bx(nx)) ax 5 assemble_matrix(p%assble_x,’poisson’) bx 5 assemble_vector(p%assble_x,’b’) ALLOCATE(ay(ny,ny),by(ny)) ay 5 assemble_matrix(p%assble_y,’poisson’) by 5 assemble_vector(p%assble_y,’b’) ALLOCATE(p%a(nxpny,nxpny),p%b(nxpny)) p%a 5 (by.TEP.ax) 1 (ay.TEP.bx) ! .TEP. stands for the tensor product p%b 5 by.TEP.bx ALLOCATE(xx(nx),xy(ny)) ALLOCATE(p%f(nxpny)) xx 5 give_vector_a(p%assble_x,’x’) ACM Transactions on Mathematical Software, Vol. 23, No. 1, March 1997.
44
•
L. Machiels and M. O. Deville
xy 5 give_vector_a(p%assble_y,’x’) p%f 5 p%bprhs2(xx,xy,nx,ny) CALL boundary2(p%a,p%f,nx,ny) ! boundary conditions CALL spotrf(’u’,nxpny,p%a,nxpny,info) ! matrix factorization PRINT p,’info spotrf ’,info END SUBROUTINE assemble_abf_2d
The first part of this function builds the Poisson matrix and the mass matrix for a two-dimensional problem; then the right-hand side is computed, and the Poisson matrix is factorized using the spotrf subroutine from the LAPACK package. Some additional subroutines give the right-hand side of the linear system and impose boundary conditions. The main program is formed by the definition and initialization of a Poisson object and a call to its solving procedure. The program contains four calls to external Fortran 77 subroutines. The subroutines zwgljd and dgljd2 in the grid_class compute the integration nodes, the integration weights, and the derivatives of Lagrangian interpolants. The Cholesky factorization and the substitution performed in the Poisson_class are achieved by calls to spotrf and spotrs from the LAPACK library. 5.2 Implementation of grid_class grid_class merits detailed description, since it exemplifies the concepts we wish to emphasize (the full listing of this class is given in the Appendix). An important topic of this section will be to illustrate the nonanticipation principle. Inside a module, the contains statement separates the definitions of the attributes and the protocol from the implementation of the methods. The module used_precision specifies the numerical model by the means of the float constant which indicates the floating-point arithmetic chosen for the computations. The protocol contains the initialization function (new_grid), the deallocation subroutine (dealloc_grid), and the functions by which some information is returned (give_n_g, give_vector_g, give_matrix_g). The computation procedures are private. The definitions of the attributes are embedded in a type definition. The meaning of the private statement inside the definition is that only procedures of the module are allowed to access the content of a variable of this type. The integration nodes and weights are stored in the arrays x and w; d is the derivative matrix of Lagrangian interpolants; dt is its transpose; and a is the canonical element Poisson operator. It is useful to distinguish between three kinds of methods: the constructor-destructor, the private, and the public methods. The methods of the first type are new_grid and dealloc_grid. The function new_grid initializes a 2
The routines are included in the Specmit library at MIT. Information about this library can be obtained from Einar Ronquist (
[email protected]).
ACM Transactions on Mathematical Software, Vol. 23, No. 1, March 1997.
Fortran 90: An Entry to Object-Oriented Programming
•
45
grid by fixing its number of nodes n. If n is not given in the calling statement, the function will ask the user interactively. Afterward, the arrays are nullified, i.e., they are defined but not associated. The purpose of dealloc_grid is to free the memory space occupied by the arrays when the information is no longer needed. The private methods are mainly computational routines. We find the ones which allocate and compute the arrays x, b, d, and dt: compute_xb and compute_ddt (these computations are performed by calls to the Fortran 77 Specmit library already mentioned). The subroutine compute_a builds up the Poisson operator. For the last kind of methods, we have give_vector_g, matrix_g, and give_n_g which return, respectively, vectors, matrices, and the number of nodes of the grid. Going through the subroutines give_vector_g and give_matrix_g provides the opportunity to illustrate an application of the nonanticipation principle. When a call to any of these subroutines occurs, the only requirement is that the grid is initialized (i.e., the new_grid function has been called). There is no assumption about what has already been computed. This explains the tests which verify if the needed vector or matrix is associated or not. In case of no association, the memory space is allocated and filled in by calling a computational subroutine. This nonanticipation principle allows the user to keep in mind a minimum of information: if the initialization function has already been processed, any message can be sent without any further assumption. Before closing this section, we note that a constructor should be applied automatically when an object is created, but Fortran 90 does not provide any mechanism to implement “automatic” methods. In Fortran 95, it is permissible to define initial values for components of types: e.g., TYPE grid PRIVATE INTEGER :: n50 ! number of nodes REAL(float),DIMENSION(:),POINTER :: z5.null( ) END TYPE grid
! nodes
Using that feature, it is easy to test, in a method, if the object has been properly initialized. For example, one can test the value of n, and if it is equal to zero the function new_grid is called. 6. CONCLUDING REMARKS In this article, we explored the Fortran 90 standard in order to show how some of its features can improve code design for solving partial differential equations. It is shown that modules, interface blocks, and derived data types allow us to adopt a programming style that is very close to objectoriented programming where a high level of abstraction and ease of code maintainability are achieved. Treating the example of a Poisson problem solver, we have observed that implementing object-oriented concepts within Fortran 90 requires us to set up a precise programming model and to follow a rigorous discipline. Special ACM Transactions on Mathematical Software, Vol. 23, No. 1, March 1997.
46
•
L. Machiels and M. O. Deville
attention has been devoted to the nonanticipation principle, which is as important as data abstraction in order to build autonomous objects. Finally, a significant advantage of Fortran 90 is that it is built up on top of Fortran 77, which is entirely supported by the new standard. So a programmer can plan a smooth transition toward object-oriented programming by a progressive rewriting while keeping Fortran 77 routines as implementation of methods in class definitions. APPENDIX MODULE grid_class
! Spectral primitives ! for a Gauss-Lobatto-Legendre grid
USE used_precision IMPLICIT NONE ! PUBLIC :: new_grid,dealloc_grid PUBLIC :: give_n_g,give_vector_g,give_matrix_g PRIVATE :: compute_xb,compute_ddt, compute_a ! TYPE, PUBLIC :: grid PRIVATE INTEGER n ! number of nodes REAL(float),DIMENSION(:),POINTER :: x ! nodes REAL(float),DIMENSION(:),POINTER :: b ! weights REAL(float),DIMENSION(:,:),POINTER :: d ! derivatives REAL(float),DIMENSION(:,:),POINTER :: dt ! transpose(d) REAL(float),DIMENSION(:,:),POINTER :: a ! Poisson operator END TYPE grid ! CONTAINS ! FUNCTION new_grid(n) RESULT(g)! initialization INTEGER,INTENT(IN),OPTIONAL :: n TYPE(grid) :: g IF (PRESENT(n)) THEN g%n5n ELSE PRINT p,“New grid” PRINT p,“--------” PRINT p,“Number of nodes ?” READ p, g%n ENDIF NULLIFY(g%x,g%b,g%d,g%dt,g%a) END FUNCTION new_grid ACM Transactions on Mathematical Software, Vol. 23, No. 1, March 1997.
Fortran 90: An Entry to Object-Oriented Programming
•
47
! SUBROUTINE dealloc_grid(g) ! destruction TYPE(grid) g g%n50 IF (ASSOCIATED(g%b)) DEALLOCATE(g%x,g%b) IF (ASSOCIATED(g%d)) DEALLOCATE(g%d,g%dt) IF (ASSOCIATED(g%a)) DEALLOCATE(g%a) END SUBROUTINE dealloc_grid ! SUBROUTINE compute_xb(g) ! computation of Gauss-Lobatto-Legendre nodes and weights TYPE(grid) g INTERFACE SUBROUTINE zwgljd(z,w,nzd,alpha,beta) INTEGER nzd REAL w(nzd),z(nzd),alpha,beta END SUBROUTINE zwgljd END INTERFACE ALLOCATE(g%x(g%n),g%b(g%n)) CALL zwgljd(g%x,g%b,g%n,zero,zero) END SUBROUTINE compute_xb ! SUBROUTINE compute_ddt(g) ! computation of Gauss-Lobatto-Legendre derivatives TYPE(grid) g INTERFACE SUBROUTINE dgljd(d,dt,z,nz,nzd,alpha,beta) INTEGER nz,nzd REAL d(nzd,nzd),dt(nzd,nzd),z(nzd),alpha,beta END SUBROUTINE dgljd END INTERFACE ALLOCATE(g%d(g%n,g%n),g%dt(g%n,g%n)) CALL dgljd(g%d,g%dt,g%x,g%n,g%n,zero,zero) END SUBROUTINE compute_ddt ! SUBROUTINE compute_a(g) ! computation of the Poisson operator ! on a canonical element TYPE(grid) g INTEGER n,i,j IF(.NOT.(ASSOCIATED(g%b))) CALL compute_xb(g) IF(.NOT.(ASSOCIATED(g%d))) CALL compute_ddt(g) n 5 g%n ACM Transactions on Mathematical Software, Vol. 23, No. 1, March 1997.
48
•
L. Machiels and M. O. Deville
ALLOCATE(g%a(n,n)) DO j51,n DO i51,j g%a(i,j)5 SUM(g%b(:)pg%d(:,j)pg%d(:,i)) ENDDO ENDDO SUBROUTINE compute_a ! FUNCTION give_vector_g(g,which) RESULT(vec) TYPE(grid) :: g CHARACTER(LEN5p) which REAL(float),DIMENSION(g%n) :: vec SELECT CASE (which) CASE(’b’) IF(.NOT.(ASSOCIATED(g%b))) CALL compute_xb(g) vec 5 g%b CASE(’x’) IF(.NOT.(ASSOCIATED(g%x))) CALL compute_xb(g) vec 5 g%x END SELECT END FUNCTION give_vector_g ! FUNCTION give_matrix_g(g,which) RESULT(mat) TYPE(grid) :: g CHARACTER(LEN5p) which REAL(float),DIMENSION(g%n,g%n) :: mat SELECT CASE (which) CASE(’a’) IF(.NOT.(ASSOCIATED(g%a))) CALL compute_a(g) mat 5 g%a CASE(’d’) IF(.NOT.(ASSOCIATED(g%d))) CALL compute_ddt(g) mat 5 g%d CASE(’dt’) IF(.NOT.(ASSOCIATED(g%dt))) CALL compute_ddt(g) mat 5 g%dt END SELECT END FUNCTION give_matrix_g ! FUNCTION give_n_g(g) ! give_n_g is the number of nodes ! of the grid INTEGER give_n_g ACM Transactions on Mathematical Software, Vol. 23, No. 1, March 1997.
Fortran 90: An Entry to Object-Oriented Programming
•
49
TYPE(grid),INTENT(IN) :: g give_n_g5g%n END FUNCTION give_n_g ! END MODULE grid_class ACKNOWLEDGMENTS
We wish to thank J.K. Reid and B. Einarsson for their comments and for running the code on several test cases. REFERENCES ADAMS, J. C., BRAINERD, W. S., MARTIN, J. T., SMITH, B. T., AND WAGENER, J. L. 1992. Fortran 90 Handbook. McGraw-Hill, New York. DUBOIS-P`ELERIN, Y. 1992. Object-oriented finite elements: Programming concepts and implementation. Ph.D. thesis, Swiss Federal Inst. of Technology, Lausanne, Switzerland. DUBOIS-P`ELERIN, Y. AND ZIMMERMANN, T. 1993. Object-Oriented Finite Element Programming: III. An Efficient Implementation in C11. Computer Methods in Applied Mechanics and Engineering, vol. 108. Elsevier Science, Amsterdam, 165–183. DUBOIS-P`ELERIN, Y., ZIMMERMANN, T., AND BOMME, P. 1992. Object-Oriented Finite Element Programming: II. A Prototype Program in Smalltalk. Computer Methods in Applied Mechanics and Engineering, vol. 98. Elsevier Science, Amsterdam, 361–397. GOLDBERG, A. AND ROBSON, D. 1983. SMALLTALK-80. The Language and Its Implementation. Addison-Wesley, Reading, Mass. KERRIGAN, J. 1993. Migrating to Fortran 90. O’Reilly and Assoc., Sebastopol, Calif. MADAY, Y. AND PATERA, A. T. 1989. Spectral element methods for incompressible NavierStokes equations. In State-of-the-Art Surveys on Computational Mechanics, A. K. Noor and J. T. Oden, Eds. ASME, New York, 71–143. METCALF, M. 1985. Effective FORTRAN 77. Clarendon Press, Oxford, U.K., 162–176. METCALF, M. AND REID, J. 1992. Fortran 90 Explained. Oxford University Press, Oxford, U.K. STROUSTRUP, B. 1991. The C11 Programming Language. Addison-Wesley, Reading, Mass. ZIMMERMANN, T., DUBOIS-P`ELERIN, Y., AND BOMME, P. 1992. Object-Oriented Finite Element Programming: I. Governing Principles. Computer Methods in Applied Mechanics and Engineering, vol. 98. Elsevier Science, Amsterdam, 291–303. Received June 1994; revised April and July 1995, January, February, March, and July 1996; accepted July 1996
ACM Transactions on Mathematical Software, Vol. 23, No. 1, March 1997.