//Initialise functions
{
if (!global.Statistics)
/**
* The namespace for all UF/Statistics utility functions, typically for static methods.
*
* @namespace Statistics
*/
global.Statistics = {};
Statistics.default_biological_sex_ratios = new Float32Array([
1.045, 1.042, 1.040, 1.038, 1.030, 1.015,
0.985, 0.975, 0.965, 0.960, 0.950, 0.930,
0.900, 0.860, 0.810, 0.750, 0.680, 0.600
]);
Statistics.default_sex_ratio_bounds = [
[1.02, 1.07], // 00 (0-1) - Human biology tightly constrains sex ratio at birth
[1.01, 1.07], // 01 (1-4)
[0.95, 1.10], // 05 (5-9)
[0.95, 1.10], // 10 (10-14)
[0.50, 1.80], // 15 (15-19) - Working ages allow war shocks & sex-selective migration
[0.50, 1.80], // 20 (20-24)
[0.50, 1.80], // 25 (25-29)
[0.50, 1.80], // 30 (30-34)
[0.50, 1.80], // 35 (35-39)
[0.50, 1.80], // 40 (40-44)
[0.50, 1.80], // 45 (45-49)
[0.50, 1.80], // 50 (50-54)
[0.50, 1.80], // 55 (55-59)
[0.50, 1.80], // 60 (60-64)
[0.30, 1.05], // 65 (65-69) - Elderly cohorts reflect female survival advantage
[0.30, 1.05], // 70 (70-74)
[0.30, 1.05], // 75 (75-79)
[0.30, 1.05] // 80 (80+)
];
/**
* Smooths age cohorts and couples sex ratios simultaneously. Enforces non-increasing
* monotonicity along the age dimension using weighted PAVA on total cohort population density,
* then allocates totals between male and female using the model's empirical sex ratio bounded
* within a biological plausibility envelope (Option 1).
*
* @alias Statistics.coupleAgeSexCohorts
*
* @param {Float32Array|Array<number>} arg0_male_rates - Raw male cohort values.
* @param {Float32Array|Array<number>} arg1_female_rates - Raw female cohort values.
* @param {Float32Array|Array<number>} arg2_band_widths - Cohort duration in years (e.g. 1 for 00, 4 for 01, 5 for others).
* @param {number} [arg3_start_index=0] - Index from which to enforce age monotonicity (defaults to 0 on annualised density).
* @param {Object} [arg4_options]
* @param {Float32Array|Array<number>} [arg4_options.baseline_sex_ratios] - Optional target sex-ratio curve M/F by cohort.
* @param {Array<Array<number>>} [arg4_options.sex_ratio_bounds] - Optional min/max sex-ratio bounds by cohort.
* @param {boolean} [arg4_options.enforce_fixed_sex_ratios=false] - If true, strictly forces M/F = baseline_sex_ratios.
* @param {Object} [arg4_options.buffers] - Optional pre-allocated flat buffers for zero GC overhead.
* @param {Float32Array} [arg4_options.female_output] - Optional target female output buffer.
* @param {Float32Array} [arg4_options.male_output] - Optional target male output buffer.
* @param {Float32Array} [arg4_options.total_output] - Optional target total output buffer.
*
* @returns {{ female: Float32Array, male: Float32Array, total: Float32Array }} Coupled smoothed cohort arrays.
*/
Statistics.coupleAgeSexCohorts = function (arg0_male_rates, arg1_female_rates, arg2_band_widths, arg3_start_index, arg4_options) {
//Convert from parameters
let male_rates = arg0_male_rates;
let female_rates = arg1_female_rates;
let band_widths = arg2_band_widths;
let start_index = (arg3_start_index !== undefined && arg3_start_index !== null) ? parseInt(arg3_start_index) : 0;
let options = (arg4_options) ? arg4_options : {};
//Initialise options
let bounds = options.sex_ratio_bounds || Statistics.default_sex_ratio_bounds;
let do_not_smooth = (options.do_not_smooth !== undefined) ? options.do_not_smooth : (options.lift_isotonic || options.pava === false || start_index < 0 || arg3_start_index === null);
let enforce_biological = (options.enforce_biological_sex_ratios !== false);
let enforce_fixed = (options.enforce_fixed_sex_ratios === true);
let preserve_sex_ratios = (options.preserve_sex_ratios === true);
//Declare local instance variables
let baseline_ratios = options.baseline_sex_ratios || Statistics.default_biological_sex_ratios;
let buffers = options.buffers;
let count = male_rates.length;
let effective_weights = (buffers && buffers.effective_weights) ? buffers.effective_weights : (options.effective_weights || new Float32Array(count));
let female_out = options.female_output || new Float32Array(count);
let male_out = options.male_output || new Float32Array(count);
let total_annualised = (buffers && buffers.total_annualised) ? buffers.total_annualised : new Float32Array(count);
let total_out = options.total_output || new Float32Array(count);
//Guard clauses
if (count === 0)
return { female: female_out, male: male_out, total: total_out };
//Function body
//1. Compute total cohort population and effective weights (band widths)
for (let i = 0; i < count; i++) {
let m_val = (male_rates[i] > 0) ? male_rates[i] : 0;
let f_val = (female_rates[i] > 0) ? female_rates[i] : 0;
let w = (band_widths && band_widths[i] > 0) ? band_widths[i] : 1;
let tot = m_val + f_val;
total_annualised[i] = tot / w;
effective_weights[i] = w;
}
//2. Run weighted PAVA on total population density to guarantee age monotonicity (if smoothing enabled)
let smoothed_density = total_annualised;
if (!do_not_smooth) {
let pava_options = (options.buffers) ? { buffers: options.buffers } : {};
smoothed_density = Statistics.pavaDecreasing(total_annualised, effective_weights, Math.max(0, start_index), pava_options);
}
//3. Reconstitute female, male, and total cohorts using male-share formulation
for (let i = 0; i < count; i++) {
let w = (band_widths && band_widths[i] > 0) ? band_widths[i] : 1;
let tot_sm = smoothed_density[i]*w;
let p_m;
if (enforce_fixed) {
let r_prior = (baseline_ratios && baseline_ratios[i] !== undefined) ?
baseline_ratios[i] :
(Statistics.default_biological_sex_ratios[i] || 1.045);
p_m = r_prior / (1.0 + r_prior);
} else {
let m_raw = (male_rates[i] > 0) ? male_rates[i] : 0;
let f_raw = (female_rates[i] > 0) ? female_rates[i] : 0;
let tot_raw = m_raw + f_raw;
if (tot_raw > 1e-9) {
p_m = m_raw / tot_raw;
} else {
let r_prior = (baseline_ratios && baseline_ratios[i] !== undefined) ?
baseline_ratios[i] :
(Statistics.default_biological_sex_ratios[i] || 1.045);
p_m = r_prior / (1.0 + r_prior);
}
if (enforce_biological && !preserve_sex_ratios) {
let cohort_bounds = (bounds && bounds[i]) ? bounds[i] : [0.40, 1.80];
let p_min = cohort_bounds[0] / (1.0 + cohort_bounds[0]);
let p_max = cohort_bounds[1] / (1.0 + cohort_bounds[1]);
p_m = Math.max(p_min, Math.min(p_max, p_m));
}
}
let m_sm = tot_sm*p_m;
let f_sm = tot_sm*(1.0 - p_m);
female_out[i] = f_sm;
male_out[i] = m_sm;
total_out[i] = tot_sm;
}
//Return statement
return { female: female_out, male: male_out, total: total_out };
};
/**
* Computes a normalised logistic transition kernel weight w in [0, 1] over a domain [start_val, end_val].
* Returns 0 for values <= start_val, 1 for values >= end_val, and an S-curve blend for intermediate values.
*
* @alias Statistics.getLogisticKernelWeight
*
* @param {number} arg0_value
* @param {number} [arg1_start_val=1750]
* @param {number} [arg2_end_val=1850]
* @param {Object} [arg3_options]
* @param {number} [arg3_options.steepness=8]
*
* @returns {number}
*/
Statistics.getLogisticKernelWeight = function (arg0_value, arg1_start_val, arg2_end_val, arg3_options) {
//Convert from parameters
let value = arg0_value;
let start_val = (arg1_start_val !== undefined && arg1_start_val !== null) ? arg1_start_val : 1750;
let end_val = (arg2_end_val !== undefined && arg2_end_val !== null) ? arg2_end_val : 1850;
let options = (arg3_options) ? arg3_options : {};
//Initialise options
let steepness = (options.steepness !== undefined) ? options.steepness : 8;
//Guard clauses
if (!Number.isFinite(value)) return 0;
if (start_val >= end_val) return (value >= end_val) ? 1 : 0;
if (value <= start_val) return 0;
if (value >= end_val) return 1;
//Declare local instance variables
let f_0 = 1 / (1 + Math.exp(steepness * 0.5));
let f_1 = 1 / (1 + Math.exp(-steepness * 0.5));
let f_x = 0;
let normalised_weight = 0;
let x = (value - start_val) / (end_val - start_val);
//Function body
f_x = 1 / (1 + Math.exp(-steepness * (x - 0.5)));
normalised_weight = (f_x - f_0) / (f_1 - f_0);
normalised_weight = Math.max(0, Math.min(1, normalised_weight));
//Return statement
return normalised_weight;
};
/**
* Enforces non-increasing monotonicity on a sequence using the weighted Pool Adjacent Violators
* Algorithm (PAVA). Values before arg2_start_index are preserved unconstrained.
*
* @alias Statistics.pavaDecreasing
*
* @param {Float32Array|Array<number>} arg0_values - Array of values to smooth.
* @param {Float32Array|Array<number>} [arg1_weights] - Optional weights (e.g. cohort band widths).
* @param {number} [arg2_start_index=0] - Index from which to enforce non-increasing monotonicity.
* @param {Object} [arg3_options]
* @param {Object} [arg3_options.buffers] - Optional pre-allocated flat buffers for zero GC overhead.
* @param {Float32Array} [arg3_options.buffers.block_vals]
* @param {Float32Array} [arg3_options.buffers.block_weights]
* @param {Int32Array} [arg3_options.buffers.block_counts]
* @param {Float32Array|Array<number>} [arg3_options.output] - Optional target output buffer.
*
* @returns {Float32Array|Array<number>} Monotonically decreasing output array.
*/
Statistics.pavaDecreasing = function (arg0_values, arg1_weights, arg2_start_index, arg3_options) {
//Convert from parameters
let values = arg0_values;
let weights = arg1_weights;
let start_index = (arg2_start_index !== undefined) ? Math.max(0, parseInt(arg2_start_index)) : 0;
let options = (arg3_options) ? arg3_options : {};
//Declare local instance variables
let buffers = options.buffers;
let total_length = values.length;
let output = (buffers && buffers.output) ? buffers.output : (options.output || new Float32Array(total_length));
//Copy unconstrained initial elements (e.g. infant and child mortality)
for (let i = 0; i < start_index; i++)
output[i] = values[i];
let active_length = total_length - start_index;
if (active_length <= 0) return output;
let block_counts = (buffers && buffers.block_counts) ? buffers.block_counts : new Int32Array(active_length);
let block_vals = (buffers && buffers.block_vals) ? buffers.block_vals : new Float32Array(active_length);
let block_weights = (buffers && buffers.block_weights) ? buffers.block_weights : new Float32Array(active_length);
//Initialise block states
for (let i = 0; i < active_length; i++) {
let src_idx = start_index + i;
block_counts[i] = 1;
block_vals[i] = values[src_idx];
block_weights[i] = (weights && weights[src_idx] !== undefined) ? weights[src_idx] : 1;
}
//PAVA pooling loop
let b = 0;
let num_blocks = active_length;
while (b < num_blocks - 1) {
//Violation of non-increasing order: previous block is smaller than next block
if (block_vals[b] < block_vals[b + 1]) {
let w_sum = block_weights[b] + block_weights[b + 1];
block_vals[b] = ((block_vals[b]*block_weights[b]) + (block_vals[b + 1]*block_weights[b + 1]))/w_sum;
block_weights[b] = w_sum;
block_counts[b] += block_counts[b + 1];
//Shift subsequent blocks left
for (let k = b + 1; k < num_blocks - 1; k++) {
block_counts[k] = block_counts[k + 1];
block_vals[k] = block_vals[k + 1];
block_weights[k] = block_weights[k + 1];
}
num_blocks--;
//Step back to check against previous block
if (b > 0) b--;
} else {
b++;
}
}
//Unpack blocks into output
let write_idx = start_index;
for (let k = 0; k < num_blocks; k++) {
let count = block_counts[k];
let val = block_vals[k];
for (let j = 0; j < count; j++)
output[write_idx++] = val;
}
//Return statement
return output;
};
}