diff --git a/.github/skills/neqsim-pipeline-and-terminal-networks/SKILL.md b/.github/skills/neqsim-pipeline-and-terminal-networks/SKILL.md index 39ae231f167..13540ec4a90 100644 --- a/.github/skills/neqsim-pipeline-and-terminal-networks/SKILL.md +++ b/.github/skills/neqsim-pipeline-and-terminal-networks/SKILL.md @@ -75,6 +75,13 @@ input while preserving the edge hydraulic model. Candidate indices are categoric minimum-temperature requirements must be invariant, and flexible/vendor designs are outside this contract. Never combine this selector with a second diameter variable on the same edge or treat a passing screen as qualified design. +For coherent direct, brownfield and redundant-loop line-ups, register named +`FieldWellNetworkProcessUnit.RouteConfiguration` options on the existing process-unit +adapter. `configuration..selection` is a categorical `ProcessAutomation` +input: one write atomically assigns exact zero/one availability to the complete +controlled edge set and preserves all per-edge hydraulic models. Do not emulate a +multi-edge line-up with independent availability actions; that permits partial +configurations during candidate application and restoration. The dedicated `TwoFluidPipeNetwork` already owns storage-node transients; do not replace it. Live well/injection coupling must reuse WellSystem/WellFlow physics. @@ -86,6 +93,7 @@ replace it. Live well/injection coupling must reuse WellSystem/WellFlow physics. | Hydraulic details: `NetworkLinearSolver`, `NetworkPipe`, pipe-model bindings | `neqsim.process.equipment.network` | Linear solve helpers and per-edge hydraulic state; edges may use Darcy-Weisbach or a configured `PipeBeggsAndBrills` model. | `NetworkLinearSolver.solve`, `NetworkLinearSolver.solveGaussian`, `NetworkPipe.getFlowRate`, `NetworkPipe.getBBModel` | | Well and facility elements | `neqsim.process.equipment.network` | IPR, tubing, choke, compressor, pump, regulator, artificial-lift, and related source/edge elements within the network. | `addWellIPR`, `addChoke`; configure through `NetworkPipe` element-specific setters | | Typed field/SURF identity: `FieldNetworkTopology` | `neqsim.process.equipment.network` | Stable production/injection roles, equipment tags and ports over the canonical network, including typed choke, pump and compressor construction. | `addPipe`, `addChoke`, `addPump`, `addPumpDifferentialPressure`, `addCompressor`, `validateForExecution`, `copyDefinition` | +| Live field route line-ups: `FieldWellNetworkProcessUnit`, `RouteConfiguration` | `neqsim.process.equipment.network` | Runs the live well/network coupler in a process model and atomically switches named multi-edge direct, brownfield or redundant configurations on the same canonical graph. | `registerExclusiveRouteSelector`, `registerRouteConfigurationSelector`, `getRouteConfigurationNames`, `setRouteConfigurationSelection` | | Canonical SURF design basis: `FieldNetworkSurfDesignBasis`, `SubseaProductionSystem` | `neqsim.process.fielddevelopment.subsea` | Read-only typed equipment and exact route geometry plus edge-keyed mechanical/cost/thermal selections into existing Class 4 and steady network contracts. | `createSurfDesignBasis`, `setLineDesign`, `createLineDesignDecisionVariable`, `createLineDesignScreeningConstraints`, `applyLineThermalDesigns`, `assessDnvStF101LineDesigns`, `assessOnBottomStabilityLineDesigns`, `estimateSurfCosts`, `createSurfCapitalCostObjective` | | SURF route cost/design: `SURFCostEstimator.LineDesign`, `PipeMechanicalDesignCalculator`, `MultilayerThermalCalculator`, DNV screening kernels | `neqsim.process.mechanicaldesign.subsea`, `neqsim.process.mechanicaldesign.pipeline`, `neqsim.process.equipment.pipeline` | Route-specific OD, wall, material, installation, typed insulation and explicit structural/environmental input with existing mechanical/thermal screening and MTO output. | `LineDesign.builder`, `dnvStF101Input`, `onBottomStabilityInput`, `resolveOverallHeatTransferCoefficientWm2K`, `getDetailedEstimateResult` | | Constrained optimization: `NetworkOptimizer`, `NetworkConstraints`, `NetworkObjectives`, `NetworkDecisionVariable` | `neqsim.process.equipment.network` | Bounded operating search plus complete fail-closed enumeration of all-discrete physical designs. | `NetworkDecisionVariable.pipeDiameter`, `NetworkDecisionVariable.customDiscrete`, `NetworkOptimizer.optimize`, `optimizeMultiObjective` | diff --git a/docs/fielddevelopment/SURF_NETWORK_INTEGRATION.md b/docs/fielddevelopment/SURF_NETWORK_INTEGRATION.md index 46f752b2b27..95ed07c770c 100644 --- a/docs/fielddevelopment/SURF_NETWORK_INTEGRATION.md +++ b/docs/fielddevelopment/SURF_NETWORK_INTEGRATION.md @@ -460,6 +460,22 @@ therefore feeds back through the same live well/network coupling instead of acti downstream flow split. Copies preserve selector definitions but keep independent edge state. +For a line-up that changes several edges together, construct named +`FieldWellNetworkProcessUnit.RouteConfiguration` objects and register them with +`registerRouteConfigurationSelector(...)`. Automation exposes +`configuration..selection`. Each option lists the controlled edges that are +fully available; every other edge appearing in any option is exactly closed. This +supports direct versus brownfield routes and an option with both paths open as one +redundant loop without rebuilding the topology. Registration rejects missing edges, +duplicate line-ups, selector overlap and a current state that does not match exactly one +declared option. A selection snapshots every controlled availability, applies and reads +back the complete line-up, and restores the snapshot after any failure. Edge geometry, +equipment identity and Darcy-Weisbach/Beggs-Brill/two-fluid selection are never changed. +The focused live-well acceptance case checks direct, higher-resistance brownfield and +dual-path configurations, exact restoration, positive flow in both redundant paths and +network mass conservation. This is synthetic integration evidence, not an outage or +brownfield field qualification. + The synthetic `FieldToFacilityOptimizationAcceptanceTest` connects two live wells and 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 @@ -481,7 +497,9 @@ physical limits to obtain a feasible optimizer result. 1. Extend the field-scale cases to live `WellSystem`/`WellFlow` production and injection boundaries, qualified pump/compressor maps, shared host power/capacity constraints and controlled reservoir-pressure updates without duplicating reservoir ownership. - Add branch/loop outage and brownfield impact studies through #4188/#3154 optimization. + The canonical multi-edge line-up contract now supports branch/loop outage and brownfield + actions through #4188/#3154 optimization; next exercise it in the larger production and + injection field cases with qualified facility maps and lifecycle evidence. 2. Qualify steady arrival temperature, DNV-ST-F101 and DNV-RP-F109 route cases against independent field, laboratory or commercial-simulator evidence. The software contract for staged OD/ID/wall/material/installation/insulation/stability alternatives now exists with synthetic diff --git a/src/main/java/neqsim/process/automation/ProcessAutomation.java b/src/main/java/neqsim/process/automation/ProcessAutomation.java index b2d2ea4a340..73c03caac14 100644 --- a/src/main/java/neqsim/process/automation/ProcessAutomation.java +++ b/src/main/java/neqsim/process/automation/ProcessAutomation.java @@ -595,6 +595,9 @@ public double getVariableValue(String address, String unitOfMeasure) { if (unit instanceof FieldWellNetworkProcessUnit && parts.length == 3 && "route".equals(parts[1])) { return getFieldRouteProperty((FieldWellNetworkProcessUnit) unit, parts[2], unitOfMeasure); } + if (unit instanceof FieldWellNetworkProcessUnit && parts.length == 3 && "configuration".equals(parts[1])) { + return getFieldConfigurationProperty((FieldWellNetworkProcessUnit) unit, parts[2], unitOfMeasure); + } LoopedPipeNetwork addressableNetwork = getAddressableNetwork(unit); if (addressableNetwork != null && parts.length == 3) { return getNetworkProperty(addressableNetwork, parts[1], parts[2], unitOfMeasure); @@ -656,6 +659,11 @@ public void setVariableValue(String address, double value, String unitOfMeasure) this.dirty = true; return; } + if (unit instanceof FieldWellNetworkProcessUnit && parts.length == 3 && "configuration".equals(parts[1])) { + setFieldConfigurationProperty((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); @@ -892,6 +900,14 @@ private List buildVariableList(String unitName, ProcessEquip "Exclusive route index; provenance: " + fieldUnit.getExclusiveRouteProvenance(selectorName)).withBounds( Double.valueOf(0.0), Double.valueOf(fieldUnit.getExclusiveRouteEdges(selectorName).size() - 1.0))); } + for (String selectorName : fieldUnit.getRouteConfigurationSelectorNames()) { + networkVariables.add(new SimulationVariable(unitName + ".configuration." + selectorName + ".selection", + "configuration." + selectorName + ".selection", VariableType.INPUT, "-", + "Route configuration index; options: " + fieldUnit.getRouteConfigurationNames(selectorName) + + "; provenance: " + fieldUnit.getRouteConfigurationProvenance(selectorName)) + .withBounds(Double.valueOf(0.0), + Double.valueOf(fieldUnit.getRouteConfigurationNames(selectorName).size() - 1.0))); + } } return enrichVariableMetadata(networkVariables); } @@ -1251,6 +1267,42 @@ private void setFieldRouteProperty(FieldWellNetworkProcessUnit unit, String targ unit.setExclusiveRouteSelection(targetProperty[0], value); } + /** + * Read one field-unit multi-edge route configuration. + * + * @param unit field well/network process unit + * @param targetAndProperty selector name and selection property + * @param unitOfMeasure requested dimensionless unit + * @return zero-based selected configuration index + */ + private double getFieldConfigurationProperty(FieldWellNetworkProcessUnit unit, String targetAndProperty, + String unitOfMeasure) { + String[] targetProperty = splitNetworkTargetProperty(targetAndProperty); + requireDimensionlessUnit(unitOfMeasure); + if (!"selection".equals(targetProperty[1])) { + throw new IllegalArgumentException("Unknown field route-configuration property: " + targetProperty[1]); + } + return unit.getRouteConfigurationSelection(targetProperty[0]); + } + + /** + * Write one field-unit multi-edge route configuration. + * + * @param unit field well/network process unit + * @param targetAndProperty selector name and selection property + * @param value zero-based selected configuration index + * @param unitOfMeasure requested dimensionless unit + */ + private void setFieldConfigurationProperty(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-configuration property: " + targetProperty[1]); + } + unit.setRouteConfigurationSelection(targetProperty[0], value); + } + /** * Require a dimensionless route-selector unit. * diff --git a/src/main/java/neqsim/process/equipment/network/FieldWellNetworkProcessUnit.java b/src/main/java/neqsim/process/equipment/network/FieldWellNetworkProcessUnit.java index c5561b8e018..96ec9747e1f 100644 --- a/src/main/java/neqsim/process/equipment/network/FieldWellNetworkProcessUnit.java +++ b/src/main/java/neqsim/process/equipment/network/FieldWellNetworkProcessUnit.java @@ -33,29 +33,99 @@ 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(); + /** Route-configuration 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 { + /** + * Immutable named line-up of available edges in one canonical field network. + * + *

+ * Every listed edge is assigned exact availability {@code 1.0}. Other edges controlled by the same selector are + * assigned exact availability {@code 0.0}. Edges outside the selector remain unchanged. + *

+ */ + public static final class RouteConfiguration implements java.io.Serializable { /** Serialization version UID. */ private static final long serialVersionUID = 1L; - /** Existing network edges in discrete selection order. */ - private final List edgeNames; + /** Stable configuration name. */ + private final String name; + + /** Existing edges that are available in this configuration. */ + private final List availableEdgeNames; + + /** + * Create one named exact route configuration. + * + * @param name stable configuration name + * @param availableEdgeNames existing edges that are fully available in this configuration + */ + public RouteConfiguration(String name, List availableEdgeNames) { + this.name = requireName(name); + if (availableEdgeNames == null || availableEdgeNames.isEmpty()) { + throw new IllegalArgumentException("Route configuration requires at least one available edge"); + } + List validatedEdges = new ArrayList(); + for (String edgeName : availableEdgeNames) { + String normalizedEdge = requireName(edgeName); + if (validatedEdges.contains(normalizedEdge)) { + throw new IllegalArgumentException("Route configuration contains duplicate edge: " + normalizedEdge); + } + validatedEdges.add(normalizedEdge); + } + this.availableEdgeNames = Collections.unmodifiableList(validatedEdges); + } + + /** + * Get the stable configuration name. + * + * @return configuration name + */ + public String getName() { + return name; + } + + /** + * Get the edges assigned exact availability one by this configuration. + * + * @return immutable available-edge list + */ + public List getAvailableEdgeNames() { + return availableEdgeNames; + } + } + + /** Immutable definition of one route-configuration selector. */ + private static final class RouteConfigurationSelector implements java.io.Serializable { + /** Serialization version UID. */ + private static final long serialVersionUID = 1L; + + /** Configurations in discrete selection order. */ + private final List configurations; + + /** Union of every edge controlled by the configurations. */ + private final List controlledEdgeNames; /** Engineering source for treating the edges as mutually exclusive routes. */ private final String provenance; + /** Whether this selector was registered through the compatible one-edge route API. */ + private final boolean exclusiveSingleEdge; + /** * Create one immutable selector definition. * - * @param edgeNames existing network edges in selection order - * @param provenance engineering source for the exclusive line-up + * @param configurations configurations in selection order + * @param controlledEdgeNames union of controlled edges + * @param provenance engineering source for the line-ups + * @param exclusiveSingleEdge true for the compatible one-edge route API */ - private ExclusiveRouteSelector(List edgeNames, String provenance) { - this.edgeNames = Collections.unmodifiableList(new ArrayList(edgeNames)); + private RouteConfigurationSelector(List configurations, List controlledEdgeNames, + String provenance, boolean exclusiveSingleEdge) { + this.configurations = Collections.unmodifiableList(new ArrayList(configurations)); + this.controlledEdgeNames = Collections.unmodifiableList(new ArrayList(controlledEdgeNames)); this.provenance = provenance; + this.exclusiveSingleEdge = exclusiveSingleEdge; } } @@ -113,8 +183,13 @@ public CouplingResult getLastCouplingResult() { @Override public ProcessEquipmentInterface copy() { FieldWellNetworkProcessUnit copy = new FieldWellNetworkProcessUnit(getName(), coupler.copy()); - for (Map.Entry entry : routeSelectors.entrySet()) { - copy.registerExclusiveRouteSelector(entry.getKey(), entry.getValue().edgeNames, entry.getValue().provenance); + for (Map.Entry entry : routeSelectors.entrySet()) { + RouteConfigurationSelector selector = entry.getValue(); + if (selector.exclusiveSingleEdge) { + copy.registerExclusiveRouteSelector(entry.getKey(), selector.controlledEdgeNames, selector.provenance); + } else { + copy.registerRouteConfigurationSelector(entry.getKey(), selector.configurations, selector.provenance); + } } return copy; } @@ -135,29 +210,98 @@ public ProcessEquipmentInterface copy() { */ 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(); + List configurations = 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); + configurations.add(new RouteConfiguration(normalizedEdge, Collections.singletonList(normalizedEdge))); + } + registerRouteConfigurationSelector(selectorName, configurations, provenance, true); + return this; + } + + /** + * Register named, discrete multi-edge route configurations over the existing canonical network. + * + *

+ * A configuration may represent a direct path, brownfield reroute, daisy-chain line-up, or redundant loop by listing + * every controlled edge that must have availability {@code 1.0}. Every other edge appearing in any option is set to + * {@code 0.0}. Registration requires the current edge state to match exactly one option and rejects overlap with + * another selector. The selector owns no topology or hydraulic model. + *

+ * + * @param selectorName stable selector identity used by process automation + * @param configurations at least two distinct named edge line-ups in selection order + * @param provenance engineering source for the route line-ups + * @return this process unit for chaining + */ + public FieldWellNetworkProcessUnit registerRouteConfigurationSelector(String selectorName, + List configurations, String provenance) { + registerRouteConfigurationSelector(selectorName, configurations, provenance, false); + return this; + } + + /** + * Register and validate one selector, preserving the compatible one-edge route surface when requested. + * + * @param selectorName stable selector identity + * @param configurations candidate line-ups in selection order + * @param provenance engineering source for the line-ups + * @param exclusiveSingleEdge true when registered by the compatible one-edge route API + */ + private void registerRouteConfigurationSelector(String selectorName, List configurations, + String provenance, boolean exclusiveSingleEdge) { + String normalizedName = requireName(selectorName); + String normalizedProvenance = requireName(provenance); + if (routeSelectors.containsKey(normalizedName)) { + throw new IllegalArgumentException("Route configuration selector is already registered: " + normalizedName); + } + if (configurations == null || configurations.size() < 2) { + throw new IllegalArgumentException("Route configuration selector requires at least two configurations"); + } + + List validatedConfigurations = new ArrayList(); + List configurationNames = new ArrayList(); + List> signatures = new ArrayList>(); + List controlledEdges = new ArrayList(); + for (RouteConfiguration configuration : configurations) { + if (configuration == null) { + throw new IllegalArgumentException("Route configurations cannot contain null"); + } + if (configurationNames.contains(configuration.getName())) { + throw new IllegalArgumentException("Duplicate route configuration name: " + configuration.getName()); + } + configurationNames.add(configuration.getName()); + List sortedActiveEdges = new ArrayList(configuration.getAvailableEdgeNames()); + Collections.sort(sortedActiveEdges); + if (signatures.contains(sortedActiveEdges)) { + throw new IllegalArgumentException("Route configurations must define distinct edge line-ups"); + } + signatures.add(sortedActiveEdges); + for (String edgeName : configuration.getAvailableEdgeNames()) { + if (getHydraulicNetwork().getPipe(edgeName) == null) { + throw new IllegalArgumentException("Route configuration edge does not exist: " + edgeName); + } + if (!controlledEdges.contains(edgeName)) { + controlledEdges.add(edgeName); + } } - if (validatedEdges.contains(normalizedEdge)) { - throw new IllegalArgumentException("Exclusive route selector contains duplicate edge: " + normalizedEdge); + validatedConfigurations.add(configuration); + } + for (Map.Entry entry : routeSelectors.entrySet()) { + for (String edgeName : controlledEdges) { + if (entry.getValue().controlledEdgeNames.contains(edgeName)) { + throw new IllegalArgumentException( + "Route configuration edge '" + edgeName + "' is already controlled by selector '" + entry.getKey() + "'"); + } } - validatedEdges.add(normalizedEdge); } - ExclusiveRouteSelector selector = new ExclusiveRouteSelector(validatedEdges, normalizedProvenance); - requireSelectedRoute(normalizedName, selector); + RouteConfigurationSelector selector = new RouteConfigurationSelector(validatedConfigurations, controlledEdges, + normalizedProvenance, exclusiveSingleEdge); + requireSelectedConfiguration(normalizedName, selector); routeSelectors.put(normalizedName, selector); - return this; } /** @@ -166,7 +310,13 @@ public FieldWellNetworkProcessUnit registerExclusiveRouteSelector(String selecto * @return immutable selector-name list */ public List getExclusiveRouteSelectorNames() { - return Collections.unmodifiableList(new ArrayList(routeSelectors.keySet())); + List names = new ArrayList(); + for (Map.Entry entry : routeSelectors.entrySet()) { + if (entry.getValue().exclusiveSingleEdge) { + names.add(entry.getKey()); + } + } + return Collections.unmodifiableList(names); } /** @@ -176,7 +326,11 @@ public List getExclusiveRouteSelectorNames() { * @return immutable route-edge list in discrete selection order */ public List getExclusiveRouteEdges(String selectorName) { - return getRouteSelector(selectorName).edgeNames; + RouteConfigurationSelector selector = getRouteSelector(selectorName); + if (!selector.exclusiveSingleEdge) { + throw new IllegalArgumentException("Selector is not an exclusive one-edge route selector: " + selectorName); + } + return selector.controlledEdgeNames; } /** @@ -186,7 +340,11 @@ public List getExclusiveRouteEdges(String selectorName) { * @return engineering source for the exclusive line-up */ public String getExclusiveRouteProvenance(String selectorName) { - return getRouteSelector(selectorName).provenance; + RouteConfigurationSelector selector = getRouteSelector(selectorName); + if (!selector.exclusiveSingleEdge) { + throw new IllegalArgumentException("Selector is not an exclusive one-edge route selector: " + selectorName); + } + return selector.provenance; } /** @@ -197,7 +355,11 @@ public String getExclusiveRouteProvenance(String selectorName) { * @throws IllegalStateException when the route availabilities are not exactly one-hot */ public int getExclusiveRouteSelection(String selectorName) { - return requireSelectedRoute(selectorName, getRouteSelector(selectorName)); + RouteConfigurationSelector selector = getRouteSelector(selectorName); + if (!selector.exclusiveSingleEdge) { + throw new IllegalArgumentException("Selector is not an exclusive one-edge route selector: " + selectorName); + } + return requireSelectedConfiguration(selectorName, selector); } /** @@ -214,31 +376,93 @@ public int getExclusiveRouteSelection(String selectorName) { * @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"); + RouteConfigurationSelector selector = getRouteSelector(selectorName); + if (!selector.exclusiveSingleEdge) { + throw new IllegalArgumentException("Selector is not an exclusive one-edge route selector: " + selectorName); } - 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]); + setSelectedConfiguration(selectorName, selector, selectedRoute); + } + + /** + * Get multi-edge route-configuration selector names in registration order. + * + * @return immutable selector-name list + */ + public List getRouteConfigurationSelectorNames() { + List names = new ArrayList(); + for (Map.Entry entry : routeSelectors.entrySet()) { + if (!entry.getValue().exclusiveSingleEdge) { + names.add(entry.getKey()); } - throw exception; } + return Collections.unmodifiableList(names); + } + + /** + * Get configuration names for one multi-edge selector. + * + * @param selectorName registered selector identity + * @return immutable configuration-name list in discrete selection order + */ + public List getRouteConfigurationNames(String selectorName) { + RouteConfigurationSelector selector = getConfigurationSelector(selectorName); + List names = new ArrayList(); + for (RouteConfiguration configuration : selector.configurations) { + names.add(configuration.getName()); + } + return Collections.unmodifiableList(names); + } + + /** + * Get the immutable configuration definitions for one multi-edge selector. + * + * @param selectorName registered selector identity + * @return immutable configurations in discrete selection order + */ + public List getRouteConfigurations(String selectorName) { + return getConfigurationSelector(selectorName).configurations; + } + + /** + * Get every canonical edge controlled by one multi-edge selector. + * + * @param selectorName registered selector identity + * @return immutable controlled-edge list + */ + public List getRouteConfigurationEdges(String selectorName) { + return getConfigurationSelector(selectorName).controlledEdgeNames; + } + + /** + * Get provenance for one multi-edge selector. + * + * @param selectorName registered selector identity + * @return engineering source for the line-ups + */ + public String getRouteConfigurationProvenance(String selectorName) { + return getConfigurationSelector(selectorName).provenance; + } + + /** + * Get the selected multi-edge route configuration. + * + * @param selectorName registered selector identity + * @return zero-based selected configuration index + */ + public int getRouteConfigurationSelection(String selectorName) { + RouteConfigurationSelector selector = getConfigurationSelector(selectorName); + return requireSelectedConfiguration(selectorName, selector); + } + + /** + * Atomically apply one named multi-edge route configuration. + * + * @param selectorName registered selector identity + * @param selectedConfiguration zero-based configuration index + */ + public void setRouteConfigurationSelection(String selectorName, double selectedConfiguration) { + RouteConfigurationSelector selector = getConfigurationSelector(selectorName); + setSelectedConfiguration(selectorName, selector, selectedConfiguration); } /** @@ -247,41 +471,105 @@ public void setExclusiveRouteSelection(String selectorName, double selectedRoute * @param selectorName registered selector identity * @return immutable selector definition */ - private ExclusiveRouteSelector getRouteSelector(String selectorName) { - ExclusiveRouteSelector selector = routeSelectors.get(selectorName); + private RouteConfigurationSelector getRouteSelector(String selectorName) { + RouteConfigurationSelector selector = routeSelectors.get(selectorName); if (selector == null) { - throw new IllegalArgumentException("Unknown exclusive route selector: " + selectorName); + throw new IllegalArgumentException("Unknown route configuration selector: " + selectorName); } return selector; } /** - * Require and return an exact one-hot route selection. + * Resolve one registered multi-edge configuration selector. + * + * @param selectorName registered selector identity + * @return immutable multi-edge selector definition + */ + private RouteConfigurationSelector getConfigurationSelector(String selectorName) { + RouteConfigurationSelector selector = getRouteSelector(selectorName); + if (selector.exclusiveSingleEdge) { + throw new IllegalArgumentException("Selector is not a multi-edge route configuration: " + selectorName); + } + return selector; + } + + /** + * Require and return one exactly matching declared route configuration. * * @param selectorName selector identity used in diagnostics * @param selector immutable selector definition - * @return zero-based selected route index + * @return zero-based selected configuration index */ - private int requireSelectedRoute(String selectorName, ExclusiveRouteSelector selector) { + private int requireSelectedConfiguration(String selectorName, RouteConfigurationSelector 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) { + for (String edgeName : selector.controlledEdgeNames) { + double availability = getHydraulicNetwork().getPipe(edgeName).getAvailability(); + if (availability != 0.0 && availability != 1.0) { + throw new IllegalStateException("Route configuration selector requires exact zero-or-one availability: " + + selectorName + " edge=" + edgeName); + } + } + for (int index = 0; index < selector.configurations.size(); index++) { + RouteConfiguration configuration = selector.configurations.get(index); + boolean matches = true; + for (String edgeName : selector.controlledEdgeNames) { + double expected = configuration.availableEdgeNames.contains(edgeName) ? 1.0 : 0.0; + if (getHydraulicNetwork().getPipe(edgeName).getAvailability() != expected) { + matches = false; + break; + } + } + if (matches) { if (selectedIndex >= 0) { - throw new IllegalStateException("Exclusive route selector has multiple available routes: " + selectorName); + throw new IllegalStateException( + "Route configuration selector has ambiguous matching line-ups: " + 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); + throw new IllegalStateException( + "Route configuration selector does not match a declared line-up: " + selectorName); } return selectedIndex; } + /** + * Apply one configuration transactionally and restore every edge after a failed write or read-back. + * + * @param selectorName selector identity used in diagnostics + * @param selector immutable selector definition + * @param selectedConfiguration zero-based configuration index + */ + private void setSelectedConfiguration(String selectorName, RouteConfigurationSelector selector, + double selectedConfiguration) { + if (!Double.isFinite(selectedConfiguration) || selectedConfiguration != Math.rint(selectedConfiguration) + || selectedConfiguration < 0.0 || selectedConfiguration >= selector.configurations.size()) { + throw new IllegalArgumentException("Route configuration selection must be a declared integer index"); + } + int selectedIndex = (int) selectedConfiguration; + double[] baseline = new double[selector.controlledEdgeNames.size()]; + for (int index = 0; index < selector.controlledEdgeNames.size(); index++) { + baseline[index] = getHydraulicNetwork().getPipe(selector.controlledEdgeNames.get(index)).getAvailability(); + } + RouteConfiguration configuration = selector.configurations.get(selectedIndex); + try { + for (String edgeName : selector.controlledEdgeNames) { + getHydraulicNetwork().getPipe(edgeName) + .setAvailability(configuration.availableEdgeNames.contains(edgeName) ? 1.0 : 0.0); + } + if (requireSelectedConfiguration(selectorName, selector) != selectedIndex) { + throw new IllegalStateException("Route configuration read-back did not match the requested line-up"); + } + isSolved = false; + } catch (RuntimeException exception) { + for (int index = 0; index < selector.controlledEdgeNames.size(); index++) { + getHydraulicNetwork().getPipe(selector.controlledEdgeNames.get(index)).setAvailability(baseline[index]); + } + throw exception; + } + } + /** * Get one stable solved sink stream for downstream processing. * diff --git a/src/main/resources/neqsim/mcp/engineering-coverage.json b/src/main/resources/neqsim/mcp/engineering-coverage.json index 980727b8513..e6becfaf200 100644 --- a/src/main/resources/neqsim/mcp/engineering-coverage.json +++ b/src/main/resources/neqsim/mcp/engineering-coverage.json @@ -1889,7 +1889,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":"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.ProcessAutomation","kind":"class","source":"src/main/java/neqsim/process/automation/ProcessAutomation.java","sourceDigest":"a7c58246caca6ab33cdc609119d5a2ad75c70631db6abadfdcd76fbcc85910e7","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-pipeline-and-terminal-networks","neqsim-plant-data"],"agents":["capability-scout","capacity-increase-screening","ccs-hydrogen","continuous-improvement","control-system","documentation","dynamic-equipment","emissions-environmental","engineering-deliverables","extract-process","field-development","flow-assurance","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"}, @@ -2592,7 +2592,7 @@ {"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":"a4fe11819d7a5846c8b3ec79bbb542ada6461644f0cb7f06eb9e452ca4aae400","domain":"process","skills":["neqsim-pipeline-and-terminal-networks"],"agents":["field-development","flow-assurance","network-energy-systems"],"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":"475a4af66ab62c6606b7941ba04868520f168d3d1dc6a6c99bd8dabcafd6a85a","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":"4590f8d4b309c9b14c7c9e552e539cd7a6dcdef4e514a89160239814c00a6f08","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.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"}, diff --git a/src/test/java/neqsim/process/equipment/network/FieldWellNetworkProcessUnitTest.java b/src/test/java/neqsim/process/equipment/network/FieldWellNetworkProcessUnitTest.java index 6ab2bb95560..e8378f02359 100644 --- a/src/test/java/neqsim/process/equipment/network/FieldWellNetworkProcessUnitTest.java +++ b/src/test/java/neqsim/process/equipment/network/FieldWellNetworkProcessUnitTest.java @@ -129,6 +129,79 @@ void switchesExclusivePressureRoutesThroughAutomationAndRestoresExactly() { assertEquals(1.0, copy.getExclusiveRouteSelection("producer pressure level"), 0.0); } + /** Multi-edge direct, brownfield and dual-path line-ups must use one canonical graph and restore exactly. */ + @Test + void switchesCanonicalMultiEdgeRouteConfigurationsThroughAutomation() { + FieldWellNetworkProcessUnit unit = createConfiguredUnit(); + unit.run(UUID.randomUUID()); + assertTrue(unit.solved(), unit.getLastCouplingResult().getMessage()); + + Separator separator = new Separator("configured host separator", unit.getOutletStream("host")); + ProcessSystem field = new ProcessSystem("configured field and host"); + field.add(unit); + field.add(separator); + ProcessModel model = new ProcessModel(); + model.add("Field", field); + model.run(); + assertTrue(model.isModelConverged()); + + ProcessAutomation automation = model.getAutomation(); + String address = "Field::configured field network.configuration.host route.selection"; + assertTrue(automation.isWritableAddress(address)); + assertEquals(Arrays.asList("primary", "brownfield", "dual path"), unit.getRouteConfigurationNames("host route")); + assertEquals(Arrays.asList("primary flowline", "primary riser"), + unit.getRouteConfigurations("host route").get(0).getAvailableEdgeNames()); + assertEquals(0.0, automation.getVariableValue(address, "-"), 0.0); + double primaryRate = unit.getOutletStream("host").getFlowRate("kg/hr"); + double primaryWellPressure = unit.getHydraulicNetwork().getNodePressure("producer"); + + automation.setVariableValue(address, 1.0, "-"); + assertFalse(unit.solved()); + assertConfiguration(unit, 0.0, 0.0, 1.0, 1.0); + model.run(); + assertTrue(model.isModelConverged()); + double brownfieldRate = unit.getOutletStream("host").getFlowRate("kg/hr"); + assertTrue(brownfieldRate < primaryRate); + assertTrue(unit.getHydraulicNetwork().getNodePressure("producer") > primaryWellPressure); + + automation.setVariableValue(address, 2.0, "-"); + assertConfiguration(unit, 1.0, 1.0, 1.0, 1.0); + model.run(); + assertTrue(model.isModelConverged()); + double dualPathRate = unit.getOutletStream("host").getFlowRate("kg/hr"); + assertTrue(dualPathRate > brownfieldRate); + assertTrue(unit.getHydraulicNetwork().getPipe("primary flowline").getFlowRate() > 0.0); + assertTrue(unit.getHydraulicNetwork().getPipe("brownfield flowline").getFlowRate() > 0.0); + assertEquals(0.0, unit.getLastCouplingResult().getMassBalanceResidualKgS(), 1.0e-8); + assertEquals(LoopedPipeNetwork.PipeModelType.DARCY_WEISBACH, + unit.getHydraulicNetwork().getEffectiveHydraulicModelType("primary flowline")); + + assertThrows(IllegalArgumentException.class, () -> automation.setVariableValue(address, 1.5, "-")); + assertConfiguration(unit, 1.0, 1.0, 1.0, 1.0); + + automation.setVariableValue(address, 0.0, "-"); + model.run(); + assertTrue(model.isModelConverged()); + assertEquals(primaryRate, unit.getOutletStream("host").getFlowRate("kg/hr"), primaryRate * 1.0e-5); + assertEquals(primaryWellPressure, unit.getHydraulicNetwork().getNodePressure("producer"), 1.0e-5); + + FieldWellNetworkProcessUnit copy = (FieldWellNetworkProcessUnit) unit.copy(); + assertEquals(Arrays.asList("host route"), copy.getRouteConfigurationSelectorNames()); + copy.setRouteConfigurationSelection("host route", 1.0); + assertEquals(0, unit.getRouteConfigurationSelection("host route")); + assertEquals(1, copy.getRouteConfigurationSelection("host route")); + + assertThrows(IllegalArgumentException.class, + () -> unit.registerRouteConfigurationSelector("overlap", + Arrays.asList( + new FieldWellNetworkProcessUnit.RouteConfiguration("primary only", Arrays.asList("primary flowline")), + new FieldWellNetworkProcessUnit.RouteConfiguration("choke only", Arrays.asList("production choke"))), + "invalid overlapping selector")); + unit.getHydraulicNetwork().getPipe("primary flowline").setAvailability(0.5); + assertThrows(IllegalStateException.class, () -> unit.getRouteConfigurationSelection("host route")); + unit.getHydraulicNetwork().getPipe("primary flowline").setAvailability(1.0); + } + /** The process unit must remain unsolved when the complete coupled convergence contract is not met. */ @Test void incompleteCouplingFailsClosed() { @@ -209,6 +282,68 @@ private FieldWellNetworkProcessUnit createRoutedUnit() { "producer pressure level", Arrays.asList("HP route", "LP route"), "synthetic HP/LP host line-up"); } + /** Create one live well with two alternative two-edge host paths and a combined looped line-up. */ + private FieldWellNetworkProcessUnit createConfiguredUnit() { + FieldNetworkTopology topology = new FieldNetworkTopology("configured 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-301", NodeRole.PRODUCTION_WELL, Service.PRODUCTION, 75.0, -200.0); + topology.addJunction("header", "MA-301", NodeRole.MANIFOLD, Service.PRODUCTION, -200.0); + topology.addJunction("primary PLEM", "PLEM-301", NodeRole.PLEM, Service.PRODUCTION, -200.0); + topology.addJunction("brownfield tie-in", "TI-301", NodeRole.BROWNFIELD_TIE_IN, Service.PRODUCTION, -200.0); + topology.addFixedPressureSink("host", "SEP-301", NodeRole.HOST, Service.PRODUCTION, 45.0, 0.0); + network.addChoke("producer", "header", "production choke", 30.0, 75.0); + topology.registerExistingEdge("production choke", "XV-301", EdgeRole.CHOKE, Service.PRODUCTION, + FlowDirection.FROM_TO, "well", "header"); + topology.addPipe("primary flowline", "FL-301", EdgeRole.FLOWLINE, Service.PRODUCTION, FlowDirection.FROM_TO, + "header", "primary outlet", "primary PLEM", "inlet", 900.0, 0.22, + LoopedPipeNetwork.PipeModelType.DARCY_WEISBACH); + topology.addPipe("primary riser", "RI-301", EdgeRole.RISER, Service.PRODUCTION, FlowDirection.FROM_TO, + "primary PLEM", "outlet", "host", "primary arrival", 700.0, 0.22, + LoopedPipeNetwork.PipeModelType.DARCY_WEISBACH); + topology.addPipe("brownfield flowline", "FL-302", EdgeRole.TIE_IN, Service.PRODUCTION, FlowDirection.FROM_TO, + "header", "brownfield outlet", "brownfield tie-in", "inlet", 2800.0, 0.16, + LoopedPipeNetwork.PipeModelType.DARCY_WEISBACH); + topology.addPipe("brownfield riser", "RI-302", EdgeRole.RISER, Service.PRODUCTION, FlowDirection.FROM_TO, + "brownfield tie-in", "outlet", "host", "brownfield arrival", 900.0, 0.16, + LoopedPipeNetwork.PipeModelType.DARCY_WEISBACH); + network.getPipe("brownfield flowline").setAvailability(0.0); + network.getPipe("brownfield riser").setAvailability(0.0); + + Stream reservoir = new Stream("configured reservoir", gas(200.0)); + reservoir.setFlowRate(0.02, "MSm3/day"); + reservoir.run(); + WellFlow well = new WellFlow("configured 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("configured field network", coupler).registerRouteConfigurationSelector( + "host route", + Arrays.asList( + new FieldWellNetworkProcessUnit.RouteConfiguration("primary", + Arrays.asList("primary flowline", "primary riser")), + new FieldWellNetworkProcessUnit.RouteConfiguration("brownfield", + Arrays.asList("brownfield flowline", "brownfield riser")), + new FieldWellNetworkProcessUnit.RouteConfiguration("dual path", + Arrays.asList("primary flowline", "primary riser", "brownfield flowline", "brownfield riser"))), + "synthetic direct, brownfield and redundant host line-ups"); + } + + /** Assert exact availability on the four controlled configuration edges. */ + private void assertConfiguration(FieldWellNetworkProcessUnit unit, double primaryFlowline, double primaryRiser, + double brownfieldFlowline, double brownfieldRiser) { + assertEquals(primaryFlowline, unit.getHydraulicNetwork().getPipe("primary flowline").getAvailability(), 0.0); + assertEquals(primaryRiser, unit.getHydraulicNetwork().getPipe("primary riser").getAvailability(), 0.0); + assertEquals(brownfieldFlowline, unit.getHydraulicNetwork().getPipe("brownfield flowline").getAvailability(), 0.0); + assertEquals(brownfieldRiser, unit.getHydraulicNetwork().getPipe("brownfield riser").getAvailability(), 0.0); + } + /** * Create a lean SRK gas. *