function [endogenousvariables, success, err, iter, exogenousvariables] = sim1(endogenousvariables, exogenousvariables, steadystate, controlled_paths_by_period, M_, options_)
% [endogenousvariables, success, err, iter] = sim1(endogenousvariables, exogenousvariables, steadystate, controlled_paths_by_period, M_, options_)
% Performs deterministic simulations with lead or lag of one period, using
% a basic Newton solver on sparse matrices.
% Uses perfect_foresight_problem DLL to construct the stacked problem.
%
% INPUTS
%   - endogenousvariables [double] N*(T+M_.maximum_lag+M_.maximum_lead) array, paths for the endogenous variables (initial condition + initial guess + terminal condition).
%   - exogenousvariables  [double] (T+M_.maximum_lag+M_.maximum_lead)*M array, paths for the exogenous variables.
%   - steadystate         [double] N*1 array, steady state for the endogenous variables.
%   - controlled_paths_by_period [struct] data from perfect_foresight_controlled_paths or shock_paths+exogenize block
%   - M_                  [struct] contains a description of the model.
%   - options_            [struct] contains various options.
% OUTPUTS
%   - endogenousvariables [double] N*(T+M_.maximum_lag+M_.maximum_lead) array, paths for the endogenous variables (solution of the perfect foresight model).
%   - success             [logical] Whether a solution was found
%   - err                 [double] ∞-norm of the residual
%   - iter                [integer] Number of iterations
%   - exogenousvariables  [double] (T+M_.maximum_lag+M_.maximum_lead)*M array, paths for the exogenous variables
%                                  (may be modified if perfect_foresight_controlled_paths or
%                                   shock_paths+exogenize is present)

% Copyright © 1996-2026 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/>.

verbose = options_.verbosity && ~options_.noprint;

if options_.simul.robust_lin_solve
    orig_warning_state = warning;
    if isoctave
        warning('off','Octave:singular-matrix');
    else
        warning('off','MATLAB:singularMatrix');
    end
end

ny = M_.endo_nbr;
periods = get_simulation_periods(options_);
vperiods = periods*ones(1,options_.simul.maxit);

if ~isempty(controlled_paths_by_period)
    for p = 1:periods
        if isempty(controlled_paths_by_period(p).exogenize_id)
            continue
        end
        endogenousvariables(controlled_paths_by_period(p).exogenize_id, p+M_.maximum_lag) = controlled_paths_by_period(p).values;
    end

    if options_.debug
        error('Debugging not available with perfect_foresight_controlled_paths or shock_paths+exogenize')
    end
    if options_.endogenous_terminal_period
        error('The endogenous_terminal_period option not available with perfect_foresight_controlled_paths or shock_paths+exogenize')
    end
end

if M_.maximum_lag > 0
    y0 = endogenousvariables(:, M_.maximum_lag);
else
    y0 = NaN(ny, 1);
end

if M_.maximum_lead > 0
    yT = endogenousvariables(:, M_.maximum_lag+periods+1);
else
    yT = NaN(ny, 1);
end

y = reshape(endogenousvariables(:, M_.maximum_lag+(1:periods)), ny*periods, 1);

if verbose
    skipline()
    printline(56)
    disp('MODEL SIMULATION:')
    skipline()
end

h1 = clock;
iter = 1;
converged = false;
first_iter_lu = [];

while ~(converged || iter > options_.simul.maxit)
    h2 = clock;

    [res, A] = perfect_foresight_problem(y, y0, yT, exogenousvariables, M_.params, steadystate, periods, M_, options_);
    % A is the stacked Jacobian with period x equations along the rows and
    % periods times variables (in declaration order) along the columns

    if ~isempty(controlled_paths_by_period)
        A = controlled_paths_substitute_stacked_jacobian(A, y, y0, yT, exogenousvariables, steadystate, controlled_paths_by_period, M_);
    end

    if options_.debug && iter==1
        [row,col]=find(A);
        row=setdiff(1:periods*ny,row);
        column=setdiff(1:periods*ny,col);
        if ~isempty(row) || ~isempty(column)
            fprintf('The stacked Jacobian is singular. The problem derives from:\n')
            if ~isempty(row)
                time_period=ceil(row/ny);
                equation=row-ny*(time_period-1);
                for eq_iter=1:length(equation)
                    fprintf('The derivative of equation %d at time %d is zero for all variables\n',equation(eq_iter),time_period(eq_iter));
                end
            end
            if ~isempty(column)
                time_period=ceil(column/ny);
                variable=column-ny*(time_period-1);
                for eq_iter=1:length(variable)
                    fprintf('The derivative with respect to variable %d at time %d is zero for all equations\n',variable(eq_iter),time_period(eq_iter));
                end
            end
        end
    end
    if iter==1 && options_.simul.check_jacobian_singularity
        check_Jacobian_for_singularity(full(A),M_.endo_names,options_);
    end
    if options_.endogenous_terminal_period && iter > 1
        for it = 1:periods
            if norm(res((it-1)*ny+(1:ny)), 'Inf') < options_.dynatol.f/1e7
                if it < periods
                    res = res(1:(it*ny));
                    A = A(1:(it*ny), 1:(it*ny));
                    yT = y(it*ny+(1:ny));
                    endogenousvariables(:, M_.maximum_lag+((it+1):periods)) = reshape(y(it*ny+(1:(ny*(periods-it)))), ny, periods-it);
                    y = y(1:(it*ny));
                    periods = it;
                end
                break
            end
        end
        vperiods(iter) = periods;
    end

    err = norm(res, 'Inf'); % Do not use max(max(abs(…))) because it omits NaN
    if options_.debug
        fprintf('\nLargest absolute residual at iteration %d: %10.3f\n',iter,err);
        if any(isnan(res)) || any(isinf(res)) || any(any(isnan(endogenousvariables))) || any(any(isinf(endogenousvariables)))
            fprintf('\nWARNING: NaN or Inf detected in the residuals or endogenous variables.\n');
        end
        skipline()
    end
    if err < options_.dynatol.f
        converged = true;
    else
        if options_.simul.robust_lin_solve
            dy = -lin_solve_robust(A, res, options_);
        else
            [mdy, first_iter_lu] = lin_solve(A, res, options_, first_iter_lu);
            dy = -mdy;
        end
        if any(isnan(dy)) || any(isinf(dy))
            if verbose
                display_critical_variables(reshape(dy,[ny periods])', M_.endo_names,'sim1', options_.noprint || ~isempty(controlled_paths_by_period),0,iter);
            end
        end
        y = y + dy;

        if ~isempty(controlled_paths_by_period)
            for p = 1:periods
                endogenize_id = controlled_paths_by_period(p).endogenize_id;
                exogenize_id = controlled_paths_by_period(p).exogenize_id;
                if isempty(endogenize_id)
                    continue
                end
                y(exogenize_id+(p-1)*M_.endo_nbr) = controlled_paths_by_period(p).values;
                exogenousvariables(p+M_.maximum_lag,endogenize_id) = exogenousvariables(p+M_.maximum_lag,endogenize_id) + dy(exogenize_id+(p-1)*M_.endo_nbr)';
            end
        end
    end
    if verbose
        fprintf('Iter: %d,\t err. = %g,\t time = %g\n', iter, err, etime(clock,h2));
    end
    iter = iter + 1;
end

endogenousvariables(:, M_.maximum_lag+(1:periods)) = reshape(y, ny, periods);

if options_.endogenous_terminal_period
    periods = get_simulation_periods(options_);
    err = compute_maxerror(endogenousvariables, exogenousvariables, steadystate, M_, options_);
end

if converged
    % initial or terminal observations may contain
    % harmless NaN or Inf. We test only values computed above
    if any(any(isnan(y))) || any(any(isinf(y)))
        success = false; % NaN or Inf occurred
        if verbose
            skipline()
            fprintf('Total time of simulation: %g.\n', etime(clock,h1))
            disp('Simulation terminated with NaN or Inf in the residuals or endogenous variables.')
            display_critical_variables(reshape(y,[ny periods])', M_.endo_names,'sim1', options_.noprint || ~isempty(controlled_paths_by_period));
            disp('There is most likely something wrong with your model. Try model_diagnostics or another simulation method.')
            printline(105)
        end
    else
        if verbose
            skipline();
            fprintf('Total time of simulation: %g.\n', etime(clock,h1))
            printline(56)
        end
        success = true; % Convergence obtained.
    end
else
    if verbose
        skipline();
        fprintf('Total time of simulation: %g.\n', etime(clock,h1))
        disp('Maximum number of iterations is reached (modify option maxit).')
        printline(62)
    end
    success = false; % more iterations are needed.
end

if verbose
    skipline();
end

if options_.simul.robust_lin_solve
    warning(orig_warning_state);
end


function [ x, flag, relres ] = lin_solve_robust(A, b ,options_)
if norm(b) < sqrt(eps) % then x = 0 is a solution
    x = 0;
    flag = 0;
    relres = 0;
    return
end

x = A\b;
if ~options_.simul.allow_nonfinite_values
    x(~isfinite(x)) = 0; %prevent non-finite values from propagating, see #1975
end
[ x, flag, relres ] = bicgstab(A, b, [], [], [], [], x); % returns immediately if x is a solution
if flag == 0
    return
end

disp_verbose(['    lin_solve_robust: Relative residual from bicgstab: ' num2str(relres,8)],options_.debug);
disp_verbose('    lin_solve_robust: Initial bicgstab failed, trying alternative start point.',options_.debug);

old_x = x;
old_relres = relres;
[ x, flag, relres ] = bicgstab(A, b);
if flag == 0
    return
end

if ~options_.simul.allow_nonfinite_values
    x(~isfinite(x)) = 0; %prevent non-finite values from propagating, see #1975
end

disp_verbose('    lin_solve_robust: Alternative start point also failed with bicgstab, trying gmres.',options_.debug);
if old_relres < relres
    x = old_x;
end
[ x, flag, relres ] = gmres(A, b, [], [], [], [], [], x);
if flag == 0
    return
end

if ~options_.simul.allow_nonfinite_values
    x(~isfinite(x)) = 0; %prevent non-finite values from propagating, see #1975
end

disp_verbose('    lin_solve_robust: Initial gmres failed, trying alternative start point.',options_.debug);

old_x = x;
old_relres = relres;
[ x, flag, relres ] = gmres(A, b);
if flag == 0
    return
end

if ~options_.simul.allow_nonfinite_values
    x(~isfinite(x)) = 0; %prevent non-finite values from propagating, see #1975
end

disp_verbose('    lin_solve_robust: Alternative start point also failed with gmres, using the (SLOW) Moore-Penrose Pseudo-Inverse.',options_.debug)

if old_relres < relres
    x = old_x;
    relres = old_relres;
end
old_x = x;
old_relres = relres;
x = pinv(full(A)) * b;
if ~options_.simul.allow_nonfinite_values
    x(~isfinite(x)) = 0; %prevent non-finite values from propagating, see #1975
end

relres = norm(b - A*x) / norm(b);
if old_relres < relres
    x = old_x;
    relres = old_relres;
end
flag = (relres > 1e-6); %corresponds to hard-coded relative tolerance in e.g. bicgstab
if flag ~= 0
    disp_verbose('    lin_solve_robust: robust_lin_solve failed to find a solution to the linear system.', options_.debug);
end

function check_Jacobian_for_singularity(jacob,endo_names,options_)
n_vars_jacob=size(jacob,2);
try
    if (~isoctave && matlab_ver_less_than('9.12')) || isempty(options_.jacobian_tolerance)
        rank_jacob = rank(jacob); %can sometimes fail
    else
        rank_jacob = rank(jacob,options_.jacobian_tolerance); %can sometimes fail
    end
catch
    rank_jacob=size(jacob,1);
end
if rank_jacob < size(jacob,1)
    disp(['sim1:  The Jacobian of the dynamic model is ' ...
        'singular'])
    disp(['sim1:  there is ' num2str(n_vars_jacob-rank_jacob) ...
        ' collinear relationships between the variables and the equations'])
    if (~isoctave && matlab_ver_less_than('9.12')) || isempty(options_.jacobian_tolerance)
        ncol = null(jacob);
    else
        ncol = null(jacob,options_.jacobian_tolerance); %can sometimes fail
    end
    n_rel = size(ncol,2);
    for i = 1:n_rel
        if n_rel  > 1
            disp(['Relation ' int2str(i)])
        end
        disp('Collinear variables:')
        for j=1:10
            k = find(abs(ncol(:,i)) > 10^-j);
            if max(abs(jacob(:,k)*ncol(k,i))) < 1e-6
                break
            end
        end
        fprintf('%s\n',endo_names{mod(k-1,length(endo_names))+1})
    end
    if (~isoctave && matlab_ver_less_than('9.12')) || isempty(options_.jacobian_tolerance)
        neq = null(jacob'); %can sometimes fail
    else
        neq = null(jacob',options_.jacobian_tolerance); %can sometimes fail
    end
    n_rel = size(neq,2);
    for i = 1:n_rel
        if n_rel  > 1
            disp(['Relation ' int2str(i)])
        end
        disp('Collinear equations')
        for j=1:10
            k = find(abs(neq(:,i)) > 10^-j);
            if max(abs(jacob(k,:)'*neq(k,i))) < 1e-6
                break
            end
        end
        equation=mod(k-1,length(endo_names))+1;
        period=ceil(k/length(endo_names));
        for ii=1:length(equation)
            fprintf('Equation %5u, period %5u\n',equation(ii),period(ii))
        end
    end
else
    disp(['sim1:  The Jacobian of the dynamic model has full rank.'])
end
