function forecasts=run(M_,options_,oo_,estim_params_,options_cond_fcst)
% function forecasts=run(M_,options_,oo_,estim_params_,options_cond_fcst)
% Computes conditional forecasts.
%
% INPUTS
% - M_                  [structure]     describing the model
% - options_            [structure]     describing the options
% - oo_:                [structure]     storing the results
% - estim_params_       [structure]     characterizing parameters to be estimated
% - options_cond_fcst   [structure]     containing the options. The fields are:
%
%                                      + replic              [integer]   scalar, number of monte carlo simulations.
%                                      + parameter_set       [char]      values of the parameters to be used:
%                                                                                               'calibration'
%                                                                                               'posterior_mode',
%                                                                                               'posterior_mean',
%                                                                                               'posterior_median',
%                                                                                               'prior_mode' or
%                                                                                               'prior mean'.
%                                                            [double]     np*1 array, values of the estimated parameters.
%                                      + controlled_varexo   [cell]       m*1 cell of row char array, list of controlled exogenous variables (only when using the deprecated syntax).
%                                      + conf_sig            [double]     scalar in [0,1], probability mass covered by the confidence bands.
%                                      + check_tol           [double]     scalar, tolerance for checking that conditioning succeeded (i.e. that the targets for controlled endogenous are reached with the computed controlled exogenous).
%
% OUTPUTS
%  - forecasts          [structure]     results of the conditional forecast exercise
%
% ALGORITHM
%
%   Relies on state-space form:
%
%       yₜ = T  yₜ₋₁ + R  εₜ
%
%   Both yₜ, the vector of endogenous variables, and εₜ are split up into controlled
%   and  uncontrolled ones, and we assume, without loss of generality, that the
%   constrained endogenous variables and the controlled shocks come first :
%
%      ⎧ y₁ₜ ⎫    ⎧ T₁₁  T₁₂ ⎫ ⎧ y₁ₜ₋₁ ⎫   ⎧ R₁₁  R₁₂ ⎫ ⎧ ε₁ₜ ⎫
%      ⎩ y₂ₜ ⎭ =  ⎩ T₂₁  T₂₂ ⎭ ⎩ y₂ₜ₋₁ ⎭ + ⎩ R₂₁  R₂₂ ⎭ ⎩ ε₂ₜ ⎭
%
%   where matrices T and R are partitioned consistently with the
%   vectors of endogenous variables and innovations. Provided that matrix
%   R₁₁ is square and full rank (a necessary condition is that the
%   number of free endogenous variables matches the number of free innovations),
%   given y₁ₜ, ε₂ₜ and yₜ₋₁ the first block of equations can be solved for ε₁ₜ:
%
%      ε₁ₜ = R₁₁ \ ( y₁ₜ - T₁₁y₁ₜ₋₁ - T₁₂y₂ₜ₋₁  - R₁₂ε₂ₜ )
%
%   and y₂ₜ can be updated by evaluating the second block of equations:
%
%      y₂ₜ = T₂₁y₁ₜ₋₁ + T₂₂y₂ₜ₋₁ +  R₂₁ε₁ₜ + R₂₂ε₂ₜ
%
%   By iterating over these two blocks of equations, we can build a forecast for
%   all the endogenous variables in the system conditional on paths for a subset of the
%   endogenous variables. This exercise is replicated by drawing different
%   sequences of free innovations. The result is a predictive distribution for
%   the uncontrolled endogenous variables, y₂ₜ, that Dynare will use to report
%   confidence bands around the point conditional forecast.
%   is used for forecasting
%
% REMARKS
% [1] Results are stored in oo_.conditional_forecast.
% [2] Use the function conditional_forecasts.plot to plot the results.

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

if ~isfield(options_cond_fcst,'parameter_set') || isempty(options_cond_fcst.parameter_set)
    if isfield(oo_,'posterior_mode')
        options_cond_fcst.parameter_set = 'posterior_mode';
    elseif isfield(oo_,'mle_mode')
        options_cond_fcst.parameter_set = 'mle_mode';
    else
        error('No valid parameter set found')
    end
end

if isempty(M_.conditional_forecast_paths)
    error('conditional_forecast: M_.conditional_forecast_paths is empty')
end

if any(cellfun(@isdates, {M_.conditional_forecast_paths.periods}))
    error('conditional_forecast: conditional_forecast_paths does not support dates')
end

% For backward compatibility: if endogenize_id is empty, we need to update it
% In old syntax, exogenize_id is the endogenous variable, and we need to add the instrument from controlled_varexo
if any(cellfun(@isempty, {M_.conditional_forecast_paths.endogenize_id}))
    if ~isfield(options_cond_fcst, 'controlled_varexo') || isempty(options_cond_fcst.controlled_varexo)
        error('conditional_forecast_paths: old syntax with "var" keyword requires controlled_varexo option in conditional_forecast command')
    end

    % NB: the preprocessor enforces the constraint that old and new syntax cannot be mixed
    n_controlled = length(options_cond_fcst.controlled_varexo);
    all_exogenize_id = unique([M_.conditional_forecast_paths.exogenize_id]);
    n_paths = length(all_exogenize_id);

    if n_controlled ~= n_paths
        error('conditional_forecast: number of old-syntax paths with "var" keyword (%d) does not match number of controlled shocks (%d)', n_paths, n_controlled)
    end

    [~, all_endogenize_id] = ismember(options_cond_fcst.controlled_varexo, M_.exo_names);
    old_syntax = true;
else
    if isfield(options_cond_fcst, 'controlled_varexo')
        error('conditional_forecast: controlled_varexo option must not be passed when using the new syntax with "exogenize" keyword in conditional_forecast_paths.')
    end
    old_syntax = false;
end

if ~isfield(options_cond_fcst,'replic') || isempty(options_cond_fcst.replic)
    options_cond_fcst.replic = 5000;
end

if ~isfield(options_cond_fcst,'periods') || isempty(options_cond_fcst.periods)
    options_cond_fcst.periods = 40;
end

if ~isfield(options_cond_fcst, 'conf_sig')  || isempty(options_cond_fcst.conf_sig)
    options_cond_fcst.conf_sig = .8;
end

if ~isfield(options_cond_fcst, 'check_tol') || isempty(options_cond_fcst.check_tol)
    options_cond_fcst.check_tol = 1e-6;
end

estimated_model = ~isequal(options_cond_fcst.parameter_set, 'calibration');

if estimated_model
    if options_.prefilter
        error('conditional_forecast: Conditional forecasting does not support the prefiltering option')
    end
    switch options_cond_fcst.parameter_set
      case 'posterior_mode'
        xparam = get_posterior_parameters('mode',M_,estim_params_,oo_,options_);
        graph_title='Posterior Mode';
      case 'posterior_mean'
        xparam = get_posterior_parameters('mean',M_,estim_params_,oo_,options_);
        graph_title='Posterior Mean';
      case 'posterior_median'
        xparam = get_posterior_parameters('median',M_,estim_params_,oo_,options_);
        graph_title='Posterior Median';
      case 'mle_mode'
        xparam = get_posterior_parameters('mode',M_,estim_params_,oo_,options_,'mle_');
        graph_title='ML Mode';
      case 'prior_mode'
        Prior = dprior(estim_params_, M_, options_.prior_trunc);
        xparam = Prior.p5(:);
        graph_title='Prior Mode';
      case 'prior_mean'
        Prior = dprior(estim_params_, M_, options_.prior_trunc);
        xparam = Prior.p1;
        graph_title='Prior Mean';
      otherwise
        disp('conditional_forecast: If the input argument is a string, then it has to be equal to:')
        disp('                      ''calibration'', ')
        disp('                      ''posterior_mode'', ')
        disp('                      ''posterior_mean'', ')
        disp('                      ''posterior_median'', ')
        disp('                      ''prior_mode'' or')
        disp('                      ''prior_mean''.')
        error('conditional_forecast: Wrong argument type!')
    end
    M_ = set_parameters_locally(M_, xparam);
    [dataset_,dataset_info] = makedataset(options_);
    gend = dataset_.nobs;

    %store qz_criterium
    options_=select_qz_criterium_value(options_);
    options_smoothed_state_uncertainty_old = options_.smoothed_state_uncertainty;
    smoother_info = set_smoother_info(M_, oo_.dr, options_);
    smoother_results = DsgeSmoother(xparam, dataset_, dataset_info, M_, oo_.dr,oo_.steady_state,oo_.exo_steady_state,oo_.exo_det_steady_state, options_, estim_params_, smoother_info);
    atT = smoother_results.smoothed.alphahat;
    ys = smoother_results.model.SteadyState;
    options_.smoothed_state_uncertainty = options_smoothed_state_uncertainty_old;
    %get constant part
    if options_.noconstant
        constant = zeros(size(ys,1),options_cond_fcst.periods+1);
    else
        if options_.loglinear
            constant = repmat(log(ys),1,options_cond_fcst.periods+1);
        else
            constant = repmat(ys,1,options_cond_fcst.periods+1);
        end
    end
    %get trend part (which also takes care of prefiltering); needs to
    %include the last period
    if M_.with_trend == 1
        [trend_addition] =compute_trend_coefficients(M_,options_,length(options_.varobs),gend+options_cond_fcst.periods);
        trend_addition = trend_addition(:,gend:end);
    else
        trend_addition=zeros(length(options_.varobs),1+options_cond_fcst.periods);
    end
    % add trend to constant
    for obs_iter=1:length(options_.varobs)
        j = strcmp(options_.varobs{obs_iter}, M_.endo_names);
        constant(j,:) = constant(j,:) + trend_addition(obs_iter,:);
    end
    trend = constant(oo_.dr.order_var,:);
    InitState(:,1) = atT(:,end);

    forecasts.initial_date = dataset_.lastdate;
else
    if isempty(options_.qz_criterium)
        options_.qz_criterium = 1+1e-6;
    end
    graph_title='Calibration';
    if ~isfield(oo_.dr,'ghx')
        error('You need to call stoch_simul before conditional_forecast')
    end
end

if options_.logged_steady_state %if steady state was previously logged, undo this
    oo_.dr.ys=exp(oo_.dr.ys);
    oo_.steady_state=exp(oo_.steady_state);
    options_.logged_steady_state=0;
end

[T, R, ys, ~, oo_.dr,M_.params] = dynare_resolve(M_, options_, oo_.dr,oo_.steady_state,oo_.exo_steady_state,oo_.exo_det_steady_state);

if options_.loglinear && isfield(oo_.dr,'ys') && options_.logged_steady_state==0 %log steady state
    oo_.dr.ys=log_variable(1:M_.endo_nbr,oo_.dr.ys,M_);
    ys=oo_.dr.ys;
    oo_.steady_state=log_variable(1:M_.endo_nbr,oo_.steady_state,M_);
    options_.logged_steady_state=1; %set option for use in stoch_simul
end

if ~isdiag(M_.Sigma_e)
    warning(sprintf('The innovations are correlated (the covariance matrix has non zero off diagonal elements), the results of the conditional forecasts will\ndepend on the ordering of the innovations (as declared after varexo) because a Cholesky decomposition is used to factorize the covariance matrix.\n\n=> It is preferable to declare the correlations in the model block (explicitly imposing the identification restrictions), unless you are satisfied\nwith the implicit identification restrictions implied by the Cholesky decomposition.'))
    sQ = chol(M_.Sigma_e,'lower');
else
    sQ = sqrt(M_.Sigma_e);
end

if ~estimated_model
    if isempty(oo_.histval.endo)
        y0 = ys;
        forecasts.initial_date = dates();
    else
        if options_.loglinear
            %make sure that only states are updated (controls have value of 0 in vector)
            y0=zeros(size(ys));
            y0_logged = log_variable(1:M_.endo_nbr,oo_.histval.endo,M_);
            y0(oo_.histval.endo~=0)=y0_logged(oo_.histval.endo~=0);
        else
            y0 = oo_.histval.endo;
        end
        forecasts.initial_date = oo_.histval.date_zero;
    end
    InitState(:,1) = y0(oo_.dr.order_var)-ys(oo_.dr.order_var,:); %initial state in deviations from steady state
    trend = repmat(ys(oo_.dr.order_var,:),1,options_cond_fcst.periods+1); %trend needs to contain correct steady state
end


% Construct a structure that stores all constraints period by period
constraints = struct('exogenize_id', cell(1, options_cond_fcst.periods), 'values', cell(1, options_cond_fcst.periods), 'endogenize_id', cell(1, options_cond_fcst.periods));
for i = 1:length(M_.conditional_forecast_paths)
    for j = 1:length(M_.conditional_forecast_paths(i).periods)
        p = M_.conditional_forecast_paths(i).periods(j);
        constraints(p).exogenize_id(end+1) = M_.conditional_forecast_paths(i).exogenize_id;
        if ~old_syntax
            constraints(p).endogenize_id(end+1) = M_.conditional_forecast_paths(i).endogenize_id;
        end
        value = M_.conditional_forecast_paths(i).value;
        if isscalar(value)
            constraints(p).values(end+1) = value;
        else
            if length(value) < j
                error('conditional_forecast_paths: the vector of values passed for endogenous %s is too short', M_.endo_names{M_.conditional_forecast_paths(i).exogenize_id})
            end
            constraints(p).values(end+1) = value(j);
        end
    end
end

if old_syntax
    for p = 1:options_cond_fcst.periods
        [~, ids] = ismember(constraints(p).exogenize_id, all_exogenize_id);
        constraints(p).endogenize_id = all_endogenize_id(ids);
    end
end

% Some sanity checks
for p = 1:options_cond_fcst.periods
    if length(constraints(p).exogenize_id) ~= length(unique(constraints(p).exogenize_id))
        error('conditional_forecast_paths: in period %d, some endogenous variable is exogenized twice', p)
    end
    if length(constraints(p).endogenize_id) ~= length(unique(constraints(p).endogenize_id))
        error('conditional_forecast_paths: in period %d, some exogenous variable is endogenized twice', p)
    end
end

NumberOfStates = length(InitState);
% Conditional forecasts; the first index in the 3rd dimension is the point forecast
forecasts_endo.cond = zeros(NumberOfStates, options_cond_fcst.periods+1, options_cond_fcst.replic+1);
forecasts_endo.cond(:,1,:) = repmat(InitState, 1, options_cond_fcst.replic+1); %set initial steady state to deviations from steady state in first period
forecasts_exo.cond = NaN(M_.exo_nbr, options_cond_fcst.periods, options_cond_fcst.replic+1); % One less period than states, we do not include the initial period

% Unconditional forecasts; same remarks as above
forecasts_endo.uncond = forecasts_endo.cond;
forecasts_exo.uncond = forecasts_exo.cond;

for p = 1:options_cond_fcst.periods
    shocks = [zeros(M_.exo_nbr,1), sQ*randn(M_.exo_nbr, options_cond_fcst.replic)];
    if old_syntax
        shocks(all_endogenize_id,:) = 0;
    end

    shocks_cond = shocks;
    endogenize_id = constraints(p).endogenize_id;

    if ~isempty(endogenize_id)
        exogenize_dr_id = oo_.dr.inv_order_var(constraints(p).exogenize_id);
        values = constraints(p).values' - trend(exogenize_dr_id,p+1); % NB: first column of trend is last actual data
        no_endogenize_id = setdiff(1:M_.exo_nbr, endogenize_id);

        shocks_cond(endogenize_id,:) = R(exogenize_dr_id,endogenize_id)\(values - T(exogenize_dr_id,:)*reshape(forecasts_endo.cond(:,p,:), NumberOfStates, options_cond_fcst.replic+1) - R(exogenize_dr_id,no_endogenize_id)*shocks(no_endogenize_id,:));
    end

    forecasts_endo.cond(:,p+1,:) = T*reshape(forecasts_endo.cond(:,p,:), NumberOfStates, options_cond_fcst.replic+1) + R*shocks_cond;
    forecasts_exo.cond(:,p,:) = shocks_cond;

    residual = max(max(abs(forecasts_endo.cond(exogenize_dr_id,p+1,:) - values)));
    if ~isempty(endogenize_id) && residual > options_cond_fcst.check_tol
        error('conditional_forecast: controlling of variables was not successful (residual of %g).\nThis can be due to numerical imprecision (e.g. explosive simulations) or because the instrument(s) do not allow controlling the variable(s).\nYou can increase the value of the "check_tol" option to bypass this error.', residual)
    end

    forecasts_endo.uncond(:,p+1,:) = T*reshape(forecasts_endo.uncond(:,p,:), NumberOfStates, options_cond_fcst.replic+1) + R*shocks;
    forecasts_exo.uncond(:,p,:) = shocks;
end

for field_cell = { 'cond', 'uncond' }
    field = field_cell{1};

    forecasts_endo.(field) = forecasts_endo.(field) + trend;

    % Save the point forecast
    for i = 1:M_.endo_nbr
        forecasts.(field).Point.(M_.endo_names{oo_.dr.order_var(i)}) = forecasts_endo.(field)(i,:,1)';
    end
    for i = 1:M_.exo_nbr
        forecasts.(field).Point.(M_.exo_names{i}) = [ NaN; forecasts_exo.(field)(i,:,1)' ];
    end

    % Compute and store mean and confidence intervals, if requested
    if options_cond_fcst.replic > 0
        % Remove the point forecast
        forecasts_endo.(field) = forecasts_endo.(field)(:,:,2:end);
        forecasts_exo.(field) = forecasts_exo.(field)(:,:,2:end);

        tt = (1-options_cond_fcst.conf_sig)/2;
        t1 = max(1,round(options_cond_fcst.replic*tt));
        t2 = min(options_cond_fcst.replic,round(options_cond_fcst.replic*(1-tt)));

        mean_forecasts_endo.(field) = mean(forecasts_endo.(field), 3);
        mean_forecasts_exo.(field) = mean(forecasts_exo.(field), 3);

        for i = 1:M_.endo_nbr
            forecasts.(field).Mean.(M_.endo_names{oo_.dr.order_var(i)}) = mean_forecasts_endo.(field)(i,:)';
            if size(forecasts_endo.(field), 2)>1
                tmp = sort(squeeze(forecasts_endo.(field)(i,:,:))');
                else
                tmp = sort(squeeze(forecasts_endo.(field)(i,:,:)));
            end
        forecasts.(field).ci.(M_.endo_names{oo_.dr.order_var(i)}) = [tmp(t1,:)' ,tmp(t2,:)' ]';
        end
        for i = 1:M_.exo_nbr
            forecasts.(field).Mean.(M_.exo_names{i}) = [ NaN; mean_forecasts_exo.(field)(i,:)' ];
            if size(forecasts_exo.(field), 2)>1
                tmp = sort(squeeze(forecasts_exo.(field)(i,:,:))');
            else
                tmp = sort(squeeze(forecasts_exo.(field)(i,:,:)));
            end
            forecasts.(field).ci.(M_.exo_names{i}) = [ NaN NaN; tmp(t1,:)', tmp(t2,:)' ]';
        end
    end
end

forecasts.graph.title = graph_title;
forecasts.graph.fname = M_.fname;
