From fcc669023302eda218fcac5d976a60e1a8fed4f6 Mon Sep 17 00:00:00 2001 From: Even Solbraa <41290109+EvenSol@users.noreply.github.com> Date: Fri, 9 Oct 2026 08:26:37 +0200 Subject: [PATCH] feat(optimization): qualify per-well pressure routing --- .../neqsim-production-optimization/SKILL.md | 1 + .../SURF_NETWORK_INTEGRATION.md | 33 +- .../process/automation/ProcessAutomation.java | 67 +++- .../network/FieldWellNetworkProcessUnit.java | 199 ++++++++++- .../equipment/network/LoopedPipeNetwork.java | 14 +- .../neqsim/mcp/engineering-coverage.json | 6 +- .../FieldWellNetworkProcessUnitTest.java | 93 ++++++ ...dToFacilityOptimizationAcceptanceTest.java | 316 +++++++++++++----- 8 files changed, 637 insertions(+), 92 deletions(-) diff --git a/.github/skills/neqsim-production-optimization/SKILL.md b/.github/skills/neqsim-production-optimization/SKILL.md index e2403782d7c..c4d77c30cc2 100644 --- a/.github/skills/neqsim-production-optimization/SKILL.md +++ b/.github/skills/neqsim-production-optimization/SKILL.md @@ -672,6 +672,7 @@ Map breakdown = decom.getBreakdown(); | 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. | | Applying coupled well-allocation actions independently or simulating a partial vector | A later rejected write can leave earlier wells changed, shared limits can be evaluated at an impossible mixed state, and rollback evidence is lost | Use one provenance-bearing `ProcessModelOperatingActionSetEvaluator` with unique action IDs and addresses. Require exact per-well and shared constraints. Inspect every `ActionCandidateEvidence`, reject any partial application, and require reverse-order restoration plus baseline reconvergence before the next candidate. Validate total/component balance and nearby monotonic constraint response; the transaction does not choose or approve the allocation. | +| Routing a well by splitting its solved outlet downstream or writing independent HP/LP availabilities | Separator pressure does not feed back into well deliverability, both routes can be open or closed, and rejected candidates can leave an invalid line-up | Register the existing alternative edges with `FieldWellNetworkProcessUnit.registerExclusiveRouteSelector(...)` and use one discrete `route..selection` action. The selector applies an exact one-hot line-up atomically; rerun the coupled model, require finite convergence and per-route constraints, and verify exact selection restoration. Zero availability is an exact closed edge in the Newton solve; fractional values remain deratings. | | Reducing fixed-total allocation to a score-only callback or continuing after rollback failure | Constraint provenance and rejected-point evidence disappear, a shared total can drift, or later trials can start from a corrupted process state | For continuous common-unit actions, compose the atomic action set with `ProcessModelAllocationOptimizer`. Declare the fixed total, seed, objective row, budget, transfer tolerance and objective tolerance provenance. Require `isModelRecovered()`, retain the full candidate trace, and inspect utilization-ranked constraints at the best feasible and best sampled objective points. Treat the sampled opportunity gap and transfer convergence as local search diagnostics, not production loss, global optimality, a shadow price, or approval. | | Labelling normalized installed-capacity utilization or margin with RPM, kW, or a flow unit | External solvers and AI agents can mistake a fraction for physical headroom, resample a mutable supplier, or join relief to only a selected hydraulic subset | Register capacities with `addEquipmentCapacityConstraints()` and retain `EvaluationResult.getInstalledEquipmentCapacityEvidence()`. Treat `getNormalizedUnit()` as `"1"`; use `getPhysicalUnit()` only for current, applicable limit, physical margin, and required relief. Preserve qualified identity, origin, evidence status, and validity applicability. | | Joining an improving rejected allocation to mutable constraint definitions or calling one sampled gap causal debottleneck value | Archived rows can be relabelled, normalized capacity margins can be mistaken for engineering-unit relief, and coupled or weak evidence can look isolated | Retain `CandidateSetEvaluationResult.getObjectiveEvidence()` and `getConstraintEvidence()`, then call `ProcessModelAllocationBottleneckAnalyzer.analyze(search)`. Require complete recovery, inspect every hard violation and its evidence class, and keep unlike relief units separate. Treat results as sampled non-causal associations, not capacity sizing, production loss, shadow price, economics, or approval. | diff --git a/docs/fielddevelopment/SURF_NETWORK_INTEGRATION.md b/docs/fielddevelopment/SURF_NETWORK_INTEGRATION.md index a448e6ba2e1..326273637c2 100644 --- a/docs/fielddevelopment/SURF_NETWORK_INTEGRATION.md +++ b/docs/fielddevelopment/SURF_NETWORK_INTEGRATION.md @@ -253,14 +253,33 @@ available under the process-unit name. A copied unit rebuilds the topology defin copies the bound `WellSystem`/`WellFlow` objects and rebinds them before execution; accepted runtime state is not shared with the source unit. +For a well that can be routed to mutually exclusive separator pressure levels, register +the existing alternative network edges with +`FieldWellNetworkProcessUnit.registerExclusiveRouteSelector(...)`. Automation then +exposes `route..selection` as a zero-based discrete input. One verified write +atomically makes the selected edge fully available and every alternative unavailable; +invalid fractional selections are rejected without changing the line-up. A zero edge +availability is an exact closed route in the Newton network solve, while fractional +availability remains a hydraulic derating. The selected HP or LP fixed-pressure sink +therefore feeds back through the same live well/network coupling instead of acting as a +downstream flow split. Copies preserve selector definitions but keep independent edge +state. + The synthetic `FieldToFacilityOptimizationAcceptanceTest` connects two live wells and -well chokes through individual gathering lines to a host, then routes the stable host -stream between two separators and mapped compressors on a common shaft. It exercises -configured well/line/separator operating limits, compressor surge/stonewall evidence, -driver and gearbox power, a shared plant-power budget, a transactional route action, -baseline restoration and an operating-envelope slice. A route rejected by compressor -or other hard evidence remains rejected; the test does not relax physical limits to -obtain a feasible optimizer result. +well chokes through exclusive per-well routes to distinct 45 bara HP and 30 bara LP +fixed-pressure sinks. Those stable sink streams feed separators, compressor-suction +scrubbers and mapped compressors on a common shaft before a pressure-controlled 100 bara +gas-export boundary. The workflow exercises configured +well/per-route-line/separator/scrubber operating limits, compressor surge/stonewall +evidence, driver and gearbox power, a shared plant-power budget, transactional discrete +route actions, qualified export-pressure evidence, exact action restoration and an +operating-envelope slice. A second deterministic acceptance case instantiates ten live +wells with twenty alternative route edges and verifies complete well, line, vessel, +shaft, shared-power and export-boundary evidence without changing the two-well physical +model or introducing a separate optimizer. The lower +pressure route must reduce well backpressure and increase deliverability. A route +rejected by compressor or other hard evidence remains rejected; the test does not relax +physical limits to obtain a feasible optimizer result. ## Dependency-ordered continuation diff --git a/src/main/java/neqsim/process/automation/ProcessAutomation.java b/src/main/java/neqsim/process/automation/ProcessAutomation.java index 610fabe7d57..b2d2ea4a340 100644 --- a/src/main/java/neqsim/process/automation/ProcessAutomation.java +++ b/src/main/java/neqsim/process/automation/ProcessAutomation.java @@ -592,6 +592,9 @@ public double getVariableValue(String address, String unitOfMeasure) { ProcessEquipmentInterface unit = findUnit(areaName, unitName); + if (unit instanceof FieldWellNetworkProcessUnit && parts.length == 3 && "route".equals(parts[1])) { + return getFieldRouteProperty((FieldWellNetworkProcessUnit) unit, parts[2], unitOfMeasure); + } LoopedPipeNetwork addressableNetwork = getAddressableNetwork(unit); if (addressableNetwork != null && parts.length == 3) { return getNetworkProperty(addressableNetwork, parts[1], parts[2], unitOfMeasure); @@ -648,6 +651,11 @@ public void setVariableValue(String address, double value, String unitOfMeasure) ProcessEquipmentInterface unit = findUnit(areaName, unitName); + if (unit instanceof FieldWellNetworkProcessUnit && parts.length == 3 && "route".equals(parts[1])) { + setFieldRouteProperty((FieldWellNetworkProcessUnit) unit, parts[2], value, unitOfMeasure); + this.dirty = true; + return; + } LoopedPipeNetwork addressableNetwork = getAddressableNetwork(unit); if (addressableNetwork != null && parts.length == 3) { setNetworkProperty(addressableNetwork, parts[1], parts[2], value, unitOfMeasure); @@ -875,7 +883,17 @@ private List buildVariableList(String unitName, ProcessEquip LoopedPipeNetwork addressableNetwork = getAddressableNetwork(unit); if (addressableNetwork != null) { - return enrichVariableMetadata(buildNetworkVariableList(unitName, addressableNetwork)); + List networkVariables = buildNetworkVariableList(unitName, addressableNetwork); + if (unit instanceof FieldWellNetworkProcessUnit) { + FieldWellNetworkProcessUnit fieldUnit = (FieldWellNetworkProcessUnit) unit; + for (String selectorName : fieldUnit.getExclusiveRouteSelectorNames()) { + networkVariables.add(new SimulationVariable(unitName + ".route." + selectorName + ".selection", + "route." + selectorName + ".selection", VariableType.INPUT, "-", + "Exclusive route index; provenance: " + fieldUnit.getExclusiveRouteProvenance(selectorName)).withBounds( + Double.valueOf(0.0), Double.valueOf(fieldUnit.getExclusiveRouteEdges(selectorName).size() - 1.0))); + } + } + return enrichVariableMetadata(networkVariables); } // Universal equipment-level outputs @@ -1197,6 +1215,53 @@ private LoopedPipeNetwork getAddressableNetwork(ProcessEquipmentInterface unit) return null; } + /** + * Read one field-unit exclusive route selection. + * + * @param unit field well/network process unit + * @param targetAndProperty selector name and selection property + * @param unitOfMeasure requested dimensionless unit + * @return zero-based selected route index + */ + private double getFieldRouteProperty(FieldWellNetworkProcessUnit unit, String targetAndProperty, + String unitOfMeasure) { + String[] targetProperty = splitNetworkTargetProperty(targetAndProperty); + requireDimensionlessUnit(unitOfMeasure); + if (!"selection".equals(targetProperty[1])) { + throw new IllegalArgumentException("Unknown field route property: " + targetProperty[1]); + } + return unit.getExclusiveRouteSelection(targetProperty[0]); + } + + /** + * Write one field-unit exclusive route selection. + * + * @param unit field well/network process unit + * @param targetAndProperty selector name and selection property + * @param value zero-based selected route index + * @param unitOfMeasure requested dimensionless unit + */ + private void setFieldRouteProperty(FieldWellNetworkProcessUnit unit, String targetAndProperty, double value, + String unitOfMeasure) { + String[] targetProperty = splitNetworkTargetProperty(targetAndProperty); + requireDimensionlessUnit(unitOfMeasure); + if (!"selection".equals(targetProperty[1])) { + throw new IllegalArgumentException("Unknown field route property: " + targetProperty[1]); + } + unit.setExclusiveRouteSelection(targetProperty[0], value); + } + + /** + * Require a dimensionless route-selector unit. + * + * @param unitOfMeasure requested unit + */ + private void requireDimensionlessUnit(String unitOfMeasure) { + if (unitOfMeasure != null && !unitOfMeasure.trim().isEmpty() && !"-".equals(unitOfMeasure)) { + throw new IllegalArgumentException("Exclusive route selection uses the dimensionless '-' unit"); + } + } + /** * Read an internal looped-network address. * diff --git a/src/main/java/neqsim/process/equipment/network/FieldWellNetworkProcessUnit.java b/src/main/java/neqsim/process/equipment/network/FieldWellNetworkProcessUnit.java index 03c7b2a1b9c..c5561b8e018 100644 --- a/src/main/java/neqsim/process/equipment/network/FieldWellNetworkProcessUnit.java +++ b/src/main/java/neqsim/process/equipment/network/FieldWellNetworkProcessUnit.java @@ -1,6 +1,10 @@ package neqsim.process.equipment.network; +import java.util.ArrayList; +import java.util.Collections; +import java.util.LinkedHashMap; import java.util.List; +import java.util.Map; import java.util.UUID; import neqsim.process.equipment.ProcessEquipmentBaseClass; import neqsim.process.equipment.ProcessEquipmentInterface; @@ -29,6 +33,32 @@ public final class FieldWellNetworkProcessUnit extends ProcessEquipmentBaseClass /** Existing live well/network coupling calculation. */ private final FieldWellNetworkCoupler coupler; + /** Exclusive route selectors over existing canonical network edges. */ + private final Map routeSelectors = new LinkedHashMap(); + + /** Immutable definition of one exclusive route selector. */ + private static final class ExclusiveRouteSelector implements java.io.Serializable { + /** Serialization version UID. */ + private static final long serialVersionUID = 1L; + + /** Existing network edges in discrete selection order. */ + private final List edgeNames; + + /** Engineering source for treating the edges as mutually exclusive routes. */ + private final String provenance; + + /** + * Create one immutable selector definition. + * + * @param edgeNames existing network edges in selection order + * @param provenance engineering source for the exclusive line-up + */ + private ExclusiveRouteSelector(List edgeNames, String provenance) { + this.edgeNames = Collections.unmodifiableList(new ArrayList(edgeNames)); + this.provenance = provenance; + } + } + /** * Create a process-system unit over one existing coupler. * @@ -82,7 +112,174 @@ public CouplingResult getLastCouplingResult() { /** {@inheritDoc} */ @Override public ProcessEquipmentInterface copy() { - return new FieldWellNetworkProcessUnit(getName(), coupler.copy()); + FieldWellNetworkProcessUnit copy = new FieldWellNetworkProcessUnit(getName(), coupler.copy()); + for (Map.Entry entry : routeSelectors.entrySet()) { + copy.registerExclusiveRouteSelector(entry.getKey(), entry.getValue().edgeNames, entry.getValue().provenance); + } + return copy; + } + + /** + * Register one discrete, mutually exclusive route selector over existing network edges. + * + *

+ * Exactly one declared edge must currently have availability {@code 1.0}; every other declared edge must have + * availability {@code 0.0}. The selector only changes those existing edge availabilities. It does not create a second + * topology or alter any hydraulic model. + *

+ * + * @param selectorName stable selector identity used by process automation + * @param edgeNames existing alternative route edges in selection order + * @param provenance engineering source for the mutually exclusive line-up + * @return this process unit for chaining + */ + public FieldWellNetworkProcessUnit registerExclusiveRouteSelector(String selectorName, List edgeNames, + String provenance) { + String normalizedName = requireName(selectorName); + String normalizedProvenance = requireName(provenance); + if (routeSelectors.containsKey(normalizedName)) { + throw new IllegalArgumentException("Exclusive route selector is already registered: " + normalizedName); + } + if (edgeNames == null || edgeNames.size() < 2) { + throw new IllegalArgumentException("Exclusive route selector requires at least two route edges"); + } + List validatedEdges = new ArrayList(); + for (String edgeName : edgeNames) { + String normalizedEdge = requireName(edgeName); + if (getHydraulicNetwork().getPipe(normalizedEdge) == null) { + throw new IllegalArgumentException("Exclusive route edge does not exist: " + normalizedEdge); + } + if (validatedEdges.contains(normalizedEdge)) { + throw new IllegalArgumentException("Exclusive route selector contains duplicate edge: " + normalizedEdge); + } + validatedEdges.add(normalizedEdge); + } + ExclusiveRouteSelector selector = new ExclusiveRouteSelector(validatedEdges, normalizedProvenance); + requireSelectedRoute(normalizedName, selector); + routeSelectors.put(normalizedName, selector); + return this; + } + + /** + * Get exclusive route-selector names in registration order. + * + * @return immutable selector-name list + */ + public List getExclusiveRouteSelectorNames() { + return Collections.unmodifiableList(new ArrayList(routeSelectors.keySet())); + } + + /** + * Get the declared route-edge names for one selector. + * + * @param selectorName registered selector identity + * @return immutable route-edge list in discrete selection order + */ + public List getExclusiveRouteEdges(String selectorName) { + return getRouteSelector(selectorName).edgeNames; + } + + /** + * Get the provenance for one selector. + * + * @param selectorName registered selector identity + * @return engineering source for the exclusive line-up + */ + public String getExclusiveRouteProvenance(String selectorName) { + return getRouteSelector(selectorName).provenance; + } + + /** + * Get the selected route index. + * + * @param selectorName registered selector identity + * @return zero-based selected route index + * @throws IllegalStateException when the route availabilities are not exactly one-hot + */ + public int getExclusiveRouteSelection(String selectorName) { + return requireSelectedRoute(selectorName, getRouteSelector(selectorName)); + } + + /** + * Atomically select one existing route and make every alternative unavailable. + * + *

+ * The candidate must be an exact zero-based integer route index. All affected edge availabilities are captured before + * mutation, written as an exact one-hot line-up, and verified. Any write/read-back failure restores every captured + * availability before the exception is propagated. A successful change marks this process unit unsolved; the + * surrounding process model must rerun the live well/network coupling before using the candidate. + *

+ * + * @param selectorName registered selector identity + * @param selectedRoute zero-based route index + */ + public void setExclusiveRouteSelection(String selectorName, double selectedRoute) { + ExclusiveRouteSelector selector = getRouteSelector(selectorName); + if (!Double.isFinite(selectedRoute) || selectedRoute != Math.rint(selectedRoute) || selectedRoute < 0.0 + || selectedRoute >= selector.edgeNames.size()) { + throw new IllegalArgumentException("Exclusive route selection must be a declared integer route index"); + } + int selectedIndex = (int) selectedRoute; + double[] baseline = new double[selector.edgeNames.size()]; + for (int index = 0; index < selector.edgeNames.size(); index++) { + baseline[index] = getHydraulicNetwork().getPipe(selector.edgeNames.get(index)).getAvailability(); + } + try { + for (int index = 0; index < selector.edgeNames.size(); index++) { + getHydraulicNetwork().getPipe(selector.edgeNames.get(index)) + .setAvailability(index == selectedIndex ? 1.0 : 0.0); + } + if (requireSelectedRoute(selectorName, selector) != selectedIndex) { + throw new IllegalStateException("Exclusive route selection read-back did not match the requested route"); + } + isSolved = false; + } catch (RuntimeException exception) { + for (int index = 0; index < selector.edgeNames.size(); index++) { + getHydraulicNetwork().getPipe(selector.edgeNames.get(index)).setAvailability(baseline[index]); + } + throw exception; + } + } + + /** + * Resolve one registered selector. + * + * @param selectorName registered selector identity + * @return immutable selector definition + */ + private ExclusiveRouteSelector getRouteSelector(String selectorName) { + ExclusiveRouteSelector selector = routeSelectors.get(selectorName); + if (selector == null) { + throw new IllegalArgumentException("Unknown exclusive route selector: " + selectorName); + } + return selector; + } + + /** + * Require and return an exact one-hot route selection. + * + * @param selectorName selector identity used in diagnostics + * @param selector immutable selector definition + * @return zero-based selected route index + */ + private int requireSelectedRoute(String selectorName, ExclusiveRouteSelector selector) { + int selectedIndex = -1; + for (int index = 0; index < selector.edgeNames.size(); index++) { + double availability = getHydraulicNetwork().getPipe(selector.edgeNames.get(index)).getAvailability(); + if (availability == 1.0) { + if (selectedIndex >= 0) { + throw new IllegalStateException("Exclusive route selector has multiple available routes: " + selectorName); + } + selectedIndex = index; + } else if (availability != 0.0) { + throw new IllegalStateException( + "Exclusive route selector requires exact zero-or-one availability: " + selectorName); + } + } + if (selectedIndex < 0) { + throw new IllegalStateException("Exclusive route selector has no available route: " + selectorName); + } + return selectedIndex; } /** diff --git a/src/main/java/neqsim/process/equipment/network/LoopedPipeNetwork.java b/src/main/java/neqsim/process/equipment/network/LoopedPipeNetwork.java index 3a090a70fba..91576b3fc0e 100644 --- a/src/main/java/neqsim/process/equipment/network/LoopedPipeNetwork.java +++ b/src/main/java/neqsim/process/equipment/network/LoopedPipeNetwork.java @@ -9119,7 +9119,19 @@ private void runNewtonRaphson(UUID id) { fluid.initProperties(); // Build ordered lists for matrix indexing - List pipeList = new ArrayList<>(pipeNames); + List pipeList = new ArrayList(); + for (String pipeName : pipeNames) { + NetworkPipe pipe = pipes.get(pipeName); + if (pipe.getAvailability() == 0.0) { + // A zero availability is an exact closed line-up, not an extremely high resistance. Keeping + // a closed edge in the Newton system couples otherwise independent fixed-pressure boundaries + // through a badly conditioned derivative and can prevent a valid alternative route from + // converging. Fractional derating remains in the hydraulic equations. + pipe.setFlowRate(0.0); + } else { + pipeList.add(pipeName); + } + } List freeNodeList = new ArrayList<>(); for (NetworkNode node : nodes.values()) { if (!node.isPressureFixed()) { diff --git a/src/main/resources/neqsim/mcp/engineering-coverage.json b/src/main/resources/neqsim/mcp/engineering-coverage.json index 186aa0dfa4a..08f06b41695 100644 --- a/src/main/resources/neqsim/mcp/engineering-coverage.json +++ b/src/main/resources/neqsim/mcp/engineering-coverage.json @@ -1864,7 +1864,7 @@ {"id":"neqsim.process.automation.AgenticProcessOptimizer","kind":"class","source":"src/main/java/neqsim/process/automation/AgenticProcessOptimizer.java","sourceDigest":"ec1f4755aae5263adca90b6db2c0f551d3cb70e5d8d9f82820ad8bc72b6d2ec8","domain":"process","skills":["neqsim-agentic-process-optimization","neqsim-optimization-and-doe","neqsim-utility-design"],"agents":["capacity-increase-screening","continuous-improvement","network-energy-systems","optimize","optimize-processmodel","utility-design"],"capabilities":[],"disposition":"review_required"}, {"id":"neqsim.process.automation.AutomationDiagnostics","kind":"class","source":"src/main/java/neqsim/process/automation/AutomationDiagnostics.java","sourceDigest":"94c0bc8e0a320cc1b76f309008a30cefed777d1376d121ff3347defe80adcbce","domain":"process","skills":["neqsim-capacity-and-utilization-analysis"],"agents":["capacity-increase-screening","network-energy-systems","optimize","optimize-processmodel"],"capabilities":[],"disposition":"review_required"}, {"id":"neqsim.process.automation.DefaultWriteValidators","kind":"class","source":"src/main/java/neqsim/process/automation/DefaultWriteValidators.java","sourceDigest":"f4e64a52a0aca344b0ada04fbdfd07249e7108768d00f45eddfb2d925477965a","domain":"process","skills":["neqsim-capacity-and-utilization-analysis"],"agents":["capacity-increase-screening","network-energy-systems","optimize","optimize-processmodel"],"capabilities":[],"disposition":"review_required"}, - {"id":"neqsim.process.automation.ProcessAutomation","kind":"class","source":"src/main/java/neqsim/process/automation/ProcessAutomation.java","sourceDigest":"6de274e717f3be27652d7e52c7b3e846b53a8a348a89180249b21e0cc287332a","domain":"process","skills":["neqsim-agentic-process-optimization","neqsim-api-patterns","neqsim-capability-map","neqsim-capacity-and-utilization-analysis","neqsim-notebook-patterns","neqsim-pid-process-operations","neqsim-plant-data"],"agents":["capability-scout","capacity-increase-screening","ccs-hydrogen","continuous-improvement","control-system","documentation","dynamic-equipment","emissions-environmental","engineering-deliverables","extract-process","gas-processing-refining","gas-quality","lng-value-chain","mechanical-design","neqsim-test","network-energy-systems","notebook-example","optimize","optimize-processmodel","plant-data","process-model","pvt-simulation","reaction-engineering","root-cause","rotating-equipment","safety-depressuring","solve-process","solve-task","technical-reader","thermo-fluid","unisim-reader","utility-design"],"capabilities":[],"disposition":"review_required"}, + {"id":"neqsim.process.automation.ProcessAutomation","kind":"class","source":"src/main/java/neqsim/process/automation/ProcessAutomation.java","sourceDigest":"76765b8d4541bc408e017ae0018812b2681d97e77c5e189fa7ba2b4943556a32","domain":"process","skills":["neqsim-agentic-process-optimization","neqsim-api-patterns","neqsim-capability-map","neqsim-capacity-and-utilization-analysis","neqsim-notebook-patterns","neqsim-pid-process-operations","neqsim-plant-data"],"agents":["capability-scout","capacity-increase-screening","ccs-hydrogen","continuous-improvement","control-system","documentation","dynamic-equipment","emissions-environmental","engineering-deliverables","extract-process","gas-processing-refining","gas-quality","lng-value-chain","mechanical-design","neqsim-test","network-energy-systems","notebook-example","optimize","optimize-processmodel","plant-data","process-model","pvt-simulation","reaction-engineering","root-cause","rotating-equipment","safety-depressuring","solve-process","solve-task","technical-reader","thermo-fluid","unisim-reader","utility-design"],"capabilities":[],"disposition":"review_required"}, {"id":"neqsim.process.automation.SensitivityAnalyzer","kind":"class","source":"src/main/java/neqsim/process/automation/SensitivityAnalyzer.java","sourceDigest":"4d6dc5ed20ad3174da794ae1cac21c05e5f84fd24854d75cae737877028608e5","domain":"process","skills":[],"agents":[],"capabilities":[],"disposition":"review_required"}, {"id":"neqsim.process.automation.SimulationVariable","kind":"class","source":"src/main/java/neqsim/process/automation/SimulationVariable.java","sourceDigest":"8ba9ef633eeb435976953266d7d7183bfd8ee98432fdcfc092516db3ac2c83ea","domain":"process","skills":["neqsim-api-patterns","neqsim-capacity-and-utilization-analysis","neqsim-plant-data"],"agents":["capability-scout","capacity-increase-screening","ccs-hydrogen","continuous-improvement","control-system","documentation","dynamic-equipment","emissions-environmental","engineering-deliverables","gas-processing-refining","gas-quality","lng-value-chain","mechanical-design","neqsim-test","network-energy-systems","notebook-example","optimize","optimize-processmodel","plant-data","process-model","pvt-simulation","reaction-engineering","root-cause","rotating-equipment","solve-process","solve-task","thermo-fluid","unisim-reader","utility-design"],"capabilities":[],"disposition":"review_required"}, {"id":"neqsim.process.automation.TransactionalBatchResult","kind":"class","source":"src/main/java/neqsim/process/automation/TransactionalBatchResult.java","sourceDigest":"d9dfe44b6876d3fc54dd51c3b4552cb9ecba75001f74c80ef91a92212899efeb","domain":"process","skills":["neqsim-capacity-and-utilization-analysis"],"agents":["capacity-increase-screening","network-energy-systems","optimize","optimize-processmodel"],"capabilities":[],"disposition":"review_required"}, @@ -2567,11 +2567,11 @@ {"id":"neqsim.process.equipment.network.CrudeParcel","kind":"class","source":"src/main/java/neqsim/process/equipment/network/CrudeParcel.java","sourceDigest":"718487b58c2f95f6f1761868d40dd81f0f322223c2b42c1da8fbc312bd22924b","domain":"process","skills":["neqsim-pipeline-and-terminal-networks"],"agents":["field-development","flow-assurance","network-energy-systems"],"capabilities":[],"disposition":"review_required"}, {"id":"neqsim.process.equipment.network.FieldNetworkTopology","kind":"class","source":"src/main/java/neqsim/process/equipment/network/FieldNetworkTopology.java","sourceDigest":"06b8a577844e34b33db67c8d71bf18107a0cbfeff722fced687c86f0e3f335e6","domain":"process","skills":[],"agents":[],"capabilities":["surf-network-hydraulic-fidelity"],"disposition":"registered_anchor"}, {"id":"neqsim.process.equipment.network.FieldWellNetworkCoupler","kind":"class","source":"src/main/java/neqsim/process/equipment/network/FieldWellNetworkCoupler.java","sourceDigest":"38b363a6149ad62fa50837ddea839545b2041dfa80afd0550cbfdc777fcce35b","domain":"process","skills":[],"agents":[],"capabilities":["surf-network-hydraulic-fidelity"],"disposition":"registered_anchor"}, - {"id":"neqsim.process.equipment.network.FieldWellNetworkProcessUnit","kind":"class","source":"src/main/java/neqsim/process/equipment/network/FieldWellNetworkProcessUnit.java","sourceDigest":"55c773c58787eafb9e2e410d738d61d84f6b5f9362efed25f7ecea5da15adbbe","domain":"process","skills":[],"agents":[],"capabilities":[],"disposition":"review_required"}, + {"id":"neqsim.process.equipment.network.FieldWellNetworkProcessUnit","kind":"class","source":"src/main/java/neqsim/process/equipment/network/FieldWellNetworkProcessUnit.java","sourceDigest":"475a4af66ab62c6606b7941ba04868520f168d3d1dc6a6c99bd8dabcafd6a85a","domain":"process","skills":[],"agents":[],"capabilities":[],"disposition":"review_required"}, {"id":"neqsim.process.equipment.network.GasLinepackState","kind":"class","source":"src/main/java/neqsim/process/equipment/network/GasLinepackState.java","sourceDigest":"7eb4b8f59fe2601603d5dbd0f882f829122dcf209fc989b0c359d5daa64030d3","domain":"process","skills":["neqsim-capability-map","neqsim-pipeline-and-terminal-networks"],"agents":["capability-scout","engineering-deliverables","field-development","flow-assurance","mechanical-design","network-energy-systems","solve-task"],"capabilities":[],"disposition":"review_required"}, {"id":"neqsim.process.equipment.network.GasQualityMetric","kind":"enum","source":"src/main/java/neqsim/process/equipment/network/GasQualityMetric.java","sourceDigest":"23b8f66a72f2ee31a8a5b3f71c5c2c797d69f4dcee2c3d1d789055bbb484ffa0","domain":"process","skills":[],"agents":[],"capabilities":[],"disposition":"review_required"}, {"id":"neqsim.process.equipment.network.LoopDetector","kind":"class","source":"src/main/java/neqsim/process/equipment/network/LoopDetector.java","sourceDigest":"2862d83ae012efcb8cba56340a85d3b1638379bacc4408b8ee66a32dddd43820","domain":"process","skills":["neqsim-pipeline-and-terminal-networks"],"agents":["field-development","flow-assurance","network-energy-systems"],"capabilities":[],"disposition":"review_required"}, - {"id":"neqsim.process.equipment.network.LoopedPipeNetwork","kind":"class","source":"src/main/java/neqsim/process/equipment/network/LoopedPipeNetwork.java","sourceDigest":"b6518cc745b480f57252eebc0f195a24f4799571dafc4b09a98c4d70392e4fd2","domain":"process","skills":["neqsim-capability-map","neqsim-flow-assurance","neqsim-pipeline-and-terminal-networks","neqsim-production-optimization","neqsim-subsea-and-wells"],"agents":["capability-scout","capacity-increase-screening","ccs-hydrogen","engineering-deliverables","field-development","flow-assurance","lng-value-chain","mechanical-design","network-energy-systems","production-chemistry","solve-task"],"capabilities":["surf-network-hydraulic-fidelity"],"disposition":"registered_anchor"}, + {"id":"neqsim.process.equipment.network.LoopedPipeNetwork","kind":"class","source":"src/main/java/neqsim/process/equipment/network/LoopedPipeNetwork.java","sourceDigest":"0afdbc70f9076c9d2690c1c48b0111d66862c9c75df8be1e99191c0ac4103d08","domain":"process","skills":["neqsim-capability-map","neqsim-flow-assurance","neqsim-pipeline-and-terminal-networks","neqsim-production-optimization","neqsim-subsea-and-wells"],"agents":["capability-scout","capacity-increase-screening","ccs-hydrogen","engineering-deliverables","field-development","flow-assurance","lng-value-chain","mechanical-design","network-energy-systems","production-chemistry","solve-task"],"capabilities":["surf-network-hydraulic-fidelity"],"disposition":"registered_anchor"}, {"id":"neqsim.process.equipment.network.NetworkAvailabilitySchedule","kind":"class","source":"src/main/java/neqsim/process/equipment/network/NetworkAvailabilitySchedule.java","sourceDigest":"8918703d9bd1df161d80fe3431dd1af75b61588082d31848c7a243a90466e73b","domain":"process","skills":["neqsim-pipeline-and-terminal-networks"],"agents":["field-development","flow-assurance","network-energy-systems"],"capabilities":[],"disposition":"review_required"}, {"id":"neqsim.process.equipment.network.NetworkCandidateEvaluation","kind":"class","source":"src/main/java/neqsim/process/equipment/network/NetworkCandidateEvaluation.java","sourceDigest":"fff07066158b002e6c301ba91742e406a1f8a01a020d57051ff57c70a2f04b3e","domain":"process","skills":[],"agents":[],"capabilities":[],"disposition":"review_required"}, {"id":"neqsim.process.equipment.network.NetworkCompositionConvergenceReport","kind":"class","source":"src/main/java/neqsim/process/equipment/network/NetworkCompositionConvergenceReport.java","sourceDigest":"7e64c63388a67b48d6ace9ebcc3077183a0d24facd904d29826aaa7cfbbd5839","domain":"process","skills":[],"agents":[],"capabilities":[],"disposition":"review_required"}, diff --git a/src/test/java/neqsim/process/equipment/network/FieldWellNetworkProcessUnitTest.java b/src/test/java/neqsim/process/equipment/network/FieldWellNetworkProcessUnitTest.java index 9f67926ba51..6ab2bb95560 100644 --- a/src/test/java/neqsim/process/equipment/network/FieldWellNetworkProcessUnitTest.java +++ b/src/test/java/neqsim/process/equipment/network/FieldWellNetworkProcessUnitTest.java @@ -3,7 +3,9 @@ import static org.junit.jupiter.api.Assertions.assertEquals; import static org.junit.jupiter.api.Assertions.assertFalse; import static org.junit.jupiter.api.Assertions.assertNotNull; +import static org.junit.jupiter.api.Assertions.assertThrows; import static org.junit.jupiter.api.Assertions.assertTrue; +import java.util.Arrays; import java.util.UUID; import org.junit.jupiter.api.Test; import neqsim.process.automation.ProcessAutomation; @@ -72,6 +74,61 @@ void copyRestoresRuntimeBindings() { assertTrue(copy.getOutletStream("host").getFlowRate("kg/hr") > 0.0); } + /** Route selection must switch distinct-pressure paths atomically and preserve live well feedback. */ + @Test + void switchesExclusivePressureRoutesThroughAutomationAndRestoresExactly() { + FieldWellNetworkProcessUnit unit = createRoutedUnit(); + unit.run(UUID.randomUUID()); + assertTrue(unit.solved(), unit.getLastCouplingResult().getMessage()); + + Separator hpSeparator = new Separator("HP separator", unit.getOutletStream("HP host")); + Separator lpSeparator = new Separator("LP separator", unit.getOutletStream("LP host")); + ProcessSystem field = new ProcessSystem("routed field and host"); + field.add(unit); + field.add(hpSeparator); + field.add(lpSeparator); + ProcessModel model = new ProcessModel(); + model.add("Field", field); + model.run(); + assertTrue(model.isModelConverged()); + double baselineRate = unit.getOutletStream("HP host").getFlowRate("kg/hr"); + double baselineWellPressure = unit.getHydraulicNetwork().getNodePressure("producer"); + assertTrue(baselineRate > 0.0); + assertEquals(0.0, unit.getOutletStream("LP host").getFlowRate("kg/hr"), 1.0e-5); + + ProcessAutomation automation = model.getAutomation(); + String routeAddress = "Field::routed field network.route.producer pressure level.selection"; + assertTrue(automation.isWritableAddress(routeAddress)); + assertEquals(0.0, automation.getVariableValue(routeAddress, "-"), 0.0); + automation.setVariableValue(routeAddress, 1.0, "-"); + assertFalse(unit.solved()); + assertEquals(0.0, unit.getHydraulicNetwork().getPipe("HP route").getAvailability(), 0.0); + assertEquals(1.0, unit.getHydraulicNetwork().getPipe("LP route").getAvailability(), 0.0); + model.run(); + + assertTrue(model.isModelConverged()); + assertEquals(1.0, automation.getVariableValue(routeAddress, "-"), 0.0); + assertEquals(0.0, unit.getOutletStream("HP host").getFlowRate("kg/hr"), 1.0e-5); + assertTrue(unit.getOutletStream("LP host").getFlowRate("kg/hr") > baselineRate); + assertTrue(unit.getHydraulicNetwork().getNodePressure("producer") < baselineWellPressure); + + assertThrows(IllegalArgumentException.class, () -> automation.setVariableValue(routeAddress, 0.5, "-")); + assertEquals(0.0, unit.getHydraulicNetwork().getPipe("HP route").getAvailability(), 0.0); + assertEquals(1.0, unit.getHydraulicNetwork().getPipe("LP route").getAvailability(), 0.0); + automation.setVariableValue(routeAddress, 0.0, "-"); + model.run(); + assertTrue(model.isModelConverged()); + assertEquals(baselineRate, unit.getOutletStream("HP host").getFlowRate("kg/hr"), baselineRate * 1.0e-5); + assertEquals(baselineWellPressure, unit.getHydraulicNetwork().getNodePressure("producer"), 1.0e-5); + + FieldWellNetworkProcessUnit copy = (FieldWellNetworkProcessUnit) unit.copy(); + assertEquals(Arrays.asList("producer pressure level"), copy.getExclusiveRouteSelectorNames()); + assertEquals(Arrays.asList("HP route", "LP route"), copy.getExclusiveRouteEdges("producer pressure level")); + copy.setExclusiveRouteSelection("producer pressure level", 1.0); + assertEquals(0.0, unit.getExclusiveRouteSelection("producer pressure level"), 0.0); + assertEquals(1.0, copy.getExclusiveRouteSelection("producer pressure level"), 0.0); + } + /** The process unit must remain unsolved when the complete coupled convergence contract is not met. */ @Test void incompleteCouplingFailsClosed() { @@ -116,6 +173,42 @@ private FieldWellNetworkProcessUnit createUnit() { return new FieldWellNetworkProcessUnit("field network", coupler); } + /** Create one live well with mutually exclusive HP and LP host paths. */ + private FieldWellNetworkProcessUnit createRoutedUnit() { + FieldNetworkTopology topology = new FieldNetworkTopology("routed field network hydraulics"); + LoopedPipeNetwork network = topology.getHydraulicNetwork(); + network.setFluidTemplate(gas(150.0)); + network.setSolverType(LoopedPipeNetwork.SolverType.NEWTON_RAPHSON); + network.setTolerance(1.0); + topology.addLiveWellNode("producer", "XT-201", NodeRole.PRODUCTION_WELL, Service.PRODUCTION, 75.0, -200.0); + topology.addJunction("header", "MA-201", NodeRole.MANIFOLD, Service.PRODUCTION, -200.0); + topology.addFixedPressureSink("HP host", "SEP-HP", NodeRole.HOST, Service.PRODUCTION, 45.0, 0.0); + topology.addFixedPressureSink("LP host", "SEP-LP", NodeRole.HOST, Service.PRODUCTION, 30.0, 0.0); + network.addChoke("producer", "header", "production choke", 30.0, 75.0); + topology.registerExistingEdge("production choke", "XV-201", EdgeRole.CHOKE, Service.PRODUCTION, + FlowDirection.FROM_TO, "well", "header"); + topology.addPipe("HP route", "FL-HP", EdgeRole.FLOWLINE, Service.PRODUCTION, FlowDirection.FROM_TO, "header", + "HP outlet", "HP host", "arrival", 1500.0, 0.20, LoopedPipeNetwork.PipeModelType.DARCY_WEISBACH); + topology.addPipe("LP route", "FL-LP", EdgeRole.FLOWLINE, Service.PRODUCTION, FlowDirection.FROM_TO, "header", + "LP outlet", "LP host", "arrival", 1500.0, 0.20, LoopedPipeNetwork.PipeModelType.DARCY_WEISBACH); + network.getPipe("HP route").setAvailability(1.0); + network.getPipe("LP route").setAvailability(0.0); + + Stream reservoir = new Stream("routed reservoir", gas(200.0)); + reservoir.setFlowRate(0.02, "MSm3/day"); + reservoir.run(); + WellFlow well = new WellFlow("routed producer well"); + well.setInletStream(reservoir); + well.setWellProductionIndex(1.0e-7); + FieldWellNetworkCoupler coupler = new FieldWellNetworkCoupler(topology); + coupler.setMaximumIterations(100); + coupler.setTolerances(1.0e-8, 1.0e-6); + coupler.setRelaxationFactor(0.7); + coupler.bindProductionWell("producer", well); + return new FieldWellNetworkProcessUnit("routed field network", coupler).registerExclusiveRouteSelector( + "producer pressure level", Arrays.asList("HP route", "LP route"), "synthetic HP/LP host line-up"); + } + /** * Create a lean SRK gas. * diff --git a/src/test/java/neqsim/process/util/optimizer/FieldToFacilityOptimizationAcceptanceTest.java b/src/test/java/neqsim/process/util/optimizer/FieldToFacilityOptimizationAcceptanceTest.java index cd79ac369cf..e4573756c95 100644 --- a/src/test/java/neqsim/process/util/optimizer/FieldToFacilityOptimizationAcceptanceTest.java +++ b/src/test/java/neqsim/process/util/optimizer/FieldToFacilityOptimizationAcceptanceTest.java @@ -3,7 +3,10 @@ import static org.junit.jupiter.api.Assertions.assertEquals; import static org.junit.jupiter.api.Assertions.assertFalse; import static org.junit.jupiter.api.Assertions.assertTrue; +import java.util.ArrayList; import java.util.Arrays; +import java.util.Collections; +import java.util.List; import java.util.UUID; import org.junit.jupiter.api.Test; import neqsim.process.equipment.capacity.CapacityConstraint; @@ -14,6 +17,7 @@ import neqsim.process.equipment.compressor.CompressorDriver; import neqsim.process.equipment.compressor.DriverType; import neqsim.process.equipment.energy.Gearbox; +import neqsim.process.equipment.mixer.Mixer; import neqsim.process.equipment.network.FieldNetworkTopology; import neqsim.process.equipment.network.FieldNetworkTopology.EdgeRole; import neqsim.process.equipment.network.FieldNetworkTopology.FlowDirection; @@ -23,15 +27,16 @@ import neqsim.process.equipment.network.FieldWellNetworkCoupler.WellResult; import neqsim.process.equipment.network.FieldWellNetworkProcessUnit; import neqsim.process.equipment.network.LoopedPipeNetwork; +import neqsim.process.equipment.network.NetworkDecisionVariable; import neqsim.process.equipment.reservoir.WellFlow; import neqsim.process.equipment.separator.Separator; -import neqsim.process.equipment.splitter.Splitter; import neqsim.process.equipment.stream.EnergyPort; import neqsim.process.equipment.stream.EnergyPortDirection; import neqsim.process.equipment.stream.EnergyPortMode; import neqsim.process.equipment.stream.EnergyType; import neqsim.process.equipment.stream.MechanicalShaft; import neqsim.process.equipment.stream.Stream; +import neqsim.process.equipment.valve.ThrottlingValve; import neqsim.process.processmodel.ProcessModel; import neqsim.process.processmodel.ProcessSystem; import neqsim.process.util.optimizer.ProcessModelOperatingActionEvaluator.HydraulicLimitRole; @@ -55,15 +60,19 @@ private static final class Fixture { private final FieldWellNetworkProcessUnit networkUnit; private final Separator highPressureSeparator; private final Separator lowPressureSeparator; - private final Splitter facilityRouter; + private final Separator highPressureScrubber; + private final Separator lowPressureScrubber; private final Compressor highPressureCompressor; private final Compressor lowPressureCompressor; + private final Mixer exportMixer; + private final ThrottlingValve exportValve; private final MechanicalShaft shaft; private final EnergyPort driverPort; private final CompressorDriver driver; private final Gearbox gearbox; private final double sharedPowerLimitKw; private final double baselineRouteAHigh; + private final List wellSuffixes; /** * Create one fixture holder. @@ -72,33 +81,42 @@ private static final class Fixture { * @param networkUnit live well/network unit * @param highPressureSeparator high-pressure separator * @param lowPressureSeparator low-pressure separator - * @param facilityRouter reversible host routing splitter + * @param highPressureScrubber high-pressure compressor suction scrubber + * @param lowPressureScrubber low-pressure compressor suction scrubber * @param highPressureCompressor high-pressure compression casing * @param lowPressureCompressor low-pressure compression casing + * @param exportMixer common gas-export boundary mixer + * @param exportValve gas-export pressure-control valve * @param shaft common shaft * @param driverPort driver output port * @param driver driver rating * @param gearbox gearbox rating * @param sharedPowerLimitKw shared plant power limit in kW * @param baselineRouteAHigh baseline producer-A high-pressure route flow in kg/hr + * @param wellSuffixes deterministic well identities */ private Fixture(ProcessModel model, FieldWellNetworkProcessUnit networkUnit, Separator highPressureSeparator, - Separator lowPressureSeparator, Splitter facilityRouter, Compressor highPressureCompressor, - Compressor lowPressureCompressor, MechanicalShaft shaft, EnergyPort driverPort, CompressorDriver driver, - Gearbox gearbox, double sharedPowerLimitKw, double baselineRouteAHigh) { + Separator lowPressureSeparator, Separator highPressureScrubber, Separator lowPressureScrubber, + Compressor highPressureCompressor, Compressor lowPressureCompressor, Mixer exportMixer, + ThrottlingValve exportValve, MechanicalShaft shaft, EnergyPort driverPort, CompressorDriver driver, + Gearbox gearbox, double sharedPowerLimitKw, double baselineRouteAHigh, List wellSuffixes) { this.model = model; this.networkUnit = networkUnit; this.highPressureSeparator = highPressureSeparator; this.lowPressureSeparator = lowPressureSeparator; - this.facilityRouter = facilityRouter; + this.highPressureScrubber = highPressureScrubber; + this.lowPressureScrubber = lowPressureScrubber; this.highPressureCompressor = highPressureCompressor; this.lowPressureCompressor = lowPressureCompressor; + this.exportMixer = exportMixer; + this.exportValve = exportValve; this.shaft = shaft; this.driverPort = driverPort; this.driver = driver; this.gearbox = gearbox; this.sharedPowerLimitKw = sharedPowerLimitKw; this.baselineRouteAHigh = baselineRouteAHigh; + this.wellSuffixes = Collections.unmodifiableList(new ArrayList(wellSuffixes)); } /** @@ -129,52 +147,47 @@ private PlantCommonShaftEvidence commonShaftEvidence(String calculationId) { @Test void evaluatesReversibleMultiWellFieldToFacilityEnvelope() { Fixture fixture = createFixture(); - ProcessModelSimulationEvaluator simulation = new ProcessModelSimulationEvaluator(fixture.model); - simulation.setIncludeStrategyCapacityConstraints(false); - simulation.addObjective("total field production", - model -> fixture.networkUnit.getOutletStream("host").getFlowRate("kg/hr"), - ProcessModelSimulationEvaluator.ObjectiveDefinition.Direction.MAXIMIZE); - simulation.addEquipmentCapacityConstraints(); - - PlantCommonShaftEvidence commonShaftRegistration = fixture.commonShaftEvidence("common-shaft-registration"); - assertTrue(commonShaftRegistration.isComplete(), commonShaftRegistration.getDiagnostics().toString()); - simulation.addCommonShaftConstraintGroup("export-common-shaft", commonShaftRegistration, - (model, calculationId) -> fixture.commonShaftEvidence(calculationId)); - PlantConstraintDefinition sharedPower = sharedPowerDefinition(); - simulation.addPlantConstraint(sharedPower, - (model, - calculationId) -> PlantSharedResourceEvidence.fromProcessModelShaftPower(sharedPower, calculationId, - fixture.sharedPowerLimitKw, model, model.isModelConverged(), "synthetic shared plant power limit") - .toPlantConstraintSample()); + ProcessModelSimulationEvaluator simulation = createSimulationEvaluator(fixture); ProcessModelOperatingAction chokeA = ProcessModelOperatingAction.continuous("choke-a", "Producer A choke", "Field::field network.choke.choke A.opening", 50.0, 95.0, "%", "synthetic choke travel envelope"); ProcessModelOperatingAction chokeB = ProcessModelOperatingAction.continuous("choke-b", "Producer B choke", "Field::field network.choke.choke B.opening", 50.0, 95.0, "%", "synthetic choke travel envelope"); - ProcessModelOperatingAction routeHigh = ProcessModelOperatingAction.continuous("route-high", - "Host flow to HP separator", "Field::facility router.splitFactor_0", 0.2, 0.8, "-", - "synthetic reversible separator allocation"); + ProcessModelOperatingAction routeA = ProcessModelOperatingAction.discrete("route-a", + "Producer A separator pressure level", "Field::field network.route.producer A pressure level.selection", + new double[] {0.0, 1.0}, "-", "synthetic qualified HP/LP route line-up"); + ProcessModelOperatingAction routeB = ProcessModelOperatingAction.discrete("route-b", + "Producer B separator pressure level", "Field::field network.route.producer B pressure level.selection", + new double[] {0.0, 1.0}, "-", "synthetic qualified HP/LP route line-up"); ProcessModelOperatingActionSetEvaluator candidates = new ProcessModelOperatingActionSetEvaluator( "field-to-facility-actions", "Field-to-facility actions", "synthetic coupled acceptance", simulation, - Arrays.asList(chokeA, chokeB, routeHigh)) + Arrays.asList(chokeA, chokeB, routeA, routeB)) .requireHydraulicConstraint(HydraulicLimitRole.WELL_INFLOW_OUTFLOW, "Field", "field network", "producer A deliverability", "synthetic well deliverability") .requireHydraulicConstraint(HydraulicLimitRole.WELL_INFLOW_OUTFLOW, "Field", "field network", "producer B deliverability", "synthetic well deliverability") .requireHydraulicConstraint(HydraulicLimitRole.GATHERING_HYDRAULICS, "Field", "field network", - "A gathering line capacity", "synthetic per-line operating limit"); + "A HP route capacity", "synthetic per-line operating limit"); - CandidateSetEvaluationResult swapped = candidates.evaluate(new double[] {60.0, 70.0, 0.4}); + CandidateSetEvaluationResult swapped = candidates.evaluate(new double[] {60.0, 70.0, 1.0, 0.0}); assertEquals(Outcome.OTHER_MODEL_CONSTRAINT_VIOLATED, swapped.getOutcome(), swapped.getDiagnostics().toString()); assertTrue(swapped.isBaselineRestored()); assertTrue(swapped.isBaselineSimulationConverged()); assertTrue(swapped.getPlantConstraintEvidence().stream().allMatch(PlantConstraintEvidence::hasAvailableEvidence)); + assertEquals(1, swapped.getProcessBoundaryConstraintEvidence().size()); + assertTrue(swapped.getProcessBoundaryConstraintEvidence().get(0).isCalculable()); + assertTrue(swapped.getInstalledEquipmentCapacityEvidence().stream() + .anyMatch(evidence -> "HP suction scrubber".equals(evidence.getEquipmentName()) + && evidence.getQualifiedConstraintName().endsWith("/gas handling"))); + assertTrue(swapped.getInstalledEquipmentCapacityEvidence().stream() + .anyMatch(evidence -> "LP suction scrubber".equals(evidence.getEquipmentName()) + && evidence.getQualifiedConstraintName().endsWith("/gas handling"))); assertBaselineRestored(fixture); ProcessModelOperatingEnvelopeStudy study = new ProcessModelOperatingEnvelopeStudy("field-to-facility-envelope", "Field-to-facility envelope", "synthetic coupled field and host acceptance", candidates); SliceResult slice = study.evaluateOneDimensional("choke-a", new double[] {70.0, 90.0}, - new double[] {70.0, 70.0, 0.5}); + new double[] {70.0, 70.0, 0.0, 1.0}); assertTrue(slice.isComplete()); assertTrue(slice.getPoints().get(0).isFeasible(), slice.getPoints().get(0).getEvaluation().getOutcome() + ": " @@ -182,37 +195,103 @@ void evaluatesReversibleMultiWellFieldToFacilityEnvelope() { assertFalse(slice.getPoints().get(1).isFeasible()); LeadingConstraintEvidence leading = slice.getPoints().get(1).getLeadingConstraint(); assertEquals(LeadingConstraintEvidence.Source.REQUIRED_HYDRAULIC, leading.getSource()); - assertTrue(leading.getQualifiedConstraintName().endsWith("/A gathering line capacity")); + assertTrue(leading.getQualifiedConstraintName().endsWith("/A HP route capacity")); assertEquals("REQUIRED_HYDRAULIC", leading.getApplicableLimitRole()); assertTrue(slice.getPoints().get(1).getEvaluation().getInstalledEquipmentCapacityEvidence().stream() - .anyMatch(evidence -> evidence.getQualifiedConstraintName().endsWith("/A gathering line capacity") + .anyMatch(evidence -> evidence.getQualifiedConstraintName().endsWith("/A HP route capacity") && evidence.getApplicableLimitRole() == CapacityConstraint.ApplicableLimitRole.CONFIGURED_OPERATING)); assertTrue(slice.getBottleneckTransitions().isEmpty()); assertBaselineRestored(fixture); } + /** + * Verify the same canonical route, capacity, compression and export-boundary contracts scale to ten live wells. + */ + @Test + void evaluatesTenWellExportBoundaryWithCompleteConstraintEvidence() { + Fixture fixture = createFixture(10); + ProcessModelSimulationEvaluator simulation = createSimulationEvaluator(fixture); + ProcessModelSimulationEvaluator.EvaluationResult result = simulation.evaluate(new double[0]); + + assertTrue(result.isSimulationConverged()); + assertTrue(result.isFeasible(), result.getErrorMessage()); + assertEquals(10, fixture.networkUnit.getLastCouplingResult().getWellResults().size()); + assertEquals(10, fixture.networkUnit.getExclusiveRouteSelectorNames().size()); + assertTrue(result.getInstalledEquipmentCapacityEvidence().size() >= 34); + assertTrue(result.getInstalledEquipmentCapacityEvidence().stream() + .allMatch(evidence -> evidence.hasFiniteEvidence() && Double.isFinite(evidence.getNormalizedUtilization()))); + assertEquals(1, result.getProcessBoundaryConstraintEvidence().size()); + assertTrue(result.getProcessBoundaryConstraintEvidence().get(0).isCalculable()); + assertTrue(result.getProcessBoundaryConstraintEvidence().get(0).isFeasible()); + + double exportRateKgHr = Math.abs(fixture.exportValve.getOutletStream().getFlowRate("kg/hr")); + double coupledWellRateKgHr = fixture.networkUnit.getLastCouplingResult().getWellResults().stream() + .mapToDouble(resultRow -> resultRow.getAppliedRateKgS() * 3600.0).sum(); + assertTrue(Double.isFinite(exportRateKgHr) && exportRateKgHr > 0.0); + assertEquals(coupledWellRateKgHr, exportRateKgHr, exportRateKgHr * 1.0e-6); + assertEquals(100.0, fixture.exportValve.getOutletStream().getPressure("bara"), 1.0e-6); + assertEquals(exportRateKgHr, result.getObjectivesRaw()[0], exportRateKgHr * 1.0e-9); + + LoopedPipeNetwork network = fixture.networkUnit.getHydraulicNetwork(); + for (int index = 0; index < fixture.wellSuffixes.size(); index++) { + String suffix = fixture.wellSuffixes.get(index); + int expectedRoute = index % 2; + assertEquals(expectedRoute, + fixture.networkUnit.getExclusiveRouteSelection("producer " + suffix + " pressure level")); + assertEquals(expectedRoute == 0 ? 1.0 : 0.0, network.getPipe(suffix + " HP route").getAvailability(), 0.0); + assertEquals(expectedRoute == 1 ? 1.0 : 0.0, network.getPipe(suffix + " LP route").getAvailability(), 0.0); + } + assertBaselineRestored(fixture); + } + /** Build and calibrate the complete synthetic field-to-host process. */ private Fixture createFixture() { + return createFixture(2); + } + + /** + * Build and calibrate a deterministic synthetic field-to-export process. + * + * @param wellCount number of live wells between 2 and 20 + * @return complete coupled fixture + */ + private Fixture createFixture(int wellCount) { + if (wellCount < 2 || wellCount > 20) { + throw new IllegalArgumentException("Synthetic acceptance fixture requires 2 to 20 wells"); + } FieldNetworkTopology topology = new FieldNetworkTopology("field network hydraulics"); LoopedPipeNetwork network = topology.getHydraulicNetwork(); network.setFluidTemplate(gas(140.0)); network.setSolverType(LoopedPipeNetwork.SolverType.NEWTON_RAPHSON); network.setTolerance(1.0); - addWellRoute(topology, "A", 75.0, 70.0); - addWellRoute(topology, "B", 75.0, 70.0); - topology.addJunction("field manifold", "MA-FIELD", NodeRole.MANIFOLD, Service.PRODUCTION, -150.0); - topology.addFixedPressureSink("host", "V-HOST", NodeRole.HOST, Service.PRODUCTION, 45.0, 0.0); - addGatheringLine(topology, "A gathering line", "A header", "A inlet"); - addGatheringLine(topology, "B gathering line", "B header", "B inlet"); - topology.addPipe("export riser", "RI-HOST", EdgeRole.RISER, Service.PRODUCTION, FlowDirection.FROM_TO, - "field manifold", "riser outlet", "host", "arrival", 500.0, 0.25, - LoopedPipeNetwork.PipeModelType.DARCY_WEISBACH); + List wellSuffixes = new ArrayList(); + for (int index = 0; index < wellCount; index++) { + String suffix = String.valueOf((char) ('A' + index)); + wellSuffixes.add(suffix); + addWellRoute(topology, suffix, 75.0, 70.0); + } + topology.addFixedPressureSink("HP host", "V-HP", NodeRole.HOST, Service.PRODUCTION, 45.0, 0.0); + topology.addFixedPressureSink("LP host", "V-LP", NodeRole.HOST, Service.PRODUCTION, 30.0, 0.0); + for (int index = 0; index < wellSuffixes.size(); index++) { + addPressureRoutes(topology, wellSuffixes.get(index), index % 2 == 0); + } FieldWellNetworkCoupler coupler = new FieldWellNetworkCoupler(topology); + coupler.setMaximumIterations(100); + coupler.setTolerances(1.0e-8, 1.0e-6); coupler.setRelaxationFactor(0.7); - coupler.bindProductionWell("producer A", well("producer A", 190.0, 1.1e-7)); - coupler.bindProductionWell("producer B", well("producer B", 185.0, 0.9e-7)); + for (int index = 0; index < wellSuffixes.size(); index++) { + String suffix = wellSuffixes.get(index); + double reservoirPressureBara = index == 0 ? 190.0 : index == 1 ? 185.0 : 184.0 - index; + double productivityIndex = index == 0 ? 1.1e-7 : index == 1 ? 0.9e-7 : (0.90 - 0.02 * index) * 1.0e-7; + coupler.bindProductionWell("producer " + suffix, + well("producer " + suffix, reservoirPressureBara, productivityIndex)); + } FieldWellNetworkProcessUnit networkUnit = new FieldWellNetworkProcessUnit("field network", coupler); + for (String suffix : wellSuffixes) { + networkUnit.registerExclusiveRouteSelector("producer " + suffix + " pressure level", + Arrays.asList(suffix + " HP route", suffix + " LP route"), "synthetic producer " + suffix + " HP/LP line-up"); + } networkUnit.run(UUID.randomUUID()); assertTrue(networkUnit.solved(), networkUnit.getLastCouplingResult().getMessage() + "; rate residual=" @@ -222,22 +301,30 @@ private Fixture createFixture() { + networkUnit.getLastCouplingResult().getMassBalanceResidualKgS()); assertTrue(network.isProductionOptimizationApplicable()); - Splitter facilityRouter = new Splitter("facility router", networkUnit.getOutletStream("host"), 2); - facilityRouter.setSplitFactors(new double[] {0.5, 0.5}); - Separator highPressureSeparator = new Separator("HP separator", facilityRouter.getSplitStream(0)); - Separator lowPressureSeparator = new Separator("LP separator", facilityRouter.getSplitStream(1)); - Compressor highPressureCompressor = new Compressor("HP compressor", highPressureSeparator.getGasOutStream()); - Compressor lowPressureCompressor = new Compressor("LP compressor", lowPressureSeparator.getGasOutStream()); + Separator highPressureSeparator = new Separator("HP separator", networkUnit.getOutletStream("HP host")); + Separator lowPressureSeparator = new Separator("LP separator", networkUnit.getOutletStream("LP host")); + Separator highPressureScrubber = new Separator("HP suction scrubber", highPressureSeparator.getGasOutStream()); + Separator lowPressureScrubber = new Separator("LP suction scrubber", lowPressureSeparator.getGasOutStream()); + Compressor highPressureCompressor = new Compressor("HP compressor", highPressureScrubber.getGasOutStream()); + Compressor lowPressureCompressor = new Compressor("LP compressor", lowPressureScrubber.getGasOutStream()); highPressureCompressor.setOutletPressure(100.0, "bara"); lowPressureCompressor.setOutletPressure(100.0, "bara"); + Mixer exportMixer = new Mixer("gas export boundary"); + exportMixer.addStream(highPressureCompressor.getOutletStream()); + exportMixer.addStream(lowPressureCompressor.getOutletStream()); + ThrottlingValve exportValve = new ThrottlingValve("gas export pressure control", exportMixer.getOutletStream()); + exportValve.setOutletPressure(100.0, "bara"); ProcessSystem process = new ProcessSystem("field to facility"); process.add(networkUnit); - process.add(facilityRouter); process.add(highPressureSeparator); process.add(lowPressureSeparator); + process.add(highPressureScrubber); + process.add(lowPressureScrubber); process.add(highPressureCompressor); process.add(lowPressureCompressor); + process.add(exportMixer); + process.add(exportValve); ProcessModel model = new ProcessModel(); model.add("Field", process); model.run(); @@ -254,6 +341,11 @@ private Fixture createFixture() { shaft.setMaximumSpeed(highPressureCompressor.getSpeed() * 1.2); highPressureCompressor.getEnergyPort("shaftPower").connect(shaft); lowPressureCompressor.getEnergyPort("shaftPower").connect(shaft); + UUID shaftCalculationId = UUID.randomUUID(); + highPressureCompressor.run(shaftCalculationId); + lowPressureCompressor.run(shaftCalculationId); + model.run(); + assertTrue(model.isModelConverged()); EnergyPort driverPort = new EnergyPort("export driver", EnergyType.SHAFT_WORK, EnergyPortDirection.OUTPUT, EnergyPortMode.CALCULATED); driverPort.connect(shaft); @@ -269,25 +361,25 @@ private Fixture createFixture() { gearbox.setSpeedRatio(1.0); gearbox.setMaximumInputPower(driverLimitKw * 1.2 * 1000.0); - double initialRouteAHigh = massFlow(network, "A gathering line"); + double initialRouteAHigh = massFlow(network, "A HP route"); network.getPipe("choke A").setChokeOpening(90.0); model.run(); assertTrue(model.isModelConverged()); assertTrue(network.isProductionOptimizationApplicable()); - double highRouteAHigh = massFlow(network, "A gathering line"); + double highRouteAHigh = massFlow(network, "A HP route"); assertTrue(highRouteAHigh > initialRouteAHigh); network.getPipe("choke A").setChokeOpening(70.0); model.run(); assertTrue(model.isModelConverged()); - double baselineRouteAHigh = massFlow(network, "A gathering line"); + double baselineRouteAHigh = massFlow(network, "A HP route"); assertTrue(highRouteAHigh > baselineRouteAHigh); double routeAHighOperatingLimit = 0.5 * (baselineRouteAHigh + highRouteAHigh); - configureInstalledLimits(networkUnit, highPressureSeparator, lowPressureSeparator, network, - routeAHighOperatingLimit); - return new Fixture(model, networkUnit, highPressureSeparator, lowPressureSeparator, facilityRouter, - highPressureCompressor, lowPressureCompressor, shaft, driverPort, driver, gearbox, baselinePowerKw * 1.8, - baselineRouteAHigh); + configureInstalledLimits(networkUnit, highPressureSeparator, lowPressureSeparator, highPressureScrubber, + lowPressureScrubber, network, routeAHighOperatingLimit, wellSuffixes); + return new Fixture(model, networkUnit, highPressureSeparator, lowPressureSeparator, highPressureScrubber, + lowPressureScrubber, highPressureCompressor, lowPressureCompressor, exportMixer, exportValve, shaft, driverPort, + driver, gearbox, baselinePowerKw * 1.8, baselineRouteAHigh, wellSuffixes); } /** Add one live well node, header and production choke. */ @@ -304,28 +396,40 @@ private void addWellRoute(FieldNetworkTopology topology, String suffix, double i FlowDirection.FROM_TO, "well", "header"); } - /** Add one well-specific gathering line to the shared field manifold. */ - private void addGatheringLine(FieldNetworkTopology topology, String name, String from, String arrivalPort) { - topology.addPipe(name, "FL-" + name, EdgeRole.FLOWLINE, Service.PRODUCTION, FlowDirection.FROM_TO, from, "outlet", - "field manifold", arrivalPort, 1200.0, 0.20, LoopedPipeNetwork.PipeModelType.DARCY_WEISBACH); + /** Add mutually exclusive well-specific routes to distinct fixed-pressure host levels. */ + private void addPressureRoutes(FieldNetworkTopology topology, String suffix, boolean highPressureSelected) { + String header = suffix + " header"; + topology.addPipe(suffix + " HP route", "FL-" + suffix + "-HP", EdgeRole.FLOWLINE, Service.PRODUCTION, + FlowDirection.FROM_TO, header, "HP outlet", "HP host", suffix + " arrival", 1200.0, 0.20, + LoopedPipeNetwork.PipeModelType.DARCY_WEISBACH); + topology.addPipe(suffix + " LP route", "FL-" + suffix + "-LP", EdgeRole.FLOWLINE, Service.PRODUCTION, + FlowDirection.FROM_TO, header, "LP outlet", "LP host", suffix + " arrival", 1200.0, 0.20, + LoopedPipeNetwork.PipeModelType.DARCY_WEISBACH); + topology.getHydraulicNetwork().getPipe(suffix + " HP route").setAvailability(highPressureSelected ? 1.0 : 0.0); + topology.getHydraulicNetwork().getPipe(suffix + " LP route").setAvailability(highPressureSelected ? 0.0 : 1.0); } /** Add operating/default limits for wells, lines and both separators. */ private void configureInstalledLimits(FieldWellNetworkProcessUnit networkUnit, Separator highPressureSeparator, - Separator lowPressureSeparator, LoopedPipeNetwork network, double routeAHighOperatingLimit) { + Separator lowPressureSeparator, Separator highPressureScrubber, Separator lowPressureScrubber, + LoopedPipeNetwork network, double routeAHighOperatingLimit, List wellSuffixes) { networkUnit.clearCapacityConstraints(); - networkUnit.addCapacityConstraint(maximumConstraint("producer A deliverability", "kg/hr", - wellRate(networkUnit, "producer A") * 1.5, wellRate(networkUnit, "producer A") * 1.3, - () -> wellRate(networkUnit, "producer A"), "synthetic well A operating procedure")); - networkUnit.addCapacityConstraint(maximumConstraint("producer B deliverability", "kg/hr", - wellRate(networkUnit, "producer B") * 1.5, wellRate(networkUnit, "producer B") * 1.3, - () -> wellRate(networkUnit, "producer B"), "synthetic well B operating procedure")); - addLineConstraint(networkUnit, network, "A gathering line", routeAHighOperatingLimit); - addLineConstraint(networkUnit, network, "B gathering line", - Math.max(100.0, massFlow(network, "B gathering line") * 1.5)); + for (String suffix : wellSuffixes) { + String nodeId = "producer " + suffix; + networkUnit.addCapacityConstraint(maximumConstraint(nodeId + " deliverability", "kg/hr", + wellRate(networkUnit, nodeId) * 1.5, wellRate(networkUnit, nodeId) * 1.3, () -> wellRate(networkUnit, nodeId), + "synthetic well " + suffix + " operating procedure")); + double highLimit = "A".equals(suffix) ? routeAHighOperatingLimit + : Math.max(100.0, massFlow(network, suffix + " HP route") * 1.5); + addLineConstraint(networkUnit, network, suffix + " HP route", highLimit); + addLineConstraint(networkUnit, network, suffix + " LP route", + Math.max(100.0, massFlow(network, suffix + " LP route") * 1.5)); + } highPressureSeparator.clearCapacityConstraints(); lowPressureSeparator.clearCapacityConstraints(); + highPressureScrubber.clearCapacityConstraints(); + lowPressureScrubber.clearCapacityConstraints(); double highSeparatorFlow = highPressureSeparator.getInletStreams().get(0).getFlowRate("kg/hr"); double lowSeparatorFlow = lowPressureSeparator.getInletStreams().get(0).getFlowRate("kg/hr"); double totalSeparatorFlow = highSeparatorFlow + lowSeparatorFlow; @@ -335,6 +439,12 @@ private void configureInstalledLimits(FieldWellNetworkProcessUnit networkUnit, S lowPressureSeparator.addCapacityConstraint(maximumConstraint("gas handling", "kg/hr", totalSeparatorFlow * 1.5, totalSeparatorFlow * 1.2, () -> lowPressureSeparator.getInletStreams().get(0).getFlowRate("kg/hr"), "synthetic LP separator operating procedure")); + highPressureScrubber.addCapacityConstraint(maximumConstraint("gas handling", "kg/hr", totalSeparatorFlow * 1.5, + totalSeparatorFlow * 1.2, () -> highPressureScrubber.getInletStreams().get(0).getFlowRate("kg/hr"), + "synthetic HP suction scrubber operating procedure")); + lowPressureScrubber.addCapacityConstraint(maximumConstraint("gas handling", "kg/hr", totalSeparatorFlow * 1.5, + totalSeparatorFlow * 1.2, () -> lowPressureScrubber.getInletStreams().get(0).getFlowRate("kg/hr"), + "synthetic LP suction scrubber operating procedure")); } /** Add one per-line capacity constraint with a preserved design rating and configured operating limit. */ @@ -392,15 +502,63 @@ private PlantConstraintDefinition sharedPowerDefinition() { .participant(PlantConstraintParticipant.direct("Field", "kW", "compressor and pump shaft power")).build(); } + /** + * Configure the common objective, equipment, shaft, shared-power and export-boundary evidence. + * + * @param fixture complete coupled field-to-export fixture + * @return configured fail-closed evaluator + */ + private ProcessModelSimulationEvaluator createSimulationEvaluator(Fixture fixture) { + ProcessModelSimulationEvaluator simulation = new ProcessModelSimulationEvaluator(fixture.model); + simulation.setIncludeStrategyCapacityConstraints(false); + simulation.addObjective("validated gas export", + model -> Math.abs(fixture.exportValve.getOutletStream().getFlowRate("kg/hr")), + ProcessModelSimulationEvaluator.ObjectiveDefinition.Direction.MAXIMIZE); + simulation.addEquipmentCapacityConstraints(); + + PlantCommonShaftEvidence commonShaftRegistration = fixture.commonShaftEvidence("common-shaft-registration"); + assertTrue(commonShaftRegistration.isComplete(), commonShaftRegistration.getDiagnostics().toString()); + simulation.addCommonShaftConstraintGroup("export-common-shaft", commonShaftRegistration, + (model, calculationId) -> fixture.commonShaftEvidence(calculationId)); + PlantConstraintDefinition sharedPower = sharedPowerDefinition(); + simulation.addPlantConstraint(sharedPower, + (model, + calculationId) -> PlantSharedResourceEvidence.fromProcessModelShaftPower(sharedPower, calculationId, + fixture.sharedPowerLimitKw, model, model.isModelConverged(), "synthetic shared plant power limit") + .toPlantConstraintSample()); + + ProcessBoundaryConstraintEvidence.Metadata exportPressure = new ProcessBoundaryConstraintEvidence.Metadata( + "gas-export-pressure", "Field", "gas export boundary", ProcessBoundaryConstraintEvidence.Kind.EXPORT_CAPACITY, + ProcessBoundaryConstraintEvidence.FlowDirection.OUT_OF_PROCESS, NetworkDecisionVariable.RateBasis.MASS, + "synthetic gas export specification", 1.0, null, null, + ProcessBoundaryConstraintEvidence.ApplicabilityStatus.APPLICABLE, "pressure", "NeqSim process replay", null, + -1); + simulation.addBoundaryConstraint("gas export pressure", exportPressure, + model -> ProcessBoundaryConstraintEvidence.Sample + .available(fixture.exportValve.getOutletStream().getPressure("bara")), + ProcessModelSimulationEvaluator.ConstraintDefinition.Type.EQUALITY, 100.0, Double.POSITIVE_INFINITY, 0.1, + "bara", true, 1.0, 0.1); + return simulation; + } + /** Verify every action and the physical network returned to the baseline route. */ private void assertBaselineRestored(Fixture fixture) { LoopedPipeNetwork network = fixture.networkUnit.getHydraulicNetwork(); - assertEquals(70.0, network.getPipe("choke A").getChokeOpening(), 0.0); - assertEquals(70.0, network.getPipe("choke B").getChokeOpening(), 0.0); - assertEquals(0.5, fixture.facilityRouter.getSplitFactors()[0], 1.0e-12); - assertEquals(0.5, fixture.facilityRouter.getSplitFactors()[1], 1.0e-12); - assertEquals(fixture.baselineRouteAHigh, massFlow(network, "A gathering line"), - fixture.baselineRouteAHigh * 1.0e-5); + for (int index = 0; index < fixture.wellSuffixes.size(); index++) { + String suffix = fixture.wellSuffixes.get(index); + int expectedRoute = index % 2; + assertEquals(70.0, network.getPipe("choke " + suffix).getChokeOpening(), 0.0); + assertEquals(expectedRoute, + fixture.networkUnit.getExclusiveRouteSelection("producer " + suffix + " pressure level")); + assertEquals(expectedRoute == 0 ? 1.0 : 0.0, network.getPipe(suffix + " HP route").getAvailability(), 0.0); + assertEquals(expectedRoute == 1 ? 1.0 : 0.0, network.getPipe(suffix + " LP route").getAvailability(), 0.0); + } + assertEquals(fixture.baselineRouteAHigh, massFlow(network, "A HP route"), fixture.baselineRouteAHigh * 1.0e-5); + assertTrue(fixture.highPressureScrubber.solved()); + assertTrue(fixture.lowPressureScrubber.solved()); + assertTrue(fixture.exportMixer.solved()); + assertTrue(fixture.exportValve.solved()); + assertEquals(100.0, fixture.exportValve.getOutletStream().getPressure("bara"), 1.0e-6); assertTrue(fixture.model.isModelConverged()); assertTrue(fixture.networkUnit.getLastCouplingResult().isConverged()); }