Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
50 changes: 18 additions & 32 deletions Source/RandomTools.Core/Options/Delay/DelayOptions.cs
Original file line number Diff line number Diff line change
Expand Up @@ -97,7 +97,6 @@ public Normal WithAutoFit(double minimum, double maximum)
public override void Validate()
{
base.Validate();

EnsureFinite(Mean);
EnsureFinite(StandardDeviation);

Expand All @@ -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);
Expand Down Expand Up @@ -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
Expand All @@ -173,11 +169,9 @@ public override void Validate()
/// </summary>
/// <param name="other">Another <see cref="Triangular"/> instance.</param>
/// <returns>True if equal, false otherwise.</returns>
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);

/// <summary>
/// Returns a hash code for the current instance.
Expand Down Expand Up @@ -310,7 +304,6 @@ public Polynomial WithReverse(bool value)
public override void Validate()
{
base.Validate();

EnsureFinite(Power);

if (Power < 0.0)
Expand All @@ -321,12 +314,10 @@ public override void Validate()
}

/// <inheritdoc/>
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;

/// <inheritdoc/>
public override int GetHashCode() =>
Expand Down Expand Up @@ -394,7 +385,6 @@ public override void Validate()
{
// Validate base numeric fields (Minimum/Maximum)
base.Validate();

EnsureFinite(AlphaValue);
EnsureFinite(BetaValue);

Expand All @@ -416,12 +406,10 @@ public override void Validate()
/// </summary>
/// <param name="other">Other <see cref="Beta"/> instance to compare.</param>
/// <returns><see langword="true"/> if all relevant fields are equal; otherwise <see langword="false"/>.</returns>
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);

/// <summary>
/// Computes a hash code based on range, time unit, and Beta distribution parameters.
Expand Down Expand Up @@ -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);
Expand Down
17 changes: 13 additions & 4 deletions Source/RandomTools.Core/Options/Delay/DelayOptionsBase.cs
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,17 @@ namespace RandomTools.Core.Options.Delay
public abstract class DelayOptionsBase<TDelayOptions> : IOptionsBase, IEquatable<TDelayOptions>
where TDelayOptions : DelayOptionsBase<TDelayOptions>
{
/// <summary>
/// Provides a default equality comparer for <see cref="double"/> values.
/// <para>
/// Used for comparing all numeric fields in options classes, such as <see cref="Minimum"/>,
/// <see cref="Maximum"/>, and other derived fields like <c>Mode</c>.
/// Note that this comparer can compare non-finite values (NaN, Infinity),
/// so validation via <see cref="EnsureFinite"/> is still required to enforce finiteness.
/// </para>
/// </summary>
protected static EqualityComparer<double> DoubleComparer => EqualityComparer<double>.Default;

/// <summary>
/// Minimum value of the delay range.
/// Can be negative, zero, or positive.
Expand Down Expand Up @@ -127,11 +138,9 @@ public virtual bool Equals(TDelayOptions? other)
if (other is null)
return false;

var comparer = EqualityComparer<double>.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;
}

Expand Down
39 changes: 18 additions & 21 deletions Source/RandomTools.Core/RandomTool.cs
Original file line number Diff line number Diff line change
Expand Up @@ -368,7 +368,7 @@ public static NormalDelay InMilliseconds(double mean, double stdDev, double min,
/// Same as <see cref="InMilliseconds(double,double,double,double)"/>
/// but uses seconds as the time unit.
/// </summary>
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)
Expand Down Expand Up @@ -410,7 +410,7 @@ public static NormalDelay InMilliseconds(double mean, double stdDev, (double Min
/// Tuple-based overload for seconds.
/// </summary>
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);

/// <summary>
/// Tuple-based overload for minutes.
Expand All @@ -420,33 +420,31 @@ public static NormalDelay InMinutes(double mean, double stdDev, (double Min, dou
}

/// <summary>
/// Factory class for generating and caching <see cref="BatesDelay"/> instances.
/// Provides methods to obtain <see cref="BatesDelay"/> instances with caching to reuse identical configurations.
/// <para>
/// 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 <see cref="BatesDelay"/> for different time units while caching instances
/// for reuse.
/// A Bates delay represents the arithmetic mean of <c>samples</c> independent uniform values
/// within the specified <c>minimum</c> and <c>maximum</c> range.
/// </para>
/// <para>
/// All configuration validation is performed by <see cref="DelayOptions.Bates.Validate()"/>.
/// All configuration is validated via <see cref="DelayOptions.Bates.Validate()"/>.
/// </para>
/// </summary>
public static class Bates
{
/// <summary>
/// Thread-safe cache for storing <see cref="BatesDelay"/> instances keyed by their options.
/// Ensures that multiple requests with identical configuration return the same instance.
/// Thread-safe cache mapping <see cref="DelayOptions.Bates"/> to <see cref="BatesDelay"/> instances.
/// Ensures that repeated requests with the same options return the same instance.
/// </summary>
private static readonly ConcurrentDictionary<DelayOptions.Bates, BatesDelay> sCache = new();

/// <summary>
/// Returns a cached <see cref="BatesDelay"/> configured with the specified minimum, maximum,
/// number of samples, and time unit.
/// Returns a cached <see cref="BatesDelay"/> configured with the specified range, number of samples, and time unit.
/// </summary>
/// <param name="minimum">The minimum delay value.</param>
/// <param name="maximum">The maximum delay value.</param>
/// <param name="samples">Number of uniform samples to average for the Bates distribution.</param>
/// <param name="unit">Time unit in which the delay will be expressed.</param>
/// <param name="minimum">Minimum delay value.</param>
/// <param name="maximum">Maximum delay value.</param>
/// <param name="samples">Number of uniform samples to average.</param>
/// <param name="unit">Time unit for the delay.</param>
/// <returns>A <see cref="BatesDelay"/> instance with the requested configuration.</returns>
public static BatesDelay For(double minimum, double maximum, int samples, TimeUnit unit)
{
var options = new DelayOptions.Bates()
Expand All @@ -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));
}

/// <summary>
/// Returns a cached <see cref="BatesDelay"/> configured for millisecond delays.
/// Returns a cached <see cref="BatesDelay"/> configured for milliseconds.
/// </summary>
public static BatesDelay InMilliseconds(double minimum, double maximum, int samples) =>
For(minimum, maximum, samples, TimeUnit.Millisecond);

/// <summary>
/// Returns a cached <see cref="BatesDelay"/> configured for second delays.
/// Returns a cached <see cref="BatesDelay"/> configured for seconds.
/// </summary>
public static BatesDelay InSeconds(double minimum, double maximum, int samples) =>
For(minimum, maximum, samples, TimeUnit.Second);

/// <summary>
/// Returns a cached <see cref="BatesDelay"/> configured for minute delays.
/// Returns a cached <see cref="BatesDelay"/> configured for minutes.
/// </summary>
public static BatesDelay InMinutes(double minimum, double maximum, int samples) =>
For(minimum, maximum, samples, TimeUnit.Minute);
Expand Down
43 changes: 26 additions & 17 deletions Source/RandomTools.Tests/BatesDelayTests.cs
Original file line number Diff line number Diff line change
Expand Up @@ -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++)
Expand All @@ -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++)
Expand All @@ -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++)
Expand All @@ -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);
}
}
}
}
1 change: 1 addition & 0 deletions Source/RandomTools.Tests/Keywords.cs
Original file line number Diff line number Diff line change
Expand Up @@ -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);
}
}
8 changes: 4 additions & 4 deletions Source/RandomTools.Tests/Options/BatesOptionsFixture.cs
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand Down
Loading
Loading