Skip to the content.

CompressorShaft: Multiple Compressor Bodies on One Shaft

neqsim.process.equipment.compressor.CompressorShaft groups several Compressor bodies that sit on one driver shaft (a single gas turbine or motor, often through a gearbox) so they all turn at the same speed. A 2- or 3-body recompression string on one turbine is the classic case.

Use it whenever more than one compressor body shares a driver. Do not let each body solve its own speed independently — that produces different speeds for machines that are physically locked together.

Degrees of Freedom (the rule that matters)

A shared shaft has exactly one mechanical degree of freedom — the common speed — and exactly one controlled target: the string’s final discharge pressure. Every intermediate inter-body pressure is a result, not a spec, and must be allowed to float off the charts.

Approach DOF accounting Verdict
Fix every stage outlet pressure and set a common speed over-constrained (N pressures + 1 speed vs 1 real DOF) Non-physical
Adjust ONE common speed to hit the final discharge; intermediates float 1 variable, 1 target Correct

Violating this rule (fixing all interstage pressures while also imposing a common speed) forces the charts to satisfy more constraints than the machine has freedoms, which is not physical.

Iterating the Common Speed

solveSpeed puts every body into fixed-speed, chart-forward mode and solves the common speed with a bracketed false-position (Illinois) secant until the reference (usually last) body’s discharge matches the target. The surrounding flowsheet is re-solved between speed guesses through a caller-supplied Runnable, so inter-body streams, suction scrubbers and mixers update each iteration.

CompressorShaft shaft = new CompressorShaft("23-KA recompression shaft (single GT)");
shaft.addCompressor(rc1); // LP body (lowest suction)
shaft.addCompressor(rc2);
shaft.addCompressor(rc3); // HP body = reference
shaft.setSpeedBounds(8000.0, 16000.0);
shaft.setMaxIterations(20);
shaft.setPressureTolerance(0.3); // bar

// One common speed -> 49 bara at the HP discharge; intermediates float:
shaft.solveSpeed(rc3, 49.0, "bara", new Runnable() {
  @Override
  public void run() {
    process.run();
  }
});

double rpm = shaft.getSpeed();
double shaftPowerW = shaft.getTotalPower();

Discharge pressure rises monotonically with speed on a centrifugal map, so the solver brackets the root at the speed bounds and then converges superlinearly. Each iteration re-solves the whole flowsheet, so it is the dominant cost: keep the iteration budget modest (setMaxIterations) and the tolerance loose (setPressureTolerance). For a large multi-area plant, run the shaft solve as an opt-in step (each shaft costs several full-field solves), and solve each train’s shaft in turn.

Fixed common speed (no iteration)

If you already know the shaft speed, apply it directly. Each body then produces its discharge from its chart at that speed (intermediate pressures float):

shaft.setSpeed(11799.0); // applies to every body, fixed-speed chart-forward mode
process.run();

Process-Integrated Speed Control: CompressorShaftCalculator

solveSpeed re-solves the whole flowsheet several times through the caller-supplied Runnable, so on a large multi-area plant it costs N full-field solves per shaft. neqsim.process.equipment.util.CompressorShaftCalculator is the process-integrated alternative: like AntiSurgeCalculator, it is a Calculator that you add to the ProcessSystem, and it converges the common speed inside the normal process.run() iteration. On every internal pass it takes one damped secant step on the common speed toward the reference body’s target discharge, so the shaft speed converges together with the recycle loops in a single run() — no separate full-field re-solves.

Prefer this over solveSpeed when the shaft sits in a plant that already iterates recycles, or when you want the shaft to re-converge automatically every time you re-run the process.

CompressorShaft shaft = new CompressorShaft("23-KA train");
shaft.addCompressor(rc1);
shaft.addCompressor(rc2);
shaft.addCompressor(rc3); // reference (last body)

// Wire the calculator: reference body rc3, target 49 bara discharge.
// Pass null as the reference to default to the last body added to the shaft.
CompressorShaftCalculator shaftCalc =
    new CompressorShaftCalculator("23-KA shaft speed", shaft, rc3, 49.0, "bara");
shaftCalc.setSpeedBounds(8000.0, 16000.0); // clamp the search
shaftCalc.setMaxStepFraction(0.10);        // max 10% speed change per pass (damping)

process.add(shaftCalc); // add AFTER the compressor bodies and their anti-surge loops
process.run();          // shaft speed converges with the recycles in one run()

double rpm = shaftCalc.getSpeed();

How it converges: higher speed raises discharge on a centrifugal map, so the calculator drives the reference outlet-pressure error to zero. The first pass (or a stalled secant) takes a proportional bump; subsequent passes take a secant step on the speed → error relation. Every step is damped by maxStepFraction and clamped to [minSpeed, maxSpeed] so it stays stable inside the recycle iteration. Each pass puts every body into fixed-speed, chart-forward mode (setSolveSpeed(false), common setSpeed, setUseCompressorChart(true), setUsePolytropicCalc(true)) — the intermediate inter-body pressures float, as required by the degrees-of-freedom rule above.

Ordering: add the CompressorShaftCalculator after the compressor bodies and their anti-surge calculators/recycles so the shaft speed is stepped once the body states and recycle flows for that pass are current.

solveSpeed vs CompressorShaftCalculator
Aspectshaft.solveSpeed(...)CompressorShaftCalculator
Where it runsStand-alone, re-runs the flowsheet via a callbackInside process.run() as a Calculator
CostN full-field solves per shaftOne run() — converges with the recycles
Root finderBracketed false-position (Illinois)Damped secant, one step per internal pass
Best forOne-off / opt-in shaft solvePlants that already iterate; auto re-convergence

When the Target Pressure Is Not Reachable

A single common speed (and, even more so, a constant-speed driver) gives the string one knob, but the performance charts impose hard limits. So a requested final discharge pressure may simply be impossible, and the solver must not pretend otherwise. There are two physically distinct cases — handle them differently:

Infeasible target pressure: two cases
CaseCauseNatureCorrect response
Pressure too highTarget above the head at the max-speed curve, above the driver power limit, or beyond stonewallGenuinely infeasible — no operating point existsSaturate at the max-speed curve, flag the point infeasible, report the max achievable pressure, keep the run alive
Pressure too lowEven at min speed the string overshoots (too much head)Recoverable — a real machine bleeds off the surplusApply a pressure-control action (downstream/upstream choke or ASV recycle); point stays feasible

Do not iterate harder past the chart (extrapolating the fan-law invents a non-physical head), and do not throw an exception that aborts a whole multi-area / multi-year run over a single infeasible duty. The professional pattern (as used by eCalc, which itself uses NeqSim for the thermodynamics) is: saturate to the physical limit → classify recoverable vs unrecoverable → return a feasibility flag with a reason and the max achievable value → continue. eCalc solves speed between its min- and max-speed curves; a request below min-speed capability triggers a configurable pressure-control strategy (downstream choke, upstream choke, or ASV recirculation) to shed the surplus head, while a request above max-speed capability or over max power marks that timestep invalid with a reason and the run reports which points failed. Reserve a hard exception for misconfiguration (no chart, no bodies, inverted bounds) — not for a well-posed but impossible pressure.

NeqSim solve result and pressure control

Both solvers implement exactly this pattern. They saturate to the nearest speed bound rather than crash, and — like eCalc — expose a feasibility result so a caller never silently proceeds on an impossible duty.

CompressorShaft.solveSpeed(...) builds a CompressorShaft.SolveResult (read it with getLastSolveResult(), or the shortcut isFeasible()); the two bracket evaluations it already does at the speed bounds give the min- and max-achievable discharge pressures for free:

shaft.solveSpeed(hpBody, 49.0, "bara", () -> process.run());

CompressorShaft.SolveResult r = shaft.getLastSolveResult();
if (!r.isFeasible()) {
  // r.getStatus() is one of PRESSURE_ABOVE_MAX_SPEED, PRESSURE_BELOW_MIN_SPEED,
  // OVER_POWER, STONEWALL, SURGE, NOT_CONFIGURED.
  double ceiling = r.getMaxAchievablePressure();   // most the string can make
  double floor = r.getMinAchievablePressure();      // least the string can make
  String json = r.toJson();                         // schema-friendly for logs/agents
}

SolveStatus values: FEASIBLE, PRESSURE_CONTROLLED, PRESSURE_ABOVE_MAX_SPEED, PRESSURE_BELOW_MIN_SPEED, OVER_POWER, STONEWALL, SURGE, NOT_CONFIGURED. The result also carries getTargetPressure(), getAchievedPressure(), getSpeed(), and getMessage().

Pressure control (the “too low” case). When the target is below what the string makes even at minimum speed, set a PressureControl so the surplus head is shed and the point stays feasible (PRESSURE_CONTROLLED), exactly like eCalc:

shaft.setPressureControl(CompressorShaft.PressureControl.DOWNSTREAM_CHOKE);
shaft.solveSpeed(hpBody, target, "bara", () -> process.run());
// -> status PRESSURE_CONTROLLED, feasible, reference discharge delivered at target

PressureControl values: NONE (default — the too-low case is reported infeasible), DOWNSTREAM_CHOKE, UPSTREAM_CHOKE, ASV_RECYCLE. At this screening level all non-NONE options net to delivering the requested pressure at minimum-speed power (the surplus head is shed).

CompressorShaftCalculator carries the same API — getLastSolveResult(), isFeasible(), setPressureControl(...), setPressureTolerance(...) — updated on every internal pass, so an optimizer / evaluate() loop can gate on shaftCalc.isFeasible() directly instead of re-deriving saturation. For finer diagnostics, Compressor still exposes getDistanceToSurge(), getDistanceToStoneWall(), and getOperatingPoint(), and a multi-area plant surfaces feasible / hardLimitExceeded through its capacity getUtilizationSnapshot().

Reserve a hard exception for misconfiguration (no bodies) — never for a well-posed but impossible pressure.

Shared Pressure Nodes

When another unit ties into an interstage — for example a 2nd-stage separator gas that joins the recompressor between bodies — model it as a pressure equality, not a second spec: slave that unit’s pressure to the floating interstage discharge, or drop a small adapting valve there. A Mixer at the join already resolves to the lowest inlet pressure, so a small let-down is automatic. This removes a degree of freedom rather than adding one.

The Mixer pressure-mismatch flag

The flip side of “a Mixer resolves to the lowest inlet pressure” is that a compressor which failed to reach its target discharge quietly drags the whole mix down — the lost pressure just disappears downstream. Taking the lowest inlet pressure is the correct physics, but you should be told it happened. Mixer therefore raises a flag whenever its active inlets arrive at materially different pressures:

mixer.run();
if (mixer.isPressureMismatch()) {
  // The outlet dropped to the lowest inlet; a higher inlet was throttled down.
  double spread = mixer.getInletPressureSpread(); // bar between highest and lowest active inlet
  double lost   = mixer.getMaxInletPressure() - mixer.getMinInletPressure();
  // Investigate the upstream unit that did not reach its target pressure.
}

The flag is recomputed on every run() and a warning is logged when it trips. The trip threshold defaults to 0.5 bar and is configurable with setPressureMismatchTolerance(bar); getMaxInletPressure() / getMinInletPressure() report the spread’s endpoints (the minimum equals the outlet pressure). Use this as the downstream companion to the shaft feasibility check above: the shaft flag tells you the machine could not make the pressure, and the mixer flag tells you where that shortfall silently propagated into the flowsheet.

Fixed-/Single-Speed Drivers

If the driver is a constant-speed motor (line frequency, no variable-speed drive) the speed is not a degree of freedom — do not iterate it. Use runAtFixedSpeed:

shaft.runAtFixedSpeed(3550.0, new Runnable() {
  @Override
  public void run() {
    process.run();
  }
});

The discharge floats off the chart at the locked speed, and any pressure spec is met by anti-surge recycle, suction throttling, or inlet guide vanes — never by moving speed.

Inlet guide vanes are a first-class fixed-speed control on Compressor: setInletGuideVaneOpening(f) (f = 1 fully open) — or setGuideVaneAngle(deg) — closes the vanes to reduce head and efficiency and lower the surge flow (via a configurable InletGuideVaneModel), so getDistanceToSurge() reflects the shifted surge line. This is distinct from moving speed (the shaft speed stays fixed).

Single-Body Shafts

A CompressorShaft with one compressor is valid and useful: solveSpeed just finds that one machine’s speed for its discharge target. So export, re-injection and gas-lift machines (one body each on their own shaft) use the same API — separate shafts keep separate speeds.

Coexistence with Anti-Surge

The per-body anti-surge loops (recycle splitter, anti-surge valve, Recycle, and the anti-surge calculator) stay attached and keep protecting each body; they adjust recycle flow, while the shaft sets speed. Apply the shaft solve after the charts and anti-surge are active. See Compressor Anti-Surge and Coordinated Control and Compressor Performance Curves.

API Summary

CompressorShaft public methods
MethodPurpose
addCompressor(Compressor)Add a body in flow order.
solveSpeed(reference, targetP, unit, runnable)Iterate the one common speed to the reference body's target discharge; intermediates float.
runAtFixedSpeed(rpm, runnable)Constant-speed driver: lock the speed, discharge floats off the chart.
setSpeed(rpm) / getSpeed()Apply / read the common shaft speed.
setSpeedBounds(min, max)Speed search bounds for solveSpeed.
setMaxIterations(n) / setPressureTolerance(bar)Root-finder budget and convergence tolerance.
getLastSolveResult() / isFeasible()Feasibility result of the last solveSpeed (status, achieved / min- / max-achievable pressure, speed).
setPressureControl(PressureControl) / getPressureControl()Shed surplus head for a target below min-speed capability (NONE / DOWNSTREAM_CHOKE / UPSTREAM_CHOKE / ASV_RECYCLE).
getTotalPower()Sum of the body shaft powers (W).
getCompressors() / getName()Members / shaft name.
CompressorShaftCalculator public methods
MethodPurpose
CompressorShaftCalculator(name, shaft, reference, targetP, unit)Wire the calculator to a shaft; reference == null uses the last body added.
setSpeedBounds(min, max)Clamp the common-speed search (rpm).
setMaxStepFraction(fraction)Max fractional speed change per internal pass (damping, e.g. 0.10).
getSpeed()Read the converged common shaft speed (rpm).
getLastSolveResult() / isFeasible()Feasibility result of the last pass (same SolveResult type as CompressorShaft).
setPressureControl(...) / setPressureTolerance(bar)Pressure-control action and the tolerance used to classify feasible / saturated.
process.add(shaftCalc)Register so it steps the speed on every run() pass (add after the bodies/anti-surge).