Add Trapezoidal suitability function and corresponding tests
CI / build-test (push) Successful in 1m10s
CI / build-test (push) Successful in 1m10s
Introduced a new Trapezoid method in the Suitability class to model suitability with a hard tolerance band and a plateau. The method returns suitability values based on defined limits and optimal ranges. Added comprehensive unit tests to validate the functionality, including edge cases for flat plateaus, linear ramps, and hard limits. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Opus 4.8
parent
d044cafad9
commit
d360093be1
@@ -23,4 +23,41 @@ public static class Suitability
|
|||||||
var z = (value - optimum) / tolerance;
|
var z = (value - optimum) / tolerance;
|
||||||
return MathF.Exp(-0.5f * z * z);
|
return MathF.Exp(-0.5f * z * z);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Trapezoidal suitability in <c>[0, 1]</c>: <c>0</c> at or beyond the hard limits
|
||||||
|
/// <paramref name="min"/>/<paramref name="max"/>, ramping linearly up to <c>1</c> at
|
||||||
|
/// <paramref name="optimalLow"/>, flat <c>1</c> across the comfortable plateau to
|
||||||
|
/// <paramref name="optimalHigh"/>, then ramping back down to <c>0</c> at <paramref name="max"/>.
|
||||||
|
/// Models a hard tolerance band with a plateau (RimWorld-style plant growth vs. temperature:
|
||||||
|
/// dormant below <paramref name="min"/> or above <paramref name="max"/>). A degenerate edge
|
||||||
|
/// (<paramref name="optimalLow"/> ≤ <paramref name="min"/> or <paramref name="optimalHigh"/> ≥
|
||||||
|
/// <paramref name="max"/>) becomes a hard step on that side. The four bounds are expected
|
||||||
|
/// ordered (<c>min ≤ optimalLow ≤ optimalHigh ≤ max</c>); pass ordered values.
|
||||||
|
/// </summary>
|
||||||
|
public static float Trapezoid(
|
||||||
|
float value,
|
||||||
|
float min,
|
||||||
|
float optimalLow,
|
||||||
|
float optimalHigh,
|
||||||
|
float max
|
||||||
|
)
|
||||||
|
{
|
||||||
|
if (value <= min || value >= max)
|
||||||
|
{
|
||||||
|
return 0f;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (value < optimalLow)
|
||||||
|
{
|
||||||
|
return optimalLow > min ? (value - min) / (optimalLow - min) : 1f;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (value > optimalHigh)
|
||||||
|
{
|
||||||
|
return max > optimalHigh ? (max - value) / (max - optimalHigh) : 1f;
|
||||||
|
}
|
||||||
|
|
||||||
|
return 1f;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -48,4 +48,45 @@ public class SuitabilityTests
|
|||||||
Assert.Equal(1f, Suitability.Gaussian(5f, 5f, 0f));
|
Assert.Equal(1f, Suitability.Gaussian(5f, 5f, 0f));
|
||||||
Assert.Equal(0f, Suitability.Gaussian(6f, 5f, 0f));
|
Assert.Equal(0f, Suitability.Gaussian(6f, 5f, 0f));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void Trapezoid_IsFlatAcrossThePlateau()
|
||||||
|
{
|
||||||
|
Assert.Equal(1f, Suitability.Trapezoid(10f, 0f, 10f, 42f, 58f), 5);
|
||||||
|
Assert.Equal(1f, Suitability.Trapezoid(25f, 0f, 10f, 42f, 58f), 5);
|
||||||
|
Assert.Equal(1f, Suitability.Trapezoid(42f, 0f, 10f, 42f, 58f), 5);
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void Trapezoid_RampsLinearlyOnEachShoulder()
|
||||||
|
{
|
||||||
|
Assert.Equal(0.5f, Suitability.Trapezoid(5f, 0f, 10f, 42f, 58f), 5); // halfway up the cold ramp
|
||||||
|
Assert.Equal(0.5f, Suitability.Trapezoid(50f, 0f, 10f, 42f, 58f), 5); // halfway down the heat ramp
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void Trapezoid_IsZeroAtAndBeyondTheHardLimits()
|
||||||
|
{
|
||||||
|
Assert.Equal(0f, Suitability.Trapezoid(0f, 0f, 10f, 42f, 58f));
|
||||||
|
Assert.Equal(0f, Suitability.Trapezoid(-5f, 0f, 10f, 42f, 58f));
|
||||||
|
Assert.Equal(0f, Suitability.Trapezoid(58f, 0f, 10f, 42f, 58f));
|
||||||
|
Assert.Equal(0f, Suitability.Trapezoid(70f, 0f, 10f, 42f, 58f));
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void Trapezoid_DegenerateShoulder_IsAHardStep()
|
||||||
|
{
|
||||||
|
// optimalLow == min: full suitability immediately above the lower limit, zero at/below it.
|
||||||
|
Assert.Equal(1f, Suitability.Trapezoid(5f, 0f, 0f, 10f, 20f));
|
||||||
|
Assert.Equal(0f, Suitability.Trapezoid(0f, 0f, 0f, 10f, 20f));
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void Trapezoid_StaysWithinUnitRange()
|
||||||
|
{
|
||||||
|
for (var v = -50f; v <= 80f; v += 1f)
|
||||||
|
{
|
||||||
|
Assert.InRange(Suitability.Trapezoid(v, 0f, 10f, 42f, 58f), 0f, 1f);
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user