classdef dprior < handle

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

    properties
        p1 = [];                         % Prior mean.
        p2 = [];                         % Prior stddev.
        p3 = [];                         % Lower bound of the prior support.
        p4 = [];                         % Upper bound of the prior support.
        p5 = [];                         % Prior mode.
        p6 = [];                         % Prior first hyperparameter.
        p7 = [];                         % Prior second hyperparameter.
        p11 = [];                        % Prior median
        lb = [];                         % Truncated prior lower bound.
        ub = [];                         % Truncated prior upper bound.
        name = {};                       % Name of the parameter
        iduniform = [];                  % Index for the uniform priors.
        idgaussian = [];                 % Index for the gaussian priors.
        idgamma = [];                    % Index for the gamma priors.
        idbeta = [];                     % Index for the beta priors.
        idinvgamma1 = [];                % Index for the inverse gamma type 1 priors.
        idinvgamma2 = [];                % Index for the inverse gamma type 2 priors.
        idweibull = [];                  % Index for the weibull priors.
    end

    methods

        function o = dprior(varargin)
        % Class constructor.
        %
        % SYNTAX
        % - o = dprior()
        %     Empty prior object.
        %
        % - o = dprior(bayestopt_, PriorTrunc)                [transition]
        % - o = dprior(bayestopt_, PriorTrunc, Uniform)       [transition]
        %     Construct from a bayestopt_-shaped struct. Deprecated; will
        %     be removed once all internal callers are migrated.
        %
        % - o = dprior(estim_params_, M_, prior_trunc)
        % - o = dprior(estim_params_, M_, prior_trunc, Uniform)
        %     Construct from the estimation parameter specification and the
        %     model structure. prior_trunc defaults to 0 if omitted.
        %     If Uniform is true, all priors are replaced by uniform draws.
        %
        % - o = dprior(param_name, key1, val1, key2, val2, ...)
        %     Construct a single-parameter prior from name-value pairs.
        %     Mandatory key: 'shape' (or 'distribution').
        %     Moment specification: 'mean'/'p1', 'std'/'p2'.
        %     Hyperparameter specification: 'h1', 'h2' (and 'h3', ... for future distributions).
        %     Optional: 'lower_bound'/'p3', 'upper_bound'/'p4', 'prior_trunc'.
            if nargin == 0
                return
            end
            first_arg = varargin{1};
            if ischar(first_arg) || isstring(first_arg)
                % Path 3: single parameter from name-value pairs
                init_from_name_value_pairs(o, first_arg, varargin{2:end});
            elseif isstruct(first_arg) && isfield(first_arg, 'var_exo')
                % Path 2: bulk construction from estim_params_
                M_ = varargin{2};
                if nargin >= 3
                    prior_trunc = varargin{3};
                else
                    prior_trunc = 0;
                end
                Uniform = false;
                if nargin >= 4, Uniform = varargin{4}; end
                init_from_estim_params(o, first_arg, M_, prior_trunc, Uniform);
            elseif isstruct(first_arg) && isfield(first_arg, 'pshape') && ~isempty(first_arg.pshape)
                % Path 1 (transition): bayestopt_-shaped struct
                PriorTrunc = 0;
                if nargin >= 2, PriorTrunc = varargin{2}; end
                Uniform = false;
                if nargin >= 3, Uniform = varargin{3}; end
                legacy_bayestopt_to_dprior(o, first_arg, PriorTrunc, Uniform);
            end
            % If none of the above matched (e.g. non-struct without pshape,
            % or empty pshape), return the empty object.
        end % dprior (constructor)

    end % methods

end % classdef --*-- Unit tests --*--

%@test:1
%$ % Fill global structures with required fields...
%$ prior_trunc = 1e-10;
%$ p0 = repmat([1; 2; 3; 4; 5; 6; 8], 2, 1);    % Prior shape
%$ p1 = .4*ones(14,1);                          % Prior mean
%$ p2 = .2*ones(14,1);                          % Prior std.
%$ p3 = NaN(14,1);
%$ p4 = NaN(14,1);
%$ p5 = NaN(14,1);
%$ p6 = NaN(14,1);
%$ p7 = NaN(14,1);
%$
%$ for i=1:14
%$    switch p0(i)
%$      case 1
%$        % Beta distribution
%$        p3(i) = 0;
%$        p4(i) = 1;
%$        [p6(i), p7(i)] = beta_specification(p1(i), p2(i)^2, p3(i), p4(i));
%$        p5(i) = compute_prior_mode([p6(i) p7(i)], 1);
%$      case 2
%$        % Gamma distribution
%$        p3(i) = 0;
%$        p4(i) = Inf;
%$        [p6(i), p7(i)] = gamma_specification(p1(i), p2(i)^2, p3(i), p4(i));
%$        p5(i) = compute_prior_mode([p6(i) p7(i)], 2);
%$      case 3
%$        % Normal distribution
%$        p3(i) = -Inf;
%$        p4(i) = Inf;
%$        p6(i) = p1(i);
%$        p7(i) = p2(i);
%$        p5(i) = p1(i);
%$      case 4
%$        % Inverse Gamma (type I) distribution
%$        p3(i) = 0;
%$        p4(i) = Inf;
%$        [p6(i), p7(i)] = inverse_gamma_specification(p1(i), p2(i)^2, p3(i), 1, false);
%$        p5(i) = compute_prior_mode([p6(i) p7(i)], 4);
%$      case 5
%$        % Uniform distribution
%$        [p1(i), p2(i), p6(i), p7(i)] = uniform_specification(p1(i), p2(i), p3(i), p4(i));
%$        p3(i) = p6(i);
%$        p4(i) = p7(i);
%$        p5(i) = compute_prior_mode([p6(i) p7(i)], 5);
%$      case 6
%$        % Inverse Gamma (type II) distribution
%$        p3(i) = 0;
%$        p4(i) = Inf;
%$        [p6(i), p7(i)] = inverse_gamma_specification(p1(i), p2(i)^2, p3(i), 2, false);
%$        p5(i) = compute_prior_mode([p6(i) p7(i)], 6);
%$      case 8
%$        % Weibull distribution
%$        p3(i) = 0;
%$        p4(i) = Inf;
%$        [p6(i), p7(i)] = weibull_specification(p1(i), p2(i)^2, p3(i));
%$        p5(i) = compute_prior_mode([p6(i) p7(i)], 8);
%$      otherwise
%$        error('This density is not implemented!')
%$    end
%$ end
%$
%$ bayestopt_.pshape = p0;
%$ bayestopt_.p1 = p1;
%$ bayestopt_.p2 = p2;
%$ bayestopt_.p3 = p3;
%$ bayestopt_.p4 = p4;
%$ bayestopt_.p5 = p5;
%$ bayestopt_.p6 = p6;
%$ bayestopt_.p7 = p7;
%$
%$ ndraws = 1e5;
%$ m0 = bayestopt_.p1; %zeros(14,1);
%$ v0 = diag(bayestopt_.p2.^2); %zeros(14);
%$
%$ % Call the tested routine
%$ try
%$    % Instantiate dprior object
%$    o = dprior(bayestopt_, prior_trunc, false);
%$    t(1) = true;
%$ catch
%$     t(1) = false;
%$ end
%$
%$ T = all(t);
%@eof:1

%@test:2
%$ try
%$    % Instantiate dprior object
%$    o = dprior();
%$    t(1) = true;
%$ catch
%$     t(1) = false;
%$ end
%$
%$ T = all(t);
%@eof:2

%@test:3
%$ % Test Path 3: single-parameter named constructor (beta)
%$ try
%$    o = dprior('alpha', 'shape', 'beta', 'mean', 0.5, 'std', 0.1);
%$    t(1) = ~isempty(o.idbeta);
%$    t(2) = isequal(o.idbeta, 1);
%$    t(3) = abs(o.p1 - 0.5) < 1e-12;
%$    t(4) = abs(o.p2 - 0.1) < 1e-12;
%$    t(5) = ~isnan(o.p6) && ~isnan(o.p7);
%$    t(6) = isequal(o.name, {'alpha'});
%$    t(7) = o.lb >= 0 && o.ub <= 1;
%$ catch e
%$    t = false;
%$ end
%$ T = all(t);
%@eof:3

%@test:4
%$ % Test Path 3: normal distribution via hyperparameters (h1/h2)
%$ try
%$    o = dprior('beta_param', 'shape', 'normal', 'h1', 0.5, 'h2', 0.2);
%$    t(1) = ~isempty(o.idgaussian);
%$    t(2) = abs(o.p1 - 0.5) < 1e-12;
%$    t(3) = abs(o.p2 - 0.2) < 1e-12;
%$    t(4) = abs(o.p6 - 0.5) < 1e-12;
%$    t(5) = abs(o.p7 - 0.2) < 1e-12;
%$    x = o.draw();
%$    t(6) = isfinite(x);
%$ catch e
%$    t = false(6, 1);
%$ end
%$ T = all(t);
%@eof:4

%@test:5
%$ % Test Path 3: gamma distribution via moments
%$ try
%$    o = dprior('stderr e', 'shape', 'gamma', 'mean', 0.5, 'std', 0.2);
%$    t(1) = isequal(o.idgamma, 1);
%$    t(2) = abs(o.p1 - 0.5) < 1e-12;
%$    t(3) = o.lb >= 0;
%$    t(4) = ~isnan(o.p5) && o.p5 >= 0;
%$    x = o.draw();
%$    t(5) = x >= o.lb;
%$ catch e
%$    t = false(5, 1);
%$ end
%$ T = all(t);
%@eof:5

%@test:6
%$ % Test Path 3: inverse gamma (type 1) via moments
%$ try
%$    o = dprior('sig', 'shape', 'inv_gamma', 'mean', 0.5, 'std', 0.2);
%$    t(1) = isequal(o.idinvgamma1, 1);
%$    t(2) = o.lb >= 0;
%$    t(3) = ~isnan(o.p6) && ~isnan(o.p7);
%$    x = o.draw();
%$    t(4) = x >= o.lb;
%$ catch e
%$    t = false(4, 1);
%$ end
%$ T = all(t);
%@eof:6

%@test:7
%$ % Test Path 3: uniform distribution via bounds
%$ try
%$    o = dprior('rho', 'shape', 'uniform', 'lower_bound', 0, 'upper_bound', 1);
%$    t(1) = isequal(o.iduniform, 1);
%$    t(2) = abs(o.p6 - 0) < 1e-12 && abs(o.p7 - 1) < 1e-12;
%$    t(3) = abs(o.p1 - 0.5) < 1e-12;
%$    t(4) = isnan(o.p5);
%$    x = o.draw();
%$    t(5) = x >= 0 && x <= 1;
%$ catch e
%$    t = false(5, 1);
%$ end
%$ T = all(t);
%@eof:7

%@test:8
%$ % Test Path 3: inverse gamma (type 2) via moments
%$ try
%$    o = dprior('sig2', 'shape', 'inv_gamma2', 'mean', 0.5, 'std', 0.2);
%$    t(1) = isequal(o.idinvgamma2, 1);
%$    t(2) = o.lb >= 0;
%$    t(3) = ~isnan(o.p6) && ~isnan(o.p7);
%$ catch e
%$    t = false(3, 1);
%$ end
%$ T = all(t);
%@eof:8

%@test:9
%$ % Test Path 3: weibull distribution via moments
%$ try
%$    o = dprior('tau', 'shape', 'weibull', 'mean', 0.5, 'std', 0.2);
%$    t(1) = isequal(o.idweibull, 1);
%$    t(2) = o.lb >= 0;
%$    t(3) = ~isnan(o.p6) && ~isnan(o.p7);
%$    x = o.draw();
%$    t(4) = x >= o.lb;
%$ catch e
%$    t = false(4, 1);
%$ end
%$ T = all(t);
%@eof:9

%@test:10
%$ % Test Path 2: bulk construction from estim_params_
%$ try
%$    estim_params_.var_exo = [1, 0.5, 0, Inf, 2, 0.5, 0.2, NaN, NaN, NaN];
%$    estim_params_.var_endo = zeros(0, 10);
%$    estim_params_.corrx = zeros(0, 10);
%$    estim_params_.corrn = zeros(0, 10);
%$    estim_params_.skew_exo = zeros(0, 10);
%$    estim_params_.endo_init_vals = zeros(0, 10);
%$    estim_params_.param_vals = [1, 0.5, -Inf, Inf, 3, 0.5, 0.1, NaN, NaN, NaN];
%$    M_.exo_names = {'e'};
%$    M_.endo_names = {};
%$    M_.param_names = {'alpha'};
%$    o = dprior(estim_params_, M_, 1e-10);
%$    t(1) = length(o) == 2;
%$    t(2) = isequal(o.name, {'stderr e'; 'alpha'});
%$    t(3) = isequal(o.idgamma, 1) && isequal(o.idgaussian, 2);
%$    t(4) = abs(o.p1(1) - 0.5) < 1e-12;
%$    t(5) = abs(o.p1(2) - 0.5) < 1e-12;
%$    t(6) = length(o.lb) == 2 && length(o.ub) == 2;
%$    x = o.draw();
%$    t(7) = length(x) == 2;
%$ catch e
%$    t = false(7, 1);
%$ end
%$ T = all(t);
%@eof:10

%@test:11
%$ % Test Path 3: error on missing shape
%$ try
%$    o = dprior('x', 'mean', 0.5, 'std', 0.1);
%$    t(1) = false;
%$ catch e
%$    t(1) = ~isempty(strfind(e.message, 'shape'));
%$ end
%$ T = all(t);
%@eof:11

%@test:12
%$ % Test Path 3: error on both moments and hyperparams
%$ try
%$    o = dprior('x', 'shape', 'beta', 'mean', 0.5, 'std', 0.1, 'h1', 2, 'h2', 3);
%$    t(1) = false;
%$ catch e
%$    t(1) = ~isempty(strfind(e.message, 'either'));
%$ end
%$ T = all(t);
%@eof:12

%@test:13
%$ % Test Path 3: beta via hyperparams (h1/h2), verify back-computed moments
%$ try
%$    o = dprior('x', 'shape', 'beta', 'h1', 2, 'h2', 5);
%$    t(1) = isequal(o.idbeta, 1);
%$    t(2) = abs(o.p1 - 2/7) < 1e-12;
%$    t(3) = abs(o.p6 - 2) < 1e-12 && abs(o.p7 - 5) < 1e-12;
%$    t(4) = o.p5 >= 0 && o.p5 <= 1;
%$    t(5) = o.lb >= 0 && o.ub <= 1;
%$ catch e
%$    t = false(5, 1);
%$ end
%$ T = all(t);
%@eof:13

%@test:14
%$ % Test Path 2 vs Path 3: same normal prior, cross-check
%$ try
%$    estim_params_ = struct('var_exo', zeros(0,10), 'var_endo', zeros(0,10), ...
%$                           'corrx', zeros(0,10), 'corrn', zeros(0,10), ...
%$                           'skew_exo', zeros(0,10), 'endo_init_vals', zeros(0,10), ...
%$                           'param_vals', [1, 0.5, -Inf, Inf, 3, 0.5, 0.2, NaN, NaN, NaN]);
%$    M_ = struct('exo_names', {{}}, 'endo_names', {{}}, 'param_names', {{'x'}});
%$    o1 = dprior(estim_params_, M_, 1e-10);
%$    o2 = dprior('x', 'shape', 'normal', 'mean', 0.5, 'std', 0.2, 'prior_trunc', 1e-10);
%$    t(1) = abs(o1.p1 - o2.p1) < 1e-12;
%$    t(2) = abs(o1.p6 - o2.p6) < 1e-12;
%$    t(3) = abs(o1.p7 - o2.p7) < 1e-12;
%$    t(4) = abs(o1.lb - o2.lb) < 1e-10;
%$    t(5) = abs(o1.ub - o2.ub) < 1e-10;
%$ catch e
%$    t = false(5, 1);
%$ end
%$ T = all(t);
%@eof:14
