Section: BLAS/LAPACK - Linear Equations#

Adapted from: gjbex/Fortran-MOOC

This program demonstrates solving linear algebra equations in Fortran.#

Linear Equation Problem#

In this notebook we will use Fortran to solve a system of linear equations of the form:

\[ \Large A \mathbf{x} = \mathbf{b} \]

where:

\[\begin{split} \Large A = \left[ \begin{array}{cccc} a_{11} & a_{12} & \cdots & a_{1n} \\ a_{21} & a_{22} & \cdots & a_{2n} \\ \vdots & \vdots & \vdots & \vdots \\ a_{m1} & a_{m2} & \cdots & a_{mn} \end{array} \right] \end{split}\]
\[\begin{split} \Large \mathbf{x} = \left[ \begin{array}{c} x_{1} \\ x_{2} \\ \vdots \\ x_{n} \end{array} \right] \end{split}\]
\[\begin{split} \Large \mathbf{b} = \left[ \begin{array}{c} b_{1} \\ b_{2} \\ \vdots \\ b_{n} \end{array} \right] \end{split}\]

The approach will be to have a main program that reads in as parameters the number of equations, the matrix \(\mathbf{A}\) and the vector \(\mathbf{x}\) as text files. The main program is called solve_equations.f90. It makes use of the LAPACK linear equation solver routine called DGESV. Information on this routine can be found at LAPACK - DGESV.

The main program makes use of a module called linalg_mod.f90, which contains subroutines to read and write matrices and arrays to and from text files.

Finally, there is a helper program called generate_array.f90 which has a subroutine that gets arguments from the command line and generates either of vector or matrix of specified size. Every element in the array or matrix is a random number.

Explanation of the Programs#

The following explanation is given by “Code Copilot” within ChatGPT 4.

Explaining generate_array.f90#

The Fortran program generate_array.f90 serves to generate either a vector or a matrix with random elements, depending on the command line arguments provided. It uses routines from a module named linalg_mod to handle array creation and output. Below is an explanation of the program structure and its key components:

Program Structure#

  1. Module Usage

    • iso_fortran_env: Utilized for standard environment variables and constants, specifically error_unit for error messages and DP alias for REAL64, indicating double precision.

    • linalg_mod: Externally defined module used here for write_array and generate_random_array, which handle array operations.

  2. Variable Declarations

    • dimensions: An allocatable integer array to store dimensions provided via command-line arguments.

    • vector: An allocatable vector of type real(kind=DP).

    • matrix: An allocatable 2D array of type real(kind=DP).

  3. Main Execution Logic

    • It begins by fetching command-line arguments into dimensions.

    • Based on the size of dimensions, it either generates a 1D vector or a 2D matrix:

      • For a single dimension, a vector of random elements is created and written.

      • For two dimensions, a matrix is created and written.

    • If neither one nor two dimensions are provided, it outputs an error indicating that only rank 1 and 2 arrays are supported.

  4. Error Handling

    • The program checks for proper memory allocation and command-line argument processing, with appropriate error messages and stops if any issues occur.

Subroutine: get_arguments#

  • Purpose: Fetches command-line arguments to determine the size of the array to be generated.

  • Process:

    • Determines the number of arguments (rank).

    • Allocates the dimensions array based on rank.

    • Reads each command argument into dimensions, converting from string to integer.

    • Handles errors in conversion and allocation, stopping the program if necessary.

Key Functionalities#

  • Array Generation: Depending on the input dimensions, it generates either a vector or a matrix with random values.

  • Modular Design: Leverages external modules for specific functionalities, which simplifies the main program.

  • Flexibility: The ability to handle both vectors and matrices makes the program versatile for different kinds of numerical data generation tasks.

Usage#

  • The program is executed with command-line arguments specifying the dimensions of the array to generate. For example:

    • ./generate_array 10 would generate a vector of size 10.

    • ./generate_array 3 4 would generate a 3x4 matrix.

Now, let’s move on to read and explain the solve_equations.f90 file.

Explaining solve_equations.f90#

The Fortran program solve_equations.f90 is designed to solve a system of linear equations using LAPACK’s dgesv subroutine, which solves systems of linear equations ( AX = B ) by performing LU decomposition with partial pivoting. Here’s a breakdown of its key components and functionalities:

Program Structure#

  1. Module Usage

    • iso_fortran_env: Standard intrinsic module providing access to the error_unit and DP (double precision kind type alias).

    • linalg_mod: Custom module providing read_array and write_array functions, used to handle input/output operations for matrices and vectors.

  2. Variable Declarations

    • nr_eqns: Number of equations in the system, derived from command-line arguments.

    • status, info: Variables for checking statuses of operations; info is used specifically with dgesv.

    • matrix_file_name, vector_file_name: File names for storing matrix ( A ) and vector ( B ).

    • b: Vector ( B ) in the equation ( AX = B ).

    • pivot: Integer array for pivot indices from the LU decomposition.

    • A: Matrix ( A ) of the system.

  3. Explicit Interface for dgesv

    • The subroutine’s interface is explicitly defined to ensure correct passing of arguments:

      • n, nrhs, a, lda, ipiv, b, ldb, and info describe the system dimensions, matrix and vector storage details, and output for status information.

  4. Main Execution Logic

    • Argument Reading: Fetches command-line arguments to determine the number of equations and file names for matrix ( A ) and vector ( B ).

    • Array Allocation: Allocates memory for ( A ), ( B ), and pivot indices.

    • File Reading: Reads matrix ( A ) and vector ( B ) from files specified by the user.

    • System Solving: Calls dgesv to solve the linear system, where ( A ) is modified to store the factors from the LU decomposition, and ( B ) is overwritten with the solution ( X ).

    • Output: Writes the solution vector ( X ) back to a file.

  5. Error Handling

    • Checks for successful allocation and reading operations, with appropriate error messages and program termination on failures.

Subroutine: get_arguments#

  • Purpose: Fetches and validates command-line arguments to ensure correct program setup.

  • Parameters:

    • nr_eqns: Number of equations/system size.

    • matrix_file_name, vector_file_name: File paths for the matrix and vector data.

Key Functionalities#

  • Linear System Solver: Uses dgesv for robust and efficient solving of linear systems, leveraging LAPACK’s optimized routines.

  • Modular File Handling: Employs modular functions from linalg_mod for input/output operations, promoting code reuse and organization.

  • Error Management: Extensive error checking ensures the program gracefully handles common issues like invalid input or memory allocation failures.

Usage#

The program is intended to be executed with three command-line arguments specifying the number of equations, matrix file name, and vector file name. For example:

./solve_equations 3 matrix.dat vector.dat

This would solve a 3x3 system of equations using matrix and vector data from matrix.dat and vector.dat.

Next, let’s proceed to read and explain the linalg_mod.f90 file.

Explaining linalg_mod.f90#

The Fortran module linalg_mod.f90 provides utility functions for reading, writing, and generating arrays. This module likely supports the functionality required by the other programs you uploaded (generate_array.f90 and solve_equations.f90). Here’s a breakdown of its contents and functionalities:

Module Structure and Functions#

  1. Module linalg_mod

    • Utilizes iso_fortran_env for standard environment definitions, such as output_unit, error_unit, and DP (double precision floating-point kind).

  2. Functionality Exposed

    • read_array: Reads an array from a file whose dimensions and rank are defined by the inputs.

    • write_array: Writes an array to a file or standard output, handling both vectors and matrices.

    • generate_random_array: Fills an allocatable array with random numbers, based on the dimensions provided.

  3. Detailed Function Descriptions

    • read_array:

      • Takes a file name, an array (allocatable), and its expected dimensions.

      • Checks if the rank of the array matches the number of dimensions provided.

      • Depending on the rank, it calls read_vector or read_matrix to perform the read operation.

      • Errors are managed by stopping the program if the ranks or dimensions do not match expectations.

    • write_array:

      • Accepts an array and an optional file name for output.

      • Uses rank-specific procedures (write_vector or write_matrix) to handle the output based on the array’s rank.

      • If no file name is provided, the output is directed to the standard output (output_unit).

    • generate_random_array:

      • Designed to allocate and populate an array (either vector or matrix) with random numbers.

      • Checks the allocation status and initializes the array with random numbers using the intrinsic random_number procedure.

      • Manages errors related to mismatched ranks or failed allocations.

Subroutines for Vector and Matrix Handling#

  • read_vector and write_vector:

    • Handle reading and writing operations for vectors, respectively.

  • read_matrix and write_matrix:

    • Handle reading and writing operations for matrices, respectively.

Key Functionalities and Error Handling#

  • The module is equipped to handle errors related to input/output operations and array manipulations, which is critical for robust numerical software.

  • Error messages are detailed, including specific issues like mismatched array ranks or failed memory allocations, which aids in debugging and user feedback.

  • Flexibility to output to either a file or standard output enhances the module’s usability across different computational scenarios.

Usage#

This module is utilized by other programs for managing arrays, particularly in numerical computations involving linear algebra. It abstracts away the details of file I/O and array initialization, allowing other parts of the software to focus on core computational logic.

Overall, linalg_mod.f90 serves as a foundational component for array management in Fortran programs, providing essential services such as reading, writing, and generating arrays with a focus on error handling and user feedback.

Program Code#

The individual program files are listed below:

In file linalg_mod.f90#

module linalg_mod
    use, intrinsic :: iso_fortran_env, only : output_unit, error_unit, DP => REAL64
    implicit none

    private

    public :: read_array, write_array, generate_random_array

contains

    subroutine read_array(file_name, array, dimensions)
        implicit none
        character(len=*), intent(in) :: file_name
        real(kind=DP), dimension(..), allocatable, intent(out) :: array
        integer, dimension(:), intent(in) :: dimensions

        if (rank(array) /= size(dimensions)) then
            write (unit=error_unit, fmt='(A)') '# error: array rank does not match dimensions'
            stop 3
        end if
        select rank(array)
            rank(1)
                call read_vector(file_name, dimensions(1), array)
            rank(2)
                call read_matrix(file_name, dimensions(1), dimensions(2), array)
            rank default
                write (unit=error_unit, fmt='(A, I0, A)') &
                    '# error: rank ', rank(array), ' not supported'
                stop 4
        end select
    end subroutine read_array

    subroutine write_array(array, file_name)
        implicit none
        real(kind=DP), dimension(..), intent(in) :: array
        character(len=*), intent(in), optional :: file_name

        select rank(array)
            rank(1)
                call write_vector(array, file_name)
            rank(2)
                call write_matrix(array, file_name)
            rank default
                write (unit=error_unit, fmt='(A, I0, A)') &
                    '# error: rank ', rank(array), ' not supported'
                stop 4
        end select
    end subroutine write_array

    subroutine read_vector(file_name, n, vector)
        implicit none
        character(len=*), intent(in) :: file_name
        integer, value :: n
        real(kind=DP), dimension(:), allocatable, intent(out) :: vector
        integer :: i, status, unit_nr
        character(len=1024) :: msg

        open (newunit=unit_nr, file=trim(file_name), access='sequential', &
            action='read', status='old', iostat=status, iomsg=msg)
        if (status /= 0) then
            write (unit=error_unit, fmt='(2A)') '# error: ', trim(msg)
            stop 1
        end if

        if (.not. allocated(vector)) then
            allocate (vector(n), stat=status)
            if (status /= 0) then
                write (unit=error_unit, fmt='(2A)') '# error: ', trim(msg)
                stop 3
            end if
        end if

        do i = 1, n
            read (unit=unit_nr, fmt=*, iostat= status, iomsg=msg) vector(i)
            if (status /= 0) then
                write (unit=error_unit, fmt='(2A)') '# error: ', trim(msg)
                stop 2
            end if
        end do

        close (unit=unit_nr)

    end subroutine read_vector

    subroutine read_matrix(file_name, nr_rows, nr_cols, matrix)
        implicit none
        character(len=*), intent(in) :: file_name
        integer, value :: nr_rows, nr_cols
        real(kind=DP), dimension(:, :), allocatable, intent(out) :: matrix
        integer :: i, status, unit_nr
        character(len=1024) :: msg

        open (newunit=unit_nr, file=trim(file_name), access='sequential', &
            action='read', status='old', iostat=status, iomsg=msg)
        if (status /= 0) then
            write (unit=error_unit, fmt='(2A)') '# error: ', trim(msg)
            stop 1
        end if

        if (.not. allocated(matrix)) then
            allocate (matrix(nr_rows, nr_cols), stat=status)
            if (status /= 0) then
                write (unit=error_unit, fmt='(2A)') '# error: ', trim(msg)
                stop 3
            end if
        end if

        do i = 1, nr_rows
            read (unit=unit_nr, fmt=*, iostat= status, iomsg=msg) matrix(i, :)
            if (status /= 0) then
                write (unit=error_unit, fmt='(2A)') '# error: ', trim(msg)
                stop 2
            end if
        end do

        close (unit=unit_nr)

    end subroutine read_matrix

    subroutine write_vector(vector, file_name)
        implicit none
        real(kind=DP), dimension(:), intent(in) :: vector
        character(len=*), intent(in), optional :: file_name
        integer :: unit_nr, i, status
        character(len=1024) :: msg

        if (present(file_name)) then
            open (newunit=unit_nr, file=trim(file_name), access='sequential', &
                action='write', status='replace', iostat=status, iomsg=msg)
            if (status /= 0) then
                write (unit=error_unit, fmt='(2A)') '# error: ', trim(msg)
                stop 1
            end if
        else
            unit_nr = output_unit
        end if

        do i = 1, size(vector)
            write (unit=unit_nr, fmt='(F27.15)') vector(i)
        end do

        if (present(file_name)) close (unit_nr)
    end subroutine write_vector

    subroutine write_matrix(matrix, file_name)
        implicit none
        real(kind=DP), dimension(:, :), intent(in) :: matrix
        character(len=*), intent(in), optional :: file_name
        integer :: unit_nr, i, status
        character(len=1024) :: msg

        if (present(file_name)) then
            open (newunit=unit_nr, file=trim(file_name), access='sequential', &
                action='write', status='replace', iostat=status, iomsg=msg)
            if (status /= 0) then
                write (unit=error_unit, fmt='(2A)') '# error: ', trim(msg)
                stop 1
            end if
        else
            unit_nr = output_unit
        end if

        do i = 1, size(matrix, 1)
            write (unit=unit_nr, fmt='(*(F27.15))') matrix(i, :)
        end do

        if (present(file_name)) close (unit_nr)
    end subroutine write_matrix

    subroutine generate_random_array(array, dimensions)
        implicit none
        real(kind=DP), dimension(..), allocatable, intent(out) :: array
        integer, dimension(:), intent(in) :: dimensions
        integer :: status

        if (rank(array) /= size(dimensions)) then
            write (unit=error_unit, fmt='(A)') '# error: array rank does not match dimensions'
            stop 3
        end if
        select rank(array)
            rank(1)
                if (.not. allocated(array)) then
                    allocate(array(dimensions(1)), stat=status)
                    if (status /= 0) then
                        write (unit=error_unit, fmt='(A, I0)') &
                            '# error: can not allocate vector of size ', dimensions(1)
                        stop 6
                    end if
                end if
                call random_number(array)
            rank(2)
                if (.not. allocated(array)) then
                    allocate(array(dimensions(1), dimensions(2)), stat=status)
                    if (status /= 0) then
                        write (unit=error_unit, fmt='(A,I0, A, I0)') &
                            '# error: can not allocate matrix of size ', &
                            dimensions(1), ' by ', dimensions(2)
                        stop 6
                    end if
                end if
                call random_number(array)
            rank default
                write (unit=error_unit, fmt='(A, I0, A)') &
                    '# error: rank ', rank(array), ' not supported'
                stop 4
        end select
    end subroutine generate_random_array

end module linalg_mod

In file generate_array.f90#

program generate_array
    use, intrinsic :: iso_fortran_env, only : error_unit, DP => REAL64
    use :: linalg_mod, only : write_array, generate_random_array
    implicit none
    integer, dimension(:), allocatable :: dimensions
    real(kind=DP), dimension(:), allocatable :: vector
    real(Kind=DP), dimension(:, :), allocatable :: matrix

    call get_arguments(dimensions)
    if (size(dimensions) == 1) then
        call generate_random_array(vector, dimensions)
        call write_array(vector)
        deallocate (vector)
    else if (size(dimensions) == 2) then
        call generate_random_array(matrix, dimensions)
        call write_array(matrix)
        deallocate (matrix)
    else
        write (unit=error_unit, fmt='(A)') '# error: only rank 1 and 2 are supported'
    end if

contains

   subroutine get_arguments(dimensions)
        implicit none
        integer, dimension(:), allocatable, intent(out) :: dimensions
        integer :: rank, status, i
        character(len=1024) :: buffer, msg
        
        rank = command_argument_count()
        if (rank < 1) then
            write (unit=error_unit, fmt='(A)') '# error: rank should be at least 1'
            stop 10
        end if
        allocate (dimensions(rank), stat=status)
        if (status /= 0) then
            write (unit=error_unit, fmt='(A)') '# error: can not allocate dimensions'
            stop 11
        end if

        do i = 1, rank
            call get_command_argument(i, buffer)
            read (buffer, fmt=*, iostat=status, iomsg=msg) dimensions(i)
            if (status /= 0) then
                write (unit=error_unit, fmt='(2A)') '# error: ', trim(msg)
                stop 11
            end if
        end do
    end subroutine get_arguments

end program generate_array

In solve_equations.f90#

program main
    use, intrinsic :: iso_fortran_env, only : error_unit, DP => REAL64
    use :: linalg_mod, only : read_array, write_array
    implicit none
    ! Made info an integer because the LAPACK routine DGESV uses an explicit
    ! interface.  In the explicit interface, the argument info is an integer.
    integer :: nr_eqns, status, info
    character(len=1024) :: matrix_file_name, vector_file_name
    ! The following line no longer works because the LAPACK routine DGESV uses
    ! an explicit interface.  In the explicit interface, the argument info
    ! is an integer, but in the implicit interface, it is a character(len=1024).
    !character(len=1024) :: matrix_file_name, vector_file_name, info
    real(kind=DP), dimension(:), allocatable :: b
    integer, dimension(:), allocatable :: pivot
    real(kind=DP), dimension(:, :), allocatable :: A
    !external :: dgesv ! not needed in gfortran and throws an error

    ! Explicit interface for the LAPACK routine DGESV
    interface
        subroutine dgesv(n, nrhs, a, lda, ipiv, b, ldb, info)
            import :: DP
            implicit none
            ! .. Scalar Arguments ..
            integer, intent (Out)             :: info
            integer, intent (In)              :: lda, ldb, n, nrhs
            ! .. Array Arguments ..
            real (Kind=dp), intent (Inout)    :: a(lda, *), b(ldb, *)
            integer, Intent (Out)             :: ipiv(n)
        end subroutine dgesv
    end interface

    call get_arguments(nr_eqns, matrix_file_name, vector_file_name)

    allocate (A(nr_eqns, nr_eqns), stat=status)

    if (status /= 0) then
        write (unit=error_unit, fmt='(A)') '#error: can not allocate A'
        stop 15
    end if

    call read_array(matrix_file_name, A, [ nr_eqns, nr_eqns ])

    allocate (b(nr_eqns), stat=status)

    if (status /= 0) then
        write (unit=error_unit, fmt='(A)') '#error: can not allocate b'
        stop 15
    end if

    call read_array(vector_file_name, b, [ nr_eqns ])

    allocate (pivot(size(b)), stat=status)

    if (status /= 0) then
        write (unit=error_unit, fmt='(A, I0)') '# error: can not allocate pivot of size ', size(b)
        stop 12
    end if

    call dgesv(size(A, 2), 1, A, size(A, 1), pivot, b, size(b), info)
    
    call write_array(b)

    deallocate (A, b, pivot)

contains

   subroutine get_arguments(nr_eqns, matrix_file_name, vector_file_name)
        implicit none
        integer, intent(out) :: nr_eqns
        character(len=*), intent(out) :: matrix_file_name, vector_file_name
        integer :: status
        character(len=1024) :: buffer, msg
        
        if (command_argument_count() /= 3) then
            write (unit=error_unit, fmt='(A)') &
                '# error: argument nr_eqns, matrix_file_name, vector_file_name'
            stop 10
        end if

        call get_command_argument(1, buffer)
        read (buffer, fmt=*, iostat=status, iomsg=msg) nr_eqns
        if (status /= 0) then
            write (unit=error_unit, fmt='(2A)') '# error: ', trim(msg)
            stop 11
        end if

        call get_command_argument(2, matrix_file_name)
        call get_command_argument(3, vector_file_name)
    end subroutine get_arguments

end program main

The above programs are compiled and run using Fortran Package Manager (fpm):

Build the Program using FPM (Fortran Package Manager)#

import os
root_dir = ""
root_dir = os.getcwd()

Since the code makes use of the LAPACK library, the following FPM configuration file (fpm.toml) was used:

name = "Section_BLAS_LAPACK_Linear_Equations"

[build]
auto-executables = true
auto-tests = true
auto-examples = true
link = ["blas", "lapack"]

[install]
library = false

[[executable]]
name="solve_equations"
source-dir="app"
main="solve_equations.f90"
code_dir = root_dir + "/" + "Fortran_Code/Section_BLAS_LAPACK_Linear_Equations"
os.chdir(code_dir)

The files solve_equations.f90 and generate_array.f90 were placed into the “app” folder, while the file linalg_mod.f90 was placed into the “src” folder.

build_status = os.system("fpm build 2>/dev/null")

Run the Program using FPM (Fortran Package Manager)#

Solve a Test Linear System of Two Equations#

As our first run, we wish to solve the following set of linear equations:

\[\begin{split} \begin{align*} 2x+8y & = 20 \\ x+2y & = 4 \end{align*} \end{split}\]

The variables in the equations are converted into components of the \(\mathbf{x}\) vector as shown below:

\[\begin{split} \begin{align*} 2x_1+8x_2 & = 20 \\ x_1+2x_2 & = 4 \end{align*} \end{split}\]

These equations are converted into matrix form as shown below:

\[\begin{split} \begin{equation*} \left[ \begin{array}{cc} 2 & 8 \\ 1 & 2 \\ \end{array} \right] \left[ \begin{array}{c} x_1 \\ x_2 \\ \end{array} \right] = \left[ \begin{array}{c} 20 \\ 4 \\ \end{array} \right] \end{equation*} \end{split}\]

Therefore we have the following:

\[\begin{split} \mathbf{A} = \left[ \begin{array}{cc} 2 & 8 \\ 1 & 2 \end{array} \right] \end{split}\]
\[\begin{split} \mathbf{x} = \left[ \begin{array}{c} x_1 \\ x_2 \end{array} \right] \end{split}\]
\[\begin{split} \mathbf{b} = \left[ \begin{array}{c} 20 \\ 4 \end{array} \right] \end{split}\]

The matrix \(\mathbf{A}\) and the vector \(\mathbf{b}\) are written into text files as shown below:

%%writefile A_test1.txt
2 8
1 2
Overwriting A_test1.txt
%%writefile b_test1.txt
20
4
Overwriting b_test1.txt

The solve_equations program can now be run with the number of equations command line argument set to 2, and the files A_test1.txt and b_test1.txt

exec_status = \
    os.system("fpm run solve_equations 2>/dev/null -- 2 A_test1.txt b_test1.txt")
         -2.000000000000000
          3.000000000000000

The results are printed in scientfic notation and in the order of \(x_1\), \(x_2\).

We now wish to use Python’s Numpy library to test these results:

import numpy as np

A = np.genfromtxt("A_test1.txt")
b = np.genfromtxt("b_test1.txt")
x = np.linalg.solve(A, b)
print("x1 = {0:2.1f}".format(x[0]))
print("x2 = {0:2.1f}".format(x[1]))
x1 = -2.0
x2 = 3.0

We can see that the Fortran code and Numpy produce the same results.

Solve a Test Linear System of Equations of Arbitrary Size#

The Fortran code can be used to solve an arbitrarily large system of equations. To test this functionality, we make use of the generate_array.f90 program to generate arrays or matrices of arbitrary size filled with random numbers.

As a start, we will use the generate_array.f90 to generate a matrix file A_test2.txt that contains a 10x10 matrix.

exec_status = os.system("fpm run generate_array 2>/dev/null -- 10 10 > A_test2.txt") 

The \(\mathbf{A}\) matrix is shown below:

import pandas as pd
A = pd.read_table("A_test2.txt", 
    header=None, 
    sep='\s+')
A
0 1 2 3 4 5 6 7 8 9
0 0.529713 0.722936 0.231301 0.533395 0.891596 0.231641 0.382307 0.784340 0.777888 0.606638
1 0.132722 0.416119 0.974570 0.749411 0.815394 0.745239 0.121193 0.507933 0.318646 0.249498
2 0.641127 0.984348 0.603688 0.903587 0.614877 0.949881 0.278559 0.883549 0.399152 0.321099
3 0.725387 0.188166 0.631688 0.896246 0.653417 0.409462 0.237410 0.426012 0.291939 0.767255
4 0.793730 0.888977 0.230383 0.101780 0.502465 0.534204 0.285539 0.014302 0.790809 0.646838
5 0.955425 0.259485 0.717656 0.798483 0.273497 0.402048 0.477898 0.208237 0.666987 0.543680
6 0.297030 0.223514 0.749741 0.507576 0.703502 0.503245 0.469250 0.482831 0.400218 0.174519
7 0.404592 0.620247 0.817913 0.786653 0.971986 0.508362 0.439124 0.751862 0.552740 0.859885
8 0.293671 0.249501 0.550982 0.743546 0.541855 0.994462 0.607571 0.356741 0.564942 0.236013
9 0.263760 0.263627 0.294597 0.858652 0.028307 0.666393 0.871802 0.514591 0.288463 0.667020

And now we generate the \(\mathbf{b}\) vector:

exec_status = os.system("fpm run generate_array 2>/dev/null -- 10 > b_test2.txt") 

The \(\mathbf{b}\) vector is shown below:

b = pd.read_table("b_test2.txt", 
    header=None, 
    sep='\s+')
b
0
0 0.388090
1 0.715672
2 0.681702
3 0.699964
4 0.878440
5 0.511733
6 0.316539
7 0.166531
8 0.834975
9 0.932608

We now use the solve_equation Fortran code to solve this linear system of equations:

exec_status = \
    os.system("fpm run solve_equations 2>/dev/null -- 10 A_test2.txt b_test2.txt")
         -1.734067700216211
          9.343018075590516
         -3.314280772160507
         11.380226062050223
          7.162188262361305
         -6.654541818653176
          7.590035710990191
        -13.172082562646260
         -6.180500314008898
         -5.535794957580744

And the results are compared to the output of Numpy:

A = np.genfromtxt("A_test2.txt")
b = np.genfromtxt("b_test2.txt")
x = np.linalg.solve(A, b)

for i in range(len(x)):
    print ("x{0:d} = {1:2.6f}".format(i+1, x[i]))
x1 = -1.734068
x2 = 9.343018
x3 = -3.314281
x4 = 11.380226
x5 = 7.162188
x6 = -6.654542
x7 = 7.590036
x8 = -13.172083
x9 = -6.180500
x10 = -5.535795

Again, we see that the results are the same.