diff --git a/src/main/java/neqsim/process/equipment/separator/GasScrubber.java b/src/main/java/neqsim/process/equipment/separator/GasScrubber.java
index b501630b71..1004990bdf 100644
--- a/src/main/java/neqsim/process/equipment/separator/GasScrubber.java
+++ b/src/main/java/neqsim/process/equipment/separator/GasScrubber.java
@@ -15,31 +15,25 @@
*
*
*
- * A gas scrubber is a vertical separator designed primarily for removing liquid
- * droplets from gas
- * streams. Unlike standard separators, the key performance metric is the
- * K-value (Souders-Brown
+ * A gas scrubber is a vertical separator designed primarily for removing liquid droplets from gas
+ * streams. Unlike standard separators, the key performance metric is the K-value (Souders-Brown
* factor) rather than liquid retention time.
*
*
* Capacity Utilization Setup
*
*
- * To get meaningful capacity utilization from
- * {@link #getCapacityUtilization()}, set:
+ * To get meaningful capacity utilization from {@link #getCapacityUtilization()}, set:
*
*
* - {@link #setInternalDiameter(double)} — scrubber inner diameter [m]
- * - {@link #setDesignGasLoadFactor(double)} — design K-factor [m/s],
- * typically 0.04–0.10 for
+ *
- {@link #setDesignGasLoadFactor(double)} — design K-factor [m/s], typically 0.04–0.10 for
* vertical scrubbers
*
*
*
- * The orientation is automatically set to "vertical" and the design liquid
- * level fraction defaults
- * to 0.1 (10%), reflecting that scrubbers hold very little liquid. For dry gas
- * (no liquid phase), a
+ * The orientation is automatically set to "vertical" and the design liquid level fraction defaults
+ * to 0.1 (10%), reflecting that scrubbers hold very little liquid. For dry gas (no liquid phase), a
* default liquid density of 1000 kg/m³ is used.
*
*
@@ -80,7 +74,7 @@ public GasScrubber(String name) {
* Constructor for GasScrubber.
*
*
- * @param name a {@link java.lang.String} object
+ * @param name a {@link java.lang.String} object
* @param inletStream a {@link neqsim.process.equipment.stream.Stream} object
*/
public GasScrubber(String name, StreamInterface inletStream) {
@@ -94,7 +88,16 @@ public GasScrubber(String name, StreamInterface inletStream) {
/** {@inheritDoc} */
@Override
public void initMechanicalDesign() {
+ // Preserve existing geometry when re-initializing
+ double prevDiameter = getInternalDiameter();
+ double prevLength = getSeparatorLength();
separatorMechanicalDesign = new GasScrubberMechanicalDesign(this);
+ if (prevDiameter > 0) {
+ separatorMechanicalDesign.setInnerDiameter(prevDiameter);
+ }
+ if (prevLength > 0) {
+ separatorMechanicalDesign.setTantanLength(prevLength);
+ }
}
/** {@inheritDoc} */
diff --git a/src/main/java/neqsim/process/equipment/separator/Separator.java b/src/main/java/neqsim/process/equipment/separator/Separator.java
index 1afe5e6a49..993ed337e2 100644
--- a/src/main/java/neqsim/process/equipment/separator/Separator.java
+++ b/src/main/java/neqsim/process/equipment/separator/Separator.java
@@ -18,23 +18,23 @@
import com.google.gson.GsonBuilder;
import neqsim.physicalproperties.PhysicalPropertyType;
import neqsim.process.design.AutoSizeable;
+import neqsim.process.electricaldesign.separator.SeparatorElectricalDesign;
import neqsim.process.equipment.ProcessEquipmentBaseClass;
+import neqsim.process.equipment.ProcessEquipmentInterface;
import neqsim.process.equipment.capacity.CapacityConstrainedEquipment;
import neqsim.process.equipment.capacity.CapacityConstraint;
import neqsim.process.equipment.capacity.StandardConstraintType;
import neqsim.process.equipment.mixer.Mixer;
+import neqsim.process.equipment.separator.entrainment.InletDeviceModel;
+import neqsim.process.equipment.separator.entrainment.MultiphaseFlowRegime;
+import neqsim.process.equipment.separator.entrainment.SeparatorPerformanceCalculator;
import neqsim.process.equipment.separator.sectiontype.ManwaySection;
import neqsim.process.equipment.separator.sectiontype.MeshSection;
import neqsim.process.equipment.separator.sectiontype.NozzleSection;
import neqsim.process.equipment.separator.sectiontype.SeparatorSection;
import neqsim.process.equipment.separator.sectiontype.ValveSection;
import neqsim.process.equipment.stream.Stream;
-import neqsim.process.equipment.ProcessEquipmentInterface;
import neqsim.process.equipment.stream.StreamInterface;
-import neqsim.process.electricaldesign.separator.SeparatorElectricalDesign;
-import neqsim.process.equipment.separator.entrainment.InletDeviceModel;
-import neqsim.process.equipment.separator.entrainment.MultiphaseFlowRegime;
-import neqsim.process.equipment.separator.entrainment.SeparatorPerformanceCalculator;
import neqsim.process.instrumentdesign.separator.SeparatorInstrumentDesign;
import neqsim.process.mechanicaldesign.separator.SeparatorMechanicalDesign;
import neqsim.process.ml.StateVector;
@@ -185,14 +185,10 @@ public void initializeTransientCalculation() {
private double gasInLiquid = 0.0;
private String gasInLiquidSpec = "mole";
- /** Length of separator volume. */
- private double separatorLength = 5.0;
- /** Inner diameter/height of separator volume. */
- private double internalDiameter = 1.0;
- private double internalRadius = internalDiameter / 2;
-
- /** Liquid level height in meters (default set to 50% of internal diameter). */
- protected double liquidLevel = 0.5 * internalDiameter;
+ /**
+ * Liquid level height in meters. Initialised to 50% of internal diameter during construction.
+ */
+ protected double liquidLevel = 0.0;
private static final double MIN_HEADSPACE_FRACTION = 0.05;
private static final double MIN_HEADSPACE_VOLUME = 1.0e-6;
@@ -203,12 +199,6 @@ public void initializeTransientCalculation() {
*/
public static final double DEFAULT_LIQUID_DENSITY_FOR_SIZING = 1000.0;
- /** Separator cross sectional area. */
- private double sepCrossArea = Math.PI * internalDiameter * internalDiameter / 4.0;
-
- /** Separator volume. */
- private double separatorVolume = sepCrossArea * separatorLength;
-
double liquidVolume;
double gasVolume;
@@ -276,10 +266,15 @@ public void initializeTransientCalculation() {
*/
public Separator(String name) {
super(name);
+ initMechanicalDesign();
+ // Set backward-compatible geometry defaults on MechanicalDesign (single source
+ // of truth)
+ separatorMechanicalDesign.setInnerDiameter(1.0);
+ separatorMechanicalDesign.setTantanLength(5.0);
+ liquidLevel = 0.5 * getInternalDiameter();
liquidVolume = calcLiquidVolume();
enforceHeadspace();
setCalculateSteadyState(true);
- initMechanicalDesign();
initElectricalDesign();
initInstrumentDesign();
initializeCapacityConstraints();
@@ -306,7 +301,16 @@ public SeparatorMechanicalDesign getMechanicalDesign() {
/** {@inheritDoc} */
@Override
public void initMechanicalDesign() {
+ // Preserve existing geometry when re-initializing
+ double prevDiameter = getInternalDiameter();
+ double prevLength = getSeparatorLength();
separatorMechanicalDesign = new SeparatorMechanicalDesign(this);
+ if (prevDiameter > 0) {
+ separatorMechanicalDesign.setInnerDiameter(prevDiameter);
+ }
+ if (prevLength > 0) {
+ separatorMechanicalDesign.setTantanLength(prevLength);
+ }
}
/** {@inheritDoc} */
@@ -531,7 +535,7 @@ protected void updateEntrainmentFromPerformanceCalculator() {
gasVelocity = getGasSuperficialVelocity();
}
- double liquidLevelFrac = liquidLevel / internalDiameter;
+ double liquidLevelFrac = liquidLevel / getInternalDiameter();
if (liquidLevelFrac < 0.0) {
liquidLevelFrac = 0.0;
}
@@ -560,10 +564,11 @@ protected void updateEntrainmentFromPerformanceCalculator() {
}
performanceCalculator.calculate(gasDensity, oilDensity, waterDensity, gasViscosity,
- oilViscosity, waterViscosity, gasVelocity, internalDiameter, separatorLength, orientation,
- liquidLevelFrac);
+ oilViscosity, waterViscosity, gasVelocity, getInternalDiameter(), getSeparatorLength(),
+ orientation, liquidLevelFrac);
- // Update entrainment fractions — use "volume" spec type for physics-based results
+ // Update entrainment fractions — use "volume" spec type for physics-based
+ // results
if (performanceCalculator.getOilInGasFraction() > 0) {
oilInGas = performanceCalculator.getOilInGasFraction();
oilInGasSpec = "volume";
@@ -613,7 +618,8 @@ public void run(UUID id) {
thermoSystem2.initProperties();
}
- // If detailed entrainment model is enabled, compute entrainment from droplet physics
+ // If detailed entrainment model is enabled, compute entrainment from droplet
+ // physics
if (useDetailedEntrainmentCalculation && performanceCalculator != null
&& thermoSystem2.getNumberOfPhases() >= 2) {
updateEntrainmentFromPerformanceCalculator();
@@ -741,9 +747,9 @@ protected void updateEntrainmentForTransient() {
double gasVolFlow = gasOutStream.getFluid().getFlowRate("m3/sec");
double crossArea;
if (orientation.equals("horizontal")) {
- crossArea = sepCrossArea - liquidArea(liquidLevel);
+ crossArea = getSepCrossArea() - liquidArea(liquidLevel);
} else {
- crossArea = sepCrossArea;
+ crossArea = getSepCrossArea();
}
if (crossArea > 1e-10) {
gasVelocity = gasVolFlow / crossArea;
@@ -753,7 +759,7 @@ protected void updateEntrainmentForTransient() {
}
}
- double liquidLevelFrac = liquidLevel / internalDiameter;
+ double liquidLevelFrac = liquidLevel / getInternalDiameter();
liquidLevelFrac = Math.max(0.0, Math.min(1.0, liquidLevelFrac));
// Compute oil volume fraction from vessel inventory phase volumes.
@@ -775,8 +781,8 @@ protected void updateEntrainmentForTransient() {
}
performanceCalculator.calculate(gasDensity, oilDensity, waterDensity, gasViscosity,
- oilViscosity, waterViscosity, gasVelocity, internalDiameter, separatorLength, orientation,
- liquidLevelFrac);
+ oilViscosity, waterViscosity, gasVelocity, getInternalDiameter(), getSeparatorLength(),
+ orientation, liquidLevelFrac);
if (performanceCalculator.getOilInGasFraction() > 0) {
oilInGas = performanceCalculator.getOilInGasFraction();
@@ -1289,44 +1295,86 @@ public void setPressureDrop(double pressureDrop) {
}
/**
- *
- * Getter for the field internalDiameter.
- *
+ * Returns the vessel internal diameter [m]. The value is stored in the MechanicalDesign (single
+ * source of truth) and accessed here via delegation.
*
- * @return the diameter
+ * @return internal diameter in metres
*/
public double getInternalDiameter() {
- return internalDiameter;
+ return separatorMechanicalDesign != null ? separatorMechanicalDesign.getInnerDiameter() : 0.0;
}
/** {@inheritDoc} */
@Override
public void setInternalDiameter(double diameter) {
double levelFraction = getLiquidLevel();
- this.internalDiameter = diameter;
- this.internalRadius = diameter / 2;
- this.sepCrossArea = Math.PI * internalDiameter * internalDiameter / 4.0;
- this.separatorVolume = sepCrossArea * separatorLength;
+ if (separatorMechanicalDesign != null) {
+ separatorMechanicalDesign.setInnerDiameter(diameter);
+ }
this.liquidLevel = clampLiquidHeight(levelFraction * getMaxLiquidHeight());
updateHoldupVolumes();
}
+ /**
+ * Returns the internal radius [m], computed from the internal diameter.
+ *
+ * @return half of the internal diameter
+ */
+ private double getInternalRadius() {
+ return getInternalDiameter() / 2.0;
+ }
+
+ /**
+ * Returns the cross-sectional area of the separator [m2], computed from the internal diameter.
+ *
+ * @return pi/4 * D^2
+ */
+ private double getSepCrossArea() {
+ double d = getInternalDiameter();
+ return Math.PI * d * d / 4.0;
+ }
+
+ /**
+ * Returns the total separator volume [m3], computed from cross-sectional area and length.
+ *
+ * @return cross-sectional area * length
+ */
+ private double getSeparatorVolume() {
+ return getSepCrossArea() * getSeparatorLength();
+ }
+
+ /**
+ * Checks whether vessel geometry has been explicitly set on this separator. When false, methods
+ * that depend on geometry (capacity utilization, gas velocity, dynamic volumes) may return NaN or
+ * zero.
+ *
+ * @return true if internal diameter is greater than zero
+ */
+ public boolean hasGeometry() {
+ return getInternalDiameter() > 0.0;
+ }
+
/**
*
- * getGasSuperficialVelocity.
+ * getGasSuperficialVelocity. Uses design liquid level fraction to determine available gas area,
+ * independent of operating liquid level.
*
*
- * @return a double
+ * @return gas superficial velocity [m/s]
*/
public double getGasSuperficialVelocity() {
+ double gasArea;
if (orientation.equals("horizontal")) {
- return thermoSystem.getPhase(0).getFlowRate("m3/sec")
- / (sepCrossArea - liquidArea(liquidLevel));
+ gasArea = getSepCrossArea() * (1.0 - designLiquidLevelFraction);
} else if (orientation.equals("vertical")) {
- return thermoSystem.getPhase(0).getFlowRate("m3/sec") / sepCrossArea;
+ gasArea = getSepCrossArea();
} else {
return 0;
}
+ if (gasArea <= 0) {
+ return 0;
+ }
+ return thermoSystem.getPhase(0).getFlowRate("m3/sec") / gasArea;
}
/**
@@ -1362,7 +1410,7 @@ public double getGasLoadFactor() {
public double getGasLoadFactor(int phaseNumber) {
double gasAreaFraction = 1.0;
if (orientation.equals("horizontal")) {
- gasAreaFraction = 1.0 - (liquidVolume / separatorVolume);
+ gasAreaFraction = 1.0 - (liquidVolume / getSeparatorVolume());
}
thermoSystem.initPhysicalProperties();
double gasDensity = thermoSystem.getPhase(0).getPhysicalProperties().getDensity();
@@ -1516,11 +1564,11 @@ public double getMaxAllowableGasFlowRate() {
double maxVelocity = getMaxAllowableGasVelocity();
double gasArea;
if (orientation.equals("horizontal")) {
- // For horizontal, gas flows through upper section (above liquid level)
- gasArea = sepCrossArea - liquidArea(liquidLevel);
+ // For horizontal, gas flows through upper section above design liquid level
+ gasArea = getSepCrossArea() * (1.0 - designLiquidLevelFraction);
} else {
// For vertical separator
- gasArea = sepCrossArea * (1.0 - designLiquidLevelFraction);
+ gasArea = getSepCrossArea() * (1.0 - designLiquidLevelFraction);
}
return maxVelocity * gasArea;
}
@@ -1883,8 +1931,8 @@ public void autoSize(double safetyFactor) {
autoSized = true;
logger.info("Separator " + getName() + " auto-sized: diameter="
- + String.format("%.3f", internalDiameter) + " m, length="
- + String.format("%.3f", separatorLength) + " m");
+ + String.format("%.3f", getInternalDiameter()) + " m, length="
+ + String.format("%.3f", getSeparatorLength()) + " m");
}
/** {@inheritDoc} */
@@ -1938,8 +1986,9 @@ public String getSizingReport() {
sb.append("=== Separator Auto-Sizing Report ===\n");
sb.append("Equipment: ").append(getName()).append("\n");
sb.append("Auto-sized: ").append(autoSized).append("\n");
- sb.append("Internal Diameter: ").append(String.format("%.3f m", internalDiameter)).append("\n");
- sb.append("Length: ").append(String.format("%.3f m", separatorLength)).append("\n");
+ sb.append("Internal Diameter: ").append(String.format("%.3f m", getInternalDiameter()))
+ .append("\n");
+ sb.append("Length: ").append(String.format("%.3f m", getSeparatorLength())).append("\n");
sb.append("Design K-factor: ").append(String.format("%.4f m/s", designGasLoadFactor))
.append("\n");
sb.append("Orientation: ").append(orientation).append("\n");
@@ -1959,7 +2008,7 @@ public String getSizingReport() {
double gasVolumeFlow = thermoSystem.getPhase("gas").getFlowRate("m3/hr");
double maxVelocity = designGasLoadFactor * Math.sqrt((liqDensity - gasDensity) / gasDensity);
double actualVelocity = gasVolumeFlow / 3600.0
- / (Math.PI * Math.pow(internalDiameter / 2, 2) * (1.0 - designLiquidLevelFraction));
+ / (Math.PI * Math.pow(getInternalDiameter() / 2, 2) * (1.0 - designLiquidLevelFraction));
sb.append("\n--- Operating Conditions ---\n");
sb.append("Gas Volume Flow: ").append(String.format("%.1f m3/hr", gasVolumeFlow))
@@ -1986,8 +2035,8 @@ public String getSizingReportJson() {
Map report = new LinkedHashMap<>();
report.put("equipmentName", getName());
report.put("autoSized", autoSized);
- report.put("internalDiameter_m", internalDiameter);
- report.put("length_m", separatorLength);
+ report.put("internalDiameter_m", getInternalDiameter());
+ report.put("length_m", getSeparatorLength());
report.put("designKFactor_mps", designGasLoadFactor);
report.put("orientation", orientation);
@@ -2009,7 +2058,7 @@ public String getSizingReportJson() {
double gasVolumeFlow = thermoSystem.getPhase("gas").getFlowRate("m3/hr");
double maxVelocity = designGasLoadFactor * Math.sqrt((liqDensity - gasDensity) / gasDensity);
double actualVelocity = gasVolumeFlow / 3600.0
- / (Math.PI * Math.pow(internalDiameter / 2, 2) * (1.0 - designLiquidLevelFraction));
+ / (Math.PI * Math.pow(getInternalDiameter() / 2, 2) * (1.0 - designLiquidLevelFraction));
report.put("gasVolumeFlow_m3hr", gasVolumeFlow);
report.put("gasDensity_kgm3", gasDensity);
@@ -2079,36 +2128,36 @@ public double liquidArea(double level) {
if (level <= 0) {
return 0;
- } else if (level >= internalDiameter) {
- return sepCrossArea;
+ } else if (level >= getInternalDiameter()) {
+ return getSepCrossArea();
}
if (orientation.equals("horizontal")) {
- if (level < internalRadius) {
- double d = internalRadius - level;
- double theta = Math.acos(d / internalRadius);
- double a = internalRadius * Math.sin(theta);
+ if (level < getInternalRadius()) {
+ double d = getInternalRadius() - level;
+ double theta = Math.acos(d / getInternalRadius());
+ double a = getInternalRadius() * Math.sin(theta);
double triArea = a * d;
- double circArea = theta * Math.pow(internalRadius, 2);
+ double circArea = theta * Math.pow(getInternalRadius(), 2);
lArea = circArea - triArea;
// System.out.printf("Area func: radius %f d %f theta %f a %f area %f\n",
- // internalRadius, d,
+ // getInternalRadius(), d,
// theta, a, lArea);
- } else if (level > internalRadius) {
- double d = level - internalRadius;
- double theta = Math.acos(d / internalRadius);
- double a = internalRadius * Math.sin(theta);
+ } else if (level > getInternalRadius()) {
+ double d = level - getInternalRadius();
+ double theta = Math.acos(d / getInternalRadius());
+ double a = getInternalRadius() * Math.sin(theta);
double triArea = a * d;
- double circArea = (Math.PI - theta) * Math.pow(internalRadius, 2);
+ double circArea = (Math.PI - theta) * Math.pow(getInternalRadius(), 2);
lArea = circArea + triArea;
// System.out.printf("Area func: radius %f d %f theta %f a %f area %f\n",
- // internalRadius, d,
+ // getInternalRadius(), d,
// theta, a, lArea);
} else {
- lArea = 0.5 * Math.PI * Math.pow(internalRadius, 2);
+ lArea = 0.5 * Math.PI * Math.pow(getInternalRadius(), 2);
}
} else if (orientation.equals("vertical")) {
- lArea = sepCrossArea;
+ lArea = getSepCrossArea();
} else {
lArea = 0;
}
@@ -2127,11 +2176,11 @@ public double calcLiquidVolume() {
double lVolume = 0.0;
if (orientation.equals("horizontal")) {
- lVolume = liquidArea(liquidLevel) * separatorLength;
+ lVolume = liquidArea(liquidLevel) * getSeparatorLength();
// System.out.printf("from function: LVL %f Area %f\n", liquidLevel,
// liquidArea(liquidLevel));
} else if (orientation.equals("vertical")) {
- lVolume = sepCrossArea * liquidLevel;
+ lVolume = getSepCrossArea() * liquidLevel;
} else {
lVolume = 0;
}
@@ -2148,12 +2197,12 @@ private void updateHoldupVolumes() {
}
protected void enforceHeadspace() {
- double rawGasVolume = separatorVolume - liquidVolume;
+ double rawGasVolume = getSeparatorVolume() - liquidVolume;
double minGasVolume = getMinGasVolume();
if (rawGasVolume < minGasVolume) {
gasVolume = Math.max(minGasVolume, 0.0);
- if (separatorVolume > 0.0) {
- double adjustedLiquidVolume = Math.max(separatorVolume - gasVolume, 0.0);
+ if (getSeparatorVolume() > 0.0) {
+ double adjustedLiquidVolume = Math.max(getSeparatorVolume() - gasVolume, 0.0);
if (Math.abs(adjustedLiquidVolume - liquidVolume) > 1.0e-12) {
liquidLevel = levelFromVolume(adjustedLiquidVolume);
liquidVolume = calcLiquidVolume();
@@ -2168,18 +2217,19 @@ protected void enforceHeadspace() {
private double getMaxLiquidHeight() {
if ("vertical".equalsIgnoreCase(orientation)) {
- return separatorLength > 0.0 ? separatorLength : internalDiameter;
+ return getSeparatorLength() > 0.0 ? getSeparatorLength() : getInternalDiameter();
}
- return internalDiameter;
+ return getInternalDiameter();
}
private double getMinGasVolume() {
- if (separatorVolume <= 0.0) {
+ if (getSeparatorVolume() <= 0.0) {
return 0.0;
}
- double candidate = Math.max(separatorVolume * MIN_HEADSPACE_FRACTION, MIN_HEADSPACE_VOLUME);
- if (candidate >= separatorVolume) {
- return 0.5 * separatorVolume;
+ double candidate =
+ Math.max(getSeparatorVolume() * MIN_HEADSPACE_FRACTION, MIN_HEADSPACE_VOLUME);
+ if (candidate >= getSeparatorVolume()) {
+ return 0.5 * getSeparatorVolume();
}
return candidate;
}
@@ -2206,11 +2256,11 @@ private double clampLiquidHeight(double height) {
* @return inner surface area in square meters
*/
public double getInnerSurfaceArea() {
- if (internalRadius <= 0.0 || separatorLength <= 0.0) {
+ if (getInternalRadius() <= 0.0 || getSeparatorLength() <= 0.0) {
return 0.0;
}
- double shellArea = 2.0 * Math.PI * internalRadius * separatorLength;
- double headArea = 2.0 * sepCrossArea;
+ double shellArea = 2.0 * Math.PI * getInternalRadius() * getSeparatorLength();
+ double headArea = 2.0 * getSepCrossArea();
return shellArea + headArea;
}
@@ -2225,7 +2275,7 @@ public double getInnerSurfaceArea() {
* @return wetted area in square meters
*/
public double getWettedArea() {
- if (internalRadius <= 0.0 || separatorLength <= 0.0) {
+ if (getInternalRadius() <= 0.0 || getSeparatorLength() <= 0.0) {
return 0.0;
}
@@ -2235,11 +2285,11 @@ public double getWettedArea() {
return 0.0;
}
- double r = internalRadius;
+ double r = getInternalRadius();
double cappedLevel = Math.min(level, 2.0 * r);
double theta = 2.0 * Math.acos((r - cappedLevel) / r); // central angle of liquid segment
- double wettedShellArea = r * theta * separatorLength; // arc length * length
+ double wettedShellArea = r * theta * getSeparatorLength(); // arc length * length
double wettedHeadArea = 2.0 * liquidArea(cappedLevel);
return wettedShellArea + wettedHeadArea;
}
@@ -2250,10 +2300,10 @@ public double getWettedArea() {
return 0.0;
}
- double wettedShellArea = 2.0 * Math.PI * internalRadius * level;
- double wettedHeadArea = sepCrossArea; // bottom head is always wetted when level > 0
- if (level >= separatorLength) {
- wettedHeadArea += sepCrossArea; // top head becomes wetted when full
+ double wettedShellArea = 2.0 * Math.PI * getInternalRadius() * level;
+ double wettedHeadArea = getSepCrossArea(); // bottom head is always wetted when level > 0
+ if (level >= getSeparatorLength()) {
+ wettedHeadArea += getSepCrossArea(); // top head becomes wetted when full
}
return wettedShellArea + wettedHeadArea;
}
@@ -2316,22 +2366,22 @@ public double levelFromVolume(double volumeTarget) {
double headspace = getMinGasVolume();
double maxLiquidVolume =
- separatorVolume > 0.0 ? Math.max(separatorVolume - headspace, 0.0) : 0.0;
+ getSeparatorVolume() > 0.0 ? Math.max(getSeparatorVolume() - headspace, 0.0) : 0.0;
double limitedVolume = Math.max(0.0, Math.min(volumeTarget, maxLiquidVolume));
double a = 0.0;
- double b = internalDiameter;
+ double b = getInternalDiameter();
if (orientation.equalsIgnoreCase("horizontal")) {
- if (internalDiameter <= 0.0) {
+ if (getInternalDiameter() <= 0.0) {
return 0.0;
}
- if (separatorLength <= 0.0) {
+ if (getSeparatorLength() <= 0.0) {
return 0.0;
}
- double areaTarget = limitedVolume / separatorLength;
+ double areaTarget = limitedVolume / getSeparatorLength();
double fa = liquidArea(a) - areaTarget;
double fb = liquidArea(b) - areaTarget;
@@ -2369,39 +2419,38 @@ public double levelFromVolume(double volumeTarget) {
return 0.5 * (a + b);
} else if (orientation.equalsIgnoreCase("vertical")) {
- if (sepCrossArea <= 0.0) {
+ if (getSepCrossArea() <= 0.0) {
return 0.0;
}
- return clampLiquidHeight(limitedVolume / sepCrossArea);
+ return clampLiquidHeight(limitedVolume / getSepCrossArea());
} else {
return 0.0;
}
}
/**
- *
- * Getter for the field separatorLength.
- *
+ * Returns the separator tan-tan length [m]. The value is stored in the MechanicalDesign (single
+ * source of truth).
*
- * @return the separatorLength
+ * @return separator length in metres
*/
public double getSeparatorLength() {
- return separatorLength;
+ return separatorMechanicalDesign != null ? separatorMechanicalDesign.getTantanLength() : 0.0;
}
/**
- *
- * Setter for the field separatorLength.
- *
+ * Sets the separator tan-tan length [m]. The value is stored in the MechanicalDesign (single
+ * source of truth).
*
- * @param separatorLength the separatorLength to set
+ * @param length the separator length to set [m]
*/
- public void setSeparatorLength(double separatorLength) {
+ public void setSeparatorLength(double length) {
double levelFraction = getLiquidLevel();
- this.separatorLength = separatorLength;
- this.separatorVolume = sepCrossArea * separatorLength;
+ if (separatorMechanicalDesign != null) {
+ separatorMechanicalDesign.setTantanLength(length);
+ }
this.liquidLevel = clampLiquidHeight(levelFraction * getMaxLiquidHeight());
updateHoldupVolumes();
}
@@ -2414,7 +2463,7 @@ public void setSeparatorLength(double separatorLength) {
* @param height weir height in meters (must be positive, less than internal diameter)
*/
public void setWeirHeight(double height) {
- this.weirHeight = Math.max(0.0, Math.min(height, internalDiameter));
+ this.weirHeight = Math.max(0.0, Math.min(height, getInternalDiameter()));
}
/**
@@ -2533,8 +2582,8 @@ public double getMistEliminatorPressureDrop() {
return 0.0;
}
double rhoGas = thermoSystem.getPhase("gas").getDensity("kg/m3");
- double gasVol = gasVolume > 0 ? gasVolume : separatorVolume * 0.5;
- double crossArea = sepCrossArea;
+ double gasVol = gasVolume > 0 ? gasVolume : getSeparatorVolume() * 0.5;
+ double crossArea = getSepCrossArea();
if (crossArea <= 0 || gasVol <= 0) {
return 0.0;
}
@@ -2731,10 +2780,10 @@ public int hashCode() {
int result = super.hashCode();
result = prime * result + Objects.hash(designLiquidLevelFraction, efficiency,
gasCarryunderFraction, gasInLiquid, gasInLiquidSpec, gasOutStream, gasSystem, gasVolume,
- inletStreamMixer, internalDiameter, liquidCarryoverFraction, liquidLevel, liquidOutStream,
- liquidSystem, liquidVolume, numberOfInputStreams, oilInGas, oilInGasSpec, orientation,
- pressureDrop, separatorLength, separatorSection, specifiedStream, thermoSystem,
- thermoSystem2, thermoSystemCloned, waterInGas, waterInGasSpec, waterSystem);
+ inletStreamMixer, getInternalDiameter(), liquidCarryoverFraction, liquidLevel,
+ liquidOutStream, liquidSystem, liquidVolume, numberOfInputStreams, oilInGas, oilInGasSpec,
+ orientation, pressureDrop, getSeparatorLength(), separatorSection, specifiedStream,
+ thermoSystem, thermoSystem2, thermoSystemCloned, waterInGas, waterInGasSpec, waterSystem);
return result;
}
@@ -2762,8 +2811,8 @@ public boolean equals(Object obj) {
&& Objects.equals(gasSystem, other.gasSystem)
&& Double.doubleToLongBits(gasVolume) == Double.doubleToLongBits(other.gasVolume)
&& Objects.equals(inletStreamMixer, other.inletStreamMixer)
- && Double.doubleToLongBits(internalDiameter) == Double
- .doubleToLongBits(other.internalDiameter)
+ && Double.doubleToLongBits(getInternalDiameter()) == Double
+ .doubleToLongBits(other.getInternalDiameter())
&& Double.doubleToLongBits(liquidCarryoverFraction) == Double
.doubleToLongBits(other.liquidCarryoverFraction)
&& Double.doubleToLongBits(liquidLevel) == Double.doubleToLongBits(other.liquidLevel)
@@ -2775,8 +2824,8 @@ public boolean equals(Object obj) {
&& Objects.equals(oilInGasSpec, other.oilInGasSpec)
&& Objects.equals(orientation, other.orientation)
&& Double.doubleToLongBits(pressureDrop) == Double.doubleToLongBits(other.pressureDrop)
- && Double.doubleToLongBits(separatorLength) == Double
- .doubleToLongBits(other.separatorLength)
+ && Double.doubleToLongBits(getSeparatorLength()) == Double
+ .doubleToLongBits(other.getSeparatorLength())
&& Objects.equals(separatorSection, other.separatorSection)
&& Objects.equals(specifiedStream, other.specifiedStream)
&& Objects.equals(thermoSystem, other.thermoSystem)
@@ -2992,8 +3041,8 @@ public double getCapacityMax() {
return mechMax;
}
// Fall back to gas load factor based capacity if mechanical design not set
- if (designGasLoadFactor > 0 && internalDiameter > 0) {
- double area = Math.PI * Math.pow(internalDiameter / 2.0, 2);
+ if (designGasLoadFactor > 0 && getInternalDiameter() > 0) {
+ double area = Math.PI * Math.pow(getInternalDiameter() / 2.0, 2);
return designGasLoadFactor * area * 3600.0; // Convert m/s * m² to m³/hr
}
return 0.0;
@@ -3100,22 +3149,25 @@ public neqsim.util.validation.ValidationResult validateSetup() {
}
// Check: Separator dimensions are positive
- if (separatorLength <= 0) {
- result.addError("dimensions", "Separator length must be positive: " + separatorLength + " m",
+ if (getSeparatorLength() <= 0) {
+ result.addError("dimensions",
+ "Separator length must be positive: " + getSeparatorLength() + " m",
"Set positive length: separator.setSeparatorLength(5.0)");
}
- if (internalDiameter <= 0) {
+ if (getInternalDiameter() <= 0) {
result.addError("dimensions",
- "Separator diameter must be positive: " + internalDiameter + " m",
+ "Separator diameter must be positive: " + getInternalDiameter() + " m",
"Set positive diameter: separator.setInternalDiameter(1.0)");
}
// Check: Liquid level is within valid range (0-1)
- if (liquidLevel < 0 || liquidLevel > internalDiameter) {
- result.addWarning("level", "Liquid level may be outside valid range: " + liquidLevel
- + " m (diameter: " + internalDiameter + " m)",
- "Set liquid level between 0 and separator diameter");
+ if (liquidLevel < 0 || liquidLevel > getInternalDiameter()) {
+ result
+ .addWarning("level",
+ "Liquid level may be outside valid range: " + liquidLevel + " m (diameter: "
+ + getInternalDiameter() + " m)",
+ "Set liquid level between 0 and separator diameter");
}
// Check: Pressure drop is non-negative
@@ -3607,17 +3659,17 @@ public java.util.List getEnabledConstraintNames() {
public double calcGasAreaAboveLevel(double liquidLevelHeight) {
if (!orientation.equalsIgnoreCase("horizontal")) {
// For vertical separator, gas area is above the liquid
- return sepCrossArea;
+ return getSepCrossArea();
}
- double h = Math.min(liquidLevelHeight, internalDiameter);
+ double h = Math.min(liquidLevelHeight, getInternalDiameter());
if (h <= 0) {
- return sepCrossArea; // Full cross-section is gas
+ return getSepCrossArea(); // Full cross-section is gas
}
- if (h >= internalDiameter) {
+ if (h >= getInternalDiameter()) {
return 0.0; // No gas area
}
// Gas area = total area - liquid area
- return sepCrossArea - liquidArea(h);
+ return getSepCrossArea() - liquidArea(h);
}
/**
@@ -3678,7 +3730,7 @@ public double calcKValueAtHLL() {
double hll = getMechanicalDesign().getHLL();
if (hll <= 0) {
// Default to 70% of internal diameter if not set
- hll = internalDiameter * 0.70;
+ hll = getInternalDiameter() * 0.70;
}
return calcKValue(hll);
}
@@ -3727,7 +3779,7 @@ public double calcDropletCutSize(double effectiveGasLength, double freeHeightAbo
double temperature = thermoSystem.getTemperature() - 273.15; // Celsius
// Gas velocity above liquid
- double gasVelocity = calcGasVelocityAboveLevel(internalDiameter - freeHeightAboveLiquid);
+ double gasVelocity = calcGasVelocityAboveLevel(getInternalDiameter() - freeHeightAboveLiquid);
if (gasVelocity <= 0 || effectiveGasLength <= 0) {
return 0.0;
}
@@ -3755,12 +3807,12 @@ public double calcDropletCutSize(double effectiveGasLength, double freeHeightAbo
public double calcDropletCutSizeAtHLL() {
double hll = getMechanicalDesign().getHLL();
if (hll <= 0) {
- hll = internalDiameter * 0.70;
+ hll = getInternalDiameter() * 0.70;
}
- double freeHeight = internalDiameter - hll;
+ double freeHeight = getInternalDiameter() - hll;
double effGasLength = getMechanicalDesign().getEffectiveLengthGas();
if (effGasLength <= 0) {
- effGasLength = separatorLength * 0.64; // Default 64% of length
+ effGasLength = getSeparatorLength() * 0.64; // Default 64% of length
}
return calcDropletCutSize(effGasLength, freeHeight);
}
@@ -3846,7 +3898,7 @@ public double calcInletMomentumFlux() {
double nozzleID = getMechanicalDesign().getInletNozzleID();
if (nozzleID <= 0) {
// Estimate nozzle size if not set
- nozzleID = internalDiameter * 0.15; // Rough estimate: 15% of vessel ID
+ nozzleID = getInternalDiameter() * 0.15; // Rough estimate: 15% of vessel ID
}
return calcInletMomentumFlux(nozzleID);
}
@@ -3912,18 +3964,18 @@ public double calcOilRetentionTime() {
double effLiquidLength = getMechanicalDesign().getEffectiveLengthLiquid();
if (nll <= 0) {
- nll = internalDiameter * 0.50;
+ nll = getInternalDiameter() * 0.50;
}
if (nil <= 0) {
- nil = internalDiameter * 0.20;
+ nil = getInternalDiameter() * 0.20;
}
if (effLiquidLength <= 0) {
- effLiquidLength = separatorLength * 0.82;
+ effLiquidLength = getSeparatorLength() * 0.82;
}
// Oil volume between NIL and NLL
double oilArea =
- calcSegmentArea(internalDiameter, nll) - calcSegmentArea(internalDiameter, nil);
+ calcSegmentArea(getInternalDiameter(), nll) - calcSegmentArea(getInternalDiameter(), nil);
double oilVolume = oilArea * effLiquidLength; // m³
// Oil flow rate
@@ -3952,14 +4004,14 @@ public double calcWaterRetentionTime() {
double effLiquidLength = getMechanicalDesign().getEffectiveLengthLiquid();
if (nil <= 0) {
- nil = internalDiameter * 0.20;
+ nil = getInternalDiameter() * 0.20;
}
if (effLiquidLength <= 0) {
- effLiquidLength = separatorLength * 0.82;
+ effLiquidLength = getSeparatorLength() * 0.82;
}
// Water volume below NIL
- double waterArea = calcSegmentArea(internalDiameter, nil);
+ double waterArea = calcSegmentArea(getInternalDiameter(), nil);
double waterVolume = waterArea * effLiquidLength; // m³
// Water flow rate
diff --git a/src/main/java/neqsim/process/mechanicaldesign/separator/GasScrubberMechanicalDesign.java b/src/main/java/neqsim/process/mechanicaldesign/separator/GasScrubberMechanicalDesign.java
index 61d2dc5cfb..79131e2f08 100644
--- a/src/main/java/neqsim/process/mechanicaldesign/separator/GasScrubberMechanicalDesign.java
+++ b/src/main/java/neqsim/process/mechanicaldesign/separator/GasScrubberMechanicalDesign.java
@@ -1,12 +1,18 @@
package neqsim.process.mechanicaldesign.separator;
+import java.util.LinkedHashMap;
+import java.util.List;
+import java.util.Map;
import org.apache.logging.log4j.LogManager;
import org.apache.logging.log4j.Logger;
import neqsim.process.equipment.ProcessEquipmentInterface;
import neqsim.process.equipment.separator.Separator;
import neqsim.process.equipment.separator.SeparatorInterface;
+import neqsim.process.equipment.separator.entrainment.InletDeviceModel;
import neqsim.process.equipment.separator.sectiontype.SeparatorSection;
import neqsim.process.mechanicaldesign.designstandards.GasScrubberDesignStandard;
+import neqsim.process.mechanicaldesign.separator.conformity.ConformityReport;
+import neqsim.process.mechanicaldesign.separator.conformity.ConformityRuleSet;
import neqsim.process.mechanicaldesign.separator.sectiontype.SepDesignSection;
/**
@@ -23,6 +29,76 @@ public class GasScrubberMechanicalDesign extends SeparatorMechanicalDesign {
/** Logger object for class. */
static Logger logger = LogManager.getLogger(GasScrubberMechanicalDesign.class);
+ // ============================================================================
+ // Inlet cyclone configuration
+ // ============================================================================
+ /** Whether inlet cyclones are installed. */
+ private boolean hasInletCyclones = false;
+ /** Number of inlet cyclones. */
+ private int numberOfInletCyclones = 0;
+ /** Inlet cyclone inner diameter [m]. */
+ private double inletCycloneDiameterM = 0.0;
+
+ // ============================================================================
+ // Demisting cyclone configuration
+ // ============================================================================
+ /** Whether demisting cyclones are installed. */
+ private boolean hasDemistingCyclones = false;
+ /** Number of demisting cyclones. */
+ private int numberOfDemistingCyclones = 0;
+ /** Demisting cyclone inner diameter [m]. */
+ private double demistingCycloneDiameterM = 0.0;
+ /** Cyclone deck elevation from bottom of vessel [m]. */
+ private double cycloneDeckElevationM = 0.0;
+ /** Cyclone tube length [m]. */
+ private double cycloneLengthM = 0.0;
+ /** Cyclone Euler number (total dp vs rho*v^2). */
+ private double cycloneEulerNumber = 4.5;
+ /** Fraction of cyclone dp to drain chamber [%]. */
+ private double cycloneDpToDrainPct = 60.0;
+
+ // ============================================================================
+ // Mesh pad configuration
+ // ============================================================================
+ /** Whether mesh pad is installed (above inlet, below cyclones). */
+ private boolean hasMeshPad = false;
+ /** Mesh pad area [m2]. */
+ private double meshPadAreaM2 = 0.0;
+ /** Mesh pad thickness [mm]. */
+ private double meshPadThicknessMm = 100.0;
+
+ // ============================================================================
+ // Vane pack configuration
+ // ============================================================================
+ /** Whether vane pack is installed. */
+ private boolean hasVanePack = false;
+ /** Vane pack area [m2]. */
+ private double vanePackAreaM2 = 0.0;
+
+ // ============================================================================
+ // Drain pipe
+ // ============================================================================
+ /** Drain pipe inner diameter [m]. */
+ private double drainPipeDiameterM = 0.0;
+
+ // ============================================================================
+ // Liquid level elevations from BTL [m] — optional for general use,
+ // but LA(H) is required when cyclones are present (drainage height check)
+ // ============================================================================
+ /** LA(LL) — Low-Low level alarm elevation from BTL [m]. */
+ private double laLLElevationM = 0.0;
+ /** LA(L) — Low level alarm elevation from BTL [m]. */
+ private double laLElevationM = 0.0;
+ /**
+ * LA(H) — High level alarm elevation from BTL [m]. Required for cyclone drainage calc.
+ */
+ private double laHElevationM = 0.0;
+ /** LA(HH) — High-High level alarm elevation from BTL [m]. */
+ private double laHHElevationM = 0.0;
+
+ /** Active conformity rule set, null if none set. */
+ private transient ConformityRuleSet conformityRuleSet = null;
+
/**
*
* Constructor for GasScrubberMechanicalDesign.
@@ -154,6 +230,658 @@ public void calcDesign() {
public void setDesign() {
((SeparatorInterface) getProcessEquipment()).setInternalDiameter(innerDiameter);
((Separator) getProcessEquipment()).setSeparatorLength(tantanLength);
- // this method will be implemented to set calculated design...
+ }
+
+ // ============================================================================
+ // Conformity checking
+ // ============================================================================
+
+ /**
+ * Sets the conformity standard to use for checking.
+ *
+ *
+ * This also enables the corresponding capacity constraints on the scrubber, so that the optimizer
+ * and capacity reporting use the same criteria.
+ *
+ *
+ * @param standardName the standard identifier: "TR3500", "API-12J", "Shell-DEP", "NORSOK-P002"
+ */
+ public void setConformityRules(String standardName) {
+ this.conformityRuleSet = ConformityRuleSet.create(standardName);
+ // Enable matching capacity constraints on the separator
+ Separator sep = (Separator) getProcessEquipment();
+ List constraintNames = conformityRuleSet.getConstraintNames(this);
+ sep.enableConstraints(constraintNames.toArray(new String[0]));
+ }
+
+ /**
+ * Runs all applicable conformity checks using the current operating state.
+ *
+ *
+ * The scrubber must have been run (process simulation) before calling this method, so that the
+ * fluid state reflects current operating conditions.
+ *
+ *
+ * @return a conformity report with all check results
+ * @throws IllegalStateException if no conformity rules have been set
+ */
+ public ConformityReport checkConformity() {
+ if (conformityRuleSet == null) {
+ throw new IllegalStateException(
+ "No conformity rules set. Call setConformityRules(\"TR3500\") first.");
+ }
+ return conformityRuleSet.evaluate(this);
+ }
+
+ /**
+ * Gets the active conformity rule set name, or null if none is set.
+ *
+ * @return the standard name, or null
+ */
+ public String getConformityStandard() {
+ return conformityRuleSet != null ? conformityRuleSet.getName() : null;
+ }
+
+ /**
+ * Sets the inlet device type by name string.
+ *
+ *
+ * Accepted names (case-insensitive): "schoepentoeter", "inlet_vane", "inlet_cyclone",
+ * "deflector_plate", "half_pipe", "impingement_plate", "none".
+ *
+ *
+ * @param deviceTypeName the inlet device type name
+ * @throws IllegalArgumentException if the name does not match any known device type
+ */
+ public void setInletDevice(String deviceTypeName) {
+ InletDeviceModel.InletDeviceType matched = null;
+ for (InletDeviceModel.InletDeviceType t : InletDeviceModel.InletDeviceType.values()) {
+ if (t.name().equalsIgnoreCase(deviceTypeName)
+ || t.getDisplayName().equalsIgnoreCase(deviceTypeName)) {
+ matched = t;
+ break;
+ }
+ }
+ if (matched == null) {
+ throw new IllegalArgumentException("Unknown inlet device type: " + deviceTypeName
+ + ". Use one of: schoepentoeter, inlet_vane, inlet_cyclone, "
+ + "deflector_plate, half_pipe, impingement_plate, none");
+ }
+ setInletDeviceType(matched);
+ }
+
+ // ============================================================================
+ // Inlet cyclone getters/setters
+ // ============================================================================
+
+ /**
+ * Configures the inlet cyclones.
+ *
+ * @param numberOfCyclones number of inlet cyclones
+ * @param cycloneDiameterM inlet cyclone inner diameter [m]
+ */
+ public void setInletCyclones(int numberOfCyclones, double cycloneDiameterM) {
+ this.hasInletCyclones = true;
+ this.numberOfInletCyclones = numberOfCyclones;
+ this.inletCycloneDiameterM = cycloneDiameterM;
+ }
+
+ /**
+ * Whether inlet cyclones are installed.
+ *
+ * @return true if inlet cyclones are configured
+ */
+ public boolean hasInletCyclones() {
+ return hasInletCyclones;
+ }
+
+ /**
+ * Gets the number of inlet cyclones.
+ *
+ * @return number of inlet cyclones
+ */
+ public int getNumberOfInletCyclones() {
+ return numberOfInletCyclones;
+ }
+
+ /**
+ * Gets the inlet cyclone inner diameter.
+ *
+ * @return cyclone diameter [m]
+ */
+ public double getInletCycloneDiameterM() {
+ return inletCycloneDiameterM;
+ }
+
+ // ============================================================================
+ // Demisting cyclone getters/setters
+ // ============================================================================
+
+ /**
+ * Configures the demisting cyclones.
+ *
+ * @param numberOfCyclones number of demisting cyclones
+ * @param cycloneDiameterM demisting cyclone inner diameter [m]
+ * @param deckElevationM cyclone deck elevation from bottom of vessel [m]
+ */
+ public void setDemistingCyclones(int numberOfCyclones, double cycloneDiameterM,
+ double deckElevationM) {
+ this.hasDemistingCyclones = true;
+ this.numberOfDemistingCyclones = numberOfCyclones;
+ this.demistingCycloneDiameterM = cycloneDiameterM;
+ this.cycloneDeckElevationM = deckElevationM;
+ }
+
+ /**
+ * Configures the demisting cyclones with tube length.
+ *
+ * @param numberOfCyclones number of demisting cyclones
+ * @param cycloneDiameterM demisting cyclone inner diameter [m]
+ * @param deckElevationM cyclone deck elevation from bottom of vessel [m]
+ * @param cycloneLengthM cyclone tube length [m]
+ */
+ public void setDemistingCyclones(int numberOfCyclones, double cycloneDiameterM,
+ double deckElevationM, double cycloneLengthM) {
+ setDemistingCyclones(numberOfCyclones, cycloneDiameterM, deckElevationM);
+ this.cycloneLengthM = cycloneLengthM;
+ }
+
+ /**
+ * Whether demisting cyclones are installed.
+ *
+ * @return true if demisting cyclones are configured
+ */
+ public boolean hasDemistingCyclones() {
+ return hasDemistingCyclones;
+ }
+
+ /**
+ * Gets the number of demisting cyclones.
+ *
+ * @return number of demisting cyclones
+ */
+ public int getNumberOfDemistingCyclones() {
+ return numberOfDemistingCyclones;
+ }
+
+ /**
+ * Gets the demisting cyclone inner diameter.
+ *
+ * @return cyclone diameter [m]
+ */
+ public double getDemistingCycloneDiameterM() {
+ return demistingCycloneDiameterM;
+ }
+
+ /**
+ * Gets the cyclone deck elevation.
+ *
+ * @return deck elevation from bottom of vessel [m]
+ */
+ public double getCycloneDeckElevationM() {
+ return cycloneDeckElevationM;
+ }
+
+ /**
+ * Sets the cyclone deck elevation.
+ *
+ * @param elevationM deck elevation from bottom of vessel [m]
+ */
+ public void setCycloneDeckElevationM(double elevationM) {
+ this.cycloneDeckElevationM = elevationM;
+ }
+
+ /**
+ * Gets the cyclone tube length.
+ *
+ * @return cyclone tube length [m]
+ */
+ public double getCycloneLengthM() {
+ return cycloneLengthM;
+ }
+
+ /**
+ * Sets the cyclone tube length.
+ *
+ * @param lengthM cyclone tube length [m]
+ */
+ public void setCycloneLengthM(double lengthM) {
+ this.cycloneLengthM = lengthM;
+ }
+
+ /**
+ * Gets the cyclone Euler number for total pressure drop.
+ *
+ * @return Euler number (dp vs rho*v^2, not 0.5*rho*v^2)
+ */
+ public double getCycloneEulerNumber() {
+ return cycloneEulerNumber;
+ }
+
+ /**
+ * Sets the cyclone Euler number.
+ *
+ * @param eulerNumber Euler number for total dp
+ */
+ public void setCycloneEulerNumber(double eulerNumber) {
+ this.cycloneEulerNumber = eulerNumber;
+ }
+
+ /**
+ * Gets the fraction of cyclone dp to drain chamber.
+ *
+ * @return fraction [%]
+ */
+ public double getCycloneDpToDrainPct() {
+ return cycloneDpToDrainPct;
+ }
+
+ /**
+ * Sets the fraction of cyclone dp to drain chamber.
+ *
+ * @param pct fraction [%]
+ */
+ public void setCycloneDpToDrainPct(double pct) {
+ this.cycloneDpToDrainPct = pct;
+ }
+
+ // ============================================================================
+ // Mesh pad getters/setters
+ // ============================================================================
+
+ /**
+ * Configures the mesh pad.
+ *
+ * @param areaM2 mesh pad area [m2]
+ * @param thicknessMm mesh pad thickness [mm]
+ */
+ public void setMeshPad(double areaM2, double thicknessMm) {
+ this.hasMeshPad = true;
+ this.meshPadAreaM2 = areaM2;
+ this.meshPadThicknessMm = thicknessMm;
+ }
+
+ /**
+ * Whether mesh pad is installed.
+ *
+ * @return true if mesh pad is configured
+ */
+ public boolean hasMeshPad() {
+ return hasMeshPad;
+ }
+
+ /**
+ * Gets the mesh pad area.
+ *
+ * @return mesh pad area [m2]
+ */
+ public double getMeshPadAreaM2() {
+ return meshPadAreaM2;
+ }
+
+ /**
+ * Gets the mesh pad thickness.
+ *
+ * @return mesh pad thickness [mm]
+ */
+ public double getMeshPadThicknessMm() {
+ return meshPadThicknessMm;
+ }
+
+ // ============================================================================
+ // Vane pack getters/setters
+ // ============================================================================
+
+ /**
+ * Configures the vane pack.
+ *
+ * @param areaM2 vane pack area [m2]
+ */
+ public void setVanePack(double areaM2) {
+ this.hasVanePack = true;
+ this.vanePackAreaM2 = areaM2;
+ }
+
+ /**
+ * Whether vane pack is installed.
+ *
+ * @return true if vane pack is configured
+ */
+ public boolean hasVanePack() {
+ return hasVanePack;
+ }
+
+ /**
+ * Gets the vane pack area.
+ *
+ * @return vane pack area [m2]
+ */
+ public double getVanePackAreaM2() {
+ return vanePackAreaM2;
+ }
+
+ // ============================================================================
+ // Drain pipe getters/setters
+ // ============================================================================
+
+ /**
+ * Sets the drain pipe inner diameter.
+ *
+ * @param diameterM drain pipe ID [m]
+ */
+ public void setDrainPipeDiameterM(double diameterM) {
+ this.drainPipeDiameterM = diameterM;
+ }
+
+ /**
+ * Gets the drain pipe inner diameter.
+ *
+ * @return drain pipe ID [m]
+ */
+ public double getDrainPipeDiameterM() {
+ return drainPipeDiameterM;
+ }
+
+ // ============================================================================
+ // Liquid level alarm elevation getters/setters
+ // ============================================================================
+
+ /**
+ * Sets the LA(LL) — Low-Low level alarm elevation from BTL.
+ *
+ * @param elevationM LA(LL) elevation [m]
+ */
+ public void setLaLLElevationM(double elevationM) {
+ this.laLLElevationM = elevationM;
+ }
+
+ /**
+ * Gets the LA(LL) elevation from BTL.
+ *
+ * @return LA(LL) elevation [m]
+ */
+ public double getLaLLElevationM() {
+ return laLLElevationM;
+ }
+
+ /**
+ * Sets the LA(L) — Low level alarm elevation from BTL.
+ *
+ * @param elevationM LA(L) elevation [m]
+ */
+ public void setLaLElevationM(double elevationM) {
+ this.laLElevationM = elevationM;
+ }
+
+ /**
+ * Gets the LA(L) elevation from BTL.
+ *
+ * @return LA(L) elevation [m]
+ */
+ public double getLaLElevationM() {
+ return laLElevationM;
+ }
+
+ /**
+ * Sets the LA(H) — High level alarm elevation from BTL. Required when demisting cyclones are
+ * present for drainage height conformity check.
+ *
+ * @param elevationM LA(H) elevation [m]
+ */
+ public void setLaHElevationM(double elevationM) {
+ this.laHElevationM = elevationM;
+ }
+
+ /**
+ * Gets the LA(H) elevation from BTL.
+ *
+ * @return LA(H) elevation [m]
+ */
+ public double getLaHElevationM() {
+ return laHElevationM;
+ }
+
+ /**
+ * Sets the LA(HH) — High-High level alarm elevation from BTL.
+ *
+ * @param elevationM LA(HH) elevation [m]
+ */
+ public void setLaHHElevationM(double elevationM) {
+ this.laHHElevationM = elevationM;
+ }
+
+ /**
+ * Gets the LA(HH) elevation from BTL.
+ *
+ * @return LA(HH) elevation [m]
+ */
+ public double getLaHHElevationM() {
+ return laHHElevationM;
+ }
+
+ /**
+ * Sets the HHLL elevation from bottom of vessel. Kept for backward compatibility; prefer
+ * {@link #setLaHElevationM(double)} for drainage calculations.
+ *
+ * @param elevationM HHLL elevation [m]
+ * @deprecated use {@link #setLaHHElevationM(double)} instead
+ */
+ @Deprecated
+ public void setHhllElevationM(double elevationM) {
+ this.laHHElevationM = elevationM;
+ }
+
+ /**
+ * Gets the HHLL elevation from bottom of vessel. Kept for backward compatibility.
+ *
+ * @return HHLL elevation [m]
+ * @deprecated use {@link #getLaHHElevationM()} instead
+ */
+ @Deprecated
+ public double getHhllElevationM() {
+ return laHHElevationM;
+ }
+
+ // ============================================================================
+ // Reporting
+ // ============================================================================
+
+ /**
+ * {@inheritDoc} Overrides to populate scrubber-specific parameters (internals, elevations) into
+ * the JSON response.
+ */
+ @Override
+ public SeparatorMechanicalDesignResponse getResponse() {
+ SeparatorMechanicalDesignResponse resp = super.getResponse();
+ resp.addSpecificParameter("equipmentSubType", "GasScrubber");
+
+ // Vessel geometry (stored in MechanicalDesign; Separator delegates to us)
+ double vesselID = innerDiameter;
+ double vesselLen = tantanLength;
+ resp.addSpecificParameter("vesselInnerDiameter_mm", vesselID * 1000.0);
+ resp.addSpecificParameter("vesselTanTan_mm", vesselLen * 1000.0);
+ resp.addSpecificParameter("inletNozzleID_mm", getInletNozzleID() * 1000.0);
+
+ // Inlet device
+ if (hasInletCyclones) {
+ Map inletCyc = new LinkedHashMap();
+ inletCyc.put("type", "Inlet Cyclones");
+ inletCyc.put("count", numberOfInletCyclones);
+ inletCyc.put("diameter_mm", inletCycloneDiameterM * 1000.0);
+ resp.addSpecificParameter("inletDevice", inletCyc);
+ }
+
+ // Mesh pad
+ if (hasMeshPad) {
+ Map mesh = new LinkedHashMap();
+ mesh.put("area_m2", meshPadAreaM2);
+ mesh.put("thickness_mm", meshPadThicknessMm);
+ resp.addSpecificParameter("meshPad", mesh);
+ }
+
+ // Vane pack
+ if (hasVanePack) {
+ Map vane = new LinkedHashMap();
+ vane.put("area_m2", vanePackAreaM2);
+ resp.addSpecificParameter("vanePack", vane);
+ }
+
+ // Demisting cyclone deck
+ if (hasDemistingCyclones) {
+ Map cyc = new LinkedHashMap();
+ cyc.put("count", numberOfDemistingCyclones);
+ cyc.put("diameter_mm", demistingCycloneDiameterM * 1000.0);
+ cyc.put("deckElevation_mm", cycloneDeckElevationM * 1000.0);
+ cyc.put("eulerNumber", cycloneEulerNumber);
+ cyc.put("dpToDrain_pct", cycloneDpToDrainPct);
+ resp.addSpecificParameter("demistingCyclones", cyc);
+ }
+
+ // Drain pipe
+ if (drainPipeDiameterM > 0) {
+ resp.addSpecificParameter("drainPipeDiameter_mm", drainPipeDiameterM * 1000.0);
+ }
+
+ // Liquid levels
+ Map levels = new LinkedHashMap();
+ if (laLLElevationM > 0) {
+ levels.put("LA_LL_mm", laLLElevationM * 1000.0);
+ }
+ if (laLElevationM > 0) {
+ levels.put("LA_L_mm", laLElevationM * 1000.0);
+ }
+ if (laHElevationM > 0) {
+ levels.put("LA_H_mm", laHElevationM * 1000.0);
+ }
+ if (laHHElevationM > 0) {
+ levels.put("LA_HH_mm", laHHElevationM * 1000.0);
+ }
+ if (!levels.isEmpty()) {
+ resp.addSpecificParameter("liquidLevels", levels);
+ }
+
+ // Drainage height
+ if (hasDemistingCyclones && laHHElevationM > 0) {
+ double drainageHeight = cycloneDeckElevationM - laHHElevationM;
+ resp.addSpecificParameter("drainageHeightAvailable_mm", drainageHeight * 1000.0);
+ }
+
+ return resp;
+ }
+
+ /**
+ * Generates a formatted text report of the scrubber mechanical design configuration. Shows vessel
+ * geometry, internals, elevations, and liquid levels in a readable table format.
+ *
+ * @return formatted text report string
+ */
+ public String toTextReport() {
+ Separator sep = (Separator) getProcessEquipment();
+ // Geometry is stored in MechanicalDesign; Separator delegates to us
+ double vesselID = innerDiameter;
+ double vesselLen = tantanLength;
+
+ StringBuilder sb = new StringBuilder();
+ String line = "======================================================================";
+ String sep2 = "----------------------------------------------------------------------";
+
+ sb.append(line).append('\n');
+ sb.append(" SCRUBBER MECHANICAL DESIGN: ").append(sep.getName()).append('\n');
+ sb.append(line).append('\n');
+
+ // Vessel geometry
+ sb.append('\n');
+ sb.append(" VESSEL GEOMETRY\n");
+ sb.append(sep2).append('\n');
+ appendRow(sb, "Internal Diameter", String.format("%.0f mm", vesselID * 1000.0));
+ appendRow(sb, "Tan-Tan Length", String.format("%.0f mm", vesselLen * 1000.0));
+ appendRow(sb, "Orientation", sep.getOrientation());
+ appendRow(sb, "Inlet Nozzle ID", String.format("%.1f mm", getInletNozzleID() * 1000.0));
+ if (getGasOutletNozzleID() > 0) {
+ appendRow(sb, "Gas Outlet Nozzle ID",
+ String.format("%.1f mm", getGasOutletNozzleID() * 1000.0));
+ }
+
+ // Internals
+ sb.append('\n');
+ sb.append(" INTERNALS\n");
+ sb.append(sep2).append('\n');
+ if (hasInletCyclones) {
+ appendRow(sb, "Inlet Device", "Inlet Cyclones");
+ appendRow(sb, " Count", String.valueOf(numberOfInletCyclones));
+ appendRow(sb, " Cyclone Diameter", String.format("%.0f mm", inletCycloneDiameterM * 1000.0));
+ } else {
+ appendRow(sb, "Inlet Device", "Schoepentoeter / Inlet Vane (via Separator)");
+ }
+ if (hasMeshPad) {
+ appendRow(sb, "Mesh Pad", "Installed");
+ appendRow(sb, " Area", String.format("%.3f m2", meshPadAreaM2));
+ appendRow(sb, " Thickness", String.format("%.0f mm", meshPadThicknessMm));
+ }
+ if (hasVanePack) {
+ appendRow(sb, "Vane Pack", "Installed");
+ appendRow(sb, " Area", String.format("%.3f m2", vanePackAreaM2));
+ }
+ if (hasDemistingCyclones) {
+ appendRow(sb, "Demisting Cyclones", "Installed");
+ appendRow(sb, " Count", String.valueOf(numberOfDemistingCyclones));
+ appendRow(sb, " Cyclone Diameter",
+ String.format("%.0f mm", demistingCycloneDiameterM * 1000.0));
+ appendRow(sb, " Deck Elevation (BTL)",
+ String.format("%.0f mm", cycloneDeckElevationM * 1000.0));
+ appendRow(sb, " Euler Number", String.format("%.1f", cycloneEulerNumber));
+ appendRow(sb, " DP to Drain", String.format("%.0f %%", cycloneDpToDrainPct));
+ }
+ if (drainPipeDiameterM > 0) {
+ appendRow(sb, "Drain Pipe Equiv. ID", String.format("%.1f mm", drainPipeDiameterM * 1000.0));
+ }
+
+ // Liquid levels
+ sb.append('\n');
+ sb.append(" LIQUID LEVELS (from BTL)\n");
+ sb.append(sep2).append('\n');
+ if (laLLElevationM > 0) {
+ appendRow(sb, "LA(LL)", String.format("%.0f mm", laLLElevationM * 1000.0));
+ }
+ if (laLElevationM > 0) {
+ appendRow(sb, "LA(L)", String.format("%.0f mm", laLElevationM * 1000.0));
+ }
+ if (laHElevationM > 0) {
+ appendRow(sb, "LA(H)", String.format("%.0f mm", laHElevationM * 1000.0));
+ }
+ if (laHHElevationM > 0) {
+ appendRow(sb, "LA(HH)", String.format("%.0f mm", laHHElevationM * 1000.0));
+ }
+ if (laLLElevationM == 0 && laLElevationM == 0 && laHElevationM == 0 && laHHElevationM == 0) {
+ appendRow(sb, "(none set)", "");
+ }
+
+ // Drainage summary
+ if (hasDemistingCyclones && laHHElevationM > 0) {
+ sb.append('\n');
+ sb.append(" DRAINAGE CHECK (per API 12J / TR3500)\n");
+ sb.append(sep2).append('\n');
+ double drainageHeight = (cycloneDeckElevationM - laHHElevationM) * 1000.0;
+ appendRow(sb, "Reference Level", "LA(HH) (most conservative)");
+ appendRow(sb, "Cyclone Deck Bottom",
+ String.format("%.0f mm", cycloneDeckElevationM * 1000.0));
+ appendRow(sb, "LA(HH)", String.format("%.0f mm", laHHElevationM * 1000.0));
+ appendRow(sb, "Height Available", String.format("%.0f mm", drainageHeight));
+ }
+
+ sb.append('\n');
+ sb.append(line).append('\n');
+ return sb.toString();
+ }
+
+ /**
+ * Appends a formatted row to the text report.
+ *
+ * @param sb the StringBuilder to append to
+ * @param label the row label
+ * @param value the row value
+ */
+ private void appendRow(StringBuilder sb, String label, String value) {
+ sb.append(String.format(" %-25s : %s%n", label, value));
}
}
diff --git a/src/main/java/neqsim/process/mechanicaldesign/separator/SeparatorMechanicalDesign.java b/src/main/java/neqsim/process/mechanicaldesign/separator/SeparatorMechanicalDesign.java
index 763b6f1fc6..f187f765d4 100644
--- a/src/main/java/neqsim/process/mechanicaldesign/separator/SeparatorMechanicalDesign.java
+++ b/src/main/java/neqsim/process/mechanicaldesign/separator/SeparatorMechanicalDesign.java
@@ -150,7 +150,8 @@ public class SeparatorMechanicalDesign extends MechanicalDesign {
private double waterOutletNozzleID = 0.0;
// ============================================================================
- // Entrainment Performance Results (populated from SeparatorPerformanceCalculator)
+ // Entrainment Performance Results (populated from
+ // SeparatorPerformanceCalculator)
// ============================================================================
/** Whether detailed entrainment calculation was used. */
@@ -547,9 +548,12 @@ public void performSizingCalculations() {
@Override
public void setDesign() {
Separator separator = (Separator) getProcessEquipment();
+ // Geometry is stored in MechanicalDesign; Separator delegates to us.
+ // We still call setInternalDiameter/setSeparatorLength to trigger side effects
+ // (liquidLevel update, holdup volume recalculation).
separator.setInternalDiameter(innerDiameter);
separator.setSeparatorLength(tantanLength);
- // Synchronize design parameters back to separator
+ // Synchronize process parameters
separator.setDesignGasLoadFactor(gasLoadFactor);
separator.setDesignLiquidLevelFraction(1.0 - Fg);
// Synchronize inlet nozzle diameter if set
diff --git a/src/main/java/neqsim/process/mechanicaldesign/separator/conformity/ConformityReport.java b/src/main/java/neqsim/process/mechanicaldesign/separator/conformity/ConformityReport.java
new file mode 100644
index 0000000000..1f99c5e5b8
--- /dev/null
+++ b/src/main/java/neqsim/process/mechanicaldesign/separator/conformity/ConformityReport.java
@@ -0,0 +1,190 @@
+package neqsim.process.mechanicaldesign.separator.conformity;
+
+import java.io.Serializable;
+import java.util.ArrayList;
+import java.util.Collections;
+import java.util.List;
+
+/**
+ * Collection of conformity check results for a separator or scrubber.
+ *
+ *
+ * Aggregates individual {@link ConformityResult} entries from vessel-level and
+ * internals-level
+ * checks. Provides summary methods and formatted printing.
+ *
+ *
+ * @author NeqSim Development Team
+ * @version 1.0
+ */
+public class ConformityReport implements Serializable {
+ /** Serialization version UID. */
+ private static final long serialVersionUID = 1000L;
+
+ private final String equipmentName;
+ private final String standard;
+ private final List results = new ArrayList();
+
+ /**
+ * Constructs a ConformityReport.
+ *
+ * @param equipmentName name of the equipment being checked
+ * @param standard the conformity standard applied
+ */
+ public ConformityReport(String equipmentName, String standard) {
+ this.equipmentName = equipmentName;
+ this.standard = standard;
+ }
+
+ /**
+ * Adds a result to the report.
+ *
+ * @param result the conformity result to add
+ */
+ public void addResult(ConformityResult result) {
+ results.add(result);
+ }
+
+ /**
+ * Gets all results.
+ *
+ * @return unmodifiable list of results
+ */
+ public List getResults() {
+ return Collections.unmodifiableList(results);
+ }
+
+ /**
+ * Gets the equipment name.
+ *
+ * @return the equipment name
+ */
+ public String getEquipmentName() {
+ return equipmentName;
+ }
+
+ /**
+ * Gets the standard name.
+ *
+ * @return the standard name
+ */
+ public String getStandard() {
+ return standard;
+ }
+
+ /**
+ * Returns true if all checks passed (PASS or WARNING or NOT_APPLICABLE).
+ *
+ * @return true if no FAIL results
+ */
+ public boolean isConforming() {
+ for (ConformityResult r : results) {
+ if (r.getStatus() == ConformityResult.Status.FAIL) {
+ return false;
+ }
+ }
+ return true;
+ }
+
+ /**
+ * Counts results with FAIL status.
+ *
+ * @return number of failed checks
+ */
+ public int getFailCount() {
+ int count = 0;
+ for (ConformityResult r : results) {
+ if (r.getStatus() == ConformityResult.Status.FAIL) {
+ count++;
+ }
+ }
+ return count;
+ }
+
+ /**
+ * Counts results with WARNING status.
+ *
+ * @return number of warning checks
+ */
+ public int getWarningCount() {
+ int count = 0;
+ for (ConformityResult r : results) {
+ if (r.getStatus() == ConformityResult.Status.WARNING) {
+ count++;
+ }
+ }
+ return count;
+ }
+
+ /**
+ * Counts results with PASS status.
+ *
+ * @return number of passed checks
+ */
+ public int getPassCount() {
+ int count = 0;
+ for (ConformityResult r : results) {
+ if (r.getStatus() == ConformityResult.Status.PASS) {
+ count++;
+ }
+ }
+ return count;
+ }
+
+ /**
+ * Prints a formatted summary table of all results.
+ *
+ * @return formatted text report
+ */
+ public String toTextReport() {
+ StringBuilder sb = new StringBuilder();
+ String line = "----------------------------------------------------------------------"
+ + "--------------------";
+ sb.append(line).append('\n');
+ sb.append(" CONFORMITY CHECK: ").append(equipmentName);
+ sb.append(" [").append(standard).append("]\n");
+ sb.append(line).append('\n');
+ sb.append(String.format(" %-25s %10s %10s %-6s %s\n",
+ "Check", "Actual", "Limit", "Unit", "Status"));
+ sb.append(line).append('\n');
+
+ for (ConformityResult r : results) {
+ String statusStr;
+ switch (r.getStatus()) {
+ case PASS:
+ statusStr = "PASS";
+ break;
+ case WARNING:
+ statusStr = "WARN";
+ break;
+ case FAIL:
+ statusStr = "FAIL";
+ break;
+ default:
+ statusStr = "N/A";
+ break;
+ }
+ if (r.getStatus() == ConformityResult.Status.NOT_APPLICABLE) {
+ sb.append(String.format(" %-25s %10s %10s %-6s %s\n",
+ r.getCheckName(), "-", "-", "", statusStr));
+ } else {
+ sb.append(String.format(" %-25s %10.4f %10.4f %-6s %s\n",
+ r.getCheckName(), r.getActualValue(), r.getLimitValue(), r.getUnit(), statusStr));
+ }
+ }
+
+ sb.append(line).append('\n');
+ sb.append(" Summary: ").append(getPassCount()).append(" PASS, ");
+ sb.append(getWarningCount()).append(" WARN, ");
+ sb.append(getFailCount()).append(" FAIL");
+ sb.append(" → ").append(isConforming() ? "CONFORMING" : "NON-CONFORMING").append('\n');
+ sb.append(line).append('\n');
+ return sb.toString();
+ }
+
+ /** {@inheritDoc} */
+ @Override
+ public String toString() {
+ return toTextReport();
+ }
+}
diff --git a/src/main/java/neqsim/process/mechanicaldesign/separator/conformity/ConformityResult.java b/src/main/java/neqsim/process/mechanicaldesign/separator/conformity/ConformityResult.java
new file mode 100644
index 0000000000..43a1eb0f68
--- /dev/null
+++ b/src/main/java/neqsim/process/mechanicaldesign/separator/conformity/ConformityResult.java
@@ -0,0 +1,237 @@
+package neqsim.process.mechanicaldesign.separator.conformity;
+
+import java.io.Serializable;
+
+/**
+ * Result of a single conformity check against a design standard.
+ *
+ *
+ * Each check evaluates an actual operating value against a limit from a named
+ * standard (e.g.,
+ * TR3500). The result includes the check name, the internal it applies to (if
+ * any), the actual
+ * value, the limit, and a pass/warning/fail status.
+ *
+ *
+ * @author NeqSim Development Team
+ * @version 1.0
+ */
+public class ConformityResult implements Serializable {
+ /** Serialization version UID. */
+ private static final long serialVersionUID = 1000L;
+
+ /**
+ * Status of a conformity check.
+ */
+ public enum Status {
+ /** Actual value is within the acceptable range. */
+ PASS,
+ /** Actual value is close to the limit (within warning threshold). */
+ WARNING,
+ /** Actual value exceeds the limit. */
+ FAIL,
+ /** Check could not be evaluated (missing data or not applicable). */
+ NOT_APPLICABLE
+ }
+
+ /**
+ * Direction of the limit check.
+ */
+ public enum LimitDirection {
+ /** Actual value must be BELOW the limit (e.g., K-factor, momentum). */
+ MAXIMUM,
+ /**
+ * Actual value must be ABOVE the limit (e.g., drainage head, retention time).
+ */
+ MINIMUM
+ }
+
+ private final String checkName;
+ private final String standard;
+ private final String internalType;
+ private final double actualValue;
+ private final double limitValue;
+ private final String unit;
+ private final Status status;
+ private final LimitDirection direction;
+ private final String description;
+
+ /**
+ * Constructs a ConformityResult.
+ *
+ * @param checkName short identifier for the check (e.g., "k-factor",
+ * "inlet-momentum")
+ * @param standard the conformity standard (e.g., "TR3500", "API-12J")
+ * @param internalType the internal this check applies to (e.g., "mesh-pad",
+ * "demisting-cyclones")
+ * or empty string for vessel-level checks
+ * @param actualValue the calculated actual value
+ * @param limitValue the acceptance limit from the standard
+ * @param unit the engineering unit (e.g., "m/s", "Pa", "mm")
+ * @param direction whether the limit is a maximum or minimum
+ * @param description human-readable description of the check
+ */
+ public ConformityResult(String checkName, String standard, String internalType,
+ double actualValue, double limitValue, String unit, LimitDirection direction,
+ String description) {
+ this.checkName = checkName;
+ this.standard = standard;
+ this.internalType = internalType;
+ this.actualValue = actualValue;
+ this.limitValue = limitValue;
+ this.unit = unit;
+ this.direction = direction;
+ this.description = description;
+ this.status = evaluateStatus(actualValue, limitValue, direction);
+ }
+
+ /**
+ * Creates a NOT_APPLICABLE result when a check cannot be evaluated.
+ *
+ * @param checkName short identifier for the check
+ * @param standard the conformity standard
+ * @param reason why the check is not applicable
+ * @return a ConformityResult with NOT_APPLICABLE status
+ */
+ public static ConformityResult notApplicable(String checkName, String standard, String reason) {
+ ConformityResult result = new ConformityResult(checkName, standard, "", Double.NaN, Double.NaN,
+ "", LimitDirection.MAXIMUM, reason);
+ return new ConformityResult(checkName, standard, "", Double.NaN, Double.NaN, "",
+ LimitDirection.MAXIMUM, reason) {
+ private static final long serialVersionUID = 1L;
+
+ @Override
+ public Status getStatus() {
+ return Status.NOT_APPLICABLE;
+ }
+ };
+ }
+
+ /**
+ * Evaluates the status based on actual value, limit, and direction.
+ *
+ * @param actual the actual value
+ * @param limit the limit value
+ * @param dir the direction (MAXIMUM or MINIMUM)
+ * @return the evaluated status
+ */
+ private static Status evaluateStatus(double actual, double limit, LimitDirection dir) {
+ if (Double.isNaN(actual) || Double.isNaN(limit)) {
+ return Status.NOT_APPLICABLE;
+ }
+ double warningThreshold = 0.9;
+ if (dir == LimitDirection.MAXIMUM) {
+ if (actual > limit) {
+ return Status.FAIL;
+ } else if (actual > limit * warningThreshold) {
+ return Status.WARNING;
+ }
+ return Status.PASS;
+ } else {
+ // MINIMUM: actual must be >= limit
+ if (actual < limit) {
+ return Status.FAIL;
+ } else if (actual < limit * (1.0 + (1.0 - warningThreshold))) {
+ return Status.WARNING;
+ }
+ return Status.PASS;
+ }
+ }
+
+ /**
+ * Gets the check name.
+ *
+ * @return the check name
+ */
+ public String getCheckName() {
+ return checkName;
+ }
+
+ /**
+ * Gets the standard name.
+ *
+ * @return the standard name
+ */
+ public String getStandard() {
+ return standard;
+ }
+
+ /**
+ * Gets the internal type this check applies to.
+ *
+ * @return the internal type, or empty string for vessel-level checks
+ */
+ public String getInternalType() {
+ return internalType;
+ }
+
+ /**
+ * Gets the actual calculated value.
+ *
+ * @return the actual value
+ */
+ public double getActualValue() {
+ return actualValue;
+ }
+
+ /**
+ * Gets the limit value from the standard.
+ *
+ * @return the limit value
+ */
+ public double getLimitValue() {
+ return limitValue;
+ }
+
+ /**
+ * Gets the engineering unit.
+ *
+ * @return the unit string
+ */
+ public String getUnit() {
+ return unit;
+ }
+
+ /**
+ * Gets the conformity status.
+ *
+ * @return PASS, WARNING, FAIL, or NOT_APPLICABLE
+ */
+ public Status getStatus() {
+ return status;
+ }
+
+ /**
+ * Gets the limit direction.
+ *
+ * @return MAXIMUM or MINIMUM
+ */
+ public LimitDirection getDirection() {
+ return direction;
+ }
+
+ /**
+ * Gets the human-readable description.
+ *
+ * @return the description
+ */
+ public String getDescription() {
+ return description;
+ }
+
+ /**
+ * Returns true if the check passed (PASS or WARNING).
+ *
+ * @return true if status is PASS or WARNING
+ */
+ public boolean isPassed() {
+ return status == Status.PASS || status == Status.WARNING;
+ }
+
+ /** {@inheritDoc} */
+ @Override
+ public String toString() {
+ return String.format("%-25s %10.4f %10.4f %-6s %-4s %s",
+ checkName, actualValue, limitValue, unit, status, description);
+ }
+}
diff --git a/src/main/java/neqsim/process/mechanicaldesign/separator/conformity/ConformityRuleSet.java b/src/main/java/neqsim/process/mechanicaldesign/separator/conformity/ConformityRuleSet.java
new file mode 100644
index 0000000000..f27f3d61f6
--- /dev/null
+++ b/src/main/java/neqsim/process/mechanicaldesign/separator/conformity/ConformityRuleSet.java
@@ -0,0 +1,275 @@
+package neqsim.process.mechanicaldesign.separator.conformity;
+
+import java.io.Serializable;
+import java.util.ArrayList;
+import java.util.List;
+import neqsim.process.equipment.separator.Separator;
+import neqsim.process.mechanicaldesign.separator.GasScrubberMechanicalDesign;
+
+/**
+ * Defines conformity rules for a specific design standard.
+ *
+ *
+ * Each rule set defines what checks to perform based on the standard (TR3500,
+ * Shell DEP, API 12J,
+ * NORSOK P-002) and what internals are installed. The checks are
+ * internals-aware: inlet devices
+ * trigger momentum checks, demisting cyclones trigger drainage checks, mesh
+ * pads trigger mesh
+ * K-value checks, etc.
+ *
+ *
+ *
+ * Usage:
+ *
+ *
+ *
+ * ConformityRuleSet rules = ConformityRuleSet.create("TR3500");
+ * ConformityReport report = rules.evaluate(scrubberMechDesign);
+ *
+ *
+ * @author NeqSim Development Team
+ * @version 1.0
+ */
+public abstract class ConformityRuleSet implements Serializable {
+ /** Serialization version UID. */
+ private static final long serialVersionUID = 1000L;
+
+ private final String name;
+
+ /**
+ * Constructs a ConformityRuleSet.
+ *
+ * @param name the standard name
+ */
+ protected ConformityRuleSet(String name) {
+ this.name = name;
+ }
+
+ /**
+ * Creates a rule set for the named standard.
+ *
+ * @param standardName the standard identifier: "TR3500", "API-12J",
+ * "Shell-DEP", "NORSOK-P002"
+ * @return a ConformityRuleSet for the named standard
+ * @throws IllegalArgumentException if the standard is not recognized
+ */
+ public static ConformityRuleSet create(String standardName) {
+ if (standardName == null) {
+ throw new IllegalArgumentException("Standard name cannot be null");
+ }
+ String normalized = standardName.trim().toUpperCase().replace(" ", "").replace("_", "");
+ if (normalized.equals("TR3500") || normalized.equals("EQUINORTR3500")) {
+ return new TR3500RuleSet();
+ }
+ throw new IllegalArgumentException("Unknown conformity standard: " + standardName
+ + ". Supported: TR3500");
+ }
+
+ /**
+ * Gets the standard name.
+ *
+ * @return the standard name
+ */
+ public String getName() {
+ return name;
+ }
+
+ /**
+ * Evaluates all applicable conformity checks against the given mechanical
+ * design.
+ *
+ *
+ * The checks run depend on what internals are installed. The method reads
+ * operating conditions
+ * from the scrubber's current fluid state (after the most recent run).
+ *
+ *
+ * @param design the scrubber mechanical design to check
+ * @return a conformity report with all check results
+ */
+ public abstract ConformityReport evaluate(GasScrubberMechanicalDesign design);
+
+ /**
+ * Returns the names of CapacityConstraints that this standard defines.
+ *
+ *
+ * These can be used to enable the corresponding constraints on the Separator
+ * via
+ * {@code separator.enableConstraints(...)}.
+ *
+ *
+ * @param design the mechanical design (to check which internals are installed)
+ * @return list of constraint names to enable
+ */
+ public abstract List getConstraintNames(GasScrubberMechanicalDesign design);
+
+ // =====================================================================
+ // TR3500 Implementation
+ // =====================================================================
+
+ /**
+ * Equinor TR3500 conformity rules for gas scrubbers.
+ *
+ *
+ * Always checks:
+ *
+ *
+ * - K-factor (Souders-Brown) vs limit (depends on internals type)
+ *
+ *
+ *
+ * If inlet vane or inlet cyclones installed:
+ *
+ *
+ * - Inlet nozzle momentum vs limit
+ *
+ *
+ *
+ * If demisting cyclones installed:
+ *
+ *
+ * - Drainage head available vs required
+ * - Cyclone pressure drop to drain
+ *
+ *
+ *
+ * If mesh pad installed:
+ *
+ *
+ * - Mesh pad gas velocity (K-value through mesh area)
+ *
+ */
+ private static class TR3500RuleSet extends ConformityRuleSet {
+ private static final long serialVersionUID = 1L;
+
+ /** Maximum K-factor for scrubbers [m/s]. */
+ private static final double K_FACTOR_LIMIT = 0.15;
+
+ /** Maximum inlet nozzle momentum [Pa]. */
+ private static final double INLET_MOMENTUM_LIMIT = 15000.0;
+
+ /** Maximum mesh K-value [m/s]. */
+ private static final double MESH_K_VALUE_LIMIT = 0.27;
+
+ /**
+ * Constructs a TR3500RuleSet.
+ */
+ TR3500RuleSet() {
+ super("TR3500");
+ }
+
+ /** {@inheritDoc} */
+ @Override
+ public ConformityReport evaluate(GasScrubberMechanicalDesign design) {
+ Separator sep = (Separator) design.getProcessEquipment();
+ ConformityReport report = new ConformityReport(sep.getName(), getName());
+
+ // Read operating conditions from the separator's current fluid state
+ neqsim.thermo.system.SystemInterface fluid = sep.getThermoSystem();
+ if (fluid == null) {
+ return report;
+ }
+ fluid.initPhysicalProperties();
+
+ double gasDensity = fluid.getPhase(0).getPhysicalProperties().getDensity();
+ double gasFlowM3s = fluid.getPhase(0).getFlowRate("m3/sec");
+
+ double liquidDensity = 1000.0; // default for dry gas
+ if (fluid.getNumberOfPhases() >= 2) {
+ if (fluid.hasPhaseType("oil")) {
+ liquidDensity = fluid.getPhase("oil").getPhysicalProperties().getDensity();
+ } else if (fluid.hasPhaseType("aqueous")) {
+ liquidDensity = fluid.getPhase("aqueous").getPhysicalProperties().getDensity();
+ }
+ }
+
+ // --- Vessel-level checks (ALWAYS) ---
+
+ // K-factor (Souders-Brown)
+ double vesselArea = Math.PI * Math.pow(design.getInnerDiameter() / 2.0, 2);
+ double gasVelocity = vesselArea > 0 ? gasFlowM3s / vesselArea : 0;
+ double kFactor = gasVelocity * Math.sqrt(gasDensity / (liquidDensity - gasDensity));
+ report.addResult(new ConformityResult("k-factor", getName(), "",
+ kFactor, K_FACTOR_LIMIT, "m/s", ConformityResult.LimitDirection.MAXIMUM,
+ "Souders-Brown K-factor at vessel cross-section"));
+
+ // --- Inlet device checks ---
+ if (design.hasInletCyclones() || design.getInletNozzleID() > 0) {
+ double inletArea = Math.PI * Math.pow(design.getInletNozzleID() / 2.0, 2);
+ double mixedDensity = gasDensity; // simplified; could weight by volume fraction
+ if (fluid.getNumberOfPhases() >= 2) {
+ double totalMassFlow = fluid.getFlowRate("kg/sec");
+ double totalVolFlow = fluid.getPhase(0).getFlowRate("m3/sec");
+ for (int i = 1; i < fluid.getNumberOfPhases(); i++) {
+ totalVolFlow += fluid.getPhase(i).getFlowRate("m3/sec");
+ }
+ mixedDensity = totalVolFlow > 0 ? totalMassFlow / totalVolFlow : gasDensity;
+ }
+ double inletVelocity = inletArea > 0 ? gasFlowM3s / inletArea : 0;
+ double inletMomentum = mixedDensity * inletVelocity * inletVelocity;
+ report.addResult(new ConformityResult("inlet-momentum", getName(), "inlet-device",
+ inletMomentum, INLET_MOMENTUM_LIMIT, "Pa", ConformityResult.LimitDirection.MAXIMUM,
+ "Inlet nozzle momentum (rho*v^2)"));
+ }
+
+ // --- Demisting cyclones checks ---
+ if (design.hasDemistingCyclones()) {
+ // Drainage head check
+ double cycloneDeckBottom = design.getCycloneDeckElevationM();
+ double laHH = design.getLaHHElevationM();
+ if (cycloneDeckBottom > 0 && laHH > 0) {
+ double drainageHead = (cycloneDeckBottom - laHH) * 1000.0; // m to mm
+
+ // Required drainage from cyclone dP
+ int nCyclones = design.getNumberOfDemistingCyclones();
+ double cycloneDiameter = design.getDemistingCycloneDiameterM();
+ double cycloneArea = nCyclones * Math.PI * Math.pow(cycloneDiameter / 2.0, 2);
+ double gasMomentumPerCyclone = cycloneArea > 0
+ ? gasDensity * Math.pow(gasFlowM3s / cycloneArea, 2)
+ : 0;
+ double cycloneDpTotal = design.getCycloneEulerNumber() * gasMomentumPerCyclone;
+ double cycloneDpToDrain = cycloneDpTotal * design.getCycloneDpToDrainPct() / 100.0;
+ double requiredDrainage = liquidDensity > 0 ? cycloneDpToDrain / (liquidDensity * 9.81) * 1000.0 : 0;
+
+ report.addResult(new ConformityResult("drainage-head", getName(), "demisting-cyclones",
+ drainageHead, requiredDrainage, "mm", ConformityResult.LimitDirection.MINIMUM,
+ "Available drainage head above LA(HH) vs required"));
+
+ // Cyclone dP to drain
+ report.addResult(new ConformityResult("cyclone-dp-to-drain", getName(),
+ "demisting-cyclones",
+ cycloneDpToDrain / 100.0, 50.0, "mbar", ConformityResult.LimitDirection.MAXIMUM,
+ "Cyclone pressure drop available to drain"));
+ } else {
+ report.addResult(ConformityResult.notApplicable("drainage-head", getName(),
+ "Cyclone deck or LA(HH) elevation not set"));
+ }
+ }
+
+ // --- Mesh pad checks ---
+ if (design.hasMeshPad()) {
+ double meshArea = design.getMeshPadAreaM2();
+ double meshGasVelocity = meshArea > 0 ? gasFlowM3s / meshArea : 0;
+ double meshKValue = meshGasVelocity * Math.sqrt(gasDensity / (liquidDensity - gasDensity));
+ report.addResult(new ConformityResult("mesh-k-value", getName(), "mesh-pad",
+ meshKValue, MESH_K_VALUE_LIMIT, "m/s", ConformityResult.LimitDirection.MAXIMUM,
+ "K-value through mesh pad area"));
+ }
+
+ return report;
+ }
+
+ /** {@inheritDoc} */
+ @Override
+ public List getConstraintNames(GasScrubberMechanicalDesign design) {
+ List names = new ArrayList();
+ names.add("gasLoadFactor");
+ names.add("kValue");
+ if (design.hasInletCyclones() || design.getInletNozzleID() > 0) {
+ names.add("inletMomentum");
+ }
+ return names;
+ }
+ }
+}
diff --git a/src/main/java/neqsim/process/mechanicaldesign/separator/conformity/package-info.java b/src/main/java/neqsim/process/mechanicaldesign/separator/conformity/package-info.java
new file mode 100644
index 0000000000..d810d91a2d
--- /dev/null
+++ b/src/main/java/neqsim/process/mechanicaldesign/separator/conformity/package-info.java
@@ -0,0 +1,12 @@
+/**
+ * Conformity checking for separator and scrubber mechanical designs.
+ *
+ *
+ * Provides standard-specific conformity rules (Equinor TR3500, Shell DEP, API
+ * 12J, NORSOK P-002)
+ * that evaluate separator/scrubber performance against acceptance criteria.
+ * Checks are
+ * internals-aware: the rules that apply depend on what internals are installed.
+ *
+ */
+package neqsim.process.mechanicaldesign.separator.conformity;