Bottleneck Analysis and Capacity Utilization
NeqSim provides functionality to analyze capacity utilization and identify bottlenecks in a process simulation. This feature is useful for production optimization and debottlenecking studies.
Overview
The bottleneck analysis identifies which unit operation in a process system is operating closest to its maximum design capacity. The analysis is based on the “utilization ratio,” defined as:
\[\text{Utilization} = \frac{\text{Current Duty}}{\text{Maximum Capacity}}\]The unit operation with the highest utilization ratio is considered the bottleneck.
Key Concepts
1. Capacity Duty (getCapacityDuty)
The getCapacityDuty() method returns the current operating load of a unit operation. The definition of “duty” varies by equipment type:
- Compressor: Total power consumption (Watts).
- Separator: Gas outlet flow rate ($m^3/hr$).
- Other Equipment: Default is 0.0 (needs implementation for specific units).
2. Maximum Capacity (getCapacityMax)
The getCapacityMax() method returns the maximum design capacity of the equipment. This value is typically set in the equipment’s mechanical design.
- Compressor:
maxDesignPower(Watts). - Separator:
maxDesignGassVolumeFlow($m^3/hr$).
3. Rest Capacity (getRestCapacity)
The getRestCapacity() method calculates the remaining available capacity:
\(\text{Rest Capacity} = \text{Maximum Capacity} - \text{Current Duty}\)
Use ProductionOptimizer.OptimizationConfig.capacityRangeForType to supply P10/P50/P90
envelopes for equipment without deterministic limits and specify a percentile via
capacityPercentile (e.g., 0.1 for P10 or 0.9 for P90 stress tests).
Implementation Details
ProcessEquipmentInterface
The ProcessEquipmentInterface defines the methods for capacity analysis:
public double getCapacityDuty();
public double getCapacityMax();
public double getRestCapacity();
ProcessSystem
The ProcessSystem class includes a method to identify the bottleneck:
public ProcessEquipmentInterface getBottleneck();
This method iterates through all unit operations in the system and returns the one with the highest utilization ratio.
Supported Equipment
Currently, the following equipment types support capacity analysis:
| Equipment | Duty Metric | Capacity Metric | How to Set Capacity | Override After autoSize |
|---|---|---|---|---|
| Separator | Gas flow (m³/s) | Max allowable gas flow | setDesignGasLoadFactor(), setInternalDiameter() |
separator.setDesignGasLoadFactor(0.15) |
| Compressor | Power (W) | Max design power | setMaximumPower(), setMaximumSpeed() |
compressor.setMaximumPower(5000.0) |
| Pump | Power (W) | Max design power | getMechanicalDesign().setMaxDesignPower() |
pump.getMechanicalDesign().setMaxDesignPower(100000) |
| Heater/Cooler | Duty (W) | Max design duty | getMechanicalDesign().setMaxDesignDuty() |
heater.getMechanicalDesign().setMaxDesignDuty(1e6) |
| ThrottlingValve | Volume flow (m³/hr) | Max volume flow | setDesignCv(), setDesignVolumeFlow() |
valve.setDesignCv(200.0) |
| Pipeline/Pipe | Volume flow (m³/hr) | Max design flow | setMaxDesignVelocity(), setDiameter() |
pipe.setMaxDesignVelocity(25.0) |
| DistillationColumn | Fs hydraulic factor | Fs limit | OptimizationConfig.columnFsFactorLimit() |
Configure in optimizer |
| Custom types | User-defined | User-defined | capacityRuleForType lambda |
N/A |
Capacity Calculation Details
Separator: Uses Souders-Brown equation with K-factor:
MaxGasFlow = K × A × √((ρ_liq - ρ_gas) / ρ_gas)
Utilization = ActualGasFlow / MaxGasFlow
Override K-factor with setDesignGasLoadFactor() to change capacity.
Compressor: Uses power-based utilization:
Utilization = ShaftPower / MaxDesignPower
MaxDesignPower comes from: (1) driver speed-power curve, (2) setMaximumPower(), or (3) mechanical design.
Pump: Uses power-based utilization:
Utilization = ShaftPower / MaxDesignPower
Valve: Uses flow-based utilization:
Utilization = ActualVolumeFlow / MaxVolumeFlow
MaxVolumeFlow derived from Cv at operating conditions.
Pipe: Uses velocity or flow-based utilization:
Utilization = ActualVolumeFlow / MaxVolumeFlow
MaxVolumeFlow = Area × MaxDesignVelocity
Notes:
- Separator: The
ProductionOptimizeruses gas volumetric flow for capacity tracking. Gas load factor (K-factor) determines max allowable gas velocity. - ThrottlingValve: Valve utilization is tracked based on volume flow vs design flow capacity.
- Pipeline: Default erosional velocity limit of 20 m/s is applied if no design velocity is set.
- Dry Gas: For separators/scrubbers with single-phase (dry gas), K-factor calculations use a default liquid density of 1000 kg/m³.
Example Usage
The following example demonstrates how to set up a simulation, define capacities, and identify the bottleneck.
import neqsim.process.equipment.compressor.Compressor;
import neqsim.process.equipment.separator.Separator;
import neqsim.process.equipment.stream.Stream;
import neqsim.process.processmodel.ProcessSystem;
import neqsim.thermo.system.SystemSrkEos;
public class BottleneckExample {
public static void main(String[] args) {
// 1. Create System
SystemSrkEos testSystem = new SystemSrkEos(298.15, 10.0);
testSystem.addComponent("methane", 100.0);
testSystem.createDatabase(true);
testSystem.setMixingRule(2);
Stream inletStream = new Stream("inlet stream", testSystem);
inletStream.setFlowRate(100.0, "MSm3/day");
inletStream.setTemperature(20.0, "C");
inletStream.setPressure(10.0, "bara");
// 2. Create Equipment and Set Capacities
Separator separator = new Separator("separator", inletStream);
// Set Separator Capacity (e.g., 200 m3/hr)
separator.getMechanicalDesign().setMaxDesignGassVolumeFlow(200.0);
Compressor compressor = new Compressor("compressor", separator.getGasOutStream());
compressor.setOutletPressure(50.0);
// Set Compressor Capacity (e.g., 5 MW)
compressor.getMechanicalDesign().maxDesignPower = 5000000.0;
// 3. Run Simulation
ProcessSystem process = new ProcessSystem();
process.add(inletStream);
process.add(separator);
process.add(compressor);
process.run();
// 4. Analyze Results
System.out.println("Separator Duty: " + separator.getCapacityDuty());
System.out.println("Separator Max: " + separator.getCapacityMax());
System.out.println("Compressor Duty: " + compressor.getCapacityDuty());
System.out.println("Compressor Max: " + compressor.getCapacityMax());
if (process.getBottleneck() != null) {
System.out.println("Bottleneck: " + process.getBottleneck().getName());
double utilization = process.getBottleneck().getCapacityDuty() / process.getBottleneck().getCapacityMax();
System.out.println("Utilization: " + (utilization * 100) + "%");
} else {
System.out.println("No bottleneck found (or capacity not set)");
}
System.out.println("Compressor Rest Capacity: " + compressor.getRestCapacity());
}
}
Extending to Other Equipment
To support capacity analysis for other equipment types (e.g., Pumps, Heat Exchangers), implement the getCapacityDuty() and getCapacityMax() methods in the respective classes. Ensure that the units for duty and capacity are consistent (e.g., both in Watts or both in kg/hr).
Multi-Constraint Capacity Analysis
For equipment with multiple capacity constraints (e.g., compressors limited by speed, power, and surge margin), NeqSim provides the CapacityConstrainedEquipment interface in neqsim.process.equipment.capacity.
Key Features
- Multiple constraints per equipment: Track speed, power, surge margin, discharge temperature, etc.
- Constraint types: HARD (trip/damage), SOFT (efficiency loss), DESIGN (normal envelope)
- Automatic integration:
ProcessSystem.getBottleneck()automatically uses multi-constraint data when available - Detailed analysis:
ProcessSystem.findBottleneck()returns specific constraint information
Constraint Types
| Type | Description | Example |
|---|---|---|
HARD |
Absolute limit - trip or damage if exceeded | Max compressor speed, surge limit |
SOFT |
Operational limit - reduced efficiency | High discharge temperature |
DESIGN |
Normal operating envelope | Separator gas load factor |
Example: Multi-Constraint Analysis
import neqsim.process.equipment.capacity.BottleneckResult;
import neqsim.process.equipment.capacity.CapacityConstraint;
// Run simulation
process.run();
// Simple bottleneck detection (works with both single and multi-constraint)
ProcessEquipmentInterface bottleneck = process.getBottleneck();
double utilization = process.getBottleneckUtilization();
System.out.println("Bottleneck: " + bottleneck.getName() + " at " + (utilization * 100) + "%");
// Detailed constraint information (multi-constraint equipment only)
BottleneckResult result = process.findBottleneck();
if (!result.isEmpty()) {
System.out.println("Equipment: " + result.getEquipmentName());
System.out.println("Limiting constraint: " + result.getConstraint().getName());
System.out.println("Utilization: " + result.getUtilizationPercent() + "%");
}
// Check specific equipment constraints
Compressor comp = (Compressor) process.getUnit("compressor");
for (CapacityConstraint c : comp.getCapacityConstraints().values()) {
System.out.printf(" %s: %.1f / %.1f %s (%.1f%%)%n",
c.getName(), c.getCurrentValue(), c.getDesignValue(),
c.getUnit(), c.getUtilizationPercent());
}
// Check for critical conditions
if (process.isAnyHardLimitExceeded()) {
System.out.println("CRITICAL: Equipment hard limits exceeded!");
}
if (process.isAnyEquipmentOverloaded()) {
System.out.println("WARNING: Equipment operating above design capacity");
}
Supported Multi-Constraint Equipment
| Equipment | Constraints |
|---|---|
| Separator | Gas load factor (vs design K-factor) |
| Compressor | Speed, Power, Surge margin |
For detailed documentation on extending to other equipment, see Capacity Constraint Framework.
Production Optimization
The bottleneck analysis feature is a powerful tool for optimizing production. By identifying the limiting constraint in a process, you can maximize throughput or identify the most effective upgrades (debottlenecking).
Optimization Workflow
- Define Objective: Configure one or more objectives (e.g., maximize throughput while penalizing power) using
OptimizationObjectiveweights. - Identify Constraints: Provide utilization limits per equipment name or type plus custom hard/soft constraints via
OptimizationConstraint. Safety margins and capacity-uncertainty factors can be applied globally so bottleneck checks keep headroom. - Iterative Solver (selectable):
BINARY_FEASIBILITY(default) targets monotonic systems and searches on feasibility margins.GOLDEN_SECTION_SCOREsamples non-monotonic responses using weighted objectives and constraint penalties to guide the search.NELDER_MEAD_SCOREapplies a simplex-based heuristic to handle noisy or coupled objectives without assuming monotonicity.PARTICLE_SWARM_SCOREexplores the design space with a configurable swarm size/inertia/weights, useful when the objective landscape has multiple peaks.GRADIENT_DESCENT_SCORE(New Jan 2026) uses finite-difference gradients with Armijo line search for smooth multi-variable problems (5-20+ variables).
- Diagnostics & reporting:
- Each run keeps an
iterationHistorywith per-iteration utilization snapshots so you can plot trajectories of bottleneck movement and score versus candidate rate to understand convergence. - Use
ProductionOptimizer.buildUtilizationSeries(result.getIterationHistory())to feed plotting libraries or CSV exports andformatUtilizationTimeline(...)to highlight bottlenecks per iteration in Markdown. - Use
ProductionOptimizer.formatUtilizationTable(result.getUtilizationRecords())to render a quick Markdown table of duties, capacities, and limits for reports. - Scenario helpers let you run a base case and multiple debottleneck cases in one call for side-by-side reporting, including KPI deltas and Markdown tables that highlight the gain relative to the baseline.
- Caching (enabled by default) reuses steady-state evaluations at similar rates to cut down on reruns during heuristic searches.
- Each run keeps an
Example: Using ProductionOptimizer
The ProductionOptimizer utility adds structured reporting and constraint handling on top of the existing bottleneck functions:
import java.util.List;
import neqsim.process.util.optimizer.ProductionOptimizer;
import neqsim.process.util.optimizer.ProductionOptimizer.ConstraintSeverity;
import neqsim.process.util.optimizer.ProductionOptimizer.OptimizationConfig;
import neqsim.process.util.optimizer.ProductionOptimizer.OptimizationConstraint;
import neqsim.process.util.optimizer.ProductionOptimizer.OptimizationObjective;
import neqsim.process.util.optimizer.ProductionOptimizer.OptimizationResult;
ProductionOptimizer optimizer = new ProductionOptimizer();
OptimizationConfig config = new OptimizationConfig(100.0, 5_000.0)
.rateUnit("kg/hr")
.tolerance(5.0)
.defaultUtilizationLimit(0.95)
.utilizationMarginFraction(0.1) // keep 10% headroom on every unit
.capacityUncertaintyFraction(0.05) // down-rate capacities for uncertainty
.capacityPercentile(0.1) // pick P10/P50/P90 from optional ranges
.capacityRangeSpreadFraction(0.15) // auto-build P10/P90 around design capacity
.columnFsFactorLimit(2.2) // set column hydraulic headroom
.utilizationLimitForName("compressor", 0.9);
OptimizationObjective objective = new OptimizationObjective("maximize rate",
proc -> process.getBottleneck().getCapacityDuty(), 1.0);
OptimizationConstraint keepPowerLow = OptimizationConstraint.lessThan("compressor load",
proc -> compressor.getCapacityDuty() / compressor.getCapacityMax(), 0.9,
ConstraintSeverity.SOFT, 5.0, "Prefer 10% safety margin on compressor");
// Enforce equipment-type constraints (e.g., pressure ratio below 10 for all compressors)
config.equipmentConstraintRule(new EquipmentConstraintRule(Compressor.class, "pressure ratio",
unit -> ((Compressor) unit).getOutStream().getPressure() / ((Compressor) unit)
.getInletStream().getPressure(), 10.0,
ProductionOptimizer.ConstraintDirection.LESS_THAN, ConstraintSeverity.HARD, 0.0,
"Keep pressure ratio within design"));
OptimizationResult result = optimizer.optimize(process, inletStream, config,
Arrays.asList(objective), Arrays.asList(keepPowerLow));
System.out.println("Optimal rate: " + result.getOptimalRate() + " " + result.getRateUnit());
System.out.println("Bottleneck: " + result.getBottleneck().getName());
result.getUtilizationRecords().forEach(record ->
System.out.println(record.getEquipmentName() + " utilization: " + record.getUtilization()));
// Optional: plot or log iteration history for transparency
result.getIterationHistory().forEach(iter -> System.out.println(
"Iter " + iter.getRate() + " " + iter.getRateUnit() + " bottleneck="
+ iter.getBottleneckName() + " feasible=" + iter.isFeasible() + " score="
+ iter.getScore() + " utilizationCount=" + iter.getUtilizations().size()));
// Quick high-level summary without manual bounds/objective wiring
OptimizationSummary summary = optimizer.quickOptimize(process, inletStream);
System.out.println("Max rate: " + summary.getMaxRate() + " " + summary.getRateUnit());
System.out.println("Limiting equipment: " + summary.getLimitingEquipment()
+ " margin=" + summary.getUtilizationMargin());
System.out.println(ProductionOptimizer.formatUtilizationTimeline(result.getIterationHistory()));
// Built-in capacity coverage now includes separators (liquid level fraction) and
// MultiStream heat exchangers (duty vs design) in addition to compressors/pumps/columns.
// Swarm search example via YAML/JSON specs
// searchMode, swarmSize, inertiaWeight, and capacityPercentile can be provided per scenario
To vary multiple feeds or set points at once (e.g., two inlet streams plus a compressor pressure),
define ManipulatedVariable instances and call the multi-variable overload:
ManipulatedVariable feedNorth = new ManipulatedVariable("north", 100.0, 800.0, "kg/hr",
(proc, value) -> northStream.setFlowRate(value, "kg/hr"));
ManipulatedVariable feedSouth = new ManipulatedVariable("south", 100.0, 800.0, "kg/hr",
(proc, value) -> southStream.setFlowRate(value, "kg/hr"));
ManipulatedVariable compressorSetPoint = new ManipulatedVariable("compressor pressure", 40.0,
80.0, "bara", (proc, value) -> compressor.setOutletPressure(value));
OptimizationResult multiVar = optimizer.optimize(process, Arrays.asList(feedNorth, feedSouth,
compressorSetPoint), config.searchMode(SearchMode.PARTICLE_SWARM_SCORE), Arrays.asList(objective),
Arrays.asList(keepPowerLow));
Multi-Variable Optimization with ManipulatedVariable
The ManipulatedVariable class enables optimization over arbitrary process parameters beyond
inlet flow rates. This is essential for complex systems where multiple degrees of freedom
affect the bottleneck—such as flow distribution between parallel trains, intermediate
pressures, or heat integration setpoints.
ManipulatedVariable API
public class ManipulatedVariable {
/**
* Create a decision variable for optimization.
*
* @param name Human-readable variable name (appears in logs/reports)
* @param lowerBound Minimum allowed value
* @param upperBound Maximum allowed value
* @param unit Engineering unit string (informational)
* @param setter BiConsumer that applies the value to the ProcessSystem
*/
public ManipulatedVariable(String name, double lowerBound, double upperBound,
String unit, BiConsumer<ProcessSystem, Double> setter);
public String getName();
public double getLowerBound();
public double getUpperBound();
public String getUnit();
public void apply(ProcessSystem process, double value);
}
The setter parameter is a BiConsumer<ProcessSystem, Double> lambda that receives the
process and a candidate value from the optimizer. This allows you to manipulate any equipment
parameter—not just stream flow rates.
Common Use Cases
| Scenario | Variables | Setter Example |
|---|---|---|
| Parallel train balancing | Split factors | splitter.setSplitFactors(new double[]{val, 0.33, 0.33-val}) |
| Dual-feed systems | Two inlet flows | feedA.setFlowRate(val, "kg/hr") |
| Pressure optimization | Compressor setpoints | comp.setOutletPressure(val) |
| Temperature control | Heater/cooler setpoints | heater.setOutletTemperature(val) |
| Recycle ratio | Recycle stream split | recycler.setFlowRate(val, "kg/hr") |
Example: Split Factor Optimization
When a process has parallel compression trains served by a common inlet, the optimal split depends on each train’s capacity curve and driver limits. The optimizer can find the best distribution:
// Define system with splitter and three parallel trains
Splitter splitter = new Splitter("inlet_splitter", inletStream, 3);
// ... compressors ups1, ups2, ups3 downstream of splitter
// Create manipulated variables
ManipulatedVariable inletFlow = new ManipulatedVariable(
"TotalInletFlow", 1_800_000.0, 2_200_000.0, "kg/hr",
(proc, val) -> inletStream.setFlowRate(val, "kg/hr"));
// Balance factor: shifts flow from train 1 to train 3
// At bal=0: equal split (33.3% / 33.3% / 33.3%)
// At bal=+0.05: train 3 gets more (28.3% / 33.3% / 38.3%)
ManipulatedVariable balanceFactor = new ManipulatedVariable(
"BalanceFactor", -0.10, 0.10, "factor",
(proc, val) -> {
double base = 1.0 / 3.0;
splitter.setSplitFactors(new double[]{base - val, base, base + val});
});
List<ManipulatedVariable> variables = Arrays.asList(inletFlow, balanceFactor);
OptimizationConfig config = new OptimizationConfig(1_800_000.0, 2_200_000.0)
.rateUnit("kg/hr")
.tolerance(1000.0)
.defaultUtilizationLimit(0.99)
.searchMode(SearchMode.GOLDEN_SECTION_SCORE);
OptimizationResult result = optimizer.optimize(process, variables, config,
Collections.singletonList(new OptimizationObjective("throughput",
proc -> inletStream.getFlowRate("kg/hr"), 1.0)),
Collections.emptyList());
System.out.println("Optimal flow: " + result.getOptimalRate() + " kg/hr");
System.out.println("Optimal split: " + Arrays.toString(splitter.getSplitFactors()));
Choosing a Search Mode for Multi-Variable Problems
| Search Mode | Best For | Characteristics |
|---|---|---|
GOLDEN_SECTION_SCORE |
1-2 variables, smooth response | Fast convergence on unimodal landscapes |
NELDER_MEAD_SCORE |
2-4 variables, noisy responses | Robust simplex method, handles local noise |
PARTICLE_SWARM_SCORE |
3+ variables, multimodal | Global search, configurable swarm size |
Caution: Gradient-free optimizers (Nelder-Mead, PSO) may explore infeasible regions where equipment solvers (e.g., compressor chart interpolation) fail or return unrealistic values. Strategies to handle this:
- Tighten bounds to stay within solver-reliable operating ranges
- Add soft constraints with high penalty weights in infeasible regions
- Use grid search as a fallback for critical decisions:
// Grid search for robustness when chart solvers are sensitive
double bestFlow = 0, bestBalance = 0, maxFeasibleFlow = 0;
for (double flow = 1_900_000; flow <= 2_150_000; flow += 10_000) {
for (double bal = -0.10; bal <= 0.10; bal += 0.02) {
inletStream.setFlowRate(flow, "kg/hr");
splitter.setSplitFactors(new double[]{0.333 - bal, 0.333, 0.333 + bal});
process.run();
double util = process.getBottleneckUtilization();
if (util < 1.0 && flow > maxFeasibleFlow) {
maxFeasibleFlow = flow;
bestFlow = flow;
bestBalance = bal;
}
}
}
Example: Dual-Feed Optimization
For systems with multiple inlet streams feeding a common process:
ManipulatedVariable feedNorth = new ManipulatedVariable(
"NorthFeed", 100.0, 800.0, "kg/hr",
(proc, val) -> northInlet.setFlowRate(val, "kg/hr"));
ManipulatedVariable feedSouth = new ManipulatedVariable(
"SouthFeed", 100.0, 800.0, "kg/hr",
(proc, val) -> southInlet.setFlowRate(val, "kg/hr"));
// Total throughput objective
OptimizationObjective totalThroughput = new OptimizationObjective(
"totalThroughput",
proc -> northInlet.getFlowRate("kg/hr") + southInlet.getFlowRate("kg/hr"),
1.0);
OptimizationResult result = optimizer.optimize(process,
Arrays.asList(feedNorth, feedSouth),
config.searchMode(SearchMode.PARTICLE_SWARM_SCORE),
Collections.singletonList(totalThroughput),
Collections.emptyList());
Practical Considerations
-
Equal-capacity trains: For parallel trains with similar equipment specs, equal split is often near-optimal. Split optimization provides more value when trains have asymmetric capacities (e.g., different compressor sizes or driver ratings).
-
Solver stability: Compressor chart solvers may produce erroneous results (e.g., 99,000% utilization) when flows fall outside the interpolation envelope. Always validate results against physical bounds.
-
Variable coupling: Some variables are tightly coupled (e.g., split factors must sum to 1.0). Encode these constraints in the setter lambda rather than relying on the optimizer.
-
Iteration budget: Multi-variable optimization requires more evaluations. Set appropriate
maxIterationsin the config (default is often too low for PSO with 3+ variables).
Comparing debottlenecking scenarios
Use compareScenarios to run a baseline plus multiple upgrades and compute KPI deltas in one
report-ready table:
ScenarioRequest baseCase = new ScenarioRequest("base", baseProcess, baseFeed, baseConfig,
Arrays.asList(objective), Arrays.asList(keepPowerLow));
ScenarioRequest upgradeCase = new ScenarioRequest("upgrade", upgradedProcess, upgradedFeed,
baseConfig, Arrays.asList(objective), Arrays.asList(keepPowerLow));
List<ScenarioKpi> kpis = Arrays.asList(ScenarioKpi.optimalRate("kg/hr"), ScenarioKpi.score());
ScenarioComparisonResult comparison = optimizer.compareScenarios(
Arrays.asList(baseCase, upgradeCase), kpis);
System.out.println(ProductionOptimizer.formatScenarioComparisonTable(comparison, kpis));
The first scenario is treated as the baseline; each KPI cell shows value (Δbaseline) so uplift from
debottlenecking is immediately visible alongside bottleneck names and feasibility flags.
Running from JSON/YAML specs
For reproducible CLI/CI runs, define scenarios in a YAML or JSON file (bounds, objectives,
constraints) and load them via ProductionOptimizationSpecLoader.load(...) while passing in a
registry of process models, feed streams, and metric functions keyed by name. This allows
side-by-side optimization of investment options without hard-coding Java configuration:
scenarios:
- name: base
process: baseProcess
feedStream: inlet
lowerBound: 100.0
upperBound: 2000.0
rateUnit: kg/hr
searchMode: BINARY_FEASIBILITY
constraints:
- name: column_pressure
metric: columnPressureRatio
limit: 1.8
direction: LESS_THAN
severity: HARD
- name: upgrade
process: upgradedProcess
feedStream: inlet
lowerBound: 100.0
upperBound: 2500.0
rateUnit: kg/hr
searchMode: PARTICLE_SWARM_SCORE
After loading, call optimizer.optimizeScenarios(...) or optimizer.compareScenarios(...) to render
side-by-side KPIs automatically for the pipeline or report.
Advanced YAML with multi-objective scoring and variable feeds
To mirror the multi-objective/variable-driven test coverage, you can encode both throughput and penalty objectives while letting a swarm search vary a feed stream directly:
scenarios:
- name: base
process: base
feedStream: feed1
lowerBound: 100.0
upperBound: 320.0
rateUnit: kg/hr
capacityPercentile: 0.9
objectives:
- name: rate
metric: throughput
weight: 1.0
type: MAXIMIZE
- name: compressorUtilPenalty
metric: compressorUtil
weight: -0.1
type: MAXIMIZE
constraints:
- name: utilizationCap
metric: compressorUtil
limit: 0.95
direction: LESS_THAN
severity: HARD
penaltyWeight: 0.0
description: Keep compressor within design
- name: upgrade
process: upgrade
lowerBound: 120.0
upperBound: 340.0
rateUnit: kg/hr
searchMode: PARTICLE_SWARM_SCORE
utilizationMarginFraction: 0.05
capacityPercentile: 0.9
variables:
- name: feed2Variable
stream: feed2
lowerBound: 120.0
upperBound: 340.0
unit: kg/hr
objectives:
- name: rate
metric: throughput
weight: 1.0
type: MAXIMIZE
constraints:
- name: utilizationCap
metric: compressorUtil
limit: 0.95
direction: LESS_THAN
severity: HARD
penaltyWeight: 0.0
description: Keep compressor within design
Hook this into ProductionOptimizationSpecLoader.load(...) with metric lambdas for throughput and
compressorUtil, then call optimizer.optimizeScenarios(...) to exercise the same workflow shown in
the regression test while generating Markdown comparison tables for reports.
Real-world spec-driven workflows
The same YAML/JSON specs can be extended to mirror common operational optimization tasks instead of toy throughput maximization:
1. Energy minimization across compressor trains
Model a three-stage compression train with interstage coolers and set the objective to minimize total power while still honoring a required discharge pressure and anti-surge utilization headroom:
scenarios:
- name: energy_min_train
process: c_train
feedStream: feed_gas
lowerBound: 40.0
upperBound: 90.0
rateUnit: bara # target discharge pressure instead of flow
variables:
- name: stage1_pressure
unit: bara
lowerBound: 30.0
upperBound: 45.0
stream: stage1_out
- name: stage2_pressure
unit: bara
lowerBound: 50.0
upperBound: 70.0
stream: stage2_out
objectives:
- name: minimize_power
metric: totalPowerMw
weight: -1.0
type: MAXIMIZE
constraints:
- name: discharge_pressure
metric: dischargePressure
limit: 90.0
direction: GREATER_THAN
severity: HARD
description: Keep export pressure above spec
- name: anti_surge_headroom
metric: minSurgeMargin
limit: 1.1
direction: GREATER_THAN
severity: HARD
description: Maintain 10% margin to surge lines on all compressors
searchMode: PARTICLE_SWARM_SCORE
inertiaWeight: 0.8
swarmSize: 24
Wire metrics via the spec loader to compute totalPowerMw from compressor duties (sum of
getShaftWork() per stage) and minSurgeMargin from a helper that returns the lowest ratio of
operating flow to surge flow across the train. Inspect result.getIterationHistory() to see where
power flattens out—large step sizes in the swarm can reveal solver-cost bottlenecks when each
iteration requires full thermodynamics and anti-surge calculations.
2. Choke optimization under sand/erosion constraints
Use a sand production limit and downstream separator capacity as hard constraints while maximizing oil throughput in a well/test separator setup. The choke opening becomes the manipulated variable, and penalty objectives can keep gas-lift rates reasonable:
scenarios:
- name: choke_max_oil
process: wellpad
feedStream: wellhead
lowerBound: 10.0
upperBound: 80.0
rateUnit: percent_open
variables:
- name: choke_opening
unit: percent
lowerBound: 10.0
upperBound: 80.0
stream: choke_setting
objectives:
- name: oil_rate
metric: stabilizedOilBpd
weight: 1.0
type: MAXIMIZE
- name: gaslift_penalty
metric: gasliftRate
weight: -0.05
type: MAXIMIZE
constraints:
- name: sand_limit
metric: sandRate
limit: 20.0
direction: LESS_THAN
severity: HARD
description: Protect downstream erosion limit (kg/day)
- name: separator_capacity
metric: separatorUtil
limit: 0.95
direction: LESS_THAN
severity: HARD
description: Keep test separator within design envelope
searchMode: BINARY_FEASIBILITY
For this case, metric functions can map to production tests: sandRate computed from empirical
correlations, separatorUtil derived from getCapacityDuty()/getCapacityMax(), and
gasliftRate pulled from a gas-lift valve set point. The feasibility-first search will quickly
highlight whether the sand constraint or separator capacity is the binding limitation, while the
iteration history logs identify performance hotspots (e.g., separator flash calculations dominating
runtime during tight binary searches).
Debottlenecking Studies
Once the bottleneck is identified (e.g., a compressor), you can simulate a “debottlenecking” project:
- Increase the capacity of the bottleneck equipment (e.g.,
compressor.getMechanicalDesign().maxDesignPower = newPower). - Re-run the optimization loop.
- Identify the new bottleneck and the new maximum production rate.
- Calculate the ROI of the upgrade based on the increased production.