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 ambient longwave model") {
31 sp.setAtmosphericConditions(101325.f, 290.f, 0.5f, 0.02f);
34 DOCTEST_CHECK_NOTHROW(LW = sp.getAmbientLongwaveFlux());
36 DOCTEST_CHECK(doctest::Approx(310.03192f).epsilon(1e-6f) == LW);
39TEST_CASE(
"SolarPosition sunrise and sunset") {
45 DOCTEST_CHECK_NOTHROW(sunrise = sp.getSunriseTime());
47 DOCTEST_CHECK_NOTHROW(sunset = sp.getSunsetTime());
49 DOCTEST_CHECK(!(sunrise.
hour == 0 && sunrise.
minute == 0));
50 DOCTEST_CHECK(!(sunset.
hour == 0 && sunset.
minute == 0));
53TEST_CASE(
"SolarPosition sun direction vector") {
59 DOCTEST_CHECK_NOTHROW(dir = sp.getSunDirectionVector());
60 DOCTEST_CHECK(dir.
x != 0.f);
61 DOCTEST_CHECK(dir.
y != 0.f);
62 DOCTEST_CHECK(dir.
z != 0.f);
65TEST_CASE(
"SolarPosition sun direction spherical") {
71 DOCTEST_CHECK_NOTHROW(dir = sp.getSunDirectionSpherical());
73 DOCTEST_CHECK(dir.
azimuth > 0.f);
76TEST_CASE(
"SolarPosition flux and fractions") {
81 sp.setAtmosphericConditions(101325.f, 300.f, 0.5f, 0.02f);
84 DOCTEST_CHECK_NOTHROW(flux = sp.getSolarFlux());
85 DOCTEST_CHECK(flux > 0.f);
87 float diffuse_fraction;
88 DOCTEST_CHECK_NOTHROW(diffuse_fraction = sp.getDiffuseFraction());
89 DOCTEST_CHECK(diffuse_fraction >= 0.f);
90 DOCTEST_CHECK(diffuse_fraction <= 1.f);
93 DOCTEST_CHECK_NOTHROW(flux_par = sp.getSolarFluxPAR());
94 DOCTEST_CHECK(flux_par > 0.f);
97 DOCTEST_CHECK_NOTHROW(flux_nir = sp.getSolarFluxNIR());
98 DOCTEST_CHECK(flux_nir > 0.f);
101TEST_CASE(
"SolarPosition elevation, zenith, azimuth") {
106 DOCTEST_CHECK_NOTHROW(elevation = sp.getSunElevation());
107 DOCTEST_CHECK(elevation >= 0.f);
108 DOCTEST_CHECK(elevation <=
M_PI / 2.f);
111 DOCTEST_CHECK_NOTHROW(zenith = sp.getSunZenith());
112 DOCTEST_CHECK(zenith >= 0.f);
113 DOCTEST_CHECK(zenith <=
M_PI);
116 DOCTEST_CHECK_NOTHROW(azimuth = sp.getSunAzimuth());
117 DOCTEST_CHECK(azimuth >= 0.f);
118 DOCTEST_CHECK(azimuth <= 2.f *
M_PI);
121TEST_CASE(
"SolarPosition turbidity calibration") {
124 std::string label =
"test_flux_timeseries";
131 std::string captured_warnings;
134 DOCTEST_CHECK_NOTHROW(turbidity = sp.calibrateTurbidityFromTimeseries(label));
138 DOCTEST_CHECK(turbidity > 0.f);
145TEST_CASE(
"SolarPosition invalid lat/long") {
148 bool had_output_1, had_output_2;
158 DOCTEST_CHECK(had_output_1);
159 DOCTEST_CHECK(had_output_2);
162TEST_CASE(
"SolarPosition invalid solar angle") {
167 sp.setAtmosphericConditions(101325.f, 300.f, 0.5f, 0.02f);
172 DOCTEST_CHECK_NOTHROW(flux = sp.getSolarFlux());
173 DOCTEST_CHECK(flux == 0.f);
177TEST_CASE(
"SolarPosition solor position overridden") {
184 DOCTEST_CHECK_NOTHROW(elevation = sp.getSunElevation());
185 DOCTEST_CHECK(elevation >= 0.f);
186 DOCTEST_CHECK(elevation <=
M_PI / 2.f);
189 DOCTEST_CHECK_NOTHROW(zenith = sp.getSunZenith());
190 DOCTEST_CHECK(zenith >= 0.f);
191 DOCTEST_CHECK(zenith <=
M_PI);
194 DOCTEST_CHECK_NOTHROW(azimuth = sp.getSunAzimuth());
195 DOCTEST_CHECK(azimuth >= 0.f);
196 DOCTEST_CHECK(azimuth <= 2.f *
M_PI);
199 DOCTEST_CHECK_NOTHROW(sun_vector = sp.getSunDirectionVector());
202 DOCTEST_CHECK_NOTHROW(sun_spherical = sp.getSunDirectionSpherical());
205TEST_CASE(
"SolarPosition cloud calibration") {
215 sp.setAtmosphericConditions(101325.f, 300.f, 0.5f, 0.02f);
217 DOCTEST_CHECK_NOTHROW(sp.enableCloudCalibration(
"net_radiation"));
220 DOCTEST_CHECK_NOTHROW(flux = sp.getSolarFlux());
221 DOCTEST_CHECK(flux > 0.f);
223 float diffuse_fraction;
224 DOCTEST_CHECK_NOTHROW(diffuse_fraction = sp.getDiffuseFraction());
225 DOCTEST_CHECK(diffuse_fraction >= 0.f);
226 DOCTEST_CHECK(diffuse_fraction <= 1.f);
229 DOCTEST_CHECK_NOTHROW(flux_par = sp.getSolarFluxPAR());
230 DOCTEST_CHECK(flux_par > 0.f);
233 DOCTEST_CHECK_NOTHROW(flux_nir = sp.getSolarFluxNIR());
234 DOCTEST_CHECK(flux_nir > 0.f);
236 DOCTEST_CHECK_NOTHROW(sp.disableCloudCalibration());
240 DOCTEST_CHECK_THROWS_AS(sp.enableCloudCalibration(
"non_existent_timeseries"), std::runtime_error);
244TEST_CASE(
"SolarPosition turbidity calculation") {
254 DOCTEST_CHECK_NOTHROW(turbidity = sp.calibrateTurbidityFromTimeseries(
"net_radiation"));
256 DOCTEST_CHECK(turbidity > 0.f);
260 DOCTEST_CHECK_THROWS_AS(turbidity = sp.calibrateTurbidityFromTimeseries(
"non_existent_timeseries"), std::runtime_error);
264TEST_CASE(
"SolarPosition setAtmosphericConditions valid inputs") {
269 DOCTEST_CHECK_NOTHROW(sp.setAtmosphericConditions(101325.f, 300.f, 0.5f, 0.02f));
272 float pressure, temperature, humidity, turbidity;
273 DOCTEST_CHECK_NOTHROW(context_s.
getGlobalData(
"atmosphere_pressure_Pa", pressure));
274 DOCTEST_CHECK_NOTHROW(context_s.
getGlobalData(
"atmosphere_temperature_K", temperature));
275 DOCTEST_CHECK_NOTHROW(context_s.
getGlobalData(
"atmosphere_humidity_rel", humidity));
276 DOCTEST_CHECK_NOTHROW(context_s.
getGlobalData(
"atmosphere_turbidity", turbidity));
278 DOCTEST_CHECK(doctest::Approx(101325.f).epsilon(1e-6f) == pressure);
279 DOCTEST_CHECK(doctest::Approx(300.f).epsilon(1e-6f) == temperature);
280 DOCTEST_CHECK(doctest::Approx(0.5f).epsilon(1e-6f) == humidity);
281 DOCTEST_CHECK(doctest::Approx(0.02f).epsilon(1e-6f) == turbidity);
284TEST_CASE(
"SolarPosition setAtmosphericConditions validation") {
289 DOCTEST_CHECK_THROWS_AS(sp.setAtmosphericConditions(-1000.f, 300.f, 0.5f, 0.02f), std::runtime_error);
292 DOCTEST_CHECK_THROWS_AS(sp.setAtmosphericConditions(0.f, 300.f, 0.5f, 0.02f), std::runtime_error);
295 DOCTEST_CHECK_THROWS_AS(sp.setAtmosphericConditions(101325.f, -10.f, 0.5f, 0.02f), std::runtime_error);
298 DOCTEST_CHECK_THROWS_AS(sp.setAtmosphericConditions(101325.f, 0.f, 0.5f, 0.02f), std::runtime_error);
301 DOCTEST_CHECK_THROWS_AS(sp.setAtmosphericConditions(101325.f, 300.f, -0.1f, 0.02f), std::runtime_error);
304 DOCTEST_CHECK_THROWS_AS(sp.setAtmosphericConditions(101325.f, 300.f, 1.5f, 0.02f), std::runtime_error);
307 DOCTEST_CHECK_THROWS_AS(sp.setAtmosphericConditions(101325.f, 300.f, 0.5f, -0.01f), std::runtime_error);
310 DOCTEST_CHECK_NOTHROW(sp.setAtmosphericConditions(0.001f, 0.001f, 0.f, 0.f));
311 DOCTEST_CHECK_NOTHROW(sp.setAtmosphericConditions(200000.f, 400.f, 1.f, 1.f));
314TEST_CASE(
"SolarPosition getAtmosphericConditions retrieval") {
319 sp.setAtmosphericConditions(98000.f, 295.f, 0.65f, 0.05f);
322 float pressure, temperature, humidity, turbidity;
323 DOCTEST_CHECK_NOTHROW(sp.getAtmosphericConditions(pressure, temperature, humidity, turbidity));
326 DOCTEST_CHECK(doctest::Approx(98000.f).epsilon(1e-6f) == pressure);
327 DOCTEST_CHECK(doctest::Approx(295.f).epsilon(1e-6f) == temperature);
328 DOCTEST_CHECK(doctest::Approx(0.65f).epsilon(1e-6f) == humidity);
329 DOCTEST_CHECK(doctest::Approx(0.05f).epsilon(1e-6f) == turbidity);
332TEST_CASE(
"SolarPosition getAtmosphericConditions defaults") {
337 float pressure, temperature, humidity, turbidity;
341 sp.getAtmosphericConditions(pressure, temperature, humidity, turbidity);
346 DOCTEST_CHECK(had_warning);
349 DOCTEST_CHECK(doctest::Approx(101325.f).epsilon(1e-6f) == pressure);
350 DOCTEST_CHECK(doctest::Approx(300.f).epsilon(1e-6f) == temperature);
351 DOCTEST_CHECK(doctest::Approx(0.5f).epsilon(1e-6f) == humidity);
352 DOCTEST_CHECK(doctest::Approx(0.02f).epsilon(1e-6f) == turbidity);
355TEST_CASE(
"SolarPosition parameter-free flux methods") {
363 sp.setAtmosphericConditions(101325.f, 300.f, 0.5f, 0.02f);
367 DOCTEST_CHECK_NOTHROW(flux = sp.getSolarFlux());
368 DOCTEST_CHECK(flux > 0.f);
372 DOCTEST_CHECK_NOTHROW(flux_par = sp.getSolarFluxPAR());
373 DOCTEST_CHECK(flux_par > 0.f);
377 DOCTEST_CHECK_NOTHROW(flux_nir = sp.getSolarFluxNIR());
378 DOCTEST_CHECK(flux_nir > 0.f);
381 float diffuse_fraction;
382 DOCTEST_CHECK_NOTHROW(diffuse_fraction = sp.getDiffuseFraction());
383 DOCTEST_CHECK(diffuse_fraction >= 0.f);
384 DOCTEST_CHECK(diffuse_fraction <= 1.f);
388 DOCTEST_CHECK_NOTHROW(lw_flux = sp.getAmbientLongwaveFlux());
389 DOCTEST_CHECK(lw_flux > 0.f);
392TEST_CASE(
"SolarPosition parameter-free methods with defaults") {
402 DOCTEST_CHECK_NOTHROW(flux = sp.getSolarFlux());
403 DOCTEST_CHECK(flux > 0.f);
406 float pressure, temperature, humidity, turbidity;
409 sp.getAtmosphericConditions(pressure, temperature, humidity, turbidity);
411 DOCTEST_CHECK(doctest::Approx(101325.f).epsilon(1e-6f) == pressure);
412 DOCTEST_CHECK(doctest::Approx(300.f).epsilon(1e-6f) == temperature);
413 DOCTEST_CHECK(doctest::Approx(0.5f).epsilon(1e-6f) == humidity);
414 DOCTEST_CHECK(doctest::Approx(0.02f).epsilon(1e-6f) == turbidity);
417TEST_CASE(
"SSolar-GOA spectral irradiance") {
424 sp.setAtmosphericConditions(87700.f, 298.f, 0.5f, 0.026f);
426 DOCTEST_CHECK_NOTHROW(sp.calculateGlobalSolarSpectrum(
"global_test"));
427 DOCTEST_CHECK_NOTHROW(sp.calculateDirectSolarSpectrum(
"direct_test"));
428 DOCTEST_CHECK_NOTHROW(sp.calculateDiffuseSolarSpectrum(
"diffuse_test"));
430 std::vector<vec2> global_spectrum, direct_spectrum, diffuse_spectrum;
431 DOCTEST_CHECK_NOTHROW(context_s.
getGlobalData(
"global_test", global_spectrum));
432 DOCTEST_CHECK_NOTHROW(context_s.
getGlobalData(
"direct_test", direct_spectrum));
433 DOCTEST_CHECK_NOTHROW(context_s.
getGlobalData(
"diffuse_test", diffuse_spectrum));
435 DOCTEST_CHECK(global_spectrum.size() == 2301);
436 DOCTEST_CHECK(direct_spectrum.size() == 2301);
437 DOCTEST_CHECK(diffuse_spectrum.size() == 2301);
439 DOCTEST_CHECK(doctest::Approx(300.f).epsilon(0.1f) == global_spectrum.front().x);
440 DOCTEST_CHECK(doctest::Approx(2600.f).epsilon(0.1f) == global_spectrum.back().x);
442 for (
const auto &point: global_spectrum) {
443 DOCTEST_CHECK(point.y >= 0.f);
444 DOCTEST_CHECK(std::isfinite(point.y));
448 float par_flux = 0.f;
449 for (
size_t i = 1; i < global_spectrum.size(); ++i) {
450 float wl = global_spectrum[i].x;
451 if (wl >= 400.f && wl <= 700.f) {
452 float dw = global_spectrum[i].x - global_spectrum[i - 1].x;
453 float avg_irr = 0.5f * (global_spectrum[i].y + global_spectrum[i - 1].y);
454 par_flux += avg_irr * dw;
457 DOCTEST_CHECK(par_flux > 400.f);
458 DOCTEST_CHECK(par_flux < 500.f);
461TEST_CASE(
"SSolar-GOA spectral resolution") {
468 sp.setAtmosphericConditions(87700.f, 298.f, 0.5f, 0.026f);
471 DOCTEST_CHECK_NOTHROW(sp.calculateGlobalSolarSpectrum(
"res_1nm", 1.0f));
472 std::vector<vec2> spectrum_1nm;
473 DOCTEST_CHECK_NOTHROW(context_s.
getGlobalData(
"res_1nm", spectrum_1nm));
474 DOCTEST_CHECK(spectrum_1nm.size() == 2301);
477 DOCTEST_CHECK_NOTHROW(sp.calculateGlobalSolarSpectrum(
"res_10nm", 10.0f));
478 std::vector<vec2> spectrum_10nm;
479 DOCTEST_CHECK_NOTHROW(context_s.
getGlobalData(
"res_10nm", spectrum_10nm));
480 DOCTEST_CHECK(spectrum_10nm.size() == 231);
483 DOCTEST_CHECK_NOTHROW(sp.calculateGlobalSolarSpectrum(
"res_50nm", 50.0f));
484 std::vector<vec2> spectrum_50nm;
485 DOCTEST_CHECK_NOTHROW(context_s.
getGlobalData(
"res_50nm", spectrum_50nm));
486 DOCTEST_CHECK(spectrum_50nm.size() == 47);
489 DOCTEST_CHECK(doctest::Approx(300.f).epsilon(0.5f) == spectrum_10nm.front().x);
490 DOCTEST_CHECK(doctest::Approx(310.f).epsilon(0.5f) == spectrum_10nm[1].x);
493 for (
const auto &point: spectrum_10nm) {
494 DOCTEST_CHECK(point.y >= 0.f);
495 DOCTEST_CHECK(std::isfinite(point.y));
499TEST_CASE(
"SSolar-GOA validation against Python reference") {
506 sp.setAtmosphericConditions(87700.f, 298.f, 0.5f, 0.026f);
508 DOCTEST_CHECK_NOTHROW(sp.calculateGlobalSolarSpectrum(
"validation"));
510 std::vector<vec2> global_spectrum;
511 DOCTEST_CHECK_NOTHROW(context_s.
getGlobalData(
"validation", global_spectrum));
514 std::ifstream ref_file(
"plugins/solarposition/tests/validate_reference_global.txt");
515 if (ref_file.is_open()) {
516 std::string header_line;
517 std::getline(ref_file, header_line);
519 std::vector<float> ref_wavelengths, ref_irradiances;
521 while (std::getline(ref_file, line)) {
522 if (line.empty() || line[0] ==
'#')
525 std::istringstream iss(line);
527 if (iss >> wl >> irr) {
528 ref_wavelengths.push_back(wl);
529 ref_irradiances.push_back(irr);
534 DOCTEST_CHECK(ref_wavelengths.size() == 2301);
536 float max_rel_error = 0.f;
537 float sum_sq_error = 0.f;
538 size_t n_compared = 0;
540 for (
size_t i = 0; i < std::min(global_spectrum.size(), ref_wavelengths.size()); ++i) {
541 DOCTEST_CHECK(doctest::Approx(ref_wavelengths[i]).epsilon(1e-6f) == global_spectrum[i].x);
543 float cpp_irr = global_spectrum[i].y;
544 float ref_irr = ref_irradiances[i];
545 float abs_error = std::fabs(cpp_irr - ref_irr);
546 float rel_error = abs_error / (ref_irr + 1e-10f);
548 max_rel_error = std::max(max_rel_error, rel_error);
549 sum_sq_error += abs_error * abs_error;
553 float rms_error = std::sqrt(sum_sq_error / n_compared);
555 DOCTEST_CHECK(max_rel_error < 0.01f);
556 DOCTEST_CHECK(rms_error < 0.01f);
559 DOCTEST_WARN(
"Python reference file not found - run validate_detailed.py to enable detailed validation");
564 return helios::runDoctestWithValidation(argc, argv);
569TEST_CASE(
"SolarPosition - Prague model initialization") {
573 DOCTEST_CHECK(!solar.isPragueSkyModelEnabled());
574 DOCTEST_CHECK_NOTHROW(solar.enablePragueSkyModel());
575 DOCTEST_CHECK(solar.isPragueSkyModelEnabled());
578TEST_CASE(
"SolarPosition - Prague angular parameter fitting - clear sky") {
581 solar.enablePragueSkyModel();
583 solar.setAtmosphericConditions(101325.f, 300.f, 0.5f, 0.05f);
584 solar.updatePragueSkyModel();
586 std::vector<float> params;
587 DOCTEST_CHECK_NOTHROW(context.
getGlobalData(
"prague_sky_spectral_params", params));
589 DOCTEST_CHECK(params.size() == 225 * 6);
593 float wavelength = params[idx + 0];
594 float L_zenith = params[idx + 1];
595 float circ_str = params[idx + 2];
596 float circ_width = params[idx + 3];
597 float horiz_bright = params[idx + 4];
598 float norm = params[idx + 5];
600 DOCTEST_CHECK(wavelength == doctest::Approx(550.0f).epsilon(1.0f));
601 DOCTEST_CHECK(L_zenith > 0.0f);
602 DOCTEST_CHECK(L_zenith < 0.5f);
603 DOCTEST_CHECK(circ_str >= 0.0f);
604 DOCTEST_CHECK(circ_str <= 20.0f);
605 DOCTEST_CHECK(circ_width >= 5.0f);
606 DOCTEST_CHECK(circ_width <= 60.0f);
607 DOCTEST_CHECK(horiz_bright >= 1.0f);
608 DOCTEST_CHECK(horiz_bright < 5.0f);
609 DOCTEST_CHECK(norm > 0.0f);
610 DOCTEST_CHECK(norm < 2.0f);
614 DOCTEST_CHECK_NOTHROW(context.
getGlobalData(
"prague_sky_valid", valid));
615 DOCTEST_CHECK(valid == 1);
618TEST_CASE(
"SolarPosition - Prague lazy evaluation") {
621 solar.enablePragueSkyModel();
623 solar.setAtmosphericConditions(101325.f, 300.f, 0.5f, 0.1f);
624 solar.updatePragueSkyModel();
627 solar.setAtmosphericConditions(101325.f, 300.f, 0.5f, 0.101f);
628 DOCTEST_CHECK(!solar.pragueSkyModelNeedsUpdate(0.33f));
631 solar.setAtmosphericConditions(101325.f, 300.f, 0.5f, 0.15f);
632 DOCTEST_CHECK(solar.pragueSkyModelNeedsUpdate(0.33f));
636 DOCTEST_CHECK(solar.pragueSkyModelNeedsUpdate(0.33f));
640 solar.setAtmosphericConditions(101325.f, 300.f, 0.5f, 0.1f);
641 solar.updatePragueSkyModel();
642 DOCTEST_CHECK(solar.pragueSkyModelNeedsUpdate(0.5f));
645TEST_CASE(
"SolarPosition - Prague performance benchmark") {
648 solar.enablePragueSkyModel();
650 solar.setAtmosphericConditions(101325.f, 300.f, 0.5f, 0.1f);
652 auto start = std::chrono::high_resolution_clock::now();
653 solar.updatePragueSkyModel();
654 auto end = std::chrono::high_resolution_clock::now();
656 auto duration_ms = std::chrono::duration_cast<std::chrono::milliseconds>(end - start);
658 DOCTEST_CHECK(duration_ms.count() < 10000);
661TEST_CASE(
"SolarPosition - Prague error handling") {
666 DOCTEST_CHECK_THROWS_WITH_AS(solar.updatePragueSkyModel(),
"ERROR (SolarPosition::updatePragueSkyModel): Prague model not enabled. Call enablePragueSkyModel() first.", std::runtime_error);
669 DOCTEST_CHECK(!solar.pragueSkyModelNeedsUpdate(0.33f));