Skip to content

Fix/compressor train dense fluid robustness#1539

Closed
olelod wants to merge 3 commits intomainfrom
fix/compressor-train-dense-fluid-robustness
Closed

Fix/compressor train dense fluid robustness#1539
olelod wants to merge 3 commits intomainfrom
fix/compressor-train-dense-fluid-robustness

Conversation

@olelod
Copy link
Copy Markdown
Contributor

@olelod olelod commented May 5, 2026

Robustness fixes for compressor train evaluation on dense / supercritical fluids

Three layered fixes — one real physical bug, two layers of EOS-failure
robustness — so compressor trains keep evaluating cleanly when fed
pathological dense-fluid operating points produced by the speed solver's
max-speed probes.

Branched from v13.7.0. All 236 compressor / train / thermo / fluid /
liquid regression tests pass.


1. fix(process): scale mass rate when LiquidRemover drops out liquid phase

LiquidRemover.propagate_stream replaced the inlet stream's fluid with a
gas-phase-only fluid via FluidStream.with_new_fluid(...), which preserves
mass_rate_kg_per_h. Because the gas phase has a lower molar mass than the
original two-phase mixture and only carries a fraction of the total mass,
holding mass_rate constant re-injects the dropped-out liquid mass into the
gas stream as "phantom mass" and inflates the actual molar / volumetric flow
handed to the next compressor stage.

For real models with non-trivial liquid drop-out this can push downstream
compressor stages many times above their chart's max rate, far into
extrapolation, producing unphysical polytropic head and target enthalpies that
NeqSim's PH flash cannot solve and which surface as IsNaNException from
PhasePrEos.molarVolumeAnalytical.

Scale the new stream's mass rate by the gas mass fraction:

gas_mass_fraction = vapor_fraction_molar * MW_gas / MW_total
new_mass_rate     = old_mass_rate * gas_mass_fraction

2. fix(process): robust outlet-pressure solver with TP-flash fallback

calculate_outlet_pressure_and_stream historically did a PH-flash
fixed-point iteration whose first guess is the Campbell formula evaluated
with inlet z and kappa. For dense supercritical fluids that first guess
can be wildly off (10000+ bara) and NeqSim's PH-flash crashes with
IsNaNException from PhasePrEos.molarVolumeAnalytical before the loop has
any chance to converge.

Two changes to make this layer robust:

  1. fluid_service.flash_ph wraps the underlying py4j call so a NeqSim
    IsNaNException surfaces as IllegalStateException at the Python boundary
    instead of bubbling up as Py4JJavaError.

  2. calculate_outlet_pressure_and_stream now tries the legacy PH-flash loop
    first (cheap, well-calibrated, preserves historical numbers for the bulk
    of operating points). If that loop refuses (NaN Campbell guess, non-finite
    target enthalpy, or PHflash IsNaNException), it falls back to a more
    robust bracket-and-bisect:

    • For a candidate outlet pressure P_out, estimate outlet temperature
      with the polytropic ideal-gas relation
      T_out ~ T_in * (P_out / P_in) ^ ((k - 1) / k) and TP-flash there.
      TP-flash is far more numerically stable than PH-flash because pressure
      and temperature uniquely fix the EoS state (no inner enthalpy root).

    • Plug the resulting average z and kappa back into Campbell:

      f(P_out) = Campbell(avg z, avg k) - P_out
      

      The physical outlet pressure is the root of f. Solve with Brent on
      [inlet_p, MAX_FIRST_GUESS_BAR].

    • At the converged P*, do one PH-flash at the real target enthalpy to
      obtain the final outlet fluid state.

    If even the bracket has no sign change (chart head is genuinely
    unphysical), the fallback raises IllegalStateException so the upstream
    speed solver can treat the proposal as infeasible.

Normal cases are unchanged (PH-flash loop succeeds, no drift in golden
numbers). Pathological dense-fluid cases that previously crashed now resolve
through the fallback.


3. fix(process): speed solver tolerates infeasible EOS probes

When a candidate (speed, rate, pressure) proposal makes the underlying
EOS / fluid layer fail (e.g. NeqSim PHflash returns NaN even after the
TP-flash fallback in the outlet-pressure solver, or a chart point is being
extrapolated outside its valid envelope), the shaft-speed solver should treat
the probe as infeasible and try a different one — not crash the whole train
evaluation.

Changes in find_fixed_shaft_speed_given_constraints (and its outer
evaluate_given_constraints wrapper):

  • Inner _calculate_compressor_train closure catches IllegalStateException
    and returns an infeasible sentinel train result
    (point_is_valid=False, rate_exceeds_maximum=True), so the speed solver
    treats it like any other out-of-capacity probe rather than letting the
    exception bubble up.

  • When the max-speed probe is rejected by the EOS rather than by a real
    chart capacity flag (chart_area_flag == NOT_CALCULATED instead of e.g.
    ABOVE_MAXIMUM_FLOW_RATE), bisect downward to find the highest evaluable
    speed and continue the normal speed search from there, instead of
    short-circuiting to the unevaluable maximum_speed.

  • Outer evaluate_given_constraints wrapper catches any remaining
    IllegalStateException and marks the timestep NOT_CALCULATED instead of
    crashing the whole run.

Adds CompressorTrainStageResultSingleTimeStep.create_infeasible and
CompressorTrainResultSingleTimeStep.create_infeasible classmethods next to
the existing create_empty constructors so the infeasible sentinel is
discoverable and reusable.

olelod added 3 commits May 5, 2026 15:54
Try the legacy PH-flash loop first; on EOS failure (NaN Campbell guess,
non-finite target enthalpy, NeqSim PHflash IsNaNException) fall back to
a bracket+bisect on outlet pressure that uses TP-flashes for property
updates and a single PH-flash at the converged point.
Treat speeds that make the EOS / fluid layer fail as infeasible probes
rather than letting the exception crash the run. Bisect down from the
max-speed probe when it's rejected by the EOS, and add an outer guard
that marks otherwise-unrecoverable timesteps as NOT_CALCULATED.
@olelod olelod force-pushed the fix/compressor-train-dense-fluid-robustness branch from ca47f99 to 68c430e Compare May 5, 2026 14:21
@olelod
Copy link
Copy Markdown
Contributor Author

olelod commented May 7, 2026

Closing — superseded by three PRs that cover the same problem space:

Layer in this PR Replacement
1. LiquidRemover mass rate scaling #1546 (same fix)
2. Outlet-pressure solver robustness #1540 (merged) — validates PH flash results, seeds PH from inlet T, rejects non-finite Campbell guesses
3. SpeedSolver tolerates infeasible probes #1543OutletFluidNotAchievableError + boundary binary-search

@olelod olelod closed this May 7, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant