diff --git a/matlab/cycle_reduction.m b/matlab/missing/mex/cycle_reduction/cycle_reduction.m similarity index 90% rename from matlab/cycle_reduction.m rename to matlab/missing/mex/cycle_reduction/cycle_reduction.m index a1c0197e1f41afeb497661842e0616d1027b8e4f..18ba3637f47cba994e6571f32edbf961cb8a0ca3 100644 --- a/matlab/cycle_reduction.m +++ b/matlab/missing/mex/cycle_reduction/cycle_reduction.m @@ -88,14 +88,13 @@ while cont if norm(A2,1) < cvg_tol cont = 0; end - elseif isnan(crit) || it == max_it - if crit < cvg_tol - info(1) = 4; - info(2) = log(norm(A2,1)); - else - info(1) = 3; - info(2) = log(norm(A1,1)); - end + elseif it == max_it + info(1) = 401; + info(2) = log(norm(A1,1)); + return + elseif isnan(crit) + info(1) = 402; + info(2) = log(norm(A1,1)) return end it = it + 1; @@ -105,9 +104,11 @@ X = -Ahat1\A0_0; if (nargin == 5 && ~isempty(ch) ) %check the solution - res = A0_0 + A1_0 * X + A2_0 * X * X; - if (sum(sum(abs(res))) > cvg_tol) - disp(['the norm residual of the residu ' num2str(res) ' compare to the tolerance criterion ' num2str(cvg_tol)]); + res = norm(A0_0 + A1_0 * X + A2_0 * X * X, 1); + if (res > cvg_tol) + info(1) = 403 + info(2) = log(res) + error(['The norm of the residual is ' num2str(res) ' whereas the tolerance criterion is ' num2str(cvg_tol)]); end end diff --git a/mex/build/cycle_reduction.am b/mex/build/cycle_reduction.am new file mode 100644 index 0000000000000000000000000000000000000000..a5aa334c9f1c403aeb8d29519adde4d4a3e203e6 --- /dev/null +++ b/mex/build/cycle_reduction.am @@ -0,0 +1,17 @@ +mex_PROGRAMS = cycle_reduction + +nodist_cycle_reduction_SOURCES = \ + mexFunction.f08 \ + matlab_mex.F08 \ + blas_lapack.F08 + +BUILT_SOURCES = $(nodist_cycle_reduction_SOURCES) +CLEANFILES = $(nodist_cycle_reduction_SOURCES) + +mexFunction.o: matlab_mex.mod blas.mod lapack.mod + +%.f08: $(top_srcdir)/../../sources/cycle_reduction/%.f08 + $(LN_S) -f $< $@ + +%.F08: $(top_srcdir)/../../sources/cycle_reduction/%.F08 + $(LN_S) -f $< $@ diff --git a/mex/build/matlab/Makefile.am b/mex/build/matlab/Makefile.am index 091eec8a1be72668f56e58eb52402b2762bb880a..e6617b39c2acbc56b2aaf519abbda9233ef2b5cb 100644 --- a/mex/build/matlab/Makefile.am +++ b/mex/build/matlab/Makefile.am @@ -1,6 +1,6 @@ ACLOCAL_AMFLAGS = -I ../../../m4 -SUBDIRS = mjdgges kronecker bytecode block_kalman_filter sobol perfect_foresight_problem num_procs block_trust_region disclyap_fast libkordersim local_state_space_iterations folded_to_unfolded_dr k_order_simul k_order_mean +SUBDIRS = mjdgges kronecker bytecode block_kalman_filter sobol perfect_foresight_problem num_procs block_trust_region disclyap_fast libkordersim local_state_space_iterations folded_to_unfolded_dr k_order_simul k_order_mean cycle_reduction # libdynare++ must come before gensylv and k_order_perturbation if ENABLE_MEX_DYNAREPLUSPLUS diff --git a/mex/build/matlab/configure.ac b/mex/build/matlab/configure.ac index 7e275f8f32f5b36866a9dc58e6b37e9e10431bbc..97f5f336815786ebc4b70215aa06fdc8c9a5bb7d 100644 --- a/mex/build/matlab/configure.ac +++ b/mex/build/matlab/configure.ac @@ -169,6 +169,7 @@ AC_CONFIG_FILES([Makefile perfect_foresight_problem/Makefile num_procs/Makefile block_trust_region/Makefile - disclyap_fast/Makefile]) + disclyap_fast/Makefile + cycle_reduction/Makefile]) AC_OUTPUT diff --git a/mex/build/matlab/cycle_reduction/Makefile.am b/mex/build/matlab/cycle_reduction/Makefile.am new file mode 100644 index 0000000000000000000000000000000000000000..f05308c1f470bfeb94c1cd51a500d029c1f6fe48 --- /dev/null +++ b/mex/build/matlab/cycle_reduction/Makefile.am @@ -0,0 +1,2 @@ +include ../mex.am +include ../../cycle_reduction.am diff --git a/mex/build/octave/Makefile.am b/mex/build/octave/Makefile.am index 495d73e7278c574e578195f87aed02bbd28991a6..7057795ede177336796323ef7a76ea5fc69c43b0 100644 --- a/mex/build/octave/Makefile.am +++ b/mex/build/octave/Makefile.am @@ -1,6 +1,6 @@ ACLOCAL_AMFLAGS = -I ../../../m4 -SUBDIRS = mjdgges kronecker bytecode block_kalman_filter sobol perfect_foresight_problem num_procs block_trust_region disclyap_fast libkordersim local_state_space_iterations folded_to_unfolded_dr k_order_simul k_order_mean +SUBDIRS = mjdgges kronecker bytecode block_kalman_filter sobol perfect_foresight_problem num_procs block_trust_region disclyap_fast libkordersim local_state_space_iterations folded_to_unfolded_dr k_order_simul k_order_mean cycle_reduction # libdynare++ must come before gensylv and k_order_perturbation if ENABLE_MEX_DYNAREPLUSPLUS diff --git a/mex/build/octave/configure.ac b/mex/build/octave/configure.ac index 32c0c97551d11de27a9d53912f043dde8da36142..9806965a31aa0267b9004ab6585c44b9f6841431 100644 --- a/mex/build/octave/configure.ac +++ b/mex/build/octave/configure.ac @@ -172,6 +172,7 @@ AC_CONFIG_FILES([Makefile perfect_foresight_problem/Makefile num_procs/Makefile block_trust_region/Makefile - disclyap_fast/Makefile]) + disclyap_fast/Makefile + cycle_reduction/Makefile]) AC_OUTPUT diff --git a/mex/build/octave/cycle_reduction/Makefile.am b/mex/build/octave/cycle_reduction/Makefile.am new file mode 100644 index 0000000000000000000000000000000000000000..f8ae0acd3f852975e6a4c92bcf51814d7b6b5cc6 --- /dev/null +++ b/mex/build/octave/cycle_reduction/Makefile.am @@ -0,0 +1,3 @@ +EXEEXT = .mex +include ../mex.am +include ../../cycle_reduction.am diff --git a/mex/sources/Makefile.am b/mex/sources/Makefile.am index c1e6727b952d5782ac46a517ade6d27bd837942b..9ebd3edf8181f7aa088669e66fcf5710be680a2e 100644 --- a/mex/sources/Makefile.am +++ b/mex/sources/Makefile.am @@ -25,7 +25,8 @@ EXTRA_DIST = \ perfect_foresight_problem \ num_procs \ block_trust_region \ - disclyap_fast + disclyap_fast \ + cycle_reduction clean-local: rm -rf `find mex/sources -name *.o` diff --git a/mex/sources/blas_lapack.F08 b/mex/sources/blas_lapack.F08 index 8df746a17734744deab6fc495044168483a9706f..a597ca5006aecde7ac075c8cbb7c8a0b2b9016f6 100644 --- a/mex/sources/blas_lapack.F08 +++ b/mex/sources/blas_lapack.F08 @@ -93,4 +93,16 @@ module lapack integer(blint), intent(out) :: info end subroutine dpotrf end interface + + interface + function dlange(norm, m, n, a, lda, work) + import :: blint, real64 + character, intent(in) :: norm + integer(blint), intent(in) :: m, n, lda + real(real64), dimension(*), intent(in) :: a + real(real64), dimension(*), intent(out) :: work + real(real64) :: dlange + end function dlange + end interface + end module lapack diff --git a/mex/sources/cycle_reduction/mexFunction.f08 b/mex/sources/cycle_reduction/mexFunction.f08 new file mode 100644 index 0000000000000000000000000000000000000000..e0e5fe8a3c463ff81eb3669ed6282b3aaf1c59b7 --- /dev/null +++ b/mex/sources/cycle_reduction/mexFunction.f08 @@ -0,0 +1,233 @@ +! Copyright © 2022 Dynare Team +! +! This file is part of Dynare. +! +! Dynare is free software: you can redistribute it and/or modify +! it under the terms of the GNU General Public License as published by +! the Free Software Foundation, either version 3 of the License, or +! (at your option) any later version. +! +! Dynare is distributed in the hope that it will be useful, +! but WITHOUT ANY WARRANTY; without even the implied warranty of +! MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +! GNU General Public License for more details. +! +! You should have received a copy of the GNU General Public License +! along with Dynare. If not, see <https://www.gnu.org/licenses/>. + +! Implements the cyclic reduction algorithm described in +! D.A. Bini, G. Latouche, B. Meini (2002), "Solving matrix polynomial equations arising in queueing problems", Linear Algebra and its Applications 340, pp. 222-244 +! D.A. Bini, B. Meini (1996), "On the solution of a nonlinear matrix equation arising in queueing problems", SIAM J. Matrix Anal. Appl. 17, pp. 906-926. + +module reduction + use matlab_mex + use lapack + use blas + implicit none + +contains + ! Matlab-like norm function calling lapack DLANGE routine + real(real64) function norm(A, c) + real(real64), dimension(:,:), intent(in) :: A + character, intent(in) :: c + real(real64), dimension(:), allocatable :: work + integer(blint) :: nrow + nrow = int(size(A,1),blint) + norm = dlange(c, nrow, int(size(A,2), blint), & + &A, nrow, work) + end function norm + + ! Updating a matrix using blas DGEMM + ! C <- alpha*A*B + beta*C + subroutine updatemat(alpha, A, B, beta, C) + real(real64), dimension(:,:), intent(in) :: A, B + real(real64), dimension(:,:), intent(inout) :: C + real(real64), intent(in) :: alpha, beta + integer(blint) :: m, n, k + m = int(size(A,1), blint) + n = int(size(B,2), blint) + k = int(size(A,2), blint) + call dgemm("N", "N", m, n, k, alpha, A, m, B, & + k, beta, C, m) + end subroutine + + ! Computing inv(A)*B using lapack DGESV + ! B <- inv(A)*B + subroutine invA_B(A, B, ipiv, info) + real(real64), dimension(:,:), intent(inout) :: A + real(real64), dimension(:,:), intent(inout) :: B + integer(blint), dimension(:), intent(inout) :: ipiv + integer(blint), intent(inout) :: info + integer(blint) :: m, n + m = int(size(A,1), blint) + n = int(size(B,2), blint) + call dgesv(m, n, A, m, ipiv, B, m, info) + end subroutine invA_B + + ! Cycle reduction algorithm + subroutine cycle_reduction(A0, A1, A2, X, cvg_tol, check, info) + real(real64), dimension(:,:), intent(in) :: A0, A1, A2 + real(real64), dimension(:,:), intent(inout) :: X + real(real64), intent(in) :: cvg_tol + logical, intent(in) :: check + real(c_double), dimension(2), intent(inout) :: info + + real(real64), dimension(:,:), allocatable :: A02, & + Q0, Q2, Ahat, invA1_A02, A1i, A0_tmp, A1_tmp + integer :: it, n, dn, max_it + integer(blint) :: info_inv + integer(blint), dimension(:), allocatable :: ipiv + real(real64) :: residual, crit + character(kind=c_char, len=10) :: cvg_tol_str, residual_str + + Ahat = A1 + A1i = A1 + n = size(A0,1) + dn = 2*n + allocate(A02(n,dn), ipiv(n), invA1_A02(n,dn), Q0(n,dn), Q2(n,dn)) + A02(:,1:n) = A0 + A02(:,n+1:dn) = A2 + it = 0 + max_it = 300 +loop: do + ! Computing [A0;A2]*(A1\[A0 A2]) + A1_tmp = A1i + invA1_A02 = A02 + call invA_B(A1_tmp, invA1_A02, ipiv, info_inv) + call updatemat(1._real64, A02(:,1:n), invA1_A02, 0._real64, Q0) + call updatemat(1._real64, A02(:,n+1:dn), invA1_A02, 0._real64, Q2) + ! Updating A02, A1 and Ahat + A1i = A1i - Q0(:,n+1:dn) - Q2(:,1:n) + A02(:,1:n) = -Q0(:,1:n) + A02(:,n+1:dn) = -Q2(:,n+1:dn) + Ahat = Ahat-Q2(:,1:n) + crit = norm(A02(:,1:n),"1") + ! Checking for stopping conditions + if (crit < cvg_tol) then + if (norm(A02(:,n+1:dn), "1") < cvg_tol) then + exit loop + end if + elseif (it == max_it) then + info(1) = 401._c_double + info(2) = real(log(norm(A1i,"1")), c_double) + exit loop + elseif (isnan(crit) .or. (info_inv /= 0_blint)) then + info(1) = 402._c_double + info(2) = real(log(norm(A1i,"1")), c_double) + exit loop + end if + it = it + 1 + end do loop + + ! Computing X = -Ahat\A0 + X = -A0 + call invA_B(Ahat, X, ipiv, info_inv) + + ! Checking residuals if necessary + if (check) then + ! A1_tmp <- A2*X + A1 + A1_tmp = A1 + call updatemat(1._real64, A2, X, 1._real64, A1_tmp) + ! A0_tmp <- A1_tmp*X + A0 + A0_tmp = A0 + call updatemat(1._real64, A1_tmp, X, 1._real64, A0_tmp) + residual = norm(A0_tmp, "1") + if (residual>cvg_tol) then + info(1) = 403._c_double + info(2) = log(residual) + write (cvg_tol_str,"(es8.2)") cvg_tol + write (residual_str,"(es8.2)") residual + call mexErrMsgTxt("The norm of the residual is "& + &// trim(residual_str) // & + &", whereas the tolerance criterion is " & + // trim(cvg_tol_str) // "." ) + end if + end if + end subroutine cycle_reduction + +end module reduction + +subroutine mexFunction(nlhs, plhs, nrhs, prhs) bind(c, name='mexFunction') + use reduction + use matlab_mex + implicit none + + type(c_ptr), dimension(*), intent(in), target :: prhs + type(c_ptr), dimension(*), intent(out) :: plhs + integer(c_int), intent(in), value :: nlhs, nrhs + + integer :: i, n + character(kind=c_char, len=2) :: num2str + real(real64) :: cvg_tol + real(real64), dimension(:,:), pointer, contiguous :: A0, A1, A2, X + real(real64), dimension(2) :: info + logical :: check + + ! 0. Checking the consistency and validity of input arguments + if (nrhs < 4) then + call mexErrMsgTxt("Must have at least 4 inputs") + end if + if (nrhs > 5) then + call mexErrMsgTxt("Too many input arguments") + end if + if (nlhs > 2) then + call mexErrMsgTxt("Too many output arguments") + end if + + do i=1,3 + if (.not. (c_associated(prhs(i)) .and. mxIsDouble(prhs(i)) .and. & + (.not. mxIsComplex(prhs(i))) .and. (.not. mxIsSparse(prhs(i))))) then + write (num2str,"(i2)") i + call mexErrMsgTxt("Argument" // trim(num2str) // " should be a real dense matrix") + end if + end do + if (.not. (c_associated(prhs(4)) .and. mxIsScalar(prhs(4)) .and. & + mxIsNumeric(prhs(4)))) then + call mexErrMsgTxt("Argument 4 should be a numeric scalar") + end if + + check = .false. + if (nrhs == 5) then + if (.not. (c_associated(prhs(5)))) then + call mexErrMsgTxt("Argument 5 should be a Matlab object") + else + if (.not. (mxIsEmpty(prhs(5)))) then + check = .true. + end if + end if + end if + + n = int(mxGetM(prhs(1))) ! Order of the considered matrices + if ((n /= mxGetN(prhs(1))) & ! Number of columns of A0 + &.or. (n /= mxGetM(prhs(2))) & ! Number of lines of A1 + &.or. (n /= mxGetN(prhs(2))) & ! Number of columns of A1 + &.or. (n /= mxGetM(prhs(3))) & ! Number of lines of A2 + &.or. (n /= mxGetN(prhs(3))) & ! Number of columns of A2 + ) then + call mexErrMsgTxt("Input dimension mismatch") + end if + + ! 1. Storing the relevant information in Fortran format + A0(1:n,1:n) => mxGetPr(prhs(1)) + A1(1:n,1:n) => mxGetPr(prhs(2)) + A2(1:n,1:n) => mxGetPr(prhs(3)) + cvg_tol = mxGetScalar(prhs(4)) + info = [0._c_double,0._c_double] + + plhs(1) = mxCreateDoubleMatrix(int(n, mwSize), int(n, mwSize), mxREAL) + X(1:n,1:n) => mxGetPr(plhs(1)) + + ! 2. Calling the Cycle Reduction algorithm + call cycle_reduction(A0, A1, A2, X, cvg_tol, check, info) + + ! 3. Editing the information output if necessary + if (nlhs == 2) then + if (info(1) == 0.) then + plhs(2) = mxCreateDoubleScalar(0._c_double) + else + plhs(2) = mxCreateDoubleMatrix(1_mwSize, 2_mwSize, mxREAL) + mxGetPr(plhs(2)) = info + end if + end if + +end subroutine mexFunction \ No newline at end of file diff --git a/mex/sources/matlab_mex.F08 b/mex/sources/matlab_mex.F08 index 31619ccfcd39c27fcb263ed2e9c1c67cfdef6727..601b78e59f2e7b6f9c3850a788dd9b67a58c34d4 100644 --- a/mex/sources/matlab_mex.F08 +++ b/mex/sources/matlab_mex.F08 @@ -105,6 +105,11 @@ module matlab_mat type(c_ptr), intent(in), value :: pm end function mxGetN + logical(c_bool) function mxIsEmpty(pm) bind(c, name="mxIsEmpty"//API_VER) + use iso_c_binding + type(c_ptr), intent(in), value :: pm + end function mxIsEmpty + !! Create, Query, and Access Data Types ! Numeric types diff --git a/tests/Makefile.am b/tests/Makefile.am index 27d0f76ee4acafb923b499f1b97befb6b9197001..81f3ec588ecc0324a379d0534d92a44fa539c707 100644 --- a/tests/Makefile.am +++ b/tests/Makefile.am @@ -1223,7 +1223,8 @@ M_TRS_FILES += run_block_byte_tests_matlab.m.trs \ test_aggregate_routine_1_2.m.trs \ test_aggregate_routine_1_2_3.m.trs \ run_kronecker_tests.m.trs \ - nonlinearsolvers.m.trs + nonlinearsolvers.m.trs \ + cyclereduction.m.trs M_XFAIL_TRS_FILES = $(patsubst %.mod, %.m.trs, $(XFAIL_MODFILES)) @@ -1235,7 +1236,8 @@ O_TRS_FILES += run_block_byte_tests_octave.o.trs \ run_all_unitary_tests.o.trs \ histval_initval_file_unit_tests.o.trs \ run_kronecker_tests.o.trs \ - nonlinearsolvers.o.trs + nonlinearsolvers.o.trs \ + cyclereduction.o.trs O_XFAIL_TRS_FILES = $(patsubst %.mod, %.o.trs, $(XFAIL_MODFILES)) diff --git a/tests/cyclereduction.m b/tests/cyclereduction.m new file mode 100644 index 0000000000000000000000000000000000000000..211e591cccd258cfd760815dd1d17a1fb9816fa9 --- /dev/null +++ b/tests/cyclereduction.m @@ -0,0 +1,140 @@ +debug = false; + +if debug + [top_test_dir, ~, ~] = fileparts(mfilename('fullpath')); +else + top_test_dir = getenv('TOP_TEST_DIR'); +end + +addpath([top_test_dir filesep '..' filesep 'matlab']); + +if ~debug + % Test Dynare Version + if ~strcmp(dynare_version(), getenv('DYNARE_VERSION')) + error('Incorrect version of Dynare is being tested') + end +end + +dynare_config; + +NumberOfTests = 0; +testFailed = 0; + +if ~debug + skipline() + disp('*** TESTING: cyclereduction.m ***'); +end + +matlab_cr_path = [top_test_dir filesep '..' filesep 'matlab' filesep 'missing' filesep 'mex' filesep 'cycle_reduction']; +addpath(matlab_cr_path); +cycle_reduction_matlab = @cycle_reduction; +rmpath(matlab_cr_path); + +if isoctave + addpath([top_test_dir filesep '..' filesep 'mex' filesep 'octave']); +else + addpath([top_test_dir filesep '..' filesep 'mex' filesep 'matlab']); +end + +t0 = clock; + +% Set the dimension of the problem to be solved. +n = 500; +% Set the equation to be solved +A = eye(n); +B = diag(30*ones(n,1)); B(1,1) = 20; B(end,end) = 20; B = B - diag(10*ones(n-1,1),-1); B = B - diag(10*ones(n-1,1),1); +C = diag(15*ones(n,1)); C = C - diag(5*ones(n-1,1),-1); C = C - diag(5*ones(n-1,1),1); +cvg_tol = 1e-12; +X1 = zeros(n,n); +X2 = zeros(n,n); + +% 1. Solve the equation with the Matlab cycle reduction algorithm +NumberOfTests = NumberOfTests+1; +tElapsed1 = 0.; +try + tic; [X1,info] = cycle_reduction_matlab(C,B,A,cvg_tol,[0.]); tElapsed1 = toc; + disp(['Elapsed time for the Matlab cycle reduction algorithm is: ' num2str(tElapsed1) ' (n=' int2str(n) ').']) + R = C+B*X1+A*X1*X1; + if (max(abs(R(:))) > cvg_tol) + testFailed = testFailed+1; + if debug + dprintf('Matlab cycle_reduction solution is wrong') + end + end +catch + testFailed = testFailed+1; + if debug + dprintf('Matlab cycle_reduction failed') + end +end + +% 2. Solve the equation with the Fortran cycle reduction algorithm +NumberOfTests = NumberOfTests+1; +tElapsed2 = 0.; +try + tic; [X2,info] = cycle_reduction(C,B,A,cvg_tol,[0.]); tElapsed2 = toc; + disp(['Elapsed time for the Fortran cycle reduction algorithm is: ' num2str(tElapsed2) ' (n=' int2str(n) ').']) + R = C+B*X2+A*X2*X2; + if (max(abs(R(:))) > cvg_tol) + testFailed = testFailed+1; + if debug + dprintf('Fortran cycle_reduction solution is wrong') + end + end +catch + testFailed = testFailed+1; + if debug + dprintf('Fortran cycle_reduction failed') + end +end + +% 3. Compare solutions of the Fortran and Matlab routines +NumberOfTests = NumberOfTests+1; +if (max(abs(X1(:) - X2(:))) > cvg_tol) + testFailed = testFailed+1; + if debug + dprintf('Fortran and Matlab cycle reduction solutions differ'); + end +end + +% Compare the Fortran and Matlab execution time +if debug + if tElapsed1<tElapsed2 + skipline() + dprintf('Matlab cyclic reduction is %5.2f times faster than its Fortran counterpart.', tElapsed2/tElapsed1) + skipline() + else + skipline() + dprintf('Fortran cyclic reduction is %5.2f times faster than its Matlab counterpart.', tElapsed1/tElapsed2) + skipline() + end +end + +t1 = clock; + +if ~debug + cd(getenv('TOP_TEST_DIR')); +else + dprintf('FAILED tests: %i', testFailed) +end + +if isoctave + fid = fopen('cyclereduction.o.trs', 'w+'); +else + fid = fopen('cyclereduction.m.trs', 'w+'); +end +if testFailed + fprintf(fid,':test-result: FAIL\n'); +else + fprintf(fid,':test-result: PASS\n'); +end +fprintf(fid,':number-tests: %i\n', NumberOfTests); +fprintf(fid,':number-failed-tests: %i\n', testFailed); +fprintf(fid,':list-of-passed-tests: cyclereduction.m\n'); +fprintf(fid,':elapsed-time: %f\n', etime(t1, t0)); +fclose(fid); + +if ~debug + exit; +end +