Skip to content

Commit 9a23f6a

Browse files
committed
Minor updates, and working on documentation.
1 parent 98ac836 commit 9a23f6a

32 files changed

Lines changed: 4654 additions & 193 deletions

Numerics/Data/Paired Data/ProbabilityOrdinate.cs

Lines changed: 560 additions & 0 deletions
Large diffs are not rendered by default.

Numerics/Distributions/Univariate/Uncertainty Analysis/UncertaintyAnalysisResults.cs

Lines changed: 225 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -28,6 +28,8 @@
2828
* THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
2929
*/
3030

31+
using Numerics.Data;
32+
using Numerics.Data.Statistics;
3133
using Numerics.Mathematics.Optimization;
3234
using Numerics.Sampling.MCMC;
3335
using Numerics.Utilities;
@@ -56,6 +58,51 @@ namespace Numerics.Distributions
5658
public class UncertaintyAnalysisResults
5759
{
5860

61+
/// <summary>
62+
/// Construct an instance of the UncertaintyAnalysisResults class.
63+
/// </summary>
64+
public UncertaintyAnalysisResults() { }
65+
66+
/// <summary>
67+
/// Constructs a new instance with computed uncertainty metrics.
68+
/// </summary>
69+
/// <param name="parentDistribution">The parent (mode/point estimate) distribution.</param>
70+
/// <param name="sampledDistributions">Array of sampled distributions from posterior or bootstrap.</param>
71+
/// <param name="probabilities">Array of non-exceedance probabilities for quantile estimation.</param>
72+
/// <param name="alpha">The confidence level (default = 0.1 for 90% CI).</param>
73+
/// <param name="minProbability">Minimum probability for mean curve computation (default = 0.001).</param>
74+
/// <param name="maxProbability">Maximum probability for mean curve computation (default = 1 - 1e-9).</param>
75+
/// <param name="recordParameterSets">If true, stores all parameter sets from sampled distributions.</param>
76+
public UncertaintyAnalysisResults(UnivariateDistributionBase parentDistribution,
77+
UnivariateDistributionBase[] sampledDistributions,
78+
double[] probabilities,
79+
double alpha = 0.1,
80+
double minProbability = 0.001,
81+
double maxProbability = 1 - 1e-9,
82+
bool recordParameterSets = false)
83+
{
84+
if (parentDistribution == null)
85+
throw new ArgumentNullException(nameof(parentDistribution));
86+
if (sampledDistributions == null || sampledDistributions.Length == 0)
87+
throw new ArgumentException("Sampled distributions cannot be null or empty.", nameof(sampledDistributions));
88+
if (probabilities == null || probabilities.Length == 0)
89+
throw new ArgumentException("Probabilities cannot be null or empty.", nameof(probabilities));
90+
91+
ProcessModeCurve(parentDistribution, probabilities);
92+
ProcessConfidenceIntervals(sampledDistributions, probabilities, alpha);
93+
ProcessMeanCurve(sampledDistributions, probabilities, minProbability, maxProbability);
94+
95+
if (recordParameterSets)
96+
ProcessParameterSets(sampledDistributions);
97+
98+
// Set default values
99+
AIC = double.NaN;
100+
BIC = double.NaN;
101+
DIC = double.NaN;
102+
RMSE = double.NaN;
103+
ERL = double.NaN;
104+
}
105+
59106
/// <summary>
60107
/// The parent probability distribution.
61108
/// </summary>
@@ -302,32 +349,192 @@ public static UncertaintyAnalysisResults FromXElement(XElement xElement)
302349
}
303350

304351
/// <summary>
305-
/// Returns uncertainty analysis results for the distribution estimated from MCMC.
352+
/// Process and set the parent distribution and computed curve (mode, plug-in, point estimate).
306353
/// </summary>
307-
/// <param name="results">The MCMC results.</param>
308-
/// <param name="distribution">The parent distribution.</param>
309-
/// <param name="probabilities">List of non-exceedance probabilities.</param>
354+
/// <param name="parentDistribution">The parent distribution.</param>
355+
/// <param name="probabilities">Array of non-exceedance probabilities.</param>
356+
public void ProcessModeCurve(UnivariateDistributionBase parentDistribution, double[] probabilities)
357+
{
358+
if (parentDistribution == null)
359+
throw new ArgumentNullException(nameof(parentDistribution));
360+
if (probabilities == null || probabilities.Length == 0)
361+
throw new ArgumentException("Probabilities cannot be null or empty.", nameof(probabilities));
362+
363+
ParentDistribution = parentDistribution;
364+
ModeCurve = ParentDistribution.InverseCDF(probabilities);
365+
}
366+
367+
/// <summary>
368+
/// Process and set the confidence intervals from a list of sampled distributions.
369+
/// </summary>
370+
/// <param name="sampledDistributions">The list of sampled distributions to process.</param>
371+
/// <param name="probabilities">Array of non-exceedance probabilities.</param>
310372
/// <param name="alpha">The confidence level; Default = 0.1, which will result in the 90% confidence intervals.</param>
311-
public static UncertaintyAnalysisResults FromMCMCResults(MCMCResults results, UnivariateDistributionBase distribution, IList<double> probabilities, double alpha = 0.1)
373+
public void ProcessConfidenceIntervals(UnivariateDistributionBase[] sampledDistributions, double[] probabilities, double alpha = 0.1)
374+
{
375+
if (sampledDistributions == null || sampledDistributions.Length == 0)
376+
throw new ArgumentException("Sampled distributions cannot be null or empty.", nameof(sampledDistributions));
377+
if (probabilities == null || probabilities.Length == 0)
378+
throw new ArgumentException("Probabilities cannot be null or empty.", nameof(probabilities));
379+
if (alpha <= 0 || alpha >= 1)
380+
throw new ArgumentOutOfRangeException(nameof(alpha), "Alpha must be between 0 and 1.");
381+
382+
int B = sampledDistributions.Length;
383+
int p = probabilities.Length;
384+
double lowerCI = alpha / 2d;
385+
double upperCI = 1d - alpha / 2d;
386+
ConfidenceIntervals = new double[p, 2];
387+
388+
// Loop over probabilities and record percentiles
389+
for (int i = 0; i < p; i++)
390+
{
391+
var XValues = new double[B];
392+
393+
// Compute quantiles in parallel
394+
Parallel.For(0, B, idx => {
395+
XValues[idx] = sampledDistributions[idx]?.InverseCDF(probabilities[i]) ?? double.NaN;
396+
});
397+
398+
// Filter valid values and sort
399+
int validCount = 0;
400+
for (int j = 0; j < B; j++)
401+
{
402+
if (!double.IsNaN(XValues[j])) validCount++;
403+
}
404+
405+
var validValues = new double[validCount];
406+
int writeIdx = 0;
407+
for (int j = 0; j < B; j++)
408+
{
409+
if (!double.IsNaN(XValues[j]))
410+
validValues[writeIdx++] = XValues[j];
411+
}
412+
413+
Array.Sort(validValues);
414+
415+
// Record percentiles for CIs
416+
ConfidenceIntervals[i, 0] = Statistics.Percentile(validValues, lowerCI, true);
417+
ConfidenceIntervals[i, 1] = Statistics.Percentile(validValues, upperCI, true);
418+
}
419+
}
420+
421+
/// <summary>
422+
/// Computes the mean (predictive) curve by averaging CDFs across all sampled distributions.
423+
/// Uses log-spaced quantiles for efficient computation across wide ranges.
424+
/// </summary>
425+
/// <param name="sampledDistributions">Array of sampled distributions from posterior or bootstrap.</param>
426+
/// <param name="probabilities">Array of non-exceedance probabilities for interpolation.</param>
427+
/// <param name="minProbability">Minimum probability for range determination (default = 0.001).</param>
428+
/// <param name="maxProbability">Maximum probability for range determination (default = 1 - 1e-9).</param>
429+
public void ProcessMeanCurve(UnivariateDistributionBase[] sampledDistributions, double[] probabilities, double minProbability = 0.001, double maxProbability = 1 - 1e-9)
312430
{
313-
if (results == null|| results.Output == null ||results.Output.Count == 0) return null;
314-
if (results.Output[0].Values.Length != distribution.NumberOfParameters) return null;
431+
if (sampledDistributions == null || sampledDistributions.Length == 0)
432+
throw new ArgumentException("Sampled distributions cannot be null or empty.", nameof(sampledDistributions));
433+
if (probabilities == null || probabilities.Length == 0)
434+
throw new ArgumentException("Probabilities cannot be null or empty.", nameof(probabilities));
435+
436+
int B = sampledDistributions.Length;
315437

316-
int realz = results.Output.Count;
317-
var dists = new UnivariateDistributionBase[results.Output.Count];
438+
// Compute min and max X values across all distributions
439+
double minX = double.MaxValue;
440+
double maxX = double.MinValue;
441+
object lockObject = new object();
318442

319-
Parallel.For(0, realz, idx =>
443+
Parallel.For(0, B, j =>
320444
{
321-
var d = distribution.Clone();
322-
d.SetParameters(results.Output[idx].Values);
323-
dists[idx] = d;
445+
if (sampledDistributions[j] != null)
446+
{
447+
var innerMin = sampledDistributions[j].InverseCDF(minProbability);
448+
var innerMax = sampledDistributions[j].InverseCDF(maxProbability);
449+
450+
lock (lockObject)
451+
{
452+
if (innerMin < minX) minX = innerMin;
453+
if (innerMax > maxX) maxX = innerMax;
454+
}
455+
}
324456
});
325457

326-
// Set up dummy bootstrap analysis
327-
var boot = new BootstrapAnalysis(distribution, ParameterEstimationMethod.MaximumLikelihood, 100, realz);
328-
var uar = boot.Estimate(probabilities, alpha, dists);
329-
uar.ParameterSets = null;
330-
return uar;
458+
// Create log-spaced quantiles for efficient coverage
459+
double shift = minX <= 0 ? Math.Abs(minX) + 1d : 0;
460+
double min = minX + shift;
461+
double max = maxX + shift;
462+
int order = (int)Math.Floor(Math.Log10(max) - Math.Log10(min));
463+
int bins = Math.Max(200, Math.Min(1000, 100 * order));
464+
465+
var quantiles = new double[bins];
466+
double delta = (Math.Log10(max) - Math.Log10(min)) / (bins - 1);
467+
468+
for (int i = 0; i < bins; i++)
469+
{
470+
double logX = Math.Log10(min) + i * delta;
471+
quantiles[i] = Math.Pow(10, logX) - shift;
472+
}
473+
474+
// Compute expected probability for each quantile
475+
var expected = new double[bins];
476+
for (int i = 0; i < bins; i++)
477+
{
478+
double total = 0d;
479+
Parallel.For(0, B, () => 0d, (j, loop, sum) =>
480+
{
481+
if (sampledDistributions[j] != null)
482+
{
483+
sum += sampledDistributions[j].CDF(quantiles[i]);
484+
}
485+
return sum;
486+
}, z => Tools.ParallelAdd(ref total, z));
487+
expected[i] = total / B;
488+
}
489+
490+
// Build monotonic interpolation points
491+
var yVals = new List<double> { quantiles[0] };
492+
var xVals = new List<double> { expected[0] };
493+
double minY = quantiles[0];
494+
double maxY = quantiles[0];
495+
496+
for (int i = 1; i < bins; i++)
497+
{
498+
if (expected[i] > xVals[xVals.Count - 1])
499+
{
500+
minY = Math.Min(minY, quantiles[i]);
501+
maxY = Math.Max(maxY, quantiles[i]);
502+
yVals.Add(quantiles[i]);
503+
xVals.Add(expected[i]);
504+
}
505+
}
506+
507+
// Determine if log transform is appropriate
508+
bool useLogTransform = minY > 0 && (Math.Log10(maxY) - Math.Log10(minY)) > 1;
509+
510+
// Interpolate mean curve at requested probabilities
511+
var linint = new Linear(xVals, yVals)
512+
{
513+
XTransform = Transform.NormalZ,
514+
YTransform = useLogTransform ? Transform.Logarithmic : Transform.None
515+
};
516+
MeanCurve = linint.Interpolate(probabilities);
517+
}
518+
519+
/// <summary>
520+
/// Processes and stores the parameter sets from all sampled distributions.
521+
/// </summary>
522+
/// <param name="sampledDistributions">Array of sampled distributions to extract parameters from.</param>
523+
public void ProcessParameterSets(UnivariateDistributionBase[] sampledDistributions)
524+
{
525+
if (sampledDistributions == null || sampledDistributions.Length == 0)
526+
throw new ArgumentException("Sampled distributions cannot be null or empty.", nameof(sampledDistributions));
527+
528+
int B = sampledDistributions.Length;
529+
ParameterSets = new ParameterSet[B];
530+
531+
Parallel.For(0, B, idx =>
532+
{
533+
if (sampledDistributions[idx] != null)
534+
{
535+
ParameterSets[idx] = new ParameterSet(sampledDistributions[idx].GetParameters, double.NaN);
536+
}
537+
});
331538
}
332539

333540
}

Numerics/Numerics.csproj

Lines changed: 6 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -8,14 +8,14 @@
88
<DebugType>embedded</DebugType>
99

1010
<Description>Numerics is a free and open-source library for .NET developed by USACE-RMC, providing a comprehensive set of methods and algorithms for numerical computations and statistical analysis.</Description>
11-
<PackageProjectUrl>https://github.com/USArmy-Corps-of-Engineers-RMC/Numerics</PackageProjectUrl>
12-
<RepositoryUrl>https://github.com/USArmy-Corps-of-Engineers-RMC/Numerics</RepositoryUrl>
11+
<PackageProjectUrl>https://github.com/USACE-RMC/Numerics</PackageProjectUrl>
12+
<RepositoryUrl>https://github.com/USACE-RMC/Numerics</RepositoryUrl>
1313
<RepositoryType>git</RepositoryType>
1414
<Title>Numerics</Title>
1515
<GeneratePackageOnBuild>False</GeneratePackageOnBuild>
1616
<Company>USACE-RMC</Company>
1717
<PackageReadmeFile>README.md</PackageReadmeFile>
18-
<PackageTags>interpolation; regression; statistics; machine learning; probability distributions; bootstrap analysis; distribution fitting; MCMC sampling; optimization;</PackageTags>
18+
<PackageTags>interpolation; regression; statistics; machine learning; probability distributions; bootstrap analysis; distribution fitting; Bayesian MCMC sampling; optimization;</PackageTags>
1919
<PackageLicenseFile>LICENSE</PackageLicenseFile>
2020
<PackageRequireLicenseAcceptance>True</PackageRequireLicenseAcceptance>
2121
<PackageId>RMC.Numerics</PackageId>
@@ -29,6 +29,9 @@
2929
<PropertyGroup>
3030
<!-- 1591 is missing xml comment for a public member. 1587 is an xml comment not placed on a valid member (in our case, mostly license text at top of files) -->
3131
<NoWarn>CS1591;CS1587</NoWarn>
32+
<Version>2.0.0</Version>
33+
<PackageReleaseNotes>This major update delivers broad improvements across the entire Numerics library, with substantial enhancements to data analysis, statistical modeling, numerical methods, distributions, optimization, machine learning utilities, and overall performance and reliability. The update reflects over a year of incremental development, code modernization, and refinement.</PackageReleaseNotes>
34+
<AssemblyVersion>2.0.0.0</AssemblyVersion>
3235
</PropertyGroup>
3336

3437

Test_Numerics/Data/Statistics/Test_GoodnessOfFit.cs

Lines changed: 23 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -29,9 +29,12 @@
2929
*/
3030

3131
using Microsoft.VisualStudio.TestTools.UnitTesting;
32+
using Microsoft.VisualStudio.TestTools.UnitTesting.Logging;
3233
using Numerics.Data.Statistics;
3334
using Numerics.Distributions;
3435
using System;
36+
using System.Reflection;
37+
using static Microsoft.ApplicationInsights.MetricDimensionNames.TelemetryContext;
3538

3639
namespace Data.Statistics
3740
{
@@ -260,12 +263,14 @@ public void Test_MAPE()
260263
/// Test that MAPE throws exception when observed values contain zero.
261264
/// </summary>
262265
[TestMethod]
263-
[ExpectedException(typeof(ArgumentException))]
264266
public void Test_MAPE_ZeroObserved()
265267
{
266268
var observed = new double[] { 2.5, 0.0, 2.1, 1.4 };
267269
var modeled = new double[] { 3.0, -0.5, 2.0, 1.5 };
268-
GoodnessOfFit.MAPE(observed, modeled);
270+
var ex = Assert.Throws<Exception>(() =>
271+
{
272+
GoodnessOfFit.MAPE(observed, modeled);
273+
});
269274
}
270275

271276
/// <summary>
@@ -303,12 +308,14 @@ public void Test_sMAPE_PerfectFit()
303308
/// Test that sMAPE throws exception when both observed and modeled values are zero.
304309
/// </summary>
305310
[TestMethod]
306-
[ExpectedException(typeof(ArgumentException))]
307311
public void Test_sMAPE_BothZero()
308312
{
309313
var observed = new double[] { 2.5, 0.0, 2.1, 1.4 };
310314
var modeled = new double[] { 3.0, 0.0, 2.0, 1.5 };
311-
GoodnessOfFit.sMAPE(observed, modeled);
315+
var ex = Assert.Throws<Exception>(() =>
316+
{
317+
GoodnessOfFit.sMAPE(observed, modeled);
318+
});
312319
}
313320

314321
/// <summary>
@@ -760,36 +767,42 @@ public void Test_ADTest()
760767
/// Test that methods throw appropriate exceptions when observed and modeled arrays have different lengths.
761768
/// </summary>
762769
[TestMethod]
763-
[ExpectedException(typeof(ArgumentOutOfRangeException))]
764770
public void Test_UnequalArrayLengths_RMSE()
765771
{
766772
var observed = new double[] { 1.0, 2.0, 3.0 };
767773
var modeled = new double[] { 1.0, 2.0 };
768-
GoodnessOfFit.RMSE(observed, modeled);
774+
var ex = Assert.Throws<Exception>(() =>
775+
{
776+
GoodnessOfFit.RMSE(observed, modeled);
777+
});
769778
}
770779

771780
/// <summary>
772781
/// Test that methods throw appropriate exceptions when observed and modeled arrays have different lengths.
773782
/// </summary>
774783
[TestMethod]
775-
[ExpectedException(typeof(ArgumentOutOfRangeException))]
776784
public void Test_UnequalArrayLengths_NSE()
777785
{
778786
var observed = new double[] { 1.0, 2.0, 3.0 };
779787
var modeled = new double[] { 1.0, 2.0 };
780-
GoodnessOfFit.NashSutcliffeEfficiency(observed, modeled);
788+
var ex = Assert.Throws<Exception>(() =>
789+
{
790+
GoodnessOfFit.NashSutcliffeEfficiency(observed, modeled);
791+
});
781792
}
782793

783794
/// <summary>
784795
/// Test that statistical tests throw appropriate exceptions with insufficient data.
785796
/// </summary>
786797
[TestMethod]
787-
[ExpectedException(typeof(ArgumentOutOfRangeException))]
788798
public void Test_InsufficientData_KS()
789799
{
790800
var observed = new double[] { };
791801
var norm = new Normal(0, 1);
792-
GoodnessOfFit.KolmogorovSmirnov(observed, norm);
802+
var ex = Assert.Throws<Exception>(() =>
803+
{
804+
GoodnessOfFit.KolmogorovSmirnov(observed, norm);
805+
});
793806
}
794807

795808
#endregion

0 commit comments

Comments
 (0)