From 88c0c3441ec09e034aaba58e8ce2cf4fa1769982 Mon Sep 17 00:00:00 2001 From: Mykola Reshetynskyi Date: Mon, 24 Nov 2025 17:01:06 +0100 Subject: [PATCH 1/2] new changes + fixes --- .../Options/Delay/DelayOptions.cs | 153 ++++++++++++++++++ .../Random/Delay/ArcsineDelay.cs | 5 +- .../Random/Delay/BatesDelay.cs | 53 ++++++ .../Random/Delay/PolynomialDelay.cs | 53 ++++++ .../Random/Delay/RandomDelay.cs | 24 +++ .../RandomTools.Tests/TriangularDelayTests.cs | 47 +++++- 6 files changed, 330 insertions(+), 5 deletions(-) create mode 100644 Source/RandomTools.Core/Random/Delay/BatesDelay.cs create mode 100644 Source/RandomTools.Core/Random/Delay/PolynomialDelay.cs diff --git a/Source/RandomTools.Core/Options/Delay/DelayOptions.cs b/Source/RandomTools.Core/Options/Delay/DelayOptions.cs index ffd31f6..7b2826c 100644 --- a/Source/RandomTools.Core/Options/Delay/DelayOptions.cs +++ b/Source/RandomTools.Core/Options/Delay/DelayOptions.cs @@ -228,5 +228,158 @@ The interval is too narrow to produce meaningful randomness. } } } + + /// + /// Configuration options for a Bates distribution used to generate + /// bounded random delays. + /// + /// The Bates distribution is defined as the arithmetic mean of + /// N independent uniform samples. It produces a smooth, + /// bell-shaped distribution that remains strictly within the + /// configured and range. + /// + /// + /// When Samples = 1, the distribution degenerates to a uniform + /// distribution. Increasing Samples makes the distribution + /// progressively smoother and more concentrated toward the center, + /// approaching a bounded normal-like shape. + /// + /// + public sealed class Bates : DelayOptionsBase + { + /// + /// Gets or sets the number of uniform samples used to construct the + /// Bates distribution. Must be at least 1. + /// + internal int Samples; + + /// + /// Sets the number of uniform samples used to compute the Bates mean. + /// Larger values produce smoother, more bell-shaped distributions. + /// + /// The number of samples (must be ≥ 1). + /// The current instance for fluent configuration. + public Bates WithSamples(int value) + { + Samples = value; + return this; + } + + /// + /// Validates the configuration and throws an + /// if the settings are not suitable for generating a Bates distribution. + /// + 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."); + } + } + + /// + public override bool Equals(Bates? other) + { + return base.Equals(other) && + other.Samples == Samples; + } + + /// + public override int GetHashCode() => + HashCode.Combine(Minimum, Maximum, TimeUnit, Samples); + } + + /// + /// Configuration options for a Polynomial / Power delay distribution. + /// + /// The Polynomial distribution generates values in [Minimum, Maximum] with a density + /// proportional to (x - Minimum)^Power or (Maximum - x)^Power if reversed. + /// + /// + /// A Power of 0 produces a uniform distribution. Higher Power values produce + /// increasingly skewed distributions toward Maximum (or Minimum if Reverse=true). + /// + /// + public sealed class Polynomial : DelayOptionsBase + { + /// + /// Gets the power exponent for the polynomial distribution. + /// Must be >= 0. + /// + internal double Power { get; private set; } = 1.0; + + /// + /// Indicates whether the distribution is reversed: density proportional to (Maximum - x)^Power. + /// + internal bool Reverse { get; private set; } = false; + + /// + /// Sets the power exponent (Power ≥ 0) for the polynomial distribution. + /// + /// Exponent value. + /// The current instance for fluent configuration. + public Polynomial WithPower(double value) + { + Power = value; + return this; + } + + /// + /// Sets whether the distribution is reversed (more values near Minimum). + /// + /// True to reverse, false for normal orientation. + /// The current instance for fluent configuration. + public Polynomial WithReverse(bool value) + { + Reverse = value; + return this; + } + + /// + /// Validates the configuration, throwing an + /// if any option is invalid. + /// + 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."); + } + } + + /// + public override bool Equals(Polynomial? other) + { + return base.Equals(other) && + other.Power == Power && + other.Reverse == Reverse; + } + + /// + public override int GetHashCode() => + HashCode.Combine(Minimum, Maximum, TimeUnit, Power, Reverse); + } } } \ No newline at end of file diff --git a/Source/RandomTools.Core/Random/Delay/ArcsineDelay.cs b/Source/RandomTools.Core/Random/Delay/ArcsineDelay.cs index 33ccd66..b1cf1cc 100644 --- a/Source/RandomTools.Core/Random/Delay/ArcsineDelay.cs +++ b/Source/RandomTools.Core/Random/Delay/ArcsineDelay.cs @@ -36,9 +36,6 @@ public ArcsineDelay(DelayOptions.Arcsine options) : base(options) /// A representing the generated delay. public override TimeSpan Next() { - double min = Options.Minimum; - double max = Options.Maximum; - // Generate a uniform random value in [0,1) double u = CoreTools.NextDouble(); @@ -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); diff --git a/Source/RandomTools.Core/Random/Delay/BatesDelay.cs b/Source/RandomTools.Core/Random/Delay/BatesDelay.cs new file mode 100644 index 0000000..705284c --- /dev/null +++ b/Source/RandomTools.Core/Random/Delay/BatesDelay.cs @@ -0,0 +1,53 @@ +using RandomTools.Core.Options.Delay; + +namespace RandomTools.Core.Random.Delay +{ + /// + /// Generates delays based on a distribution. + /// + /// The Bates distribution is defined as the arithmetic mean of + /// independent uniform random samples. + /// It produces a smooth, bell-shaped distribution strictly within the configured + /// and range. + /// + /// + /// When Samples = 1, this degenerates to a uniform distribution. Increasing + /// Samples results in a smoother, more centered distribution. + /// + /// + public sealed class BatesDelay : RandomDelay + { +#pragma warning disable IDE0290 // Use primary constructor + /// + /// Initializes a new instance of the class + /// with the specified . + /// + /// Configuration options for the Bates distribution. + public BatesDelay(DelayOptions.Bates options) : base(options) +#pragma warning restore IDE0290 // Use primary constructor + { + } + + /// + /// Generates the next random delay based on the configured Bates distribution. + /// + /// A representing the generated delay. + 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); + } + } +} diff --git a/Source/RandomTools.Core/Random/Delay/PolynomialDelay.cs b/Source/RandomTools.Core/Random/Delay/PolynomialDelay.cs new file mode 100644 index 0000000..db304ef --- /dev/null +++ b/Source/RandomTools.Core/Random/Delay/PolynomialDelay.cs @@ -0,0 +1,53 @@ +using RandomTools.Core.Options.Delay; + +namespace RandomTools.Core.Random.Delay +{ + /// + /// Generates random delays based on a Polynomial / Power distribution. + /// + /// The distribution generates values in [Minimum, Maximum] with a density + /// proportional to (t - Minimum)^Power or (Maximum - t)^Power if is true. + /// + /// + /// A Power of 0 produces a uniform distribution. Higher Power values produce + /// increasingly skewed delays toward Maximum (or Minimum if reversed). + /// + /// + public sealed class PolynomialDelay : RandomDelay + { +#pragma warning disable IDE0290 // Use primary constructor + /// + /// Initializes a new instance of with the specified options. + /// + /// Polynomial distribution configuration options. + public PolynomialDelay(DelayOptions.Polynomial options) : base(options) +#pragma warning restore IDE0290 // Use primary constructor + { + } + + /// + /// Generates the next random delay according to the configured Polynomial distribution. + /// + /// A representing the generated delay. + 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); + } + } +} diff --git a/Source/RandomTools.Core/Random/Delay/RandomDelay.cs b/Source/RandomTools.Core/Random/Delay/RandomDelay.cs index 0255ca2..d5c7641 100644 --- a/Source/RandomTools.Core/Random/Delay/RandomDelay.cs +++ b/Source/RandomTools.Core/Random/Delay/RandomDelay.cs @@ -93,5 +93,29 @@ public async Task WaitAsync(CancellationToken cancellationToken = defa return delay; } + + /// + /// Scales a fraction in the range [0,1] to the configured [Minimum, Maximum] range. + /// Throws an exception if the fraction is not in [0,1]. + /// + /// + /// A value between 0 and 1, typically produced by the underlying random distribution. + /// + /// + /// The value scaled to the range [Minimum, Maximum]. + /// + /// + /// Thrown if is less than 0 or greater than 1. + /// + protected double ScaleToRange(double fraction) + { + // Throw if the fraction is out of bounds + ArgumentOutOfRangeException.ThrowIfNegative(fraction); + ArgumentOutOfRangeException.ThrowIfGreaterThan(fraction, 1.0); + + // Scale fraction [0,1] linearly to [Minimum, Maximum] + double range = Options.Maximum - Options.Minimum; + return Options.Minimum + (fraction * range); + } } } diff --git a/Source/RandomTools.Tests/TriangularDelayTests.cs b/Source/RandomTools.Tests/TriangularDelayTests.cs index b486fdb..1df2f4c 100644 --- a/Source/RandomTools.Tests/TriangularDelayTests.cs +++ b/Source/RandomTools.Tests/TriangularDelayTests.cs @@ -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; @@ -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(); + 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(); + + 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() { From 316fbd79137d718f7b3f3ecee1757ac78d7894d9 Mon Sep 17 00:00:00 2001 From: Mykola Reshetynskyi Date: Mon, 24 Nov 2025 17:21:13 +0100 Subject: [PATCH 2/2] fixes --- Source/RandomTools.Core/Random/Delay/RandomDelay.cs | 11 ++++++----- 1 file changed, 6 insertions(+), 5 deletions(-) diff --git a/Source/RandomTools.Core/Random/Delay/RandomDelay.cs b/Source/RandomTools.Core/Random/Delay/RandomDelay.cs index d5c7641..d62ff61 100644 --- a/Source/RandomTools.Core/Random/Delay/RandomDelay.cs +++ b/Source/RandomTools.Core/Random/Delay/RandomDelay.cs @@ -30,7 +30,9 @@ public abstract class RandomDelay : RandomBase /// /// Configuration defining how delays are calculated, including range and distribution parameters. /// +#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. } @@ -96,26 +98,25 @@ public async Task WaitAsync(CancellationToken cancellationToken = defa /// /// Scales a fraction in the range [0,1] to the configured [Minimum, Maximum] range. - /// Throws an exception if the fraction is not in [0,1]. /// /// /// A value between 0 and 1, typically produced by the underlying random distribution. /// /// - /// The value scaled to the range [Minimum, Maximum]. + /// The value scaled linearly to the range [Minimum, Maximum]. /// /// /// Thrown if is less than 0 or greater than 1. /// protected double ScaleToRange(double fraction) { - // Throw if the fraction is out of bounds + // Validate that the fraction is within the normalized [0,1] range ArgumentOutOfRangeException.ThrowIfNegative(fraction); ArgumentOutOfRangeException.ThrowIfGreaterThan(fraction, 1.0); - // Scale fraction [0,1] linearly to [Minimum, Maximum] + // Linearly scale fraction to the configured range double range = Options.Maximum - Options.Minimum; - return Options.Minimum + (fraction * range); + return Math.FusedMultiplyAdd(range, fraction, Options.Minimum); } } }