Skip to content

Commit 7f3be76

Browse files
committed
Fix StudentTCopula DF validation and harden download tests against outages
StudentTCopula: DegreesOfFreedom setter no longer throws — it sets _parametersValid via the new ValidateParameters(rho, nu) and constructors follow suit. ValidateParameter now delegates to ValidateParameters so the base Theta setter validates both parameters. Unify the ν minimum on 2 + 1e-10 across ParameterConstraints and SetCopulaParameters; the previous 2.0 + Tools.DoubleMachineEpsilon expression silently rounded to 2.0 in IEEE 754 (ULP at 2.0 is 2^-51, larger than eps = 2^-53), making the optimizer fail at the boundary. Tests updated for the int→double widening and the new minimum. Test_TimeSeriesDownload: add per-service AvailableAsync probes (CHMN, USGS, GHCN, BOM) memoized via ConcurrentDictionary<string, Lazy<Task<bool>>>; replace `Online()` in all 31 integration tests so an upstream outage skips cleanly instead of failing CI. BOM URLs switched to https and 5xx response bodies are now surfaced in the exception so the next outage is self-documenting.
1 parent 19c1575 commit 7f3be76

4 files changed

Lines changed: 152 additions & 63 deletions

File tree

Numerics/Data/Time Series/Support/TimeSeriesDownload.cs

Lines changed: 21 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -942,7 +942,7 @@ public static async Task<TimeSeries> FromABOM(
942942
else parameterType = "Rainfall";
943943

944944
// Step 1: Get timeseries list to find the appropriate ts_id
945-
string tsListUrl = "http://www.bom.gov.au/waterdata/services" +
945+
string tsListUrl = "https://www.bom.gov.au/waterdata/services" +
946946
$"?service=kisters&type=QueryServices&datasource=0&format=json" +
947947
$"&request=getTimeseriesList" +
948948
$"&station_no={Uri.EscapeDataString(stationNumber)}" +
@@ -971,7 +971,6 @@ public static async Task<TimeSeries> FromABOM(
971971
try
972972
{
973973
response = await client.GetAsync(tsListUrl);
974-
response.EnsureSuccessStatusCode();
975974
}
976975
catch (HttpRequestException ex)
977976
{
@@ -984,6 +983,15 @@ public static async Task<TimeSeries> FromABOM(
984983

985984
var listResponse = await response.Content.ReadAsStringAsync();
986985

986+
// Surface the response body in the exception so transient upstream failures
987+
// (KiWIS 5xx with a JSON error payload) are self-documenting.
988+
if (!response.IsSuccessStatusCode)
989+
{
990+
string snippet = listResponse.Length > 500 ? listResponse.Substring(0, 500) + "…" : listResponse;
991+
throw new Exception(
992+
$"Failed to retrieve time series list from BOM API. Status={(int)response.StatusCode} ({response.StatusCode}). Body={snippet}");
993+
}
994+
987995
// Check for error response from KiWIS
988996
if (listResponse.Contains("\"type\":\"error\"") || listResponse.StartsWith("{\"type\":\"error\""))
989997
{
@@ -1063,7 +1071,7 @@ public static async Task<TimeSeries> FromABOM(
10631071
}
10641072

10651073
// Step 2: Get time series values using the ts_id
1066-
string valuesUrl = "http://www.bom.gov.au/waterdata/services" +
1074+
string valuesUrl = "https://www.bom.gov.au/waterdata/services" +
10671075
$"?service=kisters&type=QueryServices&datasource=0&format=json" +
10681076
$"&request=getTimeseriesValues" +
10691077
$"&ts_id={tsId}" +
@@ -1094,7 +1102,6 @@ public static async Task<TimeSeries> FromABOM(
10941102
try
10951103
{
10961104
response = await client.GetAsync(valuesUrl);
1097-
response.EnsureSuccessStatusCode();
10981105
}
10991106
catch (HttpRequestException ex)
11001107
{
@@ -1107,6 +1114,16 @@ public static async Task<TimeSeries> FromABOM(
11071114

11081115
var valuesResponse = await response.Content.ReadAsStringAsync();
11091116

1117+
// Surface the response body in the exception so transient upstream failures
1118+
// (e.g. KiWIS returning 500 with `{"code":"DatasourceError","message":"Error connecting to WDP."}`)
1119+
// are self-documenting. Without this, EnsureSuccessStatusCode would discard the body.
1120+
if (!response.IsSuccessStatusCode)
1121+
{
1122+
string snippet = valuesResponse.Length > 500 ? valuesResponse.Substring(0, 500) + "…" : valuesResponse;
1123+
throw new Exception(
1124+
$"Failed to retrieve time series values from BOM API. Status={(int)response.StatusCode} ({response.StatusCode}). Body={snippet}");
1125+
}
1126+
11101127
// Check for error response
11111128
if (valuesResponse.Contains("\"type\":\"error\"") || valuesResponse.StartsWith("{\"type\":\"error\""))
11121129
{

Numerics/Distributions/Bivariate Copulas/StudentTCopula.cs

Lines changed: 29 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -75,11 +75,8 @@ public StudentTCopula()
7575
/// </summary>
7676
/// <param name="rho">The correlation parameter ρ (rho). Must be in [-1, 1].</param>
7777
/// <param name="degreesOfFreedom">The degrees of freedom ν (nu). Must be greater than 2.</param>
78-
/// <exception cref="ArgumentOutOfRangeException">Thrown when ν ≤ 2.</exception>
7978
public StudentTCopula(double rho, double degreesOfFreedom)
8079
{
81-
if (degreesOfFreedom <= 2)
82-
throw new ArgumentOutOfRangeException(nameof(degreesOfFreedom), "The degrees of freedom must be greater than 2.");
8380
_nu = degreesOfFreedom;
8481
Theta = rho;
8582
}
@@ -91,11 +88,8 @@ public StudentTCopula(double rho, double degreesOfFreedom)
9188
/// <param name="degreesOfFreedom">The degrees of freedom ν (nu). Must be greater than 2.</param>
9289
/// <param name="marginalDistributionX">The X marginal distribution.</param>
9390
/// <param name="marginalDistributionY">The Y marginal distribution.</param>
94-
/// <exception cref="ArgumentOutOfRangeException">Thrown when ν ≤ 2.</exception>
9591
public StudentTCopula(double rho, double degreesOfFreedom, IUnivariateDistribution? marginalDistributionX, IUnivariateDistribution? marginalDistributionY)
9692
{
97-
if (degreesOfFreedom <= 2)
98-
throw new ArgumentOutOfRangeException(nameof(degreesOfFreedom), "The degrees of freedom must be greater than 2.");
9993
_nu = degreesOfFreedom;
10094
Theta = rho;
10195
MarginalDistributionX = marginalDistributionX;
@@ -115,14 +109,12 @@ public StudentTCopula(double rho, double degreesOfFreedom, IUnivariateDistributi
115109
/// <see cref="StudentT"/> and <see cref="MultivariateStudentT"/> accept non-integer
116110
/// degrees of freedom.
117111
/// </remarks>
118-
/// <exception cref="ArgumentOutOfRangeException">Thrown when the value is ≤ 2.</exception>
119112
public double DegreesOfFreedom
120113
{
121114
get { return _nu; }
122115
set
123116
{
124-
if (value <= 2)
125-
throw new ArgumentOutOfRangeException(nameof(DegreesOfFreedom), "The degrees of freedom must be greater than 2.");
117+
_parametersValid = ValidateParameters(Theta, value, false) is null;
126118
_nu = value;
127119
}
128120
}
@@ -178,18 +170,40 @@ public override double ThetaMaximum
178170
}
179171

180172
/// <inheritdoc/>
173+
/// <remarks>
174+
/// The base <see cref="BivariateCopula.Theta"/> setter calls this method on the new
175+
/// rho value; we delegate to <see cref="ValidateParameters(double, double, bool)"/>
176+
/// so that <c>_parametersValid</c> reflects the validity of both ρ and the current ν.
177+
/// </remarks>
181178
public override ArgumentOutOfRangeException? ValidateParameter(double parameter, bool throwException)
182179
{
183-
if (parameter < ThetaMinimum)
180+
return ValidateParameters(parameter, _nu, throwException);
181+
}
182+
183+
/// <summary>
184+
/// Validates the correlation ρ and degrees of freedom ν together. Returns null when
185+
/// both parameters are in their valid domains.
186+
/// </summary>
187+
/// <param name="rho">The correlation parameter ρ. Must be in [<see cref="ThetaMinimum"/>, <see cref="ThetaMaximum"/>].</param>
188+
/// <param name="degreesOfFreedom">The degrees of freedom ν. Must be finite and greater than 2.</param>
189+
/// <param name="throwException">When true, throws on the first invalid parameter; when false, returns the exception instead.</param>
190+
public ArgumentOutOfRangeException? ValidateParameters(double rho, double degreesOfFreedom, bool throwException)
191+
{
192+
if (rho < ThetaMinimum)
184193
{
185194
if (throwException) throw new ArgumentOutOfRangeException(nameof(Theta), "The correlation parameter ρ (rho) must be greater than " + ThetaMinimum.ToString() + ".");
186195
return new ArgumentOutOfRangeException(nameof(Theta), "The correlation parameter ρ (rho) must be greater than " + ThetaMinimum.ToString() + ".");
187196
}
188-
if (parameter > ThetaMaximum)
197+
if (rho > ThetaMaximum)
189198
{
190199
if (throwException) throw new ArgumentOutOfRangeException(nameof(Theta), "The correlation parameter ρ (rho) must be less than " + ThetaMaximum.ToString() + ".");
191200
return new ArgumentOutOfRangeException(nameof(Theta), "The correlation parameter ρ (rho) must be less than " + ThetaMaximum.ToString() + ".");
192201
}
202+
if (double.IsNaN(degreesOfFreedom) || double.IsInfinity(degreesOfFreedom) || degreesOfFreedom <= 2.0)
203+
{
204+
if (throwException) throw new ArgumentOutOfRangeException(nameof(DegreesOfFreedom), "The degrees of freedom must be greater than 2.");
205+
return new ArgumentOutOfRangeException(nameof(DegreesOfFreedom), "The degrees of freedom must be greater than 2.");
206+
}
193207
return null;
194208
}
195209

@@ -204,12 +218,13 @@ public override double ThetaMaximum
204218
/// ν is clamped to the valid domain (ν &gt; 2) but otherwise kept continuous — no
205219
/// integer rounding. MCMC samplers that propose continuous ν values see a smooth
206220
/// likelihood, avoiding the step-function plateaus produced by previous rounding
207-
/// behavior.
221+
/// behavior. The clamp uses 2 + 1e-10 because <c>2.0 + Tools.DoubleMachineEpsilon</c>
222+
/// rounds back to 2.0 in IEEE 754 (the ULP at 2.0 is 2^-51, larger than ε = 2^-53).
208223
/// </remarks>
209224
public override void SetCopulaParameters(double[] parameters)
210225
{
211226
Theta = parameters[0];
212-
DegreesOfFreedom = Math.Max(2.0 + Tools.DoubleMachineEpsilon, parameters[1]);
227+
DegreesOfFreedom = Math.Max(2.0 + 1E-10, parameters[1]);
213228
}
214229

215230
/// <inheritdoc/>
@@ -226,7 +241,7 @@ public override void SetCopulaParameters(double[] parameters)
226241
return new double[,]
227242
{
228243
{ -1 + Tools.DoubleMachineEpsilon, 1 - Tools.DoubleMachineEpsilon },
229-
{ 3, 30 }
244+
{ 2.0 + 1E-10, 30 }
230245
};
231246
}
232247

0 commit comments

Comments
 (0)