3#define DOCTEST_CONFIG_IMPLEMENT
5#include "doctest_utils.h"
9TEST_CASE(
"SolarPosition sun position Boulder") {
16 float theta_s = sp.getSunElevation() * 180.f /
M_PI;
17 float phi_s = sp.getSunAzimuth() * 180.f /
M_PI;
19 DOCTEST_CHECK(std::fabs(theta_s - 29.49f) <= 10.0f);
20 DOCTEST_CHECK(std::fabs(phi_s - 154.18f) <= 5.0f);
23TEST_CASE(
"SolarPosition fractional UTC offset") {
32 const float latitude = 28.6139f;
33 const float longitude = -77.2090f;
39 float az_lo = sp_lo.getSunAzimuth();
40 float az_mid = sp_mid.getSunAzimuth();
41 float az_hi = sp_hi.getSunAzimuth();
46 DOCTEST_CHECK(std::fabs(az_mid - az_lo) > 1e-4f);
47 DOCTEST_CHECK(std::fabs(az_mid - az_hi) > 1e-4f);
48 DOCTEST_CHECK(((az_lo < az_mid && az_mid < az_hi) || (az_hi < az_mid && az_mid < az_lo)));
51TEST_CASE(
"SolarPosition ambient longwave model") {
59 sp.setAtmosphericConditions(101325.f, 290.f, 0.5f, 0.02f);
62 DOCTEST_CHECK_NOTHROW(LW = sp.getAmbientLongwaveFlux());
64 DOCTEST_CHECK(doctest::Approx(310.03192f).epsilon(1e-6f) == LW);
67TEST_CASE(
"SolarPosition sunrise and sunset") {
73 DOCTEST_CHECK_NOTHROW(sunrise = sp.getSunriseTime());
75 DOCTEST_CHECK_NOTHROW(sunset = sp.getSunsetTime());
77 DOCTEST_CHECK(!(sunrise.
hour == 0 && sunrise.
minute == 0));
78 DOCTEST_CHECK(!(sunset.
hour == 0 && sunset.
minute == 0));
81TEST_CASE(
"SolarPosition sun direction vector") {
87 DOCTEST_CHECK_NOTHROW(dir = sp.getSunDirectionVector());
88 DOCTEST_CHECK(dir.
x != 0.f);
89 DOCTEST_CHECK(dir.
y != 0.f);
90 DOCTEST_CHECK(dir.
z != 0.f);
93TEST_CASE(
"SolarPosition sun direction spherical") {
99 DOCTEST_CHECK_NOTHROW(dir = sp.getSunDirectionSpherical());
101 DOCTEST_CHECK(dir.
azimuth > 0.f);
104TEST_CASE(
"SolarPosition flux and fractions") {
109 sp.setAtmosphericConditions(101325.f, 300.f, 0.5f, 0.02f);
112 DOCTEST_CHECK_NOTHROW(flux = sp.getSolarFlux());
113 DOCTEST_CHECK(flux > 0.f);
115 float diffuse_fraction;
116 DOCTEST_CHECK_NOTHROW(diffuse_fraction = sp.getDiffuseFraction());
117 DOCTEST_CHECK(diffuse_fraction >= 0.f);
118 DOCTEST_CHECK(diffuse_fraction <= 1.f);
121 DOCTEST_CHECK_NOTHROW(flux_par = sp.getSolarFluxPAR());
122 DOCTEST_CHECK(flux_par > 0.f);
125 DOCTEST_CHECK_NOTHROW(flux_nir = sp.getSolarFluxNIR());
126 DOCTEST_CHECK(flux_nir > 0.f);
129TEST_CASE(
"SolarPosition elevation, zenith, azimuth") {
134 DOCTEST_CHECK_NOTHROW(elevation = sp.getSunElevation());
135 DOCTEST_CHECK(elevation >= 0.f);
136 DOCTEST_CHECK(elevation <= M_PI / 2.f);
139 DOCTEST_CHECK_NOTHROW(zenith = sp.getSunZenith());
140 DOCTEST_CHECK(zenith >= 0.f);
141 DOCTEST_CHECK(zenith <= M_PI);
144 DOCTEST_CHECK_NOTHROW(azimuth = sp.getSunAzimuth());
145 DOCTEST_CHECK(azimuth >= 0.f);
146 DOCTEST_CHECK(azimuth <= 2.f * M_PI);
149TEST_CASE(
"SolarPosition turbidity calibration") {
152 std::string label =
"test_flux_timeseries";
159 std::string captured_warnings;
162 DOCTEST_CHECK_NOTHROW(turbidity = sp.calibrateTurbidityFromTimeseries(label));
166 DOCTEST_CHECK(turbidity > 0.f);
173TEST_CASE(
"SolarPosition invalid lat/long") {
176 bool had_output_1, had_output_2;
186 DOCTEST_CHECK(had_output_1);
187 DOCTEST_CHECK(had_output_2);
190TEST_CASE(
"SolarPosition invalid solar angle") {
195 sp.setAtmosphericConditions(101325.f, 300.f, 0.5f, 0.02f);
200 DOCTEST_CHECK_NOTHROW(flux = sp.getSolarFlux());
201 DOCTEST_CHECK(flux == 0.f);
205TEST_CASE(
"SolarPosition solor position overridden") {
212 DOCTEST_CHECK_NOTHROW(elevation = sp.getSunElevation());
213 DOCTEST_CHECK(elevation >= 0.f);
214 DOCTEST_CHECK(elevation <= M_PI / 2.f);
217 DOCTEST_CHECK_NOTHROW(zenith = sp.getSunZenith());
218 DOCTEST_CHECK(zenith >= 0.f);
219 DOCTEST_CHECK(zenith <= M_PI);
222 DOCTEST_CHECK_NOTHROW(azimuth = sp.getSunAzimuth());
223 DOCTEST_CHECK(azimuth >= 0.f);
224 DOCTEST_CHECK(azimuth <= 2.f * M_PI);
227 DOCTEST_CHECK_NOTHROW(sun_vector = sp.getSunDirectionVector());
230 DOCTEST_CHECK_NOTHROW(sun_spherical = sp.getSunDirectionSpherical());
233TEST_CASE(
"SolarPosition cloud calibration") {
243 sp.setAtmosphericConditions(101325.f, 300.f, 0.5f, 0.02f);
245 DOCTEST_CHECK_NOTHROW(sp.enableCloudCalibration(
"net_radiation"));
248 DOCTEST_CHECK_NOTHROW(flux = sp.getSolarFlux());
249 DOCTEST_CHECK(flux > 0.f);
251 float diffuse_fraction;
252 DOCTEST_CHECK_NOTHROW(diffuse_fraction = sp.getDiffuseFraction());
253 DOCTEST_CHECK(diffuse_fraction >= 0.f);
254 DOCTEST_CHECK(diffuse_fraction <= 1.f);
257 DOCTEST_CHECK_NOTHROW(flux_par = sp.getSolarFluxPAR());
258 DOCTEST_CHECK(flux_par > 0.f);
261 DOCTEST_CHECK_NOTHROW(flux_nir = sp.getSolarFluxNIR());
262 DOCTEST_CHECK(flux_nir > 0.f);
264 DOCTEST_CHECK_NOTHROW(sp.disableCloudCalibration());
268 DOCTEST_CHECK_THROWS_AS(sp.enableCloudCalibration(
"non_existent_timeseries"), std::runtime_error);
272TEST_CASE(
"SolarPosition turbidity calculation") {
282 DOCTEST_CHECK_NOTHROW(turbidity = sp.calibrateTurbidityFromTimeseries(
"net_radiation"));
284 DOCTEST_CHECK(turbidity > 0.f);
288 DOCTEST_CHECK_THROWS_AS(turbidity = sp.calibrateTurbidityFromTimeseries(
"non_existent_timeseries"), std::runtime_error);
292TEST_CASE(
"SolarPosition setAtmosphericConditions valid inputs") {
297 DOCTEST_CHECK_NOTHROW(sp.setAtmosphericConditions(101325.f, 300.f, 0.5f, 0.02f));
300 float pressure, temperature, humidity, turbidity;
301 DOCTEST_CHECK_NOTHROW(context_s.
getGlobalData(
"atmosphere_pressure_Pa", pressure));
302 DOCTEST_CHECK_NOTHROW(context_s.
getGlobalData(
"atmosphere_temperature_K", temperature));
303 DOCTEST_CHECK_NOTHROW(context_s.
getGlobalData(
"atmosphere_humidity_rel", humidity));
304 DOCTEST_CHECK_NOTHROW(context_s.
getGlobalData(
"atmosphere_turbidity", turbidity));
306 DOCTEST_CHECK(doctest::Approx(101325.f).epsilon(1e-6f) == pressure);
307 DOCTEST_CHECK(doctest::Approx(300.f).epsilon(1e-6f) == temperature);
308 DOCTEST_CHECK(doctest::Approx(0.5f).epsilon(1e-6f) == humidity);
309 DOCTEST_CHECK(doctest::Approx(0.02f).epsilon(1e-6f) == turbidity);
312TEST_CASE(
"SolarPosition setAtmosphericConditions validation") {
317 DOCTEST_CHECK_THROWS_AS(sp.setAtmosphericConditions(-1000.f, 300.f, 0.5f, 0.02f), std::runtime_error);
320 DOCTEST_CHECK_THROWS_AS(sp.setAtmosphericConditions(0.f, 300.f, 0.5f, 0.02f), std::runtime_error);
323 DOCTEST_CHECK_THROWS_AS(sp.setAtmosphericConditions(101325.f, -10.f, 0.5f, 0.02f), std::runtime_error);
326 DOCTEST_CHECK_THROWS_AS(sp.setAtmosphericConditions(101325.f, 0.f, 0.5f, 0.02f), std::runtime_error);
329 DOCTEST_CHECK_THROWS_AS(sp.setAtmosphericConditions(101325.f, 300.f, -0.1f, 0.02f), std::runtime_error);
332 DOCTEST_CHECK_THROWS_AS(sp.setAtmosphericConditions(101325.f, 300.f, 1.5f, 0.02f), std::runtime_error);
335 DOCTEST_CHECK_THROWS_AS(sp.setAtmosphericConditions(101325.f, 300.f, 0.5f, -0.01f), std::runtime_error);
338 DOCTEST_CHECK_NOTHROW(sp.setAtmosphericConditions(0.001f, 0.001f, 0.f, 0.f));
339 DOCTEST_CHECK_NOTHROW(sp.setAtmosphericConditions(200000.f, 400.f, 1.f, 1.f));
342TEST_CASE(
"SolarPosition getAtmosphericConditions retrieval") {
347 sp.setAtmosphericConditions(98000.f, 295.f, 0.65f, 0.05f);
350 float pressure, temperature, humidity, turbidity;
351 DOCTEST_CHECK_NOTHROW(sp.getAtmosphericConditions(pressure, temperature, humidity, turbidity));
354 DOCTEST_CHECK(doctest::Approx(98000.f).epsilon(1e-6f) == pressure);
355 DOCTEST_CHECK(doctest::Approx(295.f).epsilon(1e-6f) == temperature);
356 DOCTEST_CHECK(doctest::Approx(0.65f).epsilon(1e-6f) == humidity);
357 DOCTEST_CHECK(doctest::Approx(0.05f).epsilon(1e-6f) == turbidity);
360TEST_CASE(
"SolarPosition getAtmosphericConditions defaults") {
365 float pressure, temperature, humidity, turbidity;
369 sp.getAtmosphericConditions(pressure, temperature, humidity, turbidity);
374 DOCTEST_CHECK(had_warning);
377 DOCTEST_CHECK(doctest::Approx(101325.f).epsilon(1e-6f) == pressure);
378 DOCTEST_CHECK(doctest::Approx(300.f).epsilon(1e-6f) == temperature);
379 DOCTEST_CHECK(doctest::Approx(0.5f).epsilon(1e-6f) == humidity);
380 DOCTEST_CHECK(doctest::Approx(0.02f).epsilon(1e-6f) == turbidity);
383TEST_CASE(
"SolarPosition parameter-free flux methods") {
391 sp.setAtmosphericConditions(101325.f, 300.f, 0.5f, 0.02f);
395 DOCTEST_CHECK_NOTHROW(flux = sp.getSolarFlux());
396 DOCTEST_CHECK(flux > 0.f);
400 DOCTEST_CHECK_NOTHROW(flux_par = sp.getSolarFluxPAR());
401 DOCTEST_CHECK(flux_par > 0.f);
405 DOCTEST_CHECK_NOTHROW(flux_nir = sp.getSolarFluxNIR());
406 DOCTEST_CHECK(flux_nir > 0.f);
409 float diffuse_fraction;
410 DOCTEST_CHECK_NOTHROW(diffuse_fraction = sp.getDiffuseFraction());
411 DOCTEST_CHECK(diffuse_fraction >= 0.f);
412 DOCTEST_CHECK(diffuse_fraction <= 1.f);
416 DOCTEST_CHECK_NOTHROW(lw_flux = sp.getAmbientLongwaveFlux());
417 DOCTEST_CHECK(lw_flux > 0.f);
420TEST_CASE(
"SolarPosition parameter-free methods with defaults") {
430 DOCTEST_CHECK_NOTHROW(flux = sp.getSolarFlux());
431 DOCTEST_CHECK(flux > 0.f);
434 float pressure, temperature, humidity, turbidity;
437 sp.getAtmosphericConditions(pressure, temperature, humidity, turbidity);
439 DOCTEST_CHECK(doctest::Approx(101325.f).epsilon(1e-6f) == pressure);
440 DOCTEST_CHECK(doctest::Approx(300.f).epsilon(1e-6f) == temperature);
441 DOCTEST_CHECK(doctest::Approx(0.5f).epsilon(1e-6f) == humidity);
442 DOCTEST_CHECK(doctest::Approx(0.02f).epsilon(1e-6f) == turbidity);
445TEST_CASE(
"SSolar-GOA spectral irradiance") {
452 sp.setAtmosphericConditions(87700.f, 298.f, 0.5f, 0.026f);
454 DOCTEST_CHECK_NOTHROW(sp.calculateGlobalSolarSpectrum(
"global_test"));
455 DOCTEST_CHECK_NOTHROW(sp.calculateDirectSolarSpectrum(
"direct_test"));
456 DOCTEST_CHECK_NOTHROW(sp.calculateDiffuseSolarSpectrum(
"diffuse_test"));
458 std::vector<vec2> global_spectrum, direct_spectrum, diffuse_spectrum;
459 DOCTEST_CHECK_NOTHROW(context_s.
getGlobalData(
"global_test", global_spectrum));
460 DOCTEST_CHECK_NOTHROW(context_s.
getGlobalData(
"direct_test", direct_spectrum));
461 DOCTEST_CHECK_NOTHROW(context_s.
getGlobalData(
"diffuse_test", diffuse_spectrum));
463 DOCTEST_CHECK(global_spectrum.size() == 2301);
464 DOCTEST_CHECK(direct_spectrum.size() == 2301);
465 DOCTEST_CHECK(diffuse_spectrum.size() == 2301);
467 DOCTEST_CHECK(doctest::Approx(300.f).epsilon(0.1f) == global_spectrum.front().x);
468 DOCTEST_CHECK(doctest::Approx(2600.f).epsilon(0.1f) == global_spectrum.back().x);
470 for (
const auto &point: global_spectrum) {
471 DOCTEST_CHECK(point.y >= 0.f);
472 DOCTEST_CHECK(std::isfinite(point.y));
476 float par_flux = 0.f;
477 for (
size_t i = 1; i < global_spectrum.size(); ++i) {
478 float wl = global_spectrum[i].x;
479 if (wl >= 400.f && wl <= 700.f) {
480 float dw = global_spectrum[i].x - global_spectrum[i - 1].x;
481 float avg_irr = 0.5f * (global_spectrum[i].y + global_spectrum[i - 1].y);
482 par_flux += avg_irr * dw;
485 DOCTEST_CHECK(par_flux > 400.f);
486 DOCTEST_CHECK(par_flux < 500.f);
489TEST_CASE(
"SSolar-GOA spectral resolution") {
496 sp.setAtmosphericConditions(87700.f, 298.f, 0.5f, 0.026f);
499 DOCTEST_CHECK_NOTHROW(sp.calculateGlobalSolarSpectrum(
"res_1nm", 1.0f));
500 std::vector<vec2> spectrum_1nm;
501 DOCTEST_CHECK_NOTHROW(context_s.
getGlobalData(
"res_1nm", spectrum_1nm));
502 DOCTEST_CHECK(spectrum_1nm.size() == 2301);
505 DOCTEST_CHECK_NOTHROW(sp.calculateGlobalSolarSpectrum(
"res_10nm", 10.0f));
506 std::vector<vec2> spectrum_10nm;
507 DOCTEST_CHECK_NOTHROW(context_s.
getGlobalData(
"res_10nm", spectrum_10nm));
508 DOCTEST_CHECK(spectrum_10nm.size() == 231);
511 DOCTEST_CHECK_NOTHROW(sp.calculateGlobalSolarSpectrum(
"res_50nm", 50.0f));
512 std::vector<vec2> spectrum_50nm;
513 DOCTEST_CHECK_NOTHROW(context_s.
getGlobalData(
"res_50nm", spectrum_50nm));
514 DOCTEST_CHECK(spectrum_50nm.size() == 47);
517 DOCTEST_CHECK(doctest::Approx(300.f).epsilon(0.5f) == spectrum_10nm.front().x);
518 DOCTEST_CHECK(doctest::Approx(310.f).epsilon(0.5f) == spectrum_10nm[1].x);
521 for (
const auto &point: spectrum_10nm) {
522 DOCTEST_CHECK(point.y >= 0.f);
523 DOCTEST_CHECK(std::isfinite(point.y));
527TEST_CASE(
"SSolar-GOA validation against Python reference") {
534 sp.setAtmosphericConditions(87700.f, 298.f, 0.5f, 0.026f);
536 DOCTEST_CHECK_NOTHROW(sp.calculateGlobalSolarSpectrum(
"validation"));
538 std::vector<vec2> global_spectrum;
539 DOCTEST_CHECK_NOTHROW(context_s.
getGlobalData(
"validation", global_spectrum));
542 std::ifstream ref_file(
"plugins/solarposition/tests/validate_reference_global.txt");
543 if (ref_file.is_open()) {
544 std::string header_line;
545 std::getline(ref_file, header_line);
547 std::vector<float> ref_wavelengths, ref_irradiances;
549 while (std::getline(ref_file, line)) {
550 if (line.empty() || line[0] ==
'#')
553 std::istringstream iss(line);
555 if (iss >> wl >> irr) {
556 ref_wavelengths.push_back(wl);
557 ref_irradiances.push_back(irr);
562 DOCTEST_CHECK(ref_wavelengths.size() == 2301);
564 float max_rel_error = 0.f;
565 float sum_sq_error = 0.f;
566 size_t n_compared = 0;
568 for (
size_t i = 0; i < std::min(global_spectrum.size(), ref_wavelengths.size()); ++i) {
569 DOCTEST_CHECK(doctest::Approx(ref_wavelengths[i]).epsilon(1e-6f) == global_spectrum[i].x);
571 float cpp_irr = global_spectrum[i].y;
572 float ref_irr = ref_irradiances[i];
573 float abs_error = std::fabs(cpp_irr - ref_irr);
574 float rel_error = abs_error / (ref_irr + 1e-10f);
576 max_rel_error = std::max(max_rel_error, rel_error);
577 sum_sq_error += abs_error * abs_error;
581 float rms_error = std::sqrt(sum_sq_error / n_compared);
583 DOCTEST_CHECK(max_rel_error < 0.01f);
584 DOCTEST_CHECK(rms_error < 0.01f);
587 DOCTEST_WARN(
"Python reference file not found - run validate_detailed.py to enable detailed validation");
592 return helios::runDoctestWithValidation(argc, argv);
597TEST_CASE(
"SolarPosition - Prague model initialization") {
601 DOCTEST_CHECK(!solar.isPragueSkyModelEnabled());
602 DOCTEST_CHECK_NOTHROW(solar.enablePragueSkyModel());
603 DOCTEST_CHECK(solar.isPragueSkyModelEnabled());
606TEST_CASE(
"SolarPosition - Prague angular parameter fitting - clear sky") {
609 solar.enablePragueSkyModel();
611 solar.setAtmosphericConditions(101325.f, 300.f, 0.5f, 0.05f);
612 solar.updatePragueSkyModel();
614 std::vector<float> params;
615 DOCTEST_CHECK_NOTHROW(
context.getGlobalData(
"prague_sky_spectral_params", params));
617 DOCTEST_CHECK(params.size() == 225 * 6);
621 float wavelength = params[idx + 0];
622 float L_zenith = params[idx + 1];
623 float circ_str = params[idx + 2];
624 float circ_width = params[idx + 3];
625 float horiz_bright = params[idx + 4];
626 float norm = params[idx + 5];
628 DOCTEST_CHECK(wavelength == doctest::Approx(550.0f).epsilon(1.0f));
629 DOCTEST_CHECK(L_zenith > 0.0f);
630 DOCTEST_CHECK(L_zenith < 0.5f);
631 DOCTEST_CHECK(circ_str >= 0.0f);
632 DOCTEST_CHECK(circ_str <= 20.0f);
633 DOCTEST_CHECK(circ_width >= 5.0f);
634 DOCTEST_CHECK(circ_width <= 60.0f);
635 DOCTEST_CHECK(horiz_bright >= 1.0f);
636 DOCTEST_CHECK(horiz_bright < 5.0f);
637 DOCTEST_CHECK(norm > 0.0f);
638 DOCTEST_CHECK(norm < 2.0f);
642 DOCTEST_CHECK_NOTHROW(
context.getGlobalData(
"prague_sky_valid", valid));
643 DOCTEST_CHECK(valid == 1);
646TEST_CASE(
"SolarPosition - Prague lazy evaluation") {
649 solar.enablePragueSkyModel();
651 solar.setAtmosphericConditions(101325.f, 300.f, 0.5f, 0.1f);
652 solar.updatePragueSkyModel();
655 solar.setAtmosphericConditions(101325.f, 300.f, 0.5f, 0.101f);
656 DOCTEST_CHECK(!solar.pragueSkyModelNeedsUpdate(0.33f));
659 solar.setAtmosphericConditions(101325.f, 300.f, 0.5f, 0.15f);
660 DOCTEST_CHECK(solar.pragueSkyModelNeedsUpdate(0.33f));
664 DOCTEST_CHECK(solar.pragueSkyModelNeedsUpdate(0.33f));
668 solar.setAtmosphericConditions(101325.f, 300.f, 0.5f, 0.1f);
669 solar.updatePragueSkyModel();
670 DOCTEST_CHECK(solar.pragueSkyModelNeedsUpdate(0.5f));
673TEST_CASE(
"SolarPosition - Prague performance benchmark") {
676 solar.enablePragueSkyModel();
678 solar.setAtmosphericConditions(101325.f, 300.f, 0.5f, 0.1f);
680 auto start = std::chrono::high_resolution_clock::now();
681 solar.updatePragueSkyModel();
682 auto end = std::chrono::high_resolution_clock::now();
684 auto duration_ms = std::chrono::duration_cast<std::chrono::milliseconds>(end - start);
686 DOCTEST_CHECK(duration_ms.count() < 10000);
689TEST_CASE(
"SolarPosition - Prague error handling") {
694 DOCTEST_CHECK_THROWS_WITH_AS(solar.updatePragueSkyModel(),
"ERROR (SolarPosition::updatePragueSkyModel): Prague model not enabled. Call enablePragueSkyModel() first.", std::runtime_error);
697 DOCTEST_CHECK(!solar.pragueSkyModelNeedsUpdate(0.33f));