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
153 changes: 153 additions & 0 deletions Source/RandomTools.Core/Options/Delay/DelayOptions.cs
Original file line number Diff line number Diff line change
Expand Up @@ -228,5 +228,158 @@ The interval is too narrow to produce meaningful randomness.
}
}
}

/// <summary>
/// Configuration options for a <b>Bates distribution</b> used to generate
/// bounded random delays.
/// <para>
/// The Bates distribution is defined as the arithmetic mean of
/// <c>N</c> independent uniform samples. It produces a smooth,
/// bell-shaped distribution that remains strictly within the
/// configured <see cref="Minimum"/> and <see cref="Maximum"/> range.
/// </para>
/// <para>
/// When <c>Samples = 1</c>, the distribution degenerates to a uniform
/// distribution. Increasing <c>Samples</c> makes the distribution
/// progressively smoother and more concentrated toward the center,
/// approaching a bounded normal-like shape.
/// </para>
/// </summary>
public sealed class Bates : DelayOptionsBase<Bates>
{
/// <summary>
/// Gets or sets the number of uniform samples used to construct the
/// Bates distribution. Must be at least <c>1</c>.
/// </summary>
internal int Samples;

/// <summary>
/// Sets the number of uniform samples used to compute the Bates mean.
/// Larger values produce smoother, more bell-shaped distributions.
/// </summary>
/// <param name="value">The number of samples (must be ≥ 1).</param>
/// <returns>The current instance for fluent configuration.</returns>
public Bates WithSamples(int value)
{
Samples = value;
return this;
}

/// <summary>
/// Validates the configuration and throws an <see cref="OptionsValidationException"/>
/// if the settings are not suitable for generating a Bates distribution.
/// </summary>
public override void Validate()
{
base.Validate();

if ((Maximum - Minimum) <= double.Epsilon)
{
throw new OptionsValidationException(this,
$"Configured range [{Minimum}, {Maximum}] is too narrow. " +
$"A Bates distribution cannot reliably generate values within such a small interval.");
}

if (Samples < 1)
{
throw new OptionsValidationException(this,
$"Samples ({Samples}) must be at least 1 to produce meaningful Bates distribution sampling.");
}
}

/// <inheritdoc />
public override bool Equals(Bates? other)
{
return base.Equals(other) &&
other.Samples == Samples;
}

/// <inheritdoc />
public override int GetHashCode() =>
HashCode.Combine(Minimum, Maximum, TimeUnit, Samples);
}

/// <summary>
/// Configuration options for a Polynomial / Power delay distribution.
/// <para>
/// The Polynomial distribution generates values in [Minimum, Maximum] with a density
/// proportional to (x - Minimum)^Power or (Maximum - x)^Power if reversed.
/// </para>
/// <para>
/// A Power of 0 produces a uniform distribution. Higher Power values produce
/// increasingly skewed distributions toward Maximum (or Minimum if Reverse=true).
/// </para>
/// </summary>
public sealed class Polynomial : DelayOptionsBase<Polynomial>
{
/// <summary>
/// Gets the power exponent for the polynomial distribution.
/// Must be >= 0.
/// </summary>
internal double Power { get; private set; } = 1.0;

/// <summary>
/// Indicates whether the distribution is reversed: density proportional to (Maximum - x)^Power.
/// </summary>
internal bool Reverse { get; private set; } = false;

/// <summary>
/// Sets the power exponent (Power ≥ 0) for the polynomial distribution.
/// </summary>
/// <param name="value">Exponent value.</param>
/// <returns>The current instance for fluent configuration.</returns>
public Polynomial WithPower(double value)
{
Power = value;
return this;
}

/// <summary>
/// Sets whether the distribution is reversed (more values near Minimum).
/// </summary>
/// <param name="value">True to reverse, false for normal orientation.</param>
/// <returns>The current instance for fluent configuration.</returns>
public Polynomial WithReverse(bool value)
{
Reverse = value;
return this;
}

/// <summary>
/// Validates the configuration, throwing an <see cref="OptionsValidationException"/>
/// if any option is invalid.
/// </summary>
public override void Validate()
{
base.Validate();

if ((Maximum - Minimum) <= double.Epsilon)
{
throw new OptionsValidationException(this,
$"Configured range [{Minimum}, {Maximum}] is too narrow. " +
"Polynomial distribution cannot reliably generate values in such a small interval.");
}

EnsureFinite(Power);

if (Power < 0.0)
{
throw new OptionsValidationException(this,
$"Power ({Power}) must be >= 0.");
}
}

/// <inheritdoc/>
public override bool Equals(Polynomial? other)
{
return base.Equals(other) &&
other.Power == Power &&
other.Reverse == Reverse;
}

/// <inheritdoc/>
public override int GetHashCode() =>
HashCode.Combine(Minimum, Maximum, TimeUnit, Power, Reverse);
}
}
}
5 changes: 1 addition & 4 deletions Source/RandomTools.Core/Random/Delay/ArcsineDelay.cs
Original file line number Diff line number Diff line change
Expand Up @@ -36,9 +36,6 @@ public ArcsineDelay(DelayOptions.Arcsine options) : base(options)
/// <returns>A <see cref="TimeSpan"/> representing the generated delay.</returns>
public override TimeSpan Next()
{
double min = Options.Minimum;
double max = Options.Maximum;

// Generate a uniform random value in [0,1)
double u = CoreTools.NextDouble();

Expand All @@ -47,7 +44,7 @@ public override TimeSpan Next()
double sinSq = Math.Sin(angle) * Math.Sin(angle);

// Scale the result to the configured range
double value = min + sinSq * (max - min);
double value = ScaleToRange(sinSq);

// Convert the numeric value to a TimeSpan using the specified time unit
return CoreTools.ToTimeSpan(value, Options.TimeUnit);
Expand Down
53 changes: 53 additions & 0 deletions Source/RandomTools.Core/Random/Delay/BatesDelay.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,53 @@
using RandomTools.Core.Options.Delay;

namespace RandomTools.Core.Random.Delay
{
/// <summary>
/// Generates delays based on a <see cref="Bates"/> distribution.
/// <para>
/// The Bates distribution is defined as the arithmetic mean of
/// <see cref="DelayOptions.Bates.Samples"/> independent uniform random samples.
/// It produces a smooth, bell-shaped distribution strictly within the configured
/// <see cref="DelayOptions.Bates.Minimum"/> and <see cref="DelayOptions.Bates.Maximum"/> range.
/// </para>
/// <para>
/// When <c>Samples = 1</c>, this degenerates to a uniform distribution. Increasing
/// <c>Samples</c> results in a smoother, more centered distribution.
/// </para>
/// </summary>
public sealed class BatesDelay : RandomDelay<DelayOptions.Bates>
{
#pragma warning disable IDE0290 // Use primary constructor
/// <summary>
/// Initializes a new instance of the <see cref="BatesDelay"/> class
/// with the specified <see cref="DelayOptions.Bates"/>.
/// </summary>
/// <param name="options">Configuration options for the Bates distribution.</param>
public BatesDelay(DelayOptions.Bates options) : base(options)
#pragma warning restore IDE0290 // Use primary constructor
{
}

/// <summary>
/// Generates the next random delay based on the configured Bates distribution.
/// </summary>
/// <returns>A <see cref="TimeSpan"/> representing the generated delay.</returns>
public override TimeSpan Next()
{
// Number of uniform samples to average
int N = Options.Samples;
double mean = 0.0;

for (int i = 0; i < N; i++)
{
double next = CoreTools.NextDouble();
mean += (next - mean) / (i + 1);
}

// Map mean from [0,1] to the configured [Minimum, Maximum] range
double value = ScaleToRange(mean);

return CoreTools.ToTimeSpan(value, Options.TimeUnit);
}
}
}
53 changes: 53 additions & 0 deletions Source/RandomTools.Core/Random/Delay/PolynomialDelay.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,53 @@
using RandomTools.Core.Options.Delay;

namespace RandomTools.Core.Random.Delay
{
/// <summary>
/// Generates random delays based on a <b>Polynomial / Power distribution</b>.
/// <para>
/// The distribution generates values in [Minimum, Maximum] with a density
/// proportional to (t - Minimum)^Power or (Maximum - t)^Power if <see cref="DelayOptions.Polynomial.Reverse"/> is true.
/// </para>
/// <para>
/// A Power of 0 produces a uniform distribution. Higher Power values produce
/// increasingly skewed delays toward Maximum (or Minimum if reversed).
/// </para>
/// </summary>
public sealed class PolynomialDelay : RandomDelay<DelayOptions.Polynomial>
{
#pragma warning disable IDE0290 // Use primary constructor
/// <summary>
/// Initializes a new instance of <see cref="PolynomialDelay"/> with the specified options.
/// </summary>
/// <param name="options">Polynomial distribution configuration options.</param>
public PolynomialDelay(DelayOptions.Polynomial options) : base(options)
#pragma warning restore IDE0290 // Use primary constructor
{
}

/// <summary>
/// Generates the next random delay according to the configured Polynomial distribution.
/// </summary>
/// <returns>A <see cref="TimeSpan"/> representing the generated delay.</returns>
public override TimeSpan Next()
{
// Generate a uniform random value in [0,1)
double u = CoreTools.NextDouble();

// Apply inverse CDF of the Polynomial distribution to get a normalized fraction
// fraction ∈ [0,1], representing relative position in the [Minimum, Maximum] interval
double fraction = Math.Pow(u, 1.0 / (Options.Power + 1.0));

if (Options.Reverse)
{
// more values closer to Minimum
fraction = 1.0 - fraction;
}

// Scale the normalized fraction to the [Minimum, Maximum] range
double value = ScaleToRange(fraction);

return CoreTools.ToTimeSpan(value, Options.TimeUnit);
}
}
}
25 changes: 25 additions & 0 deletions Source/RandomTools.Core/Random/Delay/RandomDelay.cs
Original file line number Diff line number Diff line change
Expand Up @@ -30,7 +30,9 @@ public abstract class RandomDelay<TOptions> : RandomBase<TimeSpan, TOptions>
/// <param name="options">
/// Configuration defining how delays are calculated, including range and distribution parameters.
/// </param>
#pragma warning disable IDE0290 // Use primary constructor
protected RandomDelay(TOptions options) : base(options)
#pragma warning restore IDE0290 // Use primary constructor
{
// Base class stores options and provides the Next() method for generating delays.
}
Expand Down Expand Up @@ -93,5 +95,28 @@ public async Task<TimeSpan> WaitAsync(CancellationToken cancellationToken = defa

return delay;
}

/// <summary>
/// Scales a fraction in the range [0,1] to the configured [Minimum, Maximum] range.
/// </summary>
/// <param name="fraction">
/// A value between 0 and 1, typically produced by the underlying random distribution.
/// </param>
/// <returns>
/// The value scaled linearly to the range [Minimum, Maximum].
/// </returns>
/// <exception cref="ArgumentOutOfRangeException">
/// Thrown if <paramref name="fraction"/> is less than 0 or greater than 1.
/// </exception>
protected double ScaleToRange(double fraction)
{
// Validate that the fraction is within the normalized [0,1] range
ArgumentOutOfRangeException.ThrowIfNegative(fraction);
ArgumentOutOfRangeException.ThrowIfGreaterThan(fraction, 1.0);

// Linearly scale fraction to the configured range
double range = Options.Maximum - Options.Minimum;
return Math.FusedMultiplyAdd(range, fraction, Options.Minimum);
}
}
}
47 changes: 46 additions & 1 deletion Source/RandomTools.Tests/TriangularDelayTests.cs
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
using RandomTools.Core.Options.Delay;
using RandomTools.Core;
using RandomTools.Core.Options.Delay;
using RandomTools.Core.Random.Delay;
using System;
using System.Collections.Generic;
Expand All @@ -12,6 +13,50 @@ namespace RandomTools.Tests
[TestFixture]
public class TriangularDelayTests
{
[Test]
public void TestThree()
{
var options = new DelayOptions.Bates()
.WithMinimum(100)
.WithMaximum(200)
.WithSamples(6);

var delay = new BatesDelay(options);
var data = new List<TimeSpan>();
for (int i = 0; i < 1_000_000; i++)
{
var next = delay.Next();
data.Add(next);
}

var min = data.Min(x => x);
var max = data.Max(x => x);
Debugger.Break();
}

[Test]
public void TestTwo()
{
var options = new DelayOptions.Arcsine()
.WithMinimum(100)
.WithMaximum(150)
.WithTimeUnit(TimeUnit.Second);

var delay = new ArcsineDelay(options);
var data = new List<TimeSpan>();

for (int i = 0; i < 1_000_000; i++)
{
var next = delay.Next();
data.Add(next);
}

var min = data.Min(x => x);
var max = data.Max(x => x);

Debugger.Break();
}

[Test]
public void TestOne()
{
Expand Down
Loading