Documentation for looped pipeline network modeling with Hardy Cross solver in NeqSim.
Table of Contents
- Overview
- LoopedPipeNetwork Class
- Hardy Cross Method
- Network Topology
- Solver Configuration
- Usage Examples
- Advanced Features
- Related Documentation
Overview
Location: neqsim.process.equipment.network
The LoopedPipeNetwork class extends basic pipeline network concepts to support looped configurations commonly found in:
- Water distribution systems
- Oil and gas gathering networks
- Gas transmission systems
- District heating networks
| Class | Description |
|---|---|
LoopedPipeNetwork |
Network with loop support and Hardy Cross solver |
NetworkLoop |
Represents an independent loop in the network |
LoopDetector |
Detects loops using DFS spanning tree algorithm |
LoopedPipeNetwork Class
Class Hierarchy
ProcessEquipmentBaseClass
└── LoopedPipeNetwork
├── contains: List<PipeNode>
├── contains: List<PipeSegment>
├── contains: List<NetworkLoop>
└── uses: LoopDetector
Key Features
- Loop Detection: Automatic identification of independent loops
- Hardy Cross Solver: Iterative flow distribution algorithm
- Node Types: Source, sink, and junction nodes
- Pressure Calculation: Node pressures from flow solution
- Compositional Tracking: Full compositional mass balance
Constructor
import neqsim.process.equipment.network.LoopedPipeNetwork;
// Create network
LoopedPipeNetwork network = new LoopedPipeNetwork("Distribution Network");
// Set fluid template
network.setFluidTemplate(gasFluid);
Hardy Cross Method
Algorithm Overview
The Hardy Cross method is an iterative technique for solving flow networks with loops:
- Initial Flow Estimates: Assign flows satisfying mass balance at nodes
- Loop Detection: Identify independent loops using DFS
- Head Loss Calculation: For each loop, calculate head loss imbalance
- Flow Correction: Calculate correction for each loop
- Apply Corrections: Update all pipes in each loop
- Iterate: Repeat until convergence ($\Delta H < tolerance$)
Head Loss Equations
For turbulent flow (Darcy-Weisbach):
\[h = f \cdot \frac{L}{D} \cdot \frac{v^2}{2g} = K \cdot Q^n\]Where:
- $f$ = Darcy friction factor
- $L$ = Pipe length
- $D$ = Pipe diameter
- $v$ = Fluid velocity
- $n$ ≈ 2 for turbulent flow
Convergence Criteria
// Set convergence tolerance
network.setTolerance(1e-6); // Pressure balance tolerance
// Set maximum iterations
network.setMaxIterations(100);
// Get convergence info
int iterations = network.getIterationCount();
double residual = network.getResidual();
boolean converged = network.isConverged();
Network Topology
Node Types
| Type | Description | Required Input |
|---|---|---|
| Source | Supplies fluid (well, compressor) | Pressure, flow rate |
| Sink | Removes fluid (customer, export) | Flow rate (demand) |
| Junction | Connection point | None |
Adding Nodes
// Add source node (supply point)
network.addSourceNode("Supply-1", 50.0, 1000.0); // 50 bar, 1000 kg/hr
// Add sink node (demand point)
network.addSinkNode("Customer-1", 200.0); // 200 kg/hr demand
network.addSinkNode("Customer-2", 300.0); // 300 kg/hr demand
// Add junction node (connection point)
network.addJunctionNode("Junction-A");
network.addJunctionNode("Junction-B");
Adding Pipes
// Connect nodes with pipes
// addPipe(fromNode, toNode, pipeName, length, diameter)
network.addPipe("Supply-1", "Junction-A", "Pipe-1", 5000.0, 0.3); // 5km, 12"
network.addPipe("Junction-A", "Junction-B", "Pipe-2", 3000.0, 0.25); // 3km, 10"
network.addPipe("Junction-B", "Customer-1", "Pipe-3", 2000.0, 0.15); // 2km, 6"
network.addPipe("Junction-A", "Customer-2", "Pipe-4", 4000.0, 0.2); // 4km, 8"
// Create loop by connecting B back to A
network.addPipe("Junction-B", "Junction-A", "Pipe-5", 2500.0, 0.2); // Loop
Network Visualization
[Supply-1]
│
▼
┌─────────┐
│ Pipe-1 │
└────┬────┘
│
▼
┌──────────────┐
│ Junction-A │◄────────┐
└──────┬───────┘ │
│ │
┌────────┼────────┐ │
│ │ │ │
▼ ▼ ▼ │
Pipe-4 Pipe-2 Pipe-5 │
│ │ │ │
▼ ▼ └────────┤
Customer-2 Junction-B ────────┘
│
▼
Pipe-3
│
▼
Customer-1
Solver Configuration
Solver Types
// Set solver type
network.setSolverType(LoopedPipeNetwork.SolverType.HARDY_CROSS);
// Other options:
// SolverType.NEWTON_RAPHSON - Faster convergence, more complex
// SolverType.LINEAR - For small networks
Solver Parameters
// Convergence settings
network.setTolerance(1e-6);
network.setMaxIterations(100);
// Damping factor (0-1, helps stability)
network.setDampingFactor(0.7);
// Pressure drop model
network.setPressureDropModel("Darcy-Weisbach"); // or "Hazen-Williams"
Running the Solver
// Run network solution
network.run();
// Check convergence
if (network.isConverged()) {
System.out.println("Converged in " + network.getIterationCount() + " iterations");
} else {
System.out.println("Failed to converge. Residual: " + network.getResidual());
}
Usage Examples
Example 1: Simple Ring Main Network
import neqsim.process.equipment.network.LoopedPipeNetwork;
import neqsim.thermo.system.SystemSrkEos;
// Create gas fluid template
SystemSrkEos gas = new SystemSrkEos(298.15, 50.0);
gas.addComponent("methane", 0.9);
gas.addComponent("ethane", 0.07);
gas.addComponent("propane", 0.03);
gas.setMixingRule("classic");
// Create network
LoopedPipeNetwork network = new LoopedPipeNetwork("Ring Main");
network.setFluidTemplate(gas);
// Add nodes
network.addSourceNode("Supply", 50.0, 1000.0); // 50 bar, 1000 kg/hr
network.addJunctionNode("A");
network.addJunctionNode("B");
network.addJunctionNode("C");
network.addSinkNode("Demand-1", 300.0); // 300 kg/hr
network.addSinkNode("Demand-2", 400.0); // 400 kg/hr
network.addSinkNode("Demand-3", 300.0); // 300 kg/hr
// Connect with pipes (creates ring)
network.addPipe("Supply", "A", "Main", 2000.0, 0.3);
network.addPipe("A", "B", "Ring-AB", 1000.0, 0.2);
network.addPipe("B", "C", "Ring-BC", 1000.0, 0.2);
network.addPipe("C", "A", "Ring-CA", 1000.0, 0.2); // Closes ring
network.addPipe("A", "Demand-1", "Spur-1", 500.0, 0.15);
network.addPipe("B", "Demand-2", "Spur-2", 500.0, 0.15);
network.addPipe("C", "Demand-3", "Spur-3", 500.0, 0.15);
// Solve
network.setSolverType(LoopedPipeNetwork.SolverType.HARDY_CROSS);
network.setTolerance(1e-6);
network.run();
// Results
System.out.println("Converged: " + network.isConverged());
System.out.println("Iterations: " + network.getIterationCount());
// Print pipe flows
for (String pipeName : network.getPipeNames()) {
double flow = network.getPipeFlowRate(pipeName);
System.out.printf("%s: %.1f kg/hr%n", pipeName, flow);
}
// Print node pressures
for (String nodeName : network.getNodeNames()) {
double pressure = network.getNodePressure(nodeName);
System.out.printf("%s: %.2f bar%n", nodeName, pressure);
}
Example 2: Gas Gathering Network
// Multi-well gathering system
LoopedPipeNetwork gathering = new LoopedPipeNetwork("Gas Gathering");
gathering.setFluidTemplate(gasFluid);
// Wells as source nodes
gathering.addSourceNode("Well-A", 80.0, 500.0);
gathering.addSourceNode("Well-B", 75.0, 400.0);
gathering.addSourceNode("Well-C", 70.0, 600.0);
gathering.addSourceNode("Well-D", 72.0, 350.0);
// Central processing as sink
gathering.addSinkNode("CPF", 1850.0); // Total flow
// Manifolds as junctions
gathering.addJunctionNode("Manifold-North");
gathering.addJunctionNode("Manifold-South");
gathering.addJunctionNode("Main-Header");
// Well connections
gathering.addPipe("Well-A", "Manifold-North", "FL-A", 3000.0, 0.15);
gathering.addPipe("Well-B", "Manifold-North", "FL-B", 2500.0, 0.15);
gathering.addPipe("Well-C", "Manifold-South", "FL-C", 4000.0, 0.15);
gathering.addPipe("Well-D", "Manifold-South", "FL-D", 3500.0, 0.15);
// Trunk lines
gathering.addPipe("Manifold-North", "Main-Header", "Trunk-N", 5000.0, 0.25);
gathering.addPipe("Manifold-South", "Main-Header", "Trunk-S", 6000.0, 0.25);
// Loop for reliability
gathering.addPipe("Manifold-North", "Manifold-South", "Tie-Line", 4000.0, 0.2);
// Export
gathering.addPipe("Main-Header", "CPF", "Export", 8000.0, 0.3);
// Solve
gathering.run();
// Check minimum pressure
double minPressure = gathering.getMinimumNodePressure();
System.out.println("Minimum node pressure: " + minPressure + " bar");
Example 3: Water Distribution Network
// Water network using Hazen-Williams
LoopedPipeNetwork water = new LoopedPipeNetwork("Water Distribution");
water.setPressureDropModel("Hazen-Williams");
water.setHazenWilliamsC(130.0); // Ductile iron
// Create water fluid
SystemSrkEos waterFluid = new SystemSrkEos(293.15, 5.0);
waterFluid.addComponent("water", 1.0);
waterFluid.setMixingRule("classic");
water.setFluidTemplate(waterFluid);
// Water treatment plant
water.addSourceNode("WTP", 5.0, 10000.0); // 5 bar, 10 m³/hr
// Distribution points
water.addSinkNode("Area-A", 2500.0);
water.addSinkNode("Area-B", 3000.0);
water.addSinkNode("Area-C", 2000.0);
water.addSinkNode("Area-D", 2500.0);
// Junctions
water.addJunctionNode("J1");
water.addJunctionNode("J2");
water.addJunctionNode("J3");
water.addJunctionNode("J4");
// Pipes forming grid with multiple loops
water.addPipe("WTP", "J1", "Main-1", 1000.0, 0.4);
water.addPipe("J1", "J2", "Main-2", 800.0, 0.3);
water.addPipe("J2", "J3", "Main-3", 600.0, 0.3);
water.addPipe("J3", "J4", "Main-4", 800.0, 0.3);
water.addPipe("J4", "J1", "Main-5", 700.0, 0.25); // Loop 1
water.addPipe("J1", "J3", "Cross-1", 900.0, 0.2); // Loop 2
// Connections to areas
water.addPipe("J1", "Area-A", "Dist-A", 300.0, 0.2);
water.addPipe("J2", "Area-B", "Dist-B", 400.0, 0.2);
water.addPipe("J3", "Area-C", "Dist-C", 350.0, 0.2);
water.addPipe("J4", "Area-D", "Dist-D", 500.0, 0.2);
// Solve
water.run();
// Verify minimum pressure
for (String sink : Arrays.asList("Area-A", "Area-B", "Area-C", "Area-D")) {
double p = water.getNodePressure(sink);
System.out.printf("%s pressure: %.2f bar (min 2.0 bar required)%n", sink, p);
}
Example 4: Network with Multiple Sources
// Network with redundant supply
LoopedPipeNetwork redundant = new LoopedPipeNetwork("Redundant Supply");
redundant.setFluidTemplate(gasFluid);
// Two gas supply stations
redundant.addSourceNode("Station-A", 60.0, 5000.0);
redundant.addSourceNode("Station-B", 58.0, 4000.0);
// Large industrial customer
redundant.addSinkNode("Industry", 9000.0);
// Grid junctions
for (int i = 1; i <= 4; i++) {
redundant.addJunctionNode("J" + i);
}
// Build interconnected grid
redundant.addPipe("Station-A", "J1", "Feed-A", 5000.0, 0.4);
redundant.addPipe("Station-B", "J2", "Feed-B", 6000.0, 0.4);
redundant.addPipe("J1", "J2", "Tie-12", 3000.0, 0.3);
redundant.addPipe("J1", "J3", "Main-13", 4000.0, 0.35);
redundant.addPipe("J2", "J4", "Main-24", 4500.0, 0.35);
redundant.addPipe("J3", "J4", "Tie-34", 2000.0, 0.3);
redundant.addPipe("J3", "Industry", "Dist-3", 1000.0, 0.3);
redundant.addPipe("J4", "Industry", "Dist-4", 1500.0, 0.3);
// Solve normal operation
redundant.run();
System.out.println("Normal operation - Industry pressure: " +
redundant.getNodePressure("Industry") + " bar");
// Simulate Station-A outage
redundant.setSourceFlow("Station-A", 0.0);
redundant.run();
System.out.println("Station-A outage - Industry pressure: " +
redundant.getNodePressure("Industry") + " bar");
Advanced Features
Loop Detection
// Get detected loops
List<NetworkLoop> loops = network.getLoops();
System.out.println("Number of independent loops: " + loops.size());
for (NetworkLoop loop : loops) {
System.out.println("Loop: " + loop.getName());
for (String pipe : loop.getPipes()) {
System.out.println(" - " + pipe);
}
}
Sensitivity Analysis
For a production network containing a fixed-pressure source reservoir and an IPR
element ipr, sweep reservoir pressure in bara:
double[] pressures = {80.0, 100.0, 140.0, 180.0};
Map<String, double[]> sweep = network.sensitivityAnalysis("ipr", "reservoir_pressure", pressures);
double[] rates = sweep.get("totalFlow_kghr");
double[] valid = sweep.get("valid");
Map<Integer, String> failures = network.getSensitivityFailures();
Import java.util.Map for these result maps. The source-node name reservoir can
also be used as the sweep target. Each sample updates the fixed source pressure and
every IPR originating at that source, following
network.setReservoirPressure("reservoir", pressureBara). Other reservoirs retain
their settings. A source controlled by a connected feed stream is rejected: vary
the feed stream pressure explicitly because a network run reads that boundary
from the stream.
| Result key | Meaning |
|---|---|
paramValues |
Copy of requested values, in the selected parameter’s units |
totalFlow_kghr |
Accepted total sink mass flow in kg/h; NaN for rejected samples |
objective |
Accepted revenue objective, or total mass flow when no prices are configured; NaN for rejected samples |
converged |
1.0 if the sampled solve returned with numerical convergence, otherwise 0.0 |
applicable |
1.0 if convergence and the supported choke envelope pass, otherwise 0.0 |
valid |
1.0 if the sample is applicable and both flow and objective are finite, otherwise 0.0 |
getSensitivityFailures() returns an immutable snapshot of rejected sample indices
(zero-based) and their reasons, including exceptions, nonconvergence and unsupported
choke status. A successful zero-flow point remains valid; failed points are never
encoded as zero production. Subsequent samples still run after a failure. These
checks do not establish facility-constraint feasibility or independent well-model
qualification.
When migrating from earlier versions, check valid[i] == 1.0 before using a sample.
Code that treated a zero result as a failed solve should use getSensitivityFailures()
instead. Requested choke openings are evaluated exactly in the range 0–100%; out-of-range
values are rejected instead of silently clamped.
The original source pressure and each connected IPR’s original pressure are saved
independently, restored even after a failed sweep, and the baseline is re-solved.
If the baseline solve itself throws, the exception propagates with the original
input settings restored. The same restoration applies to well_pi,
choke_opening (including full closure), sink_pressure and pipe_diameter sweeps.
Regression coverage is in LoopedPipeNetworkSensitivityTest.
JSON Export
// Export network to JSON
String json = network.toJson();
// Includes:
// - Node definitions and pressures
// - Pipe definitions and flows
// - Loop information
// - Convergence data
Related Documentation
- Production Well Networks - IPR, choke, tubing, and multiphase elements in production networks
- Gas Network Operations - conservative mixing, quality, allocation, and linepack
- Oil Network Operations - pump edges, terminal tanks, parcels, and cargo scheduling
- Pipeline Networks (Basic) - Non-looped networks
- Pipelines - Individual pipeline modeling
- Process Systems - System integration
API Reference
LoopedPipeNetwork
// Constructor
LoopedPipeNetwork(String name)
// Fluid template
void setFluidTemplate(SystemInterface fluid)
// Node management
void addSourceNode(String name, double pressure, double flow)
void addSinkNode(String name, double demand)
void addJunctionNode(String name)
void setSourceFlow(String name, double flow)
// Pipe management
void addPipe(String from, String to, String name, double length, double diameter)
List<String> getPipeNames()
double getPipeFlowRate(String pipeName)
// Solver configuration
void setSolverType(SolverType type)
void setTolerance(double tol)
void setMaxIterations(int max)
void setDampingFactor(double factor)
void setPressureDropModel(String model)
void setHazenWilliamsC(double c)
// Run and results
void run()
boolean isConverged()
int getIterationCount()
double getResidual()
// Node results
List<String> getNodeNames()
double getNodePressure(String nodeName)
double getMinimumNodePressure()
// Loop information
List<NetworkLoop> getLoops()
// Analysis
Map<String, double[]> sensitivityAnalysis(String element, String parameterType, double[] values)
Map<Integer, String> getSensitivityFailures()
// Export
String toJson()
SolverType Enum
enum SolverType {
HARDY_CROSS,
NEWTON_RAPHSON,
LINEAR
}
NetworkLoop Class
class NetworkLoop {
String getName()
List<String> getPipes()
double getHeadLossImbalance()
}