diff --git a/Source/RandomTools.Core/Options/Delay/DelayOptions.cs b/Source/RandomTools.Core/Options/Delay/DelayOptions.cs
index 6ac2251..bba45ba 100644
--- a/Source/RandomTools.Core/Options/Delay/DelayOptions.cs
+++ b/Source/RandomTools.Core/Options/Delay/DelayOptions.cs
@@ -97,7 +97,6 @@ public Normal WithAutoFit(double minimum, double maximum)
public override void Validate()
{
base.Validate();
-
EnsureFinite(Mean);
EnsureFinite(StandardDeviation);
@@ -117,12 +116,10 @@ public override void Validate()
}
}
- public override bool Equals(Normal? other)
- {
- return base.Equals(other) &&
- other.Mean == Mean &&
- other.StandardDeviation == StandardDeviation;
- }
+ public override bool Equals(Normal? other) =>
+ base.Equals(other) &&
+ DoubleComparer.Equals(other.Mean, Mean) &&
+ DoubleComparer.Equals(other.StandardDeviation, StandardDeviation);
public override int GetHashCode() =>
HashCode.Combine(Minimum, Maximum, TimeUnit, Mean, StandardDeviation);
@@ -156,7 +153,6 @@ public Triangular WithMode(double value)
public override void Validate()
{
base.Validate();
-
EnsureFinite(Mode);
// Check that the mode lies within the defined [Minimum, Maximum] range
@@ -173,11 +169,9 @@ public override void Validate()
///
/// Another instance.
/// True if equal, false otherwise.
- public override bool Equals(Triangular? other)
- {
- return base.Equals(other) &&
- other.Mode == Mode;
- }
+ public override bool Equals(Triangular? other) =>
+ base.Equals(other) &&
+ DoubleComparer.Equals(other.Mode, Mode);
///
/// Returns a hash code for the current instance.
@@ -310,7 +304,6 @@ public Polynomial WithReverse(bool value)
public override void Validate()
{
base.Validate();
-
EnsureFinite(Power);
if (Power < 0.0)
@@ -321,12 +314,10 @@ public override void Validate()
}
///
- public override bool Equals(Polynomial? other)
- {
- return base.Equals(other) &&
- other.Power == Power &&
- other.Reverse == Reverse;
- }
+ public override bool Equals(Polynomial? other) =>
+ base.Equals(other) &&
+ DoubleComparer.Equals(other.Power, Power) &&
+ other.Reverse == Reverse;
///
public override int GetHashCode() =>
@@ -394,7 +385,6 @@ public override void Validate()
{
// Validate base numeric fields (Minimum/Maximum)
base.Validate();
-
EnsureFinite(AlphaValue);
EnsureFinite(BetaValue);
@@ -416,12 +406,10 @@ public override void Validate()
///
/// Other instance to compare.
/// if all relevant fields are equal; otherwise .
- public override bool Equals(Beta? other)
- {
- return base.Equals(other) &&
- other.AlphaValue == AlphaValue &&
- other.BetaValue == BetaValue;
- }
+ public override bool Equals(Beta? other) =>
+ base.Equals(other) &&
+ DoubleComparer.Equals(other.AlphaValue, AlphaValue) &&
+ DoubleComparer.Equals(other.BetaValue, BetaValue);
///
/// Computes a hash code based on range, time unit, and Beta distribution parameters.
@@ -489,11 +477,9 @@ public override void Validate()
}
}
- public override bool Equals(Sequence? other)
- {
- return base.Equals(other) &&
- other.Values.SequenceEqual(Values);
- }
+ public override bool Equals(Sequence? other) =>
+ base.Equals(other) &&
+ other.Values.SequenceEqual(Values);
public override int GetHashCode() =>
HashCode.Combine(Minimum, Maximum, TimeUnit, Values);
diff --git a/Source/RandomTools.Core/Options/Delay/DelayOptionsBase.cs b/Source/RandomTools.Core/Options/Delay/DelayOptionsBase.cs
index 78138ef..98d43ff 100644
--- a/Source/RandomTools.Core/Options/Delay/DelayOptionsBase.cs
+++ b/Source/RandomTools.Core/Options/Delay/DelayOptionsBase.cs
@@ -12,6 +12,17 @@ namespace RandomTools.Core.Options.Delay
public abstract class DelayOptionsBase : IOptionsBase, IEquatable
where TDelayOptions : DelayOptionsBase
{
+ ///
+ /// Provides a default equality comparer for values.
+ ///
+ /// Used for comparing all numeric fields in options classes, such as ,
+ /// , and other derived fields like Mode.
+ /// Note that this comparer can compare non-finite values (NaN, Infinity),
+ /// so validation via is still required to enforce finiteness.
+ ///
+ ///
+ protected static EqualityComparer DoubleComparer => EqualityComparer.Default;
+
///
/// Minimum value of the delay range.
/// Can be negative, zero, or positive.
@@ -127,11 +138,9 @@ public virtual bool Equals(TDelayOptions? other)
if (other is null)
return false;
- var comparer = EqualityComparer.Default;
-
return
- comparer.Equals(other.Minimum, Minimum) &&
- comparer.Equals(other.Maximum, Maximum) &&
+ DoubleComparer.Equals(other.Minimum, Minimum) &&
+ DoubleComparer.Equals(other.Maximum, Maximum) &&
other.TimeUnit == TimeUnit;
}
diff --git a/Source/RandomTools.Core/RandomTool.cs b/Source/RandomTools.Core/RandomTool.cs
index b379ec0..cb6b100 100644
--- a/Source/RandomTools.Core/RandomTool.cs
+++ b/Source/RandomTools.Core/RandomTool.cs
@@ -368,7 +368,7 @@ public static NormalDelay InMilliseconds(double mean, double stdDev, double min,
/// Same as
/// but uses seconds as the time unit.
///
- public static NormalDelay Seconds(double mean, double stdDev, double min, double max)
+ public static NormalDelay InSeconds(double mean, double stdDev, double min, double max)
{
var options = new DelayOptions.Normal()
.WithTimeUnit(TimeUnit.Second)
@@ -410,7 +410,7 @@ public static NormalDelay InMilliseconds(double mean, double stdDev, (double Min
/// Tuple-based overload for seconds.
///
public static NormalDelay InSeconds(double mean, double stdDev, (double Min, double Max) range) =>
- Seconds(mean, stdDev, range.Min, range.Max);
+ InSeconds(mean, stdDev, range.Min, range.Max);
///
/// Tuple-based overload for minutes.
@@ -420,33 +420,31 @@ public static NormalDelay InMinutes(double mean, double stdDev, (double Min, dou
}
///
- /// Factory class for generating and caching instances.
+ /// Provides methods to obtain instances with caching to reuse identical configurations.
///
- /// The Bates distribution represents the arithmetic mean of N independent uniform samples
- /// within a configured minimum and maximum range. This factory provides convenient methods
- /// to obtain a for different time units while caching instances
- /// for reuse.
+ /// A Bates delay represents the arithmetic mean of samples independent uniform values
+ /// within the specified minimum and maximum range.
///
///
- /// All configuration validation is performed by .
+ /// All configuration is validated via .
///
///
public static class Bates
{
///
- /// Thread-safe cache for storing instances keyed by their options.
- /// Ensures that multiple requests with identical configuration return the same instance.
+ /// Thread-safe cache mapping to instances.
+ /// Ensures that repeated requests with the same options return the same instance.
///
private static readonly ConcurrentDictionary sCache = new();
///
- /// Returns a cached configured with the specified minimum, maximum,
- /// number of samples, and time unit.
+ /// Returns a cached configured with the specified range, number of samples, and time unit.
///
- /// The minimum delay value.
- /// The maximum delay value.
- /// Number of uniform samples to average for the Bates distribution.
- /// Time unit in which the delay will be expressed.
+ /// Minimum delay value.
+ /// Maximum delay value.
+ /// Number of uniform samples to average.
+ /// Time unit for the delay.
+ /// A instance with the requested configuration.
public static BatesDelay For(double minimum, double maximum, int samples, TimeUnit unit)
{
var options = new DelayOptions.Bates()
@@ -455,24 +453,23 @@ public static BatesDelay For(double minimum, double maximum, int samples, TimeUn
.WithMaximum(maximum)
.WithSamples(samples);
- return sCache.GetOrAdd(options,
- _ => new BatesDelay(options));
+ return sCache.GetOrAdd(options, _ => new BatesDelay(options));
}
///
- /// Returns a cached configured for millisecond delays.
+ /// Returns a cached configured for milliseconds.
///
public static BatesDelay InMilliseconds(double minimum, double maximum, int samples) =>
For(minimum, maximum, samples, TimeUnit.Millisecond);
///
- /// Returns a cached configured for second delays.
+ /// Returns a cached configured for seconds.
///
public static BatesDelay InSeconds(double minimum, double maximum, int samples) =>
For(minimum, maximum, samples, TimeUnit.Second);
///
- /// Returns a cached configured for minute delays.
+ /// Returns a cached configured for minutes.
///
public static BatesDelay InMinutes(double minimum, double maximum, int samples) =>
For(minimum, maximum, samples, TimeUnit.Minute);
diff --git a/Source/RandomTools.Tests/BatesDelayTests.cs b/Source/RandomTools.Tests/BatesDelayTests.cs
index 992641b..980522a 100644
--- a/Source/RandomTools.Tests/BatesDelayTests.cs
+++ b/Source/RandomTools.Tests/BatesDelayTests.cs
@@ -17,12 +17,12 @@ public void SetUp()
}
[Test, Combinatorial]
- public void When_Sampling_MilliSeconds(
- [Values( 1_000.0, 5_000.0, 10_000.0)] double min,
+ public void BatesDelays_Milliseconds_ShouldMatchRangeMeanAndOrder(
+ [Values(1_000.0, 5_000.0, 10_000.0)] double min,
[Values(15_000.0, 20_000.0, 25_000.0)] double max,
[Values(1, 2, 3, 4, 5, 10, 25, 50, 100)] int samples)
{
- double expectedMean = (min + max) / 2.0;
+ double expMean = (min + max) / 2.0;
var delay = RandomTool.Delay.Bates.InMilliseconds(min, max, samples);
for (int i = 0; i < Iterations; i++)
@@ -35,20 +35,23 @@ public void When_Sampling_MilliSeconds(
_delays.Add(next);
}
- var stats = Statistics.Analyze(_delays.Select(x => x.TotalMilliseconds));
- double SEM = Statistics.StandardErrorOfMean(stats.StandardDeviation, stats.Count);
+ var (Mean, Variance, StandardDeviation, Count) = Statistics.Analyze(_delays.Select(x => x.TotalMilliseconds));
+ double SEM = Statistics.StandardErrorOfMean(StandardDeviation, Count);
double delta = Statistics.GetConfidenceDelta(ConfidenceLevel.Confidence999, SEM);
- stats.Mean.Should().BeApproximately(expectedMean, delta);
+ Mean.Should().BeApproximately(expMean, delta);
+
+ double nHat = Statistics.EstimateBatesOrder(Variance, (min, max));
+ nHat.Should().BeApproximately(samples, samples * 0.1);
}
[Test, Combinatorial]
- public void When_Sampling_Minutes(
+ public void BatesDelays_Minutes_ShouldMatchRangeMeanAndOrder(
[Values(0.5, 1.5, 2.0)] double min,
[Values(3.0, 3.5, 4.0)] double max,
[Values(1, 2, 3, 4, 5, 10, 25, 50, 100)] int samples)
{
- double expectedMean = (min + max) / 2.0;
+ double expMean = (min + max) / 2.0;
var delay = RandomTool.Delay.Bates.InMinutes(min, max, samples);
for (int i = 0; i < Iterations; i++)
@@ -61,20 +64,23 @@ public void When_Sampling_Minutes(
_delays.Add(next);
}
- var stats = Statistics.Analyze(_delays.Select(x => x.TotalMinutes));
- double SEM = Statistics.StandardErrorOfMean(stats.StandardDeviation, stats.Count);
+ var (Mean, Variance, StandardDeviation, Count) = Statistics.Analyze(_delays.Select(x => x.TotalMinutes));
+ double SEM = Statistics.StandardErrorOfMean(StandardDeviation, Count);
double delta = Statistics.GetConfidenceDelta(ConfidenceLevel.Confidence999, SEM);
- stats.Mean.Should().BeApproximately(expectedMean, delta);
+ Mean.Should().BeApproximately(expMean, delta);
+
+ double nHat = Statistics.EstimateBatesOrder(Variance, (min, max));
+ nHat.Should().BeApproximately(samples, samples * 0.1);
}
[Test, Combinatorial]
- public void When_Sampling_Seconds(
+ public void BatesDelays_Seconds_ShouldMatchRangeMeanAndOrder(
[Values(05.0, 10.0, 15.0)] double min,
[Values(20.0, 25.0, 30.0)] double max,
[Values(1, 2, 3, 4, 5, 10, 25, 50, 100)] int samples)
{
- double expectedMean = (min + max) / 2.0;
+ double expMean = (min + max) / 2.0;
var delay = RandomTool.Delay.Bates.InSeconds(min, max, samples);
for (int i = 0; i < Iterations; i++)
@@ -87,11 +93,14 @@ public void When_Sampling_Seconds(
_delays.Add(next);
}
- var stats = Statistics.Analyze(_delays.Select(x => x.TotalSeconds));
- double SEM = Statistics.StandardErrorOfMean(stats.StandardDeviation, stats.Count);
+ var (Mean, Variance, StandardDeviation, Count) = Statistics.Analyze(_delays.Select(x => x.TotalSeconds));
+ double SEM = Statistics.StandardErrorOfMean(StandardDeviation, Count);
double delta = Statistics.GetConfidenceDelta(ConfidenceLevel.Confidence999, SEM);
- stats.Mean.Should().BeApproximately(expectedMean, delta);
+ Mean.Should().BeApproximately(expMean, delta);
+
+ double nHat = Statistics.EstimateBatesOrder(Variance, (min, max));
+ nHat.Should().BeApproximately(samples, samples * 0.1);
}
}
-}
+}
\ No newline at end of file
diff --git a/Source/RandomTools.Tests/Keywords.cs b/Source/RandomTools.Tests/Keywords.cs
index c700b98..9343c4f 100644
--- a/Source/RandomTools.Tests/Keywords.cs
+++ b/Source/RandomTools.Tests/Keywords.cs
@@ -8,5 +8,6 @@ internal static class Keywords
public static string Minimum => nameof(Minimum);
public static string Maximum => nameof(Maximum);
public static string Samples => nameof(Samples);
+ public static string Mode => nameof(Mode);
}
}
diff --git a/Source/RandomTools.Tests/Options/BatesOptionsFixture.cs b/Source/RandomTools.Tests/Options/BatesOptionsFixture.cs
index feeebf4..0db6449 100644
--- a/Source/RandomTools.Tests/Options/BatesOptionsFixture.cs
+++ b/Source/RandomTools.Tests/Options/BatesOptionsFixture.cs
@@ -206,16 +206,16 @@ public void When_Used_As_Dictionary_Key_Should_Return_Correct_Value()
bool exists = dict.TryGetValue(keyToLookup, out object? actualValue);
exists.Should().BeTrue();
- actualValue.Should().BeSameAs(expectedValue);
+ actualValue.Should().Be(expectedValue);
}
[Test]
[TestCaseSource(typeof(ValuesProvider), nameof(ValuesProvider.TimeUnits))]
public void When_Valid_Options_Provided_Should_Not_Throw_On_Validate(TimeUnit unit)
{
- const double min = 300.0;
- const double max = 600.0;
- const int samples = 5;
+ double min = CoreTools.NextDouble(250, 500);
+ double max = min + CoreTools.NextDouble(200, 400);
+ int samples = CoreTools.NextInt(5, 10);
var options = new DelayOptions.Bates()
.WithTimeUnit(unit)
diff --git a/Source/RandomTools.Tests/Options/TriangularOptionsFixture.cs b/Source/RandomTools.Tests/Options/TriangularOptionsFixture.cs
index 98af4d7..f0b49dc 100644
--- a/Source/RandomTools.Tests/Options/TriangularOptionsFixture.cs
+++ b/Source/RandomTools.Tests/Options/TriangularOptionsFixture.cs
@@ -1,4 +1,5 @@
using FluentAssertions;
+using RandomTools.Core;
using RandomTools.Core.Exceptions;
using RandomTools.Core.Options.Delay;
@@ -7,6 +8,20 @@ namespace RandomTools.Tests.Options
[TestFixture]
public class TriangularOptionsFixture
{
+ [Test]
+ public void When_Unconfigured_Should_Throw_On_Validate()
+ {
+ var options = new DelayOptions.Triangular();
+
+ var ex = options.Invoking(opt => opt.Validate())
+ .Should()
+ .Throw()
+ .Which;
+
+ ex.Options.Should().Be(options).And.NotBeSameAs(options);
+ ex.Message.Should().Contain(ExceptionMessages.RangeIsTooShort);
+ }
+
[Test]
[TestCaseSource(typeof(ValuesProvider), nameof(ValuesProvider.NonFinite))]
public void When_Minimum_Is_Not_Finite_Should_Throw_On_Validate(double value)
@@ -19,8 +34,8 @@ public void When_Minimum_Is_Not_Finite_Should_Throw_On_Validate(double value)
.Throw()
.Which;
- ex.Options.Should().BeSameAs(options);
- ex.Message.Should().Contain($"({value})");
+ ex.Options.Should().Be(options).And.NotBeSameAs(options);
+ ex.Message.Should().ContainAll(Keywords.Minimum, ExceptionMessages.Format(value, true));
}
[Test]
@@ -35,8 +50,8 @@ public void When_Maximum_Is_Not_Finite_Should_Throw_On_Validate(double value)
.Throw()
.Which;
- ex.Options.Should().BeSameAs(options);
- ex.Message.Should().Contain($"({value})");
+ ex.Options.Should().Be(options).And.NotBeSameAs(options);
+ ex.Message.Should().ContainAll(Keywords.Maximum, ExceptionMessages.Format(value, true));
}
[Test]
@@ -44,6 +59,8 @@ public void When_Maximum_Is_Not_Finite_Should_Throw_On_Validate(double value)
public void When_Mode_Is_Not_Finite_Should_Throw_On_Validate(double value)
{
var options = new DelayOptions.Triangular()
+ .WithMinimum(100.0)
+ .WithMaximum(200.0)
.WithMode(value);
var ex = options.Invoking(opt => opt.Validate())
@@ -51,8 +68,111 @@ public void When_Mode_Is_Not_Finite_Should_Throw_On_Validate(double value)
.Throw()
.Which;
- ex.Options.Should().BeSameAs(options);
- ex.Message.Should().Contain($"({value})");
+ ex.Options.Should().Be(options).And.NotBeSameAs(options);
+ ex.Message.Should().ContainAll(Keywords.Mode, ExceptionMessages.Format(value, true));
+ }
+
+ [Test]
+ public void When_Mode_Is_LessThan_Minimum_Should_Throw_On_Validate()
+ {
+ double min = Math.Round(CoreTools.NextDouble(400.0, 800.0));
+ double max = Math.Round(CoreTools.NextDouble(1200.0, 1600.0));
+ double mode = Math.Round(min - 100.0);
+
+ var options = new DelayOptions.Triangular()
+ .WithTimeUnit(TimeUnit.Millisecond)
+ .WithMinimum(min)
+ .WithMaximum(max)
+ .WithMode(mode);
+
+ var ex = options.Invoking(opt => opt.Validate())
+ .Should()
+ .Throw()
+ .Which;
+
+ ex.Options.Should().Be(options).And.NotBeSameAs(options);
+ ex.Message.Should().ContainAll(
+ Keywords.Mode,
+ ExceptionMessages.Format(mode, true),
+ ExceptionMessages.Format(min, false),
+ ExceptionMessages.Format(max, false));
+ }
+
+ [Test]
+ public void When_Mode_Is_GreaterThan_Maximum_Should_Throw_On_Validate()
+ {
+ double min = Math.Round(CoreTools.NextDouble(200.0, 500.0));
+ double max = Math.Round(CoreTools.NextDouble(800.0, 1200.0));
+ double mode = Math.Round(max + 100.0);
+
+ var options = new DelayOptions.Triangular()
+ .WithTimeUnit(TimeUnit.Millisecond)
+ .WithMinimum(min)
+ .WithMaximum(max)
+ .WithMode(mode);
+
+ var ex = options.Invoking(opt => opt.Validate())
+ .Should()
+ .Throw()
+ .Which;
+
+ ex.Options.Should().Be(options).And.NotBeSameAs(options);
+ ex.Message.Should().ContainAll(
+ Keywords.Mode,
+ ExceptionMessages.Format(mode, true),
+ ExceptionMessages.Format(min, false),
+ ExceptionMessages.Format(max, false));
+ }
+
+ [Test]
+ public void When_Used_As_Dictionary_Key_Should_Return_Correct_Value()
+ {
+ const double min = 850.0;
+ const double max = 1450.0;
+ const double mode = 1200.0;
+
+ var options = new DelayOptions.Triangular()
+ .WithTimeUnit(TimeUnit.Millisecond)
+ .WithMinimum(min)
+ .WithMaximum(max)
+ .WithMode(mode);
+
+ var keyToLookup = (DelayOptions.Triangular) options.Clone();
+ var expectedValue = new object();
+
+ var dict = new Dictionary
+ {
+ [options] = expectedValue
+ };
+
+ bool exists = dict.TryGetValue(keyToLookup, out object? actualValue);
+
+ exists.Should().BeTrue();
+ actualValue.Should().Be(expectedValue);
+ }
+
+ [Test]
+ [TestCase(0.0), TestCase(0.1)]
+ [TestCase(0.2), TestCase(0.3)]
+ [TestCase(0.4), TestCase(0.5)]
+ [TestCase(0.6), TestCase(0.7)]
+ [TestCase(0.8), TestCase(0.9)]
+ [TestCase(1.0)]
+ public void When_Valid_Options_Provided_Should_Not_Throw_On_Validate(double modeFactor)
+ {
+ double min = Math.Round(CoreTools.NextDouble(600.0, 900.0));
+ double max = Math.Round(CoreTools.NextDouble(1200.0, 1600.0));
+ double mode = Math.Round(Math.FusedMultiplyAdd(modeFactor, max - min, min));
+
+ var options = new DelayOptions.Triangular()
+ .WithTimeUnit(TimeUnit.Millisecond)
+ .WithMinimum(min)
+ .WithMaximum(max)
+ .WithMode(mode);
+
+ options.Invoking(opt => opt.Validate())
+ .Should()
+ .NotThrow();
}
}
}
diff --git a/Source/RandomTools.Tests/Statistics.cs b/Source/RandomTools.Tests/Statistics.cs
index 1e4d4f1..a3754d7 100644
--- a/Source/RandomTools.Tests/Statistics.cs
+++ b/Source/RandomTools.Tests/Statistics.cs
@@ -110,6 +110,20 @@ public static double StandardErrorOfMean(double stdDev, int sampleCount)
return stdDev / Math.Sqrt(sampleCount);
}
+
+ ///
+ /// Estimates the order n of a Bates distribution from the sample variance.
+ ///
+ /// Variance of the Bates-distributed sample.
+ /// Min and Max of the underlying uniform distribution.
+ /// Estimated number of uniform variables averaged (n).
+ public static double EstimateBatesOrder(double variance, (double Min, double Max) bounds)
+ {
+ double range = bounds.Max - bounds.Min;
+ double n = range * range / (12 * variance);
+
+ return n;
+ }
}
///