using NUnit.Framework;
using Chimp.Solar;
using Chimp.Solar.Data;
using UnityEngine;
namespace Chimp.Sonar.Editor.Tests
{
[TestFixture]
public class SolarCalculatorTests
{
public class SolarTestScenario
{
public SolarInputs Input;
public SolarPosition ExpectedPosition;
public SolarTime ExpectedTime;
};
///
/// The percentage error difference between the expected value and the actual value
/// should be less than tolerance
///
private const float SolarPositionErrorTolerance = 0.001f;
private const float SolarTimeErrorTolerance = 0.003f;
///
/// Output Data
///
private SolarPosition _actualSolarPosition;
private SolarTime _actualSolarTime;
///
/// Test Inputs & Outputs
/// From: https://gml.noaa.gov/grad/solcalc/calcdetails.html (Daily and Yearly tables)
///
private static readonly SolarTestScenario[] s_correctTestData = new SolarTestScenario[]
{
// yearly data
new()
{
Input = new SolarInputs
{
Latitude = 40.0f,
Longitude = -105.0f,
TimeZone = -7,
Year = 2010,
Month = 1,
Day = 1,
Hour = 12,
Minute = 0,
Second = 0
},
ExpectedPosition = new SolarPosition
{
AzimuthAngle = 179.0474507f,
Elevation = 27.032624f,
CorrectedElevation = 27.06410813f
},
ExpectedTime = new SolarTime
{
SolarNoon = "12:03:41",
SunriseTime = "7:21:57",
SunsetTime = "16:45:25",
SunlightDurationAsMinutes = 563.4681652f
}
},
new()
{
Input = new SolarInputs
{
Latitude = 40.0f,
Longitude = -105.0f,
TimeZone = -7,
Year = 2010,
Month = 6,
Day = 6,
Hour = 12,
Minute = 0,
Second = 0
},
ExpectedPosition = new SolarPosition
{
AzimuthAngle = 180.9989928f,
Elevation = 72.69710281f,
CorrectedElevation = 72.70212982f
},
ExpectedTime = new SolarTime
{
SolarNoon = "11:58:43",
SunriseTime = "4:31:28",
SunsetTime = "19:25:58",
SunlightDurationAsMinutes = 894.4956883f
}
},
new()
{
Input = new SolarInputs
{
Latitude = 40.0f,
Longitude = -105.0f,
TimeZone = -7,
Year = 2010,
Month = 12,
Day = 12,
Hour = 12,
Minute = 0,
Second = 0
},
ExpectedPosition = new SolarPosition
{
AzimuthAngle = 181.5909104f,
Elevation = 26.87532953f,
CorrectedElevation = 26.90702632f
},
ExpectedTime = new SolarTime
{
SolarNoon = "11:53:50",
SunriseTime = "7:12:42",
SunsetTime = "16:34:57",
SunlightDurationAsMinutes = 562.2439904f
}
},
// daily data
new()
{
Input = new SolarInputs
{
Latitude = 40.0f,
Longitude = -105.0f,
TimeZone = -6,
Year = 2010,
Month = 6,
Day = 21,
Hour = 0,
Minute = 6,
Second = 0
},
ExpectedPosition = new SolarPosition
{
AzimuthAngle = 345.8691023f,
Elevation = -25.24571849f,
CorrectedElevation = -25.23348197f
},
ExpectedTime = new SolarTime
{
SolarNoon = "13:01:42",
SunriseTime = "5:31:16",
SunsetTime = "20:32:09",
SunlightDurationAsMinutes = 900.882771f
}
},
new()
{
Input = new SolarInputs
{
Latitude = 40.0f,
Longitude = -105.0f,
TimeZone = -6,
Year = 2010,
Month = 6,
Day = 21,
Hour = 6,
Minute = 30,
Second = 0
},
ExpectedPosition = new SolarPosition
{
AzimuthAngle = 66.97512151f,
Elevation = 9.124360432f,
CorrectedElevation = 9.220375537f
},
ExpectedTime = new SolarTime
{
SolarNoon = "13:01:46",
SunriseTime = "5:31:19",
SunsetTime = "20:32:12",
SunlightDurationAsMinutes = 900.8842304f
}
},
new()
{
Input = new SolarInputs
{
Latitude = 40.0f,
Longitude = -105.0f,
TimeZone = -6,
Year = 2010,
Month = 6,
Day = 21,
Hour = 13,
Minute = 0,
Second = 0
},
ExpectedPosition = new SolarPosition
{
AzimuthAngle = 178.53323f,
Elevation = 73.43367897f,
CorrectedElevation = 73.43847934f
},
ExpectedTime = new SolarTime
{
SolarNoon = "13:01:49",
SunriseTime = "5:31:23",
SunsetTime = "20:32:16",
SunlightDurationAsMinutes = 900.8813415f
}
},
new()
{
Input = new SolarInputs
{
Latitude = 40.0f,
Longitude = -105.0f,
TimeZone = -6,
Year = 2010,
Month = 6,
Day = 21,
Hour = 19,
Minute = 0,
Second = 0
},
ExpectedPosition = new SolarPosition
{
AzimuthAngle = 288.0992729f,
Elevation = 15.15457648f,
CorrectedElevation = 15.21320184f
},
ExpectedTime = new SolarTime
{
SolarNoon = "13:01:53",
SunriseTime = "5:31:26",
SunsetTime = "20:32:19",
SunlightDurationAsMinutes = 900.8747658f
}
}
};
private static readonly SolarTestScenario[] s_incorrectTestData = new SolarTestScenario[]
{
// yearly data
new()
{
Input = new SolarInputs
{
Latitude = 40.0f,
Longitude = -105.0f,
TimeZone = -7,
Year = 2010,
Month = 1,
Day = 1,
Hour = 12,
Minute = 0,
Second = 0
},
ExpectedPosition = new SolarPosition
{
AzimuthAngle = 178.0474507f,
Elevation = 26.032624f,
CorrectedElevation = 26.06410813f
},
ExpectedTime = new SolarTime
{
SolarNoon = "12:04:41",
SunriseTime = "7:22:57",
SunsetTime = "16:46:25",
SunlightDurationAsMinutes = 564.4681652f
}
},
new()
{
Input = new SolarInputs
{
Latitude = 40.0f,
Longitude = -105.0f,
TimeZone = -7,
Year = 2010,
Month = 6,
Day = 6,
Hour = 12,
Minute = 0,
Second = 0
},
ExpectedPosition = new SolarPosition
{
AzimuthAngle = 179.9989928f,
Elevation = 71.69710281f,
CorrectedElevation = 71.70212982f
},
ExpectedTime = new SolarTime
{
SolarNoon = "11:59:43",
SunriseTime = "4:32:28",
SunsetTime = "19:26:58",
SunlightDurationAsMinutes = 895.4956883f
}
},
new()
{
Input = new SolarInputs
{
Latitude = 40.0f,
Longitude = -105.0f,
TimeZone = -7,
Year = 2010,
Month = 12,
Day = 12,
Hour = 12,
Minute = 0,
Second = 0
},
ExpectedPosition = new SolarPosition
{
AzimuthAngle = 180.5909104f,
Elevation = 25.87532953f,
CorrectedElevation = 25.90702632f
},
ExpectedTime = new SolarTime
{
SolarNoon = "11:54:50",
SunriseTime = "7:13:42",
SunsetTime = "16:35:57",
SunlightDurationAsMinutes = 563.2439904f
}
},
// daily data
new()
{
Input = new SolarInputs
{
Latitude = 40.0f,
Longitude = -105.0f,
TimeZone = -6,
Year = 2010,
Month = 6,
Day = 21,
Hour = 0,
Minute = 6,
Second = 0
},
ExpectedPosition = new SolarPosition
{
AzimuthAngle = 344.8691023f,
Elevation = -24.24571849f,
CorrectedElevation = -24.23348197f
},
ExpectedTime = new SolarTime
{
SolarNoon = "13:02:42",
SunriseTime = "5:32:16",
SunsetTime = "20:33:09",
SunlightDurationAsMinutes = 901.882771f
}
},
new()
{
Input = new SolarInputs
{
Latitude = 40.0f,
Longitude = -105.0f,
TimeZone = -6,
Year = 2010,
Month = 6,
Day = 21,
Hour = 6,
Minute = 30,
Second = 0
},
ExpectedPosition = new SolarPosition
{
AzimuthAngle = 65.97512151f,
Elevation = 8.124360432f,
CorrectedElevation = 8.220375537f
},
ExpectedTime = new SolarTime
{
SolarNoon = "13:02:46",
SunriseTime = "5:32:19",
SunsetTime = "20:33:12",
SunlightDurationAsMinutes = 901.8842304f
}
},
new()
{
Input = new SolarInputs
{
Latitude = 40.0f,
Longitude = -105.0f,
TimeZone = -6,
Year = 2010,
Month = 6,
Day = 21,
Hour = 13,
Minute = 0,
Second = 0
},
ExpectedPosition = new SolarPosition
{
AzimuthAngle = 177.53323f,
Elevation = 72.43367897f,
CorrectedElevation = 72.43847934f
},
ExpectedTime = new SolarTime
{
SolarNoon = "13:02:49",
SunriseTime = "5:32:23",
SunsetTime = "20:33:16",
SunlightDurationAsMinutes = 901.8813415f
}
},
new()
{
Input = new SolarInputs
{
Latitude = 40.0f,
Longitude = -105.0f,
TimeZone = -6,
Year = 2010,
Month = 6,
Day = 21,
Hour = 19,
Minute = 0,
Second = 0
},
ExpectedPosition = new SolarPosition
{
AzimuthAngle = 287.0992729f,
Elevation = 14.15457648f,
CorrectedElevation = 14.21320184f
},
ExpectedTime = new SolarTime
{
SolarNoon = "13:02:53",
SunriseTime = "5:32:26",
SunsetTime = "20:33:19",
SunlightDurationAsMinutes = 901.8747658f
}
}
};
[SetUp]
public void SetUp()
{
_actualSolarPosition = new SolarPosition();
_actualSolarTime = new SolarTime();
}
[Test, TestCaseSource(nameof(s_correctTestData))]
public void SolarCalculator_Position_ShouldProduceCorrectValues(SolarTestScenario testScenario)
{
var calculator = new SolarCalculator(testScenario.Input);
calculator.CalculateSolarData(ref _actualSolarPosition);
AssertSolarPosition(testScenario.ExpectedPosition, _actualSolarPosition, Assert.LessOrEqual);
}
[Test, TestCaseSource(nameof(s_incorrectTestData))]
public void SolarCalculator_Position_ShouldProduceIncorrectValues(SolarTestScenario testScenario)
{
var calculator = new SolarCalculator(testScenario.Input);
calculator.CalculateSolarData(ref _actualSolarPosition);
AssertSolarPosition(testScenario.ExpectedPosition, _actualSolarPosition, Assert.Greater);
}
[Test, TestCaseSource(nameof(s_correctTestData))]
public void SolarCalculator_Time_ShouldProduceCorrectValues(SolarTestScenario testScenario)
{
var calculator = new SolarCalculator(testScenario.Input);
calculator.CalculateSolarData(ref _actualSolarTime);
AssertSolarTime(testScenario.ExpectedTime, _actualSolarTime, Assert.LessOrEqual);
}
[Test, TestCaseSource(nameof(s_incorrectTestData))]
public void SolarCalculator_Time_ShouldProduceIncorrectValues(SolarTestScenario testScenario)
{
var calculator = new SolarCalculator(testScenario.Input);
calculator.CalculateSolarData(ref _actualSolarTime);
AssertSolarTime(testScenario.ExpectedTime, _actualSolarTime, Assert.Greater);
}
private void AssertSolarPosition(SolarPosition expected, SolarPosition actual, System.Action assertMethod)
{
assertMethod(
CalculateErrorPercentage(expected.AzimuthAngle, actual.AzimuthAngle),
SolarPositionErrorTolerance
);
assertMethod(
CalculateErrorPercentage(expected.Elevation, actual.Elevation),
SolarPositionErrorTolerance
);
assertMethod(
CalculateErrorPercentage(expected.CorrectedElevation, actual.CorrectedElevation),
SolarPositionErrorTolerance
);
}
private void AssertSolarTime(SolarTime expected, SolarTime actual, System.Action assertMethod)
{
assertMethod(
CalculateErrorPercentage(
(float)System.TimeSpan.Parse(expected.SolarNoon).TotalSeconds,
(float)System.TimeSpan.Parse(actual.SolarNoon).TotalSeconds
),
SolarTimeErrorTolerance
);
assertMethod(
CalculateErrorPercentage(
(float)System.TimeSpan.Parse(expected.SunriseTime).TotalSeconds,
(float)System.TimeSpan.Parse(actual.SunriseTime).TotalSeconds
),
SolarTimeErrorTolerance
);
assertMethod(
CalculateErrorPercentage(
(float)System.TimeSpan.Parse(expected.SunsetTime).TotalSeconds,
(float)System.TimeSpan.Parse(actual.SunsetTime).TotalSeconds
),
SolarTimeErrorTolerance
);
assertMethod(
CalculateErrorPercentage(
expected.SunlightDurationAsMinutes,
actual.SunlightDurationAsMinutes
),
SolarTimeErrorTolerance
);
}
private static float CalculateErrorPercentage(float expectedValue, float actualValue)
{
return Mathf.Abs((actualValue - expectedValue) / expectedValue) * 100.0f;
}
}
}