Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 6 additions & 0 deletions docs/thermo/hydrate_models.md
Original file line number Diff line number Diff line change
Expand Up @@ -160,6 +160,12 @@ whitelist. This keeps ions out of gas and oil while satisfying the component bal
$z_i = \sum_p \beta_p x_{i,p}$. Salt and organic inhibitor effects enter the hydrate calculation through the water
fugacity of the converged aqueous phase.

The concentrated CO2/brine/MEG consumer regression uses the shared fluid-equilibrium
solver, with MEG partitioning as a molecular component and salt ions confined to
the aqueous phase. See [CO2/brine phase-state diagnostics](../thermodynamicoperations/hydrate_flash_operations.md#co2brine-phase-state-diagnostics)
for the 44 bara recipe, the two concentration bases, and the distinction between
numerical regression values and experimental validation.

CPA initialization excludes association sites only for components whose overall mole fraction
is at or below the numerical trace cutoff of $10^{-20}$. It uses the current component and total
mole counts before resetting phase compositions. A component depleted in a previous phase split
Expand Down
17 changes: 17 additions & 0 deletions docs/thermodynamicoperations/hydrate_flash_operations.md
Original file line number Diff line number Diff line change
Expand Up @@ -218,6 +218,23 @@ solid/wax models and the Pitzer water-activity model retain their physical scope
`CO2BrinePhaseEquilibrium` and `ReactiveCO2BrinePhaseEquilibrium` remain deprecated
compatibility adapters; new code should use the ordinary operation API.

The 44 bara CO2/brine/MEG regression uses 10 mol CO2 and 1 kg water, with
10 wt% NaCl on water-plus-salt and 5 wt% MEG on water-plus-MEG. Its numerical
reference is approximately 1.903 °C with a conserved CO2-rich/aqueous split.
If the percentages instead mean kilograms per 100 kg water, the smaller salt
and MEG inventories give a numerical reference of approximately 2.708 °C.
The 5 wt% NaCl plus 5 wt% MEG control gives approximately 5.433 °C.
These are numerical regression results, not experimental validation.

`CO2BrineMegHydrateTest` checks both concentration bases, independent temperature
guesses, adjacent and dense-fluid pressures, finite CO2 inventory, repeated and
cloned calculations, and the MEG inhibition trend. The shared fluid solver must
conserve every supplied species and equilibrate CO2, water and MEG fugacities.
MEG remains a partitioning molecular component; only ions are confined to the
aqueous phase. These consumer cases introduce no component whitelist, solvent
ratio dispatch flag, fitted EOS/hydrate parameter or empirical temperature offset.
Ice and salt precipitation are not included in these regressions.

Shared-EOS reactive ionic fluids use the common aqueous reaction coupling; EOS-GE models retain
their model-owned phase-role and reaction strategy. Add the input species,
call `chemicalReactionInit()`, then select the mixing rule and hydrate check.
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,164 @@
package neqsim.thermodynamicoperations.flashops.saturationops;

import static org.junit.jupiter.api.Assertions.assertEquals;
import static org.junit.jupiter.api.Assertions.assertFalse;
import static org.junit.jupiter.api.Assertions.assertTrue;
import org.junit.jupiter.api.Tag;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.params.ParameterizedTest;
import org.junit.jupiter.params.provider.CsvSource;
import neqsim.thermo.system.SystemInterface;
import neqsim.thermodynamicoperations.ThermodynamicOperations;

/**
* Conserved CO2/brine/MEG equilibrium regressions for the 44 bara NaCl failure.
*
* @author Even Solbraa
* @version 1.0
*/
class CO2BrineMegHydrateTest {
/**
* Reproduces the reported endpoint and distinguishes the two concentration bases.
*
* @param waterBasis whether dosage is specified per mass of water
* @param expectedTemperature expected numerical regression temperature in Celsius
* @throws Exception if the hydrate operation fails
*/
@ParameterizedTest
@CsvSource({"false,1.90296", "true,2.70828"})
void reportedNaClMegCaseConservesTheOriginalFeed(boolean waterBasis, double expectedTemperature) throws Exception {
SystemInterface fluid = mixture(44.0, 10.0, 5.0, 10.0, waterBasis);
HydrateEquilibriumDiagnostics evidence = solveAndAudit(fluid, Double.NaN);
assertTrue(evidence.isSaturatedCO2Boundary());
assertEquals(expectedTemperature, fluid.getTemperature("C"), 0.005);
assertEquals(2, fluid.getNumberOfPhases());
}

/**
* Preserves the passing five-percent salt control and the inhibition trend.
*
* @throws Exception if the hydrate operation fails
*/
@Test
void megLowersTheTemperatureWithoutChangingTheFivePercentControl() throws Exception {
SystemInterface control = mixture(44.0, 5.0, 5.0, 10.0, false);
solveAndAudit(control, Double.NaN);
assertEquals(5.43259, control.getTemperature("C"), 0.005);
double previous = Double.POSITIVE_INFINITY;
for (double meg : new double[] {0.0, 5.0, 10.0, 20.0}) {
SystemInterface fluid = mixture(44.0, 10.0, meg, 10.0, false);
assertTrue(solveAndAudit(fluid, Double.NaN).isSaturatedCO2Boundary());
assertTrue(fluid.getTemperature() < previous, "Increasing MEG must lower the verified boundary");
previous = fluid.getTemperature();
}
}

/**
* Checks adjacent pressures, alternative seeds and gas/liquid CO2 trials.
*
* @param pressure pressure in bara
* @param meg MEG mass percent on water plus MEG
* @throws Exception if the hydrate operation fails
*/
@Tag("slow")
@ParameterizedTest
@CsvSource({"40,5", "44,5", "48,5", "50,10", "100,5", "200,5", "44,20"})
void independentSeedsAndNearbyStatesCloseAllMolecularFugacities(double pressure, double meg) throws Exception {
double reference = Double.NaN;
for (double guess : new double[] {268.15, 278.15, 288.15}) {
SystemInterface fluid = mixture(pressure, 10.0, meg, 10.0, false);
assertTrue(solveAndAudit(fluid, guess).isSaturatedCO2Boundary());
if (Double.isFinite(reference)) {
assertEquals(reference, fluid.getTemperature(), 2.0e-4);
}
reference = fluid.getTemperature();
}
}

/**
* Does not create a free CO2 phase for an undersaturated feed or lose MEG on repeated runs.
*
* @throws Exception if the hydrate operation fails
*/
@Tag("slow")
@Test
void finiteInventoryAndRepeatedRunsRetainTheSuppliedSpecies() throws Exception {
SystemInterface limited = mixture(100.0, 10.0, 5.0, 0.5, false);
HydrateEquilibriumDiagnostics finite = solveAndAudit(limited, 273.15);
assertFalse(finite.hasCO2RichPhase());
assertFalse(finite.isSaturatedCO2Boundary());
assertEquals(1, limited.getNumberOfPhases());
SystemInterface fluid = mixture(44.0, 10.0, 5.0, 10.0, false);
solveAndAudit(fluid, Double.NaN);
double temperature = fluid.getTemperature();
solveAndAudit(fluid, 283.15);
assertEquals(temperature, fluid.getTemperature(), 2.0e-4);
SystemInterface copy = fluid.clone();
solveAndAudit(copy, 273.15);
assertEquals(temperature, copy.getTemperature(), 2.0e-4);
}

/**
* Constructs a one-kilogram-water NaCl/MEG recipe without adding CO2 during solution.
*
* @param pressure pressure in bara
* @param salt salt percent on water plus salt, or on water alone when waterBasis is true
* @param meg MEG percent on water plus MEG, or on water alone when waterBasis is true
* @param co2 molar CO2 inventory
* @param waterBasis whether the percentages are dosages per mass of water
* @return configured non-reactive electrolyte CPA mixture
*/
private static SystemInterface mixture(double pressure, double salt, double meg, double co2, boolean waterBasis) {
double saltOnBrine = waterBasis ? 100.0 * salt / (100.0 + salt) : salt;
SystemInterface fluid = CO2BrineHydratePhaseStateTest.brine(pressure, saltOnBrine, co2);
if (meg > 0.0) {
double megMass = waterBasis ? meg / 100.0 : meg / (100.0 - meg);
fluid.addComponent("MEG", megMass / 0.06206784);
fluid.setMixingRule(10);
}
return fluid;
}

/**
* Solves the endpoint and checks original species, phase balances and every molecular fugacity.
*
* @param fluid fluid to solve and audit
* @param guess temperature guess in kelvin, or NaN for the default policy
* @return immutable diagnostic evidence
* @throws Exception if the hydrate operation fails
*/
private static HydrateEquilibriumDiagnostics solveAndAudit(SystemInterface fluid, double guess) throws Exception {
double[] input = new double[fluid.getNumberOfComponents()];
for (int component = 0; component < input.length; component++) {
input[component] = fluid.getPhase(0).getComponent(component).getNumberOfmoles();
}
ThermodynamicOperations operations = new ThermodynamicOperations(fluid);
if (Double.isFinite(guess)) {
operations.hydrateFormationTemperature(guess);
} else {
operations.hydrateFormationTemperature();
}
HydrateFormationTemperatureFlash flash = (HydrateFormationTemperatureFlash) operations.getOperation();
assertTrue(flash.isConverged());
HydrateEquilibriumDiagnostics evidence = flash.getDiagnostics();
assertTrue(evidence.isConverged());
assertTrue(evidence.hasAqueousPhase());
assertTrue(Double.isFinite(evidence.getMinimumFluidTrialDistance()));
assertTrue(evidence.getComponentBalanceResidual() < 1.0e-10);
assertTrue(evidence.getChargeResidual() < 1.0e-10);
CO2BrineHydratePhaseStateTest.assertEndpoint(fluid, evidence.hasCO2RichPhase());
for (int component = 0; component < input.length; component++) {
double recovered = 0.0;
for (int phase = 0; phase < fluid.getNumberOfPhases(); phase++) {
recovered += fluid.getTotalNumberOfMoles() * fluid.getBeta(phase)
* fluid.getPhase(phase).getComponent(component).getx();
}
assertEquals(input[component], recovered, 1.0e-8);
if (fluid.getNumberOfPhases() == 2 && fluid.getPhase(0).getComponent(component).getIonicCharge() == 0.0) {
double ratio = fluid.getPhase(0).getFugacity(component) / fluid.getPhase(1).getFugacity(component);
assertEquals(0.0, Math.log(ratio), 1.0e-8, "CO2, water and MEG must each equilibrate");
}
}
return evidence;
}
}
Loading