Repository navigation
Conversation
…ffect SET is discontinuous at the critical skin wettedness cap, so SolveRoot, which converges on |f(x)|, failed on 42 timesteps of 1ZoneUncontrolled_Win_ASH55_Thermal_Comfort. Bisecting on the bracket width matches boost::math::tools::bisect, and the results are identical to develop. cf: - #11867 (comment)
…mperature (SET) ``` [ RUN ] EnergyPlusFixture.ThermalComfort_CalcStandardEffectiveTemp_ContinuousAtCriticalWettedness /media/DataExt4/Software/Others/EnergyPlus/tst/EnergyPlus/unit/ThermalComfort.unit.cc:1133: Failure Expected: (maxStep) < (0.01), actual: 0.486617 vs 0.01 /media/DataExt4/Software/Others/EnergyPlus/tst/EnergyPlus/unit/ThermalComfort.unit.cc:1134: Failure Expected: (minStep) > (0.0), actual: -0.486617 vs 0 [ FAILED ] EnergyPlusFixture.ThermalComfort_CalcStandardEffectiveTemp_ContinuousAtCriticalWettedness (607 ms) ```
…ness is capped at EvapEff SkinWetSweat = EvapEff / 0.94 is not the inverse of SkinWetTot = 0.06 + 0.94 * SkinWetSweat, so at the cap the skin evaporated as if its wettedness were EvapEff + 0.06. The cap then switched on and off between the minutes of the simulation, making SET jump by up to 0.5 C. Use (SkinWetTot - 0.06) / 0.94, as in Gagge et al. (1986): prsw=(eveff-.06)/.94. Same fix as: - CenterForTheBuiltEnvironment/comfort_tool#103 - pythermalcomfort/pythermalcomfort#490
SolveRoot bounds the SET mismatch, not the cooling effect itself, so Acc = 0.01 allowed errors up to 0.017 C where SET rises slowly with temperature (vs 0.003 C for the previous bisection). With 0.001, the max error on the annual ASH55 inputs is 0.0017 C for 4.4 root function evaluations per call on average, vs 15 with bisection.
| if (s_tc->SkinWetTot >= EvapEff) { | ||
| s_tc->SkinWetTot = EvapEff; | ||
| s_tc->SkinWetSweat = EvapEff / 0.94; | ||
| // Invert SkinWetTot = SkinWetSweat + SkinWetDiff = SkinWetSweat + (1 - SkinWetSweat) * 0.06 = 0.06 + 0.94 * SkinWetSweat, so | ||
| // that EvapHeatLoss below equals SkinWetTot * EvapHeatLossMax (Gagge et al. 1986: prsw=(eveff-.06)/.94) | ||
| s_tc->SkinWetSweat = (s_tc->SkinWetTot - 0.06) / 0.94; |
There was a problem hiding this comment.
That's the discontinuity fix.
| Real64 constexpr tempAccuracy = 0.001; // Accuracy of the SET match [deltaC] | ||
| int constexpr MaxIte = 100; | ||
| Real64 constexpr lowerBound = 0.0; | ||
| Real64 constexpr upperBound = 50.0; | ||
| int SolFla = 0; | ||
|
|
||
| // We have yet another solver? | ||
| try { | ||
| std::pair<Real64, Real64> solverResult = boost::math::tools::bisect(ce_root_function, lowerBound, upperBound, ce_root_termination); | ||
| CoolingEffect = (solverResult.first + solverResult.second) / 2; | ||
| } catch (const std::exception &e) { | ||
| General::SolveRoot(state, tempAccuracy, MaxIte, SolFla, CoolingEffect, ce_root_function, lowerBound, upperBound); | ||
| if (SolFla < 0) { |
There was a problem hiding this comment.
Now that SET is continuous, we can go back to General::SolveRoot instead of the bisection.
I first used tempAccuracy = 0.01 °C, but then lowered it to 0.001. SolveRoot stops when the SET mismatch |f(x)| is below the tolerance, not when the bracket on the cooling effect is small. Where SET rises slowly with temperature, a 0.01 tolerance on SET can therefore allow a larger error on the cooling effect.
I instrumented a standalone copy of the solver and ran it on the 38016 (air temperature, MRT, RH, air velocity, met, clo) inputs from the annual ASH55 run. I compared each result with the exact root:
| Solver | Max error [°C] | Mean error [°C] | Root function evaluations per call |
|---|---|---|---|
| Bisection (previous) | 0.0031 | 0.0015 | 15 (always) |
| SolveRoot, Acc = 0.01 | 0.0169 | 0.0012 | 3.98 |
| SolveRoot, Acc = 0.001 | 0.0017 | 0.0001 | 4.39 |
With 0.001, SolveRoot is more accurate than the bisection. It still needs about 3.4x fewer evaluations, and each evaluation is a full SET calculation. The bisection always does 15, because it stops on bracket width: two endpoints, then halvings until the [0, 50] bracket is under 0.01.
The annual E+ run (fix + bisection against fix + SolveRoot) shows the same thing. The max Cooling Effect diff is 0.018 °C with Acc = 0.01 and 0.004 °C with Acc = 0.001.
There was a problem hiding this comment.
Improved accuracy with fewer iterations = winning
(I believe the note above where you state "3.4x" should probably be 4.3x based on the table.)
| TEST_F(EnergyPlusFixture, ThermalComfort_CalcStandardEffectiveTemp_ContinuousAtCriticalWettedness) | ||
| { | ||
| // When the skin wettedness reaches EvapEff, SkinWetSweat must invert SkinWetTot = 0.06 + 0.94 * SkinWetSweat, i.e. | ||
| // (SkinWetTot - 0.06) / 0.94 as in Gagge et al. (1986). SET should then change smoothly and monotonically with temperature. | ||
| state->dataEnvrn->OutBaroPress = 101325.0; | ||
| Real64 constexpr RelHum = 0.7; | ||
| Real64 constexpr AirVel = 0.1; | ||
| Real64 constexpr ActMet = 1.1; | ||
| Real64 constexpr CloUnit = 0.5; | ||
| Real64 constexpr WorkEff = 0.0; | ||
|
|
||
| Real64 maxStep = 0.0; | ||
| Real64 minStep = std::numeric_limits<Real64>::max(); | ||
| Real64 previous = CalcStandardEffectiveTemp(*state, 32.5, 32.5, RelHum, AirVel, ActMet, CloUnit, WorkEff); | ||
| for (int i = 1; i <= 1500; ++i) { | ||
| Real64 const temp = 32.5 + i * 0.001; | ||
| Real64 const current = CalcStandardEffectiveTemp(*state, temp, temp, RelHum, AirVel, ActMet, CloUnit, WorkEff); | ||
| maxStep = std::max(maxStep, std::abs(current - previous)); | ||
| minStep = std::min(minStep, current - previous); | ||
| previous = current; | ||
| } | ||
| EXPECT_LT(maxStep, 0.01); | ||
| EXPECT_GT(minStep, 0.0); // monotonic, so the cooling effect has a single root | ||
| } |
There was a problem hiding this comment.
Before fix
[ RUN ] EnergyPlusFixture.ThermalComfort_CalcStandardEffectiveTemp_ContinuousAtCriticalWettedness
/media/DataExt4/Software/Others/EnergyPlus/tst/EnergyPlus/unit/ThermalComfort.unit.cc:1133: Failure
Expected: (maxStep) < (0.01), actual: 0.486617 vs 0.01
/media/DataExt4/Software/Others/EnergyPlus/tst/EnergyPlus/unit/ThermalComfort.unit.cc:1134: Failure
Expected: (minStep) > (0.0), actual: -0.486617 vs 0
[ FAILED ] EnergyPlusFixture.ThermalComfort_CalcStandardEffectiveTemp_ContinuousAtCriticalWettedness (607 ms)
There was a problem hiding this comment.
Looks good. There's a few lingering test values in here where @Myoldmopar was scratching his head previously.
Might be worth cleaning these up while we're in here.
|
|
| // Third Party Headers | ||
| #include <boost/math/tools/roots.hpp> |
| Real64 CalcStandardEffectiveTemp( | ||
| EnergyPlusData &state, Real64 AirTemp, Real64 RadTemp, Real64 RelHum, Real64 AirVel, Real64 ActMet, Real64 CloUnit, Real64 WorkEff) | ||
| { | ||
| auto &s_tc = state.dataThermalComforts; |
There was a problem hiding this comment.
More comfortable to read.
| if (s_tc->SkinWetTot >= EvapEff) { | ||
| s_tc->SkinWetTot = EvapEff; | ||
| s_tc->SkinWetSweat = EvapEff / 0.94; | ||
| // Invert SkinWetTot = SkinWetSweat + SkinWetDiff = SkinWetSweat + (1 - SkinWetSweat) * 0.06 = 0.06 + 0.94 * SkinWetSweat, so | ||
| // that EvapHeatLoss below equals SkinWetTot * EvapHeatLossMax (Gagge et al. 1986: prsw=(eveff-.06)/.94) | ||
| s_tc->SkinWetSweat = (s_tc->SkinWetTot - 0.06) / 0.94; |
| Real64 constexpr tempAccuracy = 0.001; // Accuracy of the SET match [deltaC] | ||
| int constexpr MaxIte = 100; | ||
| Real64 constexpr lowerBound = 0.0; | ||
| Real64 constexpr upperBound = 50.0; | ||
| int SolFla = 0; | ||
|
|
||
| // We have yet another solver? | ||
| try { | ||
| std::pair<Real64, Real64> solverResult = boost::math::tools::bisect(ce_root_function, lowerBound, upperBound, ce_root_termination); | ||
| CoolingEffect = (solverResult.first + solverResult.second) / 2; | ||
| } catch (const std::exception &e) { | ||
| General::SolveRoot(state, tempAccuracy, MaxIte, SolFla, CoolingEffect, ce_root_function, lowerBound, upperBound); | ||
| if (SolFla < 0) { |
There was a problem hiding this comment.
Improved accuracy with fewer iterations = winning
(I believe the note above where you state "3.4x" should probably be 4.3x based on the table.)
| TEST_F(EnergyPlusFixture, ThermalComfort_CalcStandardEffectiveTemp_ContinuousAtCriticalWettedness) | ||
| { | ||
| // When the skin wettedness reaches EvapEff, SkinWetSweat must invert SkinWetTot = 0.06 + 0.94 * SkinWetSweat, i.e. | ||
| // (SkinWetTot - 0.06) / 0.94 as in Gagge et al. (1986). SET should then change smoothly and monotonically with temperature. | ||
| state->dataEnvrn->OutBaroPress = 101325.0; | ||
| Real64 constexpr RelHum = 0.7; | ||
| Real64 constexpr AirVel = 0.1; | ||
| Real64 constexpr ActMet = 1.1; | ||
| Real64 constexpr CloUnit = 0.5; | ||
| Real64 constexpr WorkEff = 0.0; | ||
|
|
||
| Real64 maxStep = 0.0; | ||
| Real64 minStep = std::numeric_limits<Real64>::max(); | ||
| Real64 previous = CalcStandardEffectiveTemp(*state, 32.5, 32.5, RelHum, AirVel, ActMet, CloUnit, WorkEff); | ||
| for (int i = 1; i <= 1500; ++i) { | ||
| Real64 const temp = 32.5 + i * 0.001; | ||
| Real64 const current = CalcStandardEffectiveTemp(*state, temp, temp, RelHum, AirVel, ActMet, CloUnit, WorkEff); | ||
| maxStep = std::max(maxStep, std::abs(current - previous)); | ||
| minStep = std::min(minStep, current - previous); | ||
| previous = current; | ||
| } | ||
| EXPECT_LT(maxStep, 0.01); | ||
| EXPECT_GT(minStep, 0.0); // monotonic, so the cooling effect has a single root | ||
| } |
There was a problem hiding this comment.
Looks good. There's a few lingering test values in here where @Myoldmopar was scratching his head previously.
Might be worth cleaning these up while we're in here.


Pull request overview
REVIEW WITH https://github.com/NatLabRockies/EnergyPlus/pull/11877/changes/29ec00e6e1e616328f7fc6eb554b21d2db6f1dac..7075e054ccb57652ac8d2eb635e9f406bb86fc6f . The first commit just takes an alias for readability.
Description of the purpose of this PR
TL;DR is that the reference implementation and tables from ASRHAE 55 Appendices have an error (that the original Gagge et al. (1986) didn't have). Wasn't sure how to approach this, so I reported it to, and then fixed myself two open source implementations that are probably much closer to ASHRAE 55 (I suspect at least of the two is actually the reference...):
PRSWwhen skin wettedness is capped atWCRITso SET is no longer discontinuous CenterForTheBuiltEnvironment/comfort_tool#103p_rswwhen skin wettedness is capped atw_maxso SET is no longer discontinuous (two_nodes_gagge) pythermalcomfort/pythermalcomfort#490Should watch:
Pull Request Author
Reviewer