neqsim-production-optimization · git:20260813.829def5 · 2026-08-13 · sha256 876815766d90c663
neqsim-production-optimization git:20260813.829def5A
Immutable. This exact content is served forever at /api/v1/blob/876815766d90c663.
---
name: neqsim-production-optimization
description: "Production optimization, bottleneck analysis, decline modeling, decline-curve history matching (Arps + Duong), reservoir material balance surveillance (OGIP/OOIP, drive indices, aquifer influx), and IOR/EOR screening with NeqSim. USE WHEN: optimizing production rates, identifying facility bottlenecks, forecasting production profiles, fitting decline curves to production history, estimating reserves from pressure/production data, analyzing gas lift allocation, evaluating IOR/EOR options, or running multi-scenario production comparisons."
last_verified: "2026-08-04"
---
# NeqSim Production Optimization Skill
Reference for production forecasting, optimization, bottleneck analysis, and
improved recovery using NeqSim's field development and process simulation tools.
---
## Production Profile Modeling
### Decline Curve Types
| Model | Formula | Use Case |
|-------|---------|----------|
| Exponential | $q(t) = q_i e^{-Dt}$ | Gas wells, constant decline |
| Hyperbolic | $q(t) = q_i (1 + bDt)^{-1/b}$ | Oil wells, b = 0.3-0.8 |
| Harmonic | $q(t) = q_i / (1 + Dt)$ | Hyperbolic with b = 1 |
| Plateau + exponential | Build-up → plateau → decline | Constrained by facility |
### Production Profile Generation
```java
ProductionProfile profile = new ProductionProfile();
// Method 1: Arps decline
profile.setDeclineModel(ProductionProfile.DeclineModel.EXPONENTIAL);
profile.setPeakRate(25000.0); // boe/d
profile.setDeclineRate(0.15); // 15% per year (Di)
profile.setPlateauDuration(3); // years at peak
profile.setProjectLife(25); // total years
double[] rates = profile.generateProfile();
// Method 2: Hyperbolic with b-factor
profile.setDeclineModel(ProductionProfile.DeclineModel.HYPERBOLIC);
profile.setHyperbolicB(0.5);
// Method 3: Resource-constrained
ProductionProfileGenerator gen = new ProductionProfileGenerator();
gen.setResourceVolume(100.0); // MMboe recoverable
gen.setRecoveryFactor(0.55);
gen.setPeakRate(25000.0);
gen.setBuildUpYears(2);
gen.setPlateauYears(5);
gen.generate();
```
### Multi-Well Drill Schedule
```java
FieldProductionScheduler scheduler = new FieldProductionScheduler();
scheduler.setNumberOfWells(8);
scheduler.setDrillingInterval(6); // months between wells
scheduler.setFirstOil(2027);
scheduler.setWellProductivity(4000.0); // initial boe/d/well
scheduler.setDeclineRate(0.12); // 12%/year per well
scheduler.setFacilityCapacity(28000.0);// boe/d plateau constraint
double[][] schedule = scheduler.generateSchedule(25);
// schedule[year][0] = oil rate, [1] = gas rate, [2] = water rate
```
---
## Decline Curve Fitting & History Matching (`DeclineCurveAnalysis`)
`neqsim.pvtsimulation.util.DeclineCurveAnalysis` is a static, unit-agnostic
Arps **+ Duong** decline toolkit. Besides the forward `rate(...)`,
`cumulativeProduction(...)`, `eur(...)` and `forecast(...)` methods it now
**least-squares fits** decline parameters to a measured rate-time history — the
inverse (surveillance) direction used to estimate remaining reserves and EUR
directly from production data. Times are in days; rates keep whatever consistent
unit you supply.
```java
import java.util.Map;
import neqsim.pvtsimulation.util.DeclineCurveAnalysis;
// t[] in days, q[] in bbl/d (or Sm3/d, MMscf/d — unit-agnostic)
Map<String, Double> fit = DeclineCurveAnalysis.fitArps(t, q);
double qi = fit.get("qi"); // initial rate
double di = fit.get("di"); // nominal decline (1/day)
double b = fit.get("b"); // Arps exponent (grid-searched 0..1)
double r2 = fit.get("rSquared"); // goodness of fit
// EUR to an economic-limit rate straight from the fit
double eur = DeclineCurveAnalysis.eurFromFit(fit, 50.0); // rate-unit * days
// Windowed fit — exclude early transient / cleanup points (indices 0..2 here)
Map<String, Double> fitW = DeclineCurveAnalysis.fitArps(t, q, 3, t.length - 1);
```
The `fitArps` overloads grid-search the exponent `b` over `[0, 1]` (coarse then
fine), analytically solving `qi` and `di` for each candidate by linearising in
rate space (`ln q` vs `t` for `b≈0`; `q^-b` vs `t` otherwise). Use the windowed
overload to fit only the established boundary-dominated decline.
### Duong (2011) — tight / unconventional wells
For fracture-dominated tight-gas and shale wells the Arps model over-estimates
reserves. The **Duong (2011)** model is included:
```java
// Forward
double q = DeclineCurveAnalysis.rateDuong(q1, a, m, t); // rate at time t
double gp = DeclineCurveAnalysis.cumulativeDuong(q1, a, m, t); // cumulative
// Fit q1, a, m to a rate history (log-log q/Gp vs t straight line)
Map<String, Double> duong = DeclineCurveAnalysis.fitDuong(t, q);
// keys: "q1", "a", "m", "rSquared"
```
**Model selection:** fit both `fitArps` and `fitDuong`, compare `rSquared`, and
prefer Duong when the well is in transient linear (fracture-dominated) flow.
---
## Reservoir Material Balance & Surveillance (inverse tank models)
`neqsim.pvtsimulation.reservoirproperties.materialbalance` regresses original
hydrocarbon in place, drive mechanism and aquifer support **directly from a
measured pressure-vs-cumulative-production history**. These "inverse" tank models
complement the forward depletion model (`SimpleReservoir`) — use them for
reserves surveillance and drive diagnosis. Pressures are in bara, temperatures in
Kelvin; cumulative volumes keep any consistent surface unit and the returned
in-place volume is in the same unit.
### Gas — P/Z line, Cole plot, Havlena-Odeh (`GasMaterialBalance`)
```java
import neqsim.pvtsimulation.reservoirproperties.materialbalance.GasMaterialBalance;
// P/Z straight line → OGIP (supply Z, or let it compute Z from Sutton + Hall-Yarborough)
GasMaterialBalance.Result r = GasMaterialBalance.fitVolumetric(pressure, z, gp);
double ogip = r.getOgip(); // x-intercept where p/Z = 0
double piZi = r.getPiOverZi(); // initial p/Z intercept (bara)
double r2 = r.getRSquared();
// Compute Z internally (Sutton pseudo-criticals + Hall-Yarborough)
GasMaterialBalance.Result r2fit = GasMaterialBalance.fitVolumetric(pressure, gp, tempK, gasGravity);
// Cole plot — aquifer diagnostic (flat = volumetric depletion, rising = water influx)
double[][] cole = GasMaterialBalance.colePlot(pressure, z, gp, tempK); // [0]=Gp, [1]=F/Eg
// Havlena-Odeh with a supplied cumulative water influx We
GasMaterialBalance.Result rHO = GasMaterialBalance.fitHavlenaOdeh(pressure, z, gp, we, tempK);
```
### Oil — Havlena-Odeh, gas-cap ratio, drive indices (`OilMaterialBalance`)
```java
import neqsim.pvtsimulation.reservoirproperties.materialbalance.OilMaterialBalance;
// Build the F / Eo / Eg / Efw terms from black-oil PVT, then regress:
OilMaterialBalance.Result dep = OilMaterialBalance.fitDepletionDrive(f, eo); // OOIP (no gas cap / aquifer)
OilMaterialBalance.Result gc = OilMaterialBalance.fitGasCapDrive(f, eo, eg); // OOIP + gas-cap ratio m
OilMaterialBalance.Result wd = OilMaterialBalance.fitWaterDrive(f, eo, we, bw); // OOIP with known We
// Pirson fractional drive indices {DDI, SDI, WDI, EDI} (sum ≈ 1)
double[] di = OilMaterialBalance.driveIndices(n, m, eoTerm, egTerm, efwTerm, we, bw, fTerm);
```
### Aquifer influx — Van Everdingen-Hurst / Carter-Tracy (`VanEverdingenHurstAquifer`)
```java
import neqsim.pvtsimulation.reservoirproperties.materialbalance.VanEverdingenHurstAquifer;
double u = VanEverdingenHurstAquifer.aquiferConstant(porosity, ct, thickness, radius, angleDeg);
double[] we = VanEverdingenHurstAquifer.cumulativeInfluxCarterTracy(tD, deltaP, u, reD); // reservoir m3
String aqutab = VanEverdingenHurstAquifer.exportAqutab(tD, reD); // ECLIPSE AQUTAB include table
```
The `We` array feeds the aquifer term of `GasMaterialBalance.fitHavlenaOdeh` /
`OilMaterialBalance.fitWaterDrive`. Use `reD = Double.POSITIVE_INFINITY` for an
infinite-acting aquifer or a finite `reD` for a bounded one.
**Workflow:** (1) diagnose drive with the Cole plot / drive indices, (2) if
water drive, build `We` with Carter-Tracy, (3) regress OGIP/OOIP with the
matching `fit*` method, (4) cross-check reserves against the `DeclineCurveAnalysis`
EUR.
---
## Facility Bottleneck Analysis
### Identifying Constraints
```java
BottleneckAnalyzer analyzer = new BottleneckAnalyzer(processSystem);
analyzer.setReservoirDecline(reservoirModel);
analyzer.setRateRange(10000, 60000, 5000); // min, max, step (boe/d)
Map<String, Double> bottlenecks = analyzer.findBottlenecks();
// Returns: {"HP Separator": 35000, "Gas Compressor": 28000,
// "Export Pump": 40000, "Water Treatment": 45000}
// Bottleneck = Gas Compressor at 28,000 boe/d
```
### Facility Capacity Over Time
```java
FacilityCapacity capacity = new FacilityCapacity();
capacity.setEquipment("HP Separator", 35000.0); // boe/d
capacity.setEquipment("Gas Compressor", 28000.0);
capacity.setEquipment("Export Pump", 40000.0);
capacity.setEquipment("Water Treatment", 45000.0);
// Apply to production profile
double[] constrainedProfile = capacity.constrain(unconstrained);
String bottleneck = capacity.getActiveBottleneck(year);
```
---
## Production Allocation
### Multi-Field to Single Facility
```java
ProductionAllocator allocator = new ProductionAllocator();
allocator.addField("Field A", fieldAProfile, 0.60); // priority weight
allocator.addField("Field B", fieldBProfile, 0.30);
allocator.addField("Field C", fieldCProfile, 0.10);
allocator.setFacilityCapacity(50000.0); // boe/d
Map<String, double[]> allocation = allocator.allocate();
// Returns constrained profiles per field
```
---
## Network Optimization
### Multi-Well Gathering System
```java
NetworkSolver network = new NetworkSolver("Production Network");
network.addWell(well1, 3.0); // well, flowline length (km)
network.addWell(well2, 5.5);
network.addWell(well3, 8.0);
network.addWell(well4, 4.2);
// Mode 1: Fixed manifold pressure → find well rates
network.setSolutionMode(SolutionMode.FIXED_MANIFOLD_PRESSURE);
network.setManifoldPressure(60.0); // bara
NetworkResult result = network.solve();
for (String wellName : result.getWellNames()) {
double rate = result.getWellRate(wellName); // Sm3/d
double whp = result.getWellheadPressure(wellName); // bara
}
// Mode 2: Fixed total rate → find required manifold pressure
network.setSolutionMode(SolutionMode.FIXED_TOTAL_RATE);
network.setTargetTotalRate(50000.0); // Sm3/d
result = network.solve();
double requiredManifoldP = result.getManifoldPressure();
```
### LoopedPipeNetwork (Advanced)
Full NR-GGA production network solver with IPR, chokes, tubing VLP, Beggs-Brill
multiphase, compressors, artificial lift, water/sand/corrosion/emissions:
```java
LoopedPipeNetwork net = new LoopedPipeNetwork("Gathering");
net.setFluidTemplate(gas);
net.setSolverType(LoopedPipeNetwork.SolverType.NEWTON_RAPHSON);
net.setMaxIterations(500);
net.setTolerance(500.0);
// Wells with IPR
net.addSourceNode("R1", 230.0, 0.0);
net.addJunctionNode("MF1");
net.addWellIPR("R1", "MF1", "W1", 5e-13, true);
// Artificial lift
net.setGasLift("W1", 500.0); // kg/hr
net.setESP("W2", 80.0, 0.55); // kW, efficiency
// Water, sand, corrosion tracking
net.setWaterCut("W1", 0.15);
net.setSandRate("W1", 3.0); // kg/hr
net.setCorrosiveGas("trunk", 0.035, 0.002); // CO2, H2S mol frac
net.setCorrosionModel("trunk", "NORSOK"); // NORSOK M-506
// GHG emissions
net.setCO2EmissionFactor(2.75);
net.setMethaneSlipFactor(0.02);
net.run();
// Post-run analysis
Map<String, double[]> sand = net.calculateSandTransport();
Map<String, double[]> corr = net.calculateCorrosion();
Map<String, double[]> em = net.calculateEmissions();
double annualCO2 = net.getAnnualCO2EmissionsTonnes();
```
See [production_well_networks.md](docs/process/equipment/production_well_networks.md)
for full API documentation of all features.
---
## Gas Lift Optimization
### Single Well
```java
GasLiftCalculator glCalc = new GasLiftCalculator();
glCalc.setWellDepth(3000.0); // m
glCalc.setReservoirPressure(250.0); // bara
glCalc.setProductionRate(5000.0); // boe/d
glCalc.setGLR(500.0); // Sm3/Sm3
glCalc.setInjectionPressure(150.0); // bara
double optimalGLR = glCalc.calculateOptimalGLR();
double gasRate = glCalc.calculateInjectionRate(); // Sm3/d
```
### Multi-Well Allocation
```java
GasLiftOptimizer optimizer = new GasLiftOptimizer();
optimizer.addWell("P1", well1, glCalc1);
optimizer.addWell("P2", well2, glCalc2);
optimizer.addWell("P3", well3, glCalc3);
optimizer.setTotalGasAvailable(500000.0); // Sm3/d field gas supply
Map<String, Double> allocation = optimizer.optimize();
// Returns optimal gas injection rate per well
double totalOilGain = optimizer.getTotalOilGain();
```
### Choke + Lift-Gas Co-Optimization with Facility Constraints (strupe/øke lists)
When the decision is *which wells to choke back or open up* under **several** shared
facility ceilings at once (gas handling, produced-water/PWRI, and the lift-gas budget) —
the classic offshore "strupe/øke liste" — use the choke-and-gas-lift allocation stack in
`neqsim.process.fielddevelopment.integrated`. It co-optimizes choke opening **and**
lift-gas per well, honours discrete on/off locks (sand, lost comms, life extension) and
per-well gas ceilings, and emits an operator-ranked action list.
```java
// NIP-1: build a per-well GLPC anchored on a rigorous WellSystem nodal solve.
// base oil rate comes from NeqSim IPR-VLP; the lift response is fitted to a
// well-test peak (optimumLift, peakOil) or a fractional uplift.
GasLiftPerformanceCurve curve =
GasLiftPerformanceCurve.fromWellSystem(wellSystem, 60000.0, peakOil, 150000.0);
// or: GasLiftPerformanceCurve.fromWellSystemUplift(wellSystem, 0.25, 50000.0, 150000.0);
// NIP-2: a chokeable, gas-lifted well with bounds and operational locks.
ChokeableGasLiftWell w = new ChokeableGasLiftWell("S-24", curve)
.setMaxChokeFraction(0.60) // "0-60%"
.setCurrentChokeFraction(0.0) // current strupe setting
.setGor(750.0).setWaterCut(0.35) // for facility roll-up
.setGasHandlingLimit(750_000.0); // per-well "mye gass" ceiling
// .setForcedShut(true, "sand production"); // hard lock
// NIP-3: co-optimize choke + lift under multiple facility constraints (never throws).
ChokeAndGasLiftAllocationOptimizer opt = new ChokeAndGasLiftAllocationOptimizer()
.addWell(w) /* ...add all wells... */
.setLiftGasBudget(totalLiftGasSm3d)
.setGasHandlingLimit(maxGasSm3d) // shared compressor limit
.setWaterHandlingLimit(maxWaterSm3d) // shared produced-water/PWRI limit
.setObjective(ChokeAndGasLiftAllocationOptimizer.Objective.OIL);
ChokeAndGasLiftAllocationOptimizer.AllocationResult r = opt.optimize();
String json = r.toJson(); // schema-versioned
// NIP-4: turn the optimum into the ranked strupe/øke recommendation list.
StrupeOkeReport report = StrupeOkeReport.build(Arrays.asList(w /* ... */), r);
System.out.println(report.toTable()); // per well: OPEN / CHOKE_BACK / SHUT / NO_CHANGE + uplift
```
Screening-grade: choke is a linear deliverability scale, the facility relief is a greedy
"choke back the least valuable barrels first" search. Use `LoopedPipeNetwork` for a
rigorous coupled network solve. Distinct from `GasLiftOptimizer` (lift gas + compression
only) and `ReservoirToMarketOptimizer` (choke + one throughput cap only).
---
## Scenario Analysis
### Multi-Scenario Comparison
```java
ScenarioAnalyzer scenarios = new ScenarioAnalyzer();
// Base case
scenarios.addScenario("Base", baseEngine);
// High oil price
CashFlowEngine highPrice = baseEngine.clone();
highPrice.setOilPrice(90.0);
scenarios.addScenario("High Price", highPrice);
// Low recovery
CashFlowEngine lowRecovery = baseEngine.clone();
lowRecovery.setRecoveryFactor(0.45);
scenarios.addScenario("Low Recovery", lowRecovery);
// Accelerated drilling
CashFlowEngine accelerated = baseEngine.clone();
accelerated.setDrillingInterval(3); // 3 months vs 6
scenarios.addScenario("Accelerated", accelerated);
Map<String, CashFlowResult> results = scenarios.runAll();
scenarios.generateComparisonTable();
```
---
## IOR/EOR Screening Considerations
### Water Injection (Voidage Replacement)
```java
InjectionStrategy waterInj = InjectionStrategy.waterInjection(1.0); // VRR = 1.0
InjectionResult result = waterInj.calculateInjection(
reservoir, oilRate, gasRate, waterRate
);
double requiredRate = result.waterInjectionRate; // Sm3/d
double achievedVRR = result.achievedVRR;
```
### Gas Injection
```java
InjectionStrategy gasInj = InjectionStrategy.gasInjection(0.8); // VRR = 0.8
// May also use produced gas reinjection
```
### Key IOR/EOR Methods (Screening Criteria)
| Method | Viscosity Limit | Depth Limit | API Gravity | Recovery Boost |
|--------|----------------|-------------|-------------|----------------|
| Water flood | < 150 cP | Any | > 15° | 5-30% OOIP |
| WAG | < 10 cP | > 1500 m | 25-50° | 5-15% OOIP |
| Polymer | 10-150 cP | < 3000 m | > 15° | 5-15% OOIP |
| Steam (SAGD) | > 200 cP | < 1500 m | 7-20° | 20-50% OOIP |
| CO2 flooding | < 12 cP | > 600 m | > 25° | 8-20% OOIP |
| Surfactant | < 35 cP | < 3000 m | > 20° | 5-15% OOIP |
---
## Emissions Tracking
```java
EmissionsTracker emissions = new EmissionsTracker();
emissions.setProcessSystem(processSystem);
emissions.setFuelType("natural_gas");
emissions.setProductionRate(25000.0); // boe/d
double co2Tonnes = emissions.calculateAnnualCO2();
double intensity = emissions.getCO2Intensity(); // kgCO2/boe
```
### Detailed Emissions
```java
DetailedEmissionsCalculator detailedCalc = new DetailedEmissionsCalculator();
detailedCalc.setGasTurbinePower(25.0); // MW
detailedCalc.setFlareRate(5000.0); // Sm3/d
detailedCalc.setFugitiveRate(0.001); // fraction of throughput
Map<String, Double> breakdown = detailedCalc.calculate();
// {"turbine_CO2": ..., "flare_CO2": ..., "fugitive_CH4": ..., "total_CO2e": ...}
```
---
## Energy Efficiency
```java
EnergyEfficiencyCalculator efficiency = new EnergyEfficiencyCalculator();
efficiency.setProcessSystem(processSystem);
efficiency.setExportRate(25000.0); // boe/d
double specificPower = efficiency.getSpecificPower(); // kW/boe
double energyEfficiency = efficiency.getEfficiency(); // %
Map<String, Double> consumers = efficiency.getPowerBreakdown();
// {"Gas Compression": 15.2, "Water Injection": 8.5, "Utilities": 3.1}
```
---
## Late Life & Decommissioning
### Late Life Options
| Strategy | NeqSim Support | Key Considerations |
|----------|---------------|-------------------|
| Infill drilling | New `WellSystem` + network rebalance | Marginal well economics |
| Water shut-off | Adjust `WellSystem` water cut | Intervention cost vs benefit |
| Gas lift optimization | `GasLiftOptimizer` | Declining reservoir pressure |
| Choke + lift under gas/water/lift limits (strupe/øke list) | `ChokeAndGasLiftAllocationOptimizer` + `StrupeOkeReport` | Multi-constraint fleet, on/off locks |
| Tie-back satellite | `TiebackAnalyzer` | Host capacity utilization |
| EOR (CO2, polymer) | `InjectionStrategy` + EOS | Fluid compatibility |
| Cessation of production | `DecommissioningEstimator` | Regulatory requirements |
### Decommissioning
```java
DecommissioningEstimator decom = new DecommissioningEstimator();
decom.setNumberOfWells(6);
decom.setWellAbandonment(true); // P&A wells
decom.setSubseaRemoval(true); // Remove subsea equipment
decom.setPlatformRemoval(false); // Subsea tieback — no platform
decom.setPipelineDecommissioning(true);
decom.setWaterDepth(350.0);
decom.setRegion("Norway");
double abex = decom.estimate(); // MUSD
Map<String, Double> breakdown = decom.getBreakdown();
```
---
## Common Optimization Pitfalls
| Pitfall | Impact | Prevention |
|---------|--------|------------|
| Optimizing wells independently | Sub-optimal network, back-pressure effects | Always use `NetworkSolver` for coupled optimization |
| Ignoring facility constraints | Unrealistic production profile | Apply `FacilityCapacity` constraints to profile |
| Static gas lift allocation | Missed oil as reservoir depletes | Re-optimize gas lift periodically as BHP declines |
| Ignoring water cut increase | Overstated revenue, understated OPEX | Model watercut trajectory, include water treatment costs |
| Over-producing from best wells | Premature water/gas coning | Balanced withdrawal per reservoir zone |
| Ignoring backpressure coupling | Wrong wellhead pressures | Network solver captures well-to-well interactions |
| Encoding minimum limits as design/max constraints | Safe NPSH, minimum-flow, or residence-time margins appear overloaded | Use `setMinValue(...)` without `setDesignValue(...)`; use a HARD constraint type for trip/infeasibility limits |
| Recomputing every engineering margin as `design - current` | Minimum limits show infinite or incorrectly signed spare capacity | Use the evaluator/throughput result's `minimumConstraint` direction; feasible margins are `current - minimum` for lower limits and `maximum - current` for upper limits |
| Dropping `dataSource` when exporting bottlenecks | Installed, calculated, and default limits become indistinguishable to Python or AI workflows | Set provenance on each `CapacityConstraint` and preserve the throughput row's `dataSource` in ranking, recommendations, and archives |
| Joining plant-wide bottlenecks by unit name alone | Duplicate names in separate process areas resolve to the wrong train or area | For `ProcessModel.getUtilizationSnapshotJson()`, preserve the bottleneck `area` and use `qualifiedName` (`area::unit`) as the stable join key |
| Treating every constraint as equally credible and universally applicable | AI or optimization recommendations can rely on a screening/default limit outside its evidence range | Set `confidence` and a validity range on `CapacityConstraint`; consume the propagated `ThroughputCaseRow` presence flags, bounds, and in-range state in Java/JSON/CSV; require review when absent/out of range, and never interpret confidence as a safety probability |
| Showing only the leading constraint | Near-active constraints and weak evidence remain hidden from debottleneck decisions | For a simulated case, retain `EvaluationResult.getRankedCapacityConstraints()` or `ThroughputCaseRow.getRankedCapacityConstraints()` so later runs cannot overwrite the snapshot. Use `rankCapacityConstraints(model)` only for direct live-model inspection. Preserve `getEvidenceApplicability()` beside each item, and keep confidence/applicability out of utilization order and feasibility. |
| Dividing a finite difference by a step removed by parameter clamping | Local production and margin sensitivities are understated near operating bounds | Use the bound-aware `ProcessModelSimulationEvaluator` gradient/Jacobian methods. Keep `FORWARD` for the historical one-run-per-parameter cost or select `CENTRAL` for smooth interior points, and always check step-size stability before interpreting a derivative as shadow-value evidence. |
| Ranking debottlenecking value from one unchecked finite-difference step | Truncation error, non-converged perturbations, or regime changes look like defensible sensitivity | Use `estimateSensitivitiesWithQuality(...)` before ranking. Retain the applied stencil/steps and every perturbation's convergence, feasibility, and error; require a justified `isNumericallyStable(tolerance)` result, inspect feasibility separately, and validate nearby operating points. |
| Joining archived sensitivity arrays back to mutable evaluator lists | Objective or constraint rows can be relabelled after definition changes or later runs | Archive `getParameterSnapshots()`, `getObjectiveSnapshot()`, and `getConstraintSnapshots()` from the same `SensitivityQualityResult`. Preserve indices, units, bounds, direction/type, base values/margins, and capacity origin. Do not rank unlike raw margins or derivatives without explicit engineering scaling. |
| Treating one globally stable sensitivity matrix as evidence that every bottleneck action is usable | A failed perturbation, infeasible sample, fixed control, one-sided stencil, or unstable row/column pair can be hidden by manual joins and ad hoc filtering | Call `assessConstraintSensitivities(SensitivityQualificationPolicy)` and retain evidence flags, rejection reasons, and diagnostics for every constraint/parameter pair. Use `getAcceptedConstraintSensitivities(...)` only after selecting and recording the policy. Acceptance is local numerical evidence, not scaling, active-set proof, a KKT multiplier, or engineering approval. |
| Comparing raw margins or declaring an active set without explicit engineering scales | Unlike units become numerically rankable by accident, stale limits are reused, and a near-boundary heuristic is mistaken for KKT evidence | Create one positive, provenance-bearing `ConstraintScale` from each immutable constraint snapshot and run `ConstraintActivityAnalyzer.assess(...)` with a recorded dimensionless tolerance and sensitivity policy. Missing, duplicate, unitless, or identity-stale scales must fail closed. Keep `VIOLATED` separate from `CANDIDATE_ACTIVE`, require each scaled derivative's `isUsable()`, and do not claim ranking, shadow price, or optimizer active-set proof. |
| Interpolating a discrete operating choice or mutating a candidate without a restoration token | A solver can invent an invalid line-up, target the wrong area, hide conversion resolution behind a false rejection, or leave a rejected point in the model | Define a provenance-bearing `ProcessModelOperatingAction` with an area-qualified address. Enumerate `getAllowedValues()` for discrete actions, capture the baseline before mutation, and restore the identity-matched state after candidate validation. Keep the strict default read-back comparison unless the exact automation conversion or control tag has a known resolution; then declare it with `withReadBackTolerance(absolute, relative, provenance)` and retain the numeric residual/tolerance diagnostics. Capability or read-back success is not process feasibility or operating approval. |
| Screening a well or gathering action from a live model without rerunning and restoring the baseline | A rejected candidate can leak into the next evaluation, an exact hydraulic limit can be missing, or evidence can be used outside its validity range | Configure a zero-parameter `ProcessModelSimulationEvaluator`, wrap one action with `ProcessModelOperatingActionEvaluator`, and bind every required hydraulic constraint by exact area/equipment/constraint identity and provenance. Require candidate convergence, finite utilization/margin, no violation, in-range evidence when a range is declared, and successful baseline restoration and reconvergence. Use separate model instances for parallel candidates. |