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: *

*
    *
  1. {@link #setInternalDiameter(double)} — scrubber inner diameter [m]
  2. - *
  3. {@link #setDesignGasLoadFactor(double)} — design K-factor [m/s], - * typically 0.04–0.10 for + *
  4. {@link #setDesignGasLoadFactor(double)} — design K-factor [m/s], typically 0.04–0.10 for * vertical scrubbers
  5. *
* *

- * 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;