diff --git a/.github/skills/neqsim-compressor-antisurge-recycle/SKILL.md b/.github/skills/neqsim-compressor-antisurge-recycle/SKILL.md
index 15778d62ec5..52604889e8b 100644
--- a/.github/skills/neqsim-compressor-antisurge-recycle/SKILL.md
+++ b/.github/skills/neqsim-compressor-antisurge-recycle/SKILL.md
@@ -21,6 +21,24 @@ This skill provides patterns for anti-surge protection, minimum-speed recycle co
Standards: **API 617 / API 619**, **IEC 60534 / ISA-75**, **ISO 50001 / ISO 14064** (energy & carbon emissions).
+## Compressor pressure-target solves
+
+For `Compressor.setSolveSpeed(true)`, check `isSpeedSolveConverged()` after `run()`
+before accepting an optimization point. Read actual pressure from the outlet
+stream and the signed pressure error in bar from `getSpeedSolvePressureResidual()`.
+`getOutletPressure()` retains the requested setpoint, including after failure.
+The returned state uses the fixed-speed forward calculation, including anti-surge
+flow changes; these can leave the requested pressure unmet. `setLimitSpeed(false)`
+allows extrapolation, so pressure convergence does not establish mechanical
+feasibility. Check speed and operating-envelope constraints separately.
+`ProductionOptimizer` treats `Compressor.SpeedSolveException` as an infeasible
+trial and continues searching without evaluating stale outputs; unrelated
+exceptions propagate. Its hard `compressorSpeedSolve` constraint also rejects
+returned unconverged results even with capacity analysis enabled. Keep this gate
+when integrating chart-based compressor models into optimizers.
+See [compressor curves](../../../docs/process/equipment/compressor_curves.md#speed-calculation-from-operating-point)
+and `CompressorSpeedSolveConsistencyTest` for the tested contract.
+
## Key Java Class: `MinimumSpeedRecycleControllerStructure`
`neqsim.process.controllerdevice.structure.MinimumSpeedRecycleControllerStructure` coordinates pressure control between compressor speed and recycle-valve opening when the compressor reaches its minimum speed.
diff --git a/docs/development/TASK_LOG.md b/docs/development/TASK_LOG.md
index 6405ab46583..f1934014642 100644
--- a/docs/development/TASK_LOG.md
+++ b/docs/development/TASK_LOG.md
@@ -48,6 +48,12 @@ requirement`, or `confidential compressor route`.
**Solution:** `LoopedPipeNetwork.calculateHeadLossCompressor`, `MappedCompressorAcceptanceTest`, and the SURF integration guide.
**Notes:** A bounded chart speed search requested 100 bara and returned an internally consistent 11.647013866 bara forward state with `solved() == false`, while the network labelled it a valid chart result and retained 53.131953657 kW. Require the existing equipment convergence contract, an enabled chart, finite non-negative power, and positive finite outlet pressure/temperature before accepting mapped evidence. Reuse the existing infeasible-edge path to invalidate power and clear previous thermal state. Four regressions first establish a valid state, then exercise an unsolved result, chart disablement, non-finite power, and non-finite temperature. Keep the common network boundary in PR #4328 and the compressor's speed-search implementation in PR #4326; no system-specific solver or new constraint framework is introduced.
+### 2026-10-14 — Preserve compressor convergence requirements during optimization
+**Type:** E (Feature)
+**Keywords:** compressor, speed solve, invalid chart, optimizer, infeasible trials, lift curves, CI, 4326
+**Solution:** `Compressor.SpeedSolveException`, `ProductionOptimizer` candidate rejection and hard convergence gate, and conditional chart-setup process runs in `FlowRateOptimizer`.
+**Notes:** Reproduced all three failing optimizer cases on PR #4326 before repair. A no-valid-map trial is rejected without reading stale objectives or downstream constraints; unrelated exceptions still propagate. Capacity analysis and optional output validation cannot bypass the pressure-target convergence requirement. Repeated table setup reuses existing charts without rerunning an invalid preceding probe. Updated compressor/optimization guides, the compressor skill and the generated coverage inventory; regression tests cover stale outputs, hard convergence, recovery and chart reuse.
+
### 2026-10-14 — Repair shared CI failures and preserve workflow diagnostics
**Type:** G (Workflow)
**Keywords:** PR 4315, hermetic tests, evidence-source-cues, slow shards, required checks, Surefire, CI
diff --git a/docs/process/equipment/compressor_curves.md b/docs/process/equipment/compressor_curves.md
index 67e55e7c17a..d790eed4694 100644
--- a/docs/process/equipment/compressor_curves.md
+++ b/docs/process/equipment/compressor_curves.md
@@ -1099,7 +1099,41 @@ accepted speed is retained once the pressure residual is within 0.001 bar.
The reported speed, chart head, efficiency and power therefore describe the same
iterate. Re-running an unchanged inlet and target does not take an additional
Newton step after convergence, which could otherwise move a capacity-limited
-operating point across its limit. Configured speed limits still apply.
+operating point across its limit.
+
+The outlet stream is always calculated from the returned speed using the same
+chart-forward calculation as fixed-speed operation, including guide-vane and
+anti-surge effects. An unreachable pressure target must not be interpreted as a
+successful operating point just because `run()` returns:
+
+- `isSpeedSolveConverged()` reports whether the final outlet pressure is within
+ 0.001 bar of the requested target.
+- `getSpeedSolvePressureResidual()` returns achieved minus requested pressure in
+ bar, or `NaN` when no speed-solve result is available for the latest run.
+- `solved()` and `isSimulationValid()` are false when an enabled chart speed solve
+ misses its target; `getSimulationValidationErrors()` explains the failure.
+- Read achieved pressure from `getOutletStream().getPressure("bara")`.
+ `getOutletPressure()` retains the requested setpoint in its configured unit,
+ so repeated runs continue to solve the same target, including after failure.
+
+With `setLimitSpeed(true)`, the search is limited to the configured minimum and
+maximum speeds. A result at a speed limit is still marked non-converged if it
+does not deliver the target. With `setLimitSpeed(false)` (the default), speed
+extrapolation remains allowed for compatibility. Pressure convergence alone
+does not establish that speed, surge, stonewall or power constraints are met;
+check those constraints separately before accepting an operating point.
+
+If the search does not converge, the solver uses its best finite evaluated speed
+and reports the actual forward result with a warning. This is not proof that
+the target is globally unreachable on a non-monotonic map. A search with no valid
+chart operating point raises `Compressor.SpeedSolveException` (an
+`IllegalStateException`) instead of publishing a target-pressure result.
+`ProductionOptimizer` rejects this specific failed trial and continues searching;
+unrelated calculation exceptions still propagate. Its `compressorSpeedSolve`
+hard constraint also rejects an unmet target when capacity analysis is enabled or
+optional output validation is disabled. It does not evaluate objectives or
+downstream constraints on a failed trial's stale outlet state. Diagnostics reset
+at the start of every run, including fixed-speed runs.
When you need to determine the compressor speed required to achieve a specific operating point (flow and head), NeqSim provides a robust algorithm that works both within the defined curve range and with extrapolation beyond it.
diff --git a/docs/process/optimization/flow-rate-optimization.md b/docs/process/optimization/flow-rate-optimization.md
index a851e91cee9..5b636ce0535 100644
--- a/docs/process/optimization/flow-rate-optimization.md
+++ b/docs/process/optimization/flow-rate-optimization.md
@@ -31,6 +31,18 @@ This is essential for:
## Key Features
+Compressor pressure-target failures are infeasible operating points. The shared
+`ProductionOptimizer` handles `Compressor.SpeedSolveException` without aborting
+the search and records a hard `compressorSpeedSolve` constraint. It also rejects
+a returned compressor result whose pressure target did not converge, even when
+capacity analysis is enabled. Objectives and downstream constraints are not read
+from a failed calculation; unrelated exceptions remain visible to callers.
+
+Automatic chart setup reruns the process only when it generates a chart. This
+avoids redundant process solves between capacity, lift-curve and performance
+tables, including when a preceding sweep ended on an invalid chart trial. The
+next table evaluates its own requested operating points.
+
| Feature | Description |
|---------|-------------|
| **Pressure boundary search** | Find max flow at given inlet/outlet pressures |
diff --git a/src/main/java/neqsim/process/equipment/compressor/Compressor.java b/src/main/java/neqsim/process/equipment/compressor/Compressor.java
index 2ffe045453c..7fedad60540 100644
--- a/src/main/java/neqsim/process/equipment/compressor/Compressor.java
+++ b/src/main/java/neqsim/process/equipment/compressor/Compressor.java
@@ -56,6 +56,29 @@ public class Compressor extends TwoPortEquipment
/** Logger object for class. */
static Logger logger = LogManager.getLogger(Compressor.class);
+ /**
+ * Signals that a pressure-target search found no valid chart state for the current inlet.
+ *
+ *
+ * Optimizers may reject this operating point without hiding unrelated calculation errors.
+ *
+ *
+ * @author Even Solbraa
+ * @version 1.0
+ */
+ public static class SpeedSolveException extends IllegalStateException {
+ private static final long serialVersionUID = 1L;
+
+ /**
+ * Creates a failed chart-search diagnostic.
+ *
+ * @param compressorName name of the compressor whose search failed
+ */
+ public SpeedSolveException(String compressorName) {
+ super("Speed solver found no finite chart operating point for " + compressorName);
+ }
+ }
+
/**
* Calculation strategy used when the compressor participates in a dynamic process.
*/
@@ -102,6 +125,12 @@ public enum TransientCalculationMode {
public double dH = 0.0;
public double inletEnthalpy = 0;
private boolean solveSpeed = false;
+ /** Whether the latest speed solve delivered its requested pressure. */
+ private boolean speedSolveConverged = false;
+ /** Achieved minus requested pressure in bar for the latest speed solve. */
+ private double speedSolvePressureResidual = Double.NaN;
+ /** Absolute pressure tolerance for the speed solver, in bar. */
+ private static final double SPEED_SOLVE_PRESSURE_TOLERANCE = 1.0e-3;
public double pressure = 0.0;
private double speed = 3000;
private double maxspeed = 30000;
@@ -140,6 +169,14 @@ public enum TransientCalculationMode {
private double lastInletFlowRate = Double.NaN;
private double[] lastInletComposition = null;
private double lastOutletPressure = Double.NaN;
+ /** Unit of the cached pressure setpoint. */
+ private String lastPressureUnit = null;
+ /** Minimum speed used by the cached speed solve. */
+ private double lastMinimumSpeed = Double.NaN;
+ /** Maximum speed used by the cached speed solve. */
+ private double lastMaximumSpeed = Double.NaN;
+ /** Whether speed bounds were enforced by the cached speed solve. */
+ private boolean lastLimitSpeed = false;
private double lastSpeed = Double.NaN;
private double lastCompressionRatio = Double.NaN;
private double lastOutTemperature = Double.NaN;
@@ -720,12 +757,14 @@ public boolean needRecalculation() {
}
SystemInterface inletSystem = inStream.getThermoSystem();
if (inletSystem.getTemperature() != lastInletTemperature || inletSystem.getPressure() != lastInletPressure
- || pressure != lastOutletPressure || speed != lastSpeed || compressionRatio != lastCompressionRatio
- || outTemperature != lastOutTemperature || isentropicEfficiency != lastIsentropicEfficiency
- || polytropicEfficiency != lastPolytropicEfficiency || dH != lastPower
- || numberOfCompressorCalcSteps != lastNumberOfCompressorCalcSteps || useOutTemperature != lastUseOutTemperature
- || useCompressionRatio != lastUseCompressionRatio || usePolytropicCalc != lastUsePolytropicCalc
- || powerSet != lastPowerSet || calcPressureOut != lastCalcPressureOut || solveSpeed != lastSolveSpeed
+ || pressure != lastOutletPressure || !java.util.Objects.equals(pressureUnit, lastPressureUnit)
+ || minspeed != lastMinimumSpeed || maxspeed != lastMaximumSpeed || limitSpeed != lastLimitSpeed
+ || speed != lastSpeed || compressionRatio != lastCompressionRatio || outTemperature != lastOutTemperature
+ || isentropicEfficiency != lastIsentropicEfficiency || polytropicEfficiency != lastPolytropicEfficiency
+ || dH != lastPower || numberOfCompressorCalcSteps != lastNumberOfCompressorCalcSteps
+ || useOutTemperature != lastUseOutTemperature || useCompressionRatio != lastUseCompressionRatio
+ || usePolytropicCalc != lastUsePolytropicCalc || powerSet != lastPowerSet
+ || calcPressureOut != lastCalcPressureOut || solveSpeed != lastSolveSpeed
|| compressorChart.isUseCompressorChart() != lastUseCompressorChart
|| useEnergyEfficiencyChart != lastUseEnergyEfficiencyChart
|| useRigorousPolytropicMethod != lastUseRigorousPolytropicMethod || useGERG2008 != lastUseGERG2008
@@ -760,6 +799,10 @@ private void updateRecalculationState() {
lastInletFlowRate = inletSystem.getFlowRate("kg/hr");
lastInletComposition = inletSystem.getMolarComposition();
lastOutletPressure = pressure;
+ lastPressureUnit = pressureUnit;
+ lastMinimumSpeed = minspeed;
+ lastMaximumSpeed = maxspeed;
+ lastLimitSpeed = limitSpeed;
lastSpeed = speed;
lastCompressionRatio = compressionRatio;
lastOutTemperature = outTemperature;
@@ -1022,6 +1065,8 @@ private double getEntropyFlashTolerance(double targetEntropy) {
/** {@inheritDoc} */
@Override
public void run(UUID id) {
+ speedSolveConverged = false;
+ speedSolvePressureResidual = Double.NaN;
thermoSystem = inStream.getThermoSystem().clone();
sanitizeEfficiencyStateForRun();
@@ -1259,8 +1304,11 @@ public void run(UUID id) {
}
if (compressorChart.isUseCompressorChart()) {
if (solveSpeed) {
- double targetPressure = getOutletPressure(); // Desired outlet pressure
- double tolerance = 1e-3; // Tolerance for pressure difference (0.001 bar)
+ double targetPressure = new neqsim.util.unit.PressureUnit(pressure, pressureUnit).getValue("bara");
+ if (!Double.isFinite(targetPressure) || targetPressure <= 0.0) {
+ throw new IllegalArgumentException("Speed-solve target pressure must be finite and positive");
+ }
+ double tolerance = SPEED_SOLVE_PRESSURE_TOLERANCE;
double minSpeed = getMinimumSpeed(); // Minimum speed for the compressor
double maxSpeed = getMaximumSpeed(); // Maximum speed for the compressor
double currentSpeed = getSpeed(); // Initial guess for speed
@@ -1274,6 +1322,11 @@ public void run(UUID id) {
if (currentSpeed < chartMinSpeed * 0.5 || currentSpeed > chartMaxSpeed * 2.0) {
currentSpeed = chartRefSpeed;
}
+ if (limitSpeed) {
+ currentSpeed = Math.max(minSpeed, Math.min(maxSpeed, currentSpeed));
+ }
+ double bestSpeed = Double.NaN;
+ double bestPressureError = Double.POSITIVE_INFINITY;
double maxIterations = 200; // Maximum number of iterations
double deltaSpeed = 100.0; // Small increment for numerical derivative
@@ -1311,24 +1364,21 @@ public void run(UUID id) {
z_inlet = VegaProps[1];
}
- double polytropEff = getCompressorChart().getPolytropicEfficiency(actualFlowRate, currentSpeed);
- setPolytropicEfficiency(polytropEff / 100.0);
- if (polytropEff <= 0.0) {
- polytropEff = 0.01;
- setPolytropicEfficiency(0.01);
- }
- if (polytropEff > 100.0) {
- polytropEff = 100;
- setPolytropicEfficiency(100.0);
+ double polytropEff = applyInletGuideVaneEfficiency(
+ getCompressorChart().getPolytropicEfficiency(actualFlowRate, currentSpeed));
+ if (!Double.isFinite(polytropEff) || polytropEff <= 0.0 || polytropEff > 100.0) {
+ break;
}
+ setPolytropicEfficiency(polytropEff / 100.0);
- polytropicHead = getCompressorChart().getPolytropicHead(actualFlowRate, currentSpeed);
+ polytropicHead = applyInletGuideVaneHead(
+ getCompressorChart().getPolytropicHead(actualFlowRate, currentSpeed));
double temperature_inlet = thermoSystem.getTemperature();
double n = 1.0 / (1.0 - (kappa - 1.0) / kappa * 1.0 / (polytropEff / 100.0));
polytropicFluidHead = (getCompressorChart().getHeadUnit().equals("meter")) ? polytropicHead / 1000.0 * 9.81
: polytropicHead;
- if (polytropicFluidHead <= 0.0) {
- polytropicFluidHead = 0.0001;
+ if (!Double.isFinite(polytropicFluidHead) || polytropicFluidHead < 0.0) {
+ break;
}
double pressureRatio = Math.pow(
(polytropicFluidHead * 1000.0
@@ -1336,12 +1386,25 @@ public void run(UUID id) {
/ (n / (n - 1.0) * z_inlet * ThermodynamicConstantsInterface.R * (temperature_inlet) / MW),
n / (n - 1.0));
double currentPressure = thermoSystem.getPressure() * pressureRatio;
+ if (!Double.isFinite(currentPressure) || currentPressure <= 0.0) {
+ break;
+ }
+ double pressureError = Math.abs(currentPressure - targetPressure);
+ if (pressureError < bestPressureError) {
+ bestPressureError = pressureError;
+ bestSpeed = currentSpeed;
+ }
+
+ // Retain an accepted root before attempting another derivative or speed update.
+ if (pressureError <= tolerance) {
+ break;
+ }
// Calculate the derivative of pressure with respect to speed
- double polytropEffDelta = getCompressorChart().getPolytropicEfficiency(actualFlowRate,
- currentSpeed + deltaSpeed);
- double polytropicHeadDelta = getCompressorChart().getPolytropicHead(actualFlowRate,
- currentSpeed + deltaSpeed);
+ double polytropEffDelta = applyInletGuideVaneEfficiency(
+ getCompressorChart().getPolytropicEfficiency(actualFlowRate, currentSpeed + deltaSpeed));
+ double polytropicHeadDelta = applyInletGuideVaneHead(
+ getCompressorChart().getPolytropicHead(actualFlowRate, currentSpeed + deltaSpeed));
double nDelta = 1.0 / (1.0 - (kappa - 1.0) / kappa * 1.0 / (polytropEffDelta / 100.0));
double polytropicFluidHeadDelta = (getCompressorChart().getHeadUnit().equals("meter"))
? polytropicHeadDelta / 1000.0 * 9.81
@@ -1354,6 +1417,9 @@ public void run(UUID id) {
double pressureNew = thermoSystem.getPressure() * pressureRatioDelta;
double dPressure_dSpeed = (pressureNew - currentPressure) / deltaSpeed;
+ if (!Double.isFinite(dPressure_dSpeed)) {
+ break;
+ }
// Handle case where derivative is too small or wrong sign
if (Math.abs(dPressure_dSpeed) < 1e-6) {
@@ -1376,11 +1442,7 @@ public void run(UUID id) {
speedUpdate = Math.signum(speedUpdate) * maxSpeedUpdate;
}
- // Head, efficiency and power below describe currentSpeed. Do not move an already accepted root: doing so
- // reports a different speed and lets repeated runs drift across an equipment capacity boundary.
- if (Math.abs(currentPressure - targetPressure) > tolerance) {
- currentSpeed += relaxationFactor * speedUpdate;
- }
+ currentSpeed += relaxationFactor * speedUpdate;
if (currentSpeed < 0) {
if (minSpeed > 1) {
currentSpeed = minSpeed;
@@ -1392,79 +1454,24 @@ public void run(UUID id) {
deltaSpeed = deltaSpeed / 2;
}
- powerSet = true;
- dH = polytropicFluidHead * 1000.0 * thermoSystem.getMolarMass() / getPolytropicEfficiency()
- * thermoSystem.getTotalNumberOfMoles();
// Check if speed is within bounds
if (currentSpeed < minSpeed || currentSpeed > maxSpeed) {
if (limitSpeed) {
- setSolveSpeed(false);
- setCalcPressureOut(true);
- if (currentSpeed > maxSpeed) {
- setSpeed(maxSpeed);
- } else if (currentSpeed < minSpeed) {
- setSpeed(minSpeed);
- }
- run();
- setSolveSpeed(true);
- setCalcPressureOut(false);
- getThermoSystem().setMultiPhaseCheck(originalMultiPhaseCheck);
+ setSpeed(Math.max(minSpeed, Math.min(maxSpeed, currentSpeed)));
+ runAtSolvedSpeed(id, targetPressure);
return;
- } else {
- // throw new IllegalArgumentException(
- // "Speed out of bounds during Newton-Raphson iteration.");
}
- // throw new IllegalArgumentException(
- // "Speed out of bounds during Newton-Raphson iteration.");
- }
-
- // Check for convergence
- if (Math.abs(currentPressure - targetPressure) <= tolerance) {
- setSpeed(currentSpeed); // Update the final speed
- break;
}
iteration++;
}
- if (iteration == maxIterations) {
- // Did not converge, but use the best estimate and log a warning
- logger.warn("Newton-Raphson speed solver did not fully converge after " + maxIterations
- + " iterations. Using best estimate speed: " + currentSpeed);
- setSpeed(currentSpeed);
- }
-
- // Anti-surge check after speed solver convergence
- if (getAntiSurge().isActive()) {
- double actualFlowRate = thermoSystem.getFlowRate("m3/hr");
- double z_inlet = thermoSystem.getZ();
- if (useGERG2008 && inStream.getThermoSystem().getNumberOfPhases() == 1) {
- double[] gergProps = getThermoSystem().getPhase(0).getProperties_GERG2008();
- actualFlowRate *= gergProps[1] / z_inlet;
- }
- if (useLeachman && inStream.getThermoSystem().getNumberOfPhases() == 1) {
- double[] leachmanProps = getThermoSystem().getPhase(0).getProperties_Leachman();
- actualFlowRate *= leachmanProps[1] / z_inlet;
- }
- if (useVega && inStream.getThermoSystem().getNumberOfPhases() == 1) {
- double[] vegaProps = getThermoSystem().getPhase(0).getProperties_Vega();
- actualFlowRate *= vegaProps[1] / z_inlet;
- }
- surgeCheck = isSurge(polytropicHead, actualFlowRate);
- if (surgeCheck) {
- logger.info("Anti-surge activated in speed-solve mode. Surge flow: "
- + getCompressorChart().getSurgeCurve().getSurgeFlow(polytropicFluidHead) + " Am3/hr, actual flow: "
- + actualFlowRate + " m3/hr");
- thermoSystem.setTotalFlowRate(getAntiSurge().getSurgeControlFactor()
- * getCompressorChart().getSurgeCurve().getSurgeFlow(polytropicFluidHead), "Am3/hr");
- thermoSystem.init(3);
- fractionAntiSurge = thermoSystem.getTotalNumberOfMoles() / orginalMolarFLow - 1.0;
- getAntiSurge().setCurrentSurgeFraction(fractionAntiSurge);
- // Recompute dH with the updated flow
- dH = polytropicFluidHead * 1000.0 * thermoSystem.getMolarMass() / getPolytropicEfficiency()
- * thermoSystem.getTotalNumberOfMoles();
- }
+ if (!Double.isFinite(bestSpeed)) {
+ throw new SpeedSolveException(getName());
}
+ setSpeed(bestSpeed);
+ runAtSolvedSpeed(id, targetPressure);
+ return;
} else {
do {
double actualFlowRate = thermoSystem.getFlowRate("m3/hr");
@@ -1807,6 +1814,41 @@ public void run(UUID id) {
evaluateCapacityConstraints();
}
+ /**
+ * Evaluate the selected speed through the normal fixed-speed path, preserving the requested setpoint and modes. This
+ * keeps pressure, temperature, head and power consistent even when the search misses its target.
+ *
+ * @param id calculation identifier shared with the calling run
+ * @param targetPressure requested absolute pressure in bara
+ */
+ private void runAtSolvedSpeed(UUID id, double targetPressure) {
+ double requestedPressure = pressure;
+ String requestedPressureUnit = pressureUnit;
+ boolean requestedCalcPressureOut = calcPressureOut;
+ try {
+ solveSpeed = false;
+ calcPressureOut = false;
+ // The chart-forward pressure calculation uses bara internally.
+ pressureUnit = "bara";
+ pressure = targetPressure;
+ run(id);
+ speedSolvePressureResidual = outStream.getPressure("bara") - targetPressure;
+ speedSolveConverged = Double.isFinite(speedSolvePressureResidual)
+ && Math.abs(speedSolvePressureResidual) <= SPEED_SOLVE_PRESSURE_TOLERANCE && Double.isFinite(dH)
+ && Double.isFinite(outStream.getTemperature());
+ if (!speedSolveConverged) {
+ logger.warn("Compressor {} speed solve missed target {} bara: achieved {} bara at {} rpm (residual {} bar)",
+ getName(), targetPressure, outStream.getPressure("bara"), speed, speedSolvePressureResidual);
+ }
+ } finally {
+ solveSpeed = true;
+ calcPressureOut = requestedCalcPressureOut;
+ pressure = requestedPressure;
+ pressureUnit = requestedPressureUnit;
+ }
+ updateRecalculationState();
+ }
+
private boolean useEnergyEfficiencyChart() {
return useEnergyEfficiencyChart;
}
@@ -4134,6 +4176,32 @@ public void setSolveSpeed(boolean solveSpeed) {
this.solveSpeed = solveSpeed;
}
+ /**
+ * Check whether the latest speed solve delivered the requested pressure within 0.001 bar. This does not assert
+ * mechanical or operating-envelope feasibility. Returns false before a speed solve or after any other run mode. When
+ * speed limiting is disabled, pressure convergence can occur outside the speed limits.
+ *
+ * @return true only for a finite final speed-solve result within pressure tolerance
+ */
+ public boolean isSpeedSolveConverged() {
+ return speedSolveConverged;
+ }
+
+ /**
+ * Get achieved minus requested outlet pressure for the latest speed solve.
+ *
+ * @return signed pressure residual in bar, or NaN when the latest run produced no speed-solve result
+ */
+ public double getSpeedSolvePressureResidual() {
+ return speedSolvePressureResidual;
+ }
+
+ /** {@inheritDoc} */
+ @Override
+ public boolean solved() {
+ return super.solved() && (!solveSpeed || !compressorChart.isUseCompressorChart() || speedSolveConverged);
+ }
+
/**
* isCalcPressureOut.
*
@@ -4162,7 +4230,9 @@ public boolean isLimitSpeed() {
}
/**
- * Sets whether the compressor speed should be limited.
+ * Sets whether the pressure-target speed search should enforce the configured speed bounds. When false, extrapolation
+ * beyond the bounds is allowed for compatibility. Pressure convergence does not imply that the resulting speed is
+ * mechanically feasible.
*
* @param limitSpeed {@code true} to limit the compressor speed, {@code false} otherwise.
*/
@@ -5670,6 +5740,7 @@ public double getMaxUtilization() {
* Outlet temperature greater than inlet (compression heats gas)
* Pressure ratio greater than 1.0
* Non-NaN values for key properties
+ * Pressure-target convergence when chart speed solving is enabled
* Speed within compressor chart limits (if chart is active)
* Operating point not in surge region (if surge curve is defined)
* Operating point not beyond stonewall/choke (if stonewall curve is defined)
@@ -5677,6 +5748,9 @@ public double getMaxUtilization() {
*/
@Override
public boolean isSimulationValid() {
+ if (solveSpeed && compressorChart.isUseCompressorChart() && !speedSolveConverged) {
+ return false;
+ }
// Check basic thermodynamic system
if (thermoSystem == null) {
return false;
@@ -5779,6 +5853,10 @@ public boolean isSimulationValid() {
@Override
public List getSimulationValidationErrors() {
List errors = new ArrayList();
+ if (solveSpeed && compressorChart.isUseCompressorChart() && !speedSolveConverged) {
+ errors.add(getName() + ": Speed solve did not converge to the requested outlet pressure (residual "
+ + speedSolvePressureResidual + " bar)");
+ }
if (thermoSystem == null) {
errors.add(getName() + ": No thermodynamic system - simulation not run");
diff --git a/src/main/java/neqsim/process/util/optimizer/FlowRateOptimizer.java b/src/main/java/neqsim/process/util/optimizer/FlowRateOptimizer.java
index 3f14140f50c..ba56faa0d8a 100644
--- a/src/main/java/neqsim/process/util/optimizer/FlowRateOptimizer.java
+++ b/src/main/java/neqsim/process/util/optimizer/FlowRateOptimizer.java
@@ -739,15 +739,18 @@ public void configureProcessCompressorCharts() {
}
List compressors = getProcessCompressors();
+ boolean chartsChanged = false;
for (Compressor comp : compressors) {
- configureCompressorChart(comp);
+ chartsChanged |= configureCompressorChart(comp);
}
-
- // Re-run the process after chart setup
- if (mode == Mode.PROCESS_SYSTEM) {
- processSystem.run();
- } else {
- processModel.run();
+ // Existing charts remain valid after a sweep. Its last rejected probe need not be runnable,
+ // and rerunning it here would prevent the next table from evaluating its own operating points.
+ if (chartsChanged) {
+ if (mode == Mode.PROCESS_SYSTEM) {
+ processSystem.run();
+ } else {
+ processModel.run();
+ }
}
// Reinitialize constraints after chart setup
@@ -760,8 +763,9 @@ public void configureProcessCompressorCharts() {
* Configures a single compressor with a performance chart.
*
* @param comp the compressor to configure
+ * @return true when a chart was generated
*/
- private void configureCompressorChart(Compressor comp) {
+ private boolean configureCompressorChart(Compressor comp) {
CompressorChartInterface chart = comp.getCompressorChart();
// Check if chart needs to be generated
@@ -787,7 +791,9 @@ private void configureCompressorChart(Compressor comp) {
logger.info("Chart generated for {} with {} speed lines, max speed set to {} RPM", comp.getName(),
numberOfChartSpeeds, maxSpeed);
+ return true;
}
+ return false;
}
/**
diff --git a/src/main/java/neqsim/process/util/optimizer/ProductionOptimizer.java b/src/main/java/neqsim/process/util/optimizer/ProductionOptimizer.java
index ce561fad7c4..5b256570ed2 100644
--- a/src/main/java/neqsim/process/util/optimizer/ProductionOptimizer.java
+++ b/src/main/java/neqsim/process/util/optimizer/ProductionOptimizer.java
@@ -18,6 +18,7 @@
import org.apache.logging.log4j.LogManager;
import org.apache.logging.log4j.Logger;
import neqsim.process.equipment.ProcessEquipmentInterface;
+import neqsim.process.equipment.compressor.Compressor;
import neqsim.process.equipment.distillation.DistillationColumn;
import neqsim.process.equipment.pipeline.PipeBeggsAndBrills;
import neqsim.process.equipment.stream.StreamInterface;
@@ -1092,7 +1093,8 @@ public OptimizationConfig selectedPointReplays(int replays) {
*
*
* When enabled (default), the optimizer will mark operating points as infeasible if any equipment reports invalid
- * simulation results (e.g., negative power in compressors, NaN values, etc.).
+ * simulation results (e.g., negative power in compressors, NaN values, etc.). Failed compressor pressure-target
+ * solves are always rejected, independently of this optional validation.
*
*
* @param reject true to reject invalid simulations
@@ -3370,6 +3372,16 @@ private Evaluation evaluateProcess(ProcessSystem process, OptimizationConfig con
boolean simulationValid = true;
List validationErrors = new ArrayList<>();
for (ProcessEquipmentInterface unit : process.getUnitOperations()) {
+ if (unit instanceof Compressor) {
+ Compressor compressor = (Compressor) unit;
+ if (compressor.isSolveSpeed() && compressor.getCompressorChart().isUseCompressorChart()
+ && !compressor.isSpeedSolveConverged()) {
+ // A missed pressure target is a failed solve, even when capacity analysis is enabled or optional
+ // output validation is disabled. Envelope constraints cannot certify an unconverged calculation.
+ return failedSpeedSolveEvaluation(decisionVariables,
+ "Speed solve did not converge for " + compressor.getName());
+ }
+ }
if (!unit.isSimulationValid()) {
// If equipment has capacity constraints enabled, let the constraint system
// handle envelope violations through high utilization rather than rejecting
@@ -4085,10 +4097,38 @@ private Evaluation evaluateCandidateInternal(ProcessSystem process, List decisions, String diagnostic) {
+ ConstraintStatus status = new ConstraintStatus("compressorSpeedSolve", ConstraintSeverity.HARD, -1.0, 0.0,
+ diagnostic);
+ return new Evaluation(9.99, null, Collections.emptyList(), Collections.singletonList(status),
+ Collections.emptyMap(), decisions, false, false, Double.NEGATIVE_INFINITY);
+ }
+
private String buildVectorCacheKey(double[] candidate) {
// A convergence tolerance is not an evidence-equivalence tolerance. Nearby points can straddle a hard limit,
// and each cached evaluation retains the exact decisions that produced it.
diff --git a/src/main/resources/neqsim/mcp/engineering-coverage.json b/src/main/resources/neqsim/mcp/engineering-coverage.json
index c35f6c84261..37036214278 100644
--- a/src/main/resources/neqsim/mcp/engineering-coverage.json
+++ b/src/main/resources/neqsim/mcp/engineering-coverage.json
@@ -2403,7 +2403,7 @@
{"id":"neqsim.process.equipment.compressor.BoundaryCurve","kind":"class","source":"src/main/java/neqsim/process/equipment/compressor/BoundaryCurve.java","sourceDigest":"0727c7d3b435ba59d443af8b21acad1e77aaee3cd17c1e1d06da4c6ad8f62f4e","domain":"process","skills":[],"agents":[],"capabilities":[],"disposition":"review_required"},
{"id":"neqsim.process.equipment.compressor.BoundaryCurveInterface","kind":"interface","source":"src/main/java/neqsim/process/equipment/compressor/BoundaryCurveInterface.java","sourceDigest":"135b4afddd9df72f762fbc3c32c175e172bb59a7ad67a7a1df68b4dc3deaf09d","domain":"process","skills":[],"agents":[],"capabilities":[],"disposition":"review_required"},
{"id":"neqsim.process.equipment.compressor.CompressionTrainAssessment","kind":"class","source":"src/main/java/neqsim/process/equipment/compressor/CompressionTrainAssessment.java","sourceDigest":"f28943d6e13d41e863c38733f522bfeba7bad5e5d451ef5fc1bdf81427d00a9e","domain":"process","skills":[],"agents":[],"capabilities":[],"disposition":"review_required"},
- {"id":"neqsim.process.equipment.compressor.Compressor","kind":"class","source":"src/main/java/neqsim/process/equipment/compressor/Compressor.java","sourceDigest":"6b4344b142b0acd47b13702d4c09df8e31bb838d9229d6cec1ff410859e65da1","domain":"process","skills":["neqsim-advanced-control-mpc-and-virtual-sensing","neqsim-agentic-process-optimization","neqsim-api-patterns","neqsim-autonomous-investigation","neqsim-capability-map","neqsim-capacity-and-utilization-analysis","neqsim-capacity-increase-screening","neqsim-ccs-hydrogen","neqsim-compressor-antisurge-recycle","neqsim-controllability-operability","neqsim-dynamic-equipment-implementation","neqsim-dynamic-simulation","neqsim-hazid-fmea-eta-fta","neqsim-input-validation","neqsim-model-calibration-and-data-reconciliation","neqsim-notebook-patterns","neqsim-operational-risk-and-safety-validation","neqsim-optimization-and-doe","neqsim-physics-explanations","neqsim-pid-process-operations","neqsim-pipeline-and-terminal-networks","neqsim-plant-data","neqsim-platform-modeling","neqsim-power-generation","neqsim-process-extraction","neqsim-process-modeling","neqsim-process-safety","neqsim-production-optimization","neqsim-regression-baselines","neqsim-root-cause-analysis","neqsim-rotating-equipment-design","neqsim-standards-lookup","neqsim-stid-retriever","neqsim-task-workflow","neqsim-technical-document-reading","neqsim-troubleshooting","neqsim-unisim-reader","neqsim-utility-design"],"agents":["capability-scout","capacity-increase-screening","ccs-hydrogen","consequence-analysis","continuous-improvement","control-system","documentation","dynamic-equipment","emissions-environmental","engineering-deliverables","extract-process","field-development","flow-assurance","gas-processing-refining","gas-quality","literature-scout","lng-value-chain","mechanical-design","neqsim-test","network-energy-systems","notebook-example","optimize","optimize-processmodel","plant-data","process-model","production-chemistry","pvt-simulation","reaction-engineering","root-cause","rotating-equipment","safety-depressuring","solve-process","solve-task","standards-review","technical-reader","thermo-fluid","unisim-reader","utility-design"],"capabilities":["process-compression","equipment-sizing"],"disposition":"registered_anchor"},
+ {"id":"neqsim.process.equipment.compressor.Compressor","kind":"class","source":"src/main/java/neqsim/process/equipment/compressor/Compressor.java","sourceDigest":"494b76dd39a7652fd848a43b37f6c4d582c3d219bfb13c43035362e205d41b76","domain":"process","skills":["neqsim-advanced-control-mpc-and-virtual-sensing","neqsim-agentic-process-optimization","neqsim-api-patterns","neqsim-autonomous-investigation","neqsim-capability-map","neqsim-capacity-and-utilization-analysis","neqsim-capacity-increase-screening","neqsim-ccs-hydrogen","neqsim-compressor-antisurge-recycle","neqsim-controllability-operability","neqsim-dynamic-equipment-implementation","neqsim-dynamic-simulation","neqsim-hazid-fmea-eta-fta","neqsim-input-validation","neqsim-model-calibration-and-data-reconciliation","neqsim-notebook-patterns","neqsim-operational-risk-and-safety-validation","neqsim-optimization-and-doe","neqsim-physics-explanations","neqsim-pid-process-operations","neqsim-pipeline-and-terminal-networks","neqsim-plant-data","neqsim-platform-modeling","neqsim-power-generation","neqsim-process-extraction","neqsim-process-modeling","neqsim-process-safety","neqsim-production-optimization","neqsim-regression-baselines","neqsim-root-cause-analysis","neqsim-rotating-equipment-design","neqsim-standards-lookup","neqsim-stid-retriever","neqsim-task-workflow","neqsim-technical-document-reading","neqsim-troubleshooting","neqsim-unisim-reader","neqsim-utility-design"],"agents":["capability-scout","capacity-increase-screening","ccs-hydrogen","consequence-analysis","continuous-improvement","control-system","documentation","dynamic-equipment","emissions-environmental","engineering-deliverables","extract-process","field-development","flow-assurance","gas-processing-refining","gas-quality","literature-scout","lng-value-chain","mechanical-design","neqsim-test","network-energy-systems","notebook-example","optimize","optimize-processmodel","plant-data","process-model","production-chemistry","pvt-simulation","reaction-engineering","root-cause","rotating-equipment","safety-depressuring","solve-process","solve-task","standards-review","technical-reader","thermo-fluid","unisim-reader","utility-design"],"capabilities":["process-compression","equipment-sizing"],"disposition":"registered_anchor"},
{"id":"neqsim.process.equipment.compressor.CompressorAntiSurgeApplication","kind":"class","source":"src/main/java/neqsim/process/equipment/compressor/CompressorAntiSurgeApplication.java","sourceDigest":"5aa397c2c4fd56e0511650727a3e7fe28f2cda6b6d5bea5deaa25b1298192186","domain":"process","skills":["neqsim-dynamic-simulation","neqsim-process-modeling"],"agents":["capability-scout","capacity-increase-screening","control-system","dynamic-equipment","engineering-deliverables","gas-processing-refining","lng-value-chain","mechanical-design","neqsim-test","network-energy-systems","notebook-example","process-model","rotating-equipment","solve-process","solve-task","unisim-reader"],"capabilities":[],"disposition":"review_required"},
{"id":"neqsim.process.equipment.compressor.CompressorCatalog","kind":"class","source":"src/main/java/neqsim/process/equipment/compressor/CompressorCatalog.java","sourceDigest":"26260299b788fa7cb5ddb61e2cab0c593fe3d36f842be46896d73084188c8791","domain":"process","skills":[],"agents":[],"capabilities":[],"disposition":"review_required"},
{"id":"neqsim.process.equipment.compressor.CompressorCatalogEntry","kind":"class","source":"src/main/java/neqsim/process/equipment/compressor/CompressorCatalogEntry.java","sourceDigest":"069d425795887c2b7af82ab4388a6132244bef34a1b95fe3ed3a5e97c00b416f","domain":"process","skills":[],"agents":[],"capabilities":[],"disposition":"review_required"},
@@ -4055,7 +4055,7 @@
{"id":"neqsim.process.util.optimizer.EclipseVFPExporter","kind":"class","source":"src/main/java/neqsim/process/util/optimizer/EclipseVFPExporter.java","sourceDigest":"65f2878ffb1b401471bf26c3c9253941fc4fa30d50784b76cec8c0993282c843","domain":"process","skills":["neqsim-optimization-and-doe","neqsim-production-optimization"],"agents":["capacity-increase-screening","field-development","optimize","optimize-processmodel"],"capabilities":[],"disposition":"review_required"},
{"id":"neqsim.process.util.optimizer.EquipmentCapacityConstraintResolver","kind":"class","source":"src/main/java/neqsim/process/util/optimizer/EquipmentCapacityConstraintResolver.java","sourceDigest":"5f006f0b472c6e0f579bf68b7ef42bfa3d46e55b71677b4814f30d5760f9176a","domain":"process","skills":[],"agents":[],"capabilities":[],"disposition":"review_required"},
{"id":"neqsim.process.util.optimizer.FlowRateOptimizationResult","kind":"class","source":"src/main/java/neqsim/process/util/optimizer/FlowRateOptimizationResult.java","sourceDigest":"f75b7b79dcf04d55d70790be1c73afd69279ef4a5c1712c9ee3cecbbc1d38766","domain":"process","skills":[],"agents":[],"capabilities":[],"disposition":"review_required"},
- {"id":"neqsim.process.util.optimizer.FlowRateOptimizer","kind":"class","source":"src/main/java/neqsim/process/util/optimizer/FlowRateOptimizer.java","sourceDigest":"a669f4c5dff6e175d1a47522c82f4f87ea3e271aa98446cfb0c22efa52a79fb7","domain":"process","skills":["neqsim-optimization-and-doe","neqsim-production-optimization"],"agents":["capacity-increase-screening","field-development","optimize","optimize-processmodel"],"capabilities":[],"disposition":"review_required"},
+ {"id":"neqsim.process.util.optimizer.FlowRateOptimizer","kind":"class","source":"src/main/java/neqsim/process/util/optimizer/FlowRateOptimizer.java","sourceDigest":"8e7b47bd473bae55cd55099019f98f78966dca699caffb2a7d08e6baf7fba9cc","domain":"process","skills":["neqsim-optimization-and-doe","neqsim-production-optimization"],"agents":["capacity-increase-screening","field-development","optimize","optimize-processmodel"],"capabilities":[],"disposition":"review_required"},
{"id":"neqsim.process.util.optimizer.FluidMagicInput","kind":"class","source":"src/main/java/neqsim/process/util/optimizer/FluidMagicInput.java","sourceDigest":"5371bf7e97feef00613b31110974a23d49335298d7f261f6027082cc6d269df7","domain":"process","skills":[],"agents":[],"capabilities":[],"disposition":"review_required"},
{"id":"neqsim.process.util.optimizer.InstalledCapacityTableLoader","kind":"class","source":"src/main/java/neqsim/process/util/optimizer/InstalledCapacityTableLoader.java","sourceDigest":"5101262d5be8368204476a7403969ed7f3634780faa34f87f864ebe4a1cda011","domain":"process","skills":[],"agents":[],"capabilities":[],"disposition":"review_required"},
{"id":"neqsim.process.util.optimizer.InstalledEquipmentCapacityEvidence","kind":"class","source":"src/main/java/neqsim/process/util/optimizer/InstalledEquipmentCapacityEvidence.java","sourceDigest":"24d1dc5d4d22b5243491c16a21e4bfd8b58dd83818c9c046ebfcd99f51fc2c26","domain":"process","skills":[],"agents":[],"capabilities":[],"disposition":"review_required"},
@@ -4103,7 +4103,7 @@
{"id":"neqsim.process.util.optimizer.ProductionImpactAnalyzer","kind":"class","source":"src/main/java/neqsim/process/util/optimizer/ProductionImpactAnalyzer.java","sourceDigest":"3561a9106dcdc41ee3cd670cf8cc1fb536afeba5219687a8d363806b5b931b85","domain":"process","skills":["neqsim-optimization-and-doe"],"agents":["capacity-increase-screening","optimize","optimize-processmodel"],"capabilities":[],"disposition":"review_required"},
{"id":"neqsim.process.util.optimizer.ProductionImpactResult","kind":"class","source":"src/main/java/neqsim/process/util/optimizer/ProductionImpactResult.java","sourceDigest":"ba42639101f15c7a96975b56ed21c16d2f1ea9b2ccfc50666c92869c0faab49a","domain":"process","skills":[],"agents":[],"capabilities":[],"disposition":"review_required"},
{"id":"neqsim.process.util.optimizer.ProductionOptimizationSpecLoader","kind":"class","source":"src/main/java/neqsim/process/util/optimizer/ProductionOptimizationSpecLoader.java","sourceDigest":"b33713b2e355cdbb37bdbf4ac58e99270eae9bb34a5d63ca782b4e5cb4173702","domain":"process","skills":["neqsim-optimization-and-doe"],"agents":["capacity-increase-screening","optimize","optimize-processmodel"],"capabilities":[],"disposition":"review_required"},
- {"id":"neqsim.process.util.optimizer.ProductionOptimizer","kind":"class","source":"src/main/java/neqsim/process/util/optimizer/ProductionOptimizer.java","sourceDigest":"1409441c09e025f7d402987d9f6c397a39c376f63761ea7c2cd2225d92af0de9","domain":"process","skills":["neqsim-capacity-and-utilization-analysis","neqsim-optimization-and-doe"],"agents":["capacity-increase-screening","network-energy-systems","optimize","optimize-processmodel"],"capabilities":[],"disposition":"review_required"},
+ {"id":"neqsim.process.util.optimizer.ProductionOptimizer","kind":"class","source":"src/main/java/neqsim/process/util/optimizer/ProductionOptimizer.java","sourceDigest":"faba52e13ac98e74a74d5931ba9b1e228eeaf84f3846f6d486351a136695788c","domain":"process","skills":["neqsim-capacity-and-utilization-analysis","neqsim-compressor-antisurge-recycle","neqsim-optimization-and-doe"],"agents":["capacity-increase-screening","network-energy-systems","optimize","optimize-processmodel","rotating-equipment"],"capabilities":[],"disposition":"review_required"},
{"id":"neqsim.process.util.optimizer.RecombinationFlashGenerator","kind":"class","source":"src/main/java/neqsim/process/util/optimizer/RecombinationFlashGenerator.java","sourceDigest":"2d73d45843e79399846948144ae60c6d41bf4279a8371b195315bd8239a4de8e","domain":"process","skills":["neqsim-production-optimization"],"agents":["field-development"],"capabilities":[],"disposition":"review_required"},
{"id":"neqsim.process.util.optimizer.SQPoptimizer","kind":"class","source":"src/main/java/neqsim/process/util/optimizer/SQPoptimizer.java","sourceDigest":"b22c69868b5eeb0b5fa24b689cf618dfec3790ba4feecb0b3f65b7f100b1f0e6","domain":"process","skills":["neqsim-agentic-process-optimization","neqsim-capacity-and-utilization-analysis","neqsim-optimization-and-doe"],"agents":["capacity-increase-screening","continuous-improvement","network-energy-systems","optimize","optimize-processmodel"],"capabilities":[],"disposition":"review_required"},
{"id":"neqsim.process.util.optimizer.SensitivityAnalysis","kind":"class","source":"src/main/java/neqsim/process/util/optimizer/SensitivityAnalysis.java","sourceDigest":"379d98d0c6dfbb4c9d401a198d13f3555c4c90f0bf89819dfd47bff389da9099","domain":"process","skills":["neqsim-optimization-and-doe"],"agents":["capacity-increase-screening","optimize","optimize-processmodel"],"capabilities":[],"disposition":"review_required"},
diff --git a/src/test/java/neqsim/process/equipment/compressor/CompressorChartTest.java b/src/test/java/neqsim/process/equipment/compressor/CompressorChartTest.java
index 70f7eee36e6..c24413a59d5 100644
--- a/src/test/java/neqsim/process/equipment/compressor/CompressorChartTest.java
+++ b/src/test/java/neqsim/process/equipment/compressor/CompressorChartTest.java
@@ -465,8 +465,10 @@ public void runCurveTestMaxMinSpeed() {
comp1.setSpeed(15000);
comp1.setLimitSpeed(false);
comp1.run();
- Assertions.assertEquals(106.16307446, comp1.getOutletStream().getPressure("bara"), 0.1);
- Assertions.assertEquals(7000, comp1.getSpeed(), 0.1);
+ // The limited run must retain the requested 100 bara target for the next solve.
+ Assertions.assertEquals(100.0, comp1.getOutletStream().getPressure("bara"), 0.001);
+ Assertions.assertTrue(comp1.getSpeed() < comp1.getMinimumSpeed());
+ Assertions.assertTrue(comp1.isSpeedSolveConverged());
comp1.setOutletPressure(90);
comp1.setMinimumSpeed(0);
diff --git a/src/test/java/neqsim/process/equipment/compressor/CompressorSpeedSolveConsistencyTest.java b/src/test/java/neqsim/process/equipment/compressor/CompressorSpeedSolveConsistencyTest.java
new file mode 100644
index 00000000000..25771c57316
--- /dev/null
+++ b/src/test/java/neqsim/process/equipment/compressor/CompressorSpeedSolveConsistencyTest.java
@@ -0,0 +1,285 @@
+package neqsim.process.equipment.compressor;
+
+import static org.junit.jupiter.api.Assertions.assertEquals;
+import static org.junit.jupiter.api.Assertions.assertFalse;
+import static org.junit.jupiter.api.Assertions.assertThrows;
+import static org.junit.jupiter.api.Assertions.assertTrue;
+import java.util.UUID;
+import org.junit.jupiter.api.Test;
+import org.junit.jupiter.params.ParameterizedTest;
+import org.junit.jupiter.params.provider.ValueSource;
+import neqsim.NeqSimTest;
+import neqsim.process.equipment.stream.Stream;
+import neqsim.thermo.system.SystemSrkEos;
+
+/**
+ * Regression coverage for speed-solve pressure consistency (issue #4321).
+ *
+ * @author Even Solbraa
+ * @version 1.0
+ */
+class CompressorSpeedSolveConsistencyTest extends NeqSimTest {
+ /**
+ * Build the public synthetic chart and fluid from the issue.
+ *
+ * @return a configured fixed-speed compressor
+ */
+ private Compressor createCompressor() {
+ return createCompressor("kJ/kg");
+ }
+
+ /**
+ * Build equivalent chart data in either supported head unit.
+ *
+ * @param headUnit chart head unit, kJ/kg or meter
+ * @return configured compressor
+ */
+ private Compressor createCompressor(String headUnit) {
+ SystemSrkEos fluid = new SystemSrkEos(300.0, 4.0);
+ fluid.addComponent("methane", 0.7);
+ fluid.addComponent("ethane", 0.2);
+ fluid.addComponent("propane", 0.1);
+ fluid.setMixingRule("classic");
+ Stream feed = new Stream("feed", fluid);
+ feed.setFlowRate(1000.0, "kg/hr");
+ feed.run();
+ Compressor compressor = new Compressor("compressor", feed);
+ compressor.setUsePolytropicCalc(true);
+ double[][] flow = {{100.0, 140.0, 180.0}, {200.0, 260.0, 320.0}, {300.0, 380.0, 460.0}};
+ double[][] head = {{100.0, 80.0, 60.0}, {180.0, 140.0, 100.0}, {260.0, 200.0, 140.0}};
+ if ("meter".equals(headUnit)) {
+ for (double[] curve : head) {
+ for (int i = 0; i < curve.length; i++) {
+ curve[i] *= 1000.0 / 9.81;
+ }
+ }
+ }
+ double[][] efficiency = {{70.0, 71.0, 70.0}, {72.0, 73.0, 72.0}, {74.0, 75.0, 74.0}};
+ compressor.getCompressorChart().setCurves(new double[] {25.0, 300.0, 1.0, 1.0},
+ new double[] {1000.0, 2000.0, 3000.0}, flow, head, flow, efficiency);
+ compressor.getCompressorChart().setHeadUnit(headUnit);
+ compressor.setSpeed(2000.0);
+ compressor.setMinimumSpeed(1000.0);
+ compressor.setMaximumSpeed(3000.0);
+ return compressor;
+ }
+
+ /**
+ * A returned result must agree with a separate fixed-speed calculation, including failed searches.
+ *
+ * @param target requested pressure in bara
+ */
+ @ParameterizedTest
+ @ValueSource(doubles = {6.0, 8.0, 14.0})
+ void outletMatchesForwardReplay(double target) {
+ Compressor compressor = createCompressor();
+ compressor.setOutletPressure(target, "bara");
+ compressor.setSolveSpeed(true);
+ UUID id = UUID.randomUUID();
+ compressor.run(id);
+ Compressor forward = createCompressor();
+ forward.setSpeed(compressor.getSpeed());
+ forward.run();
+ assertEquals(forward.getOutletStream().getPressure("bara"), compressor.getOutletStream().getPressure("bara"),
+ 1.0e-9);
+ assertEquals(forward.getPower(), compressor.getPower(), 1.0e-6);
+ assertEquals(forward.getOutletStream().getTemperature(), compressor.getOutletStream().getTemperature(), 1.0e-7);
+ assertEquals(target == 14.0, compressor.isSpeedSolveConverged());
+ assertEquals(target == 14.0, compressor.solved());
+ assertEquals(target == 14.0, compressor.isSimulationValid());
+ if (target != 14.0) {
+ assertTrue(compressor.getSimulationValidationErrors().stream()
+ .anyMatch(error -> error.contains("Speed solve did not converge")));
+ }
+ assertEquals(compressor.getOutletStream().getPressure("bara") - target, compressor.getSpeedSolvePressureResidual(),
+ 1.0e-12);
+ assertEquals(target, compressor.getOutletPressure(), 0.0);
+ assertEquals(id, compressor.getCalculationIdentifier());
+ assertEquals(id, compressor.getOutletStream().getCalculationIdentifier());
+ assertFalse(compressor.needRecalculation());
+ assertTrue(Double.isFinite(compressor.getPower()));
+ assertEquals(1000.0, compressor.getOutletStream().getFlowRate("kg/hr"), 1.0e-6);
+ org.apache.logging.log4j.LogManager.getLogger(getClass()).info(
+ "Target {} bara: speed {} rpm, achieved {} bara, converged {}", target, compressor.getSpeed(),
+ compressor.getOutletStream().getPressure("bara"), compressor.isSpeedSolveConverged());
+ }
+
+ /** Failed searches must not replace the target or leave stale successful diagnostics. */
+ @Test
+ void repeatedRunsKeepTargetAndResetDiagnostics() {
+ Compressor compressor = createCompressor();
+ assertFalse(compressor.isSpeedSolveConverged());
+ assertTrue(Double.isNaN(compressor.getSpeedSolvePressureResidual()));
+ compressor.setSolveSpeed(true);
+ for (double target : new double[] {6.0, 14.0, 8.0, 14.0}) {
+ compressor.setOutletPressure(target, "bara");
+ compressor.run();
+ assertEquals(target == 14.0, compressor.isSpeedSolveConverged());
+ compressor.run();
+ assertEquals(target, compressor.getOutletPressure(), 0.0);
+ assertEquals(target == 14.0, compressor.isSpeedSolveConverged());
+ }
+ compressor.setSolveSpeed(false);
+ compressor.run();
+ assertFalse(compressor.isSpeedSolveConverged());
+ assertTrue(Double.isNaN(compressor.getSpeedSolvePressureResidual()));
+ assertTrue(compressor.solved());
+ }
+
+ /**
+ * A bounded solve must publish the boundary's forward result without losing the target or run mode.
+ *
+ * @param target requested pressure in bara
+ */
+ @ParameterizedTest
+ @ValueSource(doubles = {6.0, 100.0})
+ void speedLimitProducesConsistentFailure(double target) {
+ Compressor compressor = createCompressor();
+ compressor.setMinimumSpeed(2000.0);
+ compressor.setMaximumSpeed(2000.0);
+ compressor.setLimitSpeed(true);
+ compressor.setSolveSpeed(true);
+ compressor.setOutletPressure(target, "bara");
+ compressor.run();
+ Compressor forward = createCompressor();
+ forward.run();
+ assertEquals(2000.0, compressor.getSpeed(), 0.0);
+ assertEquals(forward.getOutletStream().getPressure("bara"), compressor.getOutletStream().getPressure("bara"),
+ 1.0e-9);
+ assertEquals(forward.getPower(), compressor.getPower(), 1.0e-6);
+ assertFalse(compressor.isSpeedSolveConverged());
+ assertFalse(compressor.solved());
+ assertTrue(compressor.isSolveSpeed());
+ assertFalse(compressor.isCalcPressureOut());
+ assertEquals(target, compressor.getOutletPressure(), 0.0);
+ compressor.run();
+ assertFalse(compressor.isSpeedSolveConverged());
+ assertEquals(target, compressor.getOutletPressure(), 0.0);
+ }
+
+ /** Disabling limits retains the documented ability to extrapolate beyond the speed bounds. */
+ @Test
+ void unlimitedSearchCanConvergeOutsideBounds() {
+ Compressor compressor = createCompressor();
+ compressor.setMaximumSpeed(2100.0);
+ compressor.setOutletPressure(14.0, "bara");
+ compressor.setSolveSpeed(true);
+ compressor.run();
+ assertTrue(compressor.isSpeedSolveConverged());
+ assertTrue(compressor.getSpeed() > compressor.getMaximumSpeed());
+ }
+
+ /** Setpoint conversion and residuals must use absolute pressure while retaining the caller's unit. */
+ @Test
+ void targetInMpaMatchesBaraAndSurvivesReplay() {
+ Compressor compressor = createCompressor();
+ compressor.setOutletPressure(1.4, "MPa");
+ compressor.setSolveSpeed(true);
+ for (int i = 0; i < 2; i++) {
+ compressor.run();
+ assertTrue(compressor.isSpeedSolveConverged());
+ assertEquals(14.0, compressor.getOutletStream().getPressure("bara"), 0.001);
+ assertEquals(1.4, compressor.getOutletPressure(), 0.0);
+ assertEquals(compressor.getOutletStream().getPressure("bara") - 14.0, compressor.getSpeedSolvePressureResidual(),
+ 1.0e-12);
+ }
+ }
+
+ /** A guide-vane correction must be included in both the search and its final forward result. */
+ @Test
+ void guideVaneSolveMatchesForwardModel() {
+ Compressor forward = createCompressor();
+ forward.setInletGuideVaneOpening(0.9);
+ forward.setSpeed(2200.0);
+ forward.run();
+ Compressor compressor = createCompressor();
+ compressor.setInletGuideVaneOpening(0.9);
+ compressor.setOutletPressure(forward.getOutletStream().getPressure("bara"), "bara");
+ compressor.setSolveSpeed(true);
+ compressor.run();
+ assertTrue(compressor.isSpeedSolveConverged());
+ assertEquals(2200.0, compressor.getSpeed(), 0.2);
+ }
+
+ /** Anti-surge flow changes must be reflected in the final state and pressure-convergence decision. */
+ @Test
+ void antiSurgeResultMatchesForwardReplay() {
+ Compressor compressor = createCompressor();
+ Compressor forward = createCompressor();
+ for (Compressor machine : new Compressor[] {compressor, forward}) {
+ machine.getCompressorChart().getSurgeCurve().setCurve(new double[] {25.0, 300.0, 1.0, 1.0},
+ new double[] {390.0, 400.0, 410.0}, new double[] {100.0, 200.0, 300.0});
+ machine.getAntiSurge().setActive(true);
+ }
+ compressor.setOutletPressure(14.0, "bara");
+ compressor.setSolveSpeed(true);
+ compressor.run();
+ forward.setSpeed(compressor.getSpeed());
+ forward.run();
+ assertEquals(forward.getOutletStream().getPressure("bara"), compressor.getOutletStream().getPressure("bara"),
+ 1.0e-9);
+ assertEquals(forward.getPower(), compressor.getPower(), 1.0e-6);
+ assertFalse(compressor.isSpeedSolveConverged());
+ assertTrue(compressor.getAntiSurge().getCurrentSurgeFraction() > 0.0);
+ assertEquals(1000.0, compressor.getOutletStream().getFlowRate("kg/hr"), 1.0e-6);
+ }
+
+ /** Invalid targets and invalid chart values cannot reuse a previous success. */
+ @Test
+ void invalidSearchDoesNotReportConvergence() {
+ Compressor compressor = createCompressor();
+ compressor.setOutletPressure(14.0, "bara");
+ compressor.setSolveSpeed(true);
+ compressor.run();
+ assertTrue(compressor.isSpeedSolveConverged());
+ compressor.setOutletPressure(Double.NaN, "bara");
+ assertThrows(IllegalArgumentException.class, () -> compressor.run());
+ assertFalse(compressor.isSpeedSolveConverged());
+ assertTrue(Double.isNaN(compressor.getSpeedSolvePressureResidual()));
+ compressor.setOutletPressure(14.0, "bara");
+ compressor.setSpeed(Double.NaN);
+ assertThrows(IllegalStateException.class, () -> compressor.run());
+ assertFalse(compressor.isSpeedSolveConverged());
+ assertTrue(compressor.isSolveSpeed());
+ }
+
+ /** Changing bounds or setpoint units must invalidate the cached pressure-target calculation. */
+ @Test
+ void speedLimitsAndUnitsInvalidateCachedResult() {
+ Compressor compressor = createCompressor();
+ compressor.setSolveSpeed(true);
+ compressor.setOutletPressure(14.0, "bara");
+ compressor.run();
+ assertFalse(compressor.needRecalculation());
+ compressor.setMaximumSpeed(2100.0);
+ assertTrue(compressor.needRecalculation());
+ compressor.run();
+ compressor.setMinimumSpeed(1100.0);
+ assertTrue(compressor.needRecalculation());
+ compressor.run();
+ compressor.setLimitSpeed(true);
+ assertTrue(compressor.needRecalculation());
+ compressor.run();
+ assertFalse(compressor.isSpeedSolveConverged());
+ assertEquals(14.0, compressor.getOutletPressure(), 0.0);
+ compressor.setOutletPressure(14.0, "barg");
+ assertTrue(compressor.needRecalculation());
+ }
+
+ /** Equivalent meter and kJ/kg maps must give the same solve result and forward power. */
+ @Test
+ void meterHeadMapMatchesEnergyHeadMap() {
+ Compressor energyHead = createCompressor();
+ Compressor meterHead = createCompressor("meter");
+ for (Compressor compressor : new Compressor[] {energyHead, meterHead}) {
+ compressor.setOutletPressure(14.0, "bara");
+ compressor.setSolveSpeed(true);
+ compressor.run();
+ assertTrue(compressor.isSpeedSolveConverged());
+ }
+ assertEquals(energyHead.getSpeed(), meterHead.getSpeed(), 1.0e-8);
+ assertEquals(energyHead.getPower(), meterHead.getPower(), 1.0e-6);
+ assertEquals(energyHead.getOutletStream().getPressure("bara"), meterHead.getOutletStream().getPressure("bara"),
+ 1.0e-9);
+ }
+}
diff --git a/src/test/java/neqsim/process/util/optimizer/BottleneckAnalysisOptimizerTest.java b/src/test/java/neqsim/process/util/optimizer/BottleneckAnalysisOptimizerTest.java
index 4335ac53a60..2eb9fa601c1 100644
--- a/src/test/java/neqsim/process/util/optimizer/BottleneckAnalysisOptimizerTest.java
+++ b/src/test/java/neqsim/process/util/optimizer/BottleneckAnalysisOptimizerTest.java
@@ -409,7 +409,8 @@ public void testCompressorInvalidWhenOutsideChartRange() {
boolean processRejectedExtremeFlow = processRunFailure != null && processRunFailure.getMessage() != null
&& processRunFailure.getMessage().contains("Failed to run unit operation")
&& (processRunFailure.getMessage().contains("Compressor Outlet Manifold")
- || rejectedNegativeOutletPipePressure(processRunFailure));
+ || rejectedNegativeOutletPipePressure(processRunFailure)
+ || rejectedInvalidCompressorMap(processRunFailure));
if (processRunFailure != null && !processRejectedExtremeFlow) {
throw processRunFailure;
}
@@ -429,6 +430,7 @@ public void testCompressorInvalidWhenOutsideChartRange() {
// At very high flow, compressors should either:
// - Have zero/negative polytropic head
// - Have speed outside chart range
+ // - Reject a pressure-target search with no finite chart operating point
// - Cause the downstream manifold run to reject the infeasible operating point
Assertions.assertTrue(anyInvalid || !ups3Errors.isEmpty() || processRejectedExtremeFlow,
"At extreme flow rates, compressors should show invalid simulation or validation errors");
@@ -455,6 +457,26 @@ private boolean rejectedNegativeOutletPipePressure(RuntimeException failure) {
return false;
}
+ /**
+ * Recognizes a failed compressor speed search at extreme flow, without accepting unrelated process failures.
+ *
+ * @param failure process run failure
+ * @return true only for a named compressor with the specific invalid-map cause and failed convergence diagnostics
+ */
+ private boolean rejectedInvalidCompressorMap(RuntimeException failure) {
+ for (Compressor compressor : new Compressor[] {ups1Comp, ups2Comp, ups3Comp}) {
+ if (!failure.getMessage().equals("Failed to run unit operation " + compressor.getName())) {
+ continue;
+ }
+ Throwable cause = failure.getCause();
+ if (cause instanceof Compressor.SpeedSolveException && cause.getMessage()
+ .equals("Speed solver found no finite chart operating point for " + compressor.getName())) {
+ return !compressor.isSpeedSolveConverged() && !compressor.solved() && !compressor.isSimulationValid();
+ }
+ }
+ return false;
+ }
+
/**
* Tests production optimizer with BINARY_FEASIBILITY search mode. This test validates that the optimizer properly
* detects and handles compressor operating envelope violations through isSimulationValid().
diff --git a/src/test/java/neqsim/process/util/optimizer/FlowRateOptimizerTest.java b/src/test/java/neqsim/process/util/optimizer/FlowRateOptimizerTest.java
index b7aecdd037f..f5b4c89d4bf 100644
--- a/src/test/java/neqsim/process/util/optimizer/FlowRateOptimizerTest.java
+++ b/src/test/java/neqsim/process/util/optimizer/FlowRateOptimizerTest.java
@@ -1,6 +1,7 @@
package neqsim.process.util.optimizer;
import static org.junit.jupiter.api.Assertions.assertEquals;
+import static org.junit.jupiter.api.Assertions.assertDoesNotThrow;
import static org.junit.jupiter.api.Assertions.assertFalse;
import static org.junit.jupiter.api.Assertions.assertThrows;
import static org.junit.jupiter.api.Assertions.assertNotNull;
@@ -836,6 +837,19 @@ private ProcessSystem createTwoStageCompressionProcess() {
// ============ Professional Lift Curve Generation Tests ============
+ /** Existing maps can be reused after a sweep has left the process at an invalid trial. */
+ @Test
+ public void chartSetupDoesNotRerunRejectedTrialWhenMapsAlreadyExist() {
+ ProcessSystem process = createTwoStageCompressionProcess();
+ FlowRateOptimizer optimizer = new FlowRateOptimizer(process, "Feed Stream", "2nd Stage Compressor");
+ optimizer.configureProcessCompressorCharts();
+ Stream feed = (Stream) process.getUnit("Feed Stream");
+ feed.setFlowRate(1.0e9, "kg/hr");
+ RuntimeException failure = assertThrows(RuntimeException.class, () -> process.run());
+ assertTrue(failure.getCause() instanceof Compressor.SpeedSolveException);
+ assertDoesNotThrow(() -> optimizer.configureProcessCompressorCharts());
+ }
+
@Test
public void testProfessionalLiftCurveGeneration() {
logger.info("\n=== Professional Lift Curve Generation Test ===");
diff --git a/src/test/java/neqsim/process/util/optimizer/ProductionOptimizerCompressorFailureTest.java b/src/test/java/neqsim/process/util/optimizer/ProductionOptimizerCompressorFailureTest.java
new file mode 100644
index 00000000000..bddcfa1546c
--- /dev/null
+++ b/src/test/java/neqsim/process/util/optimizer/ProductionOptimizerCompressorFailureTest.java
@@ -0,0 +1,122 @@
+package neqsim.process.util.optimizer;
+
+import static org.junit.jupiter.api.Assertions.assertEquals;
+import static org.junit.jupiter.api.Assertions.assertFalse;
+import static org.junit.jupiter.api.Assertions.assertThrows;
+import static org.junit.jupiter.api.Assertions.assertTrue;
+import java.util.Collections;
+import java.util.concurrent.atomic.AtomicInteger;
+import org.junit.jupiter.api.Test;
+import neqsim.NeqSimTest;
+import neqsim.process.equipment.compressor.Compressor;
+import neqsim.process.equipment.stream.Stream;
+import neqsim.process.processmodel.ProcessSystem;
+import neqsim.process.util.optimizer.ProductionOptimizer.ObjectiveType;
+import neqsim.process.util.optimizer.ProductionOptimizer.OptimizationConfig;
+import neqsim.process.util.optimizer.ProductionOptimizer.OptimizationObjective;
+import neqsim.process.util.optimizer.ProductionOptimizer.OptimizationResult;
+import neqsim.thermo.system.SystemSrkEos;
+
+/**
+ * Ensures failed compressor calculations cannot supply feasible optimizer evidence.
+ *
+ * @author Even Solbraa
+ * @version 1.0
+ */
+class ProductionOptimizerCompressorFailureTest extends NeqSimTest {
+ /**
+ * Creates a public synthetic compressor map with a reachable 14 bara target at its design feed.
+ *
+ * @return the configured compressor
+ */
+ private Compressor createCompressor() {
+ SystemSrkEos fluid = new SystemSrkEos(300.0, 4.0);
+ fluid.addComponent("methane", 0.7);
+ fluid.addComponent("ethane", 0.2);
+ fluid.addComponent("propane", 0.1);
+ fluid.setMixingRule("classic");
+ Stream feed = new Stream("feed", fluid);
+ feed.setFlowRate(1000.0, "kg/hr");
+ feed.run();
+ Compressor compressor = new Compressor("compressor", feed);
+ compressor.setUsePolytropicCalc(true);
+ double[][] flow = {{100.0, 140.0, 180.0}, {200.0, 260.0, 320.0}, {300.0, 380.0, 460.0}};
+ double[][] head = {{100.0, 80.0, 60.0}, {180.0, 140.0, 100.0}, {260.0, 200.0, 140.0}};
+ double[][] efficiency = {{70.0, 71.0, 70.0}, {72.0, 73.0, 72.0}, {74.0, 75.0, 74.0}};
+ compressor.getCompressorChart().setCurves(new double[] {25.0, 300.0, 1.0, 1.0},
+ new double[] {1000.0, 2000.0, 3000.0}, flow, head, flow, efficiency);
+ compressor.getCompressorChart().setHeadUnit("kJ/kg");
+ compressor.setSpeed(2000.0);
+ compressor.setMinimumSpeed(1000.0);
+ compressor.setMaximumSpeed(3000.0);
+ compressor.setSolveSpeed(true);
+ compressor.setOutletPressure(14.0);
+ return compressor;
+ }
+
+ /** A rejected chart trial must be infeasible without evaluating stale objectives. */
+ @Test
+ void invalidChartCandidateDoesNotAbortSearchOrEvaluateStaleResults() {
+ Compressor compressor = createCompressor();
+ compressor.getInletStream().setFlowRate(1.0e9, "kg/hr");
+ compressor.getInletStream().run();
+ assertThrows(Compressor.SpeedSolveException.class, () -> compressor.run());
+ ProcessSystem process = new ProcessSystem();
+ process.add(compressor.getInletStream());
+ process.add(compressor);
+ AtomicInteger objectiveCalls = new AtomicInteger();
+ OptimizationObjective objective = new OptimizationObjective("power", ps -> {
+ objectiveCalls.incrementAndGet();
+ return compressor.getPower();
+ }, 1.0, ObjectiveType.MINIMIZE);
+ OptimizationConfig config = new OptimizationConfig(1.0e9, 1.0e9).rateUnit("kg/hr");
+ OptimizationResult result = new ProductionOptimizer().optimize(process, compressor.getInletStream(), config,
+ Collections.singletonList(objective), Collections.emptyList());
+ assertFalse(result.isFeasible());
+ assertTrue(result.getObjectiveValues().isEmpty());
+ assertEquals(0, objectiveCalls.get());
+ assertEquals("compressorSpeedSolve", result.getConstraintStatuses().get(0).getName());
+ assertTrue(result.getConstraintStatuses().get(0).getDescription().contains("compressor"));
+ assertTrue(result.getIterationHistory().stream().noneMatch(ProductionOptimizer.IterationRecord::isFeasible));
+ assertEquals(1.0e9, result.getDecisionVariables().get("feed"), 1.0e-6);
+ }
+
+ /** Capacity-envelope handling and optional validation cannot hide a missed pressure target. */
+ @Test
+ void unconvergedSpeedIsHardFailureWithCapacityAnalysisEnabled() {
+ Compressor compressor = createCompressor();
+ compressor.setOutletPressure(1000.0);
+ compressor.setLimitSpeed(true);
+ compressor.setCapacityAnalysisEnabled(true);
+ ProcessSystem process = new ProcessSystem();
+ process.add(compressor.getInletStream());
+ process.add(compressor);
+ OptimizationConfig config = new OptimizationConfig(1000.0, 1000.0).rateUnit("kg/hr").rejectInvalidSimulations(false)
+ .defaultUtilizationLimit(100.0);
+ OptimizationResult result = new ProductionOptimizer().optimize(process, compressor.getInletStream(), config, null,
+ null);
+ assertFalse(compressor.isSpeedSolveConverged());
+ assertFalse(result.isFeasible());
+ assertEquals("compressorSpeedSolve", result.getConstraintStatuses().get(0).getName());
+ assertTrue(result.getConstraintStatuses().get(0).getMargin() < 0.0);
+ }
+
+ /** A later valid trial must recover normally after the map rejected another operating point. */
+ @Test
+ void failedChartDoesNotPoisonLaterValidCandidate() {
+ Compressor compressor = createCompressor();
+ compressor.getInletStream().setFlowRate(1.0e9, "kg/hr");
+ compressor.getInletStream().run();
+ assertThrows(Compressor.SpeedSolveException.class, () -> compressor.run());
+ ProcessSystem process = new ProcessSystem();
+ process.add(compressor.getInletStream());
+ process.add(compressor);
+ OptimizationConfig config = new OptimizationConfig(1000.0, 1000.0).rateUnit("kg/hr").defaultUtilizationLimit(100.0);
+ OptimizationResult result = new ProductionOptimizer().optimize(process, compressor.getInletStream(), config, null,
+ null);
+ assertTrue(compressor.isSpeedSolveConverged());
+ assertTrue(result.isFeasible(), result.getInfeasibilityDiagnosis());
+ assertEquals(14.0, compressor.getOutletStream().getPressure("bara"), 0.001);
+ assertEquals(1000.0, compressor.getOutletStream().getFlowRate("kg/hr"), 1.0e-6);
+ }
+}