Skip to the content.

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:

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.

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:

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

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

  1. Define Objective: Configure one or more objectives (e.g., maximize throughput while penalizing power) using OptimizationObjective weights.
  2. 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.
  3. Iterative Solver (selectable):
    • BINARY_FEASIBILITY (default) targets monotonic systems and searches on feasibility margins.
    • GOLDEN_SECTION_SCORE samples non-monotonic responses using weighted objectives and constraint penalties to guide the search.
    • NELDER_MEAD_SCORE applies a simplex-based heuristic to handle noisy or coupled objectives without assuming monotonicity.
    • PARTICLE_SWARM_SCORE explores 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).
  4. Diagnostics & reporting:
    • Each run keeps an iterationHistory with 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 and formatUtilizationTimeline(...) 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.

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:

  1. Tighten bounds to stay within solver-reliable operating ranges
  2. Add soft constraints with high penalty weights in infeasible regions
  3. 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

  1. 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).

  2. 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.

  3. 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.

  4. Iteration budget: Multi-variable optimization requires more evaluations. Set appropriate maxIterations in 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:

  1. Increase the capacity of the bottleneck equipment (e.g., compressor.getMechanicalDesign().maxDesignPower = newPower).
  2. Re-run the optimization loop.
  3. Identify the new bottleneck and the new maximum production rate.
  4. Calculate the ROI of the upgrade based on the increased production.