Compare commits
1
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
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