failure_reason semantics¶
Every analyze_steady result carries a failure_reason key. It
is a diagnostic report — independent of whether the result dict
contains usable cycle numbers — that lets callers branch on why a
step looks the way it does without having to inspect the cycle
internals. This page is the reference for what each value means and
when to branch on it.
The four values¶
Value |
Means |
Result dict carries cycle numbers? |
|---|---|---|
|
Cycle closed and the SciPy optimiser converged. |
Yes — trust |
|
HX residual exceeded tolerance, but the cycle itself produced a state. |
Yes — numbers are usable but should be treated as
approximate. |
|
SciPy couldn’t satisfy its own success criteria, even though the cycle returned a state. |
Yes — numbers exist, but the evaporating-temperature choice is not provably optimal. |
|
The cycle itself was infeasible at the requested
operating point. The model falls back to off-mode
( |
No — only off-mode placeholders. |
How to branch on it¶
The safest pattern depends on which question you’re asking.
“Is this step physically meaningful?” Use the cycle output
directly. Off-mode rows have E_cmp [W] == 0:
ok = df["E_cmp [W]"] > 0
This is robust across all four failure_reason values and is
also the recommended check inside analyze_dynamic, where
failure_reason is per-step.
“Did the model warn me about anything?” Look at
failure_reason directly:
from collections import Counter
print(Counter(df["failure_reason"]))
# e.g. Counter({'none': 1392, 'hx_not_converged': 47, 'cycle_invalid': 1})
“Is this row trustworthy for a metric I care about?” Combine the converged flag with the failure reason:
trustworthy = (df["converged"]) & (df["failure_reason"] == "none")
What triggers each value¶
These are implementation details and may shift between releases — treat the four values themselves as the stable contract, not the mechanism. Expand the cards below for the trigger conditions and the recommended lever to pull when you hit each one.
none — everything converged
Both the inner HX loop and the outer SciPy optimiser hit
their success criteria. The result dict is fully populated
and converged is True.
No action needed. This is the common path on a well-sized system at moderate ambient.
hx_not_converged — HX residual exceeded tolerance
The HX residual didn’t drop below tolerance inside the inner
iteration, but the cycle still produced a state. The result
is usable as an approximation — converged is set to
False so you can filter rows downstream.
What to do. Treat converged as the source of truth
for “trust this row?” decisions. If the rate of
hx_not_converged is uncomfortably high, oversize the
affected HX (design ε-NTU or area) so the iteration has
more headroom near phase boundaries.
optimizer_failed — SciPy didn’t satisfy its own criteria
The outer SciPy optimiser exited with success == False.
The cycle still has a state at the optimiser’s best
dT_ref_evap, just not a provably optimal one.
What to do. The numbers are reasonable but not
optimisation-grade — fine for most aggregations, suspect for
point-comparison work. Wider initial bounds on
dT_ref_evap usually clears this.
cycle_invalid — cycle was infeasible at this point
_calc_state raised, or returned a non-dict, at the
requested T_tank_w / T0 / Q_ref_cond. The model
falls back to off-mode placeholders (E_cmp = 0,
Q_ref_cond = 0).
What to do. Usually the requested duty is unreachable for the given geometry. Consider increasing the design ε-NTU, fan flow, or condenser area; or accept that the operating point is genuinely off-limits and let the off-mode row stand.
Off-mode fallback¶
When failure_reason == "cycle_invalid", the steady-state path
emits a RuntimeWarning and substitutes an off-mode result:
hp_is_on = False, all power and duty fields zeroed. This keeps
analyze_dynamic rows aligned (one row per step, regardless of
feasibility) so the resulting DataFrame is safe to vectorise over.
Where this is defined in the code¶
The diagnostic flag is set inside analyze_steady for the five
core models. See the docstrings under: