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:
where:
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#
Module Usage
iso_fortran_env
: Utilized for standard environment variables and constants, specificallyerror_unit
for error messages andDP
alias forREAL64
, indicating double precision.linalg_mod
: Externally defined module used here forwrite_array
andgenerate_random_array
, which handle array operations.
Variable Declarations
dimensions
: An allocatable integer array to store dimensions provided via command-line arguments.vector
: An allocatable vector of typereal(kind=DP)
.matrix
: An allocatable 2D array of typereal(kind=DP)
.
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.
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 onrank
.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#
Module Usage
iso_fortran_env
: Standard intrinsic module providing access to theerror_unit
andDP
(double precision kind type alias).linalg_mod
: Custom module providingread_array
andwrite_array
functions, used to handle input/output operations for matrices and vectors.
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 withdgesv
.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.
Explicit Interface for
dgesv
The subroutine’s interface is explicitly defined to ensure correct passing of arguments:
n
,nrhs
,a
,lda
,ipiv
,b
,ldb
, andinfo
describe the system dimensions, matrix and vector storage details, and output for status information.
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.
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#
Module
linalg_mod
Utilizes
iso_fortran_env
for standard environment definitions, such asoutput_unit
,error_unit
, andDP
(double precision floating-point kind).
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.
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
orread_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
orwrite_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
andwrite_vector
:Handle reading and writing operations for vectors, respectively.
read_matrix
andwrite_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:
The variables in the equations are converted into components of the \(\mathbf{x}\) vector as shown below:
These equations are converted into matrix form as shown below:
Therefore we have the following:
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.