function pdraw = from_uniform(o, u)

% Transform uniform quantiles into prior draws using inverse CDFs.
%
% INPUTS
% - o    [dprior]  Prior object.
% - u    [double]  n×m matrix of uniform random draws in [0,1], where n is the
%                  number of samples and m is the number of parameters. Each
%                  column contains uniform quantiles that will be transformed
%                  into prior draws via inverse CDFs.
%
% OUTPUTS
% - pdraw [double] n×m matrix, draws from the joint prior density.
%
% ALGORITHM
%   Transforms uniform random draws in u into actual prior draws by:
%   1. Rescaling each column of u to the truncated cumulative range
%      [lbcum, ubcum] for that parameter
%   2. Applying the appropriate inverse CDF (norminv, gaminv, betainv, etc.)
%      to convert quantiles into parameter values
%   This approach allows batched sampling with quasi-random or Latin hypercube
%   sequences generated externally and passed via u.
%
% REMARKS
% This method is equivalent to the former gsa.prior_draw but integrated into the dprior class.
% Unlike the draw() method which generates random numbers internally, from_uniform()
% takes externally generated uniform quantiles, allowing for quasi-Monte Carlo methods
% and reproducible sampling strategies.
%
% EXAMPLE
%
% >> Prior = dprior();
% >> Prior.add('alpha', 'shape', 'beta', 'mean', 0.5, 'std', 0.1);
% >> u = rand(1000, length(Prior.lb));  % or use QMC sequence
% >> samples = Prior.from_uniform(u);

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

npar = length(o.lb);
nsamp = size(u, 1);

% Compute cumulative probability bounds for truncated priors
lbcum = zeros(npar, 1);
ubcum = ones(npar, 1);

% Compute truncated cumulative probabilities for each parameter
if o.has('beta')
    id = o.idbeta;
    lbcum(id) = betainc((o.lb(id)-o.p3(id))./(o.p4(id)-o.p3(id)), o.p6(id), o.p7(id));
    ubcum(id) = betainc((o.ub(id)-o.p3(id))./(o.p4(id)-o.p3(id)), o.p6(id), o.p7(id));
end

if o.has('gamma')
    id = o.idgamma;
    lbcum(id) = gamcdf(o.lb(id)-o.p3(id), o.p6(id), o.p7(id));
    ubcum(id) = gamcdf(o.ub(id)-o.p3(id), o.p6(id), o.p7(id));
end

if o.has('gaussian')
    id = o.idgaussian;
    lbcum(id) = 0.5 * erfc(-(o.lb(id)-o.p6(id))./o.p7(id) / sqrt(2));
    ubcum(id) = 0.5 * erfc(-(o.ub(id)-o.p6(id))./o.p7(id) / sqrt(2));
end

if o.has('invgamma1')
    id = o.idinvgamma1;
    lbcum(id) = gamcdf(1./(o.ub(id)-o.p3(id)).^2, o.p7(id)/2, 2./o.p6(id));
    ubcum(id) = gamcdf(1./(o.lb(id)-o.p3(id)).^2, o.p7(id)/2, 2./o.p6(id));
end

if o.has('invgamma2')
    id = o.idinvgamma2;
    lbcum(id) = gamcdf(1./(o.ub(id)-o.p3(id)), o.p7(id)/2, 2./o.p6(id));
    ubcum(id) = gamcdf(1./(o.lb(id)-o.p3(id)), o.p7(id)/2, 2./o.p6(id));
end

if o.has('weibull')
    id = o.idweibull;
    lbcum(id) = wblcdf(o.lb(id)-o.p3(id), o.p7(id), o.p6(id));
    ubcum(id) = wblcdf(o.ub(id)-o.p3(id), o.p7(id), o.p6(id));
end

% For uniform priors, bounds are already set correctly (lbcum=0, ubcum=1)

% Transform uniform quantiles to prior draws
pdraw = NaN(nsamp, npar);
% Rescale to truncated cumulative range
u = u .* (ubcum' - lbcum') + lbcum';

if o.has('uniform')
    id = o.iduniform;
    pdraw(:, id) = u(:, id) .* (o.p4(id)' - o.p3(id)') + o.p3(id)';
end

% Apply inverse CDFs
% MATLAB's Statistics Toolbox functions support implicit expansion (broadcasting
% nsamp×k quantile matrices against 1×k parameter rows) since R2016b. However,
% Octave's statistics package and Dynare's bundled routines (matlab/missing/stats/)
% both use common_size, which only supports scalar expansion. We therefore fall
% back to per-parameter loops when the Statistics Toolbox is not available.
USE_LOOPS = isoctave || ~user_has_matlab_license('statistics_toolbox');

if o.has('gaussian')
    id = o.idgaussian;
    if USE_LOOPS
        for j = 1:length(id)
            i = id(j);
            pdraw(:, i) = norminv(u(:, i), o.p6(i), o.p7(i));
        end
    else
        pdraw(:, id) = norminv(u(:, id), o.p6(id)', o.p7(id)');
    end
end

if o.has('gamma')
    id = o.idgamma;
    if USE_LOOPS
        for j = 1:length(id)
            i = id(j);
            pdraw(:, i) = gaminv(u(:, i), o.p6(i), o.p7(i)) + o.p3(i);
        end
    else
        pdraw(:, id) = gaminv(u(:, id), o.p6(id)', o.p7(id)') + o.p3(id)';
    end
end

if o.has('beta')
    id = o.idbeta;
    if USE_LOOPS
        for j = 1:length(id)
            i = id(j);
            pdraw(:, i) = betainv(u(:, i), o.p6(i), o.p7(i)) * (o.p4(i) - o.p3(i)) + o.p3(i);
        end
    else
        pdraw(:, id) = betainv(u(:, id), o.p6(id)', o.p7(id)') .* (o.p4(id)' - o.p3(id)') + o.p3(id)';
    end
end

if o.has('invgamma1')
    id = o.idinvgamma1;
    if USE_LOOPS
        for j = 1:length(id)
            i = id(j);
            pdraw(:, i) = sqrt(1 ./ gaminv(u(:, i), o.p7(i)/2, 2/o.p6(i))) + o.p3(i);
        end
    else
        pdraw(:, id) = sqrt(1 ./ gaminv(u(:, id), o.p7(id)'/2, 2./o.p6(id)')) + o.p3(id)';
    end
end

if o.has('invgamma2')
    id = o.idinvgamma2;
    if USE_LOOPS
        for j = 1:length(id)
            i = id(j);
            pdraw(:, i) = 1 ./ gaminv(u(:, i), o.p7(i)/2, 2/o.p6(i)) + o.p3(i);
        end
    else
        pdraw(:, id) = 1 ./ gaminv(u(:, id), o.p7(id)'/2, 2./o.p6(id)') + o.p3(id)';
    end
end

if o.has('weibull')
    id = o.idweibull;
    if USE_LOOPS
        for j = 1:length(id)
            i = id(j);
            pdraw(:, i) = wblinv(u(:, i), o.p7(i), o.p6(i)) + o.p3(i);
        end
    else
        pdraw(:, id) = wblinv(u(:, id), o.p7(id)', o.p6(id)') + o.p3(id)';
    end
end

return % --*-- Unit tests --*--

%@test:1
% Build a 14-parameter prior (2 of each distribution family) with add(),
% then verify that from_uniform() produces in-bound samples with the right
% marginal moments.
prior_trunc = 1e-10;
Prior = dprior();
for rep = 1:2
    Prior.add(sprintf('p%d_beta',      rep), 'shape', 'beta',       'mean', 0.4, 'std', 0.2, 'prior_trunc', prior_trunc);
    Prior.add(sprintf('p%d_gamma',     rep), 'shape', 'gamma',      'mean', 0.4, 'std', 0.2, 'prior_trunc', prior_trunc);
    Prior.add(sprintf('p%d_normal',    rep), 'shape', 'normal',     'mean', 0.4, 'std', 0.2, 'prior_trunc', prior_trunc);
    Prior.add(sprintf('p%d_invgamma1', rep), 'shape', 'inv_gamma',  'mean', 0.4, 'std', 0.2, 'prior_trunc', prior_trunc);
    Prior.add(sprintf('p%d_uniform',   rep), 'shape', 'uniform',    'mean', 0.4, 'std', 0.2, 'prior_trunc', prior_trunc);
    Prior.add(sprintf('p%d_invgamma2', rep), 'shape', 'inv_gamma2', 'mean', 0.4, 'std', 0.2, 'prior_trunc', prior_trunc);
    Prior.add(sprintf('p%d_weibull',   rep), 'shape', 'weibull',    'mean', 0.4, 'std', 0.2, 'prior_trunc', prior_trunc);
end

ndraws = 1e5;
npar = length(Prior);

try
    u = rand(ndraws, npar);
    pdraw = Prior.from_uniform(u);
    t(1) = true;
catch
    t(1) = false;
end

if t(1)
    t(2) = isequal(size(pdraw), [ndraws, npar]);
    t(3) = all(all(pdraw >= repmat(Prior.lb', ndraws, 1), 1));
    t(4) = all(all(pdraw <= repmat(Prior.ub', ndraws, 1), 1));
    m_empirical = mean(pdraw, 1)';
    t(5) = all(abs(m_empirical - Prior.p1) < 3e-3);
    v_empirical = var(pdraw, 0, 1)';
    t(6) = all(abs(v_empirical - Prior.p2.^2) < 5e-3);
end
T = all(t);
%@eof:1

%@test:2
% Same uniform input should produce same output.
prior_trunc = 1e-10;
Prior = dprior();
Prior.add('n', 'shape', 'normal', 'mean', 0.5, 'std', 0.10, 'prior_trunc', prior_trunc);
Prior.add('g', 'shape', 'gamma',  'mean', 0.4, 'std', 0.15, 'prior_trunc', prior_trunc);
Prior.add('b', 'shape', 'beta',   'mean', 0.3, 'std', 0.08, 'prior_trunc', prior_trunc);
Prior.add('u', 'shape', 'uniform','mean', 0.6, 'std', 0.12, 'prior_trunc', prior_trunc);

try
    u = rand(100, 4);
    pdraw1 = Prior.from_uniform(u);
    pdraw2 = Prior.from_uniform(u);
    t(1) = true;
catch
    t(1) = false;
end

if t(1)
    t(2) = all(all(pdraw1 == pdraw2));
end
T = all(t);
%@eof:2

%@test:3
% Empirical moments of from_uniform draws match the declared mean/std.
prior_trunc = 1e-10;
Prior = dprior();
Prior.add('n', 'shape', 'normal', 'mean', 0.5, 'std', 0.10, 'prior_trunc', prior_trunc);
Prior.add('g', 'shape', 'gamma',  'mean', 0.4, 'std', 0.15, 'prior_trunc', prior_trunc);
Prior.add('u', 'shape', 'uniform','mean', 0.6, 'std', 0.12, 'prior_trunc', prior_trunc);

ndraws = 5000;

try
    u = rand(ndraws, 3);
    pdraw_icdf = Prior.from_uniform(u);
    t(1) = true;
catch
    t(1) = false;
end

if t(1)
    m_icdf = mean(pdraw_icdf, 1)';
    t(2) = all(abs(m_icdf - Prior.p1) < 0.05);
    v_icdf = var(pdraw_icdf, 0, 1)';
    t(3) = all(abs(v_icdf - Prior.p2.^2) < 0.02);
end
T = all(t);
%@eof:3

%@test:4
% Regression test for a Weibull parameter order bug: parameters must be
% ordered [shape, scale] and wblinv must be called with (scale, shape) per
% MATLAB convention. If swapped, the empirical mean would be wrong.
prior_trunc = 1e-10;
Prior = dprior('w', 'shape', 'weibull', 'mean', 0.8, 'std', 0.15, 'prior_trunc', prior_trunc);

ndraws = 1e4;

try
    u = rand(ndraws, 1);
    pdraw = Prior.from_uniform(u);
    t(1) = true;
catch
    t(1) = false;
end

if t(1)
    m_empirical = mean(pdraw);
    v_empirical = var(pdraw);
    t(2) = abs(m_empirical - Prior.p1) < 0.05;
    t(3) = all(pdraw > 0);
    t(4) = v_empirical > 0 && abs(v_empirical - Prior.p2^2) < 0.1;
end
T = all(t);
%@eof:4
