round(self::percentile($samples, 10), 2), // P10 'ale_ml' => round(self::percentile($samples, 50), 2), // mediana 'ale_max' => round(self::percentile($samples, 90), 2), // P90 'ale_mean' => round($sumAle / $n, 2), 'lef_mean' => round($sumLef / $n, 4), 'iterations' => $iterations, 'currency' => 'EUR', 'percentiles'=> [ 'p10' => round(self::percentile($samples, 10), 2), 'p50' => round(self::percentile($samples, 50), 2), 'p90' => round(self::percentile($samples, 90), 2), 'p99' => round(self::percentile($samples, 99), 2), ], 'histogram' => self::histogram($samples, 12), ]; } /** Campionamento da distribuzione PERT (Beta riscalata) con lambda=4. */ private static function pert(float $min, float $ml, float $max): float { if ($max <= $min) return $min; $ml = self::clamp($ml, $min, $max); $lambda = 4.0; $alpha = 1 + $lambda * ($ml - $min) / ($max - $min); $beta = 1 + $lambda * ($max - $ml) / ($max - $min); return $min + self::betaSample($alpha, $beta) * ($max - $min); } /** Campione Beta(alpha,beta) via due Gamma. */ private static function betaSample(float $a, float $b): float { $x = self::gammaSample($a); $y = self::gammaSample($b); $s = $x + $y; return $s > 0 ? $x / $s : 0.5; } /** Campione Gamma(shape, 1) - Marsaglia & Tsang. */ private static function gammaSample(float $shape): float { if ($shape < 1) { $u = self::rand01(); return self::gammaSample(1 + $shape) * pow($u, 1 / $shape); } $d = $shape - 1.0 / 3.0; $c = 1.0 / sqrt(9 * $d); while (true) { do { $x = self::randNormal(); $v = 1 + $c * $x; } while ($v <= 0); $v = $v * $v * $v; $u = self::rand01(); if ($u < 1 - 0.0331 * $x * $x * $x * $x) return $d * $v; if (log($u) < 0.5 * $x * $x + $d * (1 - $v + log($v))) return $d * $v; } } private static function randNormal(): float { // Box-Muller $u1 = self::rand01(); $u2 = self::rand01(); return sqrt(-2 * log($u1 + 1e-12)) * cos(2 * M_PI * $u2); } private static function rand01(): float { return (mt_rand() + 1) / (mt_getrandmax() + 2); } private static function percentile(array $sorted, float $p): float { $n = count($sorted); if ($n === 0) return 0.0; $rank = ($p / 100) * ($n - 1); $lo = (int) floor($rank); $hi = (int) ceil($rank); if ($lo === $hi) return $sorted[$lo]; $frac = $rank - $lo; return $sorted[$lo] * (1 - $frac) + $sorted[$hi] * $frac; } private static function histogram(array $sorted, int $bins): array { $n = count($sorted); if ($n === 0) return []; $min = $sorted[0]; $max = $sorted[$n - 1]; if ($max <= $min) return [['from' => $min, 'to' => $max, 'count' => $n]]; $w = ($max - $min) / $bins; $h = array_fill(0, $bins, 0); foreach ($sorted as $v) { $idx = (int) min($bins - 1, floor(($v - $min) / $w)); $h[$idx]++; } $out = []; for ($i = 0; $i < $bins; $i++) { $out[] = ['from' => round($min + $i * $w, 2), 'to' => round($min + ($i + 1) * $w, 2), 'count' => $h[$i]]; } return $out; } private static function clamp(float $v, float $lo, float $hi): float { return max($lo, min($hi, $v)); } private static function sort3(float $a, float $b, float $c): array { $arr = [$a, $b, $c]; sort($arr); return $arr; } }