Perfect foresight: LBJ now available under stack_solve_algo=1 (with/without block/bytecode)
Previously, LBJ was available: – under stack_solve_algo=6 when neither block nor bytecode were present – under stack_solve_algo=1 with either block or bytecode (but the documentation was not making it clear that it was LBJ) This commit merges the two values for the option, and makes them interchangeable. LBJ should now be invoked with stack_solve_algo=1 (but stack_solve_algo=6 is kept for compatibility, and is a synonymous).mr#2067
parent
9cc5a5576e
commit
06f665e231
|
@ -3533,10 +3533,9 @@ method to solve the simultaneous equation system. Because the
|
||||||
resulting Jacobian is in the order of ``n`` by ``T`` and hence will be
|
resulting Jacobian is in the order of ``n`` by ``T`` and hence will be
|
||||||
very large for long simulations with many variables, Dynare makes use
|
very large for long simulations with many variables, Dynare makes use
|
||||||
of the sparse matrix capacities of MATLAB/Octave. A slower but
|
of the sparse matrix capacities of MATLAB/Octave. A slower but
|
||||||
potentially less memory consuming alternative (``stack_solve_algo=6``)
|
potentially less memory consuming alternative (``stack_solve_algo=1``)
|
||||||
is based on a Newton-type algorithm first proposed by *Laffargue
|
is based on a Newton-type algorithm first proposed by *Laffargue
|
||||||
(1990)* and *Boucekkine (1995)*, which uses relaxation
|
(1990)* and *Boucekkine (1995)*, which avoids ever storing the full
|
||||||
techniques. Thereby, the algorithm avoids ever storing the full
|
|
||||||
Jacobian. The details of the algorithm can be found in *Juillard
|
Jacobian. The details of the algorithm can be found in *Juillard
|
||||||
(1996)*. The third type of algorithms makes use of block decomposition
|
(1996)*. The third type of algorithms makes use of block decomposition
|
||||||
techniques (divide-and-conquer methods) that exploit the structure of
|
techniques (divide-and-conquer methods) that exploit the structure of
|
||||||
|
@ -3654,9 +3653,15 @@ speed-up on large models.
|
||||||
|
|
||||||
``1``
|
``1``
|
||||||
|
|
||||||
Use a Newton algorithm with a sparse LU solver at each
|
Use the Laffargue-Boucekkine-Juillard (LBJ) algorithm proposed
|
||||||
iteration (requires ``bytecode`` and/or ``block``
|
in *Juillard (1996)*. It is slower than ``stack_solve_algo=0``,
|
||||||
option, see :ref:`model-decl`).
|
but may be less memory consuming on big models. Note that if the
|
||||||
|
``block`` option is used (see :ref:`model-decl`), a simple
|
||||||
|
Newton algorithm with sparse matrices is used for blocks which
|
||||||
|
are purely backward or forward (of type ``SOLVE BACKWARD`` or
|
||||||
|
``SOLVE FORWARD``, see :comm:`model_info`), since LBJ only makes
|
||||||
|
sense on blocks with both leads and lags (of type ``SOLVE TWO
|
||||||
|
BOUNDARIES``).
|
||||||
|
|
||||||
``2``
|
``2``
|
||||||
|
|
||||||
|
@ -3686,10 +3691,8 @@ speed-up on large models.
|
||||||
|
|
||||||
``6``
|
``6``
|
||||||
|
|
||||||
Use the historical algorithm proposed in *Juillard
|
Synonymous for ``stack_solve_algo=1``. Kept for historical
|
||||||
(1996)*: it is slower than ``stack_solve_algo=0``, but
|
reasons.
|
||||||
may be less memory consuming on big models (not
|
|
||||||
available with ``bytecode`` and/or ``block`` options).
|
|
||||||
|
|
||||||
``7``
|
``7``
|
||||||
|
|
||||||
|
|
|
@ -94,7 +94,7 @@ else
|
||||||
[oo_.endo_simul, oo_.deterministic_simulation] = ...
|
[oo_.endo_simul, oo_.deterministic_simulation] = ...
|
||||||
sim1(oo_.endo_simul, oo_.exo_simul, oo_.steady_state, M_, options_);
|
sim1(oo_.endo_simul, oo_.exo_simul, oo_.steady_state, M_, options_);
|
||||||
end
|
end
|
||||||
case 6
|
case {1 6}
|
||||||
if options_.linear_approximation
|
if options_.linear_approximation
|
||||||
error('Invalid value of stack_solve_algo option!')
|
error('Invalid value of stack_solve_algo option!')
|
||||||
end
|
end
|
||||||
|
|
|
@ -3,7 +3,7 @@ function check_input_arguments(DynareOptions, DynareModel, DynareResults)
|
||||||
%Conducts checks for inconsistent/missing inputs to deterministic
|
%Conducts checks for inconsistent/missing inputs to deterministic
|
||||||
%simulations
|
%simulations
|
||||||
|
|
||||||
% Copyright © 2015-2017 Dynare Team
|
% Copyright © 2015-2022 Dynare Team
|
||||||
%
|
%
|
||||||
% This file is part of Dynare.
|
% This file is part of Dynare.
|
||||||
%
|
%
|
||||||
|
@ -25,19 +25,15 @@ if DynareOptions.stack_solve_algo < 0 || DynareOptions.stack_solve_algo > 7
|
||||||
end
|
end
|
||||||
|
|
||||||
if ~DynareOptions.block && ~DynareOptions.bytecode && DynareOptions.stack_solve_algo ~= 0 ...
|
if ~DynareOptions.block && ~DynareOptions.bytecode && DynareOptions.stack_solve_algo ~= 0 ...
|
||||||
&& DynareOptions.stack_solve_algo ~= 6 && DynareOptions.stack_solve_algo ~= 7
|
&& DynareOptions.stack_solve_algo ~= 1 && DynareOptions.stack_solve_algo ~= 6 ...
|
||||||
error('perfect_foresight_solver:ArgCheck','PERFECT_FORESIGHT_SOLVER: you must use stack_solve_algo=0 or stack_solve_algo=6 when not using block nor bytecode option')
|
&& DynareOptions.stack_solve_algo ~= 7
|
||||||
|
error('perfect_foresight_solver:ArgCheck','PERFECT_FORESIGHT_SOLVER: you must use stack_solve_algo={0,1,6,7} when not using block nor bytecode option')
|
||||||
end
|
end
|
||||||
|
|
||||||
if DynareOptions.block && ~DynareOptions.bytecode && DynareOptions.stack_solve_algo == 5
|
if DynareOptions.block && ~DynareOptions.bytecode && DynareOptions.stack_solve_algo == 5
|
||||||
error('perfect_foresight_solver:ArgCheck','PERFECT_FORESIGHT_SOLVER: you can''t use stack_solve_algo = 5 without bytecode option')
|
error('perfect_foresight_solver:ArgCheck','PERFECT_FORESIGHT_SOLVER: you can''t use stack_solve_algo = 5 without bytecode option')
|
||||||
end
|
end
|
||||||
|
|
||||||
if (DynareOptions.block || DynareOptions.bytecode) && DynareOptions.stack_solve_algo == 6
|
|
||||||
error('perfect_foresight_solver:ArgCheck','PERFECT_FORESIGHT_SOLVER: you can''t use stack_solve_algo = 6 with block or bytecode option')
|
|
||||||
end
|
|
||||||
|
|
||||||
|
|
||||||
if isempty(DynareResults.endo_simul) || any(size(DynareResults.endo_simul) ~= [ DynareModel.endo_nbr, DynareModel.maximum_lag+DynareOptions.periods+DynareModel.maximum_lead ])
|
if isempty(DynareResults.endo_simul) || any(size(DynareResults.endo_simul) ~= [ DynareModel.endo_nbr, DynareModel.maximum_lag+DynareOptions.periods+DynareModel.maximum_lead ])
|
||||||
|
|
||||||
if DynareOptions.initval_file
|
if DynareOptions.initval_file
|
||||||
|
|
|
@ -22,8 +22,8 @@ cutoff = 1e-15;
|
||||||
|
|
||||||
if options_.stack_solve_algo==0
|
if options_.stack_solve_algo==0
|
||||||
mthd='Sparse LU';
|
mthd='Sparse LU';
|
||||||
elseif options_.stack_solve_algo==1
|
elseif options_.stack_solve_algo==1 || options_.stack_solve_algo==6
|
||||||
mthd='Relaxation';
|
mthd='LBJ';
|
||||||
elseif options_.stack_solve_algo==2
|
elseif options_.stack_solve_algo==2
|
||||||
mthd='GMRES';
|
mthd='GMRES';
|
||||||
elseif options_.stack_solve_algo==3
|
elseif options_.stack_solve_algo==3
|
||||||
|
|
|
@ -23,12 +23,7 @@ function [y, T, oo_, info] = solve_one_boundary(fname, y, x, params, steady_stat
|
||||||
% solve_tolf [double] convergence criteria
|
% solve_tolf [double] convergence criteria
|
||||||
% cutoff [double] cutoff to correct the direction in Newton in case
|
% cutoff [double] cutoff to correct the direction in Newton in case
|
||||||
% of singular jacobian matrix
|
% of singular jacobian matrix
|
||||||
% stack_solve_algo [integer] linear solver method used in the
|
% stack_solve_algo [integer] linear solver method used in the Newton algorithm
|
||||||
% Newton algorithm :
|
|
||||||
% - 1 sparse LU
|
|
||||||
% - 2 GMRES
|
|
||||||
% - 3 BicGStab
|
|
||||||
% - 4 Optimal path length
|
|
||||||
% is_forward [logical] Whether the block has to be solved forward
|
% is_forward [logical] Whether the block has to be solved forward
|
||||||
% If false, the block is solved backward
|
% If false, the block is solved backward
|
||||||
% is_dynamic [logical] If true, the block belongs to the dynamic file
|
% is_dynamic [logical] If true, the block belongs to the dynamic file
|
||||||
|
@ -212,7 +207,7 @@ for it_=start:incr:finish
|
||||||
y(y_index_eq, it_) = ya;
|
y(y_index_eq, it_) = ya;
|
||||||
%% Recompute temporary terms, since they are not given as output of lnsrch1
|
%% Recompute temporary terms, since they are not given as output of lnsrch1
|
||||||
[~, ~, T(:, it_)] = feval(fname, Block_Num, dynvars_from_endo_simul(y, it_, M), x, params, steady_state, T(:, it_), it_, false);
|
[~, ~, T(:, it_)] = feval(fname, Block_Num, dynvars_from_endo_simul(y, it_, M), x, params, steady_state, T(:, it_), it_, false);
|
||||||
elseif (is_dynamic && (stack_solve_algo==1 || stack_solve_algo==0)) || (~is_dynamic && options.solve_algo==6)
|
elseif (is_dynamic && (stack_solve_algo==1 || stack_solve_algo==0 || stack_solve_algo==6)) || (~is_dynamic && options.solve_algo==6)
|
||||||
if verbose && ~is_dynamic
|
if verbose && ~is_dynamic
|
||||||
disp('steady: Sparse LU ')
|
disp('steady: Sparse LU ')
|
||||||
end
|
end
|
||||||
|
|
|
@ -182,7 +182,7 @@ while ~(cvg || iter>maxit_)
|
||||||
dx = g1a\b- ya;
|
dx = g1a\b- ya;
|
||||||
ya = ya + lambda*dx;
|
ya = ya + lambda*dx;
|
||||||
y(y_index, y_kmin+(1:periods))=reshape(ya',length(y_index),periods);
|
y(y_index, y_kmin+(1:periods))=reshape(ya',length(y_index),periods);
|
||||||
elseif stack_solve_algo==1
|
elseif stack_solve_algo==1 || stack_solve_algo==6
|
||||||
for t=1:periods
|
for t=1:periods
|
||||||
first_elem = (t-1)*Blck_size+1;
|
first_elem = (t-1)*Blck_size+1;
|
||||||
last_elem = t*Blck_size;
|
last_elem = t*Blck_size;
|
||||||
|
|
|
@ -319,7 +319,8 @@ dynSparseMatrix::Read_SparseMatrix(const string &file_name, int Size, int period
|
||||||
for (int j = 0; j < Size; j++)
|
for (int j = 0; j < Size; j++)
|
||||||
IM_i[{ j, Size*(periods+y_kmax), 0 }] = j;
|
IM_i[{ j, Size*(periods+y_kmax), 0 }] = j;
|
||||||
}
|
}
|
||||||
else if (stack_solve_algo >= 0 && stack_solve_algo <= 4)
|
else if ((stack_solve_algo >= 0 && stack_solve_algo <= 4)
|
||||||
|
|| stack_solve_algo == 6)
|
||||||
{
|
{
|
||||||
for (int i = 0; i < u_count_init-Size; i++)
|
for (int i = 0; i < u_count_init-Size; i++)
|
||||||
{
|
{
|
||||||
|
@ -350,7 +351,8 @@ dynSparseMatrix::Read_SparseMatrix(const string &file_name, int Size, int period
|
||||||
IM_i[{ eq, var, lag }] = val;
|
IM_i[{ eq, var, lag }] = val;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
else if (((stack_solve_algo >= 0 && stack_solve_algo <= 4) && !steady_state)
|
else if ((((stack_solve_algo >= 0 && stack_solve_algo <= 4)
|
||||||
|
|| stack_solve_algo == 6) && !steady_state)
|
||||||
|| ((solve_algo >= 6 || solve_algo <= 8) && steady_state))
|
|| ((solve_algo >= 6 || solve_algo <= 8) && steady_state))
|
||||||
{
|
{
|
||||||
for (int i = 0; i < u_count_init; i++)
|
for (int i = 0; i < u_count_init; i++)
|
||||||
|
@ -3799,7 +3801,8 @@ dynSparseMatrix::Simulate_One_Boundary(int block_num, int y_size, int y_kmin, in
|
||||||
if (!x0_m)
|
if (!x0_m)
|
||||||
throw FatalExceptionHandling(" in Simulate_One_Boundary, can't allocate x0_m vector\n");
|
throw FatalExceptionHandling(" in Simulate_One_Boundary, can't allocate x0_m vector\n");
|
||||||
if (!((solve_algo == 6 && steady_state)
|
if (!((solve_algo == 6 && steady_state)
|
||||||
|| ((stack_solve_algo == 0 || stack_solve_algo == 1 || stack_solve_algo == 4) && !steady_state)))
|
|| ((stack_solve_algo == 0 || stack_solve_algo == 1 || stack_solve_algo == 4
|
||||||
|
|| stack_solve_algo == 6) && !steady_state)))
|
||||||
{
|
{
|
||||||
Init_Matlab_Sparse_Simple(size, IM_i, A_m, b_m, zero_solution, x0_m);
|
Init_Matlab_Sparse_Simple(size, IM_i, A_m, b_m, zero_solution, x0_m);
|
||||||
A_m_save = mxDuplicateArray(A_m);
|
A_m_save = mxDuplicateArray(A_m);
|
||||||
|
@ -3839,7 +3842,7 @@ dynSparseMatrix::Simulate_One_Boundary(int block_num, int y_size, int y_kmin, in
|
||||||
Solve_Matlab_GMRES(A_m, b_m, size, slowc, block_num, false, it_, x0_m);
|
Solve_Matlab_GMRES(A_m, b_m, size, slowc, block_num, false, it_, x0_m);
|
||||||
else if ((solve_algo == 8 && steady_state) || (stack_solve_algo == 3 && !steady_state))
|
else if ((solve_algo == 8 && steady_state) || (stack_solve_algo == 3 && !steady_state))
|
||||||
Solve_Matlab_BiCGStab(A_m, b_m, size, slowc, block_num, false, it_, x0_m, preconditioner);
|
Solve_Matlab_BiCGStab(A_m, b_m, size, slowc, block_num, false, it_, x0_m, preconditioner);
|
||||||
else if ((solve_algo == 6 && steady_state) || ((stack_solve_algo == 0 || stack_solve_algo == 1 || stack_solve_algo == 4) && !steady_state))
|
else if ((solve_algo == 6 && steady_state) || ((stack_solve_algo == 0 || stack_solve_algo == 1 || stack_solve_algo == 4 || stack_solve_algo == 6) && !steady_state))
|
||||||
Solve_LU_UMFPack(Ap, Ai, Ax, b, size, size, slowc, false, it_);
|
Solve_LU_UMFPack(Ap, Ai, Ax, b, size, size, slowc, false, it_);
|
||||||
}
|
}
|
||||||
return singular_system;
|
return singular_system;
|
||||||
|
@ -3898,7 +3901,7 @@ dynSparseMatrix::Simulate_Newton_One_Boundary(bool forward)
|
||||||
test_mxMalloc(r, __LINE__, __FILE__, __func__, size*sizeof(double));
|
test_mxMalloc(r, __LINE__, __FILE__, __func__, size*sizeof(double));
|
||||||
iter = 0;
|
iter = 0;
|
||||||
if ((solve_algo == 6 && steady_state)
|
if ((solve_algo == 6 && steady_state)
|
||||||
|| ((stack_solve_algo == 0 || stack_solve_algo == 1 || stack_solve_algo == 4) && !steady_state))
|
|| ((stack_solve_algo == 0 || stack_solve_algo == 1 || stack_solve_algo == 4 || stack_solve_algo == 6) && !steady_state))
|
||||||
{
|
{
|
||||||
Ap_save = static_cast<SuiteSparse_long *>(mxMalloc((size + 1) * sizeof(SuiteSparse_long)));
|
Ap_save = static_cast<SuiteSparse_long *>(mxMalloc((size + 1) * sizeof(SuiteSparse_long)));
|
||||||
test_mxMalloc(Ap_save, __LINE__, __FILE__, __func__, (size + 1) * sizeof(SuiteSparse_long));
|
test_mxMalloc(Ap_save, __LINE__, __FILE__, __func__, (size + 1) * sizeof(SuiteSparse_long));
|
||||||
|
@ -3937,7 +3940,7 @@ dynSparseMatrix::Simulate_Newton_One_Boundary(bool forward)
|
||||||
solve_linear(block_num, y_size, y_kmin, y_kmax, size, 0);
|
solve_linear(block_num, y_size, y_kmin, y_kmax, size, 0);
|
||||||
}
|
}
|
||||||
if ((solve_algo == 6 && steady_state)
|
if ((solve_algo == 6 && steady_state)
|
||||||
|| ((stack_solve_algo == 0 || stack_solve_algo == 1 || stack_solve_algo == 4) && !steady_state))
|
|| ((stack_solve_algo == 0 || stack_solve_algo == 1 || stack_solve_algo == 4 || stack_solve_algo == 6) && !steady_state))
|
||||||
{
|
{
|
||||||
mxFree(Ap_save);
|
mxFree(Ap_save);
|
||||||
mxFree(Ai_save);
|
mxFree(Ai_save);
|
||||||
|
@ -4126,7 +4129,8 @@ dynSparseMatrix::Simulate_Newton_Two_Boundaries(int blck, int y_size, int y_kmin
|
||||||
mexPrintf("MODEL SIMULATION: (method=Sparse LU)\n");
|
mexPrintf("MODEL SIMULATION: (method=Sparse LU)\n");
|
||||||
break;
|
break;
|
||||||
case 1:
|
case 1:
|
||||||
mexPrintf("MODEL SIMULATION: (method=Relaxation)\n");
|
case 6:
|
||||||
|
mexPrintf("MODEL SIMULATION: (method=LBJ)\n");
|
||||||
break;
|
break;
|
||||||
case 2:
|
case 2:
|
||||||
mexPrintf(preconditioner_print_out("MODEL SIMULATION: (method=GMRES)\n", preconditioner, false).c_str());
|
mexPrintf(preconditioner_print_out("MODEL SIMULATION: (method=GMRES)\n", preconditioner, false).c_str());
|
||||||
|
@ -4178,7 +4182,7 @@ dynSparseMatrix::Simulate_Newton_Two_Boundaries(int blck, int y_size, int y_kmin
|
||||||
}
|
}
|
||||||
if (stack_solve_algo == 0 || stack_solve_algo == 4)
|
if (stack_solve_algo == 0 || stack_solve_algo == 4)
|
||||||
Solve_LU_UMFPack(Ap, Ai, Ax, b, Size * periods, Size, slowc, true, 0, vector_table_conditional_local);
|
Solve_LU_UMFPack(Ap, Ai, Ax, b, Size * periods, Size, slowc, true, 0, vector_table_conditional_local);
|
||||||
else if (stack_solve_algo == 1)
|
else if (stack_solve_algo == 1 || stack_solve_algo == 6)
|
||||||
Solve_Matlab_Relaxation(A_m, b_m, Size, slowc, 0);
|
Solve_Matlab_Relaxation(A_m, b_m, Size, slowc, 0);
|
||||||
else if (stack_solve_algo == 2)
|
else if (stack_solve_algo == 2)
|
||||||
Solve_Matlab_GMRES(A_m, b_m, Size, slowc, blck, true, 0, x0_m);
|
Solve_Matlab_GMRES(A_m, b_m, Size, slowc, blck, true, 0, x0_m);
|
||||||
|
|
|
@ -79,6 +79,27 @@ end
|
||||||
|
|
||||||
oo0 = oo_;
|
oo0 = oo_;
|
||||||
|
|
||||||
|
perfect_foresight_setup(periods=400);
|
||||||
|
perfect_foresight_solver(stack_solve_algo=1);
|
||||||
|
|
||||||
|
if ~oo_.deterministic_simulation.status
|
||||||
|
error('Perfect foresight simulation failed')
|
||||||
|
end
|
||||||
|
|
||||||
|
oo1 = oo_;
|
||||||
|
|
||||||
|
maxabsdiff = max(max(abs(oo0.endo_simul-oo1.endo_simul)));
|
||||||
|
|
||||||
|
if max(max(abs(oo0.endo_simul-oo1.endo_simul)))>options_.dynatol.x
|
||||||
|
error('stack_solve_algo={0,1} return different paths for the endogenous variables!')
|
||||||
|
else
|
||||||
|
skipline()
|
||||||
|
fprintf('Maximum (absolute) differrence between paths is %s', num2str(maxabsdiff))
|
||||||
|
skipline()
|
||||||
|
end
|
||||||
|
|
||||||
|
% Also test stack_solve_algo=6, which is a synonymous for stack_solve_algo=1
|
||||||
|
|
||||||
perfect_foresight_setup(periods=400);
|
perfect_foresight_setup(periods=400);
|
||||||
perfect_foresight_solver(stack_solve_algo=6);
|
perfect_foresight_solver(stack_solve_algo=6);
|
||||||
|
|
||||||
|
|
|
@ -49,13 +49,13 @@ for blockFlag = 0:1
|
||||||
default_stack_solve_algo = 0;
|
default_stack_solve_algo = 0;
|
||||||
if ~blockFlag && storageFlag ~= 2
|
if ~blockFlag && storageFlag ~= 2
|
||||||
solve_algos = [1:4 9];
|
solve_algos = [1:4 9];
|
||||||
stack_solve_algos = [0 6];
|
stack_solve_algos = [0 1 6];
|
||||||
elseif blockFlag && storageFlag ~= 2
|
elseif blockFlag && storageFlag ~= 2
|
||||||
solve_algos = [1:4 6:9];
|
solve_algos = [1:4 6:9];
|
||||||
stack_solve_algos = 0:4;
|
stack_solve_algos = [0:4 6];
|
||||||
else
|
else
|
||||||
solve_algos = 1:9;
|
solve_algos = 1:9;
|
||||||
stack_solve_algos = 0:5;
|
stack_solve_algos = 0:6;
|
||||||
end
|
end
|
||||||
if has_optimization_toolbox
|
if has_optimization_toolbox
|
||||||
solve_algos = [ solve_algos 0 ];
|
solve_algos = [ solve_algos 0 ];
|
||||||
|
|
|
@ -45,13 +45,13 @@ for blockFlag = 0:1
|
||||||
default_stack_solve_algo = 0;
|
default_stack_solve_algo = 0;
|
||||||
if !blockFlag && storageFlag != 2
|
if !blockFlag && storageFlag != 2
|
||||||
solve_algos = [0:4 9];
|
solve_algos = [0:4 9];
|
||||||
stack_solve_algos = [0 6];
|
stack_solve_algos = [0 1 6];
|
||||||
elseif blockFlag && storageFlag != 2
|
elseif blockFlag && storageFlag != 2
|
||||||
solve_algos = [0:4 6:9];
|
solve_algos = [0:4 6:9];
|
||||||
stack_solve_algos = 0:4;
|
stack_solve_algos = [0:4 6];
|
||||||
else
|
else
|
||||||
solve_algos = 0:9;
|
solve_algos = 0:9;
|
||||||
stack_solve_algos = 0:5;
|
stack_solve_algos = 0:6;
|
||||||
endif
|
endif
|
||||||
|
|
||||||
# Workaround for strange race condition related to the static/dynamic
|
# Workaround for strange race condition related to the static/dynamic
|
||||||
|
|
Loading…
Reference in New Issue