diff --git a/.github/skills/neqsim-pipeline-and-terminal-networks/SKILL.md b/.github/skills/neqsim-pipeline-and-terminal-networks/SKILL.md index 82e26d22dd2..48612f6c465 100644 --- a/.github/skills/neqsim-pipeline-and-terminal-networks/SKILL.md +++ b/.github/skills/neqsim-pipeline-and-terminal-networks/SKILL.md @@ -91,7 +91,12 @@ Register configured rotating-equipment maps with existing `Pump`/`Compressor` remains the chart owner under the canonical edge identity. `ProcessModel.getPower(...)` includes solved pump/compressor demand inside standalone `LoopedPipeNetwork` and wrapped `FieldWellNetworkProcessUnit` equipment; use that total -for shared host-power evidence and fail closed on non-finite map results. +for shared host-power evidence and fail closed on non-finite map results. Registration +requires an enabled map before graph mutation, and a chart-backed compressor calculation +failure must remain infeasible rather than falling through to simplified polytropic physics. +Recheck chart availability and the equipment's `solved()` contract for every mapped +calculation. Finite forward results from a failed pressure-target speed search are not +accepted operating points; rejected results must invalidate power and clear stored thermal state. The dedicated `TwoFluidPipeNetwork` already owns storage-node transients; do not replace it. Live well/injection coupling must reuse WellSystem/WellFlow physics. diff --git a/docs/development/TASK_LOG.md b/docs/development/TASK_LOG.md index f340a7b02cd..6405ab46583 100644 --- a/docs/development/TASK_LOG.md +++ b/docs/development/TASK_LOG.md @@ -42,6 +42,12 @@ requirement`, or `confidential compressor route`. **Solution:** Extract the existing `EosMixingRuleHandler` correction and six deterministic `ClassicMixingRuleAccumulatorTest` cases from PR #4306 as a shared foundation. Use calculation-local accumulation in general covolume, classic attraction temperature second derivative, and composition-dependent attraction calculations; retain legacy serial result fields. **Notes:** All six interleaving regressions fail on the unchanged implementation and pass with the correction. The three-train compressor guide counterfactual removes 39 observed search/replay discrepancies by changing this mixing-rule class alone; single, multi-variable, staged and helper examples remain feasible with unchanged physical limits. Validation used Java 17 with Java 8 source/target; Windows Java 8 remains a CI gate. No compressor solver, tolerance, equation or public API changes. Coverage does not claim every mixing family or concurrent configuration mutation. The mixing-rules guide documents the scope. +### 2026-10-14 — Coordinate mapped-network and compressor speed-solve acceptance +**Type:** E (Feature) +**Keywords:** PR 4328, PR 4326, mapped compressor, pressure target, convergence, stale thermal state, shared power +**Solution:** `LoopedPipeNetwork.calculateHeadLossCompressor`, `MappedCompressorAcceptanceTest`, and the SURF integration guide. +**Notes:** A bounded chart speed search requested 100 bara and returned an internally consistent 11.647013866 bara forward state with `solved() == false`, while the network labelled it a valid chart result and retained 53.131953657 kW. Require the existing equipment convergence contract, an enabled chart, finite non-negative power, and positive finite outlet pressure/temperature before accepting mapped evidence. Reuse the existing infeasible-edge path to invalidate power and clear previous thermal state. Four regressions first establish a valid state, then exercise an unsolved result, chart disablement, non-finite power, and non-finite temperature. Keep the common network boundary in PR #4328 and the compressor's speed-search implementation in PR #4326; no system-specific solver or new constraint framework is introduced. + ### 2026-10-14 — Repair shared CI failures and preserve workflow diagnostics **Type:** G (Workflow) **Keywords:** PR 4315, hermetic tests, evidence-source-cues, slow shards, required checks, Surefire, CI diff --git a/docs/fielddevelopment/SURF_NETWORK_INTEGRATION.md b/docs/fielddevelopment/SURF_NETWORK_INTEGRATION.md index aaa6d04228e..76e8fdfbf25 100644 --- a/docs/fielddevelopment/SURF_NETWORK_INTEGRATION.md +++ b/docs/fielddevelopment/SURF_NETWORK_INTEGRATION.md @@ -486,6 +486,15 @@ same canonical topology. Both builders require an enabled chart before mutating preserve the configured model object, stable equipment tag and edge identity, and leave route-local hydraulic selection unchanged. Fixed-pressure and fixed-differential-pressure pump builders remain available for screening cases. +The direct `LoopedPipeNetwork` mapped-equipment builders apply the same enabled-chart +precondition before graph mutation. If a configured compressor chart fails during the +network solve, the edge reports non-finite power and an infeasible chart status; it does +not silently continue with the simplified polytropic compressor equations. +Each subsequent evaluation also requires the chart to remain enabled, the compressor's +existing `solved()` contract to pass, and finite non-negative power with positive finite +outlet pressure and temperature. A pressure-target speed search that returns a finite +forward state but misses its target therefore remains infeasible. Rejected results clear +the edge's previous thermal state so later network and shared-power checks cannot reuse it. `LoopedPipeNetwork.getPower(...)` aggregates the solved shaft power of its compressor and pump edges. `FieldWellNetworkProcessUnit` exposes that same demand, and normal diff --git a/src/main/java/neqsim/process/equipment/network/LoopedPipeNetwork.java b/src/main/java/neqsim/process/equipment/network/LoopedPipeNetwork.java index 079a6f739c5..d9de9e70dcf 100644 --- a/src/main/java/neqsim/process/equipment/network/LoopedPipeNetwork.java +++ b/src/main/java/neqsim/process/equipment/network/LoopedPipeNetwork.java @@ -2955,8 +2955,13 @@ public NetworkPipe addCompressor(String fromNode, String toNode, String elementN * @param elementName element name * @param compressor pre-configured NeqSim {@link Compressor} with chart * @return the created element + * @throws IllegalArgumentException if the compressor or enabled chart is missing */ public NetworkPipe addCompressorWithChart(String fromNode, String toNode, String elementName, Compressor compressor) { + if (compressor == null || compressor.getCompressorChart() == null + || !compressor.getCompressorChart().isUseCompressorChart()) { + throw new IllegalArgumentException("Chart-backed compressor requires an enabled compressor chart"); + } NetworkPipe element = addPipe(fromNode, toNode, elementName, 1.0, 0.5); element.setElementType(NetworkElementType.COMPRESSOR); element.setCompressorModel(compressor); @@ -3013,10 +3018,11 @@ public NetworkPipe addPumpDifferentialPressure(String fromNode, String toNode, S * @param elementName element name * @param pump configured pump with head/efficiency/NPSH curves * @return created pump edge + * @throws IllegalArgumentException if the pump or enabled chart is missing */ public NetworkPipe addPumpWithCurve(String fromNode, String toNode, String elementName, Pump pump) { - if (pump == null) { - throw new IllegalArgumentException("Configured pump model cannot be null"); + if (pump == null || pump.getPumpChart() == null || !pump.getPumpChart().isUsePumpChart()) { + throw new IllegalArgumentException("Curve-backed pump requires an enabled pump chart"); } NetworkPipe element = addPipe(fromNode, toNode, elementName, 1.0, 0.3); element.setElementType(NetworkElementType.PUMP); @@ -5573,6 +5579,9 @@ private double calculateHeadLossCompressor(NetworkPipe pipe, SystemInterface flu if (pipe.getCompressorModel() != null) { try { Compressor comp = pipe.getCompressorModel(); + if (comp.getCompressorChart() == null || !comp.getCompressorChart().isUseCompressorChart()) { + throw new IllegalStateException("Registered compressor chart is no longer enabled"); + } SystemInterface compFluid = fluid.clone(); compFluid.setPressure(upstreamNode.getPressure() / 1e5, "bara"); @@ -5585,18 +5594,29 @@ private double calculateHeadLossCompressor(NetworkPipe pipe, SystemInterface flu comp.run(); double power = comp.getPower("kW"); + SystemInterface outletFluid = comp.getOutletStream().getFluid().clone(); + double outletPressure = comp.getOutletStream().getPressure("Pa"); + double outletTemperature = outletFluid.getTemperature(); + if (!comp.solved() || !Double.isFinite(power) || power < 0.0 || !Double.isFinite(outletPressure) + || outletPressure <= 0.0 || !Double.isFinite(outletTemperature) || outletTemperature <= 0.0) { + throw new IllegalStateException("Compressor chart returned an unsolved or non-finite operating point"); + } pipe.setCompressorPower(power); pipe.setCompressorEfficiency(comp.getPolytropicEfficiency()); - SystemInterface outletFluid = comp.getOutletStream().getFluid().clone(); pipe.setThermodynamicState(compFluid, outletFluid, forward); - pipe.setOutletTemperature(outletFluid.getTemperature()); + pipe.setOutletTemperature(outletTemperature); // Pressure rise from suction to discharge - double pRise = comp.getOutletStream().getPressure("Pa") - upstreamNode.getPressure(); + double pRise = outletPressure - upstreamNode.getPressure(); pipe.setFlowRegime("Compressor-Chart"); return -Math.signum(pipe.getFlowRate()) * Math.abs(pRise); } catch (Exception ex) { logger.warn("Compressor chart calculation failed for {}: {}", pipe.getName(), ex.getMessage()); + pipe.setCompressorPower(Double.NaN); + pipe.setFlowRegime("Compressor-Chart-Infeasible"); + pipe.setThermodynamicState(null, null, true); + pipe.setOutletTemperature(Double.NaN); + return 1.0e12; } } diff --git a/src/main/resources/neqsim/mcp/engineering-coverage.json b/src/main/resources/neqsim/mcp/engineering-coverage.json index 48f7a8e1727..c35f6c84261 100644 --- a/src/main/resources/neqsim/mcp/engineering-coverage.json +++ b/src/main/resources/neqsim/mcp/engineering-coverage.json @@ -2623,7 +2623,7 @@ {"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":"6de374779ac1f055a7b82a44b4b1ab33bbb5564825fca12022b3adae8a3412e1","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":"6d564d66a0d10f33f427c6bbb3e54d4f40e8904d33db08b9aeb1dc0e367c8d95","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/LoopedPipeNetworkTest.java b/src/test/java/neqsim/process/equipment/network/LoopedPipeNetworkTest.java index 82511552306..e7ad6fb9131 100644 --- a/src/test/java/neqsim/process/equipment/network/LoopedPipeNetworkTest.java +++ b/src/test/java/neqsim/process/equipment/network/LoopedPipeNetworkTest.java @@ -8,12 +8,15 @@ import static org.junit.jupiter.api.Assertions.assertTrue; import java.util.List; import java.util.Map; +import java.util.UUID; import org.apache.logging.log4j.LogManager; import org.apache.logging.log4j.Logger; import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Disabled; import org.junit.jupiter.api.Tag; import org.junit.jupiter.api.Test; +import neqsim.process.equipment.compressor.Compressor; +import neqsim.process.equipment.pump.Pump; import neqsim.process.equipment.pipeline.PipeBeggsAndBrills; import neqsim.process.equipment.reservoir.SimpleReservoir; import neqsim.process.equipment.stream.Stream; @@ -1531,6 +1534,57 @@ void testCompressorElement() { assertEquals(LoopedPipeNetwork.NetworkElementType.COMPRESSOR, comp.getElementType()); } + /** Chart-backed rotating equipment rejects incomplete maps before mutating the graph. */ + @Test + void testMappedRotatingEquipmentRegistrationIsAtomic() { + LoopedPipeNetwork network = new LoopedPipeNetwork("mapped registration"); + network.addJunctionNode("suction"); + network.addJunctionNode("discharge"); + + Pump chartlessPump = new Pump("chartless pump", new Stream("pump feed", testGas.clone())); + Compressor chartlessCompressor = new Compressor("chartless compressor", + new Stream("compressor feed", testGas.clone())); + + assertThrows(IllegalArgumentException.class, + () -> network.addPumpWithCurve("suction", "discharge", "pump", chartlessPump)); + assertThrows(IllegalArgumentException.class, + () -> network.addCompressorWithChart("suction", "discharge", "compressor", chartlessCompressor)); + assertTrue(network.getPipeNames().isEmpty()); + } + + /** A configured compressor-chart failure must not fall through to simplified polytropic physics. */ + @Test + void testMappedCompressorFailureFailsClosed() { + LoopedPipeNetwork network = new LoopedPipeNetwork("failed mapped compressor"); + network.setFluidTemplate(testGas); + network.setSolverType(LoopedPipeNetwork.SolverType.NEWTON_RAPHSON); + network.setMaxIterations(10); + network.addSourceNode("supply", 30.0, 0.0); + network.addFixedPressureSinkNode("delivery", 60.0); + + Compressor failingCompressor = new Compressor("failing compressor", + new Stream("compressor feed", testGas.clone())) { + private static final long serialVersionUID = 1L; + + @Override + public void run(UUID id) { + throw new IllegalStateException("synthetic chart failure"); + } + }; + failingCompressor.getCompressorChart().setCurves(new double[] {298.15, 1.0, 1.0, 1.0}, new double[] {9000.0}, + new double[][] {{1000.0, 1500.0, 2000.0}}, new double[][] {{80.0, 70.0, 55.0}}, + new double[][] {{72.0, 78.0, 74.0}}); + failingCompressor.getCompressorChart().setUseCompressorChart(true); + network.addCompressorWithChart("supply", "delivery", "compressor", failingCompressor); + + network.run(); + + LoopedPipeNetwork.NetworkPipe edge = network.getPipe("compressor"); + assertEquals("Compressor-Chart-Infeasible", edge.getFlowRegime()); + assertTrue(Double.isNaN(edge.getCompressorPower())); + assertTrue(Double.isNaN(network.getPower("kW"))); + } + /** * Test regulator (pressure reducing valve) element. */ diff --git a/src/test/java/neqsim/process/equipment/network/MappedCompressorAcceptanceTest.java b/src/test/java/neqsim/process/equipment/network/MappedCompressorAcceptanceTest.java new file mode 100644 index 00000000000..d148c9c696a --- /dev/null +++ b/src/test/java/neqsim/process/equipment/network/MappedCompressorAcceptanceTest.java @@ -0,0 +1,119 @@ +package neqsim.process.equipment.network; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertNotNull; +import static org.junit.jupiter.api.Assertions.assertNull; +import static org.junit.jupiter.api.Assertions.assertTrue; +import java.util.UUID; +import org.junit.jupiter.api.Test; +import neqsim.NeqSimTest; +import neqsim.process.equipment.compressor.Compressor; +import neqsim.process.equipment.stream.Stream; +import neqsim.thermo.system.SystemInterface; +import neqsim.thermo.system.SystemSrkEos; + +/** + * Acceptance of mapped compressor calculation evidence at the canonical network boundary. + * + * @author Even Solbraa + * @version 1.0 + */ +class MappedCompressorAcceptanceTest extends NeqSimTest { + /** A returned finite state does not certify a calculation that reports unsolved. */ + @Test + void unsolvedFiniteChartResultFailsClosed() { + assertRejected(false, 1000.0, 310.0, true); + } + + /** Disabling a registered chart cannot switch an existing mapped edge to simpler physics. */ + @Test + void chartDisabledAfterRegistrationFailsClosed() { + assertRejected(true, 1000.0, 310.0, false); + } + + /** A non-finite demand cannot leave a solved chart label or reusable thermal state. */ + @Test + void nonFiniteChartPowerFailsClosed() { + assertRejected(true, Double.NaN, 310.0, true); + } + + /** A non-finite outlet cannot leave a solved chart label or reusable thermal state. */ + @Test + void nonFiniteChartTemperatureFailsClosed() { + assertRejected(true, 1000.0, Double.NaN, true); + } + + /** + * Exercise the public network solve with one deliberately incomplete chart result. + * + * @param solved result reported by the existing equipment convergence contract + * @param power returned shaft power in W + * @param temperature returned outlet temperature in K + * @param chartEnabled whether the chart remains enabled after registration + */ + private void assertRejected(boolean solved, double power, double temperature, boolean chartEnabled) { + boolean[] returnedSolved = {true}; + double[] returnedPower = {1000.0}; + double[] returnedTemperature = {310.0}; + SystemInterface fluid = new SystemSrkEos(300.0, 30.0); + fluid.addComponent("methane", 1.0); + fluid.setMixingRule("classic"); + LoopedPipeNetwork network = new LoopedPipeNetwork("mapped result acceptance"); + network.setFluidTemplate(fluid); + network.setSolverType(LoopedPipeNetwork.SolverType.NEWTON_RAPHSON); + network.setMaxIterations(2); + network.addSourceNode("supply", 30.0, 0.0); + network.addFixedPressureSinkNode("delivery", 60.0); + Compressor compressor = new Compressor("controlled mapped compressor", new Stream("feed", fluid.clone())) { + private static final long serialVersionUID = 1L; + + /** {@inheritDoc} */ + @Override + public void run(UUID id) { + SystemInterface result = getInletStream().getThermoSystem().clone(); + result.setPressure(60.0); + result.setTemperature(returnedTemperature[0]); + getOutletStream().setThermoSystem(result); + setPolytropicEfficiency(0.75); + dH = returnedPower[0]; + setCalculationIdentifier(id); + } + + /** {@inheritDoc} */ + @Override + public boolean solved() { + return returnedSolved[0]; + } + + /** {@inheritDoc} */ + @Override + public double getPower(String unit) { + return new neqsim.util.unit.PowerUnit(returnedPower[0], "W").getValue(unit); + } + }; + compressor.getCompressorChart().setCurves(new double[] {298.15, 1.0, 1.0, 1.0}, new double[] {9000.0}, + new double[][] {{1000.0, 1500.0, 2000.0}}, new double[][] {{80.0, 70.0, 55.0}}, + new double[][] {{72.0, 78.0, 74.0}}); + compressor.getCompressorChart().setUseCompressorChart(true); + LoopedPipeNetwork.NetworkPipe edge = network.addCompressorWithChart("supply", "delivery", "compressor", compressor); + network.run(); + assertEquals("Compressor-Chart", edge.getFlowRegime()); + assertTrue(Double.isFinite(edge.getCompressorPower())); + assertNotNull(edge.getInletFluid()); + assertNotNull(edge.getOutletFluid()); + + returnedSolved[0] = solved; + returnedPower[0] = power; + returnedTemperature[0] = temperature; + compressor.getCompressorChart().setUseCompressorChart(chartEnabled); + + network.run(); + + assertEquals("Compressor-Chart-Infeasible", edge.getFlowRegime()); + assertTrue(Double.isNaN(edge.getCompressorPower())); + assertTrue(Double.isNaN(network.getPower("kW"))); + assertNull(edge.getInletFluid()); + assertNull(edge.getOutletFluid()); + assertTrue(Double.isNaN(edge.getOutletTemperature())); + } +}