Replacing legacy code with Julia

The nice thing about Fortran is backward compatibility. Granted, the routine doesn’t conform to “modern” programming conventions, but it still does the job.

Here’s the original routine from Bathe’s textbook (hopefully without any transcription errors!).

COLSOL

It’s best to compile COLSOL separately without warning flags. With gfortran use the -std=legacy flag.

      SUBROUTINE COLSOL (A,V,MAXA,NN,NWK,NNM,KKK,IOUT)
C. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 
C.                                                                     .
C.    P R O G R A M                                                    .
C.         TO SOLVE FINITE ELEMENT STATIC EQUILIBRIUM EQUATIONS IN     .
C.         CORE, USING COMPACTED STORAGE AND COLUMN REDUCTION SCHEME   .
C.                                                                     .
C.   - - INPUT VARIABLES --                                            .
C.         A(NWK)    = STIFFNESS MATRIX STORED IN COMPACTED FORM        . 
C.         V(NN)     = RIGHT-HAND-SIDE LOAD VECTOR                     .
C.         MAXA(NNM) = VECTOR CONTAINING ADDRESSES OF DIAGONAL         .
C.                     ELEMENTS OF STIFFNESS MATRIX IN A               .
C.         NN        = NUMBER OF EQUATIONS                             .
C.         NWK       = NUMBER OF ELEMENTS BELOW SKYLINE OF MATRIX      .
C.         NNM       = NN + 1                                          .
C.         KKK       = INPUT FLAG                                      .
C.             EQ. 1   TRIANGULARIZATION OF STIFFNESS MATRIX           .
C.             EQ. 2   REDUCTION AND BACK-SUBSTITUTION OF LOAD VECTOR  .
C.         IOUT      = UNIT NUMBER USED FOR OUTPUT                     .
C.                                                                     .
C.   - - OUTPUT - -                                                    .
C.         A(NWK)    = D AND L - FACTORS OF STIFFNESS MATRIX            .
C.         V(NN)     = DISPLACEMENT VECTOR                              .
C.                                                                     .
C. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .
      IMPLICIT DOUBLE PRECISION (A-H,O-Z)
C. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .
C.    THIS PROGRAM US USED IN SINGLE PRECISION ARITHMETIC ON CRAY
C.    EQUIPMENT AND DOUBLE PRECISION ARITHMETIC ON IBM MACHINES,
C.    ENGINEERING WORKSTATIONS AND PCS. DEACTIVATE ABOVE LINE FOR
C.    SINGLE PRECISION ARITHMETIC.
C. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .
      DIMENSION A(NWK),V(NN),MAXA(NNM)
C
C     PERFORM L*D*L(T) FACTORIZATION OF STIFFNESS MATRIX
C
      IF (KKK-2) 40,150,150
   40 DO 140 N=1,NN
      KN=MAXA(N)
      KL=KN + 1
      KU=MAXA(N+1) - 1
      KH=KU - KL
      IF (KH) 110,90,50
   50 K=N - KH
      IC=0
      KLT=KU
      DO 80 J=1,KH
      IC=IC + 1
      KLT=KLT - 1
      KI=MAXA(K)
      ND=MAXA(K+1) - KI - 1
      IF (ND) 80,80,60
   60 KK=MIN0(IC,ND)
      C=0.
      DO 70 L=1,KK
   70 C=C + A(KI+L)*A(KLT+L)
      A(KLT)=A(KLT) - C
   80 K=K+1
   90 K=N
      B=0.
      DO 100 KK=KL,KU
      K=K - 1
      KI=MAXA(K)
      C=A(KK)/A(KI)
      B=B + C*A(KK)
  100 A(KK)=C
      A(KN)=A(KN) - B
  110 IF (A(KN)) 120,120,140
  120 WRITE (IOUT,2000) N,A(KN)
      GO TO 800
  140 CONTINUE
      GO TO 900
C
C     REDUCE RIGHT-HAND-SIDE LOAD VECTOR
C
  150 DO 180 N=1,NN
      KL=MAXA(N) + 1
      KU=MAXA(N+1) - 1
      IF (KU-KL) 180,160,160
  160 K=N
      C=0.
      DO 170 KK=KL,KU
      K=K - 1
  170 C=C + A(KK)*V(K)
      V(N)=V(N) - C
  180 CONTINUE
C
C     BACK-SUBSTITUTE
C
      DO 200 N=1,NN
      K=MAXA(N)
  200 V(N)=V(N)/A(K)
      IF (NN.EQ.1) GO TO 900
      N=NN
      DO 230 L=2,NN
      KL=MAXA(N) + 1
      KU=MAXA(N+1) - 1
      IF (KU-KL) 230,210,210
  210 K=N
      DO 220 KK=KL,KU
      K=K - 1
  220 V(K)=V(K) - A(KK)*V(N)
  230 N=N - 1
      GO TO 900
C
  800 STOP
  900 RETURN
C
 2000 FORMAT(//' STOP - STIFFNESS MATRIX NOT POSITIVE DEFINITE',//,
     1         ' NONPOSITIVE PIVOT FOR EQUATION ',I8,//,
     2         ' PIVOT = ',E20.12 )
      END

I’ve also written a few usage examples:

program main

   use skyline
   implicit none

   type(skyline_type(:)), allocatable :: A

   call example_8p4(A)
   call example_8p5(A)
   call reaction_difffusion(A)

contains 

   ! EXAMPLE 8.4, pg. 710, Finite Element Procedures, Bathe
   !
   ! Evaluate the triangular factors D and L^T of the stiffness matrix
   !
   !        | 5 -4  1    |
   !        |    6 -4  1 |
   !   K =  |       6 -4 |
   !        | Sym      5 |
   !
   subroutine example_8p4(A)
      
      type(skyline_type(:)), allocatable, intent(out) :: A
      real(dp) :: D(4)
      integer :: i 

      allocate(skyline_type(4) :: A)

      A%nnz = 9

      A%dptr = [1,2,4,7,10]
      A%data = [5._dp,6._dp,-4._dp,6._dp,-4._dp,1._dp,5._dp,-4._dp,1._dp ]

      ! Known solution for D
      D = [5._dp,14._dp/5._dp,15._dp/7._dp,5._dp/6._dp]

      call factorize(A)

      write(*,'(9X,A20,2X,A20)') "found", "expected"
      do i = 1, A%n
         write(*,'("D(",I1,",",I1,") = ",E20.12,2X,E20.12)') &
            i, i, A%data(A%dptr(i)), D(i)
      end do

   end subroutine

   ! EXAMPLE 8.5, pg. 710, Finite Element Procedures, Bathe
   !
   ! Evaluate the triangular factors D and L^T of the stiffness matrix
   !
   !        | 2 -2       -1 |
   !        |    3 -2     0 |
   !   K =  |       5 -3  0 |
   !        |         10  4 |
   !        | Sym        10 |
   !
   subroutine example_8p5(A)

      type(skyline_type(:)), allocatable, intent(out) :: A
      real(dp) :: D(5)
      integer :: i 

      allocate(skyline_type(5) :: A)

      A%nnz = 12
      A%dptr = [1,2,4,6,8,13]
      A%data = [2,3,-2,5,-2,10,-3,10,4,0,0,-1]

      ! Known solution for D
      D = [2._dp,1._dp,1._dp,1._dp,0.5_dp]

      call factorize(A)

      write(*,'(9X,A20,2X,A20)') "found", "expected"
      do i = 1, A%n
         write(*,'("D(",I1,",",I1,") = ",E20.12,2X,E20.12)') &
            i, i, A%data(A%dptr(i)), D(i)
      end do

   end subroutine


   ! REACTION-DIFFUSION EQUATION
   !
   ! We solve the PDE
   !
   !    \Delta C - (Th)^2 C = 0
   !
   ! where \Delta is the Laplace operator, and Th is Thiele's modulus.
   ! The boundary conditions are given by
   !
   !    \partial_x C = 0, at x = 0  (Neumann)
   !               C = 1, at x = 1  (Dirichlet)
   !
   ! We solve the equation using a finite-difference method on a grid of
   ! five points. The analytical solution is given by
   !
   !    C(x) = cosh(Th*x)/cosh(Th)
   !
   subroutine reaction_difffusion(A)
      
      type(skyline_type(:)), allocatable, intent(out) :: A

      integer, parameter :: n = 5
      real(dp), parameter :: sigma = 1.0_dp   ! sigma := (Thiele)^2
      
      real(dp), allocatable :: b(:)
      real(dp) :: h, x
      integer :: i

      allocate(skyline_type(n-1) :: A)
      allocate(b(n))

      h = 1.0_dp / real(n - 1,dp)

      A%nnz = 7

      A%dptr = [1,2,4,6,8]

      allocate(A%data(A%nnz))
      A%data = [ 1._dp + 0.5_dp*h**2*sigma, &
                 2._dp + h**2*sigma, -1.0_dp, &
                 2._dp + h**2*sigma, -1.0_dp, &
                 2._dp + h**2*sigma, -1.0_dp ]

      call factorize(A)

      b(1:n-1) = [0, 0, 0, 1]

      call solve(A,b)
      b(n) = 1

      write(*,'(8X,A20,2X,A20)') "found", "true"
      do i = 1, n
         x = (i-1)*h
         write(*,'("c(",I2,") = ",E20.12,2X,E20.12)') i, b(i), &
            cosh(sqrt(sigma)*x)/(cosh(sqrt(sigma)))
      end do

   end subroutine

end program

As you can see, I’m not calling COLSOL directly, but working instead on a derived type representing a Sparse matrix in Skyline storage. The derived type and procedures are defined in the following module:

module skyline

   use, intrinsic :: iso_fortran_env, only: error_unit

   implicit none
   private

   public :: skyline_type, dp
   public :: factorize, solve

   integer, parameter :: dp = kind(1.0d0)

   type :: skyline_type(n)
      integer, len :: n
      integer :: nnz = -1
      integer :: dptr(n + 1)
      real(dp), allocatable :: data(:)
   end type

   interface
      subroutine colsol(a,v,maxa,nn,nwk,nnm,kkk,iout)
         import dp
         implicit none
         real(dp) :: a(*), v(*)
         integer :: maxa(nnm)
         integer :: nn, nwk, nnm, kkk, iout
      end subroutine
   end interface

contains

   !> Factorize symmetric positive-definite matrix in-place as LDL^T
   subroutine factorize(A)
      type(skyline_type(*)), intent(inout) :: A
      integer, parameter :: kkk = 1
      real(dp) :: vdummy(1)
      call colsol(A%data,vdummy,A%dptr,a%n,A%nnz,a%n+1,kkk,error_unit)
   end subroutine

   !> Solve Ax = b for x using pre-computed LDL^T factorization. 
   !> The solution x overwrites the RHS vector b.
   subroutine solve(A,b)
      type(skyline_type(*)), intent(in) :: A
      real(dp), intent(inout) :: b(:)  ! size should match A%n
      integer, parameter :: kkk = 2
      call colsol(A%data,b,A%dptr,a%n,A%nnz,a%n+1,kkk,error_unit)
   end subroutine

end module

One thing that took me a good hour to realize was that the storage format used by Bathe is not the storage format shown on Wikipedia. The columns are stored upwards, starting from the diagonal entries as shown in this snapshot from the textbook:

The programs above have been tested with the Intel Fortran Compiler Classic. Unfortunately, gfortran and ifx lag behind with support of parameterized derived types. It’s not difficult however to modify the example to use a non-parameterized derived type. Some helper routines for matrix construction would be desirable in both cases.

4 Likes