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.
| Aspect | shaft.solveSpeed(...) | CompressorShaftCalculator |
|---|---|---|
| Where it runs | Stand-alone, re-runs the flowsheet via a callback | Inside process.run() as a Calculator |
| Cost | N full-field solves per shaft | One run() — converges with the recycles |
| Root finder | Bracketed false-position (Illinois) | Damped secant, one step per internal pass |
| Best for | One-off / opt-in shaft solve | Plants 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:
| Case | Cause | Nature | Correct response |
|---|---|---|---|
| Pressure too high | Target above the head at the max-speed curve, above the driver power limit, or beyond stonewall | Genuinely infeasible — no operating point exists | Saturate at the max-speed curve, flag the point infeasible, report the max achievable pressure, keep the run alive |
| Pressure too low | Even at min speed the string overshoots (too much head) | Recoverable — a real machine bleeds off the surplus | Apply 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
| Method | Purpose |
|---|---|
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. |
| Method | Purpose |
|---|---|
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). |
Related Documentation
- Compressors
- Compressor Performance Curves
- Compressor Anti-Surge and Coordinated Control
- Worked notebook:
examples/notebooks/CompressorShaft_ThreeStageSeparation.ipynb(three-stage separation + shared-shaft recompression + speed control, feasibility, and pressure-control demo)