From c19106b7dd1f2ba81ba48434fa24fca01a83e2c2 Mon Sep 17 00:00:00 2001 From: Even Solbraa <41290109+EvenSol@users.noreply.github.com> Date: Mon, 3 Nov 2025 16:19:10 +0100 Subject: [PATCH 1/2] Add oil assay fluid characterization support --- .../OilAssayCharacterisation.java | 325 ++++++++++++++++++ .../neqsim/thermo/system/SystemInterface.java | 10 + .../neqsim/thermo/system/SystemThermo.java | 19 +- .../OilAssayCharacterisationTest.java | 84 +++++ 4 files changed, 437 insertions(+), 1 deletion(-) create mode 100644 src/main/java/neqsim/fluid/characterisation/OilAssayCharacterisation.java create mode 100644 src/test/java/neqsim/fluid/characterisation/OilAssayCharacterisationTest.java diff --git a/src/main/java/neqsim/fluid/characterisation/OilAssayCharacterisation.java b/src/main/java/neqsim/fluid/characterisation/OilAssayCharacterisation.java new file mode 100644 index 0000000000..aee52cb117 --- /dev/null +++ b/src/main/java/neqsim/fluid/characterisation/OilAssayCharacterisation.java @@ -0,0 +1,325 @@ +package neqsim.fluid.characterisation; + +import java.io.Serializable; +import java.util.ArrayList; +import java.util.Collection; +import java.util.Collections; +import java.util.List; +import java.util.Objects; +import org.apache.logging.log4j.LogManager; +import org.apache.logging.log4j.Logger; +import neqsim.thermo.system.SystemInterface; + +/** + * Utility for characterising an oil system from assay information. + */ +public class OilAssayCharacterisation implements Cloneable, Serializable { + private static final long serialVersionUID = 1000L; + private static final Logger logger = LogManager.getLogger(OilAssayCharacterisation.class); + private static final double FRACTION_TOLERANCE = 1e-10; + private static final double KELVIN_OFFSET = 273.15; + private static final double WATER_DENSITY_60F_G_CC = 0.999016; // API definition reference density. + + private transient SystemInterface system; + private double totalAssayMass = 1.0; // kg basis when converting mass fraction to moles. + private List cuts = new ArrayList<>(); + + public OilAssayCharacterisation(SystemInterface system) { + setThermoSystem(system); + } + + public void setThermoSystem(SystemInterface system) { + this.system = Objects.requireNonNull(system, "system"); + } + + public double getTotalAssayMass() { + return totalAssayMass; + } + + public void setTotalAssayMass(double totalAssayMass) { + if (!(totalAssayMass > 0.0)) { + throw new IllegalArgumentException("Total assay mass must be positive"); + } + this.totalAssayMass = totalAssayMass; + } + + public void clearCuts() { + cuts.clear(); + } + + public void addCut(AssayCut cut) { + cuts.add(Objects.requireNonNull(cut, "cut")); + } + + public void addCuts(Collection cuts) { + if (cuts == null) { + return; + } + for (AssayCut cut : cuts) { + addCut(cut); + } + } + + public List getCuts() { + return Collections.unmodifiableList(cuts); + } + + public void apply() { + if (system == null) { + throw new IllegalStateException("Thermodynamic system not attached to assay data"); + } + if (cuts.isEmpty()) { + logger.warn("No assay cuts supplied – nothing to characterise"); + return; + } + + double[] massFractions = resolveMassFractions(); + for (int i = 0; i < cuts.size(); i++) { + AssayCut cut = cuts.get(i); + double massFraction = massFractions[i]; + if (!(massFraction > FRACTION_TOLERANCE)) { + continue; + } + + double density = cut.resolveDensity(); + double boilingPoint = cut.resolveAverageBoilingPoint(); + double molarMass = cut.resolveMolarMass(density, boilingPoint); + double moles = totalAssayMass * massFraction / molarMass; + + if (moles <= 0.0 || Double.isNaN(moles) || Double.isInfinite(moles)) { + throw new IllegalStateException( + "Calculated mole amount for assay cut " + cut.getName() + " is not finite"); + } + + system.addTBPfraction(cut.getName(), moles, molarMass, density); + } + } + + private double[] resolveMassFractions() { + double[] massFractions = new double[cuts.size()]; + double specifiedMass = 0.0; + double volumeMass = 0.0; + boolean hasVolumeFractions = false; + + for (int i = 0; i < cuts.size(); i++) { + AssayCut cut = cuts.get(i); + if (cut.hasMassFraction()) { + double massFraction = cut.getMassFraction(); + specifiedMass += massFraction; + massFractions[i] = massFraction; + } else if (cut.hasVolumeFraction()) { + hasVolumeFractions = true; + double density = cut.resolveDensity(); + volumeMass += cut.getVolumeFraction() * density; + } else { + throw new IllegalStateException( + "Assay cut " + cut.getName() + " must define a mass or volume fraction"); + } + } + + if (specifiedMass > 1.0 + 1e-6) { + throw new IllegalStateException( + "Specified mass fractions exceed unity: " + specifiedMass); + } + + double remainingMass = Math.max(0.0, 1.0 - specifiedMass); + + if (hasVolumeFractions) { + if (!(volumeMass > 0.0)) { + throw new IllegalStateException("Unable to derive mass fractions from volume data"); + } + for (int i = 0; i < cuts.size(); i++) { + AssayCut cut = cuts.get(i); + if (!cut.hasMassFraction() && cut.hasVolumeFraction()) { + double density = cut.resolveDensity(); + double cutMass = cut.getVolumeFraction() * density; + massFractions[i] = cutMass / volumeMass * remainingMass; + } + } + } + + double totalMassFraction = 0.0; + for (double fraction : massFractions) { + totalMassFraction += fraction; + } + + if (!(totalMassFraction > 0.0)) { + throw new IllegalStateException("No valid mass fractions derived from assay data"); + } + + if (Math.abs(totalMassFraction - 1.0) > 1.0e-8) { + for (int i = 0; i < massFractions.length; i++) { + massFractions[i] /= totalMassFraction; + } + } + return massFractions; + } + + @Override + public OilAssayCharacterisation clone() { + try { + OilAssayCharacterisation clone = (OilAssayCharacterisation) super.clone(); + clone.cuts = new ArrayList<>(); + for (AssayCut cut : cuts) { + clone.cuts.add(cut.clone()); + } + clone.system = system; + return clone; + } catch (CloneNotSupportedException ex) { + throw new IllegalStateException("Clone not supported", ex); + } + } + + public static final class AssayCut implements Cloneable, Serializable { + private static final long serialVersionUID = 1000L; + private final String name; + private Double massFraction; + private Double volumeFraction; + private Double density; + private Double apiGravity; + private Double averageBoilingPointKelvin; + private Double molarMass; + + public AssayCut(String name) { + this.name = Objects.requireNonNull(name, "name"); + } + + public String getName() { + return name; + } + + public AssayCut withMassFraction(double massFraction) { + this.massFraction = sanitiseFraction(massFraction); + return this; + } + + public AssayCut withWeightPercent(double weightPercent) { + this.massFraction = sanitiseFraction(weightPercent / 100.0); + return this; + } + + public AssayCut withVolumeFraction(double volumeFraction) { + this.volumeFraction = sanitiseFraction(volumeFraction); + return this; + } + + public AssayCut withVolumePercent(double volumePercent) { + this.volumeFraction = sanitiseFraction(volumePercent / 100.0); + return this; + } + + public AssayCut withDensity(double density) { + if (!(density > 0.0)) { + throw new IllegalArgumentException("Density must be positive"); + } + this.density = density; + return this; + } + + public AssayCut withApiGravity(double apiGravity) { + if (!(apiGravity > 0.0)) { + throw new IllegalArgumentException("API gravity must be positive"); + } + this.apiGravity = apiGravity; + return this; + } + + public AssayCut withAverageBoilingPointKelvin(double temperatureKelvin) { + if (!(temperatureKelvin > 0.0)) { + throw new IllegalArgumentException("Boiling point must be positive"); + } + this.averageBoilingPointKelvin = temperatureKelvin; + return this; + } + + public AssayCut withAverageBoilingPointCelsius(double temperatureCelsius) { + return withAverageBoilingPointKelvin(temperatureCelsius + KELVIN_OFFSET); + } + + public AssayCut withAverageBoilingPointFahrenheit(double temperatureFahrenheit) { + double temperatureCelsius = (temperatureFahrenheit - 32.0) * 5.0 / 9.0; + return withAverageBoilingPointKelvin(temperatureCelsius + KELVIN_OFFSET); + } + + public AssayCut withMolarMass(double molarMass) { + if (!(molarMass > 0.0)) { + throw new IllegalArgumentException("Molar mass must be positive"); + } + this.molarMass = molarMass; + return this; + } + + public boolean hasMassFraction() { + return massFraction != null; + } + + public double getMassFraction() { + if (massFraction == null) { + throw new IllegalStateException("Mass fraction not set"); + } + return massFraction; + } + + public boolean hasVolumeFraction() { + return volumeFraction != null; + } + + public double getVolumeFraction() { + if (volumeFraction == null) { + throw new IllegalStateException("Volume fraction not set"); + } + return volumeFraction; + } + + public double resolveDensity() { + if (density != null) { + return density; + } + if (apiGravity != null) { + double specificGravity = 141.5 / (apiGravity + 131.5); + return specificGravity * WATER_DENSITY_60F_G_CC; + } + throw new IllegalStateException("Density or API gravity required for cut " + name); + } + + public double resolveAverageBoilingPoint() { + if (averageBoilingPointKelvin == null) { + throw new IllegalStateException("Average boiling point missing for cut " + name); + } + return averageBoilingPointKelvin; + } + + public double resolveMolarMass(double density, double boilingPointKelvin) { + if (molarMass != null) { + return molarMass; + } + if (!(density > 0.0) || !(boilingPointKelvin > 0.0)) { + throw new IllegalStateException("Cannot derive molar mass without density and boiling point"); + } + double exponent = 2.3776; + double densityExponent = 0.9371; + double molarMassKgPerMol = 5.805e-5 * Math.pow(boilingPointKelvin, exponent) + / Math.pow(density, densityExponent); + return molarMassKgPerMol; + } + + @Override + public AssayCut clone() throws CloneNotSupportedException { + return (AssayCut) super.clone(); + } + + private static double sanitiseFraction(double fraction) { + if (fraction < 0.0) { + throw new IllegalArgumentException("Fraction cannot be negative"); + } + double candidate = fraction; + if (candidate > 1.0 + 1e-9) { + candidate = candidate / 100.0; + } + if (candidate < 0.0 || candidate > 1.0 + 1e-9) { + throw new IllegalArgumentException("Fraction must be between 0 and 1"); + } + return candidate; + } + } +} diff --git a/src/main/java/neqsim/thermo/system/SystemInterface.java b/src/main/java/neqsim/thermo/system/SystemInterface.java index 1e4c21772e..dc04d17b92 100644 --- a/src/main/java/neqsim/thermo/system/SystemInterface.java +++ b/src/main/java/neqsim/thermo/system/SystemInterface.java @@ -5,6 +5,7 @@ import neqsim.physicalproperties.interfaceproperties.InterphasePropertiesInterface; import neqsim.physicalproperties.system.PhysicalPropertyModel; import neqsim.thermo.ThermodynamicConstantsInterface; +import neqsim.fluid.characterisation.OilAssayCharacterisation; import neqsim.thermo.characterization.PseudoComponentCombiner; import neqsim.thermo.characterization.WaxModelInterface; import neqsim.thermo.component.ComponentInterface; @@ -1611,6 +1612,15 @@ public default int getPhaseNumberOfPhase(String phaseTypeName) { */ public double getVolumeFraction(int phaseNumber); + /** + *

+ * getOilAssayCharacterisation. + *

+ * + * @return a {@link neqsim.fluid.characterisation.OilAssayCharacterisation} object + */ + public OilAssayCharacterisation getOilAssayCharacterisation(); + /** *

* getWaxCharacterisation. diff --git a/src/main/java/neqsim/thermo/system/SystemThermo.java b/src/main/java/neqsim/thermo/system/SystemThermo.java index 5c4e2c19f6..1c43bf17f3 100644 --- a/src/main/java/neqsim/thermo/system/SystemThermo.java +++ b/src/main/java/neqsim/thermo/system/SystemThermo.java @@ -34,6 +34,7 @@ import neqsim.util.database.NeqSimDataBase; import neqsim.util.exception.InvalidInputException; import neqsim.util.unit.Units; +import neqsim.fluid.characterisation.OilAssayCharacterisation; /** * This is the base class of the System classes. @@ -144,6 +145,7 @@ public abstract class SystemThermo implements SystemInterface { private double totalNumberOfMoles = 0; private boolean useTVasIndependentVariables = false; protected neqsim.thermo.characterization.WaxCharacterise waxCharacterisation = null; + protected transient OilAssayCharacterisation oilAssayCharacterisation = null; /** *

@@ -1410,9 +1412,15 @@ public SystemThermo clone() { // interfaceProp.clone(); } clonedSystem.characterization = characterization.clone(); - if (clonedSystem.waxCharacterisation != null) { + if (waxCharacterisation != null) { clonedSystem.waxCharacterisation = waxCharacterisation.clone(); } + if (oilAssayCharacterisation != null) { + clonedSystem.oilAssayCharacterisation = oilAssayCharacterisation.clone(); + clonedSystem.oilAssayCharacterisation.setThermoSystem(clonedSystem); + clonedSystem.oilAssayCharacterisation + .setTotalAssayMass(oilAssayCharacterisation.getTotalAssayMass()); + } System.arraycopy(this.beta, 0, clonedSystem.beta, 0, beta.length); System.arraycopy(this.phaseType, 0, clonedSystem.phaseType, 0, phaseType.length); @@ -3433,6 +3441,15 @@ public double getVolumeFraction(int phaseNumber) { return getPhase(phaseNumber).getVolume() / getVolume(); } + /** {@inheritDoc} */ + @Override + public OilAssayCharacterisation getOilAssayCharacterisation() { + if (oilAssayCharacterisation == null) { + oilAssayCharacterisation = new OilAssayCharacterisation(this); + } + return oilAssayCharacterisation; + } + /** {@inheritDoc} */ @Override public neqsim.thermo.characterization.WaxCharacterise getWaxCharacterisation() { diff --git a/src/test/java/neqsim/fluid/characterisation/OilAssayCharacterisationTest.java b/src/test/java/neqsim/fluid/characterisation/OilAssayCharacterisationTest.java new file mode 100644 index 0000000000..1de52b1d44 --- /dev/null +++ b/src/test/java/neqsim/fluid/characterisation/OilAssayCharacterisationTest.java @@ -0,0 +1,84 @@ +package neqsim.fluid.characterisation; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertFalse; +import static org.junit.jupiter.api.Assertions.assertNotNull; +import static org.junit.jupiter.api.Assertions.assertTrue; +import java.util.Arrays; +import org.junit.jupiter.api.Test; +import neqsim.fluid.characterisation.OilAssayCharacterisation.AssayCut; +import neqsim.thermo.component.ComponentInterface; +import neqsim.thermo.system.SystemInterface; +import neqsim.thermo.system.SystemSrkEos; + +public class OilAssayCharacterisationTest { + + @Test + public void testAssayApplicationAddsPseudoComponents() { + SystemInterface system = new SystemSrkEos(298.15, 10.0); + OilAssayCharacterisation characterisation = system.getOilAssayCharacterisation(); + characterisation.clearCuts(); + + AssayCut light = new AssayCut("Light").withWeightPercent(40.0).withDensity(0.75) + .withAverageBoilingPointCelsius(200.0); + AssayCut heavy = new AssayCut("Heavy").withVolumePercent(60.0).withApiGravity(25.0) + .withAverageBoilingPointCelsius(350.0); + + characterisation.addCut(light); + characterisation.addCut(heavy); + characterisation.apply(); + + double lightBoilingPoint = 200.0 + 273.15; + double expectedLightMolarMass = 5.805e-5 * Math.pow(lightBoilingPoint, 2.3776) + / Math.pow(0.75, 0.9371); + ComponentInterface lightComponent = system.getComponent("Light_PC"); + assertNotNull(lightComponent); + assertEquals(expectedLightMolarMass, lightComponent.getMolarMass(), 1e-8); + assertEquals(0.4 / expectedLightMolarMass, lightComponent.getNumberOfmoles(), 1e-8); + + double heavyDensity = 141.5 / (25.0 + 131.5) * 0.999016; + double heavyBoilingPoint = 350.0 + 273.15; + double expectedHeavyMolarMass = 5.805e-5 * Math.pow(heavyBoilingPoint, 2.3776) + / Math.pow(heavyDensity, 0.9371); + ComponentInterface heavyComponent = system.getComponent("Heavy_PC"); + assertNotNull(heavyComponent); + assertEquals(expectedHeavyMolarMass, heavyComponent.getMolarMass(), 1e-8); + assertEquals(0.6 / expectedHeavyMolarMass, heavyComponent.getNumberOfmoles(), 1e-8); + } + + @Test + public void testTotalMassScaling() { + SystemInterface system = new SystemSrkEos(298.15, 10.0); + OilAssayCharacterisation characterisation = system.getOilAssayCharacterisation(); + characterisation.clearCuts(); + + AssayCut cut = new AssayCut("Assay").withMassFraction(1.0).withDensity(0.82) + .withAverageBoilingPointCelsius(300.0); + characterisation.addCut(cut); + characterisation.setTotalAssayMass(2.0); + characterisation.apply(); + + double boilingPoint = 300.0 + 273.15; + double expectedMolarMass = 5.805e-5 * Math.pow(boilingPoint, 2.3776) / Math.pow(0.82, 0.9371); + ComponentInterface component = system.getComponent("Assay_PC"); + assertNotNull(component); + assertEquals(2.0 / expectedMolarMass, component.getNumberOfmoles(), 1e-8); + } + + @Test + public void testCharacterisationClonedWithSystem() { + SystemInterface system = new SystemSrkEos(298.15, 10.0); + OilAssayCharacterisation original = system.getOilAssayCharacterisation(); + original.clearCuts(); + original.addCut(new AssayCut("CloneTest").withMassFraction(1.0).withDensity(0.85) + .withAverageBoilingPointCelsius(310.0)); + + SystemInterface cloned = system.clone(); + OilAssayCharacterisation cloneCharacterisation = cloned.getOilAssayCharacterisation(); + assertTrue(cloneCharacterisation.getCuts().size() == 1); + cloneCharacterisation.apply(); + + assertTrue(Arrays.asList(cloned.getComponentNames()).contains("CloneTest_PC")); + assertFalse(Arrays.asList(system.getComponentNames()).contains("CloneTest_PC")); + } +} From 25a650fda73270885bd780a321a957a7fd88e603 Mon Sep 17 00:00:00 2001 From: Even Solbraa <41290109+EvenSol@users.noreply.github.com> Date: Mon, 3 Nov 2025 15:50:42 +0000 Subject: [PATCH 2/2] update oil assay --- .../OilAssayCharacterisation.java | 30 +++- .../neqsim/thermo/system/SystemInterface.java | 4 +- .../neqsim/thermo/system/SystemThermo.java | 2 +- .../OilAssayCharacterisationTest.java | 84 --------- .../OilAssayCharacterisationTest.java | 159 ++++++++++++++++++ 5 files changed, 183 insertions(+), 96 deletions(-) rename src/main/java/neqsim/{fluid/characterisation => thermo/characterization}/OilAssayCharacterisation.java (91%) delete mode 100644 src/test/java/neqsim/fluid/characterisation/OilAssayCharacterisationTest.java create mode 100644 src/test/java/neqsim/thermo/characterization/OilAssayCharacterisationTest.java diff --git a/src/main/java/neqsim/fluid/characterisation/OilAssayCharacterisation.java b/src/main/java/neqsim/thermo/characterization/OilAssayCharacterisation.java similarity index 91% rename from src/main/java/neqsim/fluid/characterisation/OilAssayCharacterisation.java rename to src/main/java/neqsim/thermo/characterization/OilAssayCharacterisation.java index aee52cb117..54e0965fa0 100644 --- a/src/main/java/neqsim/fluid/characterisation/OilAssayCharacterisation.java +++ b/src/main/java/neqsim/thermo/characterization/OilAssayCharacterisation.java @@ -1,4 +1,4 @@ -package neqsim.fluid.characterisation; +package neqsim.thermo.characterization; import java.io.Serializable; import java.util.ArrayList; @@ -18,7 +18,8 @@ public class OilAssayCharacterisation implements Cloneable, Serializable { private static final Logger logger = LogManager.getLogger(OilAssayCharacterisation.class); private static final double FRACTION_TOLERANCE = 1e-10; private static final double KELVIN_OFFSET = 273.15; - private static final double WATER_DENSITY_60F_G_CC = 0.999016; // API definition reference density. + private static final double WATER_DENSITY_60F_G_CC = 0.999016; // API definition reference + // density. private transient SystemInterface system; private double totalAssayMass = 1.0; // kg basis when converting mass fraction to moles. @@ -82,8 +83,15 @@ public void apply() { } double density = cut.resolveDensity(); - double boilingPoint = cut.resolveAverageBoilingPoint(); - double molarMass = cut.resolveMolarMass(density, boilingPoint); + double molarMass; + if (cut.hasMolarMass()) { + // Use explicit molar mass - no boiling point needed + molarMass = cut.resolveMolarMass(0.0, 0.0); + } else { + // Calculate molar mass from density and boiling point + double boilingPoint = cut.resolveAverageBoilingPoint(); + molarMass = cut.resolveMolarMass(density, boilingPoint); + } double moles = totalAssayMass * massFraction / molarMass; if (moles <= 0.0 || Double.isNaN(moles) || Double.isInfinite(moles)) { @@ -118,8 +126,7 @@ private double[] resolveMassFractions() { } if (specifiedMass > 1.0 + 1e-6) { - throw new IllegalStateException( - "Specified mass fractions exceed unity: " + specifiedMass); + throw new IllegalStateException("Specified mass fractions exceed unity: " + specifiedMass); } double remainingMass = Math.max(0.0, 1.0 - specifiedMass); @@ -271,6 +278,10 @@ public double getVolumeFraction() { return volumeFraction; } + public boolean hasMolarMass() { + return molarMass != null; + } + public double resolveDensity() { if (density != null) { return density; @@ -294,12 +305,13 @@ public double resolveMolarMass(double density, double boilingPointKelvin) { return molarMass; } if (!(density > 0.0) || !(boilingPointKelvin > 0.0)) { - throw new IllegalStateException("Cannot derive molar mass without density and boiling point"); + throw new IllegalStateException( + "Cannot derive molar mass without density and boiling point"); } double exponent = 2.3776; double densityExponent = 0.9371; - double molarMassKgPerMol = 5.805e-5 * Math.pow(boilingPointKelvin, exponent) - / Math.pow(density, densityExponent); + double molarMassKgPerMol = + 5.805e-5 * Math.pow(boilingPointKelvin, exponent) / Math.pow(density, densityExponent); return molarMassKgPerMol; } diff --git a/src/main/java/neqsim/thermo/system/SystemInterface.java b/src/main/java/neqsim/thermo/system/SystemInterface.java index dc04d17b92..a0cc8d92f8 100644 --- a/src/main/java/neqsim/thermo/system/SystemInterface.java +++ b/src/main/java/neqsim/thermo/system/SystemInterface.java @@ -5,7 +5,7 @@ import neqsim.physicalproperties.interfaceproperties.InterphasePropertiesInterface; import neqsim.physicalproperties.system.PhysicalPropertyModel; import neqsim.thermo.ThermodynamicConstantsInterface; -import neqsim.fluid.characterisation.OilAssayCharacterisation; +import neqsim.thermo.characterization.OilAssayCharacterisation; import neqsim.thermo.characterization.PseudoComponentCombiner; import neqsim.thermo.characterization.WaxModelInterface; import neqsim.thermo.component.ComponentInterface; @@ -1617,7 +1617,7 @@ public default int getPhaseNumberOfPhase(String phaseTypeName) { * getOilAssayCharacterisation. *

* - * @return a {@link neqsim.fluid.characterisation.OilAssayCharacterisation} object + * @return a {@link neqsim.thermo.characterization.OilAssayCharacterisation} object */ public OilAssayCharacterisation getOilAssayCharacterisation(); diff --git a/src/main/java/neqsim/thermo/system/SystemThermo.java b/src/main/java/neqsim/thermo/system/SystemThermo.java index 1c43bf17f3..28f5f7e2f9 100644 --- a/src/main/java/neqsim/thermo/system/SystemThermo.java +++ b/src/main/java/neqsim/thermo/system/SystemThermo.java @@ -18,6 +18,7 @@ import neqsim.thermo.ThermodynamicConstantsInterface; import neqsim.thermo.ThermodynamicModelSettings; import neqsim.thermo.characterization.Characterise; +import neqsim.thermo.characterization.OilAssayCharacterisation; import neqsim.thermo.characterization.WaxCharacterise; import neqsim.thermo.characterization.WaxModelInterface; import neqsim.thermo.component.ComponentInterface; @@ -34,7 +35,6 @@ import neqsim.util.database.NeqSimDataBase; import neqsim.util.exception.InvalidInputException; import neqsim.util.unit.Units; -import neqsim.fluid.characterisation.OilAssayCharacterisation; /** * This is the base class of the System classes. diff --git a/src/test/java/neqsim/fluid/characterisation/OilAssayCharacterisationTest.java b/src/test/java/neqsim/fluid/characterisation/OilAssayCharacterisationTest.java deleted file mode 100644 index 1de52b1d44..0000000000 --- a/src/test/java/neqsim/fluid/characterisation/OilAssayCharacterisationTest.java +++ /dev/null @@ -1,84 +0,0 @@ -package neqsim.fluid.characterisation; - -import static org.junit.jupiter.api.Assertions.assertEquals; -import static org.junit.jupiter.api.Assertions.assertFalse; -import static org.junit.jupiter.api.Assertions.assertNotNull; -import static org.junit.jupiter.api.Assertions.assertTrue; -import java.util.Arrays; -import org.junit.jupiter.api.Test; -import neqsim.fluid.characterisation.OilAssayCharacterisation.AssayCut; -import neqsim.thermo.component.ComponentInterface; -import neqsim.thermo.system.SystemInterface; -import neqsim.thermo.system.SystemSrkEos; - -public class OilAssayCharacterisationTest { - - @Test - public void testAssayApplicationAddsPseudoComponents() { - SystemInterface system = new SystemSrkEos(298.15, 10.0); - OilAssayCharacterisation characterisation = system.getOilAssayCharacterisation(); - characterisation.clearCuts(); - - AssayCut light = new AssayCut("Light").withWeightPercent(40.0).withDensity(0.75) - .withAverageBoilingPointCelsius(200.0); - AssayCut heavy = new AssayCut("Heavy").withVolumePercent(60.0).withApiGravity(25.0) - .withAverageBoilingPointCelsius(350.0); - - characterisation.addCut(light); - characterisation.addCut(heavy); - characterisation.apply(); - - double lightBoilingPoint = 200.0 + 273.15; - double expectedLightMolarMass = 5.805e-5 * Math.pow(lightBoilingPoint, 2.3776) - / Math.pow(0.75, 0.9371); - ComponentInterface lightComponent = system.getComponent("Light_PC"); - assertNotNull(lightComponent); - assertEquals(expectedLightMolarMass, lightComponent.getMolarMass(), 1e-8); - assertEquals(0.4 / expectedLightMolarMass, lightComponent.getNumberOfmoles(), 1e-8); - - double heavyDensity = 141.5 / (25.0 + 131.5) * 0.999016; - double heavyBoilingPoint = 350.0 + 273.15; - double expectedHeavyMolarMass = 5.805e-5 * Math.pow(heavyBoilingPoint, 2.3776) - / Math.pow(heavyDensity, 0.9371); - ComponentInterface heavyComponent = system.getComponent("Heavy_PC"); - assertNotNull(heavyComponent); - assertEquals(expectedHeavyMolarMass, heavyComponent.getMolarMass(), 1e-8); - assertEquals(0.6 / expectedHeavyMolarMass, heavyComponent.getNumberOfmoles(), 1e-8); - } - - @Test - public void testTotalMassScaling() { - SystemInterface system = new SystemSrkEos(298.15, 10.0); - OilAssayCharacterisation characterisation = system.getOilAssayCharacterisation(); - characterisation.clearCuts(); - - AssayCut cut = new AssayCut("Assay").withMassFraction(1.0).withDensity(0.82) - .withAverageBoilingPointCelsius(300.0); - characterisation.addCut(cut); - characterisation.setTotalAssayMass(2.0); - characterisation.apply(); - - double boilingPoint = 300.0 + 273.15; - double expectedMolarMass = 5.805e-5 * Math.pow(boilingPoint, 2.3776) / Math.pow(0.82, 0.9371); - ComponentInterface component = system.getComponent("Assay_PC"); - assertNotNull(component); - assertEquals(2.0 / expectedMolarMass, component.getNumberOfmoles(), 1e-8); - } - - @Test - public void testCharacterisationClonedWithSystem() { - SystemInterface system = new SystemSrkEos(298.15, 10.0); - OilAssayCharacterisation original = system.getOilAssayCharacterisation(); - original.clearCuts(); - original.addCut(new AssayCut("CloneTest").withMassFraction(1.0).withDensity(0.85) - .withAverageBoilingPointCelsius(310.0)); - - SystemInterface cloned = system.clone(); - OilAssayCharacterisation cloneCharacterisation = cloned.getOilAssayCharacterisation(); - assertTrue(cloneCharacterisation.getCuts().size() == 1); - cloneCharacterisation.apply(); - - assertTrue(Arrays.asList(cloned.getComponentNames()).contains("CloneTest_PC")); - assertFalse(Arrays.asList(system.getComponentNames()).contains("CloneTest_PC")); - } -} diff --git a/src/test/java/neqsim/thermo/characterization/OilAssayCharacterisationTest.java b/src/test/java/neqsim/thermo/characterization/OilAssayCharacterisationTest.java new file mode 100644 index 0000000000..9af490b308 --- /dev/null +++ b/src/test/java/neqsim/thermo/characterization/OilAssayCharacterisationTest.java @@ -0,0 +1,159 @@ +package neqsim.thermo.characterization; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertFalse; +import static org.junit.jupiter.api.Assertions.assertNotNull; +import static org.junit.jupiter.api.Assertions.assertTrue; +import java.util.Arrays; +import org.junit.jupiter.api.Test; +import neqsim.thermo.characterization.OilAssayCharacterisation.AssayCut; +import neqsim.thermo.component.ComponentInterface; +import neqsim.thermo.system.SystemInterface; +import neqsim.thermo.system.SystemSrkEos; + +public class OilAssayCharacterisationTest { + + @Test + public void testAssayApplicationAddsPseudoComponents() { + SystemInterface system = new SystemSrkEos(298.15, 10.0); + OilAssayCharacterisation characterisation = system.getOilAssayCharacterisation(); + characterisation.clearCuts(); + + AssayCut light = new AssayCut("Light").withWeightPercent(40.0).withDensity(0.75) + .withAverageBoilingPointCelsius(200.0); + AssayCut heavy = new AssayCut("Heavy").withVolumePercent(60.0).withApiGravity(25.0) + .withAverageBoilingPointCelsius(350.0); + + characterisation.addCut(light); + characterisation.addCut(heavy); + characterisation.apply(); + + double lightBoilingPoint = 200.0 + 273.15; + double expectedLightMolarMass = + 5.805e-5 * Math.pow(lightBoilingPoint, 2.3776) / Math.pow(0.75, 0.9371); + ComponentInterface lightComponent = system.getComponent("Light_PC"); + assertNotNull(lightComponent); + assertEquals(expectedLightMolarMass, lightComponent.getMolarMass(), 1e-8); + assertEquals(0.4 / expectedLightMolarMass, lightComponent.getNumberOfmoles(), 1e-8); + + double heavyDensity = 141.5 / (25.0 + 131.5) * 0.999016; + double heavyBoilingPoint = 350.0 + 273.15; + double expectedHeavyMolarMass = + 5.805e-5 * Math.pow(heavyBoilingPoint, 2.3776) / Math.pow(heavyDensity, 0.9371); + ComponentInterface heavyComponent = system.getComponent("Heavy_PC"); + assertNotNull(heavyComponent); + assertEquals(expectedHeavyMolarMass, heavyComponent.getMolarMass(), 1e-8); + assertEquals(0.6 / expectedHeavyMolarMass, heavyComponent.getNumberOfmoles(), 1e-8); + } + + @Test + public void testTotalMassScaling() { + SystemInterface system = new SystemSrkEos(298.15, 10.0); + OilAssayCharacterisation characterisation = system.getOilAssayCharacterisation(); + characterisation.clearCuts(); + + AssayCut cut = new AssayCut("Assay").withMassFraction(1.0).withDensity(0.82) + .withAverageBoilingPointCelsius(300.0); + characterisation.addCut(cut); + characterisation.setTotalAssayMass(2.0); + characterisation.apply(); + + double boilingPoint = 300.0 + 273.15; + double expectedMolarMass = 5.805e-5 * Math.pow(boilingPoint, 2.3776) / Math.pow(0.82, 0.9371); + ComponentInterface component = system.getComponent("Assay_PC"); + assertNotNull(component); + assertEquals(2.0 / expectedMolarMass, component.getNumberOfmoles(), 1e-8); + } + + @Test + public void testCharacterisationClonedWithSystem() { + SystemInterface system = new SystemSrkEos(298.15, 10.0); + OilAssayCharacterisation original = system.getOilAssayCharacterisation(); + original.clearCuts(); + original.addCut(new AssayCut("CloneTest").withMassFraction(1.0).withDensity(0.85) + .withAverageBoilingPointCelsius(310.0)); + + SystemInterface cloned = system.clone(); + OilAssayCharacterisation cloneCharacterisation = cloned.getOilAssayCharacterisation(); + assertTrue(cloneCharacterisation.getCuts().size() == 1); + cloneCharacterisation.apply(); + + assertTrue(Arrays.asList(cloned.getComponentNames()).contains("CloneTest_PC")); + assertFalse(Arrays.asList(system.getComponentNames()).contains("CloneTest_PC")); + } + + @Test + public void testExplicitMolarMassSkipsBoilingPointRequirement() { + SystemInterface system = new SystemSrkEos(298.15, 10.0); + OilAssayCharacterisation characterisation = system.getOilAssayCharacterisation(); + characterisation.clearCuts(); + + // Create a cut with explicit molar mass but no boiling point + AssayCut cut = + new AssayCut("ExplicitMW").withMassFraction(1.0).withDensity(0.8).withMolarMass(150.0); // Explicit + // molar + // mass, + // no + // boiling + // point + + characterisation.addCut(cut); + characterisation.apply(); // Should not throw exception + + ComponentInterface component = system.getComponent("ExplicitMW_PC"); + assertNotNull(component); + assertEquals(150.0, component.getMolarMass(), 1e-8); + assertEquals(1.0 / 150.0, component.getNumberOfmoles(), 1e-8); + } + + @Test + public void testCalculatedMolarMassStillRequiresBoilingPoint() { + SystemInterface system = new SystemSrkEos(298.15, 10.0); + OilAssayCharacterisation characterisation = system.getOilAssayCharacterisation(); + characterisation.clearCuts(); + + // Create a cut without explicit molar mass or boiling point - should fail + AssayCut cut = new AssayCut("NoBoilingPoint").withMassFraction(1.0).withDensity(0.8); + // No molar mass, no boiling point + + characterisation.addCut(cut); + + // This should throw an exception because boiling point is required for molar mass calculation + try { + characterisation.apply(); + assertTrue(false, "Expected IllegalStateException for missing boiling point"); + } catch (IllegalStateException e) { + assertTrue(e.getMessage().contains("Average boiling point missing")); + } + } + + @Test + public void testMixedExplicitAndCalculatedMolarMass() { + SystemInterface system = new SystemSrkEos(298.15, 10.0); + OilAssayCharacterisation characterisation = system.getOilAssayCharacterisation(); + characterisation.clearCuts(); + + // Cut with explicit molar mass (no boiling point needed) + AssayCut explicitCut = + new AssayCut("Explicit").withMassFraction(0.5).withDensity(0.8).withMolarMass(120.0); + + // Cut with calculated molar mass (requires boiling point) + AssayCut calculatedCut = new AssayCut("Calculated").withMassFraction(0.5).withDensity(0.85) + .withAverageBoilingPointCelsius(250.0); + + characterisation.addCut(explicitCut); + characterisation.addCut(calculatedCut); + characterisation.apply(); + + // Verify explicit molar mass component + ComponentInterface explicitComponent = system.getComponent("Explicit_PC"); + assertNotNull(explicitComponent); + assertEquals(120.0, explicitComponent.getMolarMass(), 1e-8); + + // Verify calculated molar mass component + ComponentInterface calculatedComponent = system.getComponent("Calculated_PC"); + assertNotNull(calculatedComponent); + // The calculated molar mass should be different from 120.0 + assertTrue(Math.abs(calculatedComponent.getMolarMass() - 120.0) > 1.0); + } +}