Skip to content

Fix heating and cooling runtime fraction for AirLoopHVAC:UnitarySystem for airflow network simulations#11491

Open
lymereJ wants to merge 12 commits into
developfrom
fix_unitary_system_afn_runtime_fracs
Open

Fix heating and cooling runtime fraction for AirLoopHVAC:UnitarySystem for airflow network simulations#11491
lymereJ wants to merge 12 commits into
developfrom
fix_unitary_system_afn_runtime_fracs

Conversation

@lymereJ
Copy link
Copy Markdown
Collaborator

@lymereJ lymereJ commented Mar 31, 2026

Pull request overview

Pull Request Author

  • Title of PR should be user-synopsis style (clearly understandable in a standalone changelog context)
  • Label the PR with at least one of: Defect, Refactoring, NewFeature, Performance, and/or DoNoPublish
  • Pull requests that impact EnergyPlus code must also include unit tests to cover enhancement or defect repair
  • Author should provide a "walkthrough" of relevant code changes using a GitHub code review comment process
  • If any diffs are expected, author must demonstrate they are justified using plots and descriptions
  • If changes fix a defect, the fix should be demonstrated in plots and descriptions

Reviewer

  • Perform a Code Review on GitHub
  • If branch is behind develop, merge develop and build locally to check for side effects of the merge
  • If defect, verify by running develop branch and reproducing defect, then running PR and reproducing fix
  • If feature, test running new feature, try creative ways to break it
  • CI status: all green or justified
  • Check that performance is not impacted (CI Linux results include performance check)
  • Run Unit Test(s) locally
  • Check any new function arguments for performance impacts
  • Verify IDF naming conventions and styles, memos and notes and defaults
  • If new idf included, locally check the err file and other outputs

@lymereJ lymereJ added the Defect Includes code to repair a defect in EnergyPlus label Mar 31, 2026
@lymereJ
Copy link
Copy Markdown
Collaborator Author

lymereJ commented Mar 31, 2026

Here's a comparison of the duct conduction heat gains for the defect file with the "reference" file with this branch:
image

Also, as expected their energy use is virtually the same:

  • OPT4 (AirLoopHVAC:UnitarySystem):
image
  • OPT3 (AirLoopHVAC:UnitaryHeatCool):
image

Copy link
Copy Markdown
Collaborator Author

@lymereJ lymereJ left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Code walk-through:

// Report the current output
this->reportUnitarySystem(state, AirLoopNum);

// Get the actual maximum RTF for AFN simulations
Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Similar changes as for #11367. We save the RTFs at the system level instead of at the coil level and make the determination here, that way we can also include the supplemental heating coil.

int CompIndex = this->m_HeatingCoilIndex;
HVAC::FanOp fanOp = this->m_FanOpMode;
Real64 DesOutTemp = this->m_DesiredOutletTemp;

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

These were only applied when the system is controller to a setpoint. The propose changes apply to both load-based and setpoint control.

latOut);

// Check that the runtime fraction is less than one so the impact of cycling fan is correctly accounted for in the AFN
EXPECT_TRUE(state->dataAirLoop->AirLoopAFNInfo(1).AFNLoopHeatingCoilMaxRTF < 1);
Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

New unit test to make sure that the correct RTF is retrieved. This verification fails on develop.

@github-actions
Copy link
Copy Markdown

⚠️ Regressions detected on ubuntu-24.04 for commit 7c78951

Regression Summary
  • ESO Small Diffs: 703
  • Table Small Diffs: 391
  • MTR Small Diffs: 523
  • Table String Diffs: 186
  • EIO: 396
  • JSON Small Diffs: 2
  • ZSZ Small Diffs: 72
  • Table Big Diffs: 36
  • ERR: 14
  • MTR Big Diffs: 2
  • EDD: 4
  • ESO Big Diffs: 10
  • SSZ Small Diffs: 13
  • JSON Big Diffs: 2

@lymereJ lymereJ added the AirflowNetwork Related primarily on airflow-network portions of the codebase label Apr 1, 2026
Comment thread src/EnergyPlus/UnitarySystem.cc Outdated
if (state.afn->distribution_simulated && this->m_sysType != SysType::PackagedAC && this->m_sysType != SysType::PackagedHP &&
this->m_sysType != SysType::PackagedWSHP && AirLoopNum > 0) {
refAFNLoopHeatingCoilMaxRTF = state.dataAirLoop->AirLoopAFNInfo(AirLoopNum).AFNLoopHeatingCoilMaxRTF;
refAFNLoopCoolingCoilMaxRTF = state.dataAirLoop->AirLoopAFNInfo(AirLoopNum).AFNLoopDXCoilRTF;
Copy link
Copy Markdown
Collaborator

@rraustad rraustad Apr 1, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Can't you just move these last 2 lines down inside of the block below?

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I don't think so because I think this needs to happen before controlUnitarySystemtoSP or controlUnitarySystemtoLoad are called.

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I see. Because these same AFN variables are set in the child. In this parent there can be 2 heating coils so the larger RTF should be used and calling the control functions will set AFNLoopHeatingCoilMaxRTF to the RTF of the last coil called.

Comment thread src/EnergyPlus/UnitarySystem.cc Outdated
Comment thread src/EnergyPlus/UnitarySystem.cc
@NatLabRockies NatLabRockies deleted a comment from github-actions Bot Apr 25, 2026
Copy link
Copy Markdown
Collaborator

@mitchute mitchute left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

A couple more questions for consideration.

Comment thread src/EnergyPlus/UnitarySystem.cc Outdated
case HVAC::CoilType::HeatingSteam:
case HVAC::CoilType::HeatingWater:
case HVAC::CoilType::UserDefined: {
suppHeatingCoilRTF = 1.0;
Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Why are these types hard coded to RTF = 1 here?

Why not

suppHeatingCoilRTF = this->m_SuppHeatPartLoadFrac

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yup, good catch, I did not propagate the changes from the heating coil to the supplemental coil.

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

That might work. Here's how those coil types use PLR to modulate fluid flow. So it really depends on how AFN uses these global RTF variables to solve the network. The fan would provide an RTF for airflow and these coils would provide an RTF for temp? pressure drop? not sure. I suspect it's a guess either way unless someone dissects AFN to see how these RTF variables are used.

                this->m_SuppHeatPartLoadFrac = PartLoadFrac;
                this->m_SuppHeatingCycRatio = CycRatio;
                this->m_SuppHeatingSpeedRatio = SpeedRatio;

    if (this->m_suppHeatCoilType == HVAC::CoilType::HeatingWater || this->m_suppHeatCoilType == HVAC::CoilType::HeatingSteam) {
        mdot = PartLoadFrac * this->m_MaxSuppCoilFluidFlow;
        PlantUtilities::SetComponentFlowRate(
            state, mdot, this->m_SuppCoilFluidInletNode, this->m_SuppCoilFluidOutletNodeNum, this->m_SuppCoilPlantLoc);
    }

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It looks to me as if this is an indicator of air flow so I am not at all sure why AFN is looking at a coil RTF. What difference would it make on infiltration if a compressor cycled on and off? A fan yes, but a coil? I'm still not sure how/why these coil RTF variables are used.

            LoopOnOffFanRunTimeFraction(AirLoopNum) = max(m_state.dataAirLoop->AirLoopAFNInfo(AirLoopNum).AFNLoopHeatingCoilMaxRTF,
                                                          m_state.dataAirLoop->AirLoopAFNInfo(AirLoopNum).AFNLoopOnOffFanRTF,
                                                          m_state.dataAirLoop->AirLoopAFNInfo(AirLoopNum).AFNLoopDXCoilRTF);

                if (AirflowNetworkNodeData(i).AirLoopNum == AirLoopNum) {
                    RepOnOffFanRunTimeFraction = LoopOnOffFanRunTimeFraction(AirLoopNum);
                }

                AirflowNetworkReportData(i).MultiZoneInfiSenGainW *= RepOnOffFanRunTimeFraction;
                AirflowNetworkReportData(i).MultiZoneInfiSenGainJ *= RepOnOffFanRunTimeFraction;
                AirflowNetworkReportData(i).MultiZoneInfiSenLossW *= RepOnOffFanRunTimeFraction;
                AirflowNetworkReportData(i).MultiZoneInfiSenLossJ *= RepOnOffFanRunTimeFraction;
                AirflowNetworkReportData(i).MultiZoneInfiLatGainW *= RepOnOffFanRunTimeFraction;
                AirflowNetworkReportData(i).MultiZoneInfiLatGainJ *= RepOnOffFanRunTimeFraction;
                AirflowNetworkReportData(i).MultiZoneInfiLatLossW *= RepOnOffFanRunTimeFraction;
                AirflowNetworkReportData(i).MultiZoneInfiLatLossJ *= RepOnOffFanRunTimeFraction;
                AirflowNetworkReportData(i).MultiZoneVentSenGainW *= RepOnOffFanRunTimeFraction;
                AirflowNetworkReportData(i).MultiZoneVentSenGainJ *= RepOnOffFanRunTimeFraction;
                AirflowNetworkReportData(i).MultiZoneVentSenLossW *= RepOnOffFanRunTimeFraction;
                AirflowNetworkReportData(i).MultiZoneVentSenLossJ *= RepOnOffFanRunTimeFraction;
                AirflowNetworkReportData(i).MultiZoneVentLatGainW *= RepOnOffFanRunTimeFraction;
                AirflowNetworkReportData(i).MultiZoneVentLatGainJ *= RepOnOffFanRunTimeFraction;
                AirflowNetworkReportData(i).MultiZoneVentLatLossW *= RepOnOffFanRunTimeFraction;
                AirflowNetworkReportData(i).MultiZoneVentLatLossJ *= RepOnOffFanRunTimeFraction;
                AirflowNetworkReportData(i).MultiZoneMixSenGainW *= RepOnOffFanRunTimeFraction;
                AirflowNetworkReportData(i).MultiZoneMixSenGainJ *= RepOnOffFanRunTimeFraction;
                AirflowNetworkReportData(i).MultiZoneMixSenLossW *= RepOnOffFanRunTimeFraction;
                AirflowNetworkReportData(i).MultiZoneMixSenLossJ *= RepOnOffFanRunTimeFraction;
                AirflowNetworkReportData(i).MultiZoneMixLatGainW *= RepOnOffFanRunTimeFraction;
                AirflowNetworkReportData(i).MultiZoneMixLatGainJ *= RepOnOffFanRunTimeFraction;
                AirflowNetworkReportData(i).MultiZoneMixLatLossW *= RepOnOffFanRunTimeFraction;
                AirflowNetworkReportData(i).MultiZoneMixLatLossJ *= RepOnOffFanRunTimeFraction;

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think that it is used to determine the impact of cycling fan operation on the AFN (infiltration, duct leakage, conduction, etc.). For this particular issue, the fact that the runtime fraction was not picked up correctly resulted in these adjustment to not be applicable:

https://github.com/NatLabRockies/EnergyPlus/blob/develop/src/EnergyPlus/AirflowNetwork/src/Solver.cpp#L10063-L10150

Copy link
Copy Markdown
Collaborator Author

@lymereJ lymereJ Apr 30, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It looks to me as if this is an indicator of air flow so I am not at all sure why AFN is looking at a coil RTF. What difference would it make on infiltration if a compressor cycled on and off? A fan yes, but a coil?

I agree, fan RTF sounds like a better indicator but it looks like we take the max of coils and fan RTF:

OnOffFanRunTimeFraction = max(m_state.dataAirLoop->AirLoopAFNInfo(AirLoopNum).AFNLoopHeatingCoilMaxRTF,
m_state.dataAirLoop->AirLoopAFNInfo(AirLoopNum).AFNLoopOnOffFanRTF,
m_state.dataAirLoop->AirLoopAFNInfo(AirLoopNum).AFNLoopDXCoilRTF);

Comment thread src/EnergyPlus/UnitarySystem.cc Outdated
case HVAC::CoilType::CoolingWater:
case HVAC::CoilType::CoolingWaterDetailed:
case HVAC::CoilType::UserDefined:
case HVAC::CoilType::CoolingDXHXAssisted:
Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Does this need to pick the RTF off of the child coil data structs? Something like this?

case HVAC::CoilType::CoolingDXHXAssisted: {
    if (this->m_CoolingCoilIndex > 0) {
        auto const &hxCoil = state.dataHVACAssistedCC->HXAssistedCoil(this->m_CoolingCoilIndex);
        if (hxCoil.CoolingCoilIndex > 0) {
            switch (hxCoil.coolCoilType) {
            case HVAC::CoilType::CoolingDX: {
                coolingCoilRTF = state.dataCoilCoolingDX->coilCoolingDXs[hxCoil.CoolingCoilIndex].coolingCoilRuntimeFraction;
            } break;
            case HVAC::CoilType::CoolingDXSingleSpeed: {
                coolingCoilRTF = state.dataDXCoils->DXCoil(hxCoil.CoolingCoilIndex).CoolingCoilRuntimeFraction;
            } break;
            case HVAC::CoilType::CoolingDXVariableSpeed: {
                coolingCoilRTF = state.dataVariableSpeedCoils->VarSpeedCoil(hxCoil.CoolingCoilIndex).RunFrac;
            } break;
            default: {
                coolingCoilRTF = this->m_CoolingPartLoadFrac;
            } break;
            }
        }
    }

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The snippet above isn't tight enough - I'm specifically referring to the CoolingDXHXAssisted branch.

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yes, that seems like a better approach. I'll push changes.

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@jasondegraw can you give us a lesson in fan RTF vs coil RTF in how AFN uses these vars in the solver? Why does AFN need to know a coil RTF?

@lymereJ
Copy link
Copy Markdown
Collaborator Author

lymereJ commented May 28, 2026

@mitchute observed that while #11571 (a different PR) produces only minor diffs across a single file (AirflowNetwork_MultiZone_House_OvercoolDehumid), running year-long regressions on the AirflowNetwork_MultiZone_House_TwoSpeed model reveals notable differences. After investigating, I found that the current approach—which this PR extends—may have some underlying issues. The approach proposed in #11571 could potentially be more appropriate, and the observed diffs might be expected behavior.

The current approach (expanded here) determines OnOffFanRunTimeFraction by taking the maximum of AFNLoopHeatingCoilMaxRTF, AFNLoopOnOffFanRTF, and AFNLoopDXCoilRTF to estimate how cycling fan operation affects the airflow network (AFN). In contrast, #11571 proposes using only the fan runtime fraction (AFNLoopOnOffFanRTF), which could more directly represent actual fan/system cycling. I assumed that result differences might occur when AFNLoopDXCoilRTF or AFNLoopHeatingCoilMaxRTF exceed AFNLoopOnOffFanRTF. Since these variables weren't previously available as outputs, I added code to log them. The data shows several instances where AFNLoopDXCoilRTF > AFNLoopOnOffFanRTF during heating and AFNLoopDXCoilRTF is always 0 during cooling. Both of these are concerning.

image

I think that comparing these AFN variables to their non-AFN counterparts clarifies what's happening: AFNLoopOnOffFanRTF matches the fan's Fan Runtime Fraction, and during heating, AFNLoopDXCoilRTF matches the heating coil's Heating Coil Runtime Fraction. The issue seems to be that the heating coil runtime fraction is reported at the system's highest operating speed (so in isolation from the system), while the fan runtime fraction is calculated from the average flow rate based on the system's PLR. This results in inconsistencies—speed 1 operation shows coil fractions exceeding fan fractions, and speed 2 shows the opposite—which limits their reliability for determining cycling fan impact on AFN. I also ran the file using a AirLoopHVAC:UnitarySystem and got similar results.

image

It seems that #11571 could offer a potentially more sound and maintainable approach, as it might avoid dependencies on system/coil expansion logic. That said, we could potentially address these issues in the current approach, though doing so might require considering fixes across multiple coils and systems.

@rraustad, @mitchute - what are your thoughts?

@rraustad
Copy link
Copy Markdown
Collaborator

rraustad commented May 29, 2026

What I think AFN is doing is accounting for true on off cycling with adjustments such as this:

exchangeData(i).MultiZoneSen *= OnOffFanRunTimeFraction;

I am not positive but I think the only way this works is looking at the fan RTF. A coil RTF that reports what a speed is doing is not the same as what the air flow is doing. JMHO. I would welcome @jasondegraw input.

@rraustad
Copy link
Copy Markdown
Collaborator

rraustad commented May 29, 2026

The fall back position would be to leave AFN calculations alone for now and just correct the original issue. At least until this can be better understood. A simulation using constant fan compared to a system using cycling fan may provide insight into which of these methods is more accurate. e.g., a constant fan MultiZoneSen compared to cycling fan MultiZoneSen/RTF should look the same?

Update: it doesn't look like MultiZoneSen is a report variable so a valid report from AFN that adjusts that report by RTF would need to be used. Like this:

        SetupOutputVariable(m_state,
                            "AFN Zone Infiltration Sensible Heat Gain Rate",
                            Constant::Units::W,
                            AirflowNetworkReportData(i).MultiZoneInfiSenGainW,

       AirflowNetworkReportData(i).MultiZoneInfiSenGainW *= RepOnOffFanRunTimeFraction;

So, e.g., a constant fan MultiZoneInfiSenGainW compared to cycling fan MultiZoneInfiSenGainW/RTF should look the same?

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

AirflowNetwork Related primarily on airflow-network portions of the codebase Defect Includes code to repair a defect in EnergyPlus

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Incorrect heating energy use for AirLoopHVAC:UnitarySystem in AFN simulation with distribution losses

4 participants