Skip to content

Commit 2a0357a

Browse files
authored
Merge pull request #144 from USACE-RMC/bug-fixes-and-enhancements
Prepare v2.1.4 release
2 parents b5ae6ff + 8286646 commit 2a0357a

17 files changed

Lines changed: 587 additions & 52 deletions

File tree

‎.github/workflows/Snapshot.yml‎

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -10,8 +10,8 @@ jobs:
1010
with:
1111
dotnet-version: '10.0.x'
1212
project-names: 'Numerics'
13-
# Snapshot workflow appends .<run_number>-dev, so this tracks the next development patch after v2.1.3.
14-
version: '2.1.4'
13+
# Snapshot workflow appends .<run_number>-dev, so this tracks the next development patch after v2.1.4.
14+
version: '2.1.5'
1515
# PR integration already runs the full multi-target test suite.
1616
run-tests: false
1717
nuget-source: 'https://www.hec.usace.army.mil/nexus/repository/consequences-nuget-public/'

‎CITATION.cff‎

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -2,8 +2,8 @@ cff-version: 1.2.0
22
message: "If you use this software, please cite our article in the Journal of Open Source Software."
33
type: software
44
title: "Numerics: A .NET Library for Numerical Computing, Statistical Analysis, and Risk Assessment"
5-
version: "2.1.3"
6-
date-released: "2026-07-15"
5+
version: "2.1.4"
6+
date-released: "2026-07-17"
77
license: 0BSD
88
repository-code: "https://github.com/USACE-RMC/Numerics"
99
url: "https://github.com/USACE-RMC/Numerics"

‎Numerics/Data/Statistics/BoxCox.cs‎

Lines changed: 71 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
using System;
1+
using System;
22
using System.Collections.Generic;
33
using Numerics.Mathematics.Optimization;
44

@@ -28,17 +28,35 @@ public class BoxCox
2828
/// </remarks>
2929
public static void FitLambda(IList<double> values, out double lambda)
3030
{
31-
// Solve with Brent
32-
var brent = new BrentSearch((x) => { return LogLikelihood(values, x); }, -5d, 5d);
33-
brent.Maximize();
34-
if (brent.Status == OptimizationStatus.Success)
31+
lambda = double.NaN;
32+
if (!CanFitLambda(values))
33+
return;
34+
35+
// Keep BrentSearch arithmetic finite even when the profile likelihood is undefined.
36+
var brent = new BrentSearch((x) =>
3537
{
36-
lambda = brent.BestParameterSet.Values[0];
37-
}
38-
else
38+
double logLikelihood = LogLikelihood(values, x);
39+
return Tools.IsFinite(logLikelihood) ? logLikelihood : -double.MaxValue;
40+
}, -5d, 5d)
3941
{
40-
lambda = double.NaN;
41-
}
42+
ReportFailure = false,
43+
ComputeHessian = false,
44+
RecordTraces = false
45+
};
46+
47+
brent.Maximize();
48+
if (brent.Status != OptimizationStatus.Success ||
49+
brent.BestParameterSet.Values == null ||
50+
brent.BestParameterSet.Values.Length == 0)
51+
return;
52+
53+
double candidate = brent.BestParameterSet.Values[0];
54+
if (!Tools.IsFinite(candidate) ||
55+
Math.Abs(candidate) > 5d ||
56+
!Tools.IsFinite(LogLikelihood(values, candidate)))
57+
return;
58+
59+
lambda = candidate;
4260
}
4361

4462
/// <summary>
@@ -52,23 +70,38 @@ public static void FitLambda(IList<double> values, out double lambda)
5270
/// </returns>
5371
public static double LogLikelihood(IList<double> values, double lambda1)
5472
{
73+
if (!CanFitLambda(values) || !Tools.IsFinite(lambda1) || Math.Abs(lambda1) > 5d)
74+
return double.NegativeInfinity;
75+
5576
int n = values.Count;
5677
var y = new double[n];
5778
double mu = 0d;
5879
var sumX = 0d;
5980
for (int i = 0; i < n; i++)
6081
{
6182
y[i] = Transform(values[i], lambda1);
83+
if (!Tools.IsFinite(y[i]))
84+
return double.NegativeInfinity;
85+
6286
mu += y[i];
6387
sumX += Math.Log(values[i]);
6488
}
89+
if (!Tools.IsFinite(mu) || !Tools.IsFinite(sumX))
90+
return double.NegativeInfinity;
91+
6592
mu = mu / n;
6693
double sse = 0d;
6794
for (int i = 0; i < n; i++)
6895
sse += Math.Pow(y[i] - mu, 2d);
96+
if (!Tools.IsFinite(sse) || sse <= 0d)
97+
return double.NegativeInfinity;
98+
6999
double sigma = Math.Sqrt(sse / n);
100+
if (!Tools.IsFinite(sigma) || sigma <= 0d)
101+
return double.NegativeInfinity;
102+
70103
double ll = -n / 2.0d * Tools.LogSqrt2PI - n / 2.0d * Math.Log(sigma * sigma) - 1.0d / (2d * sigma * sigma) * sse + (lambda1 - 1d) * sumX;
71-
return ll;
104+
return Tools.IsFinite(ll) ? ll : double.NegativeInfinity;
72105
}
73106

74107
/// <summary>
@@ -146,5 +179,32 @@ public static List<double> InverseTransform(IList<double> values, double lambda)
146179
newValues.Add(InverseTransform(values[i], lambda));
147180
return newValues;
148181
}
182+
183+
/// <summary>
184+
/// Determines whether a sample can support Box-Cox lambda fitting.
185+
/// </summary>
186+
/// <param name="values">The sample values to inspect.</param>
187+
/// <returns><see langword="true"/> when the sample is finite, positive, and non-degenerate.</returns>
188+
private static bool CanFitLambda(IList<double> values)
189+
{
190+
if (values == null || values.Count < 2)
191+
return false;
192+
193+
double first = values[0];
194+
if (!Tools.IsFinite(first) || first <= 0d)
195+
return false;
196+
197+
bool hasDifferentValue = false;
198+
for (int i = 1; i < values.Count; i++)
199+
{
200+
double value = values[i];
201+
if (!Tools.IsFinite(value) || value <= 0d)
202+
return false;
203+
if (value != first)
204+
hasDifferentValue = true;
205+
}
206+
207+
return hasDifferentValue;
208+
}
149209
}
150210
}

‎Numerics/Data/Statistics/YeoJohnson.cs‎

Lines changed: 73 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
using Numerics.Mathematics.Optimization;
1+
using Numerics.Mathematics.Optimization;
22
using System;
33
using System.Collections.Generic;
44

@@ -24,17 +24,35 @@ public class YeoJohnson
2424
/// <param name="lambda">Output. The transformation exponent. Range -5 to +5.</param>
2525
public static void FitLambda(IList<double> values, out double lambda)
2626
{
27-
// Solve with Brent
28-
var brent = new BrentSearch((x) => { return LogLikelihood(values, x); }, -5d, 5d);
29-
brent.Maximize();
30-
if (brent.Status == OptimizationStatus.Success)
27+
lambda = double.NaN;
28+
if (!CanFitLambda(values))
29+
return;
30+
31+
// Keep BrentSearch arithmetic finite even when the profile likelihood is undefined.
32+
var brent = new BrentSearch((x) =>
3133
{
32-
lambda = brent.BestParameterSet.Values[0];
33-
}
34-
else
34+
double logLikelihood = LogLikelihood(values, x);
35+
return Tools.IsFinite(logLikelihood) ? logLikelihood : -double.MaxValue;
36+
}, -5d, 5d)
3537
{
36-
lambda = double.NaN;
37-
}
38+
ReportFailure = false,
39+
ComputeHessian = false,
40+
RecordTraces = false
41+
};
42+
43+
brent.Maximize();
44+
if (brent.Status != OptimizationStatus.Success ||
45+
brent.BestParameterSet.Values == null ||
46+
brent.BestParameterSet.Values.Length == 0)
47+
return;
48+
49+
double candidate = brent.BestParameterSet.Values[0];
50+
if (!Tools.IsFinite(candidate) ||
51+
Math.Abs(candidate) > 5d ||
52+
!Tools.IsFinite(LogLikelihood(values, candidate)))
53+
return;
54+
55+
lambda = candidate;
3856
}
3957

4058
/// <summary>
@@ -62,6 +80,9 @@ public static double FitLambda(IList<double> values)
6280
/// </returns>
6381
public static double LogLikelihood(IList<double> values, double lambda)
6482
{
83+
if (!CanFitLambda(values) || !Tools.IsFinite(lambda) || Math.Abs(lambda) > 5d)
84+
return double.NegativeInfinity;
85+
6586
int n = values.Count;
6687
var transformed = new double[n];
6788
double sum = 0d;
@@ -72,8 +93,13 @@ public static double LogLikelihood(IList<double> values, double lambda)
7293
{
7394
double xi = values[i];
7495
double yi = Transform(xi, lambda);
96+
if (!Tools.IsFinite(yi))
97+
return double.NegativeInfinity;
98+
7599
transformed[i] = yi;
76100
sum += yi;
101+
if (!Tools.IsFinite(sum))
102+
return double.NegativeInfinity;
77103

78104
// Compute derivative dT/dy for log-Jacobian
79105
double dTdy;
@@ -91,6 +117,8 @@ public static double LogLikelihood(IList<double> values, double lambda)
91117
logJacobianSum += Math.Log(dTdy);
92118
else
93119
return double.NegativeInfinity; // log-likelihood undefined
120+
if (!Tools.IsFinite(logJacobianSum))
121+
return double.NegativeInfinity;
94122
}
95123

96124
// Compute mean and SSE
@@ -101,14 +129,16 @@ public static double LogLikelihood(IList<double> values, double lambda)
101129
double resid = transformed[i] - mu;
102130
sse += resid * resid;
103131
}
132+
if (!Tools.IsFinite(sse) || sse <= 0d)
133+
return double.NegativeInfinity;
104134

105135
double sigmaSq = sse / n;
106-
if (sigmaSq <= 0 || double.IsNaN(sigmaSq))
136+
if (!Tools.IsFinite(sigmaSq) || sigmaSq <= 0)
107137
return double.NegativeInfinity;
108138

109139
double logLikelihood = -n / 2.0 * Tools.LogSqrt2PI - n / 2.0 * Math.Log(sigmaSq) - sse / (2.0 * sigmaSq) + logJacobianSum;
110140

111-
return logLikelihood;
141+
return Tools.IsFinite(logLikelihood) ? logLikelihood : double.NegativeInfinity;
112142
}
113143

114144
/// <summary>
@@ -160,11 +190,11 @@ public static double Transform(double value, double lambda)
160190
{
161191
return Math.Log(value + 1);
162192
}
163-
else if (value < 0 && lambda != 2)
193+
else if (value < 0 && Math.Abs(lambda - 2d) >= 1E-8)
164194
{
165195
return -(Math.Pow(-value + 1, 2 - lambda) - 1) / (2 - lambda);
166196
}
167-
else if (value < 0 && lambda == 2)
197+
else if (value < 0 && Math.Abs(lambda - 2d) < 1E-8)
168198
{
169199
return -Math.Log(-value + 1);
170200
}
@@ -216,11 +246,11 @@ public static double InverseTransform(double value, double lambda)
216246
{
217247
return Math.Exp(value) - 1;
218248
}
219-
else if (value < 0 && lambda != 2)
249+
else if (value < 0 && Math.Abs(lambda - 2d) >= 1E-8)
220250
{
221251
return 1 - (Math.Pow(1 - (2 - lambda) * value, 1 / (2 - lambda)));
222252
}
223-
else if (value < 0 && lambda == 2)
253+
else if (value < 0 && Math.Abs(lambda - 2d) < 1E-8)
224254
{
225255
return 1 - Math.Exp(-value);
226256
}
@@ -242,5 +272,32 @@ public static List<double> InverseTransform(IList<double> values, double lambda)
242272
newValues.Add(InverseTransform(values[i], lambda));
243273
return newValues;
244274
}
275+
276+
/// <summary>
277+
/// Determines whether a sample can support Yeo-Johnson lambda fitting.
278+
/// </summary>
279+
/// <param name="values">The sample values to inspect.</param>
280+
/// <returns><see langword="true"/> when the sample is finite and non-degenerate.</returns>
281+
private static bool CanFitLambda(IList<double> values)
282+
{
283+
if (values == null || values.Count < 2)
284+
return false;
285+
286+
double first = values[0];
287+
if (!Tools.IsFinite(first))
288+
return false;
289+
290+
bool hasDifferentValue = false;
291+
for (int i = 1; i < values.Count; i++)
292+
{
293+
double value = values[i];
294+
if (!Tools.IsFinite(value))
295+
return false;
296+
if (value != first)
297+
hasDifferentValue = true;
298+
}
299+
300+
return hasDifferentValue;
301+
}
245302
}
246303
}

‎Numerics/Distributions/Univariate/Base/UnivariateDistributionFactory.cs‎

Lines changed: 32 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -124,6 +124,38 @@ public static UnivariateDistributionBase CreateDistribution(UnivariateDistributi
124124
}
125125
}
126126

127+
/// <summary>
128+
/// Attempts to create a distribution that can be initialized without external component distributions.
129+
/// </summary>
130+
/// <param name="distributionType">Distribution type.</param>
131+
/// <param name="distribution">
132+
/// When this method returns <see langword="true"/>, contains a distribution whose
133+
/// <see cref="UnivariateDistributionBase.Type"/> matches <paramref name="distributionType"/>;
134+
/// otherwise, <see langword="null"/>.
135+
/// </param>
136+
/// <returns>
137+
/// <see langword="true"/> when the requested distribution can be created without external components;
138+
/// otherwise, <see langword="false"/>.
139+
/// </returns>
140+
/// <remarks>
141+
/// Composite distributions such as <see cref="CompetingRisks"/> and <see cref="Mixture"/>,
142+
/// user-defined distributions, and undefined enumeration values cannot be created by this method.
143+
/// </remarks>
144+
public static bool TryCreateDistribution(UnivariateDistributionType distributionType, out UnivariateDistributionBase? distribution)
145+
{
146+
if (!Enum.IsDefined(typeof(UnivariateDistributionType), distributionType) ||
147+
distributionType == UnivariateDistributionType.CompetingRisks ||
148+
distributionType == UnivariateDistributionType.Mixture ||
149+
distributionType == UnivariateDistributionType.UserDefined)
150+
{
151+
distribution = null;
152+
return false;
153+
}
154+
155+
distribution = CreateDistribution(distributionType);
156+
return true;
157+
}
158+
127159
/// <summary>
128160
/// Create a distribution from XElement.
129161
/// </summary>

0 commit comments

Comments
 (0)