diff --git a/.github/copilot-instructions.md b/.github/copilot-instructions.md index 9f3cc4188a..5ab246b134 100644 --- a/.github/copilot-instructions.md +++ b/.github/copilot-instructions.md @@ -200,7 +200,7 @@ When writing documentation that includes Java or Python code examples: - **Style & Formatting**: Java code follows Google style with project overrides from `.config/checkstyle_neqsim.xml` and formatter profiles (`.config/neqsim_formatter.xml`); keep indentation at two spaces and respect existing comment minimalism. - **Code Formatting (Spotless) - MANDATORY**: AI-generated Java is NOT auto-formatted. After creating or editing ANY `.java` file, run `./mvnw spotless:apply` (Windows: `mvnw.cmd spotless:apply`) to reformat to the project style, then `git add` the changes before committing. CI runs `./mvnw spotless:check` and FAILS the build on any unformatted file. Do not rely on local pre-commit hooks being installed, and NEVER bypass the gate with `git commit --no-verify`. - **Serialization & Copying**: Many equipment classes rely on Java serialization (`ProcessEquipmentBaseClass.copy()`); avoid introducing non-serializable fields or mark them `transient` to preserve cloning. SpotBugs enforces this via the SE_BAD_FIELD rule. When adding fields to any `Serializable` class (equipment, measurement devices, mechanical design, thermo phases), use the correct modifier order: `private transient Type field;` or `private final transient Type field;`. Common non-serializable types that need `transient`: `Function`, `BiConsumer`, `Consumer`, `Thread`, JDBC `Connection`/`Statement`, Apache Commons Math interpolators, and any inner class that doesn't implement `Serializable`. The `ProcessLogic` interface extends `Serializable`. -- **External Dependencies**: Core math depends on EJML, Commons Math, JAMA, and MTJ; check numerical stability when swapping linear algebra routines, and keep JSON/YAML handling aligned with gson/jackson versions pinned in pom.xml. +- **External Dependencies**: Core math depends on ojAlgo, Commons Math, JAMA, and MTJ; check numerical stability when swapping linear algebra routines, and keep JSON/YAML handling aligned with gson/jackson versions pinned in pom.xml. - **Java 8 Compatibility (MANDATORY)**: See the critical section at the top of this document. All code MUST compile with Java 8. The CI build will FAIL if you use Java 9+ features like `String.repeat()`, `var`, `List.of()`, etc. - **Sample Flow**: diff --git a/.github/workflows/publish_to_maven_central.yml b/.github/workflows/publish_to_maven_central.yml index c5d08552d6..d69edfef41 100644 --- a/.github/workflows/publish_to_maven_central.yml +++ b/.github/workflows/publish_to_maven_central.yml @@ -40,11 +40,6 @@ jobs: gpg-private-key: ${{ secrets.GPG_SIGNING_KEY }} gpg-passphrase: MAVEN_GPG_PASSPHRASE cache: 'maven' - # Clean Maven cache for critical dependencies to avoid stale class issues - - name: Clean dependency cache for EJML - run: | - rm -rf ~/.m2/repository/org/ejml - echo "Cleaned EJML from Maven cache to ensure fresh download" # --- Java 8 artifact --- - name: Publish package (Java 8) run: ./mvnw -f pomJava8.xml -P release --batch-mode clean deploy -Dmaven.deploy.skip=true -DskipTests -Djacoco.skip=true -Drevision=${{ steps.release_version.outputs.version }} -ntp diff --git a/pom.xml b/pom.xml index 57053774ba..e9cdbbdc33 100644 --- a/pom.xml +++ b/pom.xml @@ -102,9 +102,9 @@ 3.6.1 - org.ejml - ejml-all - 0.45.1 + org.ojalgo + ojalgo + 52.0.0 gov.nist.math diff --git a/pomJava8.xml b/pomJava8.xml index 70510769b2..c7ea3c0e8b 100644 --- a/pomJava8.xml +++ b/pomJava8.xml @@ -105,9 +105,9 @@ 3.6.1 - org.ejml - ejml-all - 0.41 + org.ojalgo + ojalgo + 52.0.0 gov.nist.math diff --git a/src/main/java/neqsim/chemicalreactions/chemicalequilibrium/ChemicalEquilibrium.java b/src/main/java/neqsim/chemicalreactions/chemicalequilibrium/ChemicalEquilibrium.java index 803bead3ae..29fc61e497 100644 --- a/src/main/java/neqsim/chemicalreactions/chemicalequilibrium/ChemicalEquilibrium.java +++ b/src/main/java/neqsim/chemicalreactions/chemicalequilibrium/ChemicalEquilibrium.java @@ -6,6 +6,7 @@ import neqsim.thermo.ThermodynamicConstantsInterface; import neqsim.thermo.component.ComponentInterface; import neqsim.thermo.system.SystemInterface; +import neqsim.util.math.LinearAlgebraOps; /** *

@@ -32,34 +33,36 @@ public class ChemicalEquilibrium implements java.io.Serializable { private static final int STAGNATION_LIMIT = 10; /** - * Iteration threshold to switch from simple to derivative-based iterations. First iterations use simple M_matrix - * (cheap), then switch to derivatives for faster quadratic convergence near the solution. + * Iteration threshold to switch from simple to derivative-based iterations. First iterations use + * simple M_matrix (cheap), then switch to derivatives for faster quadratic convergence near the + * solution. */ private static final int DERIVATIVE_SWITCH_ITERATION = 5; /** - * Error threshold to switch to derivative-based iterations. When relative error falls below this, use derivatives for - * faster convergence. + * Error threshold to switch to derivative-based iterations. When relative error falls below this, + * use derivatives for faster convergence. */ private static final double DERIVATIVE_SWITCH_ERROR = 1e-3; /** - * Flag to enable fugacity coefficient derivatives in M_matrix. When true, uses init(3) for derivative calculations - * and includes dln(fugacity)/dN terms for more accurate Newton steps. Default is false for backward compatibility and - * performance. + * Flag to enable fugacity coefficient derivatives in M_matrix. When true, uses init(3) for + * derivative calculations and includes dln(fugacity)/dN terms for more accurate Newton steps. + * Default is false for backward compatibility and performance. */ private boolean useFugacityDerivatives = false; /** - * Flag to enable automatic switching to derivatives after initial iterations. When true, starts with simple - * iterations then switches to derivatives for faster convergence. + * Flag to enable automatic switching to derivatives after initial iterations. When true, starts + * with simple iterations then switches to derivatives for faster convergence. */ private boolean useAdaptiveDerivatives = false; /** - * Flag to enable the full Smith-Missen M-matrix with the -1/n_t coupling term. When true, uses M_ij = δ_ij/n_i - - * 1/n_t instead of the simplified M_ij = δ_ij/n_i. The full form provides better quadratic convergence near the - * solution but may be less stable for some systems. Default is false for backward compatibility and stability. + * Flag to enable the full Smith-Missen M-matrix with the -1/n_t coupling term. When true, uses + * M_ij = δ_ij/n_i - 1/n_t instead of the simplified M_ij = δ_ij/n_i. The full form provides + * better quadratic convergence near the solution but may be less stable for some systems. Default + * is false for backward compatibility and stability. */ private boolean useFullMMatrix = false; @@ -160,8 +163,8 @@ public ChemicalEquilibrium(double[][] A_matrix, double[] b_element, SystemInterf for (int i = 0; i < components.length; i++) { if (components[i].getComponentName().equals("water")) { - waterNumb = i; - break; + waterNumb = i; + break; } } system.init(1, phasenumb); @@ -179,13 +182,14 @@ public ChemicalEquilibrium(double[][] A_matrix, double[] b_element, SystemInterf public void calcRefPot() { for (int i = 0; i < components.length; i++) { // calculates the reduced chemical potential mu/RT - this.chem_ref[i] = components[i].getReferencePotential() / (R * system.getPhase(phasenumb).getTemperature()); + this.chem_ref[i] = + components[i].getReferencePotential() / (R * system.getPhase(phasenumb).getTemperature()); logactivityVec[i] = 0.0; if (components[i].calcActivity()) { - logactivityVec[i] = system.getPhase(phasenumb).getLogActivityCoefficient(components[i].getComponentNumber(), - components[waterNumb].getComponentNumber()); - // System.out.println("activity " + Math.exp(logactivityVec[i]) + " " + - // components[i].getComponentName()); + logactivityVec[i] = system.getPhase(phasenumb).getLogActivityCoefficient( + components[i].getComponentNumber(), components[waterNumb].getComponentNumber()); + // System.out.println("activity " + Math.exp(logactivityVec[i]) + " " + + // components[i].getComponentName()); } } } @@ -202,62 +206,63 @@ public void chemSolve() { // If using fugacity derivatives, need init(3) for derivative calculations if (useFugacityDerivatives) { try { - system.init(3, phasenumb); + system.init(3, phasenumb); } catch (Exception ex) { - logger.debug("Failed to init(3) for derivatives, falling back to simple M_matrix"); + logger.debug("Failed to init(3) for derivatives, falling back to simple M_matrix"); } } for (int i = 0; i < NSPEC; i++) { n_mol[i] = system.getPhase(phasenumb).getComponents()[components[i].getComponentNumber()] - .getNumberOfMolesInPhase(); + .getNumberOfMolesInPhase(); for (int k = 0; k < NSPEC; k++) { - if (k == i) { - kronDelt = 1.0; - } else { - kronDelt = 0.0; - } - // M_matrix definition: M_ij = δ_ij/n_i for the simplified ideal case - // The full Smith-Missen formulation uses M_ij = δ_ij/n_i - 1/n_t - // The -1/n_t term couples all species through total moles and improves convergence - // near the solution, but may be less stable for some systems. - // Protect against division by zero using MIN_MOLES - double molesForDiv = Math.max(MIN_MOLES, - system.getPhase(phasenumb).getComponents()[components[i].getComponentNumber()].getNumberOfMolesInPhase()); - M_matrix[i][k] = kronDelt / molesForDiv; - - // Add the -1/n_t coupling term if full M-matrix is enabled - if (useFullMMatrix) { - M_matrix[i][k] -= 1.0 / Math.max(MIN_MOLES, n_t); - } - - // Add fugacity coefficient derivative if enabled - // dfugdn contains: δ_ij/n_i - 1/n_t + ∂ln(φ_i)/∂n_j - // The original M_matrix is just δ_ij/n_i (ideal, no mixing term) - // To add non-ideal effects, we need ∂ln(φ_i)/∂n_j = dfugdn - δ_ij/n_i + 1/n_t - if (useFugacityDerivatives) { - try { - int compNumI = components[i].getComponentNumber(); - int compNumK = components[k].getComponentNumber(); - double dfugdN = system.getPhase(phasenumb).getComponent(compNumI).getdfugdn(compNumK); - if (!Double.isNaN(dfugdN) && !Double.isInfinite(dfugdN)) { - // Extract just the non-ideal part: ∂ln(φ)/∂n = dfugdn - δ/n + 1/n_t - double idealPart = (i == k ? 1.0 / molesForDiv : 0.0) - 1.0 / n_t; - double nonIdealPart = dfugdN - idealPart; - M_matrix[i][k] += nonIdealPart; - } - } catch (Exception ex) { - // Derivative not available, use ideal term only - } - } - - // System.out.println("dfugdn " - // +system.getPhase(phasenumb).getComponent(i).logfugcoefdNi(this.system.getPhase(phasenumb), - // i)); - // if (i == k) System.out.println("n " - // +system.getPhase(phasenumb).getComponents()[components[i].getComponentNumber()].getNumberOfMolesInPhase() - // ); + if (k == i) { + kronDelt = 1.0; + } else { + kronDelt = 0.0; + } + // M_matrix definition: M_ij = δ_ij/n_i for the simplified ideal case + // The full Smith-Missen formulation uses M_ij = δ_ij/n_i - 1/n_t + // The -1/n_t term couples all species through total moles and improves convergence + // near the solution, but may be less stable for some systems. + // Protect against division by zero using MIN_MOLES + double molesForDiv = Math.max(MIN_MOLES, + system.getPhase(phasenumb).getComponents()[components[i].getComponentNumber()] + .getNumberOfMolesInPhase()); + M_matrix[i][k] = kronDelt / molesForDiv; + + // Add the -1/n_t coupling term if full M-matrix is enabled + if (useFullMMatrix) { + M_matrix[i][k] -= 1.0 / Math.max(MIN_MOLES, n_t); + } + + // Add fugacity coefficient derivative if enabled + // dfugdn contains: δ_ij/n_i - 1/n_t + ∂ln(φ_i)/∂n_j + // The original M_matrix is just δ_ij/n_i (ideal, no mixing term) + // To add non-ideal effects, we need ∂ln(φ_i)/∂n_j = dfugdn - δ_ij/n_i + 1/n_t + if (useFugacityDerivatives) { + try { + int compNumI = components[i].getComponentNumber(); + int compNumK = components[k].getComponentNumber(); + double dfugdN = system.getPhase(phasenumb).getComponent(compNumI).getdfugdn(compNumK); + if (!Double.isNaN(dfugdN) && !Double.isInfinite(dfugdN)) { + // Extract just the non-ideal part: ∂ln(φ)/∂n = dfugdn - δ/n + 1/n_t + double idealPart = (i == k ? 1.0 / molesForDiv : 0.0) - 1.0 / n_t; + double nonIdealPart = dfugdN - idealPart; + M_matrix[i][k] += nonIdealPart; + } + } catch (Exception ex) { + // Derivative not available, use ideal term only + } + } + + // System.out.println("dfugdn " + // +system.getPhase(phasenumb).getComponent(i).logfugcoefdNi(this.system.getPhase(phasenumb), + // i)); + // if (i == k) System.out.println("n " + // +system.getPhase(phasenumb).getComponents()[components[i].getComponentNumber()].getNumberOfMolesInPhase() + // ); } } // printComp(); @@ -280,7 +285,8 @@ public void chemSolve() { // calculates the reduced chemical potential mu/RT // Protect against log(0) by ensuring minimum moles double molesInPhase = Math.max(MIN_MOLES, - system.getPhase(phasenumb).getComponents()[components[i].getComponentNumber()].getNumberOfMolesInPhase()); + system.getPhase(phasenumb).getComponents()[components[i].getComponentNumber()] + .getNumberOfMolesInPhase()); chem_pot[i] = chem_ref[i] + Math.log(molesInPhase) - Math.log(n_t) + logactivity; // System.out.println("chem ref pot " + chem_pot[i]); } @@ -293,7 +299,8 @@ public void chemSolve() { try { M_inv_AT = M_Jama_matrix.solve(A_Jama_matrix.transpose()); } catch (Exception e) { - M_inv_AT = solveLeastSquares(M_Jama_matrix, A_Jama_matrix.transpose()); + M_inv_AT = new Matrix(LinearAlgebraOps.solveLeastSquares(M_Jama_matrix.getArrayCopy(), + A_Jama_matrix.transpose().getArrayCopy())); } AMA_matrix = A_Jama_matrix.times(M_inv_AT); // Similarly for AMU: M * Y = mu^T -> Y = M.solve(mu^T), then AMU = A * Y @@ -301,7 +308,8 @@ public void chemSolve() { try { M_inv_mu = M_Jama_matrix.solve(chem_pot_Jama_Matrix.transpose()); } catch (Exception e) { - M_inv_mu = solveLeastSquares(M_Jama_matrix, chem_pot_Jama_Matrix.transpose()); + M_inv_mu = new Matrix(LinearAlgebraOps.solveLeastSquares(M_Jama_matrix.getArrayCopy(), + chem_pot_Jama_Matrix.transpose().getArrayCopy())); } AMU_matrix = A_Jama_matrix.times(M_inv_mu); Matrix nmol = new Matrix(n_mol, 1); @@ -351,23 +359,25 @@ public void chemSolve() { // Check if solution is valid (no NaN or Inf) boolean validSolution = true; for (int i = 0; i <= NELE && validSolution; i++) { - double val = x_solve.get(i, 0); - if (Double.isNaN(val) || Double.isInfinite(val)) { - validSolution = false; - } + double val = x_solve.get(i, 0); + if (Double.isNaN(val) || Double.isInfinite(val)) { + validSolution = false; + } } if (!validSolution) { - // Try pseudo-inverse for numerically unstable cases - x_solve = solveLeastSquares(A_solve, b_solve); + // Try pseudo-inverse for numerically unstable cases + x_solve = new Matrix( + LinearAlgebraOps.solveLeastSquares(A_solve.getArrayCopy(), b_solve.getArrayCopy())); } } catch (Exception ex) { // Matrix is singular or near-singular, use pseudo-inverse try { - x_solve = solveLeastSquares(A_solve, b_solve); + x_solve = new Matrix( + LinearAlgebraOps.solveLeastSquares(A_solve.getArrayCopy(), b_solve.getArrayCopy())); } catch (Exception ex2) { - logger.error("Both regular and least-squares solve failed: " + ex2.getMessage()); - x_solve = new Matrix(NELE + 1, 1); // Zero solution as fallback + logger.error("Both regular and least-squares solve failed: " + ex2.getMessage()); + x_solve = new Matrix(NELE + 1, 1); // Zero solution as fallback } } // d_n_t = x_solve.get(NELE,0)*n_t; @@ -380,7 +390,8 @@ public void chemSolve() { try { M_inv_rhs = M_Jama_matrix.solve(rhs); } catch (Exception e) { - M_inv_rhs = solveLeastSquares(M_Jama_matrix, rhs); + M_inv_rhs = new Matrix( + LinearAlgebraOps.solveLeastSquares(M_Jama_matrix.getArrayCopy(), rhs.getArrayCopy())); } dn_matrix = M_inv_rhs.plus(new Matrix(n_mol, 1).transpose().times(x_solve.get(NELE, 0))); d_n = dn_matrix.transpose().getArray()[0]; @@ -392,23 +403,24 @@ public void chemSolve() { *

* *

- * Updates the moles in the reactive phase based on the calculated n_mol values from the Newton solver. Uses - * Phase.addMolesChemReac with totdn=0 to only affect phase moles without corrupting the total system moles (which - * would violate element conservation). + * Updates the moles in the reactive phase based on the calculated n_mol values from the Newton + * solver. Uses Phase.addMolesChemReac with totdn=0 to only affect phase moles without corrupting + * the total system moles (which would violate element conservation). *

*/ public void updateMoles() { upMoles++; for (int i = 0; i < components.length; i++) { int compNum = components[i].getComponentNumber(); - double currentMoles = system.getPhase(phasenumb).getComponents()[compNum].getNumberOfMolesInPhase(); + double currentMoles = + system.getPhase(phasenumb).getComponents()[compNum].getNumberOfMolesInPhase(); double targetMoles; if (n_mol[i] > MIN_MOLES) { - targetMoles = n_mol[i]; + targetMoles = n_mol[i]; } else { - // Use MIN_MOLES to maintain element balance while avoiding zero/negative moles - // This is more consistent than arbitrary scaling which violates conservation - targetMoles = MIN_MOLES; + // Use MIN_MOLES to maintain element balance while avoiding zero/negative moles + // This is more consistent than arbitrary scaling which violates conservation + targetMoles = MIN_MOLES; } double dn = targetMoles - currentMoles; @@ -420,20 +432,21 @@ public void updateMoles() { // Update phase total moles to match sum of component moles double phaseTotalMoles = 0; for (int i = 0; i < components.length; i++) { - phaseTotalMoles += system.getPhase(phasenumb).getComponents()[components[i].getComponentNumber()] - .getNumberOfMolesInPhase(); + phaseTotalMoles += + system.getPhase(phasenumb).getComponents()[components[i].getComponentNumber()] + .getNumberOfMolesInPhase(); } // Also include any non-reactive components for (int i = 0; i < system.getPhase(phasenumb).getNumberOfComponents(); i++) { boolean isReactive = false; for (int j = 0; j < components.length; j++) { - if (components[j].getComponentNumber() == i) { - isReactive = true; - break; - } + if (components[j].getComponentNumber() == i) { + isReactive = true; + break; + } } if (!isReactive) { - phaseTotalMoles += system.getPhase(phasenumb).getComponent(i).getNumberOfMolesInPhase(); + phaseTotalMoles += system.getPhase(phasenumb).getComponent(i).getNumberOfMolesInPhase(); } } ((neqsim.thermo.phase.Phase) system.getPhase(phasenumb)).numberOfMolesInPhase = phaseTotalMoles; @@ -468,112 +481,115 @@ public boolean solve() { try { do { - p++; - errOld = error; - error = 0.0; - - // Adaptive derivative switching: enable derivatives after initial iterations - // or when error is small enough for quadratic convergence to help - if (useAdaptiveDerivatives && !useFugacityDerivatives) { - if (p >= DERIVATIVE_SWITCH_ITERATION || errOld < DERIVATIVE_SWITCH_ERROR) { - useFugacityDerivatives = true; - logger.debug("Chemical equilibrium: switching to derivatives at iteration " + p + ", error=" + errOld); - } - } - - this.chemSolve(); - - // Early exit if chemSolve produced invalid results - if (dn_matrix == null) { - logger.warn("Chemical equilibrium: dn_matrix is null at iteration " + p); - break; - } - - double step1 = step(); - - for (int i = 0; i < NSPEC; i++) { - double molesInPhase = system.getPhase(phasenumb).getComponents()[components[i].getComponentNumber()] - .getNumberOfMolesInPhase(); - - // Skip if moles are too small to avoid division issues - if (molesInPhase < MIN_MOLES) { - continue; - } - - double dnValue = dn_matrix.get(i, 0); - - // Check for NaN or Infinite values - if (Double.isNaN(dnValue) || Double.isInfinite(dnValue)) { - logger.debug("Chemical equilibrium: NaN/Inf detected in dn_matrix at iteration " + p); - error = Double.NaN; - break; - } - - if (Math.abs(dnValue) / molesInPhase > 1e-15) { - thisError = Math.abs(dnValue) / molesInPhase; - error += Math.abs(thisError); - n_mol[i] = dnValue * step1 + molesInPhase; - } - } - - // Exit immediately if NaN detected - if (Double.isNaN(error) || Double.isInfinite(error)) { - logger.debug("Chemical equilibrium: NaN/Inf error at iteration " + p + ", P=" + system.getPressure() - + " bara, T=" + system.getTemperature() + " K"); - break; - } - - // Track best error and detect stagnation - if (error < bestError) { - bestError = error; - stagnationCount = 0; - } else { - stagnationCount++; - } - - // Exit if solver is stagnating (not making progress) - if (stagnationCount >= STAGNATION_LIMIT) { - logger.debug("Chemical equilibrium: stagnation detected at iteration " + p + ", error=" + error); - break; - } - - if (error <= errOld) { - updateMoles(); - - // Save the correct moles before init(1) might corrupt them - double[] savedMolesInPhase = new double[components.length]; - for (int i = 0; i < components.length; i++) { - savedMolesInPhase[i] = system.getPhase(phasenumb).getComponent(components[i].getComponentNumber()) - .getNumberOfMolesInPhase(); - } - - system.init(1, phasenumb); - - // Restore the correct moles after init(1) - for (int i = 0; i < components.length; i++) { - double currentMoles = system.getPhase(phasenumb).getComponent(components[i].getComponentNumber()) - .getNumberOfMolesInPhase(); - if (Math.abs(currentMoles - savedMolesInPhase[i]) > 1e-15) { - // Moles were corrupted by init(1), restore them - double diff = savedMolesInPhase[i] - currentMoles; - system.getPhase(phasenumb).addMolesChemReac(components[i].getComponentNumber(), diff); - } - } - system.init_x_y(); // Recalculate x values to match restored moles - - calcRefPot(); - } - - // Gradually relax tolerance for difficult convergence cases - if (p > 15) { - maxError *= 1.5; - } + p++; + errOld = error; + error = 0.0; + + // Adaptive derivative switching: enable derivatives after initial iterations + // or when error is small enough for quadratic convergence to help + if (useAdaptiveDerivatives && !useFugacityDerivatives) { + if (p >= DERIVATIVE_SWITCH_ITERATION || errOld < DERIVATIVE_SWITCH_ERROR) { + useFugacityDerivatives = true; + logger.debug("Chemical equilibrium: switching to derivatives at iteration " + p + + ", error=" + errOld); + } + } + + this.chemSolve(); + + // Early exit if chemSolve produced invalid results + if (dn_matrix == null) { + logger.warn("Chemical equilibrium: dn_matrix is null at iteration " + p); + break; + } + + double step1 = step(); + + for (int i = 0; i < NSPEC; i++) { + double molesInPhase = + system.getPhase(phasenumb).getComponents()[components[i].getComponentNumber()] + .getNumberOfMolesInPhase(); + + // Skip if moles are too small to avoid division issues + if (molesInPhase < MIN_MOLES) { + continue; + } + + double dnValue = dn_matrix.get(i, 0); + + // Check for NaN or Infinite values + if (Double.isNaN(dnValue) || Double.isInfinite(dnValue)) { + logger.debug("Chemical equilibrium: NaN/Inf detected in dn_matrix at iteration " + p); + error = Double.NaN; + break; + } + + if (Math.abs(dnValue) / molesInPhase > 1e-15) { + thisError = Math.abs(dnValue) / molesInPhase; + error += Math.abs(thisError); + n_mol[i] = dnValue * step1 + molesInPhase; + } + } + + // Exit immediately if NaN detected + if (Double.isNaN(error) || Double.isInfinite(error)) { + logger.debug("Chemical equilibrium: NaN/Inf error at iteration " + p + ", P=" + + system.getPressure() + " bara, T=" + system.getTemperature() + " K"); + break; + } + + // Track best error and detect stagnation + if (error < bestError) { + bestError = error; + stagnationCount = 0; + } else { + stagnationCount++; + } + + // Exit if solver is stagnating (not making progress) + if (stagnationCount >= STAGNATION_LIMIT) { + logger.debug( + "Chemical equilibrium: stagnation detected at iteration " + p + ", error=" + error); + break; + } + + if (error <= errOld) { + updateMoles(); + + // Save the correct moles before init(1) might corrupt them + double[] savedMolesInPhase = new double[components.length]; + for (int i = 0; i < components.length; i++) { + savedMolesInPhase[i] = system.getPhase(phasenumb) + .getComponent(components[i].getComponentNumber()).getNumberOfMolesInPhase(); + } + + system.init(1, phasenumb); + + // Restore the correct moles after init(1) + for (int i = 0; i < components.length; i++) { + double currentMoles = system.getPhase(phasenumb) + .getComponent(components[i].getComponentNumber()).getNumberOfMolesInPhase(); + if (Math.abs(currentMoles - savedMolesInPhase[i]) > 1e-15) { + // Moles were corrupted by init(1), restore them + double diff = savedMolesInPhase[i] - currentMoles; + system.getPhase(phasenumb).addMolesChemReac(components[i].getComponentNumber(), diff); + } + } + system.init_x_y(); // Recalculate x values to match restored moles + + calcRefPot(); + } + + // Gradually relax tolerance for difficult convergence cases + if (p > 15) { + maxError *= 1.5; + } } while (((errOld > maxError && Math.abs(error) > maxError) && p < maxIterations) || p < 2); } catch (Exception ex) { logger.error("Chemical equilibrium solver exception: " + ex.getMessage(), ex); // Restore original derivative setting if (useAdaptiveDerivatives) { - useFugacityDerivatives = originalDerivativeSetting; + useFugacityDerivatives = originalDerivativeSetting; } return false; } @@ -584,8 +600,9 @@ public boolean solve() { } if (p >= maxIterations) { - logger.debug("Chemical equilibrium: max iterations (" + maxIterations + ") reached" + ", error=" + error + ", P=" - + system.getPressure() + " bara" + ", T=" + system.getTemperature() + " K"); + logger.debug("Chemical equilibrium: max iterations (" + maxIterations + ") reached" + + ", error=" + error + ", P=" + system.getPressure() + " bara" + ", T=" + + system.getTemperature() + " K"); } // Always try to reinitialize system even if convergence failed @@ -614,14 +631,15 @@ public boolean solve() { * Enforce minimum physically reasonable concentrations for ionic species. * *

- * When the chemical equilibrium solver fails to converge, ionic species like H3O+ and OH- can be left at - * unrealistically low values. This method checks the water auto-ionization equilibrium (Kw = [H3O+][OH-]) and - * corrects unrealistic values while respecting legitimate acidic or alkaline conditions. + * When the chemical equilibrium solver fails to converge, ionic species like H3O+ and OH- can be + * left at unrealistically low values. This method checks the water auto-ionization equilibrium + * (Kw = [H3O+][OH-]) and corrects unrealistic values while respecting legitimate acidic or + * alkaline conditions. *

* *

- * The method only intervenes when both H3O+ and OH- are unrealistically low (violating Kw), not when the solution is - * legitimately acidic (high H3O+, low OH-) or alkaline (low H3O+, high OH-). + * The method only intervenes when both H3O+ and OH- are unrealistically low (violating Kw), not + * when the solution is legitimately acidic (high H3O+, low OH-) or alkaline (low H3O+, high OH-). *

*/ private void enforceMinimumIonConcentrations() { @@ -633,11 +651,11 @@ private void enforceMinimumIonConcentrations() { for (int i = 0; i < components.length; i++) { String name = components[i].getComponentName(); if (name.equals("H3O+")) { - h3oIndex = i; + h3oIndex = i; } else if (name.equals("OH-")) { - ohIndex = i; + ohIndex = i; } else if (name.equals("water")) { - waterIndex = i; + waterIndex = i; } } @@ -650,15 +668,17 @@ private void enforceMinimumIonConcentrations() { return; } - double currentH3OMoles = system.getPhase(phasenumb).getComponents()[components[h3oIndex].getComponentNumber()] - .getNumberOfMolesInPhase(); + double currentH3OMoles = + system.getPhase(phasenumb).getComponents()[components[h3oIndex].getComponentNumber()] + .getNumberOfMolesInPhase(); double currentH3OMoleFraction = currentH3OMoles / totalMoles; double currentOHMoles = 0; double currentOHMoleFraction = 0; if (ohIndex >= 0) { - currentOHMoles = system.getPhase(phasenumb).getComponents()[components[ohIndex].getComponentNumber()] - .getNumberOfMolesInPhase(); + currentOHMoles = + system.getPhase(phasenumb).getComponents()[components[ohIndex].getComponentNumber()] + .getNumberOfMolesInPhase(); currentOHMoleFraction = currentOHMoles / totalMoles; } @@ -684,20 +704,22 @@ private void enforceMinimumIonConcentrations() { double molesToAdd = targetH3OMoles - currentH3OMoles; if (molesToAdd > 0) { - system.addComponent(components[h3oIndex].getComponentNumber(), molesToAdd, phasenumb); - - // Set OH- to maintain Kw ≈ 10^-14 (x_H3O * x_OH ≈ 3.2e-18) - if (ohIndex >= 0) { - double targetOHMoleFraction = 3.2e-18 / neutralH3OMoleFraction; - double targetOHMoles = targetOHMoleFraction * totalMoles; - if (targetOHMoles > currentOHMoles) { - system.addComponent(components[ohIndex].getComponentNumber(), targetOHMoles - currentOHMoles, phasenumb); - } - } - - reinitializeAfterIonAdjustment(); - logger.debug("Chemical equilibrium: enforced water equilibrium (both ions were too low)" + ", old x(H3O+)=" - + currentH3OMoleFraction + ", new x(H3O+)=" + neutralH3OMoleFraction); + system.addComponent(components[h3oIndex].getComponentNumber(), molesToAdd, phasenumb); + + // Set OH- to maintain Kw ≈ 10^-14 (x_H3O * x_OH ≈ 3.2e-18) + if (ohIndex >= 0) { + double targetOHMoleFraction = 3.2e-18 / neutralH3OMoleFraction; + double targetOHMoles = targetOHMoleFraction * totalMoles; + if (targetOHMoles > currentOHMoles) { + system.addComponent(components[ohIndex].getComponentNumber(), + targetOHMoles - currentOHMoles, phasenumb); + } + } + + reinitializeAfterIonAdjustment(); + logger.debug("Chemical equilibrium: enforced water equilibrium (both ions were too low)" + + ", old x(H3O+)=" + currentH3OMoleFraction + ", new x(H3O+)=" + + neutralH3OMoleFraction); } } else if (h3oTooLow && !ohTooLow) { // Only H3O+ is too low but OH- is reasonable - could be alkaline solution @@ -707,14 +729,15 @@ private void enforceMinimumIonConcentrations() { // Only enforce if H3O+ is MUCH lower than what Kw predicts (factor of 1000) if (currentH3OMoleFraction < targetH3OMoleFraction * 1e-3) { - double targetH3OMoles = targetH3OMoleFraction * totalMoles; - double molesToAdd = targetH3OMoles - currentH3OMoles; - if (molesToAdd > 0) { - system.addComponent(components[h3oIndex].getComponentNumber(), molesToAdd, phasenumb); - reinitializeAfterIonAdjustment(); - logger.debug("Chemical equilibrium: adjusted H3O+ for alkaline solution" + ", old x(H3O+)=" - + currentH3OMoleFraction + ", new x(H3O+)=" + targetH3OMoleFraction); - } + double targetH3OMoles = targetH3OMoleFraction * totalMoles; + double molesToAdd = targetH3OMoles - currentH3OMoles; + if (molesToAdd > 0) { + system.addComponent(components[h3oIndex].getComponentNumber(), molesToAdd, phasenumb); + reinitializeAfterIonAdjustment(); + logger + .debug("Chemical equilibrium: adjusted H3O+ for alkaline solution" + ", old x(H3O+)=" + + currentH3OMoleFraction + ", new x(H3O+)=" + targetH3OMoleFraction); + } } } // If OH- is too low but H3O+ is reasonable (acidic solution), don't intervene @@ -730,7 +753,8 @@ private void reinitializeAfterIonAdjustment() { system.init_x_y(); system.init(1, phasenumb); } catch (Exception ex) { - logger.debug("Chemical equilibrium: failed to reinitialize after ion adjustment: " + ex.getMessage()); + logger.debug( + "Chemical equilibrium: failed to reinitialize after ion adjustment: " + ex.getMessage()); } } @@ -747,9 +771,9 @@ public boolean isUseFugacityDerivatives() { * Enable or disable fugacity coefficient derivatives in M_matrix calculation. * *

- * When enabled, the solver uses init(3, phase) to calculate fugacity derivatives and includes dln(fugacity)/dN terms - * in the M_matrix for more accurate Newton steps. This can improve convergence for non-ideal mixtures but is - * computationally more expensive. + * When enabled, the solver uses init(3, phase) to calculate fugacity derivatives and includes + * dln(fugacity)/dN terms in the M_matrix for more accurate Newton steps. This can improve + * convergence for non-ideal mixtures but is computationally more expensive. *

* * @param useFugacityDerivatives true to enable, false to disable @@ -771,14 +795,15 @@ public boolean isUseAdaptiveDerivatives() { * Enable or disable adaptive derivative switching. * *

- * When enabled, the solver starts with simple iterations (without fugacity derivatives) for the first few iterations, - * then automatically switches to derivative-based iterations for faster quadratic convergence near the solution. This - * combines the robustness of simple iterations with the speed of derivative-based methods. + * When enabled, the solver starts with simple iterations (without fugacity derivatives) for the + * first few iterations, then automatically switches to derivative-based iterations for faster + * quadratic convergence near the solution. This combines the robustness of simple iterations with + * the speed of derivative-based methods. *

* *

- * The switch occurs after {@code DERIVATIVE_SWITCH_ITERATION} iterations or when the error falls below - * {@code DERIVATIVE_SWITCH_ERROR}. + * The switch occurs after {@code DERIVATIVE_SWITCH_ITERATION} iterations or when the error falls + * below {@code DERIVATIVE_SWITCH_ERROR}. *

* * @param useAdaptiveDerivatives true to enable adaptive switching, false to disable @@ -800,9 +825,9 @@ public boolean isUseFullMMatrix() { * Enable or disable the full Smith-Missen M-matrix. * *

- * When enabled, the M-matrix uses the full form M_ij = δ_ij/n_i - 1/n_t instead of the simplified form M_ij = - * δ_ij/n_i. The -1/n_t term couples all species through total moles and can improve quadratic convergence near the - * solution, but may be less stable for some systems. + * When enabled, the M-matrix uses the full form M_ij = δ_ij/n_i - 1/n_t instead of the simplified + * form M_ij = δ_ij/n_i. The -1/n_t term couples all species through total moles and can improve + * quadratic convergence near the solution, but may be less stable for some systems. *

* * @param useFullMMatrix true to enable full M-matrix, false to use simplified form @@ -890,8 +915,8 @@ public boolean isLastConverged() { public void printComp() { for (int j = 0; j < NSPEC; j++) { System.out.println(" SVAR : " + n_mol[j]); - double activity = system.getPhase(phasenumb).getActivityCoefficient(components[j].getComponentNumber(), - components[waterNumb].getComponentNumber()); + double activity = system.getPhase(phasenumb).getActivityCoefficient( + components[j].getComponentNumber(), components[waterNumb].getComponentNumber()); System.out.println("act " + activity + " comp " + components[j].getComponentName()); } } @@ -928,45 +953,47 @@ public double step() { n_omega[i] = n_mol[i] + d_n[i]; // System.out.println("nomega " + n_omega[i] ); if (n_omega[i] < 0) { - check = i; + check = i; - step = innerStep(i, n_omega, check, step, true); - // System.out.println("step2 ... " + step); - return step; + step = innerStep(i, n_omega, check, step, true); + // System.out.println("step2 ... " + step); + return step; } else { - // chem_pot_omega[i] = R*T*(chem_ref[i]+ Math.log(n_omega[i]/n_t) + - // Math.log(system.getPhases()[1].getComponents()[components[i].getComponentNumber()].getFugacityCoefficient() - // / chem_pot_pure[i])); - // chem_pot[i] = R*T*(chem_ref[i] + Math.log(n_mol[i]/n_t)+ - // Math.log(system.getPhases()[1].getComponents()[components[i].getComponentNumber()].getFugacityCoefficient() - // / chem_pot_pure[i])); - - if (system.getPhase(phasenumb).getComponents()[components[i].getComponentNumber()].getReferenceStateType() - .equals("solvent")) { - // Protect against log(0) with MIN_MOLES - double molesInPhase = Math.max(MIN_MOLES, - system.getPhase(phasenumb).getComponents()[components[i].getComponentNumber()].getNumberOfMolesInPhase()); - chem_pot[i] = R * system.getPhase(phasenumb).getTemperature() - * (chem_ref[i] + Math.log(molesInPhase) - Math.log(n_t) + logactivityVec[i]); - // system.getPhase(phasenumb).getActivityCoefficient(components[i].getComponentNumber(),components[waterNumb].getComponentNumber()))); - // System.out.println("solvent activ: "+ i + " " + - // system.getPhases()[1].getComponents()[components[i].getComponentNumber()].getFugacityCoefficient() - // / chem_pot_pure[i]); - } else { - // Protect against log(0) with MIN_MOLES - double molesInPhase = Math.max(MIN_MOLES, - system.getPhase(phasenumb).getComponents()[components[i].getComponentNumber()].getNumberOfMolesInPhase()); - chem_pot[i] = R * system.getPhase(phasenumb).getTemperature() - * (chem_ref[i] + Math.log(molesInPhase) - Math.log(n_t) + logactivityVec[i]); - // system.getPhase(phasenumb).getActivityCoefficient(components[i].getComponentNumber(),components[waterNumb].getComponentNumber()))); - // System.out.println("solute activ : " + i + " " + - // system.getPhases()[1].getComponents()[components[i].getComponentNumber()].getFugacityCoefficient() - // / chem_pot_dilute[i]); - } - // Protect n_omega against log(0) with MIN_MOLES - double n_omega_safe = Math.max(MIN_MOLES, n_omega[i]); - chem_pot_omega[i] = R * system.getPhase(phasenumb).getTemperature() - * (chem_ref[i] + Math.log(n_omega_safe) - Math.log(n_t) + logactivityVec[i]); + // chem_pot_omega[i] = R*T*(chem_ref[i]+ Math.log(n_omega[i]/n_t) + + // Math.log(system.getPhases()[1].getComponents()[components[i].getComponentNumber()].getFugacityCoefficient() + // / chem_pot_pure[i])); + // chem_pot[i] = R*T*(chem_ref[i] + Math.log(n_mol[i]/n_t)+ + // Math.log(system.getPhases()[1].getComponents()[components[i].getComponentNumber()].getFugacityCoefficient() + // / chem_pot_pure[i])); + + if (system.getPhase(phasenumb).getComponents()[components[i].getComponentNumber()] + .getReferenceStateType().equals("solvent")) { + // Protect against log(0) with MIN_MOLES + double molesInPhase = Math.max(MIN_MOLES, + system.getPhase(phasenumb).getComponents()[components[i].getComponentNumber()] + .getNumberOfMolesInPhase()); + chem_pot[i] = R * system.getPhase(phasenumb).getTemperature() + * (chem_ref[i] + Math.log(molesInPhase) - Math.log(n_t) + logactivityVec[i]); + // system.getPhase(phasenumb).getActivityCoefficient(components[i].getComponentNumber(),components[waterNumb].getComponentNumber()))); + // System.out.println("solvent activ: "+ i + " " + + // system.getPhases()[1].getComponents()[components[i].getComponentNumber()].getFugacityCoefficient() + // / chem_pot_pure[i]); + } else { + // Protect against log(0) with MIN_MOLES + double molesInPhase = Math.max(MIN_MOLES, + system.getPhase(phasenumb).getComponents()[components[i].getComponentNumber()] + .getNumberOfMolesInPhase()); + chem_pot[i] = R * system.getPhase(phasenumb).getTemperature() + * (chem_ref[i] + Math.log(molesInPhase) - Math.log(n_t) + logactivityVec[i]); + // system.getPhase(phasenumb).getActivityCoefficient(components[i].getComponentNumber(),components[waterNumb].getComponentNumber()))); + // System.out.println("solute activ : " + i + " " + + // system.getPhases()[1].getComponents()[components[i].getComponentNumber()].getFugacityCoefficient() + // / chem_pot_dilute[i]); + } + // Protect n_omega against log(0) with MIN_MOLES + double n_omega_safe = Math.max(MIN_MOLES, n_omega[i]); + chem_pot_omega[i] = R * system.getPhase(phasenumb).getTemperature() + * (chem_ref[i] + Math.log(n_omega_safe) - Math.log(n_t) + logactivityVec[i]); } } // Added by Neeraj @@ -985,19 +1012,20 @@ public double step() { if (G_1 > 0) { G_0 = 0.0; for (i = 0; i < NSPEC; i++) { - // G_0 += chem_pot[i]*d_n[i]; - // Added by Neeraj - // Protect against division by zero - double molesInPhase = Math.max(MIN_MOLES, - system.getPhase(phasenumb).getComponents()[components[i].getComponentNumber()].getNumberOfMolesInPhase()); - G_0 += (chem_pot[i] - Alambda_matrix.get(i, 0)) * d_n[i] * (1 / molesInPhase - 1 / n_t); - // G_0 += - // (chem_pot[i]-Alambda_matrix.get(i,0))*d_n[i]*(M_Jama_matrix.get(i,i)-1/n_t); + // G_0 += chem_pot[i]*d_n[i]; + // Added by Neeraj + // Protect against division by zero + double molesInPhase = Math.max(MIN_MOLES, + system.getPhase(phasenumb).getComponents()[components[i].getComponentNumber()] + .getNumberOfMolesInPhase()); + G_0 += (chem_pot[i] - Alambda_matrix.get(i, 0)) * d_n[i] * (1 / molesInPhase - 1 / n_t); + // G_0 += + // (chem_pot[i]-Alambda_matrix.get(i,0))*d_n[i]*(M_Jama_matrix.get(i,i)-1/n_t); } // Protect against division by zero when G_0 ≈ G_1 double denominator = G_0 - G_1; if (Math.abs(denominator) > 1e-30) { - step = G_0 / denominator; + step = G_0 / denominator; } // System.out.println("step G " + step); } @@ -1033,20 +1061,20 @@ public double innerStep(int i, double[] n_omega, int check, double step, boolean agemo = (-n_mol[i] / d_n[i]) * (1.0 - 0.03); for (i = check; i < NSPEC; i++) { - n_omega[i] = n_mol[i] + d_n[i]; - - if (n_omega[i] < 0) { - step = (-n_mol[i] / d_n[i]) * (1.0 - 0.03); - if (step < agemo) { - agemo = step; - } - } + n_omega[i] = n_mol[i] + d_n[i]; + + if (n_omega[i] < 0) { + step = (-n_mol[i] / d_n[i]) * (1.0 - 0.03); + if (step < agemo) { + agemo = step; + } + } } step = agemo; if (step > 1) { - step = 1.0; + step = 1.0; } } return step; @@ -1054,75 +1082,44 @@ public double innerStep(int i, double[] n_omega, int check, double step, boolean // Method added by Neeraj /* - * public double step(){ double step=1.0; int i, check=0; double[] F = new double[NSPEC]; double[] F_omega = new - * double[NSPEC]; double[] chem_pot = new double[NSPEC]; double[] n_omega = new double[NSPEC]; + * public double step(){ double step=1.0; int i, check=0; double[] F = new double[NSPEC]; double[] + * F_omega = new double[NSPEC]; double[] chem_pot = new double[NSPEC]; double[] n_omega = new + * double[NSPEC]; * * Matrix F_matrix, F_omega_matrix, fs_matrix, f_matrix, f_omega_matrix; double fs,f,f_omega; * - * for(i = 0;i 0.5) step = 0.5; return step; } + * step = (-1)*fs/(2*(f_omega-f-fs)); //System.out.println("f "+f); + * //System.out.println("f_omega "+f_omega); //System.out.println("fs "+fs); + * //System.out.println("step " + step); //if (step > 0.5) step = 0.5; return step; } */ - /** - * Solve least-squares problem using SVD pseudo-inverse. - * - *

- * For rank-deficient or ill-conditioned matrices, this provides a more robust solution than direct inversion. Uses - * SVD decomposition: A = U * S * V^T, then x = V * S^(-1) * U^T * b. - *

- * - * @param A the coefficient matrix - * @param b the right-hand side vector - * @return the least-squares solution x - */ - private Matrix solveLeastSquares(Matrix A, Matrix b) { - Jama.SingularValueDecomposition svd = A.svd(); - Matrix U = svd.getU(); - double[] singularValues = svd.getSingularValues(); - Matrix V = svd.getV(); - - // Compute pseudo-inverse: A+ = V * S+ * U^T - // S+ is the pseudo-inverse of the diagonal matrix of singular values - int m = A.getRowDimension(); - int n = A.getColumnDimension(); - int minDim = Math.min(m, n); - double tol = 1e-12 * (singularValues.length > 0 ? singularValues[0] : 1.0); - - // Create S+ with dimensions n x m (transpose of S dimensions) - Matrix Sinv = new Matrix(n, m); - for (int i = 0; i < minDim; i++) { - if (Math.abs(singularValues[i]) > tol) { - Sinv.set(i, i, 1.0 / singularValues[i]); - } - } - - // x = V * S+ * U^T * b - return V.times(Sinv.times(U.transpose().times(b))); - } } diff --git a/src/main/java/neqsim/physicalproperties/interfaceproperties/surfacetension/GTSurfaceTensionODE.java b/src/main/java/neqsim/physicalproperties/interfaceproperties/surfacetension/GTSurfaceTensionODE.java index 118307e73e..8f24f6f1f8 100644 --- a/src/main/java/neqsim/physicalproperties/interfaceproperties/surfacetension/GTSurfaceTensionODE.java +++ b/src/main/java/neqsim/physicalproperties/interfaceproperties/surfacetension/GTSurfaceTensionODE.java @@ -3,24 +3,19 @@ import org.apache.commons.math3.ode.FirstOrderDifferentialEquations; import org.apache.logging.log4j.LogManager; import org.apache.logging.log4j.Logger; -import org.ejml.data.DMatrixRMaj; -import org.ejml.dense.row.CommonOps_DDRM; -import org.ejml.dense.row.NormOps_DDRM; -import org.ejml.dense.row.SingularOps_DDRM; -import org.ejml.dense.row.factory.DecompositionFactory_DDRM; -import org.ejml.interfaces.decomposition.SingularValueDecomposition; -import org.ejml.interfaces.decomposition.SingularValueDecomposition_F64; import neqsim.thermo.system.SystemInterface; +import neqsim.util.math.LinearAlgebraOps; /** - *

* GTSurfaceTensionODE class. * - * ODE-system for integrating the surface tension in cases where the a reference component number mole density can be - * used as integration variable. + *

+ * ODE-system for integrating the surface tension in cases where the a reference component number + * mole density can be used as integration variable. * - * This method can only be used when the reference component density varies monotonically over the interface, and where - * there are no binary interaction parameters for the attractive parameter in the EOS. + * This method can only be used when the reference component density varies monotonically over the + * interface, and where there are no binary interaction parameters for the attractive parameter in + * the EOS. *

* * @author Olaf Trygve Berglihn olaf.trygve.berglihn@sintef.no @@ -62,8 +57,8 @@ public class GTSurfaceTensionODE implements FirstOrderDifferentialEquations { * @param referenceComponent a int * @param yscale a double */ - public GTSurfaceTensionODE(SystemInterface flashedSystem, int phase1, int phase2, int referenceComponent, - double yscale) { + public GTSurfaceTensionODE(SystemInterface flashedSystem, int phase1, int phase2, + int referenceComponent, double yscale) { int i; int idx = 0; @@ -83,8 +78,8 @@ public GTSurfaceTensionODE(SystemInterface flashedSystem, int phase1, int phase2 for (i = 0; i < this.ncomp; i++) { if (i != this.refcomp) { - this.algidx[idx] = i; - idx++; + this.algidx[idx] = i; + idx++; } } @@ -93,10 +88,10 @@ public GTSurfaceTensionODE(SystemInterface flashedSystem, int phase1, int phase2 */ for (i = 0; i < this.ncomp; i++) { this.ci[i] = this.sys.getPhase(0).getComponent(i).getSurfaceTenisionInfluenceParameter(t); - this.rho_ph1[i] = this.sys.getPhase(phase1).getComponent(i).getx() / this.sys.getPhase(phase1).getMolarVolume() - / m3; - this.rho_ph2[i] = this.sys.getPhase(phase2).getComponent(i).getx() / this.sys.getPhase(phase2).getMolarVolume() - / m3; + this.rho_ph1[i] = this.sys.getPhase(phase1).getComponent(i).getx() + / this.sys.getPhase(phase1).getMolarVolume() / m3; + this.rho_ph2[i] = this.sys.getPhase(phase2).getComponent(i).getx() + / this.sys.getPhase(phase2).getMolarVolume() / m3; this.rho_k[i] = this.rho_ph1[i]; } this.rhoref_span = Math.abs(this.rho_ph2[this.refcomp] - this.rho_ph1[this.refcomp]); @@ -132,7 +127,8 @@ public void initmu() { double[] mueq2 = new double[this.ncomp]; double[] p0 = new double[1]; - GTSurfaceTensionUtils.mufun(this.sys, this.ncomp, this.t, this.rho_ph1, this.mueq, dmu_drho1, this.p0); + GTSurfaceTensionUtils.mufun(this.sys, this.ncomp, this.t, this.rho_ph1, this.mueq, dmu_drho1, + this.p0); GTSurfaceTensionUtils.mufun(this.sys, this.ncomp, this.t, this.rho_ph2, mueq2, dmu_drho2, p0); // Check flash equilibrium @@ -140,8 +136,8 @@ public void initmu() { maxerr = Math.max(maxerr, Math.abs(this.mueq[i] / mueq2[i] - 1.0)); } if (maxerr > this.reltol) { - logger.error( - "Flash is not properly solved. Maximum relative error in chemical potential: " + maxerr + " > " + reltol); + logger.error("Flash is not properly solved. Maximum relative error in chemical potential: " + + maxerr + " > " + reltol); throw new RuntimeException("Flash not solved!"); } this.initialized = true; @@ -161,20 +157,17 @@ public void computeDerivatives(double t, double[] y, double[] yDot) { double[] p = new double[1]; double[] f = new double[this.ncomp]; double[][] jac = new double[this.ncomp][this.ncomp]; - double[] rho = new double[this.ncomp]; double delta_omega; double dsigma; - double cij; - double rho0; - DMatrixRMaj dn_dnref; + double[] dn_dnref; - int j; if (!this.initialized) { this.initmu(); } - rho0 = this.rho_ph1[this.refcomp]; + double rho0 = this.rho_ph1[this.refcomp]; + double[] rho = new double[this.ncomp]; rho[this.refcomp] = t * this.rhoref_span + rho0; for (int i = 0; i < this.ncomp - 1; i++) { rho[this.algidx[i]] = this.rho_k[this.algidx[i]]; @@ -185,15 +178,14 @@ public void computeDerivatives(double t, double[] y, double[] yDot) { this.rho_k[i] = rho[i]; } - DMatrixRMaj df = new DMatrixRMaj(jac); - DMatrixRMaj ms = new DMatrixRMaj(df.numRows, 1); - SingularValueDecomposition svd = DecompositionFactory_DDRM.svd(df.numRows, df.numCols, true, true, - true); - if (!svd.decompose(df)) { - throw new RuntimeException("Decomposition failed"); + dn_dnref = LinearAlgebraOps.calcNullVector(jac); + double refScale = dn_dnref[this.refcomp]; + if (Math.abs(refScale) < 1.0e-30) { + throw new RuntimeException("Null vector reference component is zero"); + } + for (int i = 0; i < this.ncomp; i++) { + dn_dnref[i] /= refScale; } - dn_dnref = SingularOps_DDRM.nullSpace((SingularValueDecomposition_F64) svd, ms, 1e-12); // UtilEjml.EPS); - CommonOps_DDRM.divide(dn_dnref.get(this.refcomp, 0), dn_dnref); delta_omega = -(p[0] - this.p0[0]); for (int i = 0; i < this.ncomp; i++) { delta_omega += (mu[i] - this.mueq[i]) * rho[i]; @@ -201,23 +193,23 @@ public void computeDerivatives(double t, double[] y, double[] yDot) { dsigma = 0.0; for (int i = 0; i < this.ncomp; i++) { - for (j = 0; j < this.ncomp; j++) { - cij = Math.sqrt(this.ci[i] * this.ci[j]); - dsigma += cij * dn_dnref.get(i, 0) * dn_dnref.get(j, 0); + for (int j = 0; j < this.ncomp; j++) { + double cij = Math.sqrt(this.ci[i] * this.ci[j]); + dsigma += cij * dn_dnref[i] * dn_dnref[j]; } } /* - * If the discriminant becomes negative, this can be due to numerical problems when approaching bulk. Assume the - * profile is sufficiently flat if the reference density has exceeded 90% of the target bulk density. A better way - * is to use the approximations given by Davis, Statistical mechanics of surfaces and thin films, VHC Publishers - * Inc, 1996. + * If the discriminant becomes negative, this can be due to numerical problems when approaching + * bulk. Assume the profile is sufficiently flat if the reference density has exceeded 90% of + * the target bulk density. A better way is to use the approximations given by Davis, + * Statistical mechanics of surfaces and thin films, VHC Publishers Inc, 1996. */ if (delta_omega * dsigma < 0.0) { if (t > 0.9) { - dsigma = 0.; + dsigma = 0.; } else { - throw new RuntimeException("Negative discriminant"); + throw new RuntimeException("Negative discriminant"); } } else { dsigma = Math.sqrt(2.0 * delta_omega * dsigma); @@ -233,8 +225,8 @@ public void computeDerivatives(double t, double[] y, double[] yDot) { } /** - * SolveRho. Solve for the equilibrium density in the interface. Solves the equilibrium relations with the - * Newton-Raphson method. + * SolveRho. Solve for the equilibrium density in the interface. Solves the equilibrium relations + * with the Newton-Raphson method. * * @param rho Number density [mol/m3] * @param mu Chemical potential [J/mol] @@ -243,7 +235,8 @@ public void computeDerivatives(double t, double[] y, double[] yDot) { * @param f Residual of equilibrium relations. * @param jac Jacobian of the equilibrium relations. */ - private void solveRho(double[] rho, double[] mu, double[][] dmu_drho, double[] p, double[] f, double[][] jac) { + private void solveRho(double[] rho, double[] mu, double[][] dmu_drho, double[] p, double[] f, + double[][] jac) { double normf; double norm0; double norm; @@ -251,11 +244,11 @@ private void solveRho(double[] rho, double[] mu, double[][] dmu_drho, double[] p int i; int j; int iter; - DMatrixRMaj A = new DMatrixRMaj(this.ncomp - 1, this.ncomp - 1); - DMatrixRMaj b = new DMatrixRMaj(this.ncomp - 1, 1); - DMatrixRMaj x = new DMatrixRMaj(this.ncomp - 1, 1); - DMatrixRMaj x0 = new DMatrixRMaj(this.ncomp - 1, 1); - DMatrixRMaj c = new DMatrixRMaj(this.ncomp - 1, 1); + double[][] A = new double[this.ncomp - 1][this.ncomp - 1]; + double[] b = new double[this.ncomp - 1]; + double[] x = new double[this.ncomp - 1]; + double[] x0 = new double[this.ncomp - 1]; + double[] c = new double[this.ncomp - 1]; GTSurfaceTensionUtils.mufun(this.sys, this.ncomp, this.t, rho, mu, dmu_drho, p); fjacfun(mu, dmu_drho, f, jac); @@ -263,78 +256,83 @@ private void solveRho(double[] rho, double[] mu, double[][] dmu_drho, double[] p int idx1; idx1 = this.algidx[i]; - b.set(i, 0, -f[idx1]); - x0.set(i, 0, rho[idx1]); + b[i] = -f[idx1]; + x0[i] = rho[idx1]; for (j = 0; j < this.ncomp - 1; j++) { - int idx2; - - idx2 = this.algidx[j]; - A.set(i, j, jac[idx1][idx2]); + int idx2 = this.algidx[j]; + A[i][j] = jac[idx1][idx2]; } } - normf = NormOps_DDRM.normP2(b); + normf = LinearAlgebraOps.vectorNorm(b); if (normf < this.abstol) { return; } - CommonOps_DDRM.solve(A, b, x); + if (!LinearAlgebraOps.solveLinearSystem(A, b, x) + && !LinearAlgebraOps.pseudoInverseSolve(A, b, x)) { + throw new RuntimeException("Failed to solve linear system"); + } for (i = 1; i < this.ncomp - 1; i++) { - double xi; - xi = x.get(i, 0); + double xi = x[i]; if (Double.isNaN(xi)) { - throw new RuntimeException("Update is NaN"); + throw new RuntimeException("Update is NaN"); } } s = 0.8; norm = 1e16; for (iter = 0; iter < this.maxit; iter++) { - CommonOps_DDRM.elementDiv(x, x0, c); + for (i = 0; i < this.ncomp - 1; i++) { + c[i] = x[i] / x0[i]; + } norm0 = norm; - norm = NormOps_DDRM.normP2(c); + norm = LinearAlgebraOps.vectorNorm(c); if (norm < norm0) { - s = Math.min(0.8, 1.2 * s); + s = Math.min(0.8, 1.2 * s); } if (norm < this.normtol || normf < this.abstol || normf < this.reltol) { - // System.out.printf("norm(delta_rho/rho_k): %e, norm(f): %e\n", norm, normf); - break; + // System.out.printf("norm(delta_rho/rho_k): %e, norm(f): %e\n", norm, normf); + break; } double delta; for (i = 0; i < this.ncomp - 1; i++) { - delta = x.get(i, 0); - if ((rho[this.algidx[i]] + s * delta) < 0) { - s = Math.min(s, -0.5 * rho[this.algidx[i]] / delta); - // System.out.printf("s: %e\n", s); - } + delta = x[i]; + if ((rho[this.algidx[i]] + s * delta) < 0) { + s = Math.min(s, -0.5 * rho[this.algidx[i]] / delta); + // System.out.printf("s: %e\n", s); + } } for (i = 0; i < this.ncomp - 1; i++) { - delta = x.get(i, 0); - rho[this.algidx[i]] += s * delta; - x0.set(i, 0, rho[this.algidx[i]]); + delta = x[i]; + rho[this.algidx[i]] += s * delta; + x0[i] = rho[this.algidx[i]]; } GTSurfaceTensionUtils.mufun(this.sys, this.ncomp, this.t, rho, mu, dmu_drho, p); fjacfun(mu, dmu_drho, f, jac); for (i = 0; i < this.ncomp - 1; i++) { - int idx1; + int idx1; - idx1 = this.algidx[i]; - b.set(i, 0, -f[idx1]); - for (j = 0; j < this.ncomp - 1; j++) { - int idx2; + idx1 = this.algidx[i]; + b[i] = -f[idx1]; + for (j = 0; j < this.ncomp - 1; j++) { + int idx2; - idx2 = this.algidx[j]; - A.set(i, j, jac[idx1][idx2]); - } + idx2 = this.algidx[j]; + A[i][j] = jac[idx1][idx2]; + } + } + if (!LinearAlgebraOps.solveLinearSystem(A, b, x) + && !LinearAlgebraOps.pseudoInverseSolve(A, b, x)) { + throw new RuntimeException("Failed to solve linear system"); } - CommonOps_DDRM.solve(A, b, x); - normf = NormOps_DDRM.normP2(b); + normf = LinearAlgebraOps.vectorNorm(b); } if (iter >= this.maxit) { // System.out.printf("norm(f): %e\n", normf); for (i = 0; i < this.ncomp - 1; i++) { - logger.info("f[" + i + "]: " + f[this.algidx[i]]); + logger.info("f[" + i + "]: " + f[this.algidx[i]]); } throw new RuntimeException("Failed to solve for density"); } @@ -349,22 +347,16 @@ private void solveRho(double[] rho, double[] mu, double[][] dmu_drho, double[] p * @param jac an array of type double */ public void fjacfun(double[] mu, double[][] dmu_drho, double[] f, double[][] jac) { - int i; - int j; - double delta_muref; - double sqrtcref; - - double sqrtci; - double scale; - delta_muref = (this.mueq[this.refcomp] - mu[this.refcomp]); - sqrtcref = Math.sqrt(this.ci[this.refcomp]); - scale = 1.0 / sqrtcref; - for (i = 0; i < this.ncomp; i++) { - sqrtci = Math.sqrt(this.ci[i]); + double delta_muref = (this.mueq[this.refcomp] - mu[this.refcomp]); + double sqrtcref = Math.sqrt(this.ci[this.refcomp]); + double scale = 1.0 / sqrtcref; + for (int i = 0; i < this.ncomp; i++) { + double sqrtci = Math.sqrt(this.ci[i]); f[i] = scale * (sqrtci * delta_muref - sqrtcref * (this.mueq[i] - mu[i])); - for (j = 0; j < this.ncomp; j++) { - jac[i][j] = scale * (sqrtci * (-dmu_drho[this.refcomp][j]) - sqrtcref * (-dmu_drho[i][j])); + for (int j = 0; j < this.ncomp; j++) { + jac[i][j] = scale * (sqrtci * (-dmu_drho[this.refcomp][j]) - sqrtcref * (-dmu_drho[i][j])); } } } + } diff --git a/src/main/java/neqsim/process/controllerdevice/ModelPredictiveController.java b/src/main/java/neqsim/process/controllerdevice/ModelPredictiveController.java index 0bd2976cd5..8ff0501eb2 100644 --- a/src/main/java/neqsim/process/controllerdevice/ModelPredictiveController.java +++ b/src/main/java/neqsim/process/controllerdevice/ModelPredictiveController.java @@ -13,20 +13,23 @@ import java.util.UUID; import neqsim.process.measurementdevice.MeasurementDeviceInterface; import neqsim.util.NamedBaseClass; +import neqsim.util.math.LinearAlgebraOps; /** * General-purpose model predictive controller (MPC) for NeqSim process equipment. *

- * The controller supports both single-input operation and multivariable configurations with linear quality constraints. - * A first-order discrete process model is used internally to predict the future trajectory of the controlled variable - * across a configurable prediction horizon. The controller minimises a quadratic objective consisting of tracking - * error, absolute control effort and control movement. The optimal actuation is calculated analytically which keeps the + * The controller supports both single-input operation and multivariable configurations with linear + * quality constraints. A first-order discrete process model is used internally to predict the + * future trajectory of the controlled variable across a configurable prediction horizon. The + * controller minimises a quadratic objective consisting of tracking error, absolute control effort + * and control movement. The optimal actuation is calculated analytically which keeps the * implementation dependency free while still representing a full MPC formulation. *

- * In addition to the control formulation the implementation exposes a receding horizon (moving horizon) estimation - * routine. The estimator reuses the same first-order model to identify the process gain, time constant and bias from - * historical measurement and actuation data. This allows automatic tuning of the internal model parameters when - * operating on real process data without requiring external optimisation packages. + * In addition to the control formulation the implementation exposes a receding horizon (moving + * horizon) estimation routine. The estimator reuses the same first-order model to identify the + * process gain, time constant and bias from historical measurement and actuation data. This allows + * automatic tuning of the internal model parameters when operating on real process data without + * requiring external optimisation packages. *

*/ public class ModelPredictiveController extends NamedBaseClass implements ControllerDeviceInterface { @@ -82,10 +85,11 @@ public class ModelPredictiveController extends NamedBaseClass implements Control private transient MovingHorizonEstimate lastMovingHorizonEstimate; /** - * Representation of a quality constraint handled by the MPC. Each constraint links a measurement device with linear - * sensitivities describing how the quality responds to control actions, feed composition shifts and feed rate - * changes. Inequality limits are enforced softly by the quadratic program to keep the process within specification - * while still penalising unnecessary control effort. + * Representation of a quality constraint handled by the MPC. Each constraint links a measurement + * device with linear sensitivities describing how the quality responds to control actions, feed + * composition shifts and feed rate changes. Inequality limits are enforced softly by the + * quadratic program to keep the process within specification while still penalising unnecessary + * control effort. */ public static final class QualityConstraint { private final String name; @@ -102,11 +106,14 @@ public static final class QualityConstraint { private QualityConstraint(Builder builder) { this.name = builder.name; this.measurement = builder.measurement; - this.unit = builder.unit != null ? builder.unit : measurement != null ? measurement.getUnit() : "[?]"; + this.unit = + builder.unit != null ? builder.unit : measurement != null ? measurement.getUnit() : "[?]"; this.limit = builder.limit; this.margin = Math.max(0.0, builder.margin); - this.controlSensitivity = Arrays.copyOf(builder.controlSensitivity, builder.controlSensitivity.length); - this.compositionSensitivity = Collections.unmodifiableMap(new LinkedHashMap<>(builder.compositionSensitivity)); + this.controlSensitivity = + Arrays.copyOf(builder.controlSensitivity, builder.controlSensitivity.length); + this.compositionSensitivity = + Collections.unmodifiableMap(new LinkedHashMap<>(builder.compositionSensitivity)); this.rateSensitivity = builder.rateSensitivity; } @@ -127,10 +134,10 @@ public static Builder builder(String name) { double computeFeedEffect(Map deltaComposition, double deltaRate) { double effect = rateSensitivity * deltaRate; if (deltaComposition != null && !deltaComposition.isEmpty()) { - for (Map.Entry entry : compositionSensitivity.entrySet()) { - double delta = deltaComposition.getOrDefault(entry.getKey(), 0.0); - effect += entry.getValue() * delta; - } + for (Map.Entry entry : compositionSensitivity.entrySet()) { + double delta = deltaComposition.getOrDefault(entry.getKey(), 0.0); + effect += entry.getValue() * delta; + } } return effect; } @@ -185,86 +192,89 @@ public static final class Builder { private double rateSensitivity; private Builder(String name) { - if (name == null || name.trim().isEmpty()) { - throw new IllegalArgumentException("Constraint name must be provided"); - } - this.name = name; + if (name == null || name.trim().isEmpty()) { + throw new IllegalArgumentException("Constraint name must be provided"); + } + this.name = name; } public Builder measurement(MeasurementDeviceInterface device) { - this.measurement = device; - return this; + this.measurement = device; + return this; } public Builder unit(String unit) { - this.unit = unit; - return this; + this.unit = unit; + return this; } public Builder limit(double limit) { - if (!Double.isFinite(limit)) { - throw new IllegalArgumentException("Constraint limit must be finite"); - } - this.limit = limit; - return this; + if (!Double.isFinite(limit)) { + throw new IllegalArgumentException("Constraint limit must be finite"); + } + this.limit = limit; + return this; } public Builder margin(double margin) { - if (!Double.isFinite(margin) || margin < 0.0) { - throw new IllegalArgumentException("Constraint margin must be non-negative and finite"); - } - this.margin = margin; - return this; + if (!Double.isFinite(margin) || margin < 0.0) { + throw new IllegalArgumentException("Constraint margin must be non-negative and finite"); + } + this.margin = margin; + return this; } public Builder controlSensitivity(double... sensitivity) { - if (sensitivity == null || sensitivity.length == 0) { - throw new IllegalArgumentException("Control sensitivity must have at least one value"); - } - this.controlSensitivity = Arrays.copyOf(sensitivity, sensitivity.length); - return this; + if (sensitivity == null || sensitivity.length == 0) { + throw new IllegalArgumentException("Control sensitivity must have at least one value"); + } + this.controlSensitivity = Arrays.copyOf(sensitivity, sensitivity.length); + return this; } public Builder compositionSensitivity(String component, double sensitivity) { - if (component == null || component.trim().isEmpty()) { - throw new IllegalArgumentException("Component name must be provided"); - } - this.compositionSensitivity.put(component, sensitivity); - return this; + if (component == null || component.trim().isEmpty()) { + throw new IllegalArgumentException("Component name must be provided"); + } + this.compositionSensitivity.put(component, sensitivity); + return this; } public Builder compositionSensitivities(Map sensitivities) { - if (sensitivities != null) { - for (Map.Entry entry : sensitivities.entrySet()) { - compositionSensitivity.put(entry.getKey(), entry.getValue()); - } - } - return this; + if (sensitivities != null) { + for (Map.Entry entry : sensitivities.entrySet()) { + compositionSensitivity.put(entry.getKey(), entry.getValue()); + } + } + return this; } public Builder rateSensitivity(double sensitivity) { - this.rateSensitivity = sensitivity; - return this; + this.rateSensitivity = sensitivity; + return this; } public QualityConstraint build() { - if (measurement == null && (unit == null || unit.trim().isEmpty())) { - unit = "[?]"; - } - if (controlSensitivity.length == 0) { - throw new IllegalStateException("Control sensitivity must be defined for constraint '" + name + "'"); - } - if (!Double.isFinite(limit)) { - throw new IllegalStateException("Constraint limit must be finite for constraint '" + name + "'"); - } - return new QualityConstraint(this); + if (measurement == null && (unit == null || unit.trim().isEmpty())) { + unit = "[?]"; + } + if (controlSensitivity.length == 0) { + throw new IllegalStateException( + "Control sensitivity must be defined for constraint '" + name + "'"); + } + if (!Double.isFinite(limit)) { + throw new IllegalStateException( + "Constraint limit must be finite for constraint '" + name + "'"); + } + return new QualityConstraint(this); } } } /** - * Result from the moving horizon estimation routine. The estimate captures the identified first-order process - * parameters together with a mean squared prediction error and the number of samples used. + * Result from the moving horizon estimation routine. The estimate captures the identified + * first-order process parameters together with a mean squared prediction error and the number of + * samples used. */ public static final class MovingHorizonEstimate { private final double processGain; @@ -273,8 +283,8 @@ public static final class MovingHorizonEstimate { private final double meanSquaredError; private final int sampleCount; - private MovingHorizonEstimate(double processGain, double timeConstant, double processBias, double meanSquaredError, - int sampleCount) { + private MovingHorizonEstimate(double processGain, double timeConstant, double processBias, + double meanSquaredError, int sampleCount) { this.processGain = processGain; this.timeConstant = timeConstant; this.processBias = processBias; @@ -304,8 +314,9 @@ public int getSampleCount() { } /** - * Configuration options for the MPC auto-tuning routine. The parameters control how aggressive the closed-loop - * response should be as well as how the quadratic weights are scaled relative to the identified process model. + * Configuration options for the MPC auto-tuning routine. The parameters control how aggressive + * the closed-loop response should be as well as how the quadratic weights are scaled relative to + * the identified process model. */ public static final class AutoTuneConfiguration { private final double closedLoopTimeConstantRatio; @@ -382,94 +393,94 @@ public static final class Builder { private Double sampleTimeOverride; private boolean applyImmediately = true; - private Builder() { - } + private Builder() {} public Builder closedLoopTimeConstantRatio(double ratio) { - if (!Double.isFinite(ratio) || ratio <= 0.0) { - throw new IllegalArgumentException("Closed loop ratio must be positive and finite"); - } - this.closedLoopTimeConstantRatio = ratio; - return this; + if (!Double.isFinite(ratio) || ratio <= 0.0) { + throw new IllegalArgumentException("Closed loop ratio must be positive and finite"); + } + this.closedLoopTimeConstantRatio = ratio; + return this; } public Builder predictionHorizonMultiple(double multiple) { - if (!Double.isFinite(multiple) || multiple <= 0.0) { - throw new IllegalArgumentException("Prediction horizon multiple must be positive"); - } - this.predictionHorizonMultiple = multiple; - return this; + if (!Double.isFinite(multiple) || multiple <= 0.0) { + throw new IllegalArgumentException("Prediction horizon multiple must be positive"); + } + this.predictionHorizonMultiple = multiple; + return this; } public Builder controlWeightFactor(double factor) { - if (factor < 0.0) { - throw new IllegalArgumentException("Control weight factor must be non-negative"); - } - this.controlWeightFactor = factor; - return this; + if (factor < 0.0) { + throw new IllegalArgumentException("Control weight factor must be non-negative"); + } + this.controlWeightFactor = factor; + return this; } public Builder moveWeightFactor(double factor) { - if (factor < 0.0) { - throw new IllegalArgumentException("Move weight factor must be non-negative"); - } - this.moveWeightFactor = factor; - return this; + if (factor < 0.0) { + throw new IllegalArgumentException("Move weight factor must be non-negative"); + } + this.moveWeightFactor = factor; + return this; } public Builder outputWeight(double weight) { - if (weight < 0.0) { - throw new IllegalArgumentException("Output weight must be non-negative"); - } - this.outputWeight = weight; - return this; + if (weight < 0.0) { + throw new IllegalArgumentException("Output weight must be non-negative"); + } + this.outputWeight = weight; + return this; } public Builder minimumHorizon(int horizon) { - if (horizon <= 0) { - throw new IllegalArgumentException("Minimum horizon must be positive"); - } - this.minimumHorizon = horizon; - return this; + if (horizon <= 0) { + throw new IllegalArgumentException("Minimum horizon must be positive"); + } + this.minimumHorizon = horizon; + return this; } public Builder maximumHorizon(int horizon) { - if (horizon <= 0) { - throw new IllegalArgumentException("Maximum horizon must be positive"); - } - this.maximumHorizon = horizon; - return this; + if (horizon <= 0) { + throw new IllegalArgumentException("Maximum horizon must be positive"); + } + this.maximumHorizon = horizon; + return this; } public Builder sampleTimeOverride(Double sampleTime) { - if (sampleTime != null && (!Double.isFinite(sampleTime) || sampleTime <= 0.0)) { - throw new IllegalArgumentException("Sample time override must be positive and finite"); - } - this.sampleTimeOverride = sampleTime; - return this; + if (sampleTime != null && (!Double.isFinite(sampleTime) || sampleTime <= 0.0)) { + throw new IllegalArgumentException("Sample time override must be positive and finite"); + } + this.sampleTimeOverride = sampleTime; + return this; } public Builder applyImmediately(boolean apply) { - this.applyImmediately = apply; - return this; + this.applyImmediately = apply; + return this; } public Builder defaults() { - return this; + return this; } public AutoTuneConfiguration build() { - if (maximumHorizon < minimumHorizon) { - throw new IllegalStateException("Maximum horizon must be at least the minimum horizon"); - } - return new AutoTuneConfiguration(this); + if (maximumHorizon < minimumHorizon) { + throw new IllegalStateException("Maximum horizon must be at least the minimum horizon"); + } + return new AutoTuneConfiguration(this); } } } /** - * Result produced by the auto-tuning routine. The result captures the identified model parameters, recommended - * controller weights and diagnostic information about the estimation data that was used. + * Result produced by the auto-tuning routine. The result captures the identified model + * parameters, recommended controller weights and diagnostic information about the estimation data + * that was used. */ public static final class AutoTuneResult { private final double processGain; @@ -485,9 +496,10 @@ public static final class AutoTuneResult { private final int sampleCount; private final boolean applied; - private AutoTuneResult(double processGain, double timeConstant, double processBias, double outputWeight, - double controlWeight, double moveWeight, int predictionHorizon, double sampleTime, - double closedLoopTimeConstant, double meanSquaredError, int sampleCount, boolean applied) { + private AutoTuneResult(double processGain, double timeConstant, double processBias, + double outputWeight, double controlWeight, double moveWeight, int predictionHorizon, + double sampleTime, double closedLoopTimeConstant, double meanSquaredError, int sampleCount, + boolean applied) { this.processGain = processGain; this.timeConstant = timeConstant; this.processBias = processBias; @@ -568,8 +580,8 @@ public ModelPredictiveController(String name) { } /** - * Configure the set of manipulated variables handled by the MPC. Existing quality constraints are cleared because - * their sensitivity dimensions may no longer match the new set of controls. + * Configure the set of manipulated variables handled by the MPC. Existing quality constraints are + * cleared because their sensitivity dimensions may no longer match the new set of controls. * * @param names ordered list of control names (e.g. pressure, temperature) */ @@ -581,7 +593,7 @@ public final void configureControls(String... names) { controlNames.clear(); for (String name : names) { if (name == null || name.trim().isEmpty()) { - throw new IllegalArgumentException("Control names must be non-empty"); + throw new IllegalArgumentException("Control names must be non-empty"); } controlNames.add(name); } @@ -610,8 +622,8 @@ public final void configureControls(String... names) { private void ensureControlLength(int expectedLength) { if (controlVector.length != expectedLength) { - throw new IllegalStateException( - "Controller configured for " + controlVector.length + " controls but received " + expectedLength); + throw new IllegalStateException("Controller configured for " + controlVector.length + + " controls but received " + expectedLength); } } @@ -636,8 +648,8 @@ public void setInitialControlValues(double... values) { } /** - * Choose which control variable is exposed via {@link #getResponse()} to maintain compatibility with the - * {@link ControllerDeviceInterface}. + * Choose which control variable is exposed via {@link #getResponse()} to maintain compatibility + * with the {@link ControllerDeviceInterface}. * * @param index index of the primary control variable */ @@ -665,8 +677,8 @@ public void setControlLimits(int index, double min, double max) { } /** - * Constrain the permitted change of a control variable relative to the previously applied value. Limits are - * interpreted as {@code minDelta <= u - u_prev <= maxDelta}. + * Constrain the permitted change of a control variable relative to the previously applied value. + * Limits are interpreted as {@code minDelta <= u - u_prev <= maxDelta}. * * @param index control index * @param minDelta minimum permitted change (may be {@link Double#NEGATIVE_INFINITY}) @@ -712,7 +724,8 @@ public void setControlLimits(String controlName, double min, double max) { } /** - * Set quadratic weights on the absolute value of each control variable. Values must be non-negative. + * Set quadratic weights on the absolute value of each control variable. Values must be + * non-negative. * * @param weights absolute control weights */ @@ -723,7 +736,7 @@ public void setControlWeights(double... weights) { ensureControlLength(weights.length); for (int i = 0; i < weights.length; i++) { if (weights[i] < 0.0) { - throw new IllegalArgumentException("Control weights must be non-negative"); + throw new IllegalArgumentException("Control weights must be non-negative"); } controlWeightsVector[i] = weights[i]; } @@ -741,15 +754,16 @@ public void setMoveWeights(double... weights) { ensureControlLength(weights.length); for (int i = 0; i < weights.length; i++) { if (weights[i] < 0.0) { - throw new IllegalArgumentException("Move weights must be non-negative"); + throw new IllegalArgumentException("Move weights must be non-negative"); } moveWeightsVector[i] = weights[i]; } } /** - * Define the preferred steady-state operating point for each control variable. This represents the control vector - * that minimises the absolute control penalty when no tracking error is present. + * Define the preferred steady-state operating point for each control variable. This represents + * the control vector that minimises the absolute control penalty when no tracking error is + * present. * * @param references preferred control levels */ @@ -762,8 +776,9 @@ public void setPreferredControlVector(double... references) { } /** - * @deprecated Use {@link #setPreferredControlVector(double...)} to configure the nominal control point. This method - * is retained for backwards compatibility with earlier snapshots of the MPC implementation. + * @deprecated Use {@link #setPreferredControlVector(double...)} to configure the nominal control + * point. This method is retained for backwards compatibility with earlier snapshots + * of the MPC implementation. * * @param references preferred control levels for the energy terms */ @@ -816,7 +831,8 @@ public double[] getControlVector() { } /** - * Register a new quality constraint. The sensitivity vector must match the number of configured controls. + * Register a new quality constraint. The sensitivity vector must match the number of configured + * controls. * * @param constraint quality constraint description */ @@ -837,10 +853,10 @@ public void clearQualityConstraints() { } /** - * Update the stored measurement for a named quality constraint. When integrating against a live plant this allows the - * MPC to use the latest analyser or laboratory sample even if the simulation does not contain a dedicated - * {@link MeasurementDeviceInterface}. The value is stored in the controller and will be used as the baseline for the - * next optimisation step. + * Update the stored measurement for a named quality constraint. When integrating against a live + * plant this allows the MPC to use the latest analyser or laboratory sample even if the + * simulation does not contain a dedicated {@link MeasurementDeviceInterface}. The value is stored + * in the controller and will be used as the baseline for the next optimisation step. * * @param name quality constraint identifier * @param measurement measured specification value in the constraint unit @@ -852,9 +868,9 @@ public boolean updateQualityMeasurement(String name, double measurement) { } for (QualityConstraint constraint : qualityConstraints) { if (name.equals(constraint.getName())) { - constraint.setLastMeasurement(Double.isFinite(measurement) ? measurement : Double.NaN); - predictedQualityValues.remove(name); - return true; + constraint.setLastMeasurement(Double.isFinite(measurement) ? measurement : Double.NaN); + predictedQualityValues.remove(name); + return true; } } return false; @@ -875,8 +891,8 @@ public void updateQualityMeasurements(Map measurements) { } /** - * Update the predicted incoming feed composition and flow rate. The values are used as feedforward information in the - * MPC optimisation. + * Update the predicted incoming feed composition and flow rate. The values are used as + * feedforward information in the MPC optimisation. * * @param composition component molar fractions (will be normalised outside the method) * @param feedRate molar feed rate @@ -885,7 +901,7 @@ public void updateFeedConditions(Map composition, double feedRat Map copy = new LinkedHashMap<>(); if (composition != null) { for (Map.Entry entry : composition.entrySet()) { - copy.put(entry.getKey(), entry.getValue()); + copy.put(entry.getKey(), entry.getValue()); } } pendingFeedComposition = copy; @@ -911,17 +927,17 @@ public double getPredictedQuality(String name) { } /** - * Enable moving horizon (receding horizon) estimation of the internal first-order process model. The estimator - * analyses the most recent samples of measured output and applied control to update the process gain, time constant - * and bias. + * Enable moving horizon (receding horizon) estimation of the internal first-order process model. + * The estimator analyses the most recent samples of measured output and applied control to update + * the process gain, time constant and bias. * * @param windowSize number of recent samples to keep in the estimation window (minimum of - * {@value #MIN_ESTIMATION_SAMPLES}) + * {@value #MIN_ESTIMATION_SAMPLES}) */ public void enableMovingHorizonEstimation(int windowSize) { if (windowSize < MIN_ESTIMATION_SAMPLES) { throw new IllegalArgumentException( - "Estimation window must contain at least " + MIN_ESTIMATION_SAMPLES + " samples"); + "Estimation window must contain at least " + MIN_ESTIMATION_SAMPLES + " samples"); } this.movingHorizonWindow = windowSize; this.movingHorizonEstimationEnabled = true; @@ -929,8 +945,8 @@ public void enableMovingHorizonEstimation(int windowSize) { } /** - * Disable the moving horizon estimation routine. Existing history and the last estimate are retained so the method - * can be re-enabled later. + * Disable the moving horizon estimation routine. Existing history and the last estimate are + * retained so the method can be re-enabled later. */ public void disableMovingHorizonEstimation() { this.movingHorizonEstimationEnabled = false; @@ -955,8 +971,8 @@ public int getMovingHorizonEstimationWindow() { } /** - * Remove any stored estimation samples. The last estimate is cleared to make it explicit that a new identification - * cycle is required before accessing estimation results again. + * Remove any stored estimation samples. The last estimate is cleared to make it explicit that a + * new identification cycle is required before accessing estimation results again. */ public void clearMovingHorizonHistory() { estimationMeasurements.clear(); @@ -966,9 +982,9 @@ public void clearMovingHorizonHistory() { } /** - * Retrieve the latest moving horizon estimate. The result contains the identified model parameters along with a - * simple mean squared prediction error. {@code null} is returned until the estimator has processed a sufficient - * number of samples. + * Retrieve the latest moving horizon estimate. The result contains the identified model + * parameters along with a simple mean squared prediction error. {@code null} is returned until + * the estimator has processed a sufficient number of samples. * * @return most recent estimate or {@code null} when unavailable */ @@ -977,9 +993,10 @@ public MovingHorizonEstimate getLastMovingHorizonEstimate() { } /** - * Automatically identify the internal first-order process model and configure the MPC weights using the most recent - * moving-horizon estimation history. The controller must have collected at least {@value #MIN_ESTIMATION_SAMPLES} - * valid samples via {@link #enableMovingHorizonEstimation(int)} before invoking auto-tune. + * Automatically identify the internal first-order process model and configure the MPC weights + * using the most recent moving-horizon estimation history. The controller must have collected at + * least {@value #MIN_ESTIMATION_SAMPLES} valid samples via + * {@link #enableMovingHorizonEstimation(int)} before invoking auto-tune. * * @return tuning result containing the identified parameters and applied configuration */ @@ -988,33 +1005,37 @@ public AutoTuneResult autoTune() { } /** - * Automatically identify the internal first-order process model and configure the MPC weights using the most recent - * moving-horizon estimation history. + * Automatically identify the internal first-order process model and configure the MPC weights + * using the most recent moving-horizon estimation history. * - * @param configuration optional tuning configuration; if {@code null} the default configuration is used + * @param configuration optional tuning configuration; if {@code null} the default configuration + * is used * @return tuning result containing the identified parameters and applied configuration */ public AutoTuneResult autoTune(AutoTuneConfiguration configuration) { - AutoTuneConfiguration config = configuration != null ? configuration : AutoTuneConfiguration.builder().build(); + AutoTuneConfiguration config = + configuration != null ? configuration : AutoTuneConfiguration.builder().build(); MovingHorizonEstimate estimate = lastMovingHorizonEstimate; if (estimate == null) { estimate = estimateFromHistory(); if (estimate != null) { - lastMovingHorizonEstimate = estimate; + lastMovingHorizonEstimate = estimate; } } if (estimate == null) { - throw new IllegalStateException("Auto-tune requires at least " + MIN_ESTIMATION_SAMPLES + " valid samples"); + throw new IllegalStateException( + "Auto-tune requires at least " + MIN_ESTIMATION_SAMPLES + " valid samples"); } return autoTuneFromEstimate(estimate, config); } /** - * Auto-tune the controller using explicitly supplied measurement and actuation samples. This is useful when - * historical process data has been collected outside of the live controller instance. The provided lists must follow - * the same structure as the moving-horizon estimator where {@code measurements.size() == controls.size() + 1} and + * Auto-tune the controller using explicitly supplied measurement and actuation samples. This is + * useful when historical process data has been collected outside of the live controller instance. + * The provided lists must follow the same structure as the moving-horizon estimator where + * {@code measurements.size() == controls.size() + 1} and * {@code sampleTimes.size() == controls.size()}. * * @param measurements ordered list of measured process values @@ -1023,22 +1044,25 @@ public AutoTuneResult autoTune(AutoTuneConfiguration configuration) { * @param configuration optional tuning configuration * @return tuning result containing the identified parameters and applied configuration */ - public AutoTuneResult autoTune(List measurements, List controls, List sampleTimes, - AutoTuneConfiguration configuration) { + public AutoTuneResult autoTune(List measurements, List controls, + List sampleTimes, AutoTuneConfiguration configuration) { Objects.requireNonNull(measurements, "Measurement history must be supplied"); Objects.requireNonNull(controls, "Control history must be supplied"); Objects.requireNonNull(sampleTimes, "Sample time history must be supplied"); - AutoTuneConfiguration config = configuration != null ? configuration : AutoTuneConfiguration.builder().build(); + AutoTuneConfiguration config = + configuration != null ? configuration : AutoTuneConfiguration.builder().build(); MovingHorizonEstimate estimate = estimateFromSamples(measurements, controls, sampleTimes); if (estimate == null) { - throw new IllegalArgumentException("Insufficient or invalid samples supplied for auto-tuning"); + throw new IllegalArgumentException( + "Insufficient or invalid samples supplied for auto-tuning"); } lastMovingHorizonEstimate = estimate; return autoTuneFromEstimate(estimate, config); } - private AutoTuneResult autoTuneFromEstimate(MovingHorizonEstimate estimate, AutoTuneConfiguration config) { + private AutoTuneResult autoTuneFromEstimate(MovingHorizonEstimate estimate, + AutoTuneConfiguration config) { Double override = config.getSampleTimeOverride(); double sampleTime = override != null ? override : lastSampleTime; if (!Double.isFinite(sampleTime) || sampleTime <= 0.0) { @@ -1049,7 +1073,8 @@ private AutoTuneResult autoTuneFromEstimate(MovingHorizonEstimate estimate, Auto double gain = estimate.getProcessGain(); double bias = estimate.getProcessBias(); - double closedLoopTimeConstant = Math.max(sampleTime, config.getClosedLoopTimeConstantRatio() * timeConstant); + double closedLoopTimeConstant = + Math.max(sampleTime, config.getClosedLoopTimeConstantRatio() * timeConstant); double horizonSeconds = config.getPredictionHorizonMultiple() * timeConstant; int horizon = (int) Math.ceil(horizonSeconds / sampleTime); @@ -1057,7 +1082,8 @@ private AutoTuneResult autoTuneFromEstimate(MovingHorizonEstimate estimate, Auto double outputWeight = config.getOutputWeight(); double gainMagnitude = Math.max(Math.abs(gain), 1.0e-6); - double controlWeight = config.getControlWeightFactor() * sampleTime / (gainMagnitude * closedLoopTimeConstant); + double controlWeight = + config.getControlWeightFactor() * sampleTime / (gainMagnitude * closedLoopTimeConstant); double moveWeight = config.getMoveWeightFactor() * sampleTime / closedLoopTimeConstant; if (!Double.isFinite(controlWeight) || controlWeight < 0.0) { controlWeight = 0.0; @@ -1076,8 +1102,9 @@ private AutoTuneResult autoTuneFromEstimate(MovingHorizonEstimate estimate, Auto applied = true; } - return new AutoTuneResult(gain, timeConstant, bias, outputWeight, controlWeight, moveWeight, horizon, sampleTime, - closedLoopTimeConstant, estimate.getMeanSquaredError(), estimate.getSampleCount(), applied); + return new AutoTuneResult(gain, timeConstant, bias, outputWeight, controlWeight, moveWeight, + horizon, sampleTime, closedLoopTimeConstant, estimate.getMeanSquaredError(), + estimate.getSampleCount(), applied); } /** @@ -1098,8 +1125,9 @@ public void setProcessModel(double gain, double timeConstant) { } /** - * Configure the internal first order process model including dead time. Dead time is represented as an equivalent - * time constant increase because the simplified controller does not maintain an explicit delay line. + * Configure the internal first order process model including dead time. Dead time is represented + * as an equivalent time constant increase because the simplified controller does not maintain an + * explicit delay line. * * @param gain steady-state process gain relating control action to the measured variable * @param timeConstant dominant time constant of the process model (seconds) @@ -1111,8 +1139,8 @@ public void setProcessModel(double gain, double timeConstant, double deadTimeSec } /** - * Set the steady-state bias of the process model. The bias corresponds to the measured value when the manipulated - * variable is zero (for example ambient temperature). + * Set the steady-state bias of the process model. The bias corresponds to the measured value when + * the manipulated variable is zero (for example ambient temperature). * * @param bias process bias value */ @@ -1140,8 +1168,9 @@ public void setWeights(double outputWeight, double controlWeight, double moveWei } /** - * Specify the preferred steady-state control level for the single-input MPC mode. This value represents the operating - * point that minimises the absolute control penalty when tracking is not active. + * Specify the preferred steady-state control level for the single-input MPC mode. This value + * represents the operating point that minimises the absolute control penalty when tracking is not + * active. * * @param reference preferred control level */ @@ -1167,8 +1196,8 @@ public void setMoveLimits(double minDelta, double maxDelta) { } /** - * @deprecated Use {@link #setPreferredControlValue(double)} when configuring the MPC economic target. This method is - * kept for compatibility with earlier code samples. + * @deprecated Use {@link #setPreferredControlValue(double)} when configuring the MPC economic + * target. This method is kept for compatibility with earlier code samples. * * @param reference preferred steady-state control value for the single-input controller */ @@ -1252,10 +1281,11 @@ public void setTransmitter(MeasurementDeviceInterface device) { } /** - * Inject a measurement collected directly from a physical plant rather than the built-in transmitter abstraction. - * This is useful when the MPC is connected to a live facility where instrumentation values arrive asynchronously from - * the control system. The sample updates the diagnostic state of the controller and provides a fallback measurement - * if no transmitter is configured. + * Inject a measurement collected directly from a physical plant rather than the built-in + * transmitter abstraction. This is useful when the MPC is connected to a live facility where + * instrumentation values arrive asynchronously from the control system. The sample updates the + * diagnostic state of the controller and provides a fallback measurement if no transmitter is + * configured. * * @param measurement latest measured process value * @param appliedControl control signal that was active when the sample was taken @@ -1275,8 +1305,8 @@ public void ingestPlantSample(double measurement, double appliedControl, double } /** - * Convenience overload of {@link #ingestPlantSample(double, double, double)} when only the measurement and applied - * control are known. + * Convenience overload of {@link #ingestPlantSample(double, double, double)} when only the + * measurement and applied control are known. * * @param measurement measured process value * @param appliedControl applied control signal @@ -1291,10 +1321,10 @@ public void runTransient(double initResponse, double dt, UUID id) { lastSampleTime = dt; if (!qualityConstraints.isEmpty()) { if (!isActive) { - clampControlVector(); - response = controlVector[Math.min(primaryControlIndex, controlVector.length - 1)]; - lastAppliedControl = response; - return; + clampControlVector(); + response = controlVector[Math.min(primaryControlIndex, controlVector.length - 1)]; + lastAppliedControl = response; + return; } runMultivariable(); return; @@ -1331,19 +1361,21 @@ private void clampControlVector() { for (int i = 0; i < controlVector.length; i++) { double value = controlVector[i]; if (!Double.isInfinite(minControlVector[i])) { - value = Math.max(minControlVector[i], value); + value = Math.max(minControlVector[i], value); } if (!Double.isInfinite(maxControlVector[i])) { - value = Math.min(maxControlVector[i], value); + value = Math.min(maxControlVector[i], value); } - double minMoveLimit = minControlMoveVector.length > i ? minControlMoveVector[i] : Double.NEGATIVE_INFINITY; - double maxMoveLimit = maxControlMoveVector.length > i ? maxControlMoveVector[i] : Double.POSITIVE_INFINITY; + double minMoveLimit = + minControlMoveVector.length > i ? minControlMoveVector[i] : Double.NEGATIVE_INFINITY; + double maxMoveLimit = + maxControlMoveVector.length > i ? maxControlMoveVector[i] : Double.POSITIVE_INFINITY; double previous = lastControlVector.length > i ? lastControlVector[i] : 0.0; if (!Double.isInfinite(minMoveLimit)) { - value = Math.max(previous + minMoveLimit, value); + value = Math.max(previous + minMoveLimit, value); } if (!Double.isInfinite(maxMoveLimit)) { - value = Math.min(previous + maxMoveLimit, value); + value = Math.min(previous + maxMoveLimit, value); } controlVector[i] = value; } @@ -1364,12 +1396,12 @@ private void runMultivariable() { keys.addAll(lastFeedComposition.keySet()); keys.addAll(pendingFeedComposition.keySet()); for (String key : keys) { - double future = pendingFeedComposition.getOrDefault(key, 0.0); - double past = lastFeedComposition.getOrDefault(key, 0.0); - double delta = future - past; - if (Math.abs(delta) > 1.0e-12) { - deltaComposition.put(key, delta); - } + double future = pendingFeedComposition.getOrDefault(key, 0.0); + double past = lastFeedComposition.getOrDefault(key, 0.0); + double delta = future - past; + if (Math.abs(delta) > 1.0e-12) { + deltaComposition.put(key, delta); + } } deltaRate = pendingFeedRate - lastFeedRate; } @@ -1384,22 +1416,22 @@ private void runMultivariable() { double measurement = constraint.getLastMeasurement(); MeasurementDeviceInterface device = constraint.getMeasurement(); if (device != null) { - try { - double measured = constraint.getUnit() == null ? device.getMeasuredValue() - : device.getMeasuredValue(constraint.getUnit()); - if (Double.isFinite(measured)) { - measurement = measured; - } - } catch (Exception ex) { - // ignore measurement exceptions and fall back to last value - } + try { + double measured = constraint.getUnit() == null ? device.getMeasuredValue() + : device.getMeasuredValue(constraint.getUnit()); + if (Double.isFinite(measured)) { + measurement = measured; + } + } catch (Exception ex) { + // ignore measurement exceptions and fall back to last value + } } if (!Double.isFinite(measurement)) { - measurement = constraint.getLimit(); + measurement = constraint.getLimit(); } constraint.setLastMeasurement(measurement); if (idx == 0) { - lastSampledValue = measurement; + lastSampledValue = measurement; } double[] sensitivity = constraint.getControlSensitivity(); @@ -1409,25 +1441,26 @@ private void runMultivariable() { double deviation = futureMeasurement - target; double normSquared = 0.0; for (double value : sensitivity) { - normSquared += value * value; + normSquared += value * value; } if (normSquared > 1.0e-12 && Math.abs(deviation) > 1.0e-9) { - double scale = deviation / normSquared; - for (int i = 0; i < controlCount; i++) { - double desiredDelta = scale * sensitivity[i]; - double diagonalWeight = Math.max(controlWeightsVector[i], 0.0) + Math.max(moveWeightsVector[i], 0.0); - if (diagonalWeight < 1.0e-9) { - diagonalWeight = 1.0e-9; - } - feedForwardGradient[i] += diagonalWeight * desiredDelta; - } + double scale = deviation / normSquared; + for (int i = 0; i < controlCount; i++) { + double desiredDelta = scale * sensitivity[i]; + double diagonalWeight = + Math.max(controlWeightsVector[i], 0.0) + Math.max(moveWeightsVector[i], 0.0); + if (diagonalWeight < 1.0e-9) { + diagonalWeight = 1.0e-9; + } + feedForwardGradient[i] += diagonalWeight * desiredDelta; + } } double rhs = constraint.getLimit() - constraint.getMargin() - futureMeasurement; double[] row = new double[controlCount]; double dotPrev = 0.0; for (int i = 0; i < controlCount; i++) { - row[i] = sensitivity[i]; - dotPrev += sensitivity[i] * previousControl[i]; + row[i] = sensitivity[i]; + dotPrev += sensitivity[i] * previousControl[i]; } constraintRows.add(row); constraintBounds.add(rhs + dotPrev); @@ -1435,30 +1468,32 @@ private void runMultivariable() { for (int i = 0; i < controlCount; i++) { if (!Double.isInfinite(minControlVector[i])) { - double[] row = new double[controlCount]; - row[i] = -1.0; - constraintRows.add(row); - constraintBounds.add(-minControlVector[i]); + double[] row = new double[controlCount]; + row[i] = -1.0; + constraintRows.add(row); + constraintBounds.add(-minControlVector[i]); } if (!Double.isInfinite(maxControlVector[i])) { - double[] row = new double[controlCount]; - row[i] = 1.0; - constraintRows.add(row); - constraintBounds.add(maxControlVector[i]); - } - double minMoveLimit = minControlMoveVector.length > i ? minControlMoveVector[i] : Double.NEGATIVE_INFINITY; - double maxMoveLimit = maxControlMoveVector.length > i ? maxControlMoveVector[i] : Double.POSITIVE_INFINITY; + double[] row = new double[controlCount]; + row[i] = 1.0; + constraintRows.add(row); + constraintBounds.add(maxControlVector[i]); + } + double minMoveLimit = + minControlMoveVector.length > i ? minControlMoveVector[i] : Double.NEGATIVE_INFINITY; + double maxMoveLimit = + maxControlMoveVector.length > i ? maxControlMoveVector[i] : Double.POSITIVE_INFINITY; if (!Double.isInfinite(maxMoveLimit)) { - double[] row = new double[controlCount]; - row[i] = 1.0; - constraintRows.add(row); - constraintBounds.add(previousControl[i] + maxMoveLimit); + double[] row = new double[controlCount]; + row[i] = 1.0; + constraintRows.add(row); + constraintBounds.add(previousControl[i] + maxMoveLimit); } if (!Double.isInfinite(minMoveLimit)) { - double[] row = new double[controlCount]; - row[i] = -1.0; - constraintRows.add(row); - constraintBounds.add(-(previousControl[i] + minMoveLimit)); + double[] row = new double[controlCount]; + row[i] = -1.0; + constraintRows.add(row); + constraintBounds.add(-(previousControl[i] + minMoveLimit)); } } @@ -1475,14 +1510,15 @@ private void runMultivariable() { double moveWeight = Math.max(moveWeightsVector[i], 0.0); double diagonal = absoluteWeight + moveWeight; if (diagonal < 1.0e-9) { - diagonal = 1.0e-9; + diagonal = 1.0e-9; } hessian[i][i] = diagonal; gradient[i] = -absoluteWeight * preferredControlVector[i] - moveWeight * previousControl[i] - + feedForwardGradient[i]; + + feedForwardGradient[i]; } - double[] solution = solveQuadraticProgram(hessian, gradient, constraintMatrix, constraintVector); + double[] solution = + solveQuadraticProgram(hessian, gradient, constraintMatrix, constraintVector); if (solution == null) { solution = Arrays.copyOf(previousControl, controlCount); } @@ -1490,18 +1526,20 @@ private void runMultivariable() { for (int i = 0; i < controlCount; i++) { double value = solution[i]; if (!Double.isInfinite(minControlVector[i])) { - value = Math.max(minControlVector[i], value); + value = Math.max(minControlVector[i], value); } if (!Double.isInfinite(maxControlVector[i])) { - value = Math.min(maxControlVector[i], value); + value = Math.min(maxControlVector[i], value); } - double minMoveLimit = minControlMoveVector.length > i ? minControlMoveVector[i] : Double.NEGATIVE_INFINITY; - double maxMoveLimit = maxControlMoveVector.length > i ? maxControlMoveVector[i] : Double.POSITIVE_INFINITY; + double minMoveLimit = + minControlMoveVector.length > i ? minControlMoveVector[i] : Double.NEGATIVE_INFINITY; + double maxMoveLimit = + maxControlMoveVector.length > i ? maxControlMoveVector[i] : Double.POSITIVE_INFINITY; if (!Double.isInfinite(minMoveLimit)) { - value = Math.max(previousControl[i] + minMoveLimit, value); + value = Math.max(previousControl[i] + minMoveLimit, value); } if (!Double.isInfinite(maxMoveLimit)) { - value = Math.min(previousControl[i] + maxMoveLimit, value); + value = Math.min(previousControl[i] + maxMoveLimit, value); } controlVector[i] = value; } @@ -1513,8 +1551,8 @@ private void runMultivariable() { double dotNew = 0.0; double dotPrev = 0.0; for (int i = 0; i < controlCount; i++) { - dotNew += sensitivity[i] * controlVector[i]; - dotPrev += sensitivity[i] * previousControl[i]; + dotNew += sensitivity[i] * controlVector[i]; + dotPrev += sensitivity[i] * previousControl[i]; } double predicted = measurement + (dotNew - dotPrev) + feedEffect; constraint.setPredictedValue(predicted); @@ -1533,7 +1571,8 @@ private void recordEstimationSample(double measurement, double appliedControl, d if (!movingHorizonEstimationEnabled) { return; } - if (!Double.isFinite(measurement) || !Double.isFinite(appliedControl) || !Double.isFinite(dt) || dt <= 0.0) { + if (!Double.isFinite(measurement) || !Double.isFinite(appliedControl) || !Double.isFinite(dt) + || dt <= 0.0) { return; } if (estimationMeasurements.isEmpty()) { @@ -1541,7 +1580,7 @@ private void recordEstimationSample(double measurement, double appliedControl, d return; } if (estimationMeasurements.size() != estimationControls.size() + 1 - || estimationSampleTimes.size() != estimationControls.size()) { + || estimationSampleTimes.size() != estimationControls.size()) { clearMovingHorizonHistory(); estimationMeasurements.add(measurement); return; @@ -1555,7 +1594,7 @@ private void recordEstimationSample(double measurement, double appliedControl, d estimationControls.remove(0); estimationSampleTimes.remove(0); if (!estimationMeasurements.isEmpty()) { - estimationMeasurements.remove(0); + estimationMeasurements.remove(0); } } @@ -1565,8 +1604,8 @@ private void recordEstimationSample(double measurement, double appliedControl, d } private void updateMovingHorizonEstimate() { - MovingHorizonEstimate estimate = estimateFromSamples(estimationMeasurements, estimationControls, - estimationSampleTimes); + MovingHorizonEstimate estimate = + estimateFromSamples(estimationMeasurements, estimationControls, estimationSampleTimes); if (estimate == null) { return; } @@ -1579,12 +1618,12 @@ private void updateMovingHorizonEstimate() { timeConstant = estimatedTimeConstant; processBias = estimatedBias; - lastMovingHorizonEstimate = new MovingHorizonEstimate(estimatedGain, estimatedTimeConstant, estimatedBias, - estimate.getMeanSquaredError(), estimate.getSampleCount()); + lastMovingHorizonEstimate = new MovingHorizonEstimate(estimatedGain, estimatedTimeConstant, + estimatedBias, estimate.getMeanSquaredError(), estimate.getSampleCount()); } - private MovingHorizonEstimate estimateFromSamples(List measurements, List controls, - List sampleTimes) { + private MovingHorizonEstimate estimateFromSamples(List measurements, + List controls, List sampleTimes) { if (measurements == null || controls == null || sampleTimes == null) { return null; } @@ -1614,16 +1653,16 @@ private MovingHorizonEstimate estimateFromSamples(List measurements, Lis double nextMeasurement = measurements.get(i + 1); double dt = sampleTimes.get(i); if (!Double.isFinite(dt) || dt <= 0.0) { - return null; + return null; } double rateOfChange = (nextMeasurement - measurement) / dt; - double[] row = { measurement / measurementScale, control / controlScale, 1.0 }; + double[] row = {measurement / measurementScale, control / controlScale, 1.0}; for (int rowIndex = 0; rowIndex < 3; rowIndex++) { - double value = row[rowIndex]; - for (int colIndex = 0; colIndex < 3; colIndex++) { - normal[rowIndex][colIndex] += value * row[colIndex]; - } - rhs[rowIndex] += value * rateOfChange; + double value = row[rowIndex]; + for (int colIndex = 0; colIndex < 3; colIndex++) { + normal[rowIndex][colIndex] += value * row[colIndex]; + } + rhs[rowIndex] += value * rateOfChange; } } @@ -1665,7 +1704,7 @@ private MovingHorizonEstimate estimateFromSamples(List measurements, Lis double nextMeasurement = measurements.get(i + 1); double dt = sampleTimes.get(i); if (!Double.isFinite(dt) || dt <= 0.0) { - return null; + return null; } double predictedRate = alpha * measurement + beta * control + gamma; double predictedNext = measurement + dt * predictedRate; @@ -1683,20 +1722,20 @@ private MovingHorizonEstimate estimateFromHistory() { return null; } if (estimationMeasurements.size() != estimationControls.size() + 1 - || estimationSampleTimes.size() != estimationControls.size()) { + || estimationSampleTimes.size() != estimationControls.size()) { return null; } return estimateFromSamples(estimationMeasurements, estimationControls, estimationSampleTimes); } - private double[] solveQuadraticProgram(double[][] hessian, double[] gradient, double[][] constraints, - double[] bounds) { + private double[] solveQuadraticProgram(double[][] hessian, double[] gradient, + double[][] constraints, double[] bounds) { int n = gradient.length; double[] inverseDiagonal = new double[n]; for (int i = 0; i < n; i++) { double value = hessian[i][i]; if (value < 1.0e-12) { - value = 1.0e-12; + value = 1.0e-12; } inverseDiagonal[i] = 1.0 / value; } @@ -1715,23 +1754,24 @@ private double[] solveQuadraticProgram(double[][] hessian, double[] gradient, do if (constraintCount > 0) { int combinations = 1 << constraintCount; for (int mask = 1; mask < combinations; mask++) { - if (Integer.bitCount(mask) > n) { - continue; - } - double[][] activeConstraints = buildActiveMatrix(constraints, mask); - double[] activeBounds = buildActiveVector(bounds, mask); - double[] candidate = solveEqualityConstrained(inverseDiagonal, gradient, activeConstraints, activeBounds); - if (candidate == null) { - continue; - } - if (!isFeasible(candidate, constraints, bounds)) { - continue; - } - double objective = objectiveValue(hessian, gradient, candidate); - if (objective < bestObjective) { - bestObjective = objective; - bestSolution = candidate; - } + if (Integer.bitCount(mask) > n) { + continue; + } + double[][] activeConstraints = buildActiveMatrix(constraints, mask); + double[] activeBounds = buildActiveVector(bounds, mask); + double[] candidate = + solveEqualityConstrained(inverseDiagonal, gradient, activeConstraints, activeBounds); + if (candidate == null) { + continue; + } + if (!isFeasible(candidate, constraints, bounds)) { + continue; + } + double objective = objectiveValue(hessian, gradient, candidate); + if (objective < bestObjective) { + bestObjective = objective; + bestSolution = candidate; + } } } @@ -1742,24 +1782,24 @@ private double[] solveQuadraticProgram(double[][] hessian, double[] gradient, do double[] fallback = Arrays.copyOf(unconstrained, n); if (constraints != null && bounds != null) { for (int row = 0; row < constraints.length; row++) { - double[] constraint = constraints[row]; - double violation = 0.0; - for (int i = 0; i < n; i++) { - violation += constraint[i] * fallback[i]; - } - violation -= bounds[row]; - if (violation > 0.0) { - double norm = 0.0; - for (double coefficient : constraint) { - norm += coefficient * coefficient; - } - if (norm > 1.0e-12) { - double factor = violation / norm; - for (int i = 0; i < n; i++) { - fallback[i] -= factor * constraint[i]; - } - } - } + double[] constraint = constraints[row]; + double violation = 0.0; + for (int i = 0; i < n; i++) { + violation += constraint[i] * fallback[i]; + } + violation -= bounds[row]; + if (violation > 0.0) { + double norm = 0.0; + for (double coefficient : constraint) { + norm += coefficient * coefficient; + } + if (norm > 1.0e-12) { + double factor = violation / norm; + for (int i = 0; i < n; i++) { + fallback[i] -= factor * constraint[i]; + } + } + } } } if (isFeasible(fallback, constraints, bounds)) { @@ -1780,7 +1820,7 @@ private double[][] buildActiveMatrix(double[][] matrix, int mask) { int index = 0; for (int row = 0; row < matrix.length; row++) { if ((mask & (1 << row)) != 0) { - active[index++] = Arrays.copyOf(matrix[row], matrix[row].length); + active[index++] = Arrays.copyOf(matrix[row], matrix[row].length); } } return active; @@ -1795,20 +1835,20 @@ private double[] buildActiveVector(double[] vector, int mask) { int index = 0; for (int row = 0; row < vector.length; row++) { if ((mask & (1 << row)) != 0) { - active[index++] = vector[row]; + active[index++] = vector[row]; } } return active; } - private double[] solveEqualityConstrained(double[] inverseDiagonal, double[] gradient, double[][] constraints, - double[] bounds) { + private double[] solveEqualityConstrained(double[] inverseDiagonal, double[] gradient, + double[][] constraints, double[] bounds) { int n = gradient.length; int m = constraints.length; if (m == 0) { double[] solution = new double[n]; for (int i = 0; i < n; i++) { - solution[i] = -inverseDiagonal[i] * gradient[i]; + solution[i] = -inverseDiagonal[i] * gradient[i]; } return solution; } @@ -1816,11 +1856,11 @@ private double[] solveEqualityConstrained(double[] inverseDiagonal, double[] gra double[][] reduced = new double[m][m]; for (int row = 0; row < m; row++) { for (int col = 0; col < m; col++) { - double sum = 0.0; - for (int i = 0; i < n; i++) { - sum += constraints[row][i] * inverseDiagonal[i] * constraints[col][i]; - } - reduced[row][col] = sum; + double sum = 0.0; + for (int i = 0; i < n; i++) { + sum += constraints[row][i] * inverseDiagonal[i] * constraints[col][i]; + } + reduced[row][col] = sum; } } @@ -1828,7 +1868,7 @@ private double[] solveEqualityConstrained(double[] inverseDiagonal, double[] gra for (int row = 0; row < m; row++) { double sum = 0.0; for (int i = 0; i < n; i++) { - sum += constraints[row][i] * inverseDiagonal[i] * gradient[i]; + sum += constraints[row][i] * inverseDiagonal[i] * gradient[i]; } rhs[row] = -bounds[row] - sum; } @@ -1842,7 +1882,7 @@ private double[] solveEqualityConstrained(double[] inverseDiagonal, double[] gra for (int i = 0; i < n; i++) { double sum = gradient[i]; for (int row = 0; row < m; row++) { - sum += constraints[row][i] * multipliers[row]; + sum += constraints[row][i] * multipliers[row]; } solution[i] = -inverseDiagonal[i] * sum; } @@ -1854,48 +1894,9 @@ private double[] solveLinearSystem(double[][] matrix, double[] vector) { if (n == 0) { return new double[0]; } - double[][] augmented = new double[n][n + 1]; - for (int i = 0; i < n; i++) { - System.arraycopy(matrix[i], 0, augmented[i], 0, n); - augmented[i][n] = vector[i]; - } - - for (int pivot = 0; pivot < n; pivot++) { - int bestRow = pivot; - double bestValue = Math.abs(augmented[pivot][pivot]); - for (int row = pivot + 1; row < n; row++) { - double value = Math.abs(augmented[row][pivot]); - if (value > bestValue) { - bestValue = value; - bestRow = row; - } - } - if (bestValue < 1.0e-12) { - return null; - } - if (bestRow != pivot) { - double[] tmp = augmented[pivot]; - augmented[pivot] = augmented[bestRow]; - augmented[bestRow] = tmp; - } - double diagonal = augmented[pivot][pivot]; - for (int col = pivot; col <= n; col++) { - augmented[pivot][col] /= diagonal; - } - for (int row = 0; row < n; row++) { - if (row == pivot) { - continue; - } - double factor = augmented[row][pivot]; - for (int col = pivot; col <= n; col++) { - augmented[row][col] -= factor * augmented[pivot][col]; - } - } - } - double[] solution = new double[n]; - for (int i = 0; i < n; i++) { - solution[i] = augmented[i][n]; + if (!LinearAlgebraOps.solveLinearSystem(matrix, vector, solution)) { + return null; } return solution; } @@ -1910,10 +1911,10 @@ private boolean isFeasible(double[] candidate, double[][] constraints, double[] for (int row = 0; row < constraints.length; row++) { double lhs = 0.0; for (int i = 0; i < candidate.length; i++) { - lhs += constraints[row][i] * candidate[i]; + lhs += constraints[row][i] * candidate[i]; } if (lhs > bounds[row] + 1.0e-8) { - return false; + return false; } } return true; @@ -1947,13 +1948,13 @@ private double computeOptimalControl(double measurement, double dt, double previ double quadratic = sumBetaSquared + controlWeight + moveWeight; double linear = 2.0 * sumBetaError - 2.0 * controlWeight * preferredControlValue - - 2.0 * moveWeight * previousControl; + - 2.0 * moveWeight * previousControl; if (quadratic < 1.0e-12) { if (controlWeight + moveWeight > 0.0) { - double weighted = (controlWeight * preferredControlValue + moveWeight * previousControl) - / (controlWeight + moveWeight); - return clamp(weighted); + double weighted = (controlWeight * preferredControlValue + moveWeight * previousControl) + / (controlWeight + moveWeight); + return clamp(weighted); } return clamp(previousControl); } @@ -1962,8 +1963,10 @@ private double computeOptimalControl(double measurement, double dt, double previ if (!Double.isFinite(candidate)) { candidate = previousControl; } - double minBound = Double.isInfinite(minMove) ? Double.NEGATIVE_INFINITY : previousControl + minMove; - double maxBound = Double.isInfinite(maxMove) ? Double.POSITIVE_INFINITY : previousControl + maxMove; + double minBound = + Double.isInfinite(minMove) ? Double.NEGATIVE_INFINITY : previousControl + minMove; + double maxBound = + Double.isInfinite(maxMove) ? Double.POSITIVE_INFINITY : previousControl + maxMove; if (!Double.isInfinite(minBound)) { candidate = Math.max(minBound, candidate); } @@ -2012,8 +2015,8 @@ public void setOutputLimits(double min, double max) { } /** - * Predict future measurements using the internal first-order model assuming the most recent control signal is held - * constant. + * Predict future measurements using the internal first-order model assuming the most recent + * control signal is held constant. * * @param steps number of steps ahead to predict (must be positive) * @param dt sampling interval in seconds @@ -2027,11 +2030,13 @@ public double[] getPredictedTrajectory(int steps, double dt) { throw new IllegalArgumentException("Sample interval must be positive and finite"); } double measurement = Double.isFinite(lastSampledValue) ? lastSampledValue : processBias; - double control = Double.isFinite(lastAppliedControl) ? lastAppliedControl : preferredControlValue; + double control = + Double.isFinite(lastAppliedControl) ? lastAppliedControl : preferredControlValue; double tau = Math.max(timeConstant, 1.0e-9); double decay = Math.exp(-dt / tau); double[] trajectory = new double[steps]; - double steadyState = processBias + (reverseActing ? -Math.abs(processGain) : processGain) * control; + double steadyState = + processBias + (reverseActing ? -Math.abs(processGain) : processGain) * control; for (int i = 0; i < steps; i++) { measurement = decay * measurement + (1.0 - decay) * steadyState; trajectory[i] = measurement; diff --git a/src/main/java/neqsim/process/equipment/distillation/ColumnMeshResidual.java b/src/main/java/neqsim/process/equipment/distillation/ColumnMeshResidual.java index 55ae7fae27..8c0e8ea35f 100644 --- a/src/main/java/neqsim/process/equipment/distillation/ColumnMeshResidual.java +++ b/src/main/java/neqsim/process/equipment/distillation/ColumnMeshResidual.java @@ -134,19 +134,6 @@ boolean isFinite() { return max; } - /** - * Get the Euclidean norm of all residuals. - * - * @return L2 norm - */ - double getL2Norm() { - double sumSquares = 0.0; - for (int i = 0; i < values.length; i++) { - sumSquares += values[i] * values[i]; - } - return Math.sqrt(sumSquares); - } - /** * Count residuals of a given equation type. * diff --git a/src/main/java/neqsim/process/equipment/distillation/DistillationColumn.java b/src/main/java/neqsim/process/equipment/distillation/DistillationColumn.java index bb013846f6..ba84b4e42c 100644 --- a/src/main/java/neqsim/process/equipment/distillation/DistillationColumn.java +++ b/src/main/java/neqsim/process/equipment/distillation/DistillationColumn.java @@ -30,6 +30,7 @@ import neqsim.thermo.system.SystemInterface; import neqsim.thermodynamicoperations.ThermodynamicOperations; import neqsim.util.ExcludeFromJacocoGeneratedReport; +import neqsim.util.math.LinearAlgebraOps; import neqsim.util.unit.TemperatureUnit; import neqsim.util.validation.ValidationResult; @@ -37,10 +38,11 @@ * Models a tray based distillation column with optional condenser and reboiler. * *

- * The column is solved using a sequential substitution approach. The {@link #init()} method sets initial tray - * temperatures by running the feed tray and linearly distributing temperatures towards the top and bottom. During - * {@link #run(UUID)} the trays are iteratively solved in upward and downward sweeps until the summed temperature change - * between iterations is below the configured {@link #temperatureTolerance} or the iteration limit is reached. + * The column is solved using a sequential substitution approach. The {@link #init()} method sets + * initial tray temperatures by running the feed tray and linearly distributing temperatures towards + * the top and bottom. During {@link #run(UUID)} the trays are iteratively solved in upward and + * downward sweeps until the summed temperature change between iterations is below the configured + * {@link #temperatureTolerance} or the iteration limit is reached. *

* * @author esol @@ -217,8 +219,8 @@ public enum DynamicColumnModel { * Flow specification for a side-product draw. * *

- * The column uses this specification as a tear variable by adjusting the corresponding tray side-draw fraction until - * the withdrawn stream flow matches the target flow. + * The column uses this specification as a tear variable by adjusting the corresponding tray + * side-draw fraction until the withdrawn stream flow matches the target flow. *

* * @author esol @@ -244,18 +246,19 @@ public static class ColumnSideDrawSpecification implements java.io.Serializable * @param phase side-draw phase * @param targetFlowRate target side-draw flow rate * @param flowUnit flow-rate unit for the target and actual flow - * @throws IllegalArgumentException if phase is null, target flow is negative or non-finite, or the flow unit is - * empty + * @throws IllegalArgumentException if phase is null, target flow is negative or non-finite, or + * the flow unit is empty */ - public ColumnSideDrawSpecification(int trayNumber, SideDrawPhase phase, double targetFlowRate, String flowUnit) { + public ColumnSideDrawSpecification(int trayNumber, SideDrawPhase phase, double targetFlowRate, + String flowUnit) { if (phase == null) { - throw new IllegalArgumentException("Side draw phase cannot be null"); + throw new IllegalArgumentException("Side draw phase cannot be null"); } if (!Double.isFinite(targetFlowRate) || targetFlowRate < 0.0) { - throw new IllegalArgumentException("Side draw target flow must be finite and >= 0"); + throw new IllegalArgumentException("Side draw target flow must be finite and >= 0"); } if (flowUnit == null || flowUnit.trim().isEmpty()) { - throw new IllegalArgumentException("Side draw flow unit cannot be empty"); + throw new IllegalArgumentException("Side draw flow unit cannot be empty"); } this.trayNumber = trayNumber; this.phase = phase; @@ -315,7 +318,7 @@ public double getTolerance() { */ public void setTolerance(double tolerance) { if (!Double.isFinite(tolerance) || tolerance <= 0.0) { - throw new IllegalArgumentException("Side draw tolerance must be finite and positive"); + throw new IllegalArgumentException("Side draw tolerance must be finite and positive"); } this.tolerance = tolerance; } @@ -336,7 +339,7 @@ public int getMaxIterations() { */ public void setMaxIterations(int maxIterations) { if (maxIterations <= 0) { - throw new IllegalArgumentException("Side draw maxIterations must be positive"); + throw new IllegalArgumentException("Side draw maxIterations must be positive"); } this.maxIterations = maxIterations; } @@ -374,7 +377,8 @@ private double updateActualFlowRate(double actualFlowRate) { } /** - * Liquid pumparound circuit that withdraws liquid from one tray, changes temperature, and returns it to another tray. + * Liquid pumparound circuit that withdraws liquid from one tray, changes temperature, and returns + * it to another tray. * * @author esol * @version 1.0 @@ -401,8 +405,8 @@ public static class ColumnPumparound implements java.io.Serializable { * @param drawFraction fraction of tray liquid traffic withdrawn * @param temperatureDrop temperature drop from draw to return in Kelvin */ - public ColumnPumparound(String name, int drawTrayNumber, int returnTrayNumber, double drawFraction, - double temperatureDrop) { + public ColumnPumparound(String name, int drawTrayNumber, int returnTrayNumber, + double drawFraction, double temperatureDrop) { this.name = name; this.drawTrayNumber = drawTrayNumber; this.returnTrayNumber = returnTrayNumber; @@ -486,13 +490,14 @@ private double updateReturnStream(StreamInterface newDrawStream, UUID id) { SystemInterface returnSystem = newDrawStream.getThermoSystem().clone(); double returnTemperature = returnSystem.getTemperature() - temperatureDrop; if (!Double.isFinite(returnTemperature) || returnTemperature <= 0.0) { - throw new IllegalStateException("Pumparound return temperature must be finite and above 0 K"); + throw new IllegalStateException( + "Pumparound return temperature must be finite and above 0 K"); } returnSystem.setTemperature(returnTemperature); if (returnStream == null) { - returnStream = new Stream(name + " return", returnSystem); + returnStream = new Stream(name + " return", returnSystem); } else { - returnStream.setThermoSystem(returnSystem); + returnStream.setThermoSystem(returnSystem); } returnStream.run(id); lastReturnFlowKgPerHour = Math.abs(returnStream.getFlowRate("kg/hr")); @@ -512,10 +517,10 @@ private double updateReturnStream(StreamInterface newDrawStream, UUID id) { /** Solver strategy that actually completed the latest solve. */ private transient SolverType lastSolverTypeUsed = SolverType.DIRECT_SUBSTITUTION; /** - * Concrete solver chosen by the AUTO selector on the previous solve. When the column is solved again from a warm - * state (e.g. inside a recycle loop), AUTO reuses this solver directly instead of re-running the expensive - * feasibility pre-screen, candidate cloning, and multi-solver scoring on every call. Reset whenever the column - * reverts to a cold start. + * Concrete solver chosen by the AUTO selector on the previous solve. When the column is solved + * again from a warm state (e.g. inside a recycle loop), AUTO reuses this solver directly instead + * of re-running the expensive feasibility pre-screen, candidate cloning, and multi-solver scoring + * on every call. Reset whenever the column reverts to a cold start. */ private transient SolverType autoWarmStartSolver = null; /** Whether the latest run applied the opt-in full-fractionator fast path. */ @@ -560,9 +565,9 @@ private double updateReturnStream(StreamInterface newDrawStream, UUID id) { */ private boolean enforceEnergyBalanceTolerance = false; /** - * Explicit control of whether the MESH residual vector must satisfy tolerance before convergence. When not explicitly - * set, the gate is active for residual-based solver modes and inactive for substitution and temperature/flow - * accelerator modes. + * Explicit control of whether the MESH residual vector must satisfy tolerance before convergence. + * When not explicitly set, the gate is active for residual-based solver modes and inactive for + * substitution and temperature/flow accelerator modes. */ private boolean enforceMeshResidualTolerance = false; /** @@ -572,33 +577,33 @@ private double updateReturnStream(StreamInterface newDrawStream, UUID id) { private boolean doMultiPhaseCheck = true; /** - * When {@code true}, trays in the reactive section use {@link ReactiveTray} (simultaneous chemical + phase - * equilibrium via the Modified RAND method) instead of standard VLE {@link SimpleTray}. Set this before the first - * {@link #run()} call. + * When {@code true}, trays in the reactive section use {@link ReactiveTray} (simultaneous + * chemical + phase equilibrium via the Modified RAND method) instead of standard VLE + * {@link SimpleTray}. Set this before the first {@link #run()} call. */ private boolean reactive = false; /** - * First tray index (0-based, inclusive) of the reactive section. A value of {@code -1} means all middle trays (i.e. - * excluding reboiler/condenser) are reactive. + * First tray index (0-based, inclusive) of the reactive section. A value of {@code -1} means all + * middle trays (i.e. excluding reboiler/condenser) are reactive. */ private int reactiveStartTray = -1; /** - * Last tray index (0-based, inclusive) of the reactive section. A value of {@code -1} means all middle trays are - * reactive. + * Last tray index (0-based, inclusive) of the reactive section. A value of {@code -1} means all + * middle trays are reactive. */ private int reactiveEndTray = -1; /** - * Flag tracking whether the column has been solved at least once. Used to seed the sequential solver with the - * previous tray state on re-runs, preventing divergence from an unrelaxed start. + * Flag tracking whether the column has been solved at least once. Used to seed the sequential + * solver with the previous tray state on re-runs, preventing divergence from an unrelaxed start. */ private transient boolean hasBeenSolvedBefore = false; /** - * Total feed flow (kg/hr) recorded at the end of the previous solve. Used to detect whether the column needs to - * re-solve or can reuse the previous result. + * Total feed flow (kg/hr) recorded at the end of the previous solve. Used to detect whether the + * column needs to re-solve or can reuse the previous result. */ private transient double lastTotalFeedFlow = -1.0; @@ -629,9 +634,9 @@ private double updateReturnStream(StreamInterface newDrawStream, UUID id) { private double internalDiameter = 1.0; /** - * Maximum allowable Fs factor (gas load factor) for the column internals [m/s*sqrt(kg/m3)]. Used as the design basis - * for the Fs-factor capacity constraint. Typical Souders-Brown design values are 2.0-2.5 for trayed columns and up to - * 3.0 for structured packing. + * Maximum allowable Fs factor (gas load factor) for the column internals [m/s*sqrt(kg/m3)]. Used + * as the design basis for the Fs-factor capacity constraint. Typical Souders-Brown design values + * are 2.0-2.5 for trayed columns and up to 3.0 for structured packing. */ private double maxAllowableFsFactor = 2.5; @@ -716,8 +721,8 @@ private double updateReturnStream(StreamInterface newDrawStream, UUID id) { private transient double lastNaphtaliLinearSolveTimeSeconds = 0.0; /** - * Instead of Map<Integer,StreamInterface>, we store a list of feed streams per tray number. This allows - * multiple feeds to the same tray. + * Instead of Map<Integer,StreamInterface>, we store a list of feed streams per tray number. + * This allows multiple feeds to the same tray. */ private Map> feedStreams = new HashMap<>(); /** @@ -788,8 +793,9 @@ public boolean isDoMultiPhaseCheck() { private double murphreeEfficiency = 1.0; /** - * Per-stage Murphree efficiency overrides. Index 0 is the reboiler and the last stage is the condenser if present. A - * {@link Double#NaN} value means that the column-wide Murphree efficiency is used for that stage. + * Per-stage Murphree efficiency overrides. Index 0 is the reboiler and the last stage is the + * condenser if present. A {@link Double#NaN} value means that the column-wide Murphree efficiency + * is used for that stage. */ private double[] perStageMurphreeEfficiency = null; @@ -799,8 +805,8 @@ public boolean isDoMultiPhaseCheck() { private transient List convergenceHistory = new ArrayList<>(); /** - * Number of simplified inner-loop iterations between rigorous flash updates in the IO solver. Higher values reduce - * flash count but may reduce accuracy. Default 3. + * Number of simplified inner-loop iterations between rigorous flash updates in the IO solver. + * Higher values reduce flash count but may reduce accuracy. Default 3. */ private int innerLoopSteps = 3; @@ -832,7 +838,8 @@ public boolean isDoMultiPhaseCheck() { * @param hasReboiler Set true to add reboiler * @param hasCondenser Set true to add Condenser */ - public DistillationColumn(String name, int numberOfTraysLocal, boolean hasReboiler, boolean hasCondenser) { + public DistillationColumn(String name, int numberOfTraysLocal, boolean hasReboiler, + boolean hasCondenser) { super(name); this.hasReboiler = hasReboiler; this.hasCondenser = hasCondenser; @@ -865,19 +872,22 @@ public DistillationColumn(String name, int numberOfTraysLocal, boolean hasReboil /** *

- * Add a feed stream to the specified tray. (Now allows multiple streams on the same trayNumber, using a list.) + * Add a feed stream to the specified tray. (Now allows multiple streams on the same trayNumber, + * using a list.) *

* * @param inputStream the feed stream * @param feedTrayNumber the tray number (0-based in the code) to which this feed goes - * @throws IllegalArgumentException if the stream is null or the tray index is outside the column tray range + * @throws IllegalArgumentException if the stream is null or the tray index is outside the column + * tray range */ public void addFeedStream(StreamInterface inputStream, int feedTrayNumber) { if (inputStream == null) { throw new IllegalArgumentException("inputStream can not be null"); } if (feedTrayNumber < 0 || feedTrayNumber >= numberOfTrays) { - throw new IllegalArgumentException("Feed tray index must be between 0 and " + (numberOfTrays - 1)); + throw new IllegalArgumentException( + "Feed tray index must be between 0 and " + (numberOfTrays - 1)); } // Put this feed into our feedStreams list for that trayNumber feedStreams.computeIfAbsent(feedTrayNumber, k -> new ArrayList<>()).add(inputStream); @@ -907,10 +917,10 @@ public void addFeedStream(StreamInterface inputStream, int feedTrayNumber) { * Add a feed stream to the column without specifying the tray. * *

- * The feed tray is estimated automatically when the column is run. The estimate uses an existing tray temperature - * profile when available, otherwise it builds a simple temperature profile from configured condenser/reboiler - * temperatures and the feed temperature. This is a robust initial placement heuristic, not a guarantee of global - * optimum or convergence for every specification. + * The feed tray is estimated automatically when the column is run. The estimate uses an existing + * tray temperature profile when available, otherwise it builds a simple temperature profile from + * configured condenser/reboiler temperatures and the feed temperature. This is a robust initial + * placement heuristic, not a guarantee of global optimum or convergence for every specification. *

* * @param inputStream the feed stream @@ -965,8 +975,8 @@ public boolean hasCondenser() { * Estimate which tray an unassigned feed stream would be placed on. * *

- * This method does not connect the feed stream to the column. It is intended for diagnostics and for checking - * automatic feed placement before calling {@link #run()}. + * This method does not connect the feed stream to the column. It is intended for diagnostics and + * for checking automatic feed placement before calling {@link #run()}. *

* * @param inputStream feed stream to evaluate @@ -984,8 +994,8 @@ public int estimateFeedTrayNumber(StreamInterface inputStream) { * Return the tray number for a feed stream currently assigned to the column. * *

- * The lookup first compares stream object identity and then falls back to the stream name. Feed streams added with - * {@link #addFeedStream(StreamInterface)} are assigned when the column is run. + * The lookup first compares stream object identity and then falls back to the stream name. Feed + * streams added with {@link #addFeedStream(StreamInterface)} are assigned when the column is run. *

* * @param inputStream feed stream to locate @@ -1015,12 +1025,12 @@ public int getFeedTrayNumber(String streamName) { for (int trayNumber = 0; trayNumber < numberOfTrays; trayNumber++) { List feeds = feedStreams.get(trayNumber); if (feeds == null) { - continue; + continue; } for (StreamInterface feed : feeds) { - if (streamName.equals(feed.getName())) { - return trayNumber; - } + if (streamName.equals(feed.getName())) { + return trayNumber; + } } } return -1; @@ -1036,24 +1046,26 @@ private int getFeedTrayNumberByReference(StreamInterface inputStream) { for (int trayNumber = 0; trayNumber < numberOfTrays; trayNumber++) { List feeds = feedStreams.get(trayNumber); if (feeds == null) { - continue; + continue; } for (StreamInterface feed : feeds) { - if (feed == inputStream) { - return trayNumber; - } + if (feed == inputStream) { + return trayNumber; + } } } return -1; } /** - * Prepare the column for calculation by estimating tray temperatures and linking streams between trays. + * Prepare the column for calculation by estimating tray temperatures and linking streams between + * trays. * *

- * The feed tray is solved first to obtain a temperature estimate. This temperature is then used to linearly guess - * temperatures upwards to the condenser and downwards to the reboiler. Gas and liquid outlet streams are connected to - * neighbouring trays so that a subsequent call to {@link #run(UUID)} can iterate to convergence. + * The feed tray is solved first to obtain a temperature estimate. This temperature is then used + * to linearly guess temperatures upwards to the condenser and downwards to the reboiler. Gas and + * liquid outlet streams are connected to neighbouring trays so that a subsequent call to + * {@link #run(UUID)} can iterate to convergence. *

*/ public void init() { @@ -1080,22 +1092,24 @@ public void init() { // If that tray ended up single-phase, see if adding some other feed helps if (getTray(firstFeedTrayNumber).getFluid().getNumberOfPhases() == 1) { for (int i = 0; i < numberOfTrays; i++) { - if (getTray(i).getNumberOfInputStreams() > 0 && i != firstFeedTrayNumber) { - getTray(firstFeedTrayNumber).addStream(trays.get(i).getStream(0)); - getTray(firstFeedTrayNumber).run(); - // remove it again - getTray(firstFeedTrayNumber).removeInputStream(getTray(firstFeedTrayNumber).getNumberOfInputStreams() - 1); - if (getTray(firstFeedTrayNumber).getThermoSystem().getNumberOfPhases() > 1) { - break; - } - } else if (i == firstFeedTrayNumber && getTray(i).getNumberOfInputStreams() > 1) { - getTray(firstFeedTrayNumber).addStream(trays.get(i).getStream(1)); - trays.get(firstFeedTrayNumber).run(); - getTray(firstFeedTrayNumber).removeInputStream(getTray(firstFeedTrayNumber).getNumberOfInputStreams() - 1); - if (getTray(firstFeedTrayNumber).getThermoSystem().getNumberOfPhases() > 1) { - break; - } - } + if (getTray(i).getNumberOfInputStreams() > 0 && i != firstFeedTrayNumber) { + getTray(firstFeedTrayNumber).addStream(trays.get(i).getStream(0)); + getTray(firstFeedTrayNumber).run(); + // remove it again + getTray(firstFeedTrayNumber) + .removeInputStream(getTray(firstFeedTrayNumber).getNumberOfInputStreams() - 1); + if (getTray(firstFeedTrayNumber).getThermoSystem().getNumberOfPhases() > 1) { + break; + } + } else if (i == firstFeedTrayNumber && getTray(i).getNumberOfInputStreams() > 1) { + getTray(firstFeedTrayNumber).addStream(trays.get(i).getStream(1)); + trays.get(firstFeedTrayNumber).run(); + getTray(firstFeedTrayNumber) + .removeInputStream(getTray(firstFeedTrayNumber).getNumberOfInputStreams() - 1); + if (getTray(firstFeedTrayNumber).getThermoSystem().getNumberOfPhases() > 1) { + break; + } + } } } @@ -1120,21 +1134,24 @@ public void init() { // Rough guess for temperature steps double deltaTempCondenser = (feedTrayTemperature - condenserTemperature) - / (numberOfTrays * 1.0 - firstFeedTrayNumber - 1); - double deltaTempReboiler = (reboilerTemperature - feedTrayTemperature) / (firstFeedTrayNumber * 1.0); + / (numberOfTrays * 1.0 - firstFeedTrayNumber - 1); + double deltaTempReboiler = + (reboilerTemperature - feedTrayTemperature) / (firstFeedTrayNumber * 1.0); // set temperature from feed tray up double delta = 0; for (int i = firstFeedTrayNumber + 1; i < numberOfTrays; i++) { delta += deltaTempCondenser; - trays.get(i).setTemperature(getTray(firstFeedTrayNumber).getThermoSystem().getTemperature() - delta); + trays.get(i) + .setTemperature(getTray(firstFeedTrayNumber).getThermoSystem().getTemperature() - delta); } // set temperature from feed tray down delta = 0; for (int i = firstFeedTrayNumber - 1; i >= 0; i--) { delta += deltaTempReboiler; - trays.get(i).setTemperature(getTray(firstFeedTrayNumber).getThermoSystem().getTemperature() + delta); + trays.get(i) + .setTemperature(getTray(firstFeedTrayNumber).getThermoSystem().getTemperature() + delta); } // Link upward @@ -1163,10 +1180,11 @@ public void init() { *

* Solve the column until tray temperatures converge. * - * The method applies sequential substitution with an adaptive relaxation controller. Pressures are set linearly - * between bottom and top. Each iteration performs an upward sweep where liquid flows downward followed by a downward - * sweep where vapour flows upward. Tray temperatures and inter-tray stream flow rates are relaxed if the combined - * temperature, mass and energy residuals grow, providing basic line-search behaviour. + * The method applies sequential substitution with an adaptive relaxation controller. Pressures + * are set linearly between bottom and top. Each iteration performs an upward sweep where liquid + * flows downward followed by a downward sweep where vapour flows upward. Tray temperatures and + * inter-tray stream flow rates are relaxed if the combined temperature, mass and energy residuals + * grow, providing basic line-search behaviour. *

*/ @Override @@ -1210,7 +1228,8 @@ private void ensureSolveTimeIncludesElapsedWallTime(long startTime) { * @return {@code true} when an outer tear-variable solve is required */ private boolean hasActiveColumnTearVariables() { - return !sideDrawSpecifications.isEmpty() || !pumparounds.isEmpty() || hydraulicPressureDropCouplingEnabled; + return !sideDrawSpecifications.isEmpty() || !pumparounds.isEmpty() + || hydraulicPressureDropCouplingEnabled; } /** @@ -1247,29 +1266,30 @@ private void solveWithColumnTearVariables(UUID id) { lastColumnTearIterationCount = iteration + 1; lastColumnTearResidual = relativeChange; if (relativeChange <= tolerance) { - if (columnTearVariablesChanged) { - setDoInitializion(true); - solveConfiguredColumn(id); - } - updateSideDrawSpecificationResidualsOnly(); - lastColumnTearResidual = Math.max(relativeChange, getMaxSideDrawSpecificationResidual()); - lastColumnTearConverged = lastColumnTearResidual <= tolerance; - return; + if (columnTearVariablesChanged) { + setDoInitializion(true); + solveConfiguredColumn(id); + } + updateSideDrawSpecificationResidualsOnly(); + lastColumnTearResidual = Math.max(relativeChange, getMaxSideDrawSpecificationResidual()); + lastColumnTearConverged = lastColumnTearResidual <= tolerance; + return; } if (!columnTearVariablesChanged) { - updateSideDrawSpecificationResidualsOnly(); - lastColumnTearResidual = Math.max(relativeChange, getMaxSideDrawSpecificationResidual()); - lastColumnTearConverged = false; - return; + updateSideDrawSpecificationResidualsOnly(); + lastColumnTearResidual = Math.max(relativeChange, getMaxSideDrawSpecificationResidual()); + lastColumnTearConverged = false; + return; } if (iteration < iterationLimit - 1) { - setDoInitializion(true); + setDoInitializion(true); } } setDoInitializion(true); solveConfiguredColumn(id); updateSideDrawSpecificationResidualsOnly(); - lastColumnTearResidual = Math.max(lastColumnTearResidual, getMaxSideDrawSpecificationResidual()); + lastColumnTearResidual = + Math.max(lastColumnTearResidual, getMaxSideDrawSpecificationResidual()); lastColumnTearConverged = lastColumnTearResidual <= tolerance; } @@ -1324,8 +1344,10 @@ private double updateColumnTearVariables(UUID id) { private double updatePumparoundReturnStreams(UUID id) { double maxRelativeChange = 0.0; for (ColumnPumparound pumparound : pumparounds) { - StreamInterface drawStream = getTray(pumparound.getDrawTrayNumber()).getLiquidPumparoundDrawStream(); - maxRelativeChange = Math.max(maxRelativeChange, pumparound.updateReturnStream(drawStream, id)); + StreamInterface drawStream = + getTray(pumparound.getDrawTrayNumber()).getLiquidPumparoundDrawStream(); + maxRelativeChange = + Math.max(maxRelativeChange, pumparound.updateReturnStream(drawStream, id)); } lastPumparoundRelativeChange = maxRelativeChange; if (maxRelativeChange > 1.0e-12) { @@ -1342,16 +1364,17 @@ private double updatePumparoundReturnStreams(UUID id) { private double updateSideDrawSpecificationFractions() { double maxRelativeResidual = 0.0; for (ColumnSideDrawSpecification specification : sideDrawSpecifications) { - StreamInterface sideDrawStream = getSideDrawStream(specification.getTrayNumber(), specification.getPhase()); + StreamInterface sideDrawStream = + getSideDrawStream(specification.getTrayNumber(), specification.getPhase()); double actualFlowRate = sideDrawStream.getFlowRate(specification.getFlowUnit()); double residual = specification.updateActualFlowRate(actualFlowRate); maxRelativeResidual = Math.max(maxRelativeResidual, residual); if (residual <= specification.getTolerance()) { - continue; + continue; } double newFraction = calculateNextSideDrawFraction(specification, actualFlowRate); columnTearVariablesChanged = setSideDrawFractionWithinLimit(specification.getTrayNumber(), - specification.getPhase(), newFraction) || columnTearVariablesChanged; + specification.getPhase(), newFraction) || columnTearVariablesChanged; } return maxRelativeResidual; } @@ -1359,7 +1382,8 @@ private double updateSideDrawSpecificationFractions() { /** Update side-draw flow residuals without changing side-draw fractions. */ private void updateSideDrawSpecificationResidualsOnly() { for (ColumnSideDrawSpecification specification : sideDrawSpecifications) { - StreamInterface sideDrawStream = getSideDrawStream(specification.getTrayNumber(), specification.getPhase()); + StreamInterface sideDrawStream = + getSideDrawStream(specification.getTrayNumber(), specification.getPhase()); specification.updateActualFlowRate(sideDrawStream.getFlowRate(specification.getFlowUnit())); } } @@ -1384,8 +1408,10 @@ private double getMaxSideDrawSpecificationResidual() { * @param actualFlowRate latest actual flow rate * @return next candidate side-draw fraction */ - private double calculateNextSideDrawFraction(ColumnSideDrawSpecification specification, double actualFlowRate) { - double currentFraction = getSideDrawFraction(specification.getTrayNumber(), specification.getPhase()); + private double calculateNextSideDrawFraction(ColumnSideDrawSpecification specification, + double actualFlowRate) { + double currentFraction = + getSideDrawFraction(specification.getTrayNumber(), specification.getPhase()); if (specification.getTargetFlowRate() <= 1.0e-12) { return 0.0; } @@ -1421,8 +1447,10 @@ private double getSideDrawFraction(int trayNumber, SideDrawPhase phase) { * @param fraction requested side-draw fraction * @return true if the tray fraction changed, false if the requested value was already set */ - private boolean setSideDrawFractionWithinLimit(int trayNumber, SideDrawPhase phase, double fraction) { - double limitedFraction = Math.max(0.0, Math.min(getMaximumSideDrawFraction(trayNumber, phase), fraction)); + private boolean setSideDrawFractionWithinLimit(int trayNumber, SideDrawPhase phase, + double fraction) { + double limitedFraction = + Math.max(0.0, Math.min(getMaximumSideDrawFraction(trayNumber, phase), fraction)); double currentFraction = getSideDrawFraction(trayNumber, phase); if (Math.abs(limitedFraction - currentFraction) <= 1.0e-12) { return false; @@ -1463,7 +1491,7 @@ private double enforceSideDrawFeedInventoryLimit() { for (int componentIndex = 0; componentIndex < sideDrawComponentMoles.length; componentIndex++) { double sideDrawMoles = sideDrawComponentMoles[componentIndex]; if (sideDrawMoles > feedComponentMoles[componentIndex] + 1.0e-12) { - scaleFactor = Math.min(scaleFactor, feedComponentMoles[componentIndex] / sideDrawMoles); + scaleFactor = Math.min(scaleFactor, feedComponentMoles[componentIndex] / sideDrawMoles); } } if (scaleFactor >= 1.0 - 1.0e-10) { @@ -1483,10 +1511,10 @@ private void scaleSideDrawFractions(double scaleFactor) { for (int trayNumber = 0; trayNumber < numberOfTrays; trayNumber++) { SimpleTray tray = getTray(trayNumber); if (tray.getGasSideDrawFraction() > 0.0) { - tray.setGasSideDrawFraction(tray.getGasSideDrawFraction() * scaleFactor); + tray.setGasSideDrawFraction(tray.getGasSideDrawFraction() * scaleFactor); } if (tray.getLiquidSideDrawFraction() > 0.0) { - tray.setLiquidSideDrawFraction(tray.getLiquidSideDrawFraction() * scaleFactor); + tray.setLiquidSideDrawFraction(tray.getLiquidSideDrawFraction() * scaleFactor); } } } @@ -1506,7 +1534,7 @@ private double updatePressureProfileFromHydraulics() { lastHydraulicPressureDropPa = pressureDropPa; lastHydraulicPressureDropResidual = applyHydraulicPressureDrop(pressureDropPa); if (lastHydraulicPressureDropResidual > 1.0e-12) { - columnTearVariablesChanged = true; + columnTearVariablesChanged = true; } return lastHydraulicPressureDropResidual; } catch (Exception exception) { @@ -1529,19 +1557,20 @@ private double applyHydraulicPressureDrop(double pressureDropPa) { bottomTrayPressure = topTrayPressure + pressureDropBar; applyOptimizationPressureProfile(); if (!isPositiveFinite(previousBottomPressure)) { - return 1.0; + return 1.0; } return Math.abs(bottomTrayPressure - previousBottomPressure) - / Math.max(1.0e-12, Math.abs(previousBottomPressure)); + / Math.max(1.0e-12, Math.abs(previousBottomPressure)); } if (isPositiveFinite(bottomTrayPressure)) { double previousTopPressure = topTrayPressure; topTrayPressure = Math.max(1.0e-6, bottomTrayPressure - pressureDropBar); applyOptimizationPressureProfile(); if (!isPositiveFinite(previousTopPressure)) { - return 1.0; + return 1.0; } - return Math.abs(topTrayPressure - previousTopPressure) / Math.max(1.0e-12, Math.abs(previousTopPressure)); + return Math.abs(topTrayPressure - previousTopPressure) + / Math.max(1.0e-12, Math.abs(previousTopPressure)); } return 0.0; } @@ -1564,15 +1593,15 @@ private void applyDirectSpecification(ColumnSpecification spec) { if (spec.getType() == ColumnSpecification.SpecificationType.REFLUX_RATIO) { if (spec.getLocation() == ColumnSpecification.ProductLocation.TOP && hasCondenser) { - getCondenser().setRefluxRatio(spec.getTargetValue()); + getCondenser().setRefluxRatio(spec.getTargetValue()); } else if (spec.getLocation() == ColumnSpecification.ProductLocation.BOTTOM && hasReboiler) { - getReboiler().setRefluxRatio(spec.getTargetValue()); + getReboiler().setRefluxRatio(spec.getTargetValue()); } } else if (spec.getType() == ColumnSpecification.SpecificationType.DUTY) { if (spec.getLocation() == ColumnSpecification.ProductLocation.TOP && hasCondenser) { - getCondenser().setHeatInput(spec.getTargetValue()); + getCondenser().setHeatInput(spec.getTargetValue()); } else if (spec.getLocation() == ColumnSpecification.ProductLocation.BOTTOM && hasReboiler) { - getReboiler().setHeatInput(spec.getTargetValue()); + getReboiler().setHeatInput(spec.getTargetValue()); } } } @@ -1690,21 +1719,23 @@ private SolverType getEffectiveSolverTypeForRun() { // multi-solver scoring that AUTO performs on every call. Adjustable specifications keep the // full AUTO path because their continuation/homotopy logic depends on it. if (solverType == SolverType.AUTO && hasBeenSolvedBefore && autoWarmStartSolver != null - && autoWarmStartSolver != SolverType.AUTO && !hasAdjustableSpecifications()) { + && autoWarmStartSolver != SolverType.AUTO && !hasAdjustableSpecifications()) { return autoWarmStartSolver; } return solverType; } /** - * Apply opt-in fast defaults for large low-reflux full fractionators using the legacy default solver. + * Apply opt-in fast defaults for large low-reflux full fractionators using the legacy default + * solver. */ private void applyFullFractionatorFastPath() { if (!shouldApplyFullFractionatorFastPath()) { return; } int feedTrayNumber = getSingleApiFeedTrayNumber(); - if (feedTrayNumber < 0 || (!isFeedTrayNearTop(feedTrayNumber) && !isFeedTrayNearBottom(feedTrayNumber))) { + if (feedTrayNumber < 0 + || (!isFeedTrayNearTop(feedTrayNumber) && !isFeedTrayNearBottom(feedTrayNumber))) { return; } int recommendedFeedTrayNumber = getFullFractionatorFastPathFeedTrayNumber(); @@ -1715,13 +1746,14 @@ private void applyFullFractionatorFastPath() { if (movedFeeds == null || movedFeeds.isEmpty()) { return; } - feedStreams.computeIfAbsent(Integer.valueOf(recommendedFeedTrayNumber), k -> new ArrayList()) - .addAll(movedFeeds); + feedStreams.computeIfAbsent(Integer.valueOf(recommendedFeedTrayNumber), + k -> new ArrayList()).addAll(movedFeeds); resetTrayInputsToExternalFeeds(); setDoInitializion(true); lastFullFractionatorFastPathApplied = true; - lastFullFractionatorFastPathReason = "Opt-in full-fractionator fast path moved the " + "single feed from tray " - + feedTrayNumber + " to tray " + recommendedFeedTrayNumber + " and selected MESH_RESIDUAL."; + lastFullFractionatorFastPathReason = + "Opt-in full-fractionator fast path moved the " + "single feed from tray " + feedTrayNumber + + " to tray " + recommendedFeedTrayNumber + " and selected MESH_RESIDUAL."; } /** @@ -1730,12 +1762,14 @@ private void applyFullFractionatorFastPath() { * @return {@code true} when opt-in fast defaults should be applied for this run */ private boolean shouldApplyFullFractionatorFastPath() { - return fullFractionatorFastPathEnabled && !solverTypeExplicitlySet && solverType == SolverType.DIRECT_SUBSTITUTION - && hasCondenser && hasReboiler && numberOfTrays >= 10 && isLowRefluxFullFractionator(); + return fullFractionatorFastPathEnabled && !solverTypeExplicitlySet + && solverType == SolverType.DIRECT_SUBSTITUTION && hasCondenser && hasReboiler + && numberOfTrays >= 10 && isLowRefluxFullFractionator(); } /** - * Check whether the condenser reflux is in the low-reflux range where direct substitution often oscillates. + * Check whether the condenser reflux is in the low-reflux range where direct substitution often + * oscillates. * * @return {@code true} when the reflux ratio is low enough to enable the guarded fast path */ @@ -1777,8 +1811,8 @@ private int getFullFractionatorFastPathFeedTrayNumber() { } /** - * Solve the column with an outer loop that adjusts condenser/reboiler temperatures to satisfy product specifications. - * Uses a secant method for each specification that requires adjustment. + * Solve the column with an outer loop that adjusts condenser/reboiler temperatures to satisfy + * product specifications. Uses a secant method for each specification that requires adjustment. * * @param id calculation identifier */ @@ -1812,9 +1846,10 @@ private int getEffectiveSpecificationHomotopySteps() { * Solve adjustable product specifications through staged continuation targets. * *

- * The first stage starts from the current product value after a warm baseline solve and then ramps linearly to the - * user-specified final target. This avoids an abrupt jump to a difficult purity, recovery, or product-flow target - * while leaving the stored public specifications unchanged. + * The first stage starts from the current product value after a warm baseline solve and then + * ramps linearly to the user-specified final target. This avoids an abrupt jump to a difficult + * purity, recovery, or product-flow target while leaving the stored public specifications + * unchanged. *

* * @param id calculation identifier @@ -1829,10 +1864,12 @@ private void solveWithSpecificationHomotopy(UUID id, int steps) { } double feedTemp = estimateFeedTemperature(); - double topTemp = hasCondenser && getCondenser().isSetOutTemperature() ? getCondenser().getOutTemperature() - : feedTemp - 20.0; - double bottomTemp = hasReboiler && getReboiler().isSetOutTemperature() ? getReboiler().getOutTemperature() - : feedTemp + 20.0; + double topTemp = + hasCondenser && getCondenser().isSetOutTemperature() ? getCondenser().getOutTemperature() + : feedTemp - 20.0; + double bottomTemp = + hasReboiler && getReboiler().isSetOutTemperature() ? getReboiler().getOutTemperature() + : feedTemp + 20.0; applySpecificationTemperatureGuess(adjustTop, adjustBottom, topTemp, bottomTemp); setDoInitializion(!hasBeenSolvedBefore); @@ -1850,11 +1887,12 @@ private void solveWithSpecificationHomotopy(UUID id, int steps) { int stageCount = Math.max(1, steps); for (int step = 1; step <= stageCount; step++) { double fraction = step / (double) stageCount; - ColumnSpecification stagedTop = adjustTop ? createHomotopySpecification(topSpecification, topStart, fraction) - : topSpecification; - ColumnSpecification stagedBottom = adjustBottom - ? createHomotopySpecification(bottomSpecification, bottomStart, fraction) - : bottomSpecification; + ColumnSpecification stagedTop = + adjustTop ? createHomotopySpecification(topSpecification, topStart, fraction) + : topSpecification; + ColumnSpecification stagedBottom = + adjustBottom ? createHomotopySpecification(bottomSpecification, bottomStart, fraction) + : bottomSpecification; solveWithSpecificationTargets(id, stagedTop, stagedBottom); lastSpecificationHomotopyStepCount = step; setDoInitializion(false); @@ -1889,10 +1927,12 @@ private void solveWithSpecificationTargets(UUID id, ColumnSpecification effectiv double feedTemp = estimateFeedTemperature(); // Initial guesses for temperatures to adjust - double topTemp = hasCondenser && getCondenser().isSetOutTemperature() ? getCondenser().getOutTemperature() - : feedTemp - 20.0; - double bottomTemp = hasReboiler && getReboiler().isSetOutTemperature() ? getReboiler().getOutTemperature() - : feedTemp + 20.0; + double topTemp = + hasCondenser && getCondenser().isSetOutTemperature() ? getCondenser().getOutTemperature() + : feedTemp - 20.0; + double bottomTemp = + hasReboiler && getReboiler().isSetOutTemperature() ? getReboiler().getOutTemperature() + : feedTemp + 20.0; // Secant method state for top double topTemp0 = topTemp; @@ -1909,16 +1949,16 @@ private void solveWithSpecificationTargets(UUID id, ColumnSpecification effectiv for (int outerIter = 0; outerIter < maxOuterIter; outerIter++) { // Set current guess temperatures if (adjustTop) { - double currentTopTemp = (outerIter == 0) ? topTemp0 : topTemp1; - getCondenser().setOutTemperature(currentTopTemp); + double currentTopTemp = (outerIter == 0) ? topTemp0 : topTemp1; + getCondenser().setOutTemperature(currentTopTemp); } if (adjustBottom) { - double currentBottomTemp = (outerIter == 0) ? bottomTemp0 : bottomTemp1; - getReboiler().setOutTemperature(currentBottomTemp); + double currentBottomTemp = (outerIter == 0) ? bottomTemp0 : bottomTemp1; + getReboiler().setOutTemperature(currentBottomTemp); } applySpecificationTemperatureGuess(adjustTop, adjustBottom, - adjustTop ? (outerIter == 0 ? topTemp0 : topTemp1) : Double.NaN, - adjustBottom ? (outerIter == 0 ? bottomTemp0 : bottomTemp1) : Double.NaN); + adjustTop ? (outerIter == 0 ? topTemp0 : topTemp1) : Double.NaN, + adjustBottom ? (outerIter == 0 ? bottomTemp0 : bottomTemp1) : Double.NaN); // Keep the previous stage profile after the first full initialization. setDoInitializion(outerIter == 0 && !hasBeenSolvedBefore); @@ -1927,8 +1967,8 @@ private void solveWithSpecificationTargets(UUID id, ColumnSpecification effectiv solveInner(id); if (!solved()) { - logger.warn("Inner solver did not converge in outer iteration {}", outerIter); - // Try to continue with reduced step + logger.warn("Inner solver did not converge in outer iteration {}", outerIter); + // Try to continue with reduced step } // Evaluate specification errors @@ -1937,42 +1977,43 @@ private void solveWithSpecificationTargets(UUID id, ColumnSpecification effectiv lastTopSpecificationResidual = topError; lastBottomSpecificationResidual = bottomError; - logger.debug("Spec outer iteration {} topErr={} bottomErr={} topT={} bottomT={}", outerIter, topError, - bottomError, adjustTop ? (outerIter == 0 ? topTemp0 : topTemp1) : 0.0, - adjustBottom ? (outerIter == 0 ? bottomTemp0 : bottomTemp1) : 0.0); + logger.debug("Spec outer iteration {} topErr={} bottomErr={} topT={} bottomT={}", outerIter, + topError, bottomError, adjustTop ? (outerIter == 0 ? topTemp0 : topTemp1) : 0.0, + adjustBottom ? (outerIter == 0 ? bottomTemp0 : bottomTemp1) : 0.0); // Check convergence boolean topConverged = !adjustTop || Math.abs(topError) < topTol; boolean bottomConverged = !adjustBottom || Math.abs(bottomError) < bottomTol; if (topConverged && bottomConverged) { - break; + break; } // Update secant method for top temperature if (adjustTop && !topConverged) { - if (outerIter == 0) { - topErr0 = topError; - } else { - topErr1 = topError; - double newTopTemp = secantStep(topTemp0, topTemp1, topErr0, topErr1, feedTemp); - topTemp0 = topTemp1; - topErr0 = topErr1; - topTemp1 = newTopTemp; - } + if (outerIter == 0) { + topErr0 = topError; + } else { + topErr1 = topError; + double newTopTemp = secantStep(topTemp0, topTemp1, topErr0, topErr1, feedTemp); + topTemp0 = topTemp1; + topErr0 = topErr1; + topTemp1 = newTopTemp; + } } // Update secant method for bottom temperature if (adjustBottom && !bottomConverged) { - if (outerIter == 0) { - bottomErr0 = bottomError; - } else { - bottomErr1 = bottomError; - double newBottomTemp = secantStep(bottomTemp0, bottomTemp1, bottomErr0, bottomErr1, feedTemp); - bottomTemp0 = bottomTemp1; - bottomErr0 = bottomErr1; - bottomTemp1 = newBottomTemp; - } + if (outerIter == 0) { + bottomErr0 = bottomError; + } else { + bottomErr1 = bottomError; + double newBottomTemp = + secantStep(bottomTemp0, bottomTemp1, bottomErr0, bottomErr1, feedTemp); + bottomTemp0 = bottomTemp1; + bottomErr0 = bottomErr1; + bottomTemp1 = newBottomTemp; + } } } } @@ -1985,13 +2026,13 @@ private void solveWithSpecificationTargets(UUID id, ColumnSpecification effectiv * @param fraction continuation fraction, where one means the final user target * @return staged specification preserving the original tolerance and iteration limit */ - private ColumnSpecification createHomotopySpecification(ColumnSpecification specification, double startValue, - double fraction) { + private ColumnSpecification createHomotopySpecification(ColumnSpecification specification, + double startValue, double fraction) { double boundedFraction = Math.max(0.0, Math.min(1.0, fraction)); double target = startValue + boundedFraction * (specification.getTargetValue() - startValue); target = boundSpecificationTarget(specification, target); - ColumnSpecification staged = new ColumnSpecification(specification.getType(), specification.getLocation(), target, - specification.getComponentName()); + ColumnSpecification staged = new ColumnSpecification(specification.getType(), + specification.getLocation(), target, specification.getComponentName()); staged.setTolerance(specification.getTolerance()); staged.setMaxIterations(specification.getMaxIterations()); return staged; @@ -2007,7 +2048,7 @@ private ColumnSpecification createHomotopySpecification(ColumnSpecification spec private double boundSpecificationTarget(ColumnSpecification specification, double target) { double finiteTarget = Double.isFinite(target) ? target : specification.getTargetValue(); if (specification.getType() == ColumnSpecification.SpecificationType.PRODUCT_PURITY - || specification.getType() == ColumnSpecification.SpecificationType.COMPONENT_RECOVERY) { + || specification.getType() == ColumnSpecification.SpecificationType.COMPONENT_RECOVERY) { return Math.max(0.0, Math.min(1.0, finiteTarget)); } if (specification.getType() == ColumnSpecification.SpecificationType.PRODUCT_FLOW_RATE) { @@ -2024,10 +2065,11 @@ private double boundSpecificationTarget(ColumnSpecification specification, doubl * @param topTemperature top temperature guess in kelvin * @param bottomTemperature bottom temperature guess in kelvin */ - private void applySpecificationTemperatureGuess(boolean adjustTop, boolean adjustBottom, double topTemperature, - double bottomTemperature) { + private void applySpecificationTemperatureGuess(boolean adjustTop, boolean adjustBottom, + double topTemperature, double bottomTemperature) { double top = adjustTop && Double.isFinite(topTemperature) ? topTemperature : Double.NaN; - double bottom = adjustBottom && Double.isFinite(bottomTemperature) ? bottomTemperature : Double.NaN; + double bottom = + adjustBottom && Double.isFinite(bottomTemperature) ? bottomTemperature : Double.NaN; if (!Double.isFinite(top) && hasCondenser && getCondenser().isSetOutTemperature()) { top = getCondenser().getOutTemperature(); } @@ -2057,9 +2099,10 @@ private void seedTrayTemperatureProfile(double topTemperature, double bottomTemp seedTemperatures[trayIndex] = temperature; trays.get(trayIndex).setTemperature(temperature); try { - trays.get(trayIndex).getThermoSystem().setTemperature(temperature); + trays.get(trayIndex).getThermoSystem().setTemperature(temperature); } catch (RuntimeException exception) { - logger.debug("Could not seed tray temperature for {}", trays.get(trayIndex).getName(), exception); + logger.debug("Could not seed tray temperature for {}", trays.get(trayIndex).getName(), + exception); } } } @@ -2108,12 +2151,13 @@ private boolean needsAdjustment(ColumnSpecification spec) { // Reflux ratio and duty specs are handled directly; the others need outer-loop // adjustment return spec.getType() != ColumnSpecification.SpecificationType.REFLUX_RATIO - && spec.getType() != ColumnSpecification.SpecificationType.DUTY; + && spec.getType() != ColumnSpecification.SpecificationType.DUTY; } /** - * Run the inner column solver (one full solve with the currently selected solver type) without resetting convergence - * history. Used by {@link #solveWithSpecifications(UUID)} in the outer adjustment loop. + * Run the inner column solver (one full solve with the currently selected solver type) without + * resetting convergence history. Used by {@link #solveWithSpecifications(UUID)} in the outer + * adjustment loop. * * @param id calculation identifier */ @@ -2148,10 +2192,11 @@ void solveMeshResidual(UUID id) { solveInsideOut(id); updateMeshResiduals(); if (meshResidualNeedsPolishing()) { - double residualNorm = lastMeshResidual == null ? Double.NaN : lastMeshResidual.getInfinityNorm(); + double residualNorm = + lastMeshResidual == null ? Double.NaN : lastMeshResidual.getInfinityNorm(); if (!tryGuardedMeshNewtonPolish(id, residualNorm)) { - logger.debug("MESH residual Newton polish rejected for column {}; residual={}", getName(), - Double.valueOf(residualNorm)); + logger.debug("MESH residual Newton polish rejected for column {}; residual={}", getName(), + Double.valueOf(residualNorm)); } } } @@ -2160,10 +2205,11 @@ void solveMeshResidual(UUID id) { * Solve using Naphtali-Sandholm simultaneous MESH equation linearization. * *

- * The rigorous residual solver is warm-started from the current inside-out path. If the Newton refinement does not - * produce a residual-improving state, the accepted inside-out state is kept. In that rejected-state path, the column - * products and solve metrics remain the inside-out warm-start values. This preserves the robust legacy behavior while - * making the new solver an explicit residual-driven option. + * The rigorous residual solver is warm-started from the current inside-out path. If the Newton + * refinement does not produce a residual-improving state, the accepted inside-out state is kept. + * In that rejected-state path, the column products and solve metrics remain the inside-out + * warm-start values. This preserves the robust legacy behavior while making the new solver an + * explicit residual-driven option. *

* * @param id calculation identifier @@ -2188,8 +2234,8 @@ boolean solveNaphtaliSandholm(UUID id) { List clones = new java.util.ArrayList<>(); List flowRates = new java.util.ArrayList<>(); for (StreamInterface feed : entry.getValue()) { - clones.add(feed.getThermoSystem().clone()); - flowRates.add(feed.getFlowRate("mol/hr")); + clones.add(feed.getThermoSystem().clone()); + flowRates.add(feed.getFlowRate("mol/hr")); } originalFeedSystems.put(entry.getKey(), clones); originalFeedFlowRates.put(entry.getKey(), flowRates); @@ -2200,7 +2246,8 @@ boolean solveNaphtaliSandholm(UUID id) { } prepareColumnForSolve(); - NaphtaliSandholmSolver solver = new NaphtaliSandholmSolver(this, originalFeedSystems, originalFeedFlowRates); + NaphtaliSandholmSolver solver = + new NaphtaliSandholmSolver(this, originalFeedSystems, originalFeedFlowRates); solver.setMaxIterations(maxNumberOfIterations); solver.setTolerance(1.0e-8); boolean accepted = solver.solve(id); @@ -2208,8 +2255,8 @@ boolean solveNaphtaliSandholm(UUID id) { markSolverTypeUsed(SolverType.NAPHTALI_SANDHOLM); double temperatureResidual = accepted ? solver.getLastTemperatureResidual() : 1.0e10; - finalizeNaphtaliSolve(id, solver.getLastIterations(), temperatureResidual, solver.getLastMassBalanceError(), - solver.getLastEnergyResidual(), startTime); + finalizeNaphtaliSolve(id, solver.getLastIterations(), temperatureResidual, + solver.getLastMassBalanceError(), solver.getLastEnergyResidual(), startTime); hasBeenSolvedBefore = true; lastTotalFeedFlow = -1.0; @@ -2229,8 +2276,8 @@ boolean solveNaphtaliSandholm(UUID id) { * @param energyResidual final energy residual * @param startTime nano time when the solve started */ - private void finalizeNaphtaliSolve(UUID id, int iterations, double temperatureResidual, double massResidual, - double energyResidual, long startTime) { + private void finalizeNaphtaliSolve(UUID id, int iterations, double temperatureResidual, + double massResidual, double energyResidual, long startTime) { err = temperatureResidual; lastIterationCount = iterations; lastTemperatureResidual = temperatureResidual; @@ -2295,13 +2342,14 @@ private double finiteOr(double value, double fallback) { } /** - * Try one Newton polishing pass on a deep-copied candidate column and accept it only if the MESH residual norm - * improves. + * Try one Newton polishing pass on a deep-copied candidate column and accept it only if the MESH + * residual norm improves. * *

- * Running the aggressive Newton accelerator on a candidate protects the accepted inside-out solution from flash - * failures, non-finite states, or residual growth. This provides a bounded line-search style guard for the - * residual-monitored solver without changing the legacy Newton solver contract. + * Running the aggressive Newton accelerator on a candidate protects the accepted inside-out + * solution from flash failures, non-finite states, or residual growth. This provides a bounded + * line-search style guard for the residual-monitored solver without changing the legacy Newton + * solver contract. *

* * @param id calculation identifier @@ -2332,30 +2380,32 @@ private boolean tryGuardedMeshNewtonPolish(UUID id, double baselineResidualNorm) } double candidateResidualNorm = candidate.lastMeshResidual == null ? Double.NaN - : candidate.lastMeshResidual.getInfinityNorm(); + : candidate.lastMeshResidual.getInfinityNorm(); double candidateProductDrawResidual = candidate.getLastMeshProductDrawResidualNorm(); boolean residualImproved = Double.isFinite(candidateResidualNorm) - && candidateResidualNorm < baselineResidualNorm * 0.999; - boolean productDrawGateRecovered = productDrawGateRecovered(candidateResidualNorm, candidateProductDrawResidual, - baselineResidualNorm, baselineProductDrawResidual); + && candidateResidualNorm < baselineResidualNorm * 0.999; + boolean productDrawGateRecovered = productDrawGateRecovered(candidateResidualNorm, + candidateProductDrawResidual, baselineResidualNorm, baselineProductDrawResidual); if (!residualImproved && !productDrawGateRecovered) { logger.debug("MESH Newton polish rejected: residual {} did not improve baseline {}.", - Double.valueOf(candidateResidualNorm), Double.valueOf(baselineResidualNorm)); + Double.valueOf(candidateResidualNorm), Double.valueOf(baselineResidualNorm)); return false; } - if (!productDrawGateRecovered && !meshPolishProductSplitMatches(candidate, baselineGasFlow, baselineLiquidFlow)) { + if (!productDrawGateRecovered + && !meshPolishProductSplitMatches(candidate, baselineGasFlow, baselineLiquidFlow)) { logger.debug( - "MESH Newton polish rejected: product split changed from gas/liquid " + "{}/{} kg/hr to {}/{} kg/hr.", - Double.valueOf(baselineGasFlow), Double.valueOf(baselineLiquidFlow), - Double.valueOf(getProductFlowKgPerHour(candidate.gasOutStream)), - Double.valueOf(getProductFlowKgPerHour(candidate.liquidOutStream))); + "MESH Newton polish rejected: product split changed from gas/liquid " + + "{}/{} kg/hr to {}/{} kg/hr.", + Double.valueOf(baselineGasFlow), Double.valueOf(baselineLiquidFlow), + Double.valueOf(getProductFlowKgPerHour(candidate.gasOutStream)), + Double.valueOf(getProductFlowKgPerHour(candidate.liquidOutStream))); return false; } acceptSolvedStateCandidate(candidate); logger.debug("MESH Newton polish accepted: residual {} improved baseline {}.", - Double.valueOf(candidateResidualNorm), Double.valueOf(baselineResidualNorm)); + Double.valueOf(candidateResidualNorm), Double.valueOf(baselineResidualNorm)); return true; } @@ -2366,15 +2416,17 @@ private boolean tryGuardedMeshNewtonPolish(UUID id, double baselineResidualNorm) * @param candidateProductDrawResidual candidate product-draw residual norm * @param baselineResidualNorm baseline MESH infinity norm * @param baselineProductDrawResidual baseline product-draw residual norm - * @return {@code true} when product-draw residuals satisfy their gate without worsening the overall residual norm + * @return {@code true} when product-draw residuals satisfy their gate without worsening the + * overall residual norm */ - private boolean productDrawGateRecovered(double candidateResidualNorm, double candidateProductDrawResidual, - double baselineResidualNorm, double baselineProductDrawResidual) { + private boolean productDrawGateRecovered(double candidateResidualNorm, + double candidateProductDrawResidual, double baselineResidualNorm, + double baselineProductDrawResidual) { return Double.isFinite(candidateResidualNorm) && Double.isFinite(candidateProductDrawResidual) - && Double.isFinite(baselineProductDrawResidual) - && candidateResidualNorm <= Math.max(baselineResidualNorm, meshResidualTolerance) - && candidateProductDrawResidual <= meshProductDrawResidualTolerance - && candidateProductDrawResidual < baselineProductDrawResidual; + && Double.isFinite(baselineProductDrawResidual) + && candidateResidualNorm <= Math.max(baselineResidualNorm, meshResidualTolerance) + && candidateProductDrawResidual <= meshProductDrawResidualTolerance + && candidateProductDrawResidual < baselineProductDrawResidual; } /** @@ -2385,15 +2437,15 @@ private boolean productDrawGateRecovered(double candidateResidualNorm, double ca * @param baselineLiquidFlow liquid product flow before polishing in kg/hr * @return {@code true} when both product flows remain within tolerance */ - private boolean meshPolishProductSplitMatches(DistillationColumn candidate, double baselineGasFlow, - double baselineLiquidFlow) { + private boolean meshPolishProductSplitMatches(DistillationColumn candidate, + double baselineGasFlow, double baselineLiquidFlow) { if (candidate == null) { return false; } double candidateGasFlow = getProductFlowKgPerHour(candidate.gasOutStream); double candidateLiquidFlow = getProductFlowKgPerHour(candidate.liquidOutStream); return productFlowWithinMeshPolishTolerance(candidateGasFlow, baselineGasFlow) - && productFlowWithinMeshPolishTolerance(candidateLiquidFlow, baselineLiquidFlow); + && productFlowWithinMeshPolishTolerance(candidateLiquidFlow, baselineLiquidFlow); } /** @@ -2424,7 +2476,8 @@ private boolean productFlowWithinMeshPolishTolerance(double candidateFlow, doubl if (!Double.isFinite(candidateFlow) || !Double.isFinite(baselineFlow)) { return false; } - double tolerance = Math.max(1.0e-8, Math.abs(baselineFlow) * MESH_POLISH_PRODUCT_FLOW_TOLERANCE); + double tolerance = + Math.max(1.0e-8, Math.abs(baselineFlow) * MESH_POLISH_PRODUCT_FLOW_TOLERANCE); return Math.abs(candidateFlow - baselineFlow) <= tolerance; } @@ -2432,10 +2485,10 @@ private boolean productFlowWithinMeshPolishTolerance(double candidateFlow, doubl * Copy the solved state from an accepted candidate back to this live column. * *

- * User-facing feed stream maps are intentionally not replaced: they may contain stream object identities supplied by - * callers and are reused on later runs. The candidate tray network already contains equivalent cloned feed streams - * for the accepted solved state, and the preserved maps will refresh tray inputs from the caller-owned streams on the - * next solve. + * User-facing feed stream maps are intentionally not replaced: they may contain stream object + * identities supplied by callers and are reused on later runs. The candidate tray network already + * contains equivalent cloned feed streams for the accepted solved state, and the preserved maps + * will refresh tray inputs from the caller-owned streams on the next solve. *

* * @param candidate accepted candidate column @@ -2452,7 +2505,8 @@ private void acceptSolvedStateCandidate(DistillationColumn candidate) { // caller-held streams at their stale pre-polish flows, removing the stale-to-solved product // flow difference from the surrounding process mass balance. this.gasOutStream = adoptSolvedProductStream(this.gasOutStream, candidate.gasOutStream); - this.liquidOutStream = adoptSolvedProductStream(this.liquidOutStream, candidate.liquidOutStream); + this.liquidOutStream = + adoptSolvedProductStream(this.liquidOutStream, candidate.liquidOutStream); this.stream_3isset = candidate.stream_3isset; this.heater = candidate.heater; this.separator2 = candidate.separator2; @@ -2482,7 +2536,8 @@ private void acceptSolvedStateCandidate(DistillationColumn candidate) { this.lastMatrixInsideOutTemperatureResidual = candidate.lastMatrixInsideOutTemperatureResidual; this.lastMatrixInsideOutSolveTimeSeconds = candidate.lastMatrixInsideOutSolveTimeSeconds; this.lastNaphtaliAnalyticJacobianColumns = candidate.lastNaphtaliAnalyticJacobianColumns; - this.lastNaphtaliFiniteDifferenceJacobianColumns = candidate.lastNaphtaliFiniteDifferenceJacobianColumns; + this.lastNaphtaliFiniteDifferenceJacobianColumns = + candidate.lastNaphtaliFiniteDifferenceJacobianColumns; this.lastNaphtaliThermoEvaluationCount = candidate.lastNaphtaliThermoEvaluationCount; this.lastNaphtaliThermoCacheHitCount = candidate.lastNaphtaliThermoCacheHitCount; this.lastNaphtaliJacobianBuildTimeSeconds = candidate.lastNaphtaliJacobianBuildTimeSeconds; @@ -2503,28 +2558,30 @@ private void acceptSolvedStateCandidate(DistillationColumn candidate) { this.lastAutoFeasibilityReport = candidate.lastAutoFeasibilityReport; this.lastInitializationReport = candidate.lastInitializationReport; this.lastAutoSolverHistory = candidate.lastAutoSolverHistory == null ? new ArrayList() - : new ArrayList(candidate.lastAutoSolverHistory); + : new ArrayList(candidate.lastAutoSolverHistory); this.specificationHomotopySteps = candidate.specificationHomotopySteps; this.lastSpecificationHomotopyStepCount = candidate.lastSpecificationHomotopyStepCount; } /** - * Copy an accepted candidate product stream's solved state into the live product stream while preserving the live - * stream's object identity. + * Copy an accepted candidate product stream's solved state into the live product stream while + * preserving the live stream's object identity. * *

- * Downstream equipment that captured {@link #getGasOutStream()} or {@link #getLiquidOutStream()} before the column - * solved holds those stream objects by reference. Replacing the field with the candidate's freshly copied stream - * orphaned the caller-held objects at their stale (pre-polish) flows, which dropped the difference between the stale - * and solved product flows from the overall process mass balance. Copying the solved thermodynamic system into the - * existing stream keeps the caller-held reference live and mass-consistent. + * Downstream equipment that captured {@link #getGasOutStream()} or {@link #getLiquidOutStream()} + * before the column solved holds those stream objects by reference. Replacing the field with the + * candidate's freshly copied stream orphaned the caller-held objects at their stale (pre-polish) + * flows, which dropped the difference between the stale and solved product flows from the overall + * process mass balance. Copying the solved thermodynamic system into the existing stream keeps + * the caller-held reference live and mass-consistent. *

* * @param live the existing product stream whose identity must be preserved * @param solved the accepted candidate product stream carrying the solved state * @return the product stream reference to retain on this column */ - private static StreamInterface adoptSolvedProductStream(StreamInterface live, StreamInterface solved) { + private static StreamInterface adoptSolvedProductStream(StreamInterface live, + StreamInterface solved) { if (live == null) { return solved; } @@ -2599,9 +2656,8 @@ void recordAutoSolverEvent(String event) { * @param reason reason the accelerator result was rejected */ void acceptDampedFallbackCandidate(DistillationColumn candidate, String reason) { - logger.warn( - "Accelerated solver result rejected for column {}: {}. Using damped " + "substitution fallback candidate.", - getName(), reason); + logger.warn("Accelerated solver result rejected for column {}: {}. Using damped " + + "substitution fallback candidate.", getName(), reason); acceptSolvedStateCandidate(candidate); lastSolverTypeUsed = SolverType.DAMPED_SUBSTITUTION; lastSolveStatusReason = reason; @@ -2611,9 +2667,9 @@ void acceptDampedFallbackCandidate(DistillationColumn candidate, String reason) * Accept a residual-monitored warm-start state after rejecting a Naphtali-Sandholm candidate. * *

- * The accepted state comes from the warm-start solver, but the strategy reported to callers remains - * {@link SolverType#NAPHTALI_SANDHOLM}. Naphtali-Sandholm telemetry from the rejected candidate is preserved so - * diagnostics still show the attempted linearization work. + * The accepted state comes from the warm-start solver, but the strategy reported to callers + * remains {@link SolverType#NAPHTALI_SANDHOLM}. Naphtali-Sandholm telemetry from the rejected + * candidate is preserved so diagnostics still show the attempted linearization work. *

* * @param candidate solved warm-start candidate to keep @@ -2629,9 +2685,8 @@ void acceptNaphtaliWarmStartCandidate(DistillationColumn candidate, String reaso int denseLinearSolveCount = lastNaphtaliDenseLinearSolveCount; double linearSolveTimeSeconds = lastNaphtaliLinearSolveTimeSeconds; - logger.warn( - "Naphtali-Sandholm candidate rejected for column {}: {}. Keeping " + "residual-monitored warm-start state.", - getName(), reason); + logger.warn("Naphtali-Sandholm candidate rejected for column {}: {}. Keeping " + + "residual-monitored warm-start state.", getName(), reason); acceptSolvedStateCandidate(candidate); lastSolverTypeUsed = SolverType.NAPHTALI_SANDHOLM; lastSolveStatusReason = reason; @@ -2652,7 +2707,8 @@ void acceptNaphtaliWarmStartCandidate(DistillationColumn candidate, String reaso */ private boolean meshResidualNeedsPolishing() { return lastMeshResidual == null || !lastMeshResidual.isFinite() - || lastMeshResidual.getInfinityNorm() > meshResidualTolerance || !productDrawResidualsSatisfied(); + || lastMeshResidual.getInfinityNorm() > meshResidualTolerance + || !productDrawResidualsSatisfied(); } /** @@ -2674,23 +2730,24 @@ private double evaluateSpecError(ColumnSpecification spec) { } switch (spec.getType()) { - case PRODUCT_PURITY: { - double currentPurity = productStream.getFluid().getComponent(spec.getComponentName()).getz(); - return currentPurity - spec.getTargetValue(); - } - case COMPONENT_RECOVERY: { - double productCompFlow = productStream.getFluid().getComponent(spec.getComponentName()) - .getTotalFlowRate("mol/hr"); - double totalFeedCompFlow = getTotalFeedComponentFlow(spec.getComponentName()); - double recovery = (totalFeedCompFlow > 1.0e-12) ? productCompFlow / totalFeedCompFlow : 0.0; - return recovery - spec.getTargetValue(); - } - case PRODUCT_FLOW_RATE: { - double currentFlow = productStream.getFluid().getFlowRate("mol/hr"); - return currentFlow - spec.getTargetValue(); - } - default: - return 0.0; + case PRODUCT_PURITY: { + double currentPurity = + productStream.getFluid().getComponent(spec.getComponentName()).getz(); + return currentPurity - spec.getTargetValue(); + } + case COMPONENT_RECOVERY: { + double productCompFlow = productStream.getFluid().getComponent(spec.getComponentName()) + .getTotalFlowRate("mol/hr"); + double totalFeedCompFlow = getTotalFeedComponentFlow(spec.getComponentName()); + double recovery = (totalFeedCompFlow > 1.0e-12) ? productCompFlow / totalFeedCompFlow : 0.0; + return recovery - spec.getTargetValue(); + } + case PRODUCT_FLOW_RATE: { + double currentFlow = productStream.getFluid().getFlowRate("mol/hr"); + return currentFlow - spec.getTargetValue(); + } + default: + return 0.0; } } @@ -2718,8 +2775,8 @@ private double estimateFeedTemperature() { int count = 0; for (List feeds : feedStreams.values()) { for (StreamInterface feed : feeds) { - sumTemp += feed.getTemperature("K"); - count++; + sumTemp += feed.getTemperature("K"); + count++; } } return count > 0 ? sumTemp / count : 300.0; @@ -2771,16 +2828,16 @@ private int estimateFeedTrayNumber(double feedTemperature) { for (int trayNumber = firstFeedTray; trayNumber <= lastFeedTray; trayNumber++) { double trayTemperature = useTrayProfile ? trays.get(trayNumber).getTemperature() - : estimateTrayTemperatureFromColumnEnds(trayNumber, feedTemperature); + : estimateTrayTemperatureFromColumnEnds(trayNumber, feedTemperature); if (!isUsableTemperature(trayTemperature)) { - continue; + continue; } double temperatureDifference = Math.abs(trayTemperature - feedTemperature); - if (temperatureDifference < minimumTemperatureDifference - || Math.abs(temperatureDifference - minimumTemperatureDifference) <= FEED_TRAY_TIE_TOLERANCE) { - minimumTemperatureDifference = temperatureDifference; - bestTray = trayNumber; + if (temperatureDifference < minimumTemperatureDifference || Math + .abs(temperatureDifference - minimumTemperatureDifference) <= FEED_TRAY_TIE_TOLERANCE) { + minimumTemperatureDifference = temperatureDifference; + bestTray = trayNumber; } } return bestTray; @@ -2822,13 +2879,14 @@ private boolean hasUsableTrayTemperatureProfile(int firstFeedTray, int lastFeedT for (int trayNumber = firstFeedTray; trayNumber <= lastFeedTray; trayNumber++) { double trayTemperature = trays.get(trayNumber).getTemperature(); if (!isUsableTemperature(trayTemperature)) { - continue; + continue; } minimumTemperature = Math.min(minimumTemperature, trayTemperature); maximumTemperature = Math.max(maximumTemperature, trayTemperature); temperatureCount++; } - return temperatureCount >= 2 && Math.abs(maximumTemperature - minimumTemperature) > MINIMUM_FEED_PROFILE_SPAN; + return temperatureCount >= 2 + && Math.abs(maximumTemperature - minimumTemperature) > MINIMUM_FEED_PROFILE_SPAN; } /** @@ -2846,7 +2904,7 @@ private double estimateTrayTemperatureFromColumnEnds(int trayNumber, double feed double bottomTemperature = estimateBottomFeedProfileTemperature(feedTemperature); double topTemperature = estimateTopFeedProfileTemperature(feedTemperature, bottomTemperature); if (!isUsableTemperature(bottomTemperature) || !isUsableTemperature(topTemperature) - || bottomTemperature - topTemperature <= MINIMUM_FEED_PROFILE_SPAN) { + || bottomTemperature - topTemperature <= MINIMUM_FEED_PROFILE_SPAN) { return Double.NaN; } @@ -2861,7 +2919,8 @@ private double estimateTrayTemperatureFromColumnEnds(int trayNumber, double feed * @return bottom temperature estimate in Kelvin */ private double estimateBottomFeedProfileTemperature(double feedTemperature) { - if (hasReboiler && getReboiler().isSetOutTemperature() && isUsableTemperature(getReboiler().getOutTemperature())) { + if (hasReboiler && getReboiler().isSetOutTemperature() + && isUsableTemperature(getReboiler().getOutTemperature())) { return getReboiler().getOutTemperature(); } double trayTemperature = trays.get(0).getTemperature(); @@ -2878,10 +2937,11 @@ private double estimateBottomFeedProfileTemperature(double feedTemperature) { * @param bottomTemperature bottom temperature estimate in Kelvin * @return top temperature estimate in Kelvin */ - private double estimateTopFeedProfileTemperature(double feedTemperature, double bottomTemperature) { + private double estimateTopFeedProfileTemperature(double feedTemperature, + double bottomTemperature) { int topTrayNumber = numberOfTrays - 1; if (hasCondenser && getCondenser().isSetOutTemperature() - && isUsableTemperature(getCondenser().getOutTemperature())) { + && isUsableTemperature(getCondenser().getOutTemperature())) { return getCondenser().getOutTemperature(); } double trayTemperature = trays.get(topTrayNumber).getTemperature(); @@ -2889,7 +2949,8 @@ && isUsableTemperature(getCondenser().getOutTemperature())) { return trayTemperature; } double topTemperature = feedTemperature - FEED_PROFILE_END_TEMPERATURE_OFFSET; - if (isUsableTemperature(bottomTemperature) && topTemperature >= bottomTemperature - MINIMUM_FEED_PROFILE_SPAN) { + if (isUsableTemperature(bottomTemperature) + && topTemperature >= bottomTemperature - MINIMUM_FEED_PROFILE_SPAN) { topTemperature = bottomTemperature - FEED_PROFILE_END_TEMPERATURE_OFFSET; } return topTemperature; @@ -2909,8 +2970,8 @@ private boolean isUsableTemperature(double temperature) { * Result from a rigorous tray-count and feed-tray search. * *

- * The result is immutable and records the selected tray count, selected feed tray, product purity, duty estimates and - * convergence diagnostics from the final candidate run. + * The result is immutable and records the selected tray count, selected feed tray, product + * purity, duty estimates and convergence diagnostics from the final candidate run. *

* * @author esol @@ -2958,10 +3019,11 @@ public static class TrayOptimizationResult implements java.io.Serializable { * @param convergedCases number of evaluated cases that converged * @param message diagnostic message describing the outcome */ - public TrayOptimizationResult(boolean feasible, int numberOfTrays, int feedTrayNumber, String componentName, - boolean topProduct, double targetPurity, double productPurity, double reboilerDuty, double condenserDuty, - double totalAbsoluteDuty, int iterationCount, double temperatureResidual, double massResidual, - double energyResidual, int evaluatedCases, int convergedCases, String message) { + public TrayOptimizationResult(boolean feasible, int numberOfTrays, int feedTrayNumber, + String componentName, boolean topProduct, double targetPurity, double productPurity, + double reboilerDuty, double condenserDuty, double totalAbsoluteDuty, int iterationCount, + double temperatureResidual, double massResidual, double energyResidual, int evaluatedCases, + int convergedCases, String message) { this.feasible = feasible; this.numberOfTrays = numberOfTrays; this.feedTrayNumber = feedTrayNumber; @@ -3139,8 +3201,9 @@ public String getMessage() { * Result from initializing a rigorous column with shortcut FUG design estimates. * *

- * The result records the Fenske-Underwood-Gilliland estimates and the translated rigorous-column settings applied to - * {@link DistillationColumn}: total stage count, bottom-up feed tray and condenser reflux ratio. + * The result records the Fenske-Underwood-Gilliland estimates and the translated rigorous-column + * settings applied to {@link DistillationColumn}: total stage count, bottom-up feed tray and + * condenser reflux ratio. *

* * @author esol @@ -3181,10 +3244,11 @@ public static class ShortcutInitializationResult implements java.io.Serializable * @param heavyKey heavy-key component name * @param message diagnostic message */ - public ShortcutInitializationResult(boolean initialized, int totalStageCount, int feedTrayNumber, - int feedTrayNumberFromTop, double minimumStages, double minimumRefluxRatio, double actualStages, - double actualRefluxRatio, double condenserDuty, double reboilerDuty, String lightKey, String heavyKey, - String message) { + public ShortcutInitializationResult(boolean initialized, int totalStageCount, + int feedTrayNumber, int feedTrayNumberFromTop, double minimumStages, + double minimumRefluxRatio, double actualStages, double actualRefluxRatio, + double condenserDuty, double reboilerDuty, String lightKey, String heavyKey, + String message) { this.initialized = initialized; this.totalStageCount = totalStageCount; this.feedTrayNumber = feedTrayNumber; @@ -3322,9 +3386,9 @@ public String getMessage() { * Result from an economic tray-count, feed-tray, and optional reflux/boilup search. * *

- * The result extends the rigorous tray optimization result with mechanical design, installed capital cost, annual - * utility cost, and annualized total-cost metrics. Costs are screening-level estimates using the column mechanical - * design and column cost-estimation correlations. + * The result extends the rigorous tray optimization result with mechanical design, installed + * capital cost, annual utility cost, and annualized total-cost metrics. Costs are screening-level + * estimates using the column mechanical design and column cost-estimation correlations. *

* * @author esol @@ -3369,16 +3433,17 @@ public static class EconomicTrayOptimizationResult extends TrayOptimizationResul * @param reboilerRatio selected reboiler boilup/reflux ratio, or {@link Double#NaN} */ public EconomicTrayOptimizationResult(TrayOptimizationResult baseResult, double capitalCost, - double annualUtilityCost, double annualizedCapitalCost, double totalAnnualizedCost, double capitalChargeFactor, - double operatingHoursPerYear, double steamCostPerTonne, double coolingWaterCostPerM3, double trayEfficiency, - int actualTrays, double columnDiameter, double columnHeight, double condenserRefluxRatio, - double reboilerRatio) { + double annualUtilityCost, double annualizedCapitalCost, double totalAnnualizedCost, + double capitalChargeFactor, double operatingHoursPerYear, double steamCostPerTonne, + double coolingWaterCostPerM3, double trayEfficiency, int actualTrays, double columnDiameter, + double columnHeight, double condenserRefluxRatio, double reboilerRatio) { super(baseResult.isFeasible(), baseResult.getNumberOfTrays(), baseResult.getFeedTrayNumber(), - baseResult.getComponentName(), baseResult.isTopProduct(), baseResult.getTargetPurity(), - baseResult.getProductPurity(), baseResult.getReboilerDuty(), baseResult.getCondenserDuty(), - baseResult.getTotalAbsoluteDuty(), baseResult.getIterationCount(), baseResult.getTemperatureResidual(), - baseResult.getMassResidual(), baseResult.getEnergyResidual(), baseResult.getEvaluatedCases(), - baseResult.getConvergedCases(), baseResult.getMessage()); + baseResult.getComponentName(), baseResult.isTopProduct(), baseResult.getTargetPurity(), + baseResult.getProductPurity(), baseResult.getReboilerDuty(), + baseResult.getCondenserDuty(), baseResult.getTotalAbsoluteDuty(), + baseResult.getIterationCount(), baseResult.getTemperatureResidual(), + baseResult.getMassResidual(), baseResult.getEnergyResidual(), + baseResult.getEvaluatedCases(), baseResult.getConvergedCases(), baseResult.getMessage()); this.capitalCost = capitalCost; this.annualUtilityCost = annualUtilityCost; this.annualizedCapitalCost = annualizedCapitalCost; @@ -3586,32 +3651,33 @@ private ColumnOptimizationState copy() { * Find the minimum tray count and best feed tray that meet a product specification. * *

- * The search evaluates total tray count and feed tray together. It returns the first total tray count that has a - * converged case meeting the requested purity, then selects the feed tray with the lowest absolute - * condenser-plus-reboiler duty for that tray count. The selected candidate is applied back to this column and the - * final solved state is left in the object. + * The search evaluates total tray count and feed tray together. It returns the first total tray + * count that has a converged case meeting the requested purity, then selects the feed tray with + * the lowest absolute condenser-plus-reboiler duty for that tray count. The selected candidate is + * applied back to this column and the final solved state is left in the object. *

* * @param productSpec the target purity (mole fraction) of the key component * @param componentName the name of the key component * @param isTopProduct true if the spec is for the top product, false for the bottom product * @param maxTrays the maximum total tray count to try including reboiler/condenser if present - * @return structured optimization result with selected tray count, feed tray, duties and residuals + * @return structured optimization result with selected tray count, feed tray, duties and + * residuals */ - public TrayOptimizationResult findOptimalTrayConfiguration(double productSpec, String componentName, - boolean isTopProduct, int maxTrays) { + public TrayOptimizationResult findOptimalTrayConfiguration(double productSpec, + String componentName, boolean isTopProduct, int maxTrays) { long optimizationStartNanos = System.nanoTime(); ColumnOptimizationState state = captureColumnOptimizationState(); List optimizationFeeds = collectOptimizationFeeds(); if (optimizationFeeds.isEmpty()) { return createInfeasibleTrayOptimizationResult(productSpec, componentName, isTopProduct, 0, 0, - "No feed streams are connected to the column."); + "No feed streams are connected to the column."); } int minimumTrayCount = getMinimumOptimizationTrayCount(); if (maxTrays < minimumTrayCount) { return createInfeasibleTrayOptimizationResult(productSpec, componentName, isTopProduct, 0, 0, - "Maximum tray count is below the minimum searchable column size."); + "Maximum tray count is below the minimum searchable column size."); } int evaluatedCases = 0; @@ -3623,44 +3689,50 @@ public TrayOptimizationResult findOptimalTrayConfiguration(double productSpec, S int lastFeedTray = getLastFeedTrayCandidate(); for (int feedTray = firstFeedTray; feedTray <= lastFeedTray; feedTray++) { - if (isTrayOptimizationSearchBudgetExceeded(evaluatedCases, optimizationStartNanos)) { - String budgetMessage = createTrayOptimizationBudgetMessage(evaluatedCases, optimizationStartNanos); - if (bestForTrayCount != null) { - return applyTrayOptimizationResult(bestForTrayCount, optimizationFeeds, state, productSpec, componentName, - isTopProduct, evaluatedCases, convergedCases, "Selected best candidate found before " + budgetMessage); - } - return createInfeasibleTrayOptimizationResult(productSpec, componentName, isTopProduct, evaluatedCases, - convergedCases, budgetMessage); - } - evaluatedCases++; - TrayOptimizationResult candidate = evaluateTrayOptimizationCandidate(totalTrayCount, feedTray, productSpec, - componentName, isTopProduct, optimizationFeeds, state); - if (solved()) { - convergedCases++; - } - if (candidate.isFeasible() && isBetterTrayOptimizationCandidate(candidate, bestForTrayCount)) { - bestForTrayCount = candidate; - } - if (isTrayOptimizationSearchBudgetExceeded(evaluatedCases, optimizationStartNanos)) { - String budgetMessage = createTrayOptimizationBudgetMessage(evaluatedCases, optimizationStartNanos); - if (bestForTrayCount != null) { - return applyTrayOptimizationResult(bestForTrayCount, optimizationFeeds, state, productSpec, componentName, - isTopProduct, evaluatedCases, convergedCases, "Selected best candidate found before " + budgetMessage); - } - return createInfeasibleTrayOptimizationResult(productSpec, componentName, isTopProduct, evaluatedCases, - convergedCases, budgetMessage); - } + if (isTrayOptimizationSearchBudgetExceeded(evaluatedCases, optimizationStartNanos)) { + String budgetMessage = + createTrayOptimizationBudgetMessage(evaluatedCases, optimizationStartNanos); + if (bestForTrayCount != null) { + return applyTrayOptimizationResult(bestForTrayCount, optimizationFeeds, state, + productSpec, componentName, isTopProduct, evaluatedCases, convergedCases, + "Selected best candidate found before " + budgetMessage); + } + return createInfeasibleTrayOptimizationResult(productSpec, componentName, isTopProduct, + evaluatedCases, convergedCases, budgetMessage); + } + evaluatedCases++; + TrayOptimizationResult candidate = evaluateTrayOptimizationCandidate(totalTrayCount, + feedTray, productSpec, componentName, isTopProduct, optimizationFeeds, state); + if (solved()) { + convergedCases++; + } + if (candidate.isFeasible() + && isBetterTrayOptimizationCandidate(candidate, bestForTrayCount)) { + bestForTrayCount = candidate; + } + if (isTrayOptimizationSearchBudgetExceeded(evaluatedCases, optimizationStartNanos)) { + String budgetMessage = + createTrayOptimizationBudgetMessage(evaluatedCases, optimizationStartNanos); + if (bestForTrayCount != null) { + return applyTrayOptimizationResult(bestForTrayCount, optimizationFeeds, state, + productSpec, componentName, isTopProduct, evaluatedCases, convergedCases, + "Selected best candidate found before " + budgetMessage); + } + return createInfeasibleTrayOptimizationResult(productSpec, componentName, isTopProduct, + evaluatedCases, convergedCases, budgetMessage); + } } if (bestForTrayCount != null) { - return applyTrayOptimizationResult(bestForTrayCount, optimizationFeeds, state, productSpec, componentName, - isTopProduct, evaluatedCases, convergedCases, - "Selected minimum-tray candidate with lowest duty for that tray count."); + return applyTrayOptimizationResult(bestForTrayCount, optimizationFeeds, state, productSpec, + componentName, isTopProduct, evaluatedCases, convergedCases, + "Selected minimum-tray candidate with lowest duty for that tray count."); } } - return createInfeasibleTrayOptimizationResult(productSpec, componentName, isTopProduct, evaluatedCases, - convergedCases, "No converged tray/feed-tray candidate met the product spec."); + return createInfeasibleTrayOptimizationResult(productSpec, componentName, isTopProduct, + evaluatedCases, convergedCases, + "No converged tray/feed-tray candidate met the product spec."); } /** @@ -3672,8 +3744,10 @@ public TrayOptimizationResult findOptimalTrayConfiguration(double productSpec, S * @param maxTrays the maximum total tray count to try including reboiler/condenser if present * @return the optimal number of trays, or -1 if the spec could not be met */ - public int findOptimalNumberOfTrays(double productSpec, String componentName, boolean isTopProduct, int maxTrays) { - TrayOptimizationResult result = findOptimalTrayConfiguration(productSpec, componentName, isTopProduct, maxTrays); + public int findOptimalNumberOfTrays(double productSpec, String componentName, + boolean isTopProduct, int maxTrays) { + TrayOptimizationResult result = + findOptimalTrayConfiguration(productSpec, componentName, isTopProduct, maxTrays); return result.isFeasible() ? result.getNumberOfTrays() : -1; } @@ -3681,11 +3755,12 @@ public int findOptimalNumberOfTrays(double productSpec, String componentName, bo * Find the tray count and feed tray that minimize annualized column cost. * *

- * This method searches tray count and feed tray for all converged candidates that meet the product specification, - * then selects the candidate with the lowest annualized cost. The cost is calculated as annualized installed capital - * plus reboiler/condenser utility cost using the column mechanical design and cost-estimation correlations. Default - * assumptions are 15%/year capital charge factor, 8000 operating hours/year, 25 USD/tonne steam, and 0.03 USD/m3 - * cooling water. + * This method searches tray count and feed tray for all converged candidates that meet the + * product specification, then selects the candidate with the lowest annualized cost. The cost is + * calculated as annualized installed capital plus reboiler/condenser utility cost using the + * column mechanical design and cost-estimation correlations. Default assumptions are 15%/year + * capital charge factor, 8000 operating hours/year, 25 USD/tonne steam, and 0.03 USD/m3 cooling + * water. *

* * @param productSpec the target purity (mole fraction) of the key component @@ -3694,10 +3769,10 @@ public int findOptimalNumberOfTrays(double productSpec, String componentName, bo * @param maxTrays the maximum total tray count to try including reboiler/condenser if present * @return economic optimization result with process, mechanical design, and cost metrics */ - public EconomicTrayOptimizationResult findEconomicOptimalTrayConfiguration(double productSpec, String componentName, - boolean isTopProduct, int maxTrays) { - return findEconomicOptimalTrayConfiguration(productSpec, componentName, isTopProduct, maxTrays, 0.15, 8000.0, 25.0, - 0.03, getCurrentMechanicalDesignTrayEfficiency()); + public EconomicTrayOptimizationResult findEconomicOptimalTrayConfiguration(double productSpec, + String componentName, boolean isTopProduct, int maxTrays) { + return findEconomicOptimalTrayConfiguration(productSpec, componentName, isTopProduct, maxTrays, + 0.15, 8000.0, 25.0, 0.03, getCurrentMechanicalDesignTrayEfficiency()); } /** @@ -3714,20 +3789,22 @@ public EconomicTrayOptimizationResult findEconomicOptimalTrayConfiguration(doubl * @param trayEfficiency overall tray efficiency used for actual tray count and column height * @return economic optimization result with process, mechanical design, and cost metrics */ - public EconomicTrayOptimizationResult findEconomicOptimalTrayConfiguration(double productSpec, String componentName, - boolean isTopProduct, int maxTrays, double capitalChargeFactor, double operatingHoursPerYear, - double steamCostPerTonne, double coolingWaterCostPerM3, double trayEfficiency) { - return findEconomicOptimalTrayConfiguration(productSpec, componentName, isTopProduct, maxTrays, null, null, - capitalChargeFactor, operatingHoursPerYear, steamCostPerTonne, coolingWaterCostPerM3, trayEfficiency); + public EconomicTrayOptimizationResult findEconomicOptimalTrayConfiguration(double productSpec, + String componentName, boolean isTopProduct, int maxTrays, double capitalChargeFactor, + double operatingHoursPerYear, double steamCostPerTonne, double coolingWaterCostPerM3, + double trayEfficiency) { + return findEconomicOptimalTrayConfiguration(productSpec, componentName, isTopProduct, maxTrays, + null, null, capitalChargeFactor, operatingHoursPerYear, steamCostPerTonne, + coolingWaterCostPerM3, trayEfficiency); } /** * Find the annualized-cost optimum for tray count, feed tray, and optional ratio candidates. * *

- * If reflux or reboiler ratio candidate arrays are supplied, each positive finite ratio is tried for every - * tray-count/feed-tray case. If an array is {@code null} or empty, the current column specification is preserved for - * that end of the column. + * If reflux or reboiler ratio candidate arrays are supplied, each positive finite ratio is tried + * for every tray-count/feed-tray case. If an array is {@code null} or empty, the current column + * specification is preserved for that end of the column. *

* * @param productSpec the target purity (mole fraction) of the key component @@ -3743,24 +3820,25 @@ public EconomicTrayOptimizationResult findEconomicOptimalTrayConfiguration(doubl * @param trayEfficiency overall tray efficiency used for actual tray count and column height * @return economic optimization result with process, mechanical design, and cost metrics */ - public EconomicTrayOptimizationResult findEconomicOptimalTrayConfiguration(double productSpec, String componentName, - boolean isTopProduct, int maxTrays, double[] condenserRefluxRatios, double[] reboilerRatios, - double capitalChargeFactor, double operatingHoursPerYear, double steamCostPerTonne, double coolingWaterCostPerM3, - double trayEfficiency) { + public EconomicTrayOptimizationResult findEconomicOptimalTrayConfiguration(double productSpec, + String componentName, boolean isTopProduct, int maxTrays, double[] condenserRefluxRatios, + double[] reboilerRatios, double capitalChargeFactor, double operatingHoursPerYear, + double steamCostPerTonne, double coolingWaterCostPerM3, double trayEfficiency) { long optimizationStartNanos = System.nanoTime(); ColumnOptimizationState state = captureColumnOptimizationState(); List optimizationFeeds = collectOptimizationFeeds(); if (optimizationFeeds.isEmpty()) { - return createInfeasibleEconomicTrayOptimizationResult(productSpec, componentName, isTopProduct, 0, 0, - capitalChargeFactor, operatingHoursPerYear, steamCostPerTonne, coolingWaterCostPerM3, trayEfficiency, - "No feed streams are connected to the column."); + return createInfeasibleEconomicTrayOptimizationResult(productSpec, componentName, + isTopProduct, 0, 0, capitalChargeFactor, operatingHoursPerYear, steamCostPerTonne, + coolingWaterCostPerM3, trayEfficiency, "No feed streams are connected to the column."); } int minimumTrayCount = getMinimumOptimizationTrayCount(); if (maxTrays < minimumTrayCount) { - return createInfeasibleEconomicTrayOptimizationResult(productSpec, componentName, isTopProduct, 0, 0, - capitalChargeFactor, operatingHoursPerYear, steamCostPerTonne, coolingWaterCostPerM3, trayEfficiency, - "Maximum tray count is below the minimum searchable column size."); + return createInfeasibleEconomicTrayOptimizationResult(productSpec, componentName, + isTopProduct, 0, 0, capitalChargeFactor, operatingHoursPerYear, steamCostPerTonne, + coolingWaterCostPerM3, trayEfficiency, + "Maximum tray count is below the minimum searchable column size."); } double[] refluxCandidates = getEconomicRatioCandidates(condenserRefluxRatios); @@ -3775,67 +3853,77 @@ public EconomicTrayOptimizationResult findEconomicOptimalTrayConfiguration(doubl int lastFeedTray = getLastFeedTrayCandidate(); for (int feedTray = firstFeedTray; feedTray <= lastFeedTray; feedTray++) { - for (int refluxIndex = 0; refluxIndex < refluxCandidates.length; refluxIndex++) { - for (int reboilerIndex = 0; reboilerIndex < reboilerCandidates.length; reboilerIndex++) { - if (isTrayOptimizationSearchBudgetExceeded(evaluatedCases, optimizationStartNanos)) { - String budgetMessage = createTrayOptimizationBudgetMessage(evaluatedCases, optimizationStartNanos); - if (bestCandidate != null) { - return applyEconomicTrayOptimizationResult(bestCandidate, optimizationFeeds, state, productSpec, - componentName, isTopProduct, evaluatedCases, convergedCases, capitalChargeFactor, - operatingHoursPerYear, steamCostPerTonne, coolingWaterCostPerM3, trayEfficiency, - "Selected best economic candidate found before " + budgetMessage); - } - return createInfeasibleEconomicTrayOptimizationResult(productSpec, componentName, isTopProduct, - evaluatedCases, convergedCases, capitalChargeFactor, operatingHoursPerYear, steamCostPerTonne, - coolingWaterCostPerM3, trayEfficiency, budgetMessage); - } - evaluatedCases++; - EconomicTrayOptimizationResult candidate = evaluateEconomicTrayOptimizationCandidate(totalTrayCount, - feedTray, productSpec, componentName, isTopProduct, optimizationFeeds, state, - refluxCandidates[refluxIndex], reboilerCandidates[reboilerIndex], capitalChargeFactor, - operatingHoursPerYear, steamCostPerTonne, coolingWaterCostPerM3, trayEfficiency); - if (solved()) { - convergedCases++; - } - if (candidate.isFeasible() && isBetterEconomicTrayOptimizationCandidate(candidate, bestCandidate)) { - bestCandidate = candidate; - } - if (isTrayOptimizationSearchBudgetExceeded(evaluatedCases, optimizationStartNanos)) { - String budgetMessage = createTrayOptimizationBudgetMessage(evaluatedCases, optimizationStartNanos); - if (bestCandidate != null) { - return applyEconomicTrayOptimizationResult(bestCandidate, optimizationFeeds, state, productSpec, - componentName, isTopProduct, evaluatedCases, convergedCases, capitalChargeFactor, - operatingHoursPerYear, steamCostPerTonne, coolingWaterCostPerM3, trayEfficiency, - "Selected best economic candidate found before " + budgetMessage); - } - return createInfeasibleEconomicTrayOptimizationResult(productSpec, componentName, isTopProduct, - evaluatedCases, convergedCases, capitalChargeFactor, operatingHoursPerYear, steamCostPerTonne, - coolingWaterCostPerM3, trayEfficiency, budgetMessage); - } - } - } + for (int refluxIndex = 0; refluxIndex < refluxCandidates.length; refluxIndex++) { + for (int reboilerIndex = 0; reboilerIndex < reboilerCandidates.length; reboilerIndex++) { + if (isTrayOptimizationSearchBudgetExceeded(evaluatedCases, optimizationStartNanos)) { + String budgetMessage = + createTrayOptimizationBudgetMessage(evaluatedCases, optimizationStartNanos); + if (bestCandidate != null) { + return applyEconomicTrayOptimizationResult(bestCandidate, optimizationFeeds, state, + productSpec, componentName, isTopProduct, evaluatedCases, convergedCases, + capitalChargeFactor, operatingHoursPerYear, steamCostPerTonne, + coolingWaterCostPerM3, trayEfficiency, + "Selected best economic candidate found before " + budgetMessage); + } + return createInfeasibleEconomicTrayOptimizationResult(productSpec, componentName, + isTopProduct, evaluatedCases, convergedCases, capitalChargeFactor, + operatingHoursPerYear, steamCostPerTonne, coolingWaterCostPerM3, trayEfficiency, + budgetMessage); + } + evaluatedCases++; + EconomicTrayOptimizationResult candidate = evaluateEconomicTrayOptimizationCandidate( + totalTrayCount, feedTray, productSpec, componentName, isTopProduct, + optimizationFeeds, state, refluxCandidates[refluxIndex], + reboilerCandidates[reboilerIndex], capitalChargeFactor, operatingHoursPerYear, + steamCostPerTonne, coolingWaterCostPerM3, trayEfficiency); + if (solved()) { + convergedCases++; + } + if (candidate.isFeasible() + && isBetterEconomicTrayOptimizationCandidate(candidate, bestCandidate)) { + bestCandidate = candidate; + } + if (isTrayOptimizationSearchBudgetExceeded(evaluatedCases, optimizationStartNanos)) { + String budgetMessage = + createTrayOptimizationBudgetMessage(evaluatedCases, optimizationStartNanos); + if (bestCandidate != null) { + return applyEconomicTrayOptimizationResult(bestCandidate, optimizationFeeds, state, + productSpec, componentName, isTopProduct, evaluatedCases, convergedCases, + capitalChargeFactor, operatingHoursPerYear, steamCostPerTonne, + coolingWaterCostPerM3, trayEfficiency, + "Selected best economic candidate found before " + budgetMessage); + } + return createInfeasibleEconomicTrayOptimizationResult(productSpec, componentName, + isTopProduct, evaluatedCases, convergedCases, capitalChargeFactor, + operatingHoursPerYear, steamCostPerTonne, coolingWaterCostPerM3, trayEfficiency, + budgetMessage); + } + } + } } } if (bestCandidate != null) { - return applyEconomicTrayOptimizationResult(bestCandidate, optimizationFeeds, state, productSpec, componentName, - isTopProduct, evaluatedCases, convergedCases, capitalChargeFactor, operatingHoursPerYear, steamCostPerTonne, - coolingWaterCostPerM3, trayEfficiency, "Selected annualized-cost optimum candidate."); + return applyEconomicTrayOptimizationResult(bestCandidate, optimizationFeeds, state, + productSpec, componentName, isTopProduct, evaluatedCases, convergedCases, + capitalChargeFactor, operatingHoursPerYear, steamCostPerTonne, coolingWaterCostPerM3, + trayEfficiency, "Selected annualized-cost optimum candidate."); } - return createInfeasibleEconomicTrayOptimizationResult(productSpec, componentName, isTopProduct, evaluatedCases, - convergedCases, capitalChargeFactor, operatingHoursPerYear, steamCostPerTonne, coolingWaterCostPerM3, - trayEfficiency, "No converged economic tray/feed-tray candidate met the product spec."); + return createInfeasibleEconomicTrayOptimizationResult(productSpec, componentName, isTopProduct, + evaluatedCases, convergedCases, capitalChargeFactor, operatingHoursPerYear, + steamCostPerTonne, coolingWaterCostPerM3, trayEfficiency, + "No converged economic tray/feed-tray candidate met the product spec."); } /** * Initialize the rigorous column from Fenske-Underwood-Gilliland shortcut estimates. * *

- * The method runs {@link ShortcutDistillationColumn}, converts its stage and feed-tray estimates into this column's - * bottom-up tray indexing, rebuilds the tray stack, adds the feed at the shortcut-estimated feed tray, applies - * condenser reflux/duty and reboiler duty estimates, and stores light-key/heavy-key recovery specifications for later - * rigorous solving. + * The method runs {@link ShortcutDistillationColumn}, converts its stage and feed-tray estimates + * into this column's bottom-up tray indexing, rebuilds the tray stack, adds the feed at the + * shortcut-estimated feed tray, applies condenser reflux/duty and reboiler duty estimates, and + * stores light-key/heavy-key recovery specifications for later rigorous solving. *

* * @param feedStream feed stream used for the shortcut calculation and rigorous column @@ -3846,15 +3934,16 @@ public EconomicTrayOptimizationResult findEconomicOptimalTrayConfiguration(doubl * @param refluxRatioMultiplier actual reflux divided by minimum reflux, normally greater than 1 * @return shortcut initialization result with applied rigorous-column settings */ - public ShortcutInitializationResult initializeFromShortcut(StreamInterface feedStream, String lightKey, - String heavyKey, double lightKeyRecoveryDistillate, double heavyKeyRecoveryBottoms, - double refluxRatioMultiplier) { + public ShortcutInitializationResult initializeFromShortcut(StreamInterface feedStream, + String lightKey, String heavyKey, double lightKeyRecoveryDistillate, + double heavyKeyRecoveryBottoms, double refluxRatioMultiplier) { if (feedStream == null) { lastShortcutInitializationResult = createFailedShortcutInitialization(lightKey, heavyKey, - "No feed stream was supplied for shortcut initialization."); + "No feed stream was supplied for shortcut initialization."); return lastShortcutInitializationResult; } - ShortcutDistillationColumn shortcut = new ShortcutDistillationColumn(getName() + " shortcut", feedStream); + ShortcutDistillationColumn shortcut = + new ShortcutDistillationColumn(getName() + " shortcut", feedStream); shortcut.setLightKey(lightKey); shortcut.setHeavyKey(heavyKey); shortcut.setLightKeyRecoveryDistillate(lightKeyRecoveryDistillate); @@ -3867,18 +3956,19 @@ public ShortcutInitializationResult initializeFromShortcut(StreamInterface feedS } catch (Exception exception) { logger.warn("Shortcut initialization failed for column {}", getName(), exception); lastShortcutInitializationResult = createFailedShortcutInitialization(lightKey, heavyKey, - "Shortcut calculation failed: " + exception.getMessage()); + "Shortcut calculation failed: " + exception.getMessage()); return lastShortcutInitializationResult; } if (!shortcut.isSolved()) { lastShortcutInitializationResult = createFailedShortcutInitialization(lightKey, heavyKey, - "Shortcut calculation did not solve. Check key-component order and recoveries."); + "Shortcut calculation did not solve. Check key-component order and recoveries."); return lastShortcutInitializationResult; } int totalStageCount = getShortcutTotalStageCount(shortcut); - int feedTrayNumber = convertShortcutFeedTrayFromTop(shortcut.getFeedTrayNumber(), totalStageCount); + int feedTrayNumber = + convertShortcutFeedTrayFromTop(shortcut.getFeedTrayNumber(), totalStageCount); ColumnOptimizationState state = captureColumnOptimizationState(); applyShortcutEndpointDuties(state, shortcut); rebuildColumnForOptimization(totalStageCount, state); @@ -3886,10 +3976,11 @@ public ShortcutInitializationResult initializeFromShortcut(StreamInterface feedS setTopComponentRecovery(lightKey, lightKeyRecoveryDistillate); setBottomComponentRecovery(heavyKey, heavyKeyRecoveryBottoms); - lastShortcutInitializationResult = new ShortcutInitializationResult(true, totalStageCount, feedTrayNumber, - shortcut.getFeedTrayNumber(), shortcut.getMinimumNumberOfStages(), shortcut.getMinimumRefluxRatio(), - shortcut.getActualNumberOfStages(), shortcut.getActualRefluxRatio(), shortcut.getCondenserDuty(), - shortcut.getReboilerDuty(), lightKey, heavyKey, "Shortcut estimates applied to rigorous column."); + lastShortcutInitializationResult = new ShortcutInitializationResult(true, totalStageCount, + feedTrayNumber, shortcut.getFeedTrayNumber(), shortcut.getMinimumNumberOfStages(), + shortcut.getMinimumRefluxRatio(), shortcut.getActualNumberOfStages(), + shortcut.getActualRefluxRatio(), shortcut.getCondenserDuty(), shortcut.getReboilerDuty(), + lightKey, heavyKey, "Shortcut estimates applied to rigorous column."); return lastShortcutInitializationResult; } @@ -3906,8 +3997,9 @@ public ShortcutInitializationResult getLastShortcutInitializationResult() { * Screen the current column setup before automatic solver candidate probing. * *

- * This method reuses the normal setup validator and adds commercial-style active-bound warnings for specifications - * that are mathematically valid but likely to make solver continuation or outer tear-variable convergence difficult. + * This method reuses the normal setup validator and adds commercial-style active-bound warnings + * for specifications that are mathematically valid but likely to make solver continuation or + * outer tear-variable convergence difficult. *

* * @return validation result with errors and active-bound warnings for the automatic solver @@ -3928,22 +4020,24 @@ boolean tryAutomaticShortcutInitialization(StringBuilder summary) { StreamInterface feedStream = getPrimaryExternalFeedStream(); if (feedStream == null || feedStream.getThermoSystem() == null) { return recordInitializationAttempt(summary, "shortcut initialization", false, - "skipped because the column has no external feed stream"); + "skipped because the column has no external feed stream"); } String[] keys = selectAutomaticShortcutKeys(feedStream.getThermoSystem()); if (keys == null) { return recordInitializationAttempt(summary, "shortcut initialization", false, - "skipped because fewer than two non-water feed components were available"); + "skipped because fewer than two non-water feed components were available"); } ColumnSpecification originalTopSpecification = topSpecification; ColumnSpecification originalBottomSpecification = bottomSpecification; - ShortcutInitializationResult result = initializeFromShortcut(feedStream, keys[0], keys[1], 0.95, 0.95, 1.4); + ShortcutInitializationResult result = + initializeFromShortcut(feedStream, keys[0], keys[1], 0.95, 0.95, 1.4); topSpecification = originalTopSpecification; bottomSpecification = originalBottomSpecification; String message = result.getMessage() + " lightKey=" + keys[0] + " heavyKey=" + keys[1]; - return recordInitializationAttempt(summary, "shortcut initialization", result.isInitialized(), message); + return recordInitializationAttempt(summary, "shortcut initialization", result.isInitialized(), + message); } /** @@ -3956,12 +4050,12 @@ boolean tryThermodynamicProfileInitialization(StringBuilder summary) { StreamInterface feedStream = getPrimaryExternalFeedStream(); if (feedStream == null || feedStream.getThermoSystem() == null) { return recordInitializationAttempt(summary, "thermodynamic profile initialization", false, - "skipped because the column has no external feed stream"); + "skipped because the column has no external feed stream"); } double feedTemperature = feedStream.getThermoSystem().getTemperature(); if (!Double.isFinite(feedTemperature) || feedTemperature <= 0.0) { return recordInitializationAttempt(summary, "thermodynamic profile initialization", false, - "skipped because the feed temperature is not finite and positive"); + "skipped because the feed temperature is not finite and positive"); } double topTemperature = Math.max(150.0, feedTemperature - 20.0); @@ -3969,7 +4063,7 @@ boolean tryThermodynamicProfileInitialization(StringBuilder summary) { seedTrayTemperatureProfile(topTemperature, bottomTemperature); setDoInitializion(true); return recordInitializationAttempt(summary, "thermodynamic profile initialization", true, - "seeded tray temperatures from " + topTemperature + " K to " + bottomTemperature + " K"); + "seeded tray temperatures from " + topTemperature + " K to " + bottomTemperature + " K"); } /** @@ -3981,20 +4075,21 @@ boolean tryThermodynamicProfileInitialization(StringBuilder summary) { private String[] selectAutomaticShortcutKeys(SystemInterface system) { String lightKey = null; String heavyKey = null; - for (int componentIndex = 0; componentIndex < system.getNumberOfComponents(); componentIndex++) { + for (int componentIndex = 0; componentIndex < system + .getNumberOfComponents(); componentIndex++) { String componentName = system.getPhase(0).getComponent(componentIndex).getComponentName(); if ("water".equalsIgnoreCase(componentName)) { - continue; + continue; } if (lightKey == null) { - lightKey = componentName; + lightKey = componentName; } heavyKey = componentName; } if (lightKey == null || heavyKey == null || lightKey.equalsIgnoreCase(heavyKey)) { return null; } - return new String[] { lightKey, heavyKey }; + return new String[] {lightKey, heavyKey}; } /** @@ -4016,10 +4111,12 @@ private StreamInterface getPrimaryExternalFeedStream() { * @param message detailed diagnostic message * @return {@code applied} */ - private boolean recordInitializationAttempt(StringBuilder summary, String label, boolean applied, String message) { + private boolean recordInitializationAttempt(StringBuilder summary, String label, boolean applied, + String message) { String token = getInitializationReportToken(label); String displayLabel = getInitializationReportLabel(label); - String report = token + " " + displayLabel + " " + (applied ? "applied" : "skipped") + ": " + message; + String report = + token + " " + displayLabel + " " + (applied ? "applied" : "skipped") + ": " + message; setLastInitializationReport(report); recordAutoSolverEvent(report); if (summary != null) { @@ -4068,10 +4165,10 @@ private String getInitializationReportLabel(String label) { * @param message diagnostic message * @return failed initialization result */ - private ShortcutInitializationResult createFailedShortcutInitialization(String lightKey, String heavyKey, - String message) { - return new ShortcutInitializationResult(false, -1, -1, -1, Double.NaN, Double.NaN, Double.NaN, Double.NaN, - Double.NaN, Double.NaN, lightKey, heavyKey, message); + private ShortcutInitializationResult createFailedShortcutInitialization(String lightKey, + String heavyKey, String message) { + return new ShortcutInitializationResult(false, -1, -1, -1, Double.NaN, Double.NaN, Double.NaN, + Double.NaN, Double.NaN, Double.NaN, lightKey, heavyKey, message); } /** @@ -4080,20 +4177,23 @@ private ShortcutInitializationResult createFailedShortcutInitialization(String l * @param shortcut shortcut column to configure * @param feedStream feed stream providing pressure if no endpoint pressure is already set */ - private void applyShortcutPressureBasis(ShortcutDistillationColumn shortcut, StreamInterface feedStream) { - double feedPressure = feedStream.getFluid() == null ? Double.NaN : feedStream.getPressure("bara"); + private void applyShortcutPressureBasis(ShortcutDistillationColumn shortcut, + StreamInterface feedStream) { + double feedPressure = + feedStream.getFluid() == null ? Double.NaN : feedStream.getPressure("bara"); double condenserPressure = isPositiveFinite(topTrayPressure) ? topTrayPressure : feedPressure; - double reboilerPressure = isPositiveFinite(bottomTrayPressure) ? bottomTrayPressure : condenserPressure; + double reboilerPressure = + isPositiveFinite(bottomTrayPressure) ? bottomTrayPressure : condenserPressure; if (isPositiveFinite(condenserPressure)) { shortcut.setCondenserPressure(condenserPressure); if (!isPositiveFinite(topTrayPressure)) { - setTopPressure(condenserPressure); + setTopPressure(condenserPressure); } } if (isPositiveFinite(reboilerPressure)) { shortcut.setReboilerPressure(reboilerPressure); if (!isPositiveFinite(bottomTrayPressure)) { - setBottomPressure(reboilerPressure); + setBottomPressure(reboilerPressure); } } } @@ -4133,7 +4233,8 @@ private int convertShortcutFeedTrayFromTop(int feedTrayFromTop, int totalStageCo * @param state column optimization state to update before rebuilding trays * @param shortcut solved shortcut column */ - private void applyShortcutEndpointDuties(ColumnOptimizationState state, ShortcutDistillationColumn shortcut) { + private void applyShortcutEndpointDuties(ColumnOptimizationState state, + ShortcutDistillationColumn shortcut) { if (hasCondenser) { state.condenserRefluxSet = true; state.condenserRefluxRatio = Math.max(0.0, shortcut.getActualRefluxRatio()); @@ -4157,7 +4258,7 @@ private ColumnOptimizationState captureColumnOptimizationState() { state.reboilerRefluxRatio = reboiler.getRefluxRatio(); state.reboilerHasSetTemperature = reboiler.isSetOutTemperature(); if (state.reboilerHasSetTemperature) { - state.reboilerTemperature = reboiler.getOutTemperature(); + state.reboilerTemperature = reboiler.getOutTemperature(); } state.reboilerHeatInput = reboiler.heatInput; } @@ -4167,7 +4268,7 @@ private ColumnOptimizationState captureColumnOptimizationState() { state.condenserRefluxRatio = condenser.getRefluxRatio(); state.condenserHasSetTemperature = condenser.isSetOutTemperature(); if (state.condenserHasSetTemperature) { - state.condenserTemperature = condenser.getOutTemperature(); + state.condenserTemperature = condenser.getOutTemperature(); } state.condenserHeatInput = condenser.heatInput; state.totalCondenser = condenser.totalCondenser; @@ -4211,9 +4312,10 @@ private int getMinimumOptimizationTrayCount() { * @param optimizationStartNanos value from {@link System#nanoTime()} at search start * @return {@code true} when candidate-count or elapsed-time budget has been reached */ - private boolean isTrayOptimizationSearchBudgetExceeded(int evaluatedCases, long optimizationStartNanos) { - return evaluatedCases >= maxTrayOptimizationCandidates - || getTrayOptimizationElapsedSeconds(optimizationStartNanos) >= maxTrayOptimizationTimeSeconds; + private boolean isTrayOptimizationSearchBudgetExceeded(int evaluatedCases, + long optimizationStartNanos) { + return evaluatedCases >= maxTrayOptimizationCandidates || getTrayOptimizationElapsedSeconds( + optimizationStartNanos) >= maxTrayOptimizationTimeSeconds; } /** @@ -4233,22 +4335,24 @@ private double getTrayOptimizationElapsedSeconds(long optimizationStartNanos) { * @param optimizationStartNanos value from {@link System#nanoTime()} at search start * @return diagnostic message explaining the active budget limits */ - private String createTrayOptimizationBudgetMessage(int evaluatedCases, long optimizationStartNanos) { - double elapsedSeconds = Math.round(getTrayOptimizationElapsedSeconds(optimizationStartNanos) * 10.0) / 10.0; - return "Tray optimization stopped after evaluating " + evaluatedCases + " candidate cases in " + elapsedSeconds - + " s due to the configured search budget. Increase " - + "max tray optimization candidates or time for larger studies."; + private String createTrayOptimizationBudgetMessage(int evaluatedCases, + long optimizationStartNanos) { + double elapsedSeconds = + Math.round(getTrayOptimizationElapsedSeconds(optimizationStartNanos) * 10.0) / 10.0; + return "Tray optimization stopped after evaluating " + evaluatedCases + " candidate cases in " + + elapsedSeconds + " s due to the configured search budget. Increase " + + "max tray optimization candidates or time for larger studies."; } /** - * Run the current tray optimization candidate, retrying with damped substitution when the configured solver leaves - * the candidate unconverged. + * Run the current tray optimization candidate, retrying with damped substitution when the + * configured solver leaves the candidate unconverged. * *

- * Tray optimization is a configuration search. A candidate should not be discarded solely because the default - * direct-substitution path stalls from a cold start when the more robust damped substitution solver can solve the - * same thermodynamic and hydraulic setup. The retry is scoped to the current candidate and restores the caller's - * configured solver type before returning. + * Tray optimization is a configuration search. A candidate should not be discarded solely because + * the default direct-substitution path stalls from a cold start when the more robust damped + * substitution solver can solve the same thermodynamic and hydraulic setup. The retry is scoped + * to the current candidate and restores the caller's configured solver type before returning. *

* * @return {@code true} if the candidate is solved after the configured solver or damped fallback @@ -4257,7 +4361,8 @@ private boolean runTrayOptimizationCandidateWithFallback() { try { run(); } catch (Exception exception) { - logger.debug("Tray optimization candidate failed with configured solver {}.", solverType, exception); + logger.debug("Tray optimization candidate failed with configured solver {}.", solverType, + exception); return false; } @@ -4272,7 +4377,8 @@ private boolean runTrayOptimizationCandidateWithFallback() { run(UUID.randomUUID()); return solved(); } catch (Exception exception) { - logger.debug("Tray optimization damped fallback failed for solver {}.", configuredSolverType, exception); + logger.debug("Tray optimization damped fallback failed for solver {}.", configuredSolverType, + exception); return false; } finally { solverType = configuredSolverType; @@ -4291,20 +4397,21 @@ private boolean runTrayOptimizationCandidateWithFallback() { * @param state captured column settings to apply during rebuild * @return candidate result, feasible only when the column converged and met the purity spec */ - private TrayOptimizationResult evaluateTrayOptimizationCandidate(int totalTrayCount, int feedTray, double productSpec, - String componentName, boolean isTopProduct, List optimizationFeeds, - ColumnOptimizationState state) { + private TrayOptimizationResult evaluateTrayOptimizationCandidate(int totalTrayCount, int feedTray, + double productSpec, String componentName, boolean isTopProduct, + List optimizationFeeds, ColumnOptimizationState state) { rebuildColumnForOptimization(totalTrayCount, state); addOptimizationFeedsToTray(optimizationFeeds, feedTray); if (!runTrayOptimizationCandidateWithFallback()) { - return createTrayOptimizationResult(false, totalTrayCount, feedTray, productSpec, componentName, isTopProduct, - Double.NaN, 0, 0, "Candidate did not converge."); + return createTrayOptimizationResult(false, totalTrayCount, feedTray, productSpec, + componentName, isTopProduct, Double.NaN, 0, 0, "Candidate did not converge."); } double productPurity = getProductComponentMoleFraction(componentName, isTopProduct); boolean feasible = productPurity >= productSpec; - return createTrayOptimizationResult(feasible, totalTrayCount, feedTray, productSpec, componentName, isTopProduct, - productPurity, 0, 0, feasible ? "Candidate met product specification." : "Candidate purity below target."); + return createTrayOptimizationResult(feasible, totalTrayCount, feedTray, productSpec, + componentName, isTopProduct, productPurity, 0, 0, + feasible ? "Candidate met product specification." : "Candidate purity below target."); } /** @@ -4326,24 +4433,28 @@ private TrayOptimizationResult evaluateTrayOptimizationCandidate(int totalTrayCo * @param trayEfficiency overall tray efficiency used for actual tray count and column height * @return economic candidate result, feasible only when converged and meeting the purity spec */ - private EconomicTrayOptimizationResult evaluateEconomicTrayOptimizationCandidate(int totalTrayCount, int feedTray, - double productSpec, String componentName, boolean isTopProduct, List optimizationFeeds, - ColumnOptimizationState baseState, double condenserRefluxRatio, double reboilerRatio, double capitalChargeFactor, - double operatingHoursPerYear, double steamCostPerTonne, double coolingWaterCostPerM3, double trayEfficiency) { + private EconomicTrayOptimizationResult evaluateEconomicTrayOptimizationCandidate( + int totalTrayCount, int feedTray, double productSpec, String componentName, + boolean isTopProduct, List optimizationFeeds, + ColumnOptimizationState baseState, double condenserRefluxRatio, double reboilerRatio, + double capitalChargeFactor, double operatingHoursPerYear, double steamCostPerTonne, + double coolingWaterCostPerM3, double trayEfficiency) { ColumnOptimizationState candidateState = baseState.copy(); applyEconomicRatioOverrides(candidateState, condenserRefluxRatio, reboilerRatio); - TrayOptimizationResult trayResult = evaluateTrayOptimizationCandidate(totalTrayCount, feedTray, productSpec, - componentName, isTopProduct, optimizationFeeds, candidateState); + TrayOptimizationResult trayResult = evaluateTrayOptimizationCandidate(totalTrayCount, feedTray, + productSpec, componentName, isTopProduct, optimizationFeeds, candidateState); if (!trayResult.isFeasible()) { - return createEconomicTrayOptimizationResult(trayResult, createEmptyEconomicTrayOptimizationMetrics(), - capitalChargeFactor, operatingHoursPerYear, steamCostPerTonne, coolingWaterCostPerM3, trayEfficiency, - getSelectedCondenserRatio(candidateState), getSelectedReboilerRatio(candidateState)); + return createEconomicTrayOptimizationResult(trayResult, + createEmptyEconomicTrayOptimizationMetrics(), capitalChargeFactor, operatingHoursPerYear, + steamCostPerTonne, coolingWaterCostPerM3, trayEfficiency, + getSelectedCondenserRatio(candidateState), getSelectedReboilerRatio(candidateState)); } - EconomicTrayOptimizationMetrics metrics = calculateEconomicTrayOptimizationMetrics(capitalChargeFactor, - operatingHoursPerYear, steamCostPerTonne, coolingWaterCostPerM3, trayEfficiency); - return createEconomicTrayOptimizationResult(trayResult, metrics, capitalChargeFactor, operatingHoursPerYear, - steamCostPerTonne, coolingWaterCostPerM3, trayEfficiency, getSelectedCondenserRatio(candidateState), - getSelectedReboilerRatio(candidateState)); + EconomicTrayOptimizationMetrics metrics = + calculateEconomicTrayOptimizationMetrics(capitalChargeFactor, operatingHoursPerYear, + steamCostPerTonne, coolingWaterCostPerM3, trayEfficiency); + return createEconomicTrayOptimizationResult(trayResult, metrics, capitalChargeFactor, + operatingHoursPerYear, steamCostPerTonne, coolingWaterCostPerM3, trayEfficiency, + getSelectedCondenserRatio(candidateState), getSelectedReboilerRatio(candidateState)); } /** @@ -4367,29 +4478,32 @@ steamCostPerTonne, coolingWaterCostPerM3, trayEfficiency, getSelectedCondenserRa */ private EconomicTrayOptimizationResult applyEconomicTrayOptimizationResult( EconomicTrayOptimizationResult selectedResult, List optimizationFeeds, - ColumnOptimizationState state, double productSpec, String componentName, boolean isTopProduct, int evaluatedCases, - int convergedCases, double capitalChargeFactor, double operatingHoursPerYear, double steamCostPerTonne, - double coolingWaterCostPerM3, double trayEfficiency, String message) { + ColumnOptimizationState state, double productSpec, String componentName, boolean isTopProduct, + int evaluatedCases, int convergedCases, double capitalChargeFactor, + double operatingHoursPerYear, double steamCostPerTonne, double coolingWaterCostPerM3, + double trayEfficiency, String message) { ColumnOptimizationState selectedState = state.copy(); applyEconomicRatioOverrides(selectedState, selectedResult.getCondenserRefluxRatio(), - selectedResult.getReboilerRatio()); + selectedResult.getReboilerRatio()); rebuildColumnForOptimization(selectedResult.getNumberOfTrays(), selectedState); addOptimizationFeedsToTray(optimizationFeeds, selectedResult.getFeedTrayNumber()); if (!runTrayOptimizationCandidateWithFallback()) { - return createInfeasibleEconomicTrayOptimizationResult(productSpec, componentName, isTopProduct, evaluatedCases, - convergedCases, capitalChargeFactor, operatingHoursPerYear, steamCostPerTonne, coolingWaterCostPerM3, - trayEfficiency, "Selected economic candidate did not converge when reapplied."); + return createInfeasibleEconomicTrayOptimizationResult(productSpec, componentName, + isTopProduct, evaluatedCases, convergedCases, capitalChargeFactor, operatingHoursPerYear, + steamCostPerTonne, coolingWaterCostPerM3, trayEfficiency, + "Selected economic candidate did not converge when reapplied."); } double productPurity = getProductComponentMoleFraction(componentName, isTopProduct); TrayOptimizationResult trayResult = createTrayOptimizationResult(productPurity >= productSpec, - selectedResult.getNumberOfTrays(), selectedResult.getFeedTrayNumber(), productSpec, componentName, isTopProduct, - productPurity, evaluatedCases, convergedCases, message); - EconomicTrayOptimizationMetrics metrics = calculateEconomicTrayOptimizationMetrics(capitalChargeFactor, - operatingHoursPerYear, steamCostPerTonne, coolingWaterCostPerM3, trayEfficiency); - return createEconomicTrayOptimizationResult(trayResult, metrics, capitalChargeFactor, operatingHoursPerYear, - steamCostPerTonne, coolingWaterCostPerM3, trayEfficiency, selectedResult.getCondenserRefluxRatio(), - selectedResult.getReboilerRatio()); + selectedResult.getNumberOfTrays(), selectedResult.getFeedTrayNumber(), productSpec, + componentName, isTopProduct, productPurity, evaluatedCases, convergedCases, message); + EconomicTrayOptimizationMetrics metrics = + calculateEconomicTrayOptimizationMetrics(capitalChargeFactor, operatingHoursPerYear, + steamCostPerTonne, coolingWaterCostPerM3, trayEfficiency); + return createEconomicTrayOptimizationResult(trayResult, metrics, capitalChargeFactor, + operatingHoursPerYear, steamCostPerTonne, coolingWaterCostPerM3, trayEfficiency, + selectedResult.getCondenserRefluxRatio(), selectedResult.getReboilerRatio()); } /** @@ -4399,8 +4513,8 @@ private EconomicTrayOptimizationResult applyEconomicTrayOptimizationResult( * @param condenserRefluxRatio condenser reflux-ratio candidate, or {@link Double#NaN} * @param reboilerRatio reboiler boilup/reflux-ratio candidate, or {@link Double#NaN} */ - private void applyEconomicRatioOverrides(ColumnOptimizationState state, double condenserRefluxRatio, - double reboilerRatio) { + private void applyEconomicRatioOverrides(ColumnOptimizationState state, + double condenserRefluxRatio, double reboilerRatio) { if (hasCondenser && isPositiveFinite(condenserRefluxRatio)) { state.condenserRefluxSet = true; state.condenserRefluxRatio = condenserRefluxRatio; @@ -4419,18 +4533,18 @@ private void applyEconomicRatioOverrides(ColumnOptimizationState state, double c */ private double[] getEconomicRatioCandidates(double[] ratios) { if (ratios == null || ratios.length == 0) { - return new double[] { Double.NaN }; + return new double[] {Double.NaN}; } double[] sanitized = new double[ratios.length]; int count = 0; for (int ratioIndex = 0; ratioIndex < ratios.length; ratioIndex++) { if (isPositiveFinite(ratios[ratioIndex])) { - sanitized[count] = ratios[ratioIndex]; - count++; + sanitized[count] = ratios[ratioIndex]; + count++; } } if (count == 0) { - return new double[] { Double.NaN }; + return new double[] {Double.NaN}; } double[] result = new double[count]; System.arraycopy(sanitized, 0, result, 0, count); @@ -4457,8 +4571,9 @@ private boolean isPositiveFinite(double value) { * @param trayEfficiency overall tray efficiency used for actual tray count and column height * @return populated economic metrics for the current column state */ - private EconomicTrayOptimizationMetrics calculateEconomicTrayOptimizationMetrics(double capitalChargeFactor, - double operatingHoursPerYear, double steamCostPerTonne, double coolingWaterCostPerM3, double trayEfficiency) { + private EconomicTrayOptimizationMetrics calculateEconomicTrayOptimizationMetrics( + double capitalChargeFactor, double operatingHoursPerYear, double steamCostPerTonne, + double coolingWaterCostPerM3, double trayEfficiency) { EconomicTrayOptimizationMetrics metrics = new EconomicTrayOptimizationMetrics(); DistillationColumnMechanicalDesign design = new DistillationColumnMechanicalDesign(this); design.setTrayEfficiency(trayEfficiency); @@ -4486,8 +4601,8 @@ private EconomicTrayOptimizationMetrics calculateEconomicTrayOptimizationMetrics if (!isPositiveFinite(metrics.capitalCost)) { metrics.capitalCost = design.calculateTotalSystemCost(); } - metrics.annualUtilityCost = costEstimate.calcAnnualUtilityCost(operatingHoursPerYear, steamCostPerTonne, - coolingWaterCostPerM3); + metrics.annualUtilityCost = costEstimate.calcAnnualUtilityCost(operatingHoursPerYear, + steamCostPerTonne, coolingWaterCostPerM3); metrics.annualizedCapitalCost = metrics.capitalCost * capitalChargeFactor; metrics.totalAnnualizedCost = metrics.annualizedCapitalCost + metrics.annualUtilityCost; metrics.actualTrays = design.getActualTrays(); @@ -4527,14 +4642,16 @@ private EconomicTrayOptimizationMetrics createEmptyEconomicTrayOptimizationMetri * @param reboilerRatio selected reboiler boilup/reflux ratio, or {@link Double#NaN} * @return economic optimization result */ - private EconomicTrayOptimizationResult createEconomicTrayOptimizationResult(TrayOptimizationResult trayResult, - EconomicTrayOptimizationMetrics metrics, double capitalChargeFactor, double operatingHoursPerYear, - double steamCostPerTonne, double coolingWaterCostPerM3, double trayEfficiency, double condenserRefluxRatio, + private EconomicTrayOptimizationResult createEconomicTrayOptimizationResult( + TrayOptimizationResult trayResult, EconomicTrayOptimizationMetrics metrics, + double capitalChargeFactor, double operatingHoursPerYear, double steamCostPerTonne, + double coolingWaterCostPerM3, double trayEfficiency, double condenserRefluxRatio, double reboilerRatio) { - return new EconomicTrayOptimizationResult(trayResult, metrics.capitalCost, metrics.annualUtilityCost, - metrics.annualizedCapitalCost, metrics.totalAnnualizedCost, capitalChargeFactor, operatingHoursPerYear, - steamCostPerTonne, coolingWaterCostPerM3, trayEfficiency, metrics.actualTrays, metrics.columnDiameter, - metrics.columnHeight, condenserRefluxRatio, reboilerRatio); + return new EconomicTrayOptimizationResult(trayResult, metrics.capitalCost, + metrics.annualUtilityCost, metrics.annualizedCapitalCost, metrics.totalAnnualizedCost, + capitalChargeFactor, operatingHoursPerYear, steamCostPerTonne, coolingWaterCostPerM3, + trayEfficiency, metrics.actualTrays, metrics.columnDiameter, metrics.columnHeight, + condenserRefluxRatio, reboilerRatio); } /** @@ -4553,15 +4670,16 @@ private EconomicTrayOptimizationResult createEconomicTrayOptimizationResult(Tray * @param message diagnostic message * @return infeasible economic optimization result */ - private EconomicTrayOptimizationResult createInfeasibleEconomicTrayOptimizationResult(double productSpec, - String componentName, boolean isTopProduct, int evaluatedCases, int convergedCases, double capitalChargeFactor, - double operatingHoursPerYear, double steamCostPerTonne, double coolingWaterCostPerM3, double trayEfficiency, + private EconomicTrayOptimizationResult createInfeasibleEconomicTrayOptimizationResult( + double productSpec, String componentName, boolean isTopProduct, int evaluatedCases, + int convergedCases, double capitalChargeFactor, double operatingHoursPerYear, + double steamCostPerTonne, double coolingWaterCostPerM3, double trayEfficiency, String message) { - TrayOptimizationResult trayResult = createInfeasibleTrayOptimizationResult(productSpec, componentName, isTopProduct, - evaluatedCases, convergedCases, message); - return createEconomicTrayOptimizationResult(trayResult, createEmptyEconomicTrayOptimizationMetrics(), - capitalChargeFactor, operatingHoursPerYear, steamCostPerTonne, coolingWaterCostPerM3, trayEfficiency, - Double.NaN, Double.NaN); + TrayOptimizationResult trayResult = createInfeasibleTrayOptimizationResult(productSpec, + componentName, isTopProduct, evaluatedCases, convergedCases, message); + return createEconomicTrayOptimizationResult(trayResult, + createEmptyEconomicTrayOptimizationMetrics(), capitalChargeFactor, operatingHoursPerYear, + steamCostPerTonne, coolingWaterCostPerM3, trayEfficiency, Double.NaN, Double.NaN); } /** @@ -4571,12 +4689,13 @@ private EconomicTrayOptimizationResult createInfeasibleEconomicTrayOptimizationR * @param currentBest current best result, or {@code null} * @return {@code true} if the candidate has a lower annualized cost or better tie-breaker */ - private boolean isBetterEconomicTrayOptimizationCandidate(EconomicTrayOptimizationResult candidate, - EconomicTrayOptimizationResult currentBest) { + private boolean isBetterEconomicTrayOptimizationCandidate( + EconomicTrayOptimizationResult candidate, EconomicTrayOptimizationResult currentBest) { if (currentBest == null) { return true; } - double costDifference = candidate.getTotalAnnualizedCost() - currentBest.getTotalAnnualizedCost(); + double costDifference = + candidate.getTotalAnnualizedCost() - currentBest.getTotalAnnualizedCost(); if (Math.abs(costDifference) > 1.0e-6) { return costDifference < 0.0; } @@ -4651,19 +4770,20 @@ private double getSelectedReboilerRatio(ColumnOptimizationState state) { * @return final optimization result from the applied selected candidate */ private TrayOptimizationResult applyTrayOptimizationResult(TrayOptimizationResult selectedResult, - List optimizationFeeds, ColumnOptimizationState state, double productSpec, String componentName, - boolean isTopProduct, int evaluatedCases, int convergedCases, String message) { + List optimizationFeeds, ColumnOptimizationState state, double productSpec, + String componentName, boolean isTopProduct, int evaluatedCases, int convergedCases, + String message) { rebuildColumnForOptimization(selectedResult.getNumberOfTrays(), state); addOptimizationFeedsToTray(optimizationFeeds, selectedResult.getFeedTrayNumber()); if (!runTrayOptimizationCandidateWithFallback()) { - return createInfeasibleTrayOptimizationResult(productSpec, componentName, isTopProduct, evaluatedCases, - convergedCases, "Selected candidate did not converge when reapplied."); + return createInfeasibleTrayOptimizationResult(productSpec, componentName, isTopProduct, + evaluatedCases, convergedCases, "Selected candidate did not converge when reapplied."); } double productPurity = getProductComponentMoleFraction(componentName, isTopProduct); - return createTrayOptimizationResult(productPurity >= productSpec, selectedResult.getNumberOfTrays(), - selectedResult.getFeedTrayNumber(), productSpec, componentName, isTopProduct, productPurity, evaluatedCases, - convergedCases, message); + return createTrayOptimizationResult(productPurity >= productSpec, + selectedResult.getNumberOfTrays(), selectedResult.getFeedTrayNumber(), productSpec, + componentName, isTopProduct, productPurity, evaluatedCases, convergedCases, message); } /** @@ -4693,10 +4813,10 @@ private void rebuildColumnForOptimization(int totalTrayCount, ColumnOptimization reboiler.setMultiPhaseCheck(doMultiPhaseCheck); reboiler.setHeatInput(state.reboilerHeatInput); if (state.reboilerRefluxSet) { - reboiler.setRefluxRatio(state.reboilerRefluxRatio); + reboiler.setRefluxRatio(state.reboilerRefluxRatio); } if (state.reboilerHasSetTemperature) { - reboiler.setOutTemperature(state.reboilerTemperature); + reboiler.setOutTemperature(state.reboilerTemperature); } trays.add(reboiler); } @@ -4714,10 +4834,10 @@ private void rebuildColumnForOptimization(int totalTrayCount, ColumnOptimization condenser.setHeatInput(state.condenserHeatInput); condenser.setTotalCondenser(state.totalCondenser); if (state.condenserRefluxSet) { - condenser.setRefluxRatio(state.condenserRefluxRatio); + condenser.setRefluxRatio(state.condenserRefluxRatio); } if (state.condenserHasSetTemperature) { - condenser.setOutTemperature(state.condenserTemperature); + condenser.setOutTemperature(state.condenserTemperature); } trays.add(condenser); } @@ -4766,10 +4886,12 @@ private void applyOptimizationPressureProfile() { * @param state captured column settings to use for endpoint temperatures */ private void applyOptimizationTemperatureProfile(ColumnOptimizationState state) { - if (!state.reboilerHasSetTemperature || !state.condenserHasSetTemperature || numberOfTrays <= 1) { + if (!state.reboilerHasSetTemperature || !state.condenserHasSetTemperature + || numberOfTrays <= 1) { return; } - double temperatureStep = (state.condenserTemperature - state.reboilerTemperature) / (numberOfTrays - 1.0); + double temperatureStep = + (state.condenserTemperature - state.reboilerTemperature) / (numberOfTrays - 1.0); for (int trayIndex = 0; trayIndex < numberOfTrays; trayIndex++) { trays.get(trayIndex).setTemperature(state.reboilerTemperature + trayIndex * temperatureStep); } @@ -4804,15 +4926,16 @@ private double getProductComponentMoleFraction(String componentName, boolean isT * @param message diagnostic message * @return optimization result populated from current duties and residuals */ - private TrayOptimizationResult createTrayOptimizationResult(boolean feasible, int totalTrayCount, int feedTray, - double productSpec, String componentName, boolean isTopProduct, double productPurity, int evaluatedCases, - int convergedCases, String message) { + private TrayOptimizationResult createTrayOptimizationResult(boolean feasible, int totalTrayCount, + int feedTray, double productSpec, String componentName, boolean isTopProduct, + double productPurity, int evaluatedCases, int convergedCases, String message) { double reboilerDuty = hasReboiler ? getReboiler().getDuty() : 0.0; double condenserDuty = hasCondenser ? getCondenser().getDuty() : 0.0; double totalAbsoluteDuty = Math.abs(reboilerDuty) + Math.abs(condenserDuty); - return new TrayOptimizationResult(feasible, feasible ? totalTrayCount : -1, feasible ? feedTray : -1, componentName, - isTopProduct, productSpec, productPurity, reboilerDuty, condenserDuty, totalAbsoluteDuty, lastIterationCount, - lastTemperatureResidual, lastMassResidual, lastEnergyResidual, evaluatedCases, convergedCases, message); + return new TrayOptimizationResult(feasible, feasible ? totalTrayCount : -1, + feasible ? feedTray : -1, componentName, isTopProduct, productSpec, productPurity, + reboilerDuty, condenserDuty, totalAbsoluteDuty, lastIterationCount, lastTemperatureResidual, + lastMassResidual, lastEnergyResidual, evaluatedCases, convergedCases, message); } /** @@ -4826,11 +4949,12 @@ private TrayOptimizationResult createTrayOptimizationResult(boolean feasible, in * @param message diagnostic message * @return infeasible optimization result */ - private TrayOptimizationResult createInfeasibleTrayOptimizationResult(double productSpec, String componentName, - boolean isTopProduct, int evaluatedCases, int convergedCases, String message) { - return new TrayOptimizationResult(false, -1, -1, componentName, isTopProduct, productSpec, Double.NaN, 0.0, 0.0, - 0.0, lastIterationCount, lastTemperatureResidual, lastMassResidual, lastEnergyResidual, evaluatedCases, - convergedCases, message); + private TrayOptimizationResult createInfeasibleTrayOptimizationResult(double productSpec, + String componentName, boolean isTopProduct, int evaluatedCases, int convergedCases, + String message) { + return new TrayOptimizationResult(false, -1, -1, componentName, isTopProduct, productSpec, + Double.NaN, 0.0, 0.0, 0.0, lastIterationCount, lastTemperatureResidual, lastMassResidual, + lastEnergyResidual, evaluatedCases, convergedCases, message); } /** @@ -4895,7 +5019,8 @@ private void solveSequential(UUID id, double initialRelaxation) { StreamInterface[] currentGasStreams = new StreamInterface[numberOfTrays]; StreamInterface[] currentLiquidStreams = new StreamInterface[numberOfTrays]; - double relaxation = Math.max(minSequentialRelaxation, Math.min(maxAdaptiveRelaxation, initialRelaxation)); + double relaxation = + Math.max(minSequentialRelaxation, Math.min(maxAdaptiveRelaxation, initialRelaxation)); // Run the feed tray to establish initial conditions. // On re-runs this is skipped because the tray already holds a valid state @@ -4909,10 +5034,11 @@ private void solveSequential(UUID id, double initialRelaxation) { double totalFeedFlow = 0.0; for (List feeds : feedStreams.values()) { for (StreamInterface f : feeds) { - totalFeedFlow += Math.abs(f.getFlowRate("kg/hr")); + totalFeedFlow += Math.abs(f.getFlowRate("kg/hr")); } } - double divergenceThreshold = Math.max(totalFeedFlow * MAX_RELAXED_INTERNAL_TRAFFIC_TO_FEED_RATIO, 1.0e3); + double divergenceThreshold = + Math.max(totalFeedFlow * MAX_RELAXED_INTERNAL_TRAFFIC_TO_FEED_RATIO, 1.0e3); boolean divergenceRecoveryApplied = false; // Snapshot tray state before iterations as a safe recovery point. @@ -4927,19 +5053,19 @@ private void solveSequential(UUID id, double initialRelaxation) { // relaxation-based damping is active from the very first iteration. if (hasBeenSolvedBefore) { for (int i = 0; i < numberOfTrays; i++) { - previousGasStreams[i] = snapshotGasStreams[i].clone(); - previousLiquidStreams[i] = snapshotLiquidStreams[i].clone(); + previousGasStreams[i] = snapshotGasStreams[i].clone(); + previousLiquidStreams[i] = snapshotLiquidStreams[i].clone(); } } int baseIterationLimit = computeIterationLimit(); int iterationLimit = baseIterationLimit; int polishIterationLimit = baseIterationLimit - + Math.max(POLISH_ITERATION_MARGIN, (int) Math.ceil(0.5 * numberOfTrays)); + + Math.max(POLISH_ITERATION_MARGIN, (int) Math.ceil(0.5 * numberOfTrays)); int overflowIncrement = Math.max(3, (int) Math.ceil(0.5 * numberOfTrays)); int overflowBand = Math.max(overflowIncrement, numberOfTrays); int maxIterationLimit = Math.max(iterationLimit, maxNumberOfIterations) - + overflowBand * ITERATION_OVERFLOW_MULTIPLIER; + + overflowBand * ITERATION_OVERFLOW_MULTIPLIER; double baseTempTolerance = getEffectiveTemperatureTolerance(); double baseMassTolerance = getEffectiveMassBalanceTolerance(); double baseEnergyTolerance = getEffectiveEnthalpyBalanceTolerance(); @@ -4954,70 +5080,70 @@ private void solveSequential(UUID id, double initialRelaxation) { iter++; for (int i = 0; i < numberOfTrays; i++) { - oldtemps[i] = trays.get(i).getThermoSystem().getTemperature(); + oldtemps[i] = trays.get(i).getThermoSystem().getTemperature(); } for (int i = firstFeedTrayNumber; i > 1; i--) { - int replaceStream = trays.get(i - 1).getNumberOfInputStreams() - 1; - StreamInterface relaxedLiquid = applyRelaxationFast(previousLiquidStreams[i], trays.get(i).getLiquidOutStream(), - relaxation); - trays.get(i - 1).replaceStream(replaceStream, relaxedLiquid); - currentLiquidStreams[i] = relaxedLiquid; - trays.get(i - 1).run(id); - applyMurphreeCorrection(i - 1); + int replaceStream = trays.get(i - 1).getNumberOfInputStreams() - 1; + StreamInterface relaxedLiquid = applyRelaxationFast(previousLiquidStreams[i], + trays.get(i).getLiquidOutStream(), relaxation); + trays.get(i - 1).replaceStream(replaceStream, relaxedLiquid); + currentLiquidStreams[i] = relaxedLiquid; + trays.get(i - 1).run(id); + applyMurphreeCorrection(i - 1); } int streamNumb = trays.get(0).getNumberOfInputStreams() - 1; - StreamInterface reboilerFeed = applyRelaxationFast(previousLiquidStreams[1], trays.get(1).getLiquidOutStream(), - relaxation); + StreamInterface reboilerFeed = applyRelaxationFast(previousLiquidStreams[1], + trays.get(1).getLiquidOutStream(), relaxation); trays.get(0).replaceStream(streamNumb, reboilerFeed); currentLiquidStreams[1] = reboilerFeed; trays.get(0).run(id); applyMurphreeCorrection(0); for (int i = 1; i <= numberOfTrays - 1; i++) { - int replaceStream = trays.get(i).getNumberOfInputStreams() - 2; - if (i == (numberOfTrays - 1)) { - replaceStream = trays.get(i).getNumberOfInputStreams() - 1; - } - StreamInterface relaxedGas = applyRelaxationFast(previousGasStreams[i - 1], trays.get(i - 1).getGasOutStream(), - relaxation); - trays.get(i).replaceStream(replaceStream, relaxedGas); - currentGasStreams[i - 1] = relaxedGas; - trays.get(i).run(id); - applyMurphreeCorrection(i); + int replaceStream = trays.get(i).getNumberOfInputStreams() - 2; + if (i == (numberOfTrays - 1)) { + replaceStream = trays.get(i).getNumberOfInputStreams() - 1; + } + StreamInterface relaxedGas = applyRelaxationFast(previousGasStreams[i - 1], + trays.get(i - 1).getGasOutStream(), relaxation); + trays.get(i).replaceStream(replaceStream, relaxedGas); + currentGasStreams[i - 1] = relaxedGas; + trays.get(i).run(id); + applyMurphreeCorrection(i); } for (int i = numberOfTrays - 2; i >= firstFeedTrayNumber; i--) { - int replaceStream = trays.get(i).getNumberOfInputStreams() - 1; - StreamInterface relaxedLiquid = applyRelaxationFast(previousLiquidStreams[i + 1], - trays.get(i + 1).getLiquidOutStream(), relaxation); - trays.get(i).replaceStream(replaceStream, relaxedLiquid); - currentLiquidStreams[i + 1] = relaxedLiquid; - trays.get(i).run(id); - applyMurphreeCorrection(i); + int replaceStream = trays.get(i).getNumberOfInputStreams() - 1; + StreamInterface relaxedLiquid = applyRelaxationFast(previousLiquidStreams[i + 1], + trays.get(i + 1).getLiquidOutStream(), relaxation); + trays.get(i).replaceStream(replaceStream, relaxedLiquid); + currentLiquidStreams[i + 1] = relaxedLiquid; + trays.get(i).run(id); + applyMurphreeCorrection(i); } double temperatureResidual = 0.0; double effectiveRelaxation = Math.max(minTemperatureRelaxation, Math.min(1.0, relaxation)); for (int i = 0; i < numberOfTrays; i++) { - double updated = trays.get(i).getThermoSystem().getTemperature(); - if (Double.isNaN(updated) || Double.isInfinite(updated)) { - updated = oldtemps[i]; - } - double newTemp = oldtemps[i] + effectiveRelaxation * (updated - oldtemps[i]); - trays.get(i).setTemperature(newTemp); - temperatureResidual += Math.abs(newTemp - oldtemps[i]); + double updated = trays.get(i).getThermoSystem().getTemperature(); + if (Double.isNaN(updated) || Double.isInfinite(updated)) { + updated = oldtemps[i]; + } + double newTemp = oldtemps[i] + effectiveRelaxation * (updated - oldtemps[i]); + trays.get(i).setTemperature(newTemp); + temperatureResidual += Math.abs(newTemp - oldtemps[i]); } temperatureResidual /= Math.max(1, numberOfTrays); err = temperatureResidual; - boolean evaluateBalances = shouldEvaluateBalances(iter, iterationLimit, polishing, err, baseTempTolerance, - balanceCheckStride); + boolean evaluateBalances = shouldEvaluateBalances(iter, iterationLimit, polishing, err, + baseTempTolerance, balanceCheckStride); if (evaluateBalances || !massEnergyEvaluated) { - massErr = getMassBalanceError(); - energyErr = getEnergyBalanceError(); - massEnergyEvaluated = true; + massErr = getMassBalanceError(); + energyErr = getEnergyBalanceError(); + massEnergyEvaluated = true; } double tempScaled = err / baseTempTolerance; @@ -5025,13 +5151,14 @@ private void solveSequential(UUID id, double initialRelaxation) { double energyScaled = energyErr / baseEnergyTolerance; double combinedResidual = Math.max(tempScaled, massScaled); if (Double.isFinite(energyScaled)) { - combinedResidual = Math.max(combinedResidual, Math.min(energyScaled, maxEnergyRelaxationWeight)); + combinedResidual = + Math.max(combinedResidual, Math.min(energyScaled, maxEnergyRelaxationWeight)); } if (combinedResidual > previousCombinedResidual * 1.05) { - relaxation = Math.max(minSequentialRelaxation, relaxation * relaxationDecreaseFactor); + relaxation = Math.max(minSequentialRelaxation, relaxation * relaxationDecreaseFactor); } else if (combinedResidual < previousCombinedResidual * 0.98) { - relaxation = Math.min(maxAdaptiveRelaxation, relaxation * relaxationIncreaseFactor); + relaxation = Math.min(maxAdaptiveRelaxation, relaxation * relaxationIncreaseFactor); } previousCombinedResidual = combinedResidual; @@ -5042,98 +5169,105 @@ private void solveSequential(UUID id, double initialRelaxation) { // so that subsequent iterations are heavily damped. This is a one-shot // recovery that does not fire when the column is already converging. if (!divergenceRecoveryApplied && iter <= 10) { - double maxTrayFlow = 0.0; - for (int i = 0; i < numberOfTrays; i++) { - maxTrayFlow = Math.max(maxTrayFlow, Math.abs(trays.get(i).getGasOutStream().getFlowRate("kg/hr"))); - maxTrayFlow = Math.max(maxTrayFlow, Math.abs(trays.get(i).getLiquidOutStream().getFlowRate("kg/hr"))); - } - if (maxTrayFlow > divergenceThreshold) { - relaxation = minSequentialRelaxation; - for (int i = 0; i < numberOfTrays; i++) { - previousGasStreams[i] = snapshotGasStreams[i].clone(); - previousLiquidStreams[i] = snapshotLiquidStreams[i].clone(); - } - divergenceRecoveryApplied = true; - internalTrafficCapActive = true; - previousCombinedResidual = Double.POSITIVE_INFINITY; - logger.info( - "Divergence detected at iter {}, maxTrayFlow={} > threshold={}. " - + "Restoring from snapshot and reducing relaxation to {}.", - iter, maxTrayFlow, divergenceThreshold, relaxation); - } + double maxTrayFlow = 0.0; + for (int i = 0; i < numberOfTrays; i++) { + maxTrayFlow = + Math.max(maxTrayFlow, Math.abs(trays.get(i).getGasOutStream().getFlowRate("kg/hr"))); + maxTrayFlow = Math.max(maxTrayFlow, + Math.abs(trays.get(i).getLiquidOutStream().getFlowRate("kg/hr"))); + } + if (maxTrayFlow > divergenceThreshold) { + relaxation = minSequentialRelaxation; + for (int i = 0; i < numberOfTrays; i++) { + previousGasStreams[i] = snapshotGasStreams[i].clone(); + previousLiquidStreams[i] = snapshotLiquidStreams[i].clone(); + } + divergenceRecoveryApplied = true; + internalTrafficCapActive = true; + previousCombinedResidual = Double.POSITIVE_INFINITY; + logger.info( + "Divergence detected at iter {}, maxTrayFlow={} > threshold={}. " + + "Restoring from snapshot and reducing relaxation to {}.", + iter, maxTrayFlow, divergenceThreshold, relaxation); + } } for (int i = 0; i < numberOfTrays; i++) { - if (currentGasStreams[i] != null) { - previousGasStreams[i] = currentGasStreams[i]; - } - if (currentLiquidStreams[i] != null) { - previousLiquidStreams[i] = currentLiquidStreams[i]; - } + if (currentGasStreams[i] != null) { + previousGasStreams[i] = currentGasStreams[i]; + } + if (currentLiquidStreams[i] != null) { + previousLiquidStreams[i] = currentLiquidStreams[i]; + } } // Absolute flow magnitude check: if tray flows are vastly larger than // the total feed, the solver has diverged beyond recovery. Break early // and report a large mass residual so callers can detect the failure. if (divergenceRecoveryApplied && iter > 15) { - double maxFlow = 0.0; - for (int i = 0; i < numberOfTrays; i++) { - maxFlow = Math.max(maxFlow, Math.abs(trays.get(i).getGasOutStream().getFlowRate("kg/hr"))); - maxFlow = Math.max(maxFlow, Math.abs(trays.get(i).getLiquidOutStream().getFlowRate("kg/hr"))); - } - if (maxFlow > 1000.0 * totalFeedFlow) { - logger.warn( - "Column solver diverged: maxTrayFlow={} exceeds 1000x totalFeed={}. " + "Terminating at iteration {}.", - maxFlow, totalFeedFlow, iter); - massErr = maxFlow / Math.max(1.0, totalFeedFlow); - break; - } + double maxFlow = 0.0; + for (int i = 0; i < numberOfTrays; i++) { + maxFlow = + Math.max(maxFlow, Math.abs(trays.get(i).getGasOutStream().getFlowRate("kg/hr"))); + maxFlow = + Math.max(maxFlow, Math.abs(trays.get(i).getLiquidOutStream().getFlowRate("kg/hr"))); + } + if (maxFlow > 1000.0 * totalFeedFlow) { + logger.warn("Column solver diverged: maxTrayFlow={} exceeds 1000x totalFeed={}. " + + "Terminating at iteration {}.", maxFlow, totalFeedFlow, iter); + massErr = maxFlow / Math.max(1.0, totalFeedFlow); + break; + } } double guardedFlow = getMaximumTrayOutletFlowKgPerHour(); - if (divergenceRecoveryApplied && iter > 15 && guardedFlow >= 0.99 * getMaximumRelaxedInternalFlowKgPerHour()) { - logger.warn("Column solver reached internal traffic guard: maxTrayFlow={} at iteration {}.", guardedFlow, iter); - massErr = Math.max(massErr, guardedFlow / Math.max(1.0, totalFeedFlow)); - lastInternalTrafficGuardReached = true; - break; + if (divergenceRecoveryApplied && iter > 15 + && guardedFlow >= 0.99 * getMaximumRelaxedInternalFlowKgPerHour()) { + logger.warn("Column solver reached internal traffic guard: maxTrayFlow={} at iteration {}.", + guardedFlow, iter); + massErr = Math.max(massErr, guardedFlow / Math.max(1.0, totalFeedFlow)); + lastInternalTrafficGuardReached = true; + break; } - logger.debug("iteration {} relaxation={} tempErr={} massErr={} energyErr={}", iter, relaxation, err, massErr, - energyErr); + logger.debug("iteration {} relaxation={} tempErr={} massErr={} energyErr={}", iter, + relaxation, err, massErr, energyErr); if (convergenceHistory != null) { - recordConvergence(new double[] { err, massErr, energyErr }); + recordConvergence(new double[] {err, massErr, energyErr}); } boolean energyWithinBase = !enforceEnergyBalanceTolerance || energyErr <= baseEnergyTolerance; - boolean withinBaseTolerance = err <= baseTempTolerance && massErr <= baseMassTolerance && energyWithinBase; + boolean withinBaseTolerance = + err <= baseTempTolerance && massErr <= baseMassTolerance && energyWithinBase; if (withinBaseTolerance) { - boolean energyPolishingAvailable = enforceEnergyBalanceTolerance && polishEnergyTolerance < baseEnergyTolerance; - boolean polishingAvailable = polishMassTolerance < baseMassTolerance || energyPolishingAvailable - || polishTempTolerance < baseTempTolerance; + boolean energyPolishingAvailable = + enforceEnergyBalanceTolerance && polishEnergyTolerance < baseEnergyTolerance; + boolean polishingAvailable = polishMassTolerance < baseMassTolerance + || energyPolishingAvailable || polishTempTolerance < baseTempTolerance; - if (!polishing && polishingAvailable - && (massErr > polishMassTolerance || (energyPolishingAvailable && energyErr > polishEnergyTolerance))) { - polishing = true; - iterationLimit = Math.max(iterationLimit, polishIterationLimit); - previousCombinedResidual = Double.POSITIVE_INFINITY; - continue; - } + if (!polishing && polishingAvailable && (massErr > polishMassTolerance + || (energyPolishingAvailable && energyErr > polishEnergyTolerance))) { + polishing = true; + iterationLimit = Math.max(iterationLimit, polishIterationLimit); + previousCombinedResidual = Double.POSITIVE_INFINITY; + continue; + } - double tempTarget = polishing ? polishTempTolerance : baseTempTolerance; - double massTarget = polishing ? polishMassTolerance : baseMassTolerance; - double energyTarget = polishing ? polishEnergyTolerance : baseEnergyTolerance; - boolean energyWithinTarget = !enforceEnergyBalanceTolerance || energyErr <= energyTarget; + double tempTarget = polishing ? polishTempTolerance : baseTempTolerance; + double massTarget = polishing ? polishMassTolerance : baseMassTolerance; + double energyTarget = polishing ? polishEnergyTolerance : baseEnergyTolerance; + boolean energyWithinTarget = !enforceEnergyBalanceTolerance || energyErr <= energyTarget; - if (err <= tempTarget && massErr <= massTarget && energyWithinTarget) { - break; - } + if (err <= tempTarget && massErr <= massTarget && energyWithinTarget) { + break; + } } if (iter >= iterationLimit && err > baseTempTolerance && iterationLimit < maxIterationLimit) { - iterationLimit = Math.min(maxIterationLimit, iterationLimit + overflowIncrement); - continue; + iterationLimit = Math.min(maxIterationLimit, iterationLimit + overflowIncrement); + continue; } } @@ -5212,8 +5346,8 @@ private double getEffectiveEnthalpyBalanceTolerance() { * Estimate a scaling factor that reflects the degree of distillation complexity. * *

- * The factor increases with the number of theoretical stages and independent feed streams and is bounded to avoid - * overly loose convergence criteria. + * The factor increases with the number of theoretical stages and independent feed streams and is + * bounded to avoid overly loose convergence criteria. *

* * @return scaling multiplier for recommended tolerances @@ -5269,8 +5403,8 @@ private int getTotalFeedCount() { * @param balanceCheckStride cadence for periodic balance checks * @return {@code true} if balances should be evaluated */ - private boolean shouldEvaluateBalances(int iteration, int iterationLimit, boolean polishing, double tempResidual, - double baseTempTolerance, int balanceCheckStride) { + private boolean shouldEvaluateBalances(int iteration, int iterationLimit, boolean polishing, + double tempResidual, double baseTempTolerance, int balanceCheckStride) { if (polishing || iteration <= 2 || iteration >= iterationLimit - 1) { return true; } @@ -5308,9 +5442,10 @@ private int prepareColumnForSolve() { int feedTrayNumber = entry.getKey(); List trayFeeds = entry.getValue(); for (StreamInterface feedStream : trayFeeds) { - numeroffeeds[feedTrayNumber]++; - SystemInterface cloned = feedStream.getThermoSystem().clone(); - trays.get(feedTrayNumber).getStream(numeroffeeds[feedTrayNumber] - 1).setThermoSystem(cloned); + numeroffeeds[feedTrayNumber]++; + SystemInterface cloned = feedStream.getThermoSystem().clone(); + trays.get(feedTrayNumber).getStream(numeroffeeds[feedTrayNumber] - 1) + .setThermoSystem(cloned); } } @@ -5323,16 +5458,16 @@ private int prepareColumnForSolve() { *

* Key improvements over basic sequential substitution: *

    - *
  • K-value caching: previous iteration K-values are stored to track composition convergence and detect stagnation - * early.
  • - *
  • Composition-based convergence: monitors maximum relative K-value change alongside temperature and balance - * residuals.
  • - *
  • Stripping factor correction: applies a bulk flow correction between outer iterations based on the ratio of - * computed-to-assumed vapor/liquid split on each tray.
  • - *
  • Accelerated relaxation ramp: increases relaxation faster (1.3× vs 1.2×) when residuals decrease, enabling the - * IO method to reach full step sooner.
  • - *
  • Lazy balance evaluation: mass/energy balances are only recomputed when temperatures are close to tolerance, - * reducing expensive per-tray flow rate queries.
  • + *
  • K-value caching: previous iteration K-values are stored to track composition convergence + * and detect stagnation early.
  • + *
  • Composition-based convergence: monitors maximum relative K-value change alongside + * temperature and balance residuals.
  • + *
  • Stripping factor correction: applies a bulk flow correction between outer iterations based + * on the ratio of computed-to-assumed vapor/liquid split on each tray.
  • + *
  • Accelerated relaxation ramp: increases relaxation faster (1.3× vs 1.2×) when residuals + * decrease, enabling the IO method to reach full step sooner.
  • + *
  • Lazy balance evaluation: mass/energy balances are only recomputed when temperatures are + * close to tolerance, reducing expensive per-tray flow rate queries.
  • *
* * @param id calculation identifier @@ -5385,10 +5520,11 @@ void solveInsideOut(UUID id) { double totalFeedFlowIO = 0.0; for (List feeds : feedStreams.values()) { for (StreamInterface f : feeds) { - totalFeedFlowIO += Math.abs(f.getFlowRate("kg/hr")); + totalFeedFlowIO += Math.abs(f.getFlowRate("kg/hr")); } } - double divergenceThresholdIO = Math.max(totalFeedFlowIO * MAX_RELAXED_INTERNAL_TRAFFIC_TO_FEED_RATIO, 1.0e3); + double divergenceThresholdIO = + Math.max(totalFeedFlowIO * MAX_RELAXED_INTERNAL_TRAFFIC_TO_FEED_RATIO, 1.0e3); boolean divergenceRecoveryAppliedIO = false; // Snapshot tray state before iterations as a safe recovery point. @@ -5402,8 +5538,8 @@ void solveInsideOut(UUID id) { // On re-runs, seed previous-stream arrays from the snapshot. if (hasBeenSolvedBefore) { for (int i = 0; i < numberOfTrays; i++) { - previousGasStreams[i] = snapshotGasStreamsIO[i].clone(); - previousLiquidStreams[i] = snapshotLiquidStreamsIO[i].clone(); + previousGasStreams[i] = snapshotGasStreamsIO[i].clone(); + previousLiquidStreams[i] = snapshotLiquidStreamsIO[i].clone(); } } @@ -5420,11 +5556,11 @@ void solveInsideOut(UUID id) { int baseIterationLimit = computeIterationLimit(); int iterationLimit = baseIterationLimit; int polishIterationLimit = baseIterationLimit - + Math.max(POLISH_ITERATION_MARGIN, (int) Math.ceil(0.5 * numberOfTrays)); + + Math.max(POLISH_ITERATION_MARGIN, (int) Math.ceil(0.5 * numberOfTrays)); int overflowIncrement = Math.max(3, (int) Math.ceil(0.5 * numberOfTrays)); int overflowBand = Math.max(overflowIncrement, numberOfTrays); int maxIterationLimit = Math.max(iterationLimit, maxNumberOfIterations) - + overflowBand * ITERATION_OVERFLOW_MULTIPLIER; + + overflowBand * ITERATION_OVERFLOW_MULTIPLIER; double baseTempTolerance = getEffectiveTemperatureTolerance(); double baseMassTolerance = getEffectiveMassBalanceTolerance(); double baseEnergyTolerance = getEffectiveEnthalpyBalanceTolerance(); @@ -5442,44 +5578,44 @@ void solveInsideOut(UUID id) { Arrays.fill(currentLiquidStreams, null); for (int i = 0; i < numberOfTrays; i++) { - oldtemps[i] = trays.get(i).getThermoSystem().getTemperature(); + oldtemps[i] = trays.get(i).getThermoSystem().getTemperature(); } // Phase 1: Liquid sweep (feed → reboiler) with relaxed tear streams for (int stage = firstFeedTrayNumber; stage >= 1; stage--) { - int target = stage - 1; - int replaceStream = trays.get(target).getNumberOfInputStreams() - 1; - StreamInterface relaxedLiquid = applyRelaxationFast(previousLiquidStreams[stage], - trays.get(stage).getLiquidOutStream(), relaxation); - trays.get(target).replaceStream(replaceStream, relaxedLiquid); - currentLiquidStreams[stage] = relaxedLiquid; - trays.get(target).run(id); - applyMurphreeCorrection(target); + int target = stage - 1; + int replaceStream = trays.get(target).getNumberOfInputStreams() - 1; + StreamInterface relaxedLiquid = applyRelaxationFast(previousLiquidStreams[stage], + trays.get(stage).getLiquidOutStream(), relaxation); + trays.get(target).replaceStream(replaceStream, relaxedLiquid); + currentLiquidStreams[stage] = relaxedLiquid; + trays.get(target).run(id); + applyMurphreeCorrection(target); } // Phase 2: Vapor sweep (reboiler → condenser) with relaxed tear streams for (int stage = 1; stage <= numberOfTrays - 1; stage++) { - int replaceStream = trays.get(stage).getNumberOfInputStreams() - 2; - if (stage == (numberOfTrays - 1)) { - replaceStream = trays.get(stage).getNumberOfInputStreams() - 1; - } - StreamInterface relaxedGas = applyRelaxationFast(previousGasStreams[stage - 1], - trays.get(stage - 1).getGasOutStream(), relaxation); - trays.get(stage).replaceStream(replaceStream, relaxedGas); - currentGasStreams[stage - 1] = relaxedGas; - trays.get(stage).run(id); - applyMurphreeCorrection(stage); + int replaceStream = trays.get(stage).getNumberOfInputStreams() - 2; + if (stage == (numberOfTrays - 1)) { + replaceStream = trays.get(stage).getNumberOfInputStreams() - 1; + } + StreamInterface relaxedGas = applyRelaxationFast(previousGasStreams[stage - 1], + trays.get(stage - 1).getGasOutStream(), relaxation); + trays.get(stage).replaceStream(replaceStream, relaxedGas); + currentGasStreams[stage - 1] = relaxedGas; + trays.get(stage).run(id); + applyMurphreeCorrection(stage); } // Phase 3: Polish liquid sweep (condenser → feed) for better coupling for (int stage = numberOfTrays - 2; stage >= firstFeedTrayNumber; stage--) { - int replaceStream = trays.get(stage).getNumberOfInputStreams() - 1; - StreamInterface relaxedLiquid = applyRelaxationFast(previousLiquidStreams[stage + 1], - trays.get(stage + 1).getLiquidOutStream(), relaxation); - trays.get(stage).replaceStream(replaceStream, relaxedLiquid); - currentLiquidStreams[stage + 1] = relaxedLiquid; - trays.get(stage).run(id); - applyMurphreeCorrection(stage); + int replaceStream = trays.get(stage).getNumberOfInputStreams() - 1; + StreamInterface relaxedLiquid = applyRelaxationFast(previousLiquidStreams[stage + 1], + trays.get(stage + 1).getLiquidOutStream(), relaxation); + trays.get(stage).replaceStream(replaceStream, relaxedLiquid); + currentLiquidStreams[stage + 1] = relaxedLiquid; + trays.get(stage).run(id); + applyMurphreeCorrection(stage); } // Phase 4: Stripping factor correction — adjust temperatures using V/L flow @@ -5488,23 +5624,24 @@ void solveInsideOut(UUID id) { double effectiveRelaxation = Math.max(minTemperatureRelaxation, Math.min(1.0, relaxation)); for (int i = 0; i < numberOfTrays; i++) { - double updated = trays.get(i).getThermoSystem().getTemperature(); - - // Stripping factor correction: if V/L ratio on a tray is far from unity, - // bias temperature update toward the flow-corrected value - double vaporFlow = trays.get(i).getGasOutStream().getFlowRate("kg/hr"); - double liquidFlow = trays.get(i).getLiquidOutStream().getFlowRate("kg/hr"); - double strippingCorrection = 1.0; - if (liquidFlow > 1e-12 && vaporFlow > 1e-12) { - double vOverL = vaporFlow / liquidFlow; - // Mild correction: push temperature up if too much liquid, down if too much - // vapor - strippingCorrection = 1.0 + 0.05 * Math.max(-1.0, Math.min(1.0, Math.log(vOverL))); - } - - double newTemp = oldtemps[i] + effectiveRelaxation * strippingCorrection * (updated - oldtemps[i]); - trays.get(i).setTemperature(newTemp); - temperatureResidual += Math.abs(newTemp - oldtemps[i]); + double updated = trays.get(i).getThermoSystem().getTemperature(); + + // Stripping factor correction: if V/L ratio on a tray is far from unity, + // bias temperature update toward the flow-corrected value + double vaporFlow = trays.get(i).getGasOutStream().getFlowRate("kg/hr"); + double liquidFlow = trays.get(i).getLiquidOutStream().getFlowRate("kg/hr"); + double strippingCorrection = 1.0; + if (liquidFlow > 1e-12 && vaporFlow > 1e-12) { + double vOverL = vaporFlow / liquidFlow; + // Mild correction: push temperature up if too much liquid, down if too much + // vapor + strippingCorrection = 1.0 + 0.05 * Math.max(-1.0, Math.min(1.0, Math.log(vOverL))); + } + + double newTemp = + oldtemps[i] + effectiveRelaxation * strippingCorrection * (updated - oldtemps[i]); + trays.get(i).setTemperature(newTemp); + temperatureResidual += Math.abs(newTemp - oldtemps[i]); } temperatureResidual /= Math.max(1, numberOfTrays); err = temperatureResidual; @@ -5517,50 +5654,52 @@ void solveInsideOut(UUID id) { // Fit simplified K-value model after 2nd rigorous outer iteration if (outerIterCount >= 2 && prevOuterKvalues != null && innerLoopSteps > 0) { - double[] currentTemps = new double[numberOfTrays]; - for (int i = 0; i < numberOfTrays; i++) { - currentTemps[i] = trays.get(i).getThermoSystem().getTemperature(); - } - kModel.fit(prevOuterKvalues, prevOuterTemps, previousKvalues, currentTemps); + double[] currentTemps = new double[numberOfTrays]; + for (int i = 0; i < numberOfTrays; i++) { + currentTemps[i] = trays.get(i).getThermoSystem().getTemperature(); + } + kModel.fit(prevOuterKvalues, prevOuterTemps, previousKvalues, currentTemps); } // Save outer-loop state for next model fitting prevOuterKvalues = previousKvalues.clone(); for (int i = 0; i < numberOfTrays; i++) { - prevOuterTemps[i] = trays.get(i).getThermoSystem().getTemperature(); - if (prevOuterKvalues[i] != null) { - prevOuterKvalues[i] = prevOuterKvalues[i].clone(); - } + prevOuterTemps[i] = trays.get(i).getThermoSystem().getTemperature(); + if (prevOuterKvalues[i] != null) { + prevOuterKvalues[i] = prevOuterKvalues[i].clone(); + } } // Run simplified inner-loop iterations (no PH-flash) if model is fitted if (kModel.fitted && innerLoopSteps > 0 && !polishing) { - double latestInnerTempResidual = err; - for (int inner = 0; inner < innerLoopSteps; inner++) { - double innerTempResidual = innerLoopIteration(kModel, relaxation); - latestInnerTempResidual = innerTempResidual; - latestSurrogateResidual = innerTempResidual; - totalInnerLoopIterations++; - // Log inner iteration (inner iters don't count in outer iteration budget) - logger.debug("inside-out INNER step {}/{} tempErr={}", inner + 1, innerLoopSteps, innerTempResidual); - if (convergenceHistory != null) { - convergenceHistory.add(new double[] { innerTempResidual, massErr, energyErr, kValueResidual }); - } - // If inner loop has converged, no need for more inner steps - if (innerTempResidual < baseTempTolerance * 0.5) { - break; - } - } - // Do not let the simplified inner loop hide the rigorous outer-loop residual. - err = Math.max(err, latestInnerTempResidual); - } - - boolean evaluateBalances = shouldEvaluateBalances(iter, iterationLimit, polishing, err, baseTempTolerance, - balanceCheckStride); + double latestInnerTempResidual = err; + for (int inner = 0; inner < innerLoopSteps; inner++) { + double innerTempResidual = innerLoopIteration(kModel, relaxation); + latestInnerTempResidual = innerTempResidual; + latestSurrogateResidual = innerTempResidual; + totalInnerLoopIterations++; + // Log inner iteration (inner iters don't count in outer iteration budget) + logger.debug("inside-out INNER step {}/{} tempErr={}", inner + 1, innerLoopSteps, + innerTempResidual); + if (convergenceHistory != null) { + convergenceHistory + .add(new double[] {innerTempResidual, massErr, energyErr, kValueResidual}); + } + // If inner loop has converged, no need for more inner steps + if (innerTempResidual < baseTempTolerance * 0.5) { + break; + } + } + // Do not let the simplified inner loop hide the rigorous outer-loop residual. + err = Math.max(err, latestInnerTempResidual); + } + + boolean evaluateBalances = shouldEvaluateBalances(iter, iterationLimit, polishing, err, + baseTempTolerance, balanceCheckStride); if (evaluateBalances || !massEnergyEvaluated) { - massErr = getMassBalanceError(); - energyErr = getEnergyBalanceError(); - massEnergyEvaluated = true; + massErr = getMassBalanceError(); + energyErr = getEnergyBalanceError(); + massEnergyEvaluated = true; } double tempScaled = err / baseTempTolerance; @@ -5568,114 +5707,124 @@ void solveInsideOut(UUID id) { double energyScaled = energyErr / baseEnergyTolerance; double combinedResidual = Math.max(tempScaled, massScaled); if (Double.isFinite(energyScaled)) { - combinedResidual = Math.max(combinedResidual, Math.min(energyScaled, maxEnergyRelaxationWeight)); + combinedResidual = + Math.max(combinedResidual, Math.min(energyScaled, maxEnergyRelaxationWeight)); } // Accelerated adaptive relaxation for IO method if (combinedResidual > previousCombinedResidual * 1.05) { - relaxation = Math.max(minInsideOutRelaxation, relaxation * relaxationDecreaseFactor); + relaxation = Math.max(minInsideOutRelaxation, relaxation * relaxationDecreaseFactor); } else if (combinedResidual < previousCombinedResidual * 0.95) { - // More aggressive increase than sequential — IO can tolerate faster ramp - relaxation = Math.min(maxAdaptiveRelaxation, relaxation * ioRelaxationIncreaseFactor); + // More aggressive increase than sequential — IO can tolerate faster ramp + relaxation = Math.min(maxAdaptiveRelaxation, relaxation * ioRelaxationIncreaseFactor); } previousCombinedResidual = combinedResidual; // Divergence recovery (same logic as solveSequential). if (!divergenceRecoveryAppliedIO && iter <= 10) { - double maxTrayFlow = 0.0; - for (int i = 0; i < numberOfTrays; i++) { - maxTrayFlow = Math.max(maxTrayFlow, Math.abs(trays.get(i).getGasOutStream().getFlowRate("kg/hr"))); - maxTrayFlow = Math.max(maxTrayFlow, Math.abs(trays.get(i).getLiquidOutStream().getFlowRate("kg/hr"))); - } - if (maxTrayFlow > divergenceThresholdIO) { - relaxation = minInsideOutRelaxation; - for (int i = 0; i < numberOfTrays; i++) { - previousGasStreams[i] = snapshotGasStreamsIO[i].clone(); - previousLiquidStreams[i] = snapshotLiquidStreamsIO[i].clone(); - } - divergenceRecoveryAppliedIO = true; - internalTrafficCapActive = true; - previousCombinedResidual = Double.POSITIVE_INFINITY; - logger.info( - "inside-out divergence detected at iter {}, maxTrayFlow={} > threshold={}. " - + "Restoring from snapshot and reducing relaxation to {}.", - iter, maxTrayFlow, divergenceThresholdIO, relaxation); - } + double maxTrayFlow = 0.0; + for (int i = 0; i < numberOfTrays; i++) { + maxTrayFlow = + Math.max(maxTrayFlow, Math.abs(trays.get(i).getGasOutStream().getFlowRate("kg/hr"))); + maxTrayFlow = Math.max(maxTrayFlow, + Math.abs(trays.get(i).getLiquidOutStream().getFlowRate("kg/hr"))); + } + if (maxTrayFlow > divergenceThresholdIO) { + relaxation = minInsideOutRelaxation; + for (int i = 0; i < numberOfTrays; i++) { + previousGasStreams[i] = snapshotGasStreamsIO[i].clone(); + previousLiquidStreams[i] = snapshotLiquidStreamsIO[i].clone(); + } + divergenceRecoveryAppliedIO = true; + internalTrafficCapActive = true; + previousCombinedResidual = Double.POSITIVE_INFINITY; + logger.info( + "inside-out divergence detected at iter {}, maxTrayFlow={} > threshold={}. " + + "Restoring from snapshot and reducing relaxation to {}.", + iter, maxTrayFlow, divergenceThresholdIO, relaxation); + } } for (int i = 0; i < numberOfTrays; i++) { - if (currentGasStreams[i] != null) { - previousGasStreams[i] = currentGasStreams[i]; - } - if (currentLiquidStreams[i] != null) { - previousLiquidStreams[i] = currentLiquidStreams[i]; - } + if (currentGasStreams[i] != null) { + previousGasStreams[i] = currentGasStreams[i]; + } + if (currentLiquidStreams[i] != null) { + previousLiquidStreams[i] = currentLiquidStreams[i]; + } } double guardedFlow = getMaximumTrayOutletFlowKgPerHour(); - if (divergenceRecoveryAppliedIO && iter > 15 && guardedFlow >= 0.99 * getMaximumRelaxedInternalFlowKgPerHour()) { - logger.warn("Inside-out solver reached internal traffic guard: maxTrayFlow={} at iteration {}.", guardedFlow, - iter); - massErr = Math.max(massErr, guardedFlow / Math.max(1.0, totalFeedFlowIO)); - lastInternalTrafficGuardReached = true; - break; + if (divergenceRecoveryAppliedIO && iter > 15 + && guardedFlow >= 0.99 * getMaximumRelaxedInternalFlowKgPerHour()) { + logger.warn( + "Inside-out solver reached internal traffic guard: maxTrayFlow={} at iteration {}.", + guardedFlow, iter); + massErr = Math.max(massErr, guardedFlow / Math.max(1.0, totalFeedFlowIO)); + lastInternalTrafficGuardReached = true; + break; } - logger.debug("inside-out iteration {} relaxation={} tempErr={} massErr={} energyErr={} kErr={} outerFlashes={}", - iter, relaxation, err, massErr, energyErr, kValueResidual, totalFlashSweeps); + logger.debug( + "inside-out iteration {} relaxation={} tempErr={} massErr={} energyErr={} kErr={} outerFlashes={}", + iter, relaxation, err, massErr, energyErr, kValueResidual, totalFlashSweeps); if (convergenceHistory != null) { - recordConvergence(new double[] { err, massErr, energyErr, kValueResidual }); + recordConvergence(new double[] {err, massErr, energyErr, kValueResidual}); } boolean energyWithinBase = !enforceEnergyBalanceTolerance || energyErr <= baseEnergyTolerance; - boolean withinBaseTolerance = err <= baseTempTolerance && massErr <= baseMassTolerance && energyWithinBase; + boolean withinBaseTolerance = + err <= baseTempTolerance && massErr <= baseMassTolerance && energyWithinBase; if (withinBaseTolerance) { - boolean energyPolishingAvailable = enforceEnergyBalanceTolerance && polishEnergyTolerance < baseEnergyTolerance; - boolean polishingAvailable = polishMassTolerance < baseMassTolerance || energyPolishingAvailable - || polishTempTolerance < baseTempTolerance; + boolean energyPolishingAvailable = + enforceEnergyBalanceTolerance && polishEnergyTolerance < baseEnergyTolerance; + boolean polishingAvailable = polishMassTolerance < baseMassTolerance + || energyPolishingAvailable || polishTempTolerance < baseTempTolerance; - if (!polishing && polishingAvailable - && (massErr > polishMassTolerance || (energyPolishingAvailable && energyErr > polishEnergyTolerance))) { - polishing = true; - iterationLimit = Math.max(iterationLimit, polishIterationLimit); - previousCombinedResidual = Double.POSITIVE_INFINITY; - continue; - } + if (!polishing && polishingAvailable && (massErr > polishMassTolerance + || (energyPolishingAvailable && energyErr > polishEnergyTolerance))) { + polishing = true; + iterationLimit = Math.max(iterationLimit, polishIterationLimit); + previousCombinedResidual = Double.POSITIVE_INFINITY; + continue; + } - double tempTarget = polishing ? polishTempTolerance : baseTempTolerance; - double massTarget = polishing ? polishMassTolerance : baseMassTolerance; - double energyTarget = polishing ? polishEnergyTolerance : baseEnergyTolerance; - boolean energyWithinTarget = !enforceEnergyBalanceTolerance || energyErr <= energyTarget; + double tempTarget = polishing ? polishTempTolerance : baseTempTolerance; + double massTarget = polishing ? polishMassTolerance : baseMassTolerance; + double energyTarget = polishing ? polishEnergyTolerance : baseEnergyTolerance; + boolean energyWithinTarget = !enforceEnergyBalanceTolerance || energyErr <= energyTarget; - if (err <= tempTarget && massErr <= massTarget && energyWithinTarget) { - break; - } + if (err <= tempTarget && massErr <= massTarget && energyWithinTarget) { + break; + } } // Early termination: if K-values have converged but mass/energy haven't, // the problem may be ill-conditioned — avoid wasting iterations if (kValueResidual < 1.0e-6 && iter > 5 && err > baseTempTolerance * 10) { - logger.warn("Inside-out: K-values converged but temperatures stagnated at iter {}", iter); + logger.warn("Inside-out: K-values converged but temperatures stagnated at iter {}", iter); } if (shouldSwitchInsideOutToNewton(iter, err, baseTempTolerance, kValueResidual)) { - storeInsideOutTelemetry(totalFlashSweeps, totalInnerLoopIterations, kValueResidual, latestSurrogateResidual); - hasBeenSolvedBefore = false; - setDoInitializion(true); - solveNewton(id); - return; + storeInsideOutTelemetry(totalFlashSweeps, totalInnerLoopIterations, kValueResidual, + latestSurrogateResidual); + hasBeenSolvedBefore = false; + setDoInitializion(true); + solveNewton(id); + return; } if (iter >= iterationLimit && err > baseTempTolerance && iterationLimit < maxIterationLimit) { - iterationLimit = Math.min(maxIterationLimit, iterationLimit + overflowIncrement); - continue; + iterationLimit = Math.min(maxIterationLimit, iterationLimit + overflowIncrement); + continue; } } - storeInsideOutTelemetry(totalFlashSweeps, totalInnerLoopIterations, kValueResidual, latestSurrogateResidual); + storeInsideOutTelemetry(totalFlashSweeps, totalInnerLoopIterations, kValueResidual, + latestSurrogateResidual); finalizeSolve(id, iter, err, massErr, energyErr, startTime); } @@ -5683,10 +5832,11 @@ void solveInsideOut(UUID id) { * Decide when the inside-out fixed-point stage should hand off to simultaneous Newton. * *

- * Large columns spend most of their wall time in repeated tray flashes once the K-value profile is already well - * shaped. The hybrid handoff follows the common inside-out/simultaneous-correction pattern: use cheap fixed-point - * sweeps for initialization, then solve the coupled temperature correction when additional sweeps mostly polish the - * same profile. + * Large columns spend most of their wall time in repeated tray flashes once the K-value profile + * is already well shaped. The hybrid handoff follows the common + * inside-out/simultaneous-correction pattern: use cheap fixed-point sweeps for initialization, + * then solve the coupled temperature correction when additional sweeps mostly polish the same + * profile. *

* * @param iteration current inside-out outer iteration @@ -5695,8 +5845,8 @@ void solveInsideOut(UUID id) { * @param kValueResidual current K-value residual * @return {@code true} when a Newton handoff is expected to be cheaper than more sweeps */ - private boolean shouldSwitchInsideOutToNewton(int iteration, double temperatureResidual, double temperatureTolerance, - double kValueResidual) { + private boolean shouldSwitchInsideOutToNewton(int iteration, double temperatureResidual, + double temperatureTolerance, double kValueResidual) { if (numberOfTrays < 8 || iteration < Math.max(6, numberOfTrays / 2)) { return false; } @@ -5704,7 +5854,7 @@ private boolean shouldSwitchInsideOutToNewton(int iteration, double temperatureR return false; } return !Double.isFinite(kValueResidual) || kValueResidual < 5.0e-2 - || temperatureResidual < temperatureTolerance * 100.0; + || temperatureResidual < temperatureTolerance * 100.0; } /** @@ -5715,8 +5865,8 @@ private boolean shouldSwitchInsideOutToNewton(int iteration, double temperatureR * @param kValueResidual latest K-value residual * @param surrogateResidual latest surrogate-model residual */ - private void storeInsideOutTelemetry(int outerFlashSweeps, int innerLoopIterations, double kValueResidual, - double surrogateResidual) { + private void storeInsideOutTelemetry(int outerFlashSweeps, int innerLoopIterations, + double kValueResidual, double surrogateResidual) { lastInsideOutOuterFlashSweeps = outerFlashSweeps; lastInsideOutInnerLoopIterations = innerLoopIterations; lastInsideOutKValueResidual = kValueResidual; @@ -5728,9 +5878,9 @@ private void storeInsideOutTelemetry(int outerFlashSweeps, int innerLoopIteratio * Solve the column with matrix inside-out component balances before rigorous polishing. * *

- * The matrix stage solves component material-balance tridiagonal systems using cached K-values and cached - * K-temperature derivatives. It is accepted only as a warm start; the final products and convergence metrics still - * come from the rigorous inside-out solver. + * The matrix stage solves component material-balance tridiagonal systems using cached K-values + * and cached K-temperature derivatives. It is accepted only as a warm start; the final products + * and convergence metrics still come from the rigorous inside-out solver. *

* * @param id calculation identifier @@ -5760,8 +5910,8 @@ void solveMatrixInsideOut(UUID id) { boolean wasSolvedBefore = hasBeenSolvedBefore; DistillationColumnMatrixSolver matrixSolver = new DistillationColumnMatrixSolver(this); - int matrixIterationLimit = Math.max(2, - Math.min(Math.max(4, numberOfTrays), Math.max(2, maxNumberOfIterations / 4))); + int matrixIterationLimit = + Math.max(2, Math.min(Math.max(4, numberOfTrays), Math.max(2, maxNumberOfIterations / 4))); matrixSolver.setMaxIterations(matrixIterationLimit); matrixSolver.setTolerance(Math.max(getEffectiveTemperatureTolerance(), 5.0e-2)); matrixSolver.setDampingFactor(Math.max(0.2, Math.min(0.6, minInsideOutRelaxation))); @@ -5770,7 +5920,8 @@ void solveMatrixInsideOut(UUID id) { try { matrixWarmStartAccepted = matrixSolver.solve(id); } catch (RuntimeException exception) { - logger.debug("Matrix inside-out warm start failed; continuing with rigorous inside-out.", exception); + logger.debug("Matrix inside-out warm start failed; continuing with rigorous inside-out.", + exception); } int matrixIterations = matrixSolver.getLastIterationCount(); @@ -5793,15 +5944,15 @@ void solveMatrixInsideOut(UUID id) { lastIterationCount += matrixIterations; lastSolveTimeSeconds += matrixSolveTime; logger.debug("Matrix inside-out stage iterations={} residual={} accepted={}", matrixIterations, - matrixTemperatureResidual, matrixWarmStartAccepted); + matrixTemperatureResidual, matrixWarmStartAccepted); } /** * Decide whether the matrix warm-start stage should be skipped for the current column size. * *

- * The matrix component-balance stage has fixed setup cost and is only expected to pay off on larger columns. Small - * benchmark columns are faster with the rigorous inside-out path directly. + * The matrix component-balance stage has fixed setup cost and is only expected to pay off on + * larger columns. Small benchmark columns are faster with the rigorous inside-out path directly. *

* * @return {@code true} when the adaptive matrix solver should use rigorous inside-out directly @@ -5814,9 +5965,9 @@ private boolean shouldBypassMatrixInsideOutWarmStart() { * Compute the maximum relative K-value change compared to the previous iteration. * *

- * K-values are computed as the ratio of vapor to liquid mole fractions for each component on each tray. This provides - * a composition-based convergence metric that complements the temperature-based metric, similar to what commercial - * inside-out implementations track. + * K-values are computed as the ratio of vapor to liquid mole fractions for each component on each + * tray. This provides a composition-based convergence metric that complements the + * temperature-based metric, similar to what commercial inside-out implementations track. *

* * @param previousKvalues K-values from the previous iteration (null if first iteration) @@ -5831,20 +5982,20 @@ private double computeKvalueResidual(double[][] previousKvalues) { for (int i = 0; i < numberOfTrays; i++) { SystemInterface fluid = trays.get(i).getThermoSystem(); if (fluid.getNumberOfPhases() < 2) { - continue; + continue; } int nc = fluid.getNumberOfComponents(); for (int j = 0; j < nc; j++) { - double xj = fluid.getPhase(1).getComponent(j).getx(); - double yj = fluid.getPhase(0).getComponent(j).getx(); - if (xj > 1e-15) { - double kCurrent = yj / xj; - double kPrevious = previousKvalues[i][j]; - if (kPrevious > 1e-15) { - double relChange = Math.abs(kCurrent - kPrevious) / kPrevious; - maxRelChange = Math.max(maxRelChange, relChange); - } - } + double xj = fluid.getPhase(1).getComponent(j).getx(); + double yj = fluid.getPhase(0).getComponent(j).getx(); + if (xj > 1e-15) { + double kCurrent = yj / xj; + double kPrevious = previousKvalues[i][j]; + if (kPrevious > 1e-15) { + double relChange = Math.abs(kCurrent - kPrevious) / kPrevious; + maxRelChange = Math.max(maxRelChange, relChange); + } + } } } return maxRelChange; @@ -5862,12 +6013,12 @@ private double[][] cacheCurrentKvalues() { int nc = fluid.getNumberOfComponents(); kvalues[i] = new double[nc]; if (fluid.getNumberOfPhases() >= 2) { - for (int j = 0; j < nc; j++) { - double xj = fluid.getPhase(1).getComponent(j).getx(); - if (xj > 1e-15) { - kvalues[i][j] = fluid.getPhase(0).getComponent(j).getx() / xj; - } - } + for (int j = 0; j < nc; j++) { + double xj = fluid.getPhase(1).getComponent(j).getx(); + if (xj > 1e-15) { + kvalues[i][j] = fluid.getPhase(0).getComponent(j).getx() / xj; + } + } } } return kvalues; @@ -5883,10 +6034,10 @@ private double[][] cacheCurrentKvalues() { * ln K_i = a_i + b_i / T * * - * where T is in Kelvin. The coefficients are fitted from rigorous flash results at two temperature points (the - * current and previous outer-loop temperatures). Between outer-loop updates, compositions are estimated using this - * simplified model instead of full PH-flash calculations, reducing computational cost by a factor of approximately - * {@code innerLoopSteps}. + * where T is in Kelvin. The coefficients are fitted from rigorous flash results at two + * temperature points (the current and previous outer-loop temperatures). Between outer-loop + * updates, compositions are estimated using this simplified model instead of full PH-flash + * calculations, reducing computational cost by a factor of approximately {@code innerLoopSteps}. */ static class SimplifiedKvalueModel { /** Intercept coefficient: lnK = a + b/T. Indexed [tray][component]. */ @@ -5927,34 +6078,34 @@ static class SimplifiedKvalueModel { */ void fit(double[][] kvalues1, double[] temps1, double[][] kvalues2, double[] temps2) { for (int i = 0; i < nTrays; i++) { - double t1 = temps1[i]; - double t2 = temps2[i]; - if (t1 < 1.0 || t2 < 1.0 || Math.abs(t1 - t2) < 0.01) { - // Temperatures too close or invalid — use single-point model (b=0) - for (int j = 0; j < nComponents && j < kvalues2[i].length; j++) { - if (kvalues2[i][j] > 1e-30) { - coeffA[i][j] = Math.log(kvalues2[i][j]); - coeffB[i][j] = 0.0; - } - } - continue; - } - double invT1 = 1.0 / t1; - double invT2 = 1.0 / t2; - double dInvT = invT2 - invT1; - for (int j = 0; j < nComponents && j < kvalues1[i].length && j < kvalues2[i].length; j++) { - double k1 = kvalues1[i][j]; - double k2 = kvalues2[i][j]; - if (k1 > 1e-30 && k2 > 1e-30) { - double lnK1 = Math.log(k1); - double lnK2 = Math.log(k2); - coeffB[i][j] = (lnK2 - lnK1) / dInvT; - coeffA[i][j] = lnK1 - coeffB[i][j] * invT1; - } else if (k2 > 1e-30) { - coeffA[i][j] = Math.log(k2); - coeffB[i][j] = 0.0; - } - } + double t1 = temps1[i]; + double t2 = temps2[i]; + if (t1 < 1.0 || t2 < 1.0 || Math.abs(t1 - t2) < 0.01) { + // Temperatures too close or invalid — use single-point model (b=0) + for (int j = 0; j < nComponents && j < kvalues2[i].length; j++) { + if (kvalues2[i][j] > 1e-30) { + coeffA[i][j] = Math.log(kvalues2[i][j]); + coeffB[i][j] = 0.0; + } + } + continue; + } + double invT1 = 1.0 / t1; + double invT2 = 1.0 / t2; + double dInvT = invT2 - invT1; + for (int j = 0; j < nComponents && j < kvalues1[i].length && j < kvalues2[i].length; j++) { + double k1 = kvalues1[i][j]; + double k2 = kvalues2[i][j]; + if (k1 > 1e-30 && k2 > 1e-30) { + double lnK1 = Math.log(k1); + double lnK2 = Math.log(k2); + coeffB[i][j] = (lnK2 - lnK1) / dInvT; + coeffA[i][j] = lnK1 - coeffB[i][j] * invT1; + } else if (k2 > 1e-30) { + coeffA[i][j] = Math.log(k2); + coeffB[i][j] = 0.0; + } + } } fitted = true; } @@ -5969,7 +6120,7 @@ void fit(double[][] kvalues1, double[] temps1, double[][] kvalues2, double[] tem */ double predict(int tray, int component, double temperature) { if (temperature < 1.0) { - return 1.0; + return 1.0; } double lnK = coeffA[tray][component] + coeffB[tray][component] / temperature; // Bound to prevent extreme values @@ -5982,9 +6133,9 @@ void fit(double[][] kvalues1, double[] temps1, double[][] kvalues2, double[] tem * Perform a simplified inner-loop iteration using the K-value model instead of rigorous flash. * *

- * This method updates tray compositions using the simplified K-value correlation and adjusts temperatures via a - * bubble-point calculation (sum of K*x = 1 condition). No PH-flash is called, making each inner iteration much - * cheaper than a rigorous outer iteration. + * This method updates tray compositions using the simplified K-value correlation and adjusts + * temperatures via a bubble-point calculation (sum of K*x = 1 condition). No PH-flash is called, + * making each inner iteration much cheaper than a rigorous outer iteration. *

* * @param model the fitted simplified K-value model @@ -5998,7 +6149,7 @@ private double innerLoopIteration(SimplifiedKvalueModel model, double relaxation for (int i = 0; i < numberOfTrays; i++) { SystemInterface fluid = trays.get(i).getThermoSystem(); if (fluid.getNumberOfPhases() < 2) { - continue; + continue; } double trayTemp = fluid.getTemperature(); @@ -6008,49 +6159,49 @@ private double innerLoopIteration(SimplifiedKvalueModel model, double relaxation double sumKx = 0.0; double[] kPredicted = new double[nc]; for (int j = 0; j < nc; j++) { - kPredicted[j] = model.predict(i, j, trayTemp); - double xj = fluid.getPhase(1).getComponent(j).getx(); - sumKx += kPredicted[j] * xj; + kPredicted[j] = model.predict(i, j, trayTemp); + double xj = fluid.getPhase(1).getComponent(j).getx(); + sumKx += kPredicted[j] * xj; } // Bubble-point temperature correction: if sum(K*x) != 1, adjust T // Using Newton-like step: dT = -f(T)/f'(T) where f(T) = sum(K*x) - 1 // f'(T) ≈ -sum(b_j/T^2 * K_j * x_j) (derivative of K model w.r.t. T) if (sumKx > 1e-10) { - double dfdt = 0.0; - for (int j = 0; j < nc; j++) { - double xj = fluid.getPhase(1).getComponent(j).getx(); - dfdt += -model.coeffB[i][j] / (trayTemp * trayTemp) * kPredicted[j] * xj; - } - - double correction = 0.0; - if (Math.abs(dfdt) > 1e-15) { - correction = -(sumKx - 1.0) / dfdt; - // Safeguard: limit step size - correction = Math.max(-15.0, Math.min(15.0, correction)); - } - - double newTemp = trayTemp + effectiveRelaxation * correction; - // Ensure temperature stays positive - newTemp = Math.max(50.0, newTemp); - tempResidual += Math.abs(newTemp - trayTemp); - trays.get(i).setTemperature(newTemp); - - // Update vapor compositions: y_j = K_j * x_j / sum(K*x) - // (normalized to ensure summation) - if (sumKx > 1e-10) { - for (int j = 0; j < nc; j++) { - double xj = fluid.getPhase(1).getComponent(j).getx(); - double newYj = Math.max(0.0, kPredicted[j] * xj / sumKx); - // Only update if we have vapor phase access - try { - fluid.getPhase(0).getComponent(j).setx(newYj); - } catch (Exception ex) { - // If composition update fails, skip this component - logger.debug("Inner loop: could not update y[{}] on tray {}", j, i); - } - } - } + double dfdt = 0.0; + for (int j = 0; j < nc; j++) { + double xj = fluid.getPhase(1).getComponent(j).getx(); + dfdt += -model.coeffB[i][j] / (trayTemp * trayTemp) * kPredicted[j] * xj; + } + + double correction = 0.0; + if (Math.abs(dfdt) > 1e-15) { + correction = -(sumKx - 1.0) / dfdt; + // Safeguard: limit step size + correction = Math.max(-15.0, Math.min(15.0, correction)); + } + + double newTemp = trayTemp + effectiveRelaxation * correction; + // Ensure temperature stays positive + newTemp = Math.max(50.0, newTemp); + tempResidual += Math.abs(newTemp - trayTemp); + trays.get(i).setTemperature(newTemp); + + // Update vapor compositions: y_j = K_j * x_j / sum(K*x) + // (normalized to ensure summation) + if (sumKx > 1e-10) { + for (int j = 0; j < nc; j++) { + double xj = fluid.getPhase(1).getComponent(j).getx(); + double newYj = Math.max(0.0, kPredicted[j] * xj / sumKx); + // Only update if we have vapor phase access + try { + fluid.getPhase(0).getComponent(j).setx(newYj); + } catch (Exception ex) { + // If composition update fails, skip this component + logger.debug("Inner loop: could not update y[{}] on tray {}", j, i); + } + } + } } } @@ -6105,7 +6256,7 @@ public void runBroyden(UUID id) { int baseIterationLimit = computeIterationLimit(); int iterationLimit = baseIterationLimit; int polishIterationLimit = baseIterationLimit - + Math.max(POLISH_ITERATION_MARGIN, (int) Math.ceil(0.5 * numberOfTrays)); + + Math.max(POLISH_ITERATION_MARGIN, (int) Math.ceil(0.5 * numberOfTrays)); double baseTempTolerance = getEffectiveTemperatureTolerance(); double baseMassTolerance = getEffectiveMassBalanceTolerance(); double baseEnergyTolerance = getEffectiveEnthalpyBalanceTolerance(); @@ -6121,13 +6272,13 @@ public void runBroyden(UUID id) { iter++; err = 0.0; for (int i = 0; i < numberOfTrays; i++) { - oldtemps[i] = trays.get(i).getThermoSystem().getTemperature(); + oldtemps[i] = trays.get(i).getThermoSystem().getTemperature(); } for (int i = firstFeedTrayNumber; i > 1; i--) { - int replaceStream1 = trays.get(i - 1).getNumberOfInputStreams() - 1; - trays.get(i - 1).replaceStream(replaceStream1, trays.get(i).getLiquidOutStream()); - trays.get(i - 1).run(id); + int replaceStream1 = trays.get(i - 1).getNumberOfInputStreams() - 1; + trays.get(i - 1).replaceStream(replaceStream1, trays.get(i).getLiquidOutStream()); + trays.get(i - 1).run(id); } int streamNumb = trays.get(0).getNumberOfInputStreams() - 1; @@ -6135,76 +6286,78 @@ public void runBroyden(UUID id) { trays.get(0).run(id); for (int i = 1; i <= numberOfTrays - 1; i++) { - int replaceStream = trays.get(i).getNumberOfInputStreams() - 2; - if (i == (numberOfTrays - 1)) { - replaceStream = trays.get(i).getNumberOfInputStreams() - 1; - } - trays.get(i).replaceStream(replaceStream, trays.get(i - 1).getGasOutStream()); - trays.get(i).run(id); + int replaceStream = trays.get(i).getNumberOfInputStreams() - 2; + if (i == (numberOfTrays - 1)) { + replaceStream = trays.get(i).getNumberOfInputStreams() - 1; + } + trays.get(i).replaceStream(replaceStream, trays.get(i - 1).getGasOutStream()); + trays.get(i).run(id); } for (int i = numberOfTrays - 2; i >= firstFeedTrayNumber; i--) { - int replaceStream = trays.get(i).getNumberOfInputStreams() - 1; - trays.get(i).replaceStream(replaceStream, trays.get(i + 1).getLiquidOutStream()); - trays.get(i).run(id); + int replaceStream = trays.get(i).getNumberOfInputStreams() - 1; + trays.get(i).replaceStream(replaceStream, trays.get(i + 1).getLiquidOutStream()); + trays.get(i).run(id); } for (int i = 0; i < numberOfTrays; i++) { - delta[i] = trays.get(i).getThermoSystem().getTemperature() - oldtemps[i]; - double newTemp = oldtemps[i] + delta[i] + 0.3 * (delta[i] - oldDelta[i]); - trays.get(i).setTemperature(newTemp); - oldDelta[i] = delta[i]; - err += Math.abs(newTemp - oldtemps[i]); + delta[i] = trays.get(i).getThermoSystem().getTemperature() - oldtemps[i]; + double newTemp = oldtemps[i] + delta[i] + 0.3 * (delta[i] - oldDelta[i]); + trays.get(i).setTemperature(newTemp); + oldDelta[i] = delta[i]; + err += Math.abs(newTemp - oldtemps[i]); } - boolean evaluateBalances = shouldEvaluateBalances(iter, iterationLimit, polishing, err, baseTempTolerance, - balanceCheckStride); + boolean evaluateBalances = shouldEvaluateBalances(iter, iterationLimit, polishing, err, + baseTempTolerance, balanceCheckStride); if (evaluateBalances || !massEnergyEvaluated) { - massErr = getMassBalanceError(); - energyErr = getEnergyBalanceError(); - massEnergyEvaluated = true; + massErr = getMassBalanceError(); + energyErr = getEnergyBalanceError(); + massEnergyEvaluated = true; } - logger - .debug("error iteration = " + iter + " err = " + err + " massErr= " + massErr + " energyErr= " + energyErr); + logger.debug("error iteration = " + iter + " err = " + err + " massErr= " + massErr + + " energyErr= " + energyErr); boolean improved = err < monotonicBaseline; monotonicBaseline = err; if (!improved) { - break; + break; } boolean energyWithinBase = !enforceEnergyBalanceTolerance || energyErr <= baseEnergyTolerance; - boolean withinBaseTolerance = err <= baseTempTolerance && massErr <= baseMassTolerance && energyWithinBase; + boolean withinBaseTolerance = + err <= baseTempTolerance && massErr <= baseMassTolerance && energyWithinBase; if (withinBaseTolerance) { - boolean energyPolishingAvailable = enforceEnergyBalanceTolerance && polishEnergyTolerance < baseEnergyTolerance; - boolean polishingAvailable = polishMassTolerance < baseMassTolerance || energyPolishingAvailable - || polishTempTolerance < baseTempTolerance; + boolean energyPolishingAvailable = + enforceEnergyBalanceTolerance && polishEnergyTolerance < baseEnergyTolerance; + boolean polishingAvailable = polishMassTolerance < baseMassTolerance + || energyPolishingAvailable || polishTempTolerance < baseTempTolerance; - if (!polishing && polishingAvailable - && (massErr > polishMassTolerance || (energyPolishingAvailable && energyErr > polishEnergyTolerance))) { - polishing = true; - iterationLimit = Math.max(iterationLimit, polishIterationLimit); - monotonicBaseline = Double.POSITIVE_INFINITY; - continue; - } + if (!polishing && polishingAvailable && (massErr > polishMassTolerance + || (energyPolishingAvailable && energyErr > polishEnergyTolerance))) { + polishing = true; + iterationLimit = Math.max(iterationLimit, polishIterationLimit); + monotonicBaseline = Double.POSITIVE_INFINITY; + continue; + } - double tempTarget = polishing ? polishTempTolerance : baseTempTolerance; - double massTarget = polishing ? polishMassTolerance : baseMassTolerance; - double energyTarget = polishing ? polishEnergyTolerance : baseEnergyTolerance; - boolean energyWithinTarget = !enforceEnergyBalanceTolerance || energyErr <= energyTarget; + double tempTarget = polishing ? polishTempTolerance : baseTempTolerance; + double massTarget = polishing ? polishMassTolerance : baseMassTolerance; + double energyTarget = polishing ? polishEnergyTolerance : baseEnergyTolerance; + boolean energyWithinTarget = !enforceEnergyBalanceTolerance || energyErr <= energyTarget; - if (err <= tempTarget && massErr <= massTarget && energyWithinTarget) { - break; - } + if (err <= tempTarget && massErr <= massTarget && energyWithinTarget) { + break; + } } } double totalFeedFlowBroyden = 0.0; for (List feeds : feedStreams.values()) { for (StreamInterface f : feeds) { - totalFeedFlowBroyden += Math.abs(f.getFlowRate("kg/hr")); + totalFeedFlowBroyden += Math.abs(f.getFlowRate("kg/hr")); } } @@ -6217,9 +6370,10 @@ public void runBroyden(UUID id) { * Solve the column using Wegstein acceleration of successive substitution. * *

- * Wegstein's method uses two consecutive fixed-point iterates to extrapolate a better estimate. For temperatures on - * each tray the acceleration factor q is computed from the slope of the fixed-point map: q = s / (s - 1) where s = - * (x_{k} - x_{k-1}) / (g(x_{k}) - g(x_{k-1})). The factor is bounded to [-5, 0] to prevent divergence. + * Wegstein's method uses two consecutive fixed-point iterates to extrapolate a better estimate. + * For temperatures on each tray the acceleration factor q is computed from the slope of the + * fixed-point map: q = s / (s - 1) where s = (x_{k} - x_{k-1}) / (g(x_{k}) - g(x_{k-1})). The + * factor is bounded to [-5, 0] to prevent divergence. *

* * @param id calculation identifier @@ -6271,7 +6425,7 @@ void solveWegstein(UUID id) { int overflowIncrement = Math.max(3, (int) Math.ceil(0.5 * numberOfTrays)); int overflowBand = Math.max(overflowIncrement, numberOfTrays); int maxIterationLimit = Math.max(iterationLimit, maxNumberOfIterations) - + overflowBand * ITERATION_OVERFLOW_MULTIPLIER; + + overflowBand * ITERATION_OVERFLOW_MULTIPLIER; double baseTempTolerance = getEffectiveTemperatureTolerance(); double baseMassTolerance = getEffectiveMassBalanceTolerance(); double baseEnergyTolerance = getEffectiveEnthalpyBalanceTolerance(); @@ -6285,15 +6439,15 @@ void solveWegstein(UUID id) { double[] xk = new double[numberOfTrays]; for (int i = 0; i < numberOfTrays; i++) { - xk[i] = trays.get(i).getThermoSystem().getTemperature(); + xk[i] = trays.get(i).getThermoSystem().getTemperature(); } // Standard tray sweep (same as direct substitution) for (int i = firstFeedTrayNumber; i > 1; i--) { - int replaceStream = trays.get(i - 1).getNumberOfInputStreams() - 1; - trays.get(i - 1).replaceStream(replaceStream, trays.get(i).getLiquidOutStream()); - trays.get(i - 1).run(id); - applyMurphreeCorrection(i - 1); + int replaceStream = trays.get(i - 1).getNumberOfInputStreams() - 1; + trays.get(i - 1).replaceStream(replaceStream, trays.get(i).getLiquidOutStream()); + trays.get(i - 1).run(id); + applyMurphreeCorrection(i - 1); } int streamNumb = trays.get(0).getNumberOfInputStreams() - 1; @@ -6302,86 +6456,87 @@ void solveWegstein(UUID id) { applyMurphreeCorrection(0); for (int i = 1; i <= numberOfTrays - 1; i++) { - int replaceStream = trays.get(i).getNumberOfInputStreams() - 2; - if (i == (numberOfTrays - 1)) { - replaceStream = trays.get(i).getNumberOfInputStreams() - 1; - } - trays.get(i).replaceStream(replaceStream, trays.get(i - 1).getGasOutStream()); - trays.get(i).run(id); - applyMurphreeCorrection(i); + int replaceStream = trays.get(i).getNumberOfInputStreams() - 2; + if (i == (numberOfTrays - 1)) { + replaceStream = trays.get(i).getNumberOfInputStreams() - 1; + } + trays.get(i).replaceStream(replaceStream, trays.get(i - 1).getGasOutStream()); + trays.get(i).run(id); + applyMurphreeCorrection(i); } for (int i = numberOfTrays - 2; i >= firstFeedTrayNumber; i--) { - int replaceStream = trays.get(i).getNumberOfInputStreams() - 1; - trays.get(i).replaceStream(replaceStream, trays.get(i + 1).getLiquidOutStream()); - trays.get(i).run(id); - applyMurphreeCorrection(i); + int replaceStream = trays.get(i).getNumberOfInputStreams() - 1; + trays.get(i).replaceStream(replaceStream, trays.get(i + 1).getLiquidOutStream()); + trays.get(i).run(id); + applyMurphreeCorrection(i); } // Compute g(x_k) = direct substitution output double[] gxk = new double[numberOfTrays]; for (int i = 0; i < numberOfTrays; i++) { - gxk[i] = trays.get(i).getThermoSystem().getTemperature(); + gxk[i] = trays.get(i).getThermoSystem().getTemperature(); } // Apply Wegstein acceleration after warm-up period double temperatureResidual = 0.0; for (int i = 0; i < numberOfTrays; i++) { - double newTemp; - if (wegsteinReady && iter > warmUpIterations) { - double denominator = (xk[i] - prevInput[i]); - if (Math.abs(denominator) > 1.0e-10) { - double s = (gxk[i] - prevOutput[i]) / denominator; - double q = s / (s - 1.0); - // Bound q conservatively: [-2, 0] to avoid oscillation - q = Math.max(-2.0, Math.min(0.0, q)); - double candidate = (1.0 - q) * gxk[i] + q * xk[i]; - // Safeguard: limit step size to avoid overshooting - double maxStep = 30.0; - if (Math.abs(candidate - xk[i]) > maxStep) { - candidate = xk[i] + Math.signum(candidate - xk[i]) * maxStep; - } - newTemp = candidate; - } else { - newTemp = gxk[i]; - } - } else { - newTemp = gxk[i]; // Direct substitution during warm-up - } - - // Store for next iteration - prevOutput[i] = gxk[i]; - prevInput[i] = xk[i]; - - trays.get(i).setTemperature(newTemp); - temperatureResidual += Math.abs(newTemp - xk[i]); + double newTemp; + if (wegsteinReady && iter > warmUpIterations) { + double denominator = (xk[i] - prevInput[i]); + if (Math.abs(denominator) > 1.0e-10) { + double s = (gxk[i] - prevOutput[i]) / denominator; + double q = s / (s - 1.0); + // Bound q conservatively: [-2, 0] to avoid oscillation + q = Math.max(-2.0, Math.min(0.0, q)); + double candidate = (1.0 - q) * gxk[i] + q * xk[i]; + // Safeguard: limit step size to avoid overshooting + double maxStep = 30.0; + if (Math.abs(candidate - xk[i]) > maxStep) { + candidate = xk[i] + Math.signum(candidate - xk[i]) * maxStep; + } + newTemp = candidate; + } else { + newTemp = gxk[i]; + } + } else { + newTemp = gxk[i]; // Direct substitution during warm-up + } + + // Store for next iteration + prevOutput[i] = gxk[i]; + prevInput[i] = xk[i]; + + trays.get(i).setTemperature(newTemp); + temperatureResidual += Math.abs(newTemp - xk[i]); } wegsteinReady = true; temperatureResidual /= Math.max(1, numberOfTrays); err = temperatureResidual; - boolean evaluateBalances = shouldEvaluateBalances(iter, iterationLimit, false, err, baseTempTolerance, - balanceCheckStride); + boolean evaluateBalances = shouldEvaluateBalances(iter, iterationLimit, false, err, + baseTempTolerance, balanceCheckStride); if (evaluateBalances || !massEnergyEvaluated) { - massErr = getMassBalanceError(); - energyErr = getEnergyBalanceError(); - massEnergyEvaluated = true; + massErr = getMassBalanceError(); + energyErr = getEnergyBalanceError(); + massEnergyEvaluated = true; } if (convergenceHistory != null) { - recordConvergence(new double[] { err, massErr, energyErr }); + recordConvergence(new double[] {err, massErr, energyErr}); } - logger.debug("Wegstein iteration {} tempErr={} massErr={} energyErr={}", iter, err, massErr, energyErr); + logger.debug("Wegstein iteration {} tempErr={} massErr={} energyErr={}", iter, err, massErr, + energyErr); boolean energyWithinBase = !enforceEnergyBalanceTolerance || energyErr <= baseEnergyTolerance; if (err <= baseTempTolerance && massErr <= baseMassTolerance && energyWithinBase) { - break; + break; } if (iter >= iterationLimit && err > baseTempTolerance && iterationLimit < maxIterationLimit) { - iterationLimit = Math.min(maxIterationLimit, iterationLimit + overflowIncrement); - continue; + iterationLimit = Math.min(maxIterationLimit, iterationLimit + overflowIncrement); + continue; } } @@ -6390,9 +6545,9 @@ void solveWegstein(UUID id) { energyErr = getEnergyBalanceError(); if (!Double.isFinite(err) || !Double.isFinite(energyErr) || err > baseTempTolerance - || massErr > baseMassTolerance) { - logger.warn("Wegstein did not converge cleanly for column {}. Falling back to direct " + "substitution.", - getName()); + || massErr > baseMassTolerance) { + logger.warn("Wegstein did not converge cleanly for column {}. Falling back to direct " + + "substitution.", getName()); solveDirectFallbackFromFreshInitialization(id); return; } @@ -6406,7 +6561,8 @@ void solveWegstein(UUID id) { * @param id calculation identifier * @param firstFeedTrayNumber index of the lowest feed tray */ - private void synchronizeTrayStreamsAfterAcceleratedTemperatureUpdate(UUID id, int firstFeedTrayNumber) { + private void synchronizeTrayStreamsAfterAcceleratedTemperatureUpdate(UUID id, + int firstFeedTrayNumber) { StreamInterface[] previousGasStreams = new StreamInterface[numberOfTrays]; StreamInterface[] previousLiquidStreams = new StreamInterface[numberOfTrays]; for (int trayIndex = 0; trayIndex < numberOfTrays; trayIndex++) { @@ -6430,8 +6586,8 @@ private void solveDampedFallbackFromFreshInitialization(UUID id) { } /** - * Rerun direct substitution from a fresh tray initialization after a guarded accelerator is slower or less stable - * than the base fixed-point method. + * Rerun direct substitution from a fresh tray initialization after a guarded accelerator is + * slower or less stable than the base fixed-point method. * * @param id calculation identifier */ @@ -6450,7 +6606,8 @@ private void solveDirectFallbackFromFreshInitialization(UUID id) { * @param exception exception that caused the accelerator to be rejected */ void solveDampedFallbackAfterAcceleratorFailure(UUID id, RuntimeException exception) { - logger.warn("Accelerated solver failed for column {}. Falling back to damped substitution.", getName(), exception); + logger.warn("Accelerated solver failed for column {}. Falling back to damped substitution.", + getName(), exception); solveDampedFallbackFromFreshInitialization(id); } @@ -6461,8 +6618,8 @@ void solveDampedFallbackAfterAcceleratorFailure(UUID id, RuntimeException except * @param reason reason the accelerator result was rejected */ void solveDampedFallbackAfterRejectedAccelerator(UUID id, String reason) { - logger.warn("Accelerated solver result rejected for column {}: {}. Falling back to damped " + "substitution.", - getName(), reason); + logger.warn("Accelerated solver result rejected for column {}: {}. Falling back to damped " + + "substitution.", getName(), reason); solveDampedFallbackFromFreshInitialization(id); } @@ -6497,10 +6654,11 @@ private boolean useGuardedNewtonFallback() { * Solve the column using a sum-rates tearing method. * *

- * The sum-rates method adjusts tray liquid flow rates based on the ratio of computed to assumed total flow leaving - * each tray. This is effective for absorber and stripper columns where the temperature profile is relatively flat. - * The method alternates between: (1) bubble-point temperature calculations on each tray, and (2) flow rate - * corrections using the sum-rates formula of Burningham and Otto (1967). + * The sum-rates method adjusts tray liquid flow rates based on the ratio of computed to assumed + * total flow leaving each tray. This is effective for absorber and stripper columns where the + * temperature profile is relatively flat. The method alternates between: (1) bubble-point + * temperature calculations on each tray, and (2) flow rate corrections using the sum-rates + * formula of Burningham and Otto (1967). *

* * @param id calculation identifier @@ -6514,9 +6672,11 @@ void solveSumRates(UUID id) { if (useGuardedSumRatesFallback()) { markSolverTypeUsed(SolverType.DAMPED_SUBSTITUTION); solveDampedSubstitution(id); - if (lastSolveStatus == SolveStatus.RIGOROUS_CONVERGED || lastSolveStatus == SolveStatus.RECONCILED_PRODUCTS) { - setLastSolveStatus(lastSolveStatus, - "Sum-rates is guarded to damped substitution for columns with condenser/reboiler " + "energy equipment"); + if (lastSolveStatus == SolveStatus.RIGOROUS_CONVERGED + || lastSolveStatus == SolveStatus.RECONCILED_PRODUCTS) { + setLastSolveStatus(lastSolveStatus, + "Sum-rates is guarded to damped substitution for columns with condenser/reboiler " + + "energy equipment"); } return; } @@ -6556,15 +6716,15 @@ void solveSumRates(UUID id) { iter++; for (int i = 0; i < numberOfTrays; i++) { - oldtemps[i] = trays.get(i).getThermoSystem().getTemperature(); + oldtemps[i] = trays.get(i).getThermoSystem().getTemperature(); } // Standard tray-by-tray sweep for (int i = firstFeedTrayNumber; i > 1; i--) { - int replaceStream = trays.get(i - 1).getNumberOfInputStreams() - 1; - trays.get(i - 1).replaceStream(replaceStream, trays.get(i).getLiquidOutStream()); - trays.get(i - 1).run(id); - applyMurphreeCorrection(i - 1); + int replaceStream = trays.get(i - 1).getNumberOfInputStreams() - 1; + trays.get(i - 1).replaceStream(replaceStream, trays.get(i).getLiquidOutStream()); + trays.get(i - 1).run(id); + applyMurphreeCorrection(i - 1); } int streamNumb = trays.get(0).getNumberOfInputStreams() - 1; @@ -6573,20 +6733,20 @@ void solveSumRates(UUID id) { applyMurphreeCorrection(0); for (int i = 1; i <= numberOfTrays - 1; i++) { - int replaceStream = trays.get(i).getNumberOfInputStreams() - 2; - if (i == (numberOfTrays - 1)) { - replaceStream = trays.get(i).getNumberOfInputStreams() - 1; - } - trays.get(i).replaceStream(replaceStream, trays.get(i - 1).getGasOutStream()); - trays.get(i).run(id); - applyMurphreeCorrection(i); + int replaceStream = trays.get(i).getNumberOfInputStreams() - 2; + if (i == (numberOfTrays - 1)) { + replaceStream = trays.get(i).getNumberOfInputStreams() - 1; + } + trays.get(i).replaceStream(replaceStream, trays.get(i - 1).getGasOutStream()); + trays.get(i).run(id); + applyMurphreeCorrection(i); } for (int i = numberOfTrays - 2; i >= firstFeedTrayNumber; i--) { - int replaceStream = trays.get(i).getNumberOfInputStreams() - 1; - trays.get(i).replaceStream(replaceStream, trays.get(i + 1).getLiquidOutStream()); - trays.get(i).run(id); - applyMurphreeCorrection(i); + int replaceStream = trays.get(i).getNumberOfInputStreams() - 1; + trays.get(i).replaceStream(replaceStream, trays.get(i + 1).getLiquidOutStream()); + trays.get(i).run(id); + applyMurphreeCorrection(i); } // Sum-rates flow correction: adjust tray temperatures with flow-weighted @@ -6594,17 +6754,17 @@ void solveSumRates(UUID id) { double totalFlowRatio = 0.0; int countTrays = 0; for (int i = 0; i < numberOfTrays; i++) { - double vaporOut = trays.get(i).getGasOutStream().getFlowRate("kg/hr"); - double liquidOut = trays.get(i).getLiquidOutStream().getFlowRate("kg/hr"); - double totalOut = vaporOut + liquidOut; - double totalIn = 0.0; - for (int j = 0; j < trays.get(i).getNumberOfInputStreams(); j++) { - totalIn += trays.get(i).getStream(j).getFluid().getFlowRate("kg/hr"); - } - if (totalIn > 1e-12) { - totalFlowRatio += totalOut / totalIn; - countTrays++; - } + double vaporOut = trays.get(i).getGasOutStream().getFlowRate("kg/hr"); + double liquidOut = trays.get(i).getLiquidOutStream().getFlowRate("kg/hr"); + double totalOut = vaporOut + liquidOut; + double totalIn = 0.0; + for (int j = 0; j < trays.get(i).getNumberOfInputStreams(); j++) { + totalIn += trays.get(i).getStream(j).getFluid().getFlowRate("kg/hr"); + } + if (totalIn > 1e-12) { + totalFlowRatio += totalOut / totalIn; + countTrays++; + } } double avgFlowRatio = countTrays > 0 ? totalFlowRatio / countTrays : 1.0; double flowCorrection = Math.max(0.5, Math.min(1.5, 1.0 / avgFlowRatio)); @@ -6613,31 +6773,32 @@ void solveSumRates(UUID id) { double effectiveRelaxation = relaxation * flowCorrection; effectiveRelaxation = Math.max(minTemperatureRelaxation, Math.min(1.0, effectiveRelaxation)); for (int i = 0; i < numberOfTrays; i++) { - double updated = trays.get(i).getThermoSystem().getTemperature(); - double newTemp = oldtemps[i] + effectiveRelaxation * (updated - oldtemps[i]); - trays.get(i).setTemperature(newTemp); - temperatureResidual += Math.abs(newTemp - oldtemps[i]); + double updated = trays.get(i).getThermoSystem().getTemperature(); + double newTemp = oldtemps[i] + effectiveRelaxation * (updated - oldtemps[i]); + trays.get(i).setTemperature(newTemp); + temperatureResidual += Math.abs(newTemp - oldtemps[i]); } temperatureResidual /= Math.max(1, numberOfTrays); err = temperatureResidual; - boolean evaluateBalances = shouldEvaluateBalances(iter, iterationLimit, false, err, baseTempTolerance, - balanceCheckStride); + boolean evaluateBalances = shouldEvaluateBalances(iter, iterationLimit, false, err, + baseTempTolerance, balanceCheckStride); if (evaluateBalances || !massEnergyEvaluated) { - massErr = getMassBalanceError(); - energyErr = getEnergyBalanceError(); - massEnergyEvaluated = true; + massErr = getMassBalanceError(); + energyErr = getEnergyBalanceError(); + massEnergyEvaluated = true; } if (convergenceHistory != null) { - recordConvergence(new double[] { err, massErr, energyErr }); + recordConvergence(new double[] {err, massErr, energyErr}); } - logger.debug("sum-rates iteration {} tempErr={} massErr={} energyErr={}", iter, err, massErr, energyErr); + logger.debug("sum-rates iteration {} tempErr={} massErr={} energyErr={}", iter, err, massErr, + energyErr); boolean energyWithinBase = !enforceEnergyBalanceTolerance || energyErr <= baseEnergyTolerance; if (err <= baseTempTolerance && massErr <= baseMassTolerance && energyWithinBase) { - break; + break; } } @@ -6648,20 +6809,21 @@ void solveSumRates(UUID id) { * Solve the column using a Newton-Raphson simultaneous temperature correction method. * *

- * This is inspired by the Naphtali-Sandholm (1971) approach of solving MESH equations simultaneously, adapted to - * NeqSim's tray-by-tray flash infrastructure. The method treats the N tray temperatures as the independent variables. - * A residual vector is formed by running full tray sweeps and measuring the temperature discrepancy each tray - * exhibits after equilibrium. The Jacobian is computed by finite-difference perturbation of each tray temperature. + * This is inspired by the Naphtali-Sandholm (1971) approach of solving MESH equations + * simultaneously, adapted to NeqSim's tray-by-tray flash infrastructure. The method treats the N + * tray temperatures as the independent variables. A residual vector is formed by running full + * tray sweeps and measuring the temperature discrepancy each tray exhibits after equilibrium. The + * Jacobian is computed by finite-difference perturbation of each tray temperature. *

* *

* Key features: *

    - *
  • Simultaneous correction: all tray temperatures are updated together using a dense N×N Jacobian solved by - * Gaussian elimination with partial pivoting.
  • + *
  • Simultaneous correction: all tray temperatures are updated together using a dense N×N + * Jacobian solved by Gaussian elimination with partial pivoting.
  • *
  • Line search: the full Newton step is scaled back if it increases residuals.
  • - *
  • Warm-up: a few direct-substitution iterations are performed first to get close to the solution basin where - * Newton convergence is quadratic.
  • + *
  • Warm-up: a few direct-substitution iterations are performed first to get close to the + * solution basin where Newton convergence is quadratic.
  • *
* * @param id calculation identifier @@ -6699,7 +6861,8 @@ void solveNewton(UUID id) { double baseEnergyTolerance = getEffectiveEnthalpyBalanceTolerance(); int baseIterationLimit = computeIterationLimit(); int iterationLimit = Math.max(baseIterationLimit, maxNumberOfIterations); - int maxIterationLimit = iterationLimit + Math.max(numberOfTrays, 3) * ITERATION_OVERFLOW_MULTIPLIER; + int maxIterationLimit = + iterationLimit + Math.max(numberOfTrays, 3) * ITERATION_OVERFLOW_MULTIPLIER; // Warm-up: run a few direct substitution iterations to establish a reasonable // profile @@ -6716,14 +6879,14 @@ void solveNewton(UUID id) { err = tempRes; if (convergenceHistory != null) { - massErr = getMassBalanceError(); - energyErr = getEnergyBalanceError(); - recordConvergence(new double[] { err, massErr, energyErr }); + massErr = getMassBalanceError(); + energyErr = getEnergyBalanceError(); + recordConvergence(new double[] {err, massErr, energyErr}); } logger.debug("newton warm-up iteration {} tempErr={}", iter, err); if (err < baseTempTolerance) { - break; + break; } } @@ -6747,20 +6910,20 @@ void solveNewton(UUID id) { // Save current temperatures for (int i = 0; i < numberOfTrays; i++) { - temperatures[i] = trays.get(i).getThermoSystem().getTemperature(); + temperatures[i] = trays.get(i).getThermoSystem().getTemperature(); } // Compute base residuals: run a full sweep at current temperatures, // residual = (post-sweep temperature) - (pre-sweep temperature) performFullTraySweep(id, firstFeedTrayNumber, previousGasStreams, previousLiquidStreams, 1.0); for (int i = 0; i < numberOfTrays; i++) { - residuals[i] = trays.get(i).getThermoSystem().getTemperature() - temperatures[i]; + residuals[i] = trays.get(i).getThermoSystem().getTemperature() - temperatures[i]; } // Check if already converged double normRes = 0.0; for (int i = 0; i < numberOfTrays; i++) { - normRes += Math.abs(residuals[i]); + normRes += Math.abs(residuals[i]); } normRes /= Math.max(1, numberOfTrays); err = normRes; @@ -6769,14 +6932,15 @@ void solveNewton(UUID id) { energyErr = getEnergyBalanceError(); if (convergenceHistory != null) { - recordConvergence(new double[] { err, massErr, energyErr }); + recordConvergence(new double[] {err, massErr, energyErr}); } - logger.debug("newton iteration {} tempErr={} massErr={} energyErr={}", iter, err, massErr, energyErr); + logger.debug("newton iteration {} tempErr={} massErr={} energyErr={}", iter, err, massErr, + energyErr); boolean energyOk = !enforceEnergyBalanceTolerance || energyErr <= baseEnergyTolerance; if (err <= baseTempTolerance && massErr <= baseMassTolerance && energyOk) { - break; + break; } // Compute Jacobian by finite differences with banded structure @@ -6788,9 +6952,9 @@ void solveNewton(UUID id) { // Zero the Jacobian — entries outside the band stay zero for (int i = 0; i < numberOfTrays; i++) { - for (int jj = 0; jj < numberOfTrays; jj++) { - jacobian[i][jj] = 0.0; - } + for (int jj = 0; jj < numberOfTrays; jj++) { + jacobian[i][jj] = 0.0; + } } // Determine which columns actually need perturbation. @@ -6802,54 +6966,56 @@ void solveNewton(UUID id) { boolean[] needsPerturb = new boolean[numberOfTrays]; double residualSkipThreshold = 0.5 * baseTempTolerance; for (int j = 0; j < numberOfTrays; j++) { - if (numberOfTrays <= 6) { - needsPerturb[j] = true; - } else { - int rowStart = Math.max(0, j - halfBand); - int rowEnd = Math.min(numberOfTrays - 1, j + halfBand); - double bandResidualMax = 0.0; - for (int i = rowStart; i <= rowEnd; i++) { - double r = Math.abs(residuals[i]); - if (r > bandResidualMax) { - bandResidualMax = r; - } - } - // Always perturb the diagonal column itself, even if its own residual is - // tight — the rest of the band may still couple through off-diagonal entries - // on the next iteration. The skip only fires when the entire band is tight. - needsPerturb[j] = bandResidualMax > residualSkipThreshold; - } + if (numberOfTrays <= 6) { + needsPerturb[j] = true; + } else { + int rowStart = Math.max(0, j - halfBand); + int rowEnd = Math.min(numberOfTrays - 1, j + halfBand); + double bandResidualMax = 0.0; + for (int i = rowStart; i <= rowEnd; i++) { + double r = Math.abs(residuals[i]); + if (r > bandResidualMax) { + bandResidualMax = r; + } + } + // Always perturb the diagonal column itself, even if its own residual is + // tight — the rest of the band may still couple through off-diagonal entries + // on the next iteration. The skip only fires when the entire band is tight. + needsPerturb[j] = bandResidualMax > residualSkipThreshold; + } } for (int j = 0; j < numberOfTrays; j++) { - if (!needsPerturb[j]) { - continue; - } - - // Reset temperatures to base state - for (int i = 0; i < numberOfTrays; i++) { - trays.get(i).setTemperature(temperatures[i]); - trays.get(i).getThermoSystem().setTemperature(temperatures[i]); - } - - // Perturb tray j - double pertT = temperatures[j] + perturbation; - trays.get(j).setTemperature(pertT); - trays.get(j).getThermoSystem().setTemperature(pertT); - - // Run sweep with perturbed temperature - performFullTraySweep(id, firstFeedTrayNumber, previousGasStreams, previousLiquidStreams, 1.0); - - // Compute perturbed residuals — only for rows within band of column j - int rowStart = numberOfTrays <= 6 ? 0 : Math.max(0, j - halfBand); - int rowEnd = numberOfTrays <= 6 ? numberOfTrays - 1 : Math.min(numberOfTrays - 1, j + halfBand); - for (int i = rowStart; i <= rowEnd; i++) { - double pertResidual = trays.get(i).getThermoSystem().getTemperature() - temperatures[i]; - if (j == i) { - pertResidual = trays.get(i).getThermoSystem().getTemperature() - pertT; - } - jacobian[i][j] = (pertResidual - residuals[i]) / perturbation; - } + if (!needsPerturb[j]) { + continue; + } + + // Reset temperatures to base state + for (int i = 0; i < numberOfTrays; i++) { + trays.get(i).setTemperature(temperatures[i]); + trays.get(i).getThermoSystem().setTemperature(temperatures[i]); + } + + // Perturb tray j + double pertT = temperatures[j] + perturbation; + trays.get(j).setTemperature(pertT); + trays.get(j).getThermoSystem().setTemperature(pertT); + + // Run sweep with perturbed temperature + performFullTraySweep(id, firstFeedTrayNumber, previousGasStreams, previousLiquidStreams, + 1.0); + + // Compute perturbed residuals — only for rows within band of column j + int rowStart = numberOfTrays <= 6 ? 0 : Math.max(0, j - halfBand); + int rowEnd = + numberOfTrays <= 6 ? numberOfTrays - 1 : Math.min(numberOfTrays - 1, j + halfBand); + for (int i = rowStart; i <= rowEnd; i++) { + double pertResidual = trays.get(i).getThermoSystem().getTemperature() - temperatures[i]; + if (j == i) { + pertResidual = trays.get(i).getThermoSystem().getTemperature() - pertT; + } + jacobian[i][j] = (pertResidual - residuals[i]) / perturbation; + } } // For the Newton correction, we want to solve: J * deltaT = -residuals @@ -6871,76 +7037,80 @@ void solveNewton(UUID id) { // pivoting double[] rhs = new double[numberOfTrays]; for (int i = 0; i < numberOfTrays; i++) { - rhs[i] = -residuals[i]; + rhs[i] = -residuals[i]; } double[] deltaT = solveLinearSystem(jacobian, rhs); if (deltaT == null || !isFiniteVector(deltaT)) { - // Singular Jacobian — fall back to direct substitution step - logger.warn("Newton: singular Jacobian at iter {}, using direct substitution step", iter); - for (int i = 0; i < numberOfTrays; i++) { - trays.get(i).setTemperature(temperatures[i] + 0.5 * residuals[i]); - trays.get(i).getThermoSystem().setTemperature(temperatures[i] + 0.5 * residuals[i]); - } - // Reset damping memo: the Jacobian was bad, so prior step length is not a reliable hint. - lastSuccessfulStepLength = 1.0; - continue; + // Singular Jacobian — fall back to direct substitution step + logger.warn("Newton: singular Jacobian at iter {}, using direct substitution step", iter); + for (int i = 0; i < numberOfTrays; i++) { + trays.get(i).setTemperature(temperatures[i] + 0.5 * residuals[i]); + trays.get(i).getThermoSystem().setTemperature(temperatures[i] + 0.5 * residuals[i]); + } + // Reset damping memo: the Jacobian was bad, so prior step length is not a reliable hint. + lastSuccessfulStepLength = 1.0; + continue; } // Line search: try full Newton step, halve if residual increases. // Damping memo: start at min(1.0, 2.0 * lastSuccessful) to skip probes that // would predictably fail given prior nonlinearity. Periodically reset to 1.0 // so the algorithm can recover the full step when conditions improve. - double trialStart = (iter % LINESEARCH_RESET_PERIOD == 0) ? 1.0 : Math.min(1.0, 2.0 * lastSuccessfulStepLength); + double trialStart = (iter % LINESEARCH_RESET_PERIOD == 0) ? 1.0 + : Math.min(1.0, 2.0 * lastSuccessfulStepLength); double bestStepLength = trialStart; double bestNormRes = normRes; for (double stepLength = trialStart; stepLength >= 0.125; stepLength *= 0.5) { - // Apply trial step - for (int i = 0; i < numberOfTrays; i++) { - double newTemp = temperatures[i] + stepLength * deltaT[i]; - // Safeguard: keep temperatures reasonable - newTemp = Math.max(50.0, Math.min(1000.0, newTemp)); - trays.get(i).setTemperature(newTemp); - trays.get(i).getThermoSystem().setTemperature(newTemp); - } - - // Check trial step quality with a sweep - performFullTraySweep(id, firstFeedTrayNumber, previousGasStreams, previousLiquidStreams, 1.0); - - double trialNormRes = 0.0; - for (int i = 0; i < numberOfTrays; i++) { - double trialRes = trays.get(i).getThermoSystem().getTemperature() - temperatures[i] - stepLength * deltaT[i]; - trialNormRes += Math.abs(trialRes); - } - trialNormRes /= Math.max(1, numberOfTrays); - - if (trialNormRes < bestNormRes) { - bestStepLength = stepLength; - bestNormRes = trialNormRes; - break; // Accept first improving step - } + // Apply trial step + for (int i = 0; i < numberOfTrays; i++) { + double newTemp = temperatures[i] + stepLength * deltaT[i]; + // Safeguard: keep temperatures reasonable + newTemp = Math.max(50.0, Math.min(1000.0, newTemp)); + trays.get(i).setTemperature(newTemp); + trays.get(i).getThermoSystem().setTemperature(newTemp); + } + + // Check trial step quality with a sweep + performFullTraySweep(id, firstFeedTrayNumber, previousGasStreams, previousLiquidStreams, + 1.0); + + double trialNormRes = 0.0; + for (int i = 0; i < numberOfTrays; i++) { + double trialRes = trays.get(i).getThermoSystem().getTemperature() - temperatures[i] + - stepLength * deltaT[i]; + trialNormRes += Math.abs(trialRes); + } + trialNormRes /= Math.max(1, numberOfTrays); + + if (trialNormRes < bestNormRes) { + bestStepLength = stepLength; + bestNormRes = trialNormRes; + break; // Accept first improving step + } } // Apply the best step if (bestStepLength < 1.0) { - // Need to re-apply since the loop may have tried smaller steps - for (int i = 0; i < numberOfTrays; i++) { - double newTemp = temperatures[i] + bestStepLength * deltaT[i]; - newTemp = Math.max(50.0, Math.min(1000.0, newTemp)); - trays.get(i).setTemperature(newTemp); - trays.get(i).getThermoSystem().setTemperature(newTemp); - } + // Need to re-apply since the loop may have tried smaller steps + for (int i = 0; i < numberOfTrays; i++) { + double newTemp = temperatures[i] + bestStepLength * deltaT[i]; + newTemp = Math.max(50.0, Math.min(1000.0, newTemp)); + trays.get(i).setTemperature(newTemp); + trays.get(i).getThermoSystem().setTemperature(newTemp); + } } // Update damping memo for next iteration's line-search start point. lastSuccessfulStepLength = bestStepLength; - logger.debug("newton iteration {} step={} normRes={}->{}", iter, bestStepLength, normRes, bestNormRes); + logger.debug("newton iteration {} step={} normRes={}->{}", iter, bestStepLength, normRes, + bestNormRes); // Overflow: extend limit if not converged if (iter >= iterationLimit && err > baseTempTolerance && iterationLimit < maxIterationLimit) { - iterationLimit = Math.min(maxIterationLimit, iterationLimit + 3); + iterationLimit = Math.min(maxIterationLimit, iterationLimit + 3); } } @@ -6963,14 +7133,15 @@ void solveNewton(UUID id) { * @param previousLiquidStreams cached liquid streams from previous iteration (updated in-place) * @param relaxation relaxation factor for stream blending */ - private void performFullTraySweep(UUID id, int firstFeedTrayNumber, StreamInterface[] previousGasStreams, - StreamInterface[] previousLiquidStreams, double relaxation) { + private void performFullTraySweep(UUID id, int firstFeedTrayNumber, + StreamInterface[] previousGasStreams, StreamInterface[] previousLiquidStreams, + double relaxation) { // Downward liquid sweep: feed → reboiler for (int stage = firstFeedTrayNumber; stage >= 1; stage--) { int target = stage - 1; int replaceStream = trays.get(target).getNumberOfInputStreams() - 1; StreamInterface relaxedLiquid = applyRelaxation(previousLiquidStreams[stage], - trays.get(stage).getLiquidOutStream(), relaxation); + trays.get(stage).getLiquidOutStream(), relaxation); trays.get(target).replaceStream(replaceStream, relaxedLiquid); previousLiquidStreams[stage] = relaxedLiquid.clone(); trays.get(target).run(id); @@ -6981,10 +7152,10 @@ private void performFullTraySweep(UUID id, int firstFeedTrayNumber, StreamInterf for (int stage = 1; stage <= numberOfTrays - 1; stage++) { int replaceStream = trays.get(stage).getNumberOfInputStreams() - 2; if (stage == (numberOfTrays - 1)) { - replaceStream = trays.get(stage).getNumberOfInputStreams() - 1; + replaceStream = trays.get(stage).getNumberOfInputStreams() - 1; } StreamInterface relaxedGas = applyRelaxation(previousGasStreams[stage - 1], - trays.get(stage - 1).getGasOutStream(), relaxation); + trays.get(stage - 1).getGasOutStream(), relaxation); trays.get(stage).replaceStream(replaceStream, relaxedGas); previousGasStreams[stage - 1] = relaxedGas.clone(); trays.get(stage).run(id); @@ -6993,8 +7164,9 @@ private void performFullTraySweep(UUID id, int firstFeedTrayNumber, StreamInterf } /** - * Compute the average absolute temperature residual across all trays. The residual is the difference between the - * tray's stored temperature and its thermo system temperature after a flash. + * Compute the average absolute temperature residual across all trays. The residual is the + * difference between the tray's stored temperature and its thermo system temperature after a + * flash. * * @return average absolute temperature change per tray (K) */ @@ -7037,7 +7209,8 @@ private double[] captureStoredTrayTemperatures() { private double computeTemperatureResidual(double[] referenceTemperatures) { double residual = 0.0; for (int i = 0; i < numberOfTrays; i++) { - residual += Math.abs(trays.get(i).getThermoSystem().getTemperature() - referenceTemperatures[i]); + residual += + Math.abs(trays.get(i).getThermoSystem().getTemperature() - referenceTemperatures[i]); } return residual / Math.max(1, numberOfTrays); } @@ -7051,72 +7224,27 @@ private double computeTemperatureResidual(double[] referenceTemperatures) { */ private double[] solveLinearSystem(double[][] matrixA, double[] vectorB) { int n = vectorB.length; - // Create copies to avoid modifying the originals from the caller's perspective - double[][] a = new double[n][n]; - double[] b = new double[n]; for (int i = 0; i < n; i++) { - b[i] = vectorB[i]; - if (!Double.isFinite(b[i])) { - return null; + if (!Double.isFinite(vectorB[i])) { + return null; } for (int j = 0; j < n; j++) { - a[i][j] = matrixA[i][j]; - if (!Double.isFinite(a[i][j])) { - return null; - } + if (!Double.isFinite(matrixA[i][j])) { + return null; + } } } - // Forward elimination with partial pivoting - for (int k = 0; k < n; k++) { - // Find pivot - int maxRow = k; - double maxVal = Math.abs(a[k][k]); - for (int i = k + 1; i < n; i++) { - if (Math.abs(a[i][k]) > maxVal) { - maxVal = Math.abs(a[i][k]); - maxRow = i; - } - } - - if (!Double.isFinite(maxVal) || maxVal < 1e-30) { - return null; // Singular matrix - } - - // Swap rows - if (maxRow != k) { - double[] tempRow = a[k]; - a[k] = a[maxRow]; - a[maxRow] = tempRow; - double tempB = b[k]; - b[k] = b[maxRow]; - b[maxRow] = tempB; - } - - // Eliminate - for (int i = k + 1; i < n; i++) { - double factor = a[i][k] / a[k][k]; - for (int j = k + 1; j < n; j++) { - a[i][j] -= factor * a[k][j]; - } - b[i] -= factor * b[k]; - a[i][k] = 0.0; - } + double[] solution = new double[n]; + if (!LinearAlgebraOps.solveLinearSystem(matrixA, vectorB, solution)) { + return null; } - - // Back substitution - double[] x = new double[n]; - for (int i = n - 1; i >= 0; i--) { - double sum = b[i]; - for (int j = i + 1; j < n; j++) { - sum -= a[i][j] * x[j]; - } - x[i] = sum / a[i][i]; - if (!Double.isFinite(x[i])) { - return null; + for (int i = 0; i < n; i++) { + if (!Double.isFinite(solution[i])) { + return null; } } - return x; + return solution; } /** @@ -7128,7 +7256,7 @@ private double[] solveLinearSystem(double[][] matrixA, double[] vectorB) { private boolean isFiniteVector(double[] values) { for (int i = 0; i < values.length; i++) { if (!Double.isFinite(values[i])) { - return false; + return false; } } return true; @@ -7167,11 +7295,12 @@ public void setNumberOfTrays(int number) { int change = tempNumberOfTrays - oldNumberOfTrays; if (change > 0) { for (int i = 0; i < change; i++) { - trays.add(1, createMiddleTray("SimpleTray" + (oldNumberOfTrays + i + 1), oldNumberOfTrays + i)); + trays.add(1, + createMiddleTray("SimpleTray" + (oldNumberOfTrays + i + 1), oldNumberOfTrays + i)); } } else if (change < 0) { for (int i = 0; i > change; i--) { - trays.remove(1); + trays.remove(1); } } numberOfTrays = tempNumberOfTrays; @@ -7180,8 +7309,8 @@ public void setNumberOfTrays(int number) { } /** - * Create a middle tray (between reboiler and condenser). Sets the reactive flash flag when the column is in reactive - * mode and the tray index falls inside the reactive section. + * Create a middle tray (between reboiler and condenser). Sets the reactive flash flag when the + * column is in reactive mode and the tray index falls inside the reactive section. * * @param name the tray name * @param middleTrayIndex 0-based index among the middle trays (excluding reboiler/condenser) @@ -7209,9 +7338,9 @@ private boolean isInReactiveSection(int middleTrayIndex) { } /** - * Enable or disable reactive distillation for all middle trays. When enabled, middle trays use {@link ReactiveTray} - * (simultaneous chemical + phase equilibrium via the Modified RAND method). Can be called after construction; - * existing trays will be replaced. + * Enable or disable reactive distillation for all middle trays. When enabled, middle trays use + * {@link ReactiveTray} (simultaneous chemical + phase equilibrium via the Modified RAND method). + * Can be called after construction; existing trays will be replaced. * * @param reactive {@code true} to enable reactive distillation */ @@ -7223,9 +7352,10 @@ public void setReactive(boolean reactive) { } /** - * Enable reactive distillation on a specific section of middle trays. Tray indices are 0-based among the middle trays - * (excluding reboiler/condenser). For example, in a column with reboiler + 10 middle trays + condenser, - * {@code setReactive(true, 3, 7)} makes trays 4–8 (1-based) of the middle section reactive. + * Enable reactive distillation on a specific section of middle trays. Tray indices are 0-based + * among the middle trays (excluding reboiler/condenser). For example, in a column with reboiler + + * 10 middle trays + condenser, {@code setReactive(true, 3, 7)} makes trays 4–8 (1-based) of the + * middle section reactive. * * @param reactive {@code true} to enable reactive distillation * @param startTray first reactive middle-tray index (0-based, inclusive) @@ -7239,8 +7369,8 @@ public void setReactive(boolean reactive, int startTray, int endTray) { } /** - * Update the reactive flash flag on middle trays to match the current reactive mode configuration. Called - * automatically by {@link #setReactive}. + * Update the reactive flash flag on middle trays to match the current reactive mode + * configuration. Called automatically by {@link #setReactive}. */ private void replaceMiddleTrays() { int start = hasReboiler ? 1 : 0; @@ -7269,12 +7399,13 @@ public boolean isReactive() { public void setSolverType(SolverType solverType) { this.solverType = solverType == null ? SolverType.DIRECT_SUBSTITUTION : solverType; this.solverTypeExplicitlySet = true; - this.lastSolverTypeUsed = this.solverType == SolverType.AUTO ? SolverType.DIRECT_SUBSTITUTION : this.solverType; + this.lastSolverTypeUsed = + this.solverType == SolverType.AUTO ? SolverType.DIRECT_SUBSTITUTION : this.solverType; } /** - * Enable or disable the opt-in fast path for large full fractionators that still use the legacy default - * direct-substitution solver. + * Enable or disable the opt-in fast path for large full fractionators that still use the legacy + * default direct-substitution solver. * * @param enabled {@code true} to allow automatic feed re-centering and MESH residual selection */ @@ -7310,11 +7441,11 @@ public String getLastFullFractionatorFastPathReason() { } /** - * Enable or disable the post-success damped-substitution verification run for accelerated solvers (Wegstein, - * Sum-Rates, Naphtali-Sandholm, MESH residual). Off by default. When enabled every successful accelerated solve is - * double-checked against a fresh damped-substitution solve on a column clone; if product flows differ by more than - * 2 % the damped result is accepted. The verification roughly doubles wallclock time, so it is recommended only - * for regression auditing. + * Enable or disable the post-success damped-substitution verification run for accelerated solvers + * (Wegstein, Sum-Rates, Naphtali-Sandholm, MESH residual). Off by default. When enabled every + * successful accelerated solve is double-checked against a fresh damped-substitution solve on a + * column clone; if product flows differ by more than 2 % the damped result is accepted. The + * verification roughly doubles wallclock time, so it is recommended only for regression auditing. * * @param enabled {@code true} to verify accelerated results against damped substitution */ @@ -7395,23 +7526,23 @@ public double getBottomPressure() { @Override public boolean solved() { boolean acceptableStatus = lastSolveStatus == SolveStatus.RIGOROUS_CONVERGED - || lastSolveStatus == SolveStatus.RECONCILED_PRODUCTS; + || lastSolveStatus == SolveStatus.RECONCILED_PRODUCTS; return acceptableStatus && residualConvergenceSatisfied(); } /** * Check whether the current residual diagnostics satisfy all active rigorous convergence gates. * - * @return {@code true} when temperature, mass, energy, internal traffic, MESH, and specification gates are all - * satisfied + * @return {@code true} when temperature, mass, energy, internal traffic, MESH, and specification + * gates are all satisfied */ private boolean residualConvergenceSatisfied() { boolean temperatureSolved = err < getEffectiveTemperatureTolerance(); boolean massSolved = lastMassResidual <= getEffectiveMassBalanceTolerance(); boolean energySolved = !enforceEnergyBalanceTolerance - || lastEnergyResidual <= getEffectiveEnthalpyBalanceTolerance(); - return temperatureSolved && massSolved && energySolved && internalTrafficSatisfied() && meshResidualsSatisfied() - && specificationsSatisfied(); + || lastEnergyResidual <= getEffectiveEnthalpyBalanceTolerance(); + return temperatureSolved && massSolved && energySolved && internalTrafficSatisfied() + && meshResidualsSatisfied() && specificationsSatisfied(); } /** @@ -7421,7 +7552,7 @@ private boolean residualConvergenceSatisfied() { */ private boolean internalTrafficSatisfied() { return Double.isFinite(lastInternalTrafficRatio) && !lastInternalTrafficGuardReached - && lastInternalTrafficRatio <= MAX_RELAXED_INTERNAL_TRAFFIC_TO_FEED_RATIO; + && lastInternalTrafficRatio <= MAX_RELAXED_INTERNAL_TRAFFIC_TO_FEED_RATIO; } /** @@ -7436,8 +7567,9 @@ private boolean meshResidualsSatisfied() { if (lastMeshResidual == null) { return false; } - return lastMeshResidual.isFinite() && lastMeshResidual.getInfinityNorm() <= meshResidualTolerance - && productDrawResidualsSatisfied(); + return lastMeshResidual.isFinite() + && lastMeshResidual.getInfinityNorm() <= meshResidualTolerance + && productDrawResidualsSatisfied(); } /** @@ -7447,7 +7579,8 @@ private boolean meshResidualsSatisfied() { */ private boolean productDrawResidualsSatisfied() { double productDrawResidual = getLastMeshProductDrawResidualNorm(); - return Double.isFinite(productDrawResidual) && productDrawResidual <= meshProductDrawResidualTolerance; + return Double.isFinite(productDrawResidual) + && productDrawResidual <= meshProductDrawResidualTolerance; } /** @@ -7463,7 +7596,8 @@ private boolean isEffectiveMeshResidualToleranceEnforced() { } /** - * Check whether a solver uses a residual-based formulation that should satisfy the MESH residual gate by default. + * Check whether a solver uses a residual-based formulation that should satisfy the MESH residual + * gate by default. * * @param type solver type to inspect * @return {@code true} when the solver should enforce full MESH residual diagnostics by default @@ -7727,15 +7861,17 @@ public double getLastBottomSpecificationResidual() { * @return maximum absolute top or bottom specification residual */ public double getLastSpecificationResidual() { - return Math.max(Math.abs(lastTopSpecificationResidual), Math.abs(lastBottomSpecificationResidual)); + return Math.max(Math.abs(lastTopSpecificationResidual), + Math.abs(lastBottomSpecificationResidual)); } /** * Set the number of continuation stages used for adjustable product specifications. * *

- * A value of one preserves the legacy direct outer-loop solve. Values above one ramp purity, recovery, and - * product-flow targets from the current product value to the final target over the requested number of stages. + * A value of one preserves the legacy direct outer-loop solve. Values above one ramp purity, + * recovery, and product-flow targets from the current product value to the final target over the + * requested number of stages. *

* * @param steps number of homotopy stages, must be positive @@ -7870,8 +8006,9 @@ public double getLastSolveTimeSeconds() { * Build a human-readable convergence diagnostic report for the latest column solve. * *

- * The report is intended for notebooks, agents, and troubleshooting scripts that need to know which convergence gate - * failed and which common modelling choices should be checked first. It does not change the column state. + * The report is intended for notebooks, agents, and troubleshooting scripts that need to know + * which convergence gate failed and which common modelling choices should be checked first. It + * does not change the column state. *

* * @return multi-line diagnostic report with residuals, feed-tray placement, and recommendations @@ -7888,72 +8025,89 @@ public String getConvergenceDiagnostics() { if (lastSolveStatusReason != null && !lastSolveStatusReason.trim().isEmpty()) { diagnostics.append(" Solve status reason: ").append(lastSolveStatusReason).append("\n"); } - if (lastFullFractionatorFastPathReason != null && !lastFullFractionatorFastPathReason.trim().isEmpty()) { - diagnostics.append(" Opt-in full-fractionator fast path: ").append(lastFullFractionatorFastPathReason) - .append("\n"); + if (lastFullFractionatorFastPathReason != null + && !lastFullFractionatorFastPathReason.trim().isEmpty()) { + diagnostics.append(" Opt-in full-fractionator fast path: ") + .append(lastFullFractionatorFastPathReason).append("\n"); } - diagnostics.append(" Trays: ").append(numberOfTrays).append(" total, ").append(getEffectiveStageCount()) - .append(" equilibrium stages").append("\n"); + diagnostics.append(" Trays: ").append(numberOfTrays).append(" total, ") + .append(getEffectiveStageCount()).append(" equilibrium stages").append("\n"); diagnostics.append(" Iterations: ").append(lastIterationCount).append("\n"); diagnostics.append(" Solve time: ").append(lastSolveTimeSeconds).append(" s\n"); if (lastAutoSolverSummary != null && !lastAutoSolverSummary.trim().isEmpty()) { diagnostics.append(" Automatic solver candidates:\n"); diagnostics.append(lastAutoSolverSummary); if (!lastAutoSolverSummary.endsWith("\n")) { - diagnostics.append("\n"); + diagnostics.append("\n"); } } if (specificationHomotopySteps > 1 || lastSpecificationHomotopyStepCount > 0) { - diagnostics.append(" Specification homotopy: ").append(lastSpecificationHomotopyStepCount).append("/") - .append(specificationHomotopySteps).append(" stages\n"); + diagnostics.append(" Specification homotopy: ").append(lastSpecificationHomotopyStepCount) + .append("/").append(specificationHomotopySteps).append(" stages\n"); } if (lastInsideOutOuterFlashSweeps > 0 || lastInsideOutInnerLoopIterations > 0) { diagnostics.append(" Inside-out model:\n"); - diagnostics.append(" outer flash sweeps: ").append(lastInsideOutOuterFlashSweeps).append("\n"); - diagnostics.append(" inner loop iterations: ").append(lastInsideOutInnerLoopIterations).append("\n"); + diagnostics.append(" outer flash sweeps: ").append(lastInsideOutOuterFlashSweeps) + .append("\n"); + diagnostics.append(" inner loop iterations: ").append(lastInsideOutInnerLoopIterations) + .append("\n"); diagnostics.append(" k-value residual: ").append(lastInsideOutKValueResidual).append("\n"); - diagnostics.append(" surrogate residual: ").append(lastInsideOutSurrogateResidual).append("\n"); - diagnostics.append(" surrogate resets: ").append(lastInsideOutSurrogateResetCount).append("\n"); + diagnostics.append(" surrogate residual: ").append(lastInsideOutSurrogateResidual) + .append("\n"); + diagnostics.append(" surrogate resets: ").append(lastInsideOutSurrogateResetCount) + .append("\n"); } if (solverType == SolverType.MATRIX_INSIDE_OUT || lastMatrixInsideOutWarmStartUsed - || lastMatrixInsideOutWarmStartBypassed) { + || lastMatrixInsideOutWarmStartBypassed) { diagnostics.append(" Matrix inside-out:\n"); - diagnostics.append(" warm start used: ").append(lastMatrixInsideOutWarmStartUsed).append("\n"); - diagnostics.append(" warm start bypassed: ").append(lastMatrixInsideOutWarmStartBypassed).append("\n"); - diagnostics.append(" matrix iterations: ").append(lastMatrixInsideOutIterationCount).append("\n"); - diagnostics.append(" matrix temperature residual: ").append(lastMatrixInsideOutTemperatureResidual) - .append(" K\n"); - diagnostics.append(" matrix time: ").append(lastMatrixInsideOutSolveTimeSeconds).append(" s\n"); + diagnostics.append(" warm start used: ").append(lastMatrixInsideOutWarmStartUsed) + .append("\n"); + diagnostics.append(" warm start bypassed: ").append(lastMatrixInsideOutWarmStartBypassed) + .append("\n"); + diagnostics.append(" matrix iterations: ").append(lastMatrixInsideOutIterationCount) + .append("\n"); + diagnostics.append(" matrix temperature residual: ") + .append(lastMatrixInsideOutTemperatureResidual).append(" K\n"); + diagnostics.append(" matrix time: ").append(lastMatrixInsideOutSolveTimeSeconds) + .append(" s\n"); } if (lastNaphtaliAnalyticJacobianColumns > 0 || lastNaphtaliFiniteDifferenceJacobianColumns > 0 - || lastNaphtaliThermoEvaluationCount > 0) { + || lastNaphtaliThermoEvaluationCount > 0) { diagnostics.append(" Naphtali-Sandholm Jacobian:\n"); - diagnostics.append(" semi-analytic columns: ").append(lastNaphtaliAnalyticJacobianColumns).append("\n"); - diagnostics.append(" finite-difference columns: ").append(lastNaphtaliFiniteDifferenceJacobianColumns) - .append("\n"); - diagnostics.append(" thermodynamic evaluations: ").append(lastNaphtaliThermoEvaluationCount).append("\n"); - diagnostics.append(" thermodynamic cache hits: ").append(lastNaphtaliThermoCacheHitCount).append("\n"); - diagnostics.append(" jacobian build time: ").append(lastNaphtaliJacobianBuildTimeSeconds).append(" s\n"); - diagnostics.append(" block linear solves: ").append(lastNaphtaliBlockLinearSolveCount).append("\n"); - diagnostics.append(" dense linear solves: ").append(lastNaphtaliDenseLinearSolveCount).append("\n"); - diagnostics.append(" linear solve time: ").append(lastNaphtaliLinearSolveTimeSeconds).append(" s\n"); + diagnostics.append(" semi-analytic columns: ").append(lastNaphtaliAnalyticJacobianColumns) + .append("\n"); + diagnostics.append(" finite-difference columns: ") + .append(lastNaphtaliFiniteDifferenceJacobianColumns).append("\n"); + diagnostics.append(" thermodynamic evaluations: ") + .append(lastNaphtaliThermoEvaluationCount).append("\n"); + diagnostics.append(" thermodynamic cache hits: ").append(lastNaphtaliThermoCacheHitCount) + .append("\n"); + diagnostics.append(" jacobian build time: ").append(lastNaphtaliJacobianBuildTimeSeconds) + .append(" s\n"); + diagnostics.append(" block linear solves: ").append(lastNaphtaliBlockLinearSolveCount) + .append("\n"); + diagnostics.append(" dense linear solves: ").append(lastNaphtaliDenseLinearSolveCount) + .append("\n"); + diagnostics.append(" linear solve time: ").append(lastNaphtaliLinearSolveTimeSeconds) + .append(" s\n"); } diagnostics.append(" Residuals:\n"); diagnostics.append(" temperature: ").append(lastTemperatureResidual).append(" K (tolerance ") - .append(getEffectiveTemperatureTolerance()).append(")\n"); + .append(getEffectiveTemperatureTolerance()).append(")\n"); diagnostics.append(" mass: ").append(lastMassResidual).append(" (tolerance ") - .append(getEffectiveMassBalanceTolerance()).append(")\n"); + .append(getEffectiveMassBalanceTolerance()).append(")\n"); diagnostics.append(" energy: ").append(lastEnergyResidual).append(" (tolerance ") - .append(getEffectiveEnthalpyBalanceTolerance()).append(", enforced=").append(enforceEnergyBalanceTolerance) - .append(")\n"); - diagnostics.append(" mesh infinity norm: ").append(getLastMeshResidualNorm()).append(" (tolerance ") - .append(meshResidualTolerance).append(", enforced=").append(isEffectiveMeshResidualToleranceEnforced()) - .append(")\n"); - diagnostics.append(" material: ").append(getLastMeshMaterialResidualNorm()).append(", equilibrium: ") - .append(getLastMeshEquilibriumResidualNorm()).append(", summation: ").append(getLastMeshSummationResidualNorm()) - .append(", energy: ").append(getLastMeshEnergyResidualNorm()).append(", product draw: ") - .append(getLastMeshProductDrawResidualNorm()).append(", specification: ") - .append(getLastMeshSpecificationResidualNorm()).append("\n"); + .append(getEffectiveEnthalpyBalanceTolerance()).append(", enforced=") + .append(enforceEnergyBalanceTolerance).append(")\n"); + diagnostics.append(" mesh infinity norm: ").append(getLastMeshResidualNorm()) + .append(" (tolerance ").append(meshResidualTolerance).append(", enforced=") + .append(isEffectiveMeshResidualToleranceEnforced()).append(")\n"); + diagnostics.append(" material: ").append(getLastMeshMaterialResidualNorm()) + .append(", equilibrium: ").append(getLastMeshEquilibriumResidualNorm()) + .append(", summation: ").append(getLastMeshSummationResidualNorm()).append(", energy: ") + .append(getLastMeshEnergyResidualNorm()).append(", product draw: ") + .append(getLastMeshProductDrawResidualNorm()).append(", specification: ") + .append(getLastMeshSpecificationResidualNorm()).append("\n"); diagnostics.append(" Feed trays:\n"); if (feedStreams.isEmpty()) { @@ -7962,17 +8116,17 @@ public String getConvergenceDiagnostics() { List feedTrayNumbers = new ArrayList(feedStreams.keySet()); Collections.sort(feedTrayNumbers); for (Integer feedTrayNumber : feedTrayNumbers) { - int stagesBelow = Math.max(0, feedTrayNumber.intValue()); - int stagesAbove = Math.max(0, numberOfTrays - feedTrayNumber.intValue() - 1); - diagnostics.append(" tray ").append(feedTrayNumber).append(" with ") - .append(feedStreams.get(feedTrayNumber).size()).append(" feed(s), ").append(stagesAbove) - .append(" stages above, ").append(stagesBelow).append(" stages below"); - if (isFeedTrayNearTop(feedTrayNumber.intValue())) { - diagnostics.append(" (near top/condenser)"); - } else if (isFeedTrayNearBottom(feedTrayNumber.intValue())) { - diagnostics.append(" (near bottom/reboiler)"); - } - diagnostics.append("\n"); + int stagesBelow = Math.max(0, feedTrayNumber.intValue()); + int stagesAbove = Math.max(0, numberOfTrays - feedTrayNumber.intValue() - 1); + diagnostics.append(" tray ").append(feedTrayNumber).append(" with ") + .append(feedStreams.get(feedTrayNumber).size()).append(" feed(s), ").append(stagesAbove) + .append(" stages above, ").append(stagesBelow).append(" stages below"); + if (isFeedTrayNearTop(feedTrayNumber.intValue())) { + diagnostics.append(" (near top/condenser)"); + } else if (isFeedTrayNearBottom(feedTrayNumber.intValue())) { + diagnostics.append(" (near bottom/reboiler)"); + } + diagnostics.append("\n"); } } @@ -7995,45 +8149,52 @@ private int appendConvergenceRecommendations(StringBuilder diagnostics, boolean int count = 0; for (Integer feedTrayNumber : feedStreams.keySet()) { if (isFeedTrayNearTop(feedTrayNumber.intValue())) { - diagnostics.append(" - Feed tray ").append(feedTrayNumber) - .append(" is close to the condenser. For debutanizer/depropanizer-style ") - .append("hydrocarbon splits, start near the middle of the column and move the feed ") - .append("only after the base case converges.\n"); - count++; + diagnostics.append(" - Feed tray ").append(feedTrayNumber) + .append(" is close to the condenser. For debutanizer/depropanizer-style ") + .append("hydrocarbon splits, start near the middle of the column and move the feed ") + .append("only after the base case converges.\n"); + count++; } else if (isFeedTrayNearBottom(feedTrayNumber.intValue())) { - diagnostics.append(" - Feed tray ").append(feedTrayNumber) - .append(" is close to the reboiler. Check whether the feed should enter higher in ") - .append("the column or whether a side draw/flash should be represented explicitly.\n"); - count++; - } - } - if (hasCondenser && getCondenser().getRefluxRatio() > 0.0 && getCondenser().getRefluxRatio() <= 0.2) { - diagnostics.append(" - Condenser reflux ratio is low (").append(getCondenser().getRefluxRatio()) - .append("). Low reflux can make tray-temperature substitution oscillatory; ") - .append("try a higher reflux during initialization before tightening the spec.\n"); + diagnostics.append(" - Feed tray ").append(feedTrayNumber) + .append(" is close to the reboiler. Check whether the feed should enter higher in ") + .append("the column or whether a side draw/flash should be represented explicitly.\n"); + count++; + } + } + if (hasCondenser && getCondenser().getRefluxRatio() > 0.0 + && getCondenser().getRefluxRatio() <= 0.2) { + diagnostics.append(" - Condenser reflux ratio is low (") + .append(getCondenser().getRefluxRatio()) + .append("). Low reflux can make tray-temperature substitution oscillatory; ") + .append("try a higher reflux during initialization before tightening the spec.\n"); count++; } - if ((!solved || lastIterationCount > Math.max(20, getEffectiveStageCount() * 3)) && hasCondenser && hasReboiler - && solverType != SolverType.MESH_RESIDUAL && solverType != SolverType.NAPHTALI_SANDHOLM) { - diagnostics.append(" - For full hydrocarbon fractionators, benchmark ").append(SolverType.NAPHTALI_SANDHOLM) - .append(", ").append(SolverType.MESH_RESIDUAL).append(", or ").append(SolverType.NEWTON) - .append(" after checking feed-tray placement. They can be faster than direct ") - .append("substitution for well-conditioned columns.\n"); + if ((!solved || lastIterationCount > Math.max(20, getEffectiveStageCount() * 3)) && hasCondenser + && hasReboiler && solverType != SolverType.MESH_RESIDUAL + && solverType != SolverType.NAPHTALI_SANDHOLM) { + diagnostics.append(" - For full hydrocarbon fractionators, benchmark ") + .append(SolverType.NAPHTALI_SANDHOLM).append(", ").append(SolverType.MESH_RESIDUAL) + .append(", or ").append(SolverType.NEWTON) + .append(" after checking feed-tray placement. They can be faster than direct ") + .append("substitution for well-conditioned columns.\n"); count++; } double productDrawResidual = getLastMeshProductDrawResidualNorm(); - if (Double.isFinite(productDrawResidual) && productDrawResidual > meshProductDrawResidualTolerance) { - diagnostics.append(" - Product draw residual is above the product-draw tolerance. This means ") - .append("the exposed overhead/bottom streams do not match the terminal tray traffic; ").append("use ") - .append(SolverType.NAPHTALI_SANDHOLM).append(" or ").append(SolverType.MESH_RESIDUAL) - .append(" or inspect reflux, boilup, and product specifications before trusting the ") - .append("product split.\n"); + if (Double.isFinite(productDrawResidual) + && productDrawResidual > meshProductDrawResidualTolerance) { + diagnostics + .append(" - Product draw residual is above the product-draw tolerance. This means ") + .append("the exposed overhead/bottom streams do not match the terminal tray traffic; ") + .append("use ").append(SolverType.NAPHTALI_SANDHOLM).append(" or ") + .append(SolverType.MESH_RESIDUAL) + .append(" or inspect reflux, boilup, and product specifications before trusting the ") + .append("product split.\n"); count++; } if (!solved) { diagnostics.append(" - Inspect the residual above the tolerance: temperature usually ") - .append("points to tray/specification oscillation, while mass residual points to ") - .append("stream wiring or divergent internal L/V traffic.\n"); + .append("points to tray/specification oscillation, while mass residual points to ") + .append("stream wiring or divergent internal L/V traffic.\n"); count++; } return count; @@ -8097,9 +8258,9 @@ public boolean isEnforceEnergyBalanceTolerance() { } /** - * Control whether the latest MESH residual vector must satisfy tolerance during convergence checks. Calling this - * method explicitly overrides the default behavior where residual-based solvers enforce the gate and substitution or - * temperature/flow accelerator solvers do not. + * Control whether the latest MESH residual vector must satisfy tolerance during convergence + * checks. Calling this method explicitly overrides the default behavior where residual-based + * solvers enforce the gate and substitution or temperature/flow accelerator solvers do not. * * @param enforce {@code true} to require MESH residuals to satisfy the configured tolerance */ @@ -8183,7 +8344,8 @@ public double getMaxTrayOptimizationTimeSeconds() { */ public void setMaxTrayOptimizationTimeSeconds(double maxTimeSeconds) { if (!isPositiveFinite(maxTimeSeconds)) { - throw new IllegalArgumentException("Maximum tray optimization time must be finite and positive."); + throw new IllegalArgumentException( + "Maximum tray optimization time must be finite and positive."); } this.maxTrayOptimizationTimeSeconds = maxTimeSeconds; } @@ -8231,16 +8393,18 @@ public double getInternalDiameter() { } /** - * Calculates the Fs factor for the distillation column. The Fs factor is a measure of the gas flow rate through the - * column relative to the cross-sectional area and the density of the gas. + * Calculates the Fs factor for the distillation column. The Fs factor is a measure of the gas + * flow rate through the column relative to the cross-sectional area and the density of the gas. * *

- * The Fs factor (gas load factor) is defined as {@code Fs = Vs * sqrt(rho_gas)} where {@code Vs} is the superficial - * gas velocity (m/s) and {@code rho_gas} is the gas density (kg/m3). It is proportional to the aerodynamic lift - * exerted by the gas on the liquid and is the primary hydraulic capacity indicator for the column. + * The Fs factor (gas load factor) is defined as {@code Fs = Vs * sqrt(rho_gas)} where {@code Vs} + * is the superficial gas velocity (m/s) and {@code rho_gas} is the gas density (kg/m3). It is + * proportional to the aerodynamic lift exerted by the gas on the liquid and is the primary + * hydraulic capacity indicator for the column. *

* - * @return the Fs factor in m/s*sqrt(kg/m3), or 0 if streams are not initialized or the internal diameter is not set + * @return the Fs factor in m/s*sqrt(kg/m3), or 0 if streams are not initialized or the internal + * diameter is not set */ public double getFsFactor() { if (getGasOutStream() == null || getGasOutStream().getThermoSystem() == null) { @@ -8251,12 +8415,12 @@ public double getFsFactor() { return 0.0; } return getGasOutStream().getThermoSystem().getFlowRate("m3/sec") / intArea - * Math.sqrt(getGasOutStream().getThermoSystem().getDensity("kg/m3")); + * Math.sqrt(getGasOutStream().getThermoSystem().getDensity("kg/m3")); } /** - * Gets the maximum allowable Fs factor (gas load factor) used as the design basis for the Fs-factor capacity - * constraint. + * Gets the maximum allowable Fs factor (gas load factor) used as the design basis for the + * Fs-factor capacity constraint. * * @return maximum allowable Fs factor in m/s*sqrt(kg/m3) */ @@ -8265,8 +8429,8 @@ public double getMaxAllowableFsFactor() { } /** - * Sets the maximum allowable Fs factor (gas load factor) used as the design basis for the Fs-factor capacity - * constraint. + * Sets the maximum allowable Fs factor (gas load factor) used as the design basis for the + * Fs-factor capacity constraint. * *

* Re-initializes the capacity constraints so the new design value takes effect immediately. @@ -8306,8 +8470,8 @@ public boolean isFsFactorWithinDesignLimit() { } /** - * Calculates the minimum vessel internal diameter required to keep the Fs factor at or below the maximum allowable - * value for the current gas flow rate. + * Calculates the minimum vessel internal diameter required to keep the Fs factor at or below the + * maximum allowable value for the current gas flow rate. * *

* From {@code Fs = Vs * sqrt(rho_gas)} and {@code Vs = Q / A}, the minimum diameter is @@ -8333,19 +8497,21 @@ public double getMinimumDiameterForFsLimit() { * Sets up the default capacity constraints for the distillation column. * *

- * Registers an Fs-factor (gas load factor) constraint that uses the live {@link #getFsFactor()} value against the - * {@link #getMaxAllowableFsFactor()} design basis. This makes the column participate in process-wide bottleneck - * analysis, capacity utilization summaries, and optimization constraint checking in the same way as other - * capacity-constrained equipment. + * Registers an Fs-factor (gas load factor) constraint that uses the live {@link #getFsFactor()} + * value against the {@link #getMaxAllowableFsFactor()} design basis. This makes the column + * participate in process-wide bottleneck analysis, capacity utilization summaries, and + * optimization constraint checking in the same way as other capacity-constrained equipment. *

*/ @Override protected void initializeDefaultConstraints() { - neqsim.process.equipment.capacity.CapacityConstraint fsConstraint = new neqsim.process.equipment.capacity.CapacityConstraint( - "fsFactor", "m/s*sqrt(kg/m3)", neqsim.process.equipment.capacity.CapacityConstraint.ConstraintType.SOFT); + neqsim.process.equipment.capacity.CapacityConstraint fsConstraint = + new neqsim.process.equipment.capacity.CapacityConstraint("fsFactor", "m/s*sqrt(kg/m3)", + neqsim.process.equipment.capacity.CapacityConstraint.ConstraintType.SOFT); fsConstraint.setDesignValue(maxAllowableFsFactor); fsConstraint.setMaxValue(maxAllowableFsFactor); - fsConstraint.setSeverity(neqsim.process.equipment.capacity.CapacityConstraint.ConstraintSeverity.SOFT); + fsConstraint + .setSeverity(neqsim.process.equipment.capacity.CapacityConstraint.ConstraintSeverity.SOFT); fsConstraint.setDescription("Column Fs factor (gas load factor) vs maximum allowable"); fsConstraint.setDataSource("equipment"); fsConstraint.setValueSupplier(this::getFsFactor); @@ -8353,10 +8519,12 @@ protected void initializeDefaultConstraints() { } /** - * Rebuilds the capacity constraints so updated design values (e.g. max allowable Fs factor) take effect. + * Rebuilds the capacity constraints so updated design values (e.g. max allowable Fs factor) take + * effect. */ private void reinitializeCapacityConstraints() { - neqsim.process.equipment.capacity.CapacityConstraint existing = getCapacityConstraints().get("fsFactor"); + neqsim.process.equipment.capacity.CapacityConstraint existing = + getCapacityConstraints().get("fsFactor"); if (existing != null) { existing.setDesignValue(maxAllowableFsFactor); existing.setMaxValue(maxAllowableFsFactor); @@ -8369,8 +8537,8 @@ private void reinitializeCapacityConstraints() { * Create and run a column internals designer for this column. * *

- * Evaluates hydraulics on every tray (flooding, weeping, entrainment, downcomer backup, pressure drop, efficiency) - * and sizes the column diameter from the controlling tray. + * Evaluates hydraulics on every tray (flooding, weeping, entrainment, downcomer backup, pressure + * drop, efficiency) and sizes the column diameter from the controlling tray. *

* * @param internalsType tray type ("sieve", "valve", "bubble-cap") or "packed" @@ -8396,8 +8564,8 @@ public ColumnInternalsDesigner calcColumnInternals() { * Set a gas or liquid side-draw fraction on a tray. * *

- * Side draws are implemented on the tray outlet itself, so all column solver paths use the residual gas/liquid - * traffic for inter-tray flow and expose the withdrawn stream separately. + * Side draws are implemented on the tray outlet itself, so all column solver paths use the + * residual gas/liquid traffic for inter-tray flow and expose the withdrawn stream separately. *

* * @param trayNumber bottom-up tray index @@ -8463,10 +8631,10 @@ public List getSideDrawStreams() { for (int trayNumber = 0; trayNumber < numberOfTrays; trayNumber++) { SimpleTray tray = getTray(trayNumber); if (tray.getGasSideDrawFraction() > 0.0) { - sideDrawStreams.add(tray.getGasSideDrawStream()); + sideDrawStreams.add(tray.getGasSideDrawStream()); } if (tray.getLiquidSideDrawFraction() > 0.0) { - sideDrawStreams.add(tray.getLiquidSideDrawStream()); + sideDrawStreams.add(tray.getLiquidSideDrawStream()); } } return Collections.unmodifiableList(sideDrawStreams); @@ -8476,9 +8644,9 @@ public List getSideDrawStreams() { * Add a side-draw flow specification solved as a column tear variable. * *

- * The solver adjusts the side-draw fraction on the requested tray until the side-product stream flow matches the - * target. This turns side draws into formal product specifications while preserving the existing tray split - * implementation. + * The solver adjusts the side-draw fraction on the requested tray until the side-product stream + * flow matches the target. This turns side draws into formal product specifications while + * preserving the existing tray split implementation. *

* * @param trayNumber bottom-up tray index @@ -8488,10 +8656,11 @@ public List getSideDrawStreams() { * @return configured side-draw specification * @throws IllegalArgumentException if the tray number, phase, flow rate, or unit is invalid */ - public ColumnSideDrawSpecification addSideDrawFlowSpecification(int trayNumber, SideDrawPhase phase, double flowRate, - String unit) { + public ColumnSideDrawSpecification addSideDrawFlowSpecification(int trayNumber, + SideDrawPhase phase, double flowRate, String unit) { validateTrayIndex(trayNumber, "side draw specification tray"); - ColumnSideDrawSpecification specification = new ColumnSideDrawSpecification(trayNumber, phase, flowRate, unit); + ColumnSideDrawSpecification specification = + new ColumnSideDrawSpecification(trayNumber, phase, flowRate, unit); sideDrawSpecifications.add(specification); if (flowRate > 0.0 && getSideDrawFraction(trayNumber, phase) <= 0.0) { setSideDrawFractionWithinLimit(trayNumber, phase, 0.05); @@ -8647,9 +8816,9 @@ public boolean isDynamicColumnModelExperimental() { * Add a liquid pumparound circuit. * *

- * The draw is treated as an internal liquid withdrawal, not as a side-product stream. The return stream is updated - * between column solves and added to the configured return tray as an internal recycle, so external mass-balance - * reporting continues to use only true feeds and products. + * The draw is treated as an internal liquid withdrawal, not as a side-product stream. The return + * stream is updated between column solves and added to the configured return tray as an internal + * recycle, so external mass-balance reporting continues to use only true feeds and products. *

* * @param name pumparound name @@ -8658,7 +8827,8 @@ public boolean isDynamicColumnModelExperimental() { * @param drawFraction fraction of tray liquid traffic withdrawn * @param temperatureDrop temperature drop from draw to return in Kelvin * @return configured pumparound definition - * @throws IllegalArgumentException if tray numbers, draw fraction, or temperature drop are invalid + * @throws IllegalArgumentException if tray numbers, draw fraction, or temperature drop are + * invalid */ public ColumnPumparound addLiquidPumparound(String name, int drawTrayNumber, int returnTrayNumber, double drawFraction, double temperatureDrop) { @@ -8674,8 +8844,8 @@ public ColumnPumparound addLiquidPumparound(String name, int drawTrayNumber, int throw new IllegalArgumentException("Only one liquid pumparound draw is supported per tray"); } - ColumnPumparound pumparound = new ColumnPumparound(name, drawTrayNumber, returnTrayNumber, drawFraction, - temperatureDrop); + ColumnPumparound pumparound = + new ColumnPumparound(name, drawTrayNumber, returnTrayNumber, drawFraction, temperatureDrop); pumparounds.add(pumparound); getTray(drawTrayNumber).setLiquidPumparoundDrawFraction(drawFraction); setDoInitializion(true); @@ -8775,9 +8945,9 @@ public List getOutletStreams() { * Get all external feed streams connected to a tray. * *

- * Feeds registered through {@link #addFeedStream(StreamInterface, int)} are always included. The method also includes - * named streams added directly to the tray to preserve legacy workflows that use - * {@code getTray(index).addStream(stream)} for side feeds or stripping gas. + * Feeds registered through {@link #addFeedStream(StreamInterface, int)} are always included. The + * method also includes named streams added directly to the tray to preserve legacy workflows that + * use {@code getTray(index).addStream(stream)} for side feeds or stripping gas. *

* * @param trayIndex tray index to inspect @@ -8792,7 +8962,7 @@ List getExternalFeedStreams(int trayIndex) { List directFeeds = directExternalFeedStreams.get(trayIndex); if (directFeeds != null) { for (StreamInterface directFeed : directFeeds) { - addStreamIfMissingByIdentity(externalFeeds, directFeed); + addStreamIfMissingByIdentity(externalFeeds, directFeed); } } return externalFeeds; @@ -8807,12 +8977,12 @@ private List getAllExternalFeedStreams() { List externalFeeds = new ArrayList<>(); for (List feedList : feedStreams.values()) { for (StreamInterface feed : feedList) { - addStreamIfMissingByIdentity(externalFeeds, feed); + addStreamIfMissingByIdentity(externalFeeds, feed); } } for (List directFeedList : directExternalFeedStreams.values()) { for (StreamInterface directFeed : directFeedList) { - addStreamIfMissingByIdentity(externalFeeds, directFeed); + addStreamIfMissingByIdentity(externalFeeds, directFeed); } } return externalFeeds; @@ -8828,16 +8998,16 @@ private void captureDirectExternalTrayFeeds() { List externalFeeds = getExternalFeedStreams(trayIndex); SimpleTray tray = trays.get(trayIndex); for (int streamIndex = 0; streamIndex < tray.getNumberOfInputStreams(); streamIndex++) { - StreamInterface stream = tray.getStream(streamIndex); - if (isUnregisteredExternalTrayFeed(stream, externalFeeds, registeredFeedNames)) { - List directFeeds = directExternalFeedStreams.get(trayIndex); - if (directFeeds == null) { - directFeeds = new ArrayList<>(); - directExternalFeedStreams.put(trayIndex, directFeeds); - } - directFeeds.add(stream); - externalFeeds.add(stream); - } + StreamInterface stream = tray.getStream(streamIndex); + if (isUnregisteredExternalTrayFeed(stream, externalFeeds, registeredFeedNames)) { + List directFeeds = directExternalFeedStreams.get(trayIndex); + if (directFeeds == null) { + directFeeds = new ArrayList<>(); + directExternalFeedStreams.put(trayIndex, directFeeds); + } + directFeeds.add(stream); + externalFeeds.add(stream); + } } } } @@ -8846,9 +9016,10 @@ private void captureDirectExternalTrayFeeds() { * Collect the names of all feeds registered through {@link #addFeedStream(StreamInterface, int)}. * *

- * Iterative process solving (for example a recycle loop, or the solver's own candidate-copy accept path) can leave - * cloned copies of a registered feed on its feed tray. Such clones share the registered feed name but have a - * different object identity, so they must never be mistaken for genuine legacy direct side feeds. + * Iterative process solving (for example a recycle loop, or the solver's own candidate-copy + * accept path) can leave cloned copies of a registered feed on its feed tray. Such clones share + * the registered feed name but have a different object identity, so they must never be mistaken + * for genuine legacy direct side feeds. *

* * @return set of registered feed stream names (non-null, non-empty names only) @@ -8857,16 +9028,16 @@ private Set collectRegisteredFeedNames() { Set names = new HashSet<>(); for (List feedList : feedStreams.values()) { if (feedList == null) { - continue; + continue; } for (StreamInterface feed : feedList) { - if (feed == null) { - continue; - } - String name = feed.getName(); - if (name != null && !name.trim().isEmpty()) { - names.add(name); - } + if (feed == null) { + continue; + } + String name = feed.getName(); + if (name != null && !name.trim().isEmpty()) { + names.add(name); + } } } return names; @@ -8876,9 +9047,10 @@ private Set collectRegisteredFeedNames() { * Drop previously captured direct external feeds that are actually clones of registered feeds. * *

- * A cloned registered feed shares the registered feed name but has a different identity. Keeping such clones in - * {@link #directExternalFeedStreams} would inflate {@link #getExternalFeedStreams(int)} on every solve and make the - * tray feed inventory grow without bound across repeated runs. + * A cloned registered feed shares the registered feed name but has a different identity. Keeping + * such clones in {@link #directExternalFeedStreams} would inflate + * {@link #getExternalFeedStreams(int)} on every solve and make the tray feed inventory grow + * without bound across repeated runs. *

* * @param trayIndex tray whose captured direct feeds should be pruned @@ -8892,8 +9064,9 @@ private void pruneClonedDirectExternalFeeds(int trayIndex, Set registere Iterator iterator = directFeeds.iterator(); while (iterator.hasNext()) { StreamInterface directFeed = iterator.next(); - if (directFeed == null || (directFeed.getName() != null && registeredFeedNames.contains(directFeed.getName()))) { - iterator.remove(); + if (directFeed == null + || (directFeed.getName() != null && registeredFeedNames.contains(directFeed.getName()))) { + iterator.remove(); } } if (directFeeds.isEmpty()) { @@ -8908,10 +9081,10 @@ private void resetTrayInputsToExternalFeeds() { for (int trayIndex = 0; trayIndex < trays.size(); trayIndex++) { List trayInputs = new ArrayList<>(getExternalFeedStreams(trayIndex)); for (ColumnPumparound pumparound : pumparounds) { - StreamInterface returnStream = pumparound.getReturnStream(); - if (pumparound.getReturnTrayNumber() == trayIndex && returnStream != null) { - trayInputs.add(returnStream); - } + StreamInterface returnStream = pumparound.getReturnStream(); + if (pumparound.getReturnTrayNumber() == trayIndex && returnStream != null) { + trayInputs.add(returnStream); + } } trays.get(trayIndex).resetInputStreams(trayInputs); } @@ -8939,7 +9112,8 @@ private int getFirstExternalFeedTrayNumber() { * @param streams stream list to update * @param candidate stream to add */ - private void addStreamIfMissingByIdentity(List streams, StreamInterface candidate) { + private void addStreamIfMissingByIdentity(List streams, + StreamInterface candidate) { if (!containsStreamByIdentity(streams, candidate)) { streams.add(candidate); } @@ -8952,10 +9126,11 @@ private void addStreamIfMissingByIdentity(List streams, StreamI * @param candidate stream to find * @return {@code true} if the exact stream object is present */ - private boolean containsStreamByIdentity(List streams, StreamInterface candidate) { + private boolean containsStreamByIdentity(List streams, + StreamInterface candidate) { for (StreamInterface stream : streams) { if (stream == candidate) { - return true; + return true; } } return false; @@ -8969,8 +9144,8 @@ private boolean containsStreamByIdentity(List streams, StreamIn * @param registeredFeedNames names of feeds registered through the column API * @return {@code true} if the stream looks like a named direct external feed */ - private boolean isUnregisteredExternalTrayFeed(StreamInterface stream, List knownExternalFeeds, - Set registeredFeedNames) { + private boolean isUnregisteredExternalTrayFeed(StreamInterface stream, + List knownExternalFeeds, Set registeredFeedNames) { if (stream == null || containsStreamByIdentity(knownExternalFeeds, stream)) { return false; } @@ -8995,14 +9170,14 @@ private boolean isUnregisteredExternalTrayFeed(StreamInterface stream, List 1e-12) { - trayRelativeError = Math.max(trayRelativeError, imbalance / absInlet); + trayRelativeError = Math.max(trayRelativeError, imbalance / absInlet); } totalInlet += absInlet; totalResidual += imbalance; @@ -9480,7 +9660,8 @@ public double getMassBalanceError() { } /** - * Calculates the relative mass imbalance between external feed streams and public product streams. + * Calculates the relative mass imbalance between external feed streams and public product + * streams. * * @return relative external mass imbalance based on kg/hr flow rates */ @@ -9507,22 +9688,22 @@ public double getEnergyBalanceError() { double inlet = 0.0; int numberOfInputStreams = trays.get(i).getNumberOfInputStreams(); for (int j = 0; j < numberOfInputStreams; j++) { - inlet += getFiniteStreamEnthalpy(trays.get(i).getStream(j)); + inlet += getFiniteStreamEnthalpy(trays.get(i).getStream(j)); } double outlet = getFiniteStreamEnthalpy(trays.get(i).getGasOutStream()); outlet += getFiniteStreamEnthalpy(trays.get(i).getLiquidOutStream()); if (trays.get(i) instanceof Reboiler) { - inlet += getFiniteDiagnosticValue(((Reboiler) trays.get(i)).getDuty()); + inlet += getFiniteDiagnosticValue(((Reboiler) trays.get(i)).getDuty()); } else if (trays.get(i) instanceof Condenser) { - inlet += getFiniteDiagnosticValue(((Condenser) trays.get(i)).getDuty()); + inlet += getFiniteDiagnosticValue(((Condenser) trays.get(i)).getDuty()); } double absInlet = Math.abs(inlet); double imbalance = Math.abs(inlet - outlet); if (absInlet > 1e-12) { - trayRelativeError = Math.max(trayRelativeError, imbalance / absInlet); + trayRelativeError = Math.max(trayRelativeError, imbalance / absInlet); } totalInlet += absInlet; totalResidual += imbalance; @@ -9567,7 +9748,8 @@ private double getFiniteDiagnosticValue(double value) { private static final int MAX_CONVERGENCE_HISTORY = 500; /** - * Append a residual snapshot to the convergence history, capping at {@link #MAX_CONVERGENCE_HISTORY} entries. + * Append a residual snapshot to the convergence history, capping at + * {@link #MAX_CONVERGENCE_HISTORY} entries. * * @param entry residual array to record */ @@ -9578,16 +9760,17 @@ private void recordConvergence(double[] entry) { } /** - * Apply Murphree tray efficiency correction to the vapor leaving a tray. The correction blends the equilibrium vapor - * composition with the inlet vapor composition: + * Apply Murphree tray efficiency correction to the vapor leaving a tray. The correction blends + * the equilibrium vapor composition with the inlet vapor composition: * *
    * y_i^out = y_i^in + E_MV * (y_i^eq - y_i^in)
    * 
* - * where {@code E_MV} is the Murphree efficiency, {@code y_i^eq} is the equilibrium composition from the flash, and - * {@code y_i^in} is the inlet vapor composition. When efficiency is 1.0, the tray is ideal and no correction is - * applied. The correction is skipped for reboilers and condensers (first and last trays). + * where {@code E_MV} is the Murphree efficiency, {@code y_i^eq} is the equilibrium composition + * from the flash, and {@code y_i^in} is the inlet vapor composition. When efficiency is 1.0, the + * tray is ideal and no correction is applied. The correction is skipped for reboilers and + * condensers (first and last trays). * * @param trayIndex index of the tray in the {@code trays} list */ @@ -9635,7 +9818,7 @@ private void applyMurphreeCorrection(int trayIndex) { } if (sumY > 1e-15) { for (int j = 0; j < nc; j++) { - yActual[j] /= sumY; + yActual[j] /= sumY; } } @@ -9668,7 +9851,8 @@ private void applyMurphreeCorrection(int trayIndex) { * @param relaxation relaxation factor applied to the update * @return relaxed stream instance to be used in the next tear */ - private StreamInterface applyRelaxation(StreamInterface previous, StreamInterface current, double relaxation) { + private StreamInterface applyRelaxation(StreamInterface previous, StreamInterface current, + double relaxation) { return applyRelaxationInternal(previous, current, relaxation, false); } @@ -9680,7 +9864,8 @@ private StreamInterface applyRelaxation(StreamInterface previous, StreamInterfac * @param relaxation relaxation factor applied to the update * @return relaxed stream instance to be used in the next tear */ - private StreamInterface applyRelaxationFast(StreamInterface previous, StreamInterface current, double relaxation) { + private StreamInterface applyRelaxationFast(StreamInterface previous, StreamInterface current, + double relaxation) { return applyRelaxationInternal(previous, current, relaxation, true); } @@ -9693,36 +9878,38 @@ private StreamInterface applyRelaxationFast(StreamInterface previous, StreamInte * @param skipUnchangedReflash whether an unchanged clone can reuse the current stream flash state * @return relaxed stream instance to be used in the next tear */ - private StreamInterface applyRelaxationInternal(StreamInterface previous, StreamInterface current, double relaxation, - boolean skipUnchangedReflash) { + private StreamInterface applyRelaxationInternal(StreamInterface previous, StreamInterface current, + double relaxation, boolean skipUnchangedReflash) { double maximumInternalFlow = getMaximumRelaxedInternalFlowKgPerHour(); // Fast path: no damping needed; clone the already-flashed tray outlet. if (previous == null || relaxation >= 1.0) { StreamInterface relaxed = current.clone(); - boolean requiresReflash = internalTrafficCapActive || !Double.isFinite(relaxed.getFlowRate("kg/hr")); + boolean requiresReflash = + internalTrafficCapActive || !Double.isFinite(relaxed.getFlowRate("kg/hr")); if (requiresReflash) { - capStreamFlow(relaxed, maximumInternalFlow); + capStreamFlow(relaxed, maximumInternalFlow); } if (requiresReflash || !skipUnchangedReflash) { - relaxed.run(); + relaxed.run(); } return relaxed; } StreamInterface relaxed = current.clone(); double step = Math.max(0.0, Math.min(1.0, relaxation)); - double previousFlow = getRelaxedInternalFlow(previous.getFlowRate("kg/hr"), maximumInternalFlow); + double previousFlow = + getRelaxedInternalFlow(previous.getFlowRate("kg/hr"), maximumInternalFlow); double currentFlow = getRelaxedInternalFlow(current.getFlowRate("kg/hr"), maximumInternalFlow); double mixedFlow = previousFlow + step * (currentFlow - previousFlow); mixedFlow = getRelaxedInternalFlow(mixedFlow, maximumInternalFlow); relaxed.setFlowRate(mixedFlow, "kg/hr"); double mixedTemperature = previous.getTemperature("K") - + step * (current.getTemperature("K") - previous.getTemperature("K")); + + step * (current.getTemperature("K") - previous.getTemperature("K")); relaxed.setTemperature(mixedTemperature, "K"); double mixedPressure = previous.getPressure("bara") - + step * (current.getPressure("bara") - previous.getPressure("bara")); + + step * (current.getPressure("bara") - previous.getPressure("bara")); relaxed.setPressure(mixedPressure, "bara"); double[] zPrev = previous.getThermoSystem().getMolarComposition(); @@ -9743,16 +9930,16 @@ private StreamInterface applyRelaxationInternal(StreamInterface previous, Stream double molesCurr_i = zCurr[i] * totalMolesCurr; double mixedMoles_i = molesPrev_i + step * (molesCurr_i - molesPrev_i); if (Double.isFinite(mixedMoles_i) && mixedMoles_i > 0.0) { - zMixed[i] = mixedMoles_i; - totalMolesMixed += mixedMoles_i; + zMixed[i] = mixedMoles_i; + totalMolesMixed += mixedMoles_i; } else { - zMixed[i] = 0.0; + zMixed[i] = 0.0; } } if (totalMolesMixed > 1e-12 && relaxed.getThermoSystem().getTotalNumberOfMoles() > 1e-100) { for (int i = 0; i < zMixed.length; i++) { - zMixed[i] /= totalMolesMixed; + zMixed[i] /= totalMolesMixed; } relaxed.getThermoSystem().setMolarComposition(zMixed); } @@ -9772,17 +9959,18 @@ private StreamInterface applyRelaxationInternal(StreamInterface previous, Stream * @param currentComposition current molar composition vector * @return {@code true} when all streams expose the same component count and composition length */ - private boolean canRelaxMolarComposition(StreamInterface previous, StreamInterface current, StreamInterface relaxed, - double[] previousComposition, double[] currentComposition) { + private boolean canRelaxMolarComposition(StreamInterface previous, StreamInterface current, + StreamInterface relaxed, double[] previousComposition, double[] currentComposition) { if (previous == null || current == null || relaxed == null || previousComposition == null - || currentComposition == null) { + || currentComposition == null) { return false; } int previousComponents = previous.getThermoSystem().getNumberOfComponents(); int currentComponents = current.getThermoSystem().getNumberOfComponents(); int relaxedComponents = relaxed.getThermoSystem().getNumberOfComponents(); return previousComponents == currentComponents && currentComponents == relaxedComponents - && previousComposition.length == previousComponents && currentComposition.length == currentComponents; + && previousComposition.length == previousComponents + && currentComposition.length == currentComponents; } /** @@ -9805,7 +9993,8 @@ private double getRelaxedInternalFlow(double flow, double maximumInternalFlow) { * @return maximum allowed internal tear-stream flow in kg/hr */ private double getMaximumRelaxedInternalFlowKgPerHour() { - return Math.max(1.0e3, getTotalExternalFeedFlowKgPerHour() * MAX_RELAXED_INTERNAL_TRAFFIC_TO_FEED_RATIO); + return Math.max(1.0e3, + getTotalExternalFeedFlowKgPerHour() * MAX_RELAXED_INTERNAL_TRAFFIC_TO_FEED_RATIO); } /** @@ -9847,8 +10036,8 @@ private void capStreamFlow(StreamInterface stream, double maximumFlow) { * @param energyResidual final relative energy residual * @param startTime nano time when the solve started */ - private void finalizeSolve(UUID id, int iterations, double temperatureResidual, double massResidual, - double energyResidual, long startTime) { + private void finalizeSolve(UUID id, int iterations, double temperatureResidual, + double massResidual, double energyResidual, long startTime) { err = temperatureResidual; lastIterationCount = iterations; lastTemperatureResidual = temperatureResidual; @@ -9857,7 +10046,8 @@ private void finalizeSolve(UUID id, int iterations, double temperatureResidual, lastSolveTimeSeconds = (System.nanoTime() - startTime) / 1.0e9; lastUsedFeedFlashFallback = false; - gasOutStream.setThermoSystem(trays.get(numberOfTrays - 1).getGasOutStream().getThermoSystem().clone()); + gasOutStream + .setThermoSystem(trays.get(numberOfTrays - 1).getGasOutStream().getThermoSystem().clone()); gasOutStream.setCalculationIdentifier(id); liquidOutStream.setThermoSystem(trays.get(0).getLiquidOutStream().getThermoSystem().clone()); liquidOutStream.setCalculationIdentifier(id); @@ -9868,12 +10058,13 @@ private void finalizeSolve(UUID id, int iterations, double temperatureResidual, if (!internalTrafficSatisfied()) { capInternalTrayTraffic(); lastInternalTrafficGuardReached = true; - lastInternalTrafficRatio = Math.min(getInternalTrafficRatio(), MAX_RELAXED_INTERNAL_TRAFFIC_TO_FEED_RATIO); + lastInternalTrafficRatio = + Math.min(getInternalTrafficRatio(), MAX_RELAXED_INTERNAL_TRAFFIC_TO_FEED_RATIO); } boolean fallbackProductsApplied = false; if ((!internalTrafficSatisfied() || lastMassResidual > getEffectiveMassBalanceTolerance() - || getExternalMassBalanceError() > getEffectiveMassBalanceTolerance() || bottomProductPhaseInvalid()) - && updateProductsFromOverallFeedFlash(id)) { + || getExternalMassBalanceError() > getEffectiveMassBalanceTolerance() + || bottomProductPhaseInvalid()) && updateProductsFromOverallFeedFlash(id)) { fallbackProductsApplied = true; } synchronizeColumnEndProductStreams(id); @@ -9882,19 +10073,19 @@ && updateProductsFromOverallFeedFlash(id)) { lastMassResidual = Math.max(lastMassResidual, getExternalMassBalanceError()); if (lastInternalTrafficGuardReached) { lastMassResidual = Math.max(lastMassResidual, - lastInternalTrafficRatio / MAX_SOLVED_INTERNAL_TRAFFIC_TO_FEED_RATIO); + lastInternalTrafficRatio / MAX_SOLVED_INTERNAL_TRAFFIC_TO_FEED_RATIO); } boolean anyFeedMultiPhase = false; for (List feeds : feedStreams.values()) { for (StreamInterface feed : feeds) { - if (feed.getThermoSystem().doMultiPhaseCheck()) { - anyFeedMultiPhase = true; - break; - } + if (feed.getThermoSystem().doMultiPhaseCheck()) { + anyFeedMultiPhase = true; + break; + } } if (anyFeedMultiPhase) { - break; + break; } } if (anyFeedMultiPhase) { @@ -9916,9 +10107,9 @@ && updateProductsFromOverallFeedFlash(id)) { * Synchronize condenser and reboiler product streams with the exposed column products. * *

- * Product reconciliation updates {@link #gasOutStream} and {@link #liquidOutStream}. Legacy callers may also read the - * condenser product or reboiler liquid stream directly, so those equipment-level product streams must be kept on the - * same balanced basis. + * Product reconciliation updates {@link #gasOutStream} and {@link #liquidOutStream}. Legacy + * callers may also read the condenser product or reboiler liquid stream directly, so those + * equipment-level product streams must be kept on the same balanced basis. *

* * @param id calculation identifier to assign to synchronized product streams @@ -9972,11 +10163,12 @@ private void synchronizeTerminalProductDrawStreams(UUID id) { private void updateLastSolveStatus(boolean productReconciled, boolean fallbackProductsApplied) { if (fallbackProductsApplied) { setLastSolveStatus(SolveStatus.FALLBACK_PRODUCTS, - "Public products were generated from guarded fallback flash products"); + "Public products were generated from guarded fallback flash products"); return; } if (lastInternalTrafficGuardReached) { - setLastSolveStatus(SolveStatus.FAILED, "Internal tray traffic exceeded the rigorous solved-state guard"); + setLastSolveStatus(SolveStatus.FAILED, + "Internal tray traffic exceeded the rigorous solved-state guard"); return; } if (!residualConvergenceSatisfied()) { @@ -9985,12 +10177,13 @@ private void updateLastSolveStatus(boolean productReconciled, boolean fallbackPr } if (productReconciled) { if (!isEffectiveMeshResidualToleranceEnforced()) { - setLastSolveStatus(SolveStatus.RECONCILED_PRODUCTS, - "Public products were materially reconciled after the tray solve"); - return; + setLastSolveStatus(SolveStatus.RECONCILED_PRODUCTS, + "Public products were materially reconciled after the tray solve"); + return; } } - setLastSolveStatus(SolveStatus.RIGOROUS_CONVERGED, "Tray solution satisfies active rigorous convergence gates"); + setLastSolveStatus(SolveStatus.RIGOROUS_CONVERGED, + "Tray solution satisfies active rigorous convergence gates"); } /** @@ -10024,7 +10217,8 @@ private boolean isLiquidLikeProduct(StreamInterface productStream) { return false; } SystemInterface system = productStream.getThermoSystem(); - return system.hasPhaseType("oil") || system.hasPhaseType("liquid") || system.hasPhaseType("aqueous"); + return system.hasPhaseType("oil") || system.hasPhaseType("liquid") + || system.hasPhaseType("aqueous"); } /** Cap cached internal tray outlet streams to the emergency traffic limit. */ @@ -10062,12 +10256,13 @@ private boolean updateProductsFromExternalComponentBalance(UUID id) { // tray systems makes the data flow explicit and defends against any pre-call mutation of the // public stream's thermo system that may collapse a two-phase tray system into a single // phase. - SystemInterface topTraySystem = trays.get(numberOfTrays - 1).getGasOutStream().getThermoSystem(); + SystemInterface topTraySystem = + trays.get(numberOfTrays - 1).getGasOutStream().getThermoSystem(); SystemInterface bottomTraySystem = trays.get(0).getLiquidOutStream().getThermoSystem(); double[] topProductComponentMoles = getPhaseFilteredComponentMoles(topTraySystem, true); double[] bottomProductComponentMoles = getPhaseFilteredComponentMoles(bottomTraySystem, false); if (feedComponentMoles.length != topProductComponentMoles.length - || feedComponentMoles.length != bottomProductComponentMoles.length) { + || feedComponentMoles.length != bottomProductComponentMoles.length) { return false; } @@ -10076,7 +10271,7 @@ private boolean updateProductsFromExternalComponentBalance(UUID id) { for (int componentIndex = 0; componentIndex < feedComponentMoles.length; componentIndex++) { feedTotalMoles += Math.max(0.0, feedComponentMoles[componentIndex]); currentProductTotalMoles += Math.max(0.0, topProductComponentMoles[componentIndex]) - + Math.max(0.0, bottomProductComponentMoles[componentIndex]); + + Math.max(0.0, bottomProductComponentMoles[componentIndex]); } if (feedTotalMoles <= 1.0e-20 || currentProductTotalMoles <= 1.0e-20) { return false; @@ -10091,11 +10286,11 @@ private boolean updateProductsFromExternalComponentBalance(UUID id) { double currentBottomMoles = Math.max(0.0, bottomProductComponentMoles[componentIndex]); double currentComponentMoles = currentTopMoles + currentBottomMoles; if (currentComponentMoles > 1.0e-20) { - double terminalProductMoles = Math.max(0.0, - feedComponentMoles[componentIndex] - sideDrawComponentMoles[componentIndex]); - double componentScale = terminalProductMoles / currentComponentMoles; - balancedTopProductComponentMoles[componentIndex] = currentTopMoles * componentScale; - balancedBottomProductComponentMoles[componentIndex] = currentBottomMoles * componentScale; + double terminalProductMoles = Math.max(0.0, + feedComponentMoles[componentIndex] - sideDrawComponentMoles[componentIndex]); + double componentScale = terminalProductMoles / currentComponentMoles; + balancedTopProductComponentMoles[componentIndex] = currentTopMoles * componentScale; + balancedBottomProductComponentMoles[componentIndex] = currentBottomMoles * componentScale; } topTotalMoles += balancedTopProductComponentMoles[componentIndex]; bottomTotalMoles += balancedBottomProductComponentMoles[componentIndex]; @@ -10105,8 +10300,10 @@ private boolean updateProductsFromExternalComponentBalance(UUID id) { return false; } - boolean materialChange = componentMolesMateriallyDiffer(topProductComponentMoles, balancedTopProductComponentMoles) - || componentMolesMateriallyDiffer(bottomProductComponentMoles, balancedBottomProductComponentMoles); + boolean materialChange = + componentMolesMateriallyDiffer(topProductComponentMoles, balancedTopProductComponentMoles) + || componentMolesMateriallyDiffer(bottomProductComponentMoles, + balancedBottomProductComponentMoles); updateProductStreamFromComponentMoles(gasOutStream, balancedTopProductComponentMoles, id); updateProductStreamFromComponentMoles(liquidOutStream, balancedBottomProductComponentMoles, id); @@ -10118,20 +10315,21 @@ private boolean updateProductsFromExternalComponentBalance(UUID id) { if (hasReboiler && !isLiquidLikeProduct(liquidOutStream)) { double terminalProductTotalMoles = 0.0; for (int componentIndex = 0; componentIndex < feedComponentMoles.length; componentIndex++) { - terminalProductTotalMoles += Math.max(0.0, - feedComponentMoles[componentIndex] - sideDrawComponentMoles[componentIndex]); + terminalProductTotalMoles += Math.max(0.0, + feedComponentMoles[componentIndex] - sideDrawComponentMoles[componentIndex]); } if (terminalProductTotalMoles > 1.0e-20 && currentProductTotalMoles > 1.0e-20) { - double overallScale = terminalProductTotalMoles / currentProductTotalMoles; - for (int componentIndex = 0; componentIndex < feedComponentMoles.length; componentIndex++) { - balancedTopProductComponentMoles[componentIndex] = Math.max(0.0, topProductComponentMoles[componentIndex]) - * overallScale; - balancedBottomProductComponentMoles[componentIndex] = Math.max(0.0, - bottomProductComponentMoles[componentIndex]) * overallScale; - } - updateProductStreamFromComponentMoles(gasOutStream, balancedTopProductComponentMoles, id); - updateProductStreamFromComponentMoles(liquidOutStream, balancedBottomProductComponentMoles, id); - materialChange = true; + double overallScale = terminalProductTotalMoles / currentProductTotalMoles; + for (int componentIndex = 0; componentIndex < feedComponentMoles.length; componentIndex++) { + balancedTopProductComponentMoles[componentIndex] = + Math.max(0.0, topProductComponentMoles[componentIndex]) * overallScale; + balancedBottomProductComponentMoles[componentIndex] = + Math.max(0.0, bottomProductComponentMoles[componentIndex]) * overallScale; + } + updateProductStreamFromComponentMoles(gasOutStream, balancedTopProductComponentMoles, id); + updateProductStreamFromComponentMoles(liquidOutStream, balancedBottomProductComponentMoles, + id); + materialChange = true; } } @@ -10142,7 +10340,8 @@ private boolean updateProductsFromExternalComponentBalance(UUID id) { // reboiler T/P is producing a spurious vapor product. This avoids the overall-feed-flash // fallback for Inside-Out, Matrix-IO, and Newton solvers on small heavy-rich columns. if (hasReboiler && !isLiquidLikeProduct(liquidOutStream)) { - updateProductStreamWithForcedPhase(liquidOutStream, balancedBottomProductComponentMoles, "liquid", id); + updateProductStreamWithForcedPhase(liquidOutStream, balancedBottomProductComponentMoles, + "liquid", id); materialChange = true; } return materialChange; @@ -10177,30 +10376,32 @@ private boolean componentMolesMateriallyDiffer(double[] before, double[] after) * Captures the raw terminal tray draw streams used by product-draw residual diagnostics. * *

- * Product reconciliation and guarded fallback updates can change the public column products after the tray solver has - * produced terminal draws. MESH diagnostics must compare public products to these raw terminal draws rather than to - * synchronized clones of the public products. + * Product reconciliation and guarded fallback updates can change the public column products after + * the tray solver has produced terminal draws. MESH diagnostics must compare public products to + * these raw terminal draws rather than to synchronized clones of the public products. *

* * @param id calculation identifier to assign to the captured draw streams */ private void captureTerminalProductDrawStreams(UUID id) { - terminalGasProductDrawStream = new Stream("", - trays.get(numberOfTrays - 1).getGasOutStream().getThermoSystem().clone()); + terminalGasProductDrawStream = + new Stream("", trays.get(numberOfTrays - 1).getGasOutStream().getThermoSystem().clone()); terminalGasProductDrawStream.setCalculationIdentifier(id); - terminalLiquidProductDrawStream = new Stream("", trays.get(0).getLiquidOutStream().getThermoSystem().clone()); + terminalLiquidProductDrawStream = + new Stream("", trays.get(0).getLiquidOutStream().getThermoSystem().clone()); terminalLiquidProductDrawStream.setCalculationIdentifier(id); } /** - * Replaces a product stream fluid with the same thermodynamic model at the current stream temperature and pressure - * but with specified component mole amounts. + * Replaces a product stream fluid with the same thermodynamic model at the current stream + * temperature and pressure but with specified component mole amounts. * * @param productStream stream to update * @param componentMoles component mole amounts on the stream-flow basis * @param id calculation identifier to assign after the update */ - private void updateProductStreamFromComponentMoles(StreamInterface productStream, double[] componentMoles, UUID id) { + private void updateProductStreamFromComponentMoles(StreamInterface productStream, + double[] componentMoles, UUID id) { SystemInterface balancedSystem = productStream.getThermoSystem().clone(); double productTemperature = productStream.getTemperature("K"); double productPressure = productStream.getPressure("bara"); @@ -10217,14 +10418,15 @@ private void updateProductStreamFromComponentMoles(StreamInterface productStream } /** - * Replace a product stream fluid with the same thermodynamic model at the product stream's own temperature and - * pressure, forcing a single phase identity instead of running a TP flash. + * Replace a product stream fluid with the same thermodynamic model at the product stream's own + * temperature and pressure, forcing a single phase identity instead of running a TP flash. * *

- * Used as a phase-preserving rescue when an accelerator solver (Inside-Out, Matrix-IO, Newton) converges to a tray-0 - * oil-phase composition that, when re-flashed in isolation at the reboiler T/P, collapses to single-phase gas. The - * tray system itself was two-phase by construction, so the moles drawn from it are valid; forcing the phase preserves - * the rigorous solver result and avoids the spurious overall-feed-flash fallback that otherwise triggers via + * Used as a phase-preserving rescue when an accelerator solver (Inside-Out, Matrix-IO, Newton) + * converges to a tray-0 oil-phase composition that, when re-flashed in isolation at the reboiler + * T/P, collapses to single-phase gas. The tray system itself was two-phase by construction, so + * the moles drawn from it are valid; forcing the phase preserves the rigorous solver result and + * avoids the spurious overall-feed-flash fallback that otherwise triggers via * {@code bottomProductPhaseInvalid()}. *

* @@ -10233,8 +10435,8 @@ private void updateProductStreamFromComponentMoles(StreamInterface productStream * @param phaseTypeName phase type description ("gas" or "liquid") * @param id calculation identifier to assign after the update */ - private void updateProductStreamWithForcedPhase(StreamInterface productStream, double[] componentMoles, - String phaseTypeName, UUID id) { + private void updateProductStreamWithForcedPhase(StreamInterface productStream, + double[] componentMoles, String phaseTypeName, UUID id) { SystemInterface productSystem = productStream.getThermoSystem().clone(); double productTemperature = productStream.getTemperature("K"); double productPressure = productStream.getPressure("bara"); @@ -10254,8 +10456,9 @@ private void updateProductStreamWithForcedPhase(StreamInterface productStream, d * Update public products from an overall equilibrium flash of all external feeds. * *

- * This fallback is used only after the tray solver has produced non-physical internal traffic. It gives bounded, - * mass-conserving products for diagnostics without claiming that the rigorous tray MESH problem has converged. + * This fallback is used only after the tray solver has produced non-physical internal traffic. It + * gives bounded, mass-conserving products for diagnostics without claiming that the rigorous tray + * MESH problem has converged. *

* * @param id calculation identifier to assign to fallback products @@ -10303,9 +10506,9 @@ private boolean updateProductsFromOverallFeedFlash(UUID id) { * Create fallback products from a bounded shortcut equilibrium split. * *

- * The split uses bottom terminal K-values and a Rachford-Rice vapor-fraction estimate to place volatile components - * preferentially in the vapor and heavy components preferentially in the liquid. It is only used when an overall TP - * flash does not expose both gas and liquid phases. + * The split uses bottom terminal K-values and a Rachford-Rice vapor-fraction estimate to place + * volatile components preferentially in the vapor and heavy components preferentially in the + * liquid. It is only used when an overall TP flash does not expose both gas and liquid phases. *

* * @param feedSystem combined external feed system @@ -10329,7 +10532,8 @@ private boolean updateProductsFromShortcutEquilibriumSplit(SystemInterface feedS kSystem.init(0); kSystem.init(1); - double vaporFraction = estimateShortcutVaporFraction(feedComponentMoles, kSystem, getCurrentProductVaporFraction()); + double vaporFraction = estimateShortcutVaporFraction(feedComponentMoles, kSystem, + getCurrentProductVaporFraction()); double vaporToLiquidRatio = vaporFraction / Math.max(1.0e-12, 1.0 - vaporFraction); double[] topComponentMoles = new double[feedComponentMoles.length]; double[] bottomComponentMoles = new double[feedComponentMoles.length]; @@ -10338,14 +10542,15 @@ private boolean updateProductsFromShortcutEquilibriumSplit(SystemInterface feedS double kValue = Math.max(1.0e-8, kSystem.getComponent(componentIndex).getK()); double denominator = 1.0 + vaporToLiquidRatio * kValue; if (!Double.isFinite(denominator) || denominator <= 1.0e-12) { - denominator = 1.0e-12; + denominator = 1.0e-12; } bottomComponentMoles[componentIndex] = feedMoles / denominator; topComponentMoles[componentIndex] = feedMoles - bottomComponentMoles[componentIndex]; } updateProductStreamFromComponentMolesAsPhase(gasOutStream, topComponentMoles, "gas", id); - updateProductStreamFromComponentMolesAsPhase(liquidOutStream, bottomComponentMoles, "liquid", id); + updateProductStreamFromComponentMolesAsPhase(liquidOutStream, bottomComponentMoles, "liquid", + id); return true; } @@ -10367,8 +10572,10 @@ private double estimateShortcutVaporFraction(double[] feedComponentMoles, System return clampShortcutVaporFraction(fallbackVaporFraction); } - double liquidLimitResidual = evaluateRachfordRiceResidual(feedComponentMoles, kSystem, totalMoles, 0.0); - double vaporLimitResidual = evaluateRachfordRiceResidual(feedComponentMoles, kSystem, totalMoles, 1.0); + double liquidLimitResidual = + evaluateRachfordRiceResidual(feedComponentMoles, kSystem, totalMoles, 0.0); + double vaporLimitResidual = + evaluateRachfordRiceResidual(feedComponentMoles, kSystem, totalMoles, 1.0); if (!Double.isFinite(liquidLimitResidual) || !Double.isFinite(vaporLimitResidual)) { return clampShortcutVaporFraction(fallbackVaporFraction); } @@ -10383,14 +10590,15 @@ private double estimateShortcutVaporFraction(double[] feedComponentMoles, System double upperVaporFraction = 1.0; for (int iteration = 0; iteration < 80; iteration++) { double trialVaporFraction = 0.5 * (lowerVaporFraction + upperVaporFraction); - double residual = evaluateRachfordRiceResidual(feedComponentMoles, kSystem, totalMoles, trialVaporFraction); + double residual = + evaluateRachfordRiceResidual(feedComponentMoles, kSystem, totalMoles, trialVaporFraction); if (!Double.isFinite(residual)) { - return clampShortcutVaporFraction(fallbackVaporFraction); + return clampShortcutVaporFraction(fallbackVaporFraction); } if (residual > 0.0) { - lowerVaporFraction = trialVaporFraction; + lowerVaporFraction = trialVaporFraction; } else { - upperVaporFraction = trialVaporFraction; + upperVaporFraction = trialVaporFraction; } } return clampShortcutVaporFraction(0.5 * (lowerVaporFraction + upperVaporFraction)); @@ -10405,15 +10613,15 @@ private double estimateShortcutVaporFraction(double[] feedComponentMoles, System * @param vaporFraction trial vapor fraction from zero to one * @return Rachford-Rice residual */ - private double evaluateRachfordRiceResidual(double[] feedComponentMoles, SystemInterface kSystem, double totalMoles, - double vaporFraction) { + private double evaluateRachfordRiceResidual(double[] feedComponentMoles, SystemInterface kSystem, + double totalMoles, double vaporFraction) { double residual = 0.0; for (int componentIndex = 0; componentIndex < feedComponentMoles.length; componentIndex++) { double z = Math.max(0.0, feedComponentMoles[componentIndex]) / totalMoles; double kValue = Math.max(1.0e-8, kSystem.getComponent(componentIndex).getK()); double denominator = 1.0 + vaporFraction * (kValue - 1.0); if (!Double.isFinite(denominator) || denominator <= 1.0e-12) { - return Double.NaN; + return Double.NaN; } residual += z * (kValue - 1.0) / denominator; } @@ -10456,8 +10664,8 @@ private double getCurrentProductVaporFraction() { * @param phaseTypeName phase type name to assign to the product * @param id calculation identifier to assign to the product */ - private void updateProductStreamFromComponentMolesAsPhase(StreamInterface productStream, double[] componentMoles, - String phaseTypeName, UUID id) { + private void updateProductStreamFromComponentMolesAsPhase(StreamInterface productStream, + double[] componentMoles, String phaseTypeName, UUID id) { SystemInterface productSystem = productStream.getThermoSystem().clone(); productSystem.setMolarFlowRates(componentMoles); productSystem.setTemperature(getFallbackProductTemperature()); @@ -10552,7 +10760,7 @@ private int findPhaseIndex(SystemInterface system, String phaseTypeName, int fal } for (int phaseIndex = 0; phaseIndex < numberOfPhases; phaseIndex++) { if (phaseTypeName.equals(system.getPhase(phaseIndex).getPhaseTypeName())) { - return phaseIndex; + return phaseIndex; } } return Math.max(0, Math.min(fallbackPhaseIndex, numberOfPhases - 1)); @@ -10572,12 +10780,12 @@ private int findLiquidPhaseIndex(SystemInterface system) { for (int phaseIndex = 0; phaseIndex < numberOfPhases; phaseIndex++) { String phaseTypeName = system.getPhase(phaseIndex).getPhaseTypeName(); if ("liquid".equals(phaseTypeName) || "oil".equals(phaseTypeName)) { - return phaseIndex; + return phaseIndex; } } for (int phaseIndex = 0; phaseIndex < numberOfPhases; phaseIndex++) { if (!"gas".equals(system.getPhase(phaseIndex).getPhaseTypeName())) { - return phaseIndex; + return phaseIndex; } } return -1; @@ -10590,7 +10798,8 @@ private int findLiquidPhaseIndex(SystemInterface system) { * @param phaseIndex phase index to extract * @return normalized single-phase system */ - private SystemInterface createNormalizedPhaseSystem(SystemInterface sourceSystem, int phaseIndex) { + private SystemInterface createNormalizedPhaseSystem(SystemInterface sourceSystem, + int phaseIndex) { SystemInterface phaseSystem = sourceSystem.phaseToSystem(phaseIndex); double targetMoles = getNormalizedPhaseMoles(sourceSystem, phaseIndex); scaleSystemMoles(phaseSystem, targetMoles); @@ -10612,7 +10821,7 @@ private double getNormalizedPhaseMoles(SystemInterface sourceSystem, int phaseIn for (int i = 0; i < sourceSystem.getNumberOfPhases(); i++) { double moles = sourceSystem.getPhase(i).getNumberOfMolesInPhase(); if (Double.isFinite(moles) && moles > 0.0) { - phaseMoleSum += moles; + phaseMoleSum += moles; } } if (totalMoles > 0.0 && phaseMoleSum > 0.0) { @@ -10636,10 +10845,12 @@ private void scaleSystemMoles(SystemInterface system, double targetMoles) { double scaleFactor = Math.max(0.0, targetMoles) / currentMoles; for (int phaseIndex = 0; phaseIndex < system.getMaxNumberOfPhases(); phaseIndex++) { for (int componentIndex = 0; componentIndex < system.getPhase(phaseIndex) - .getNumberOfComponents(); componentIndex++) { - double moles = system.getPhase(phaseIndex).getComponent(componentIndex).getNumberOfMolesInPhase() * scaleFactor; - system.getPhase(phaseIndex).getComponent(componentIndex).setNumberOfMolesInPhase(moles); - system.getPhase(phaseIndex).getComponent(componentIndex).setNumberOfmoles(moles); + .getNumberOfComponents(); componentIndex++) { + double moles = + system.getPhase(phaseIndex).getComponent(componentIndex).getNumberOfMolesInPhase() + * scaleFactor; + system.getPhase(phaseIndex).getComponent(componentIndex).setNumberOfMolesInPhase(moles); + system.getPhase(phaseIndex).getComponent(componentIndex).setNumberOfmoles(moles); } } system.setTotalNumberOfMoles(Math.max(0.0, targetMoles)); @@ -10669,9 +10880,9 @@ private double getMaximumTrayOutletFlowKgPerHour() { double maximumInternalFlow = 0.0; for (int trayIndex = 0; trayIndex < numberOfTrays; trayIndex++) { maximumInternalFlow = Math.max(maximumInternalFlow, - getFiniteAbsoluteFlow(trays.get(trayIndex).getGasOutStream())); + getFiniteAbsoluteFlow(trays.get(trayIndex).getGasOutStream())); maximumInternalFlow = Math.max(maximumInternalFlow, - getFiniteAbsoluteFlow(trays.get(trayIndex).getLiquidOutStream())); + getFiniteAbsoluteFlow(trays.get(trayIndex).getLiquidOutStream())); } return maximumInternalFlow; } @@ -10686,7 +10897,7 @@ private double getTotalExternalFeedFlowKgPerHour() { for (StreamInterface feed : getAllExternalFeedStreams()) { double flow = feed.getFlowRate("kg/hr"); if (Double.isFinite(flow)) { - totalFeedFlow += Math.abs(flow); + totalFeedFlow += Math.abs(flow); } } return totalFeedFlow; @@ -10706,8 +10917,8 @@ private double getFiniteAbsoluteFlow(StreamInterface stream) { /** * Calculates total component mole amounts entering the column through all external feeds. * - * @return component mole amounts on the stream-flow basis used by NeqSim streams, or an empty array if external feeds - * do not share a common component basis + * @return component mole amounts on the stream-flow basis used by NeqSim streams, or an empty + * array if external feeds do not share a common component basis */ private double[] getFeedComponentMoles() { int componentCount = getNumberOfComponentsFromFeeds(); @@ -10718,10 +10929,10 @@ private double[] getFeedComponentMoles() { for (StreamInterface feed : getAllExternalFeedStreams()) { double[] componentMoles = getComponentMoles(feed.getThermoSystem()); if (componentMoles.length != componentCount) { - return new double[0]; + return new double[0]; } for (int componentIndex = 0; componentIndex < feedComponentMoles.length; componentIndex++) { - feedComponentMoles[componentIndex] += componentMoles[componentIndex]; + feedComponentMoles[componentIndex] += componentMoles[componentIndex]; } } return feedComponentMoles; @@ -10738,10 +10949,11 @@ private double[] getSideDrawComponentMoles(int componentCount) { for (StreamInterface sideDrawStream : getSideDrawStreams()) { double[] componentMoles = getComponentMoles(sideDrawStream.getThermoSystem()); if (componentMoles.length != componentCount) { - continue; + continue; } - for (int componentIndex = 0; componentIndex < sideDrawComponentMoles.length; componentIndex++) { - sideDrawComponentMoles[componentIndex] += componentMoles[componentIndex]; + for (int componentIndex = + 0; componentIndex < sideDrawComponentMoles.length; componentIndex++) { + sideDrawComponentMoles[componentIndex] += componentMoles[componentIndex]; } } return sideDrawComponentMoles; @@ -10770,10 +10982,11 @@ private double[] getComponentMoles(SystemInterface system) { for (int componentIndex = 0; componentIndex < componentMoles.length; componentIndex++) { double componentTotal = 0.0; for (int phaseIndex = 0; phaseIndex < system.getNumberOfPhases(); phaseIndex++) { - componentTotal += system.getPhase(phaseIndex).getComponent(componentIndex).getNumberOfMolesInPhase(); + componentTotal += + system.getPhase(phaseIndex).getComponent(componentIndex).getNumberOfMolesInPhase(); } if (componentTotal <= 0.0 && system.getNumberOfPhases() > 0) { - componentTotal = system.getPhase(0).getComponent(componentIndex).getNumberOfmoles(); + componentTotal = system.getPhase(0).getComponent(componentIndex).getNumberOfmoles(); } componentMoles[componentIndex] = componentTotal; } @@ -10784,15 +10997,17 @@ private double[] getComponentMoles(SystemInterface system) { * Sum component mole amounts contributed by gas-like or liquid-like phases only. * *

- * Tray terminal thermo systems can hold both an ascending gas phase and a descending liquid phase. When reconciling a - * single-phase public product against the external feed mass balance, the moles attributed to the product must come - * only from the relevant phase. If no matching phase is present (e.g. a single-phase oil reboiler or pure-vapor - * distillate), this method falls back to {@link #getComponentMoles(SystemInterface)} so the reconciliation step still - * has a non-zero composition to scale. + * Tray terminal thermo systems can hold both an ascending gas phase and a descending liquid + * phase. When reconciling a single-phase public product against the external feed mass balance, + * the moles attributed to the product must come only from the relevant phase. If no matching + * phase is present (e.g. a single-phase oil reboiler or pure-vapor distillate), this method falls + * back to {@link #getComponentMoles(SystemInterface)} so the reconciliation step still has a + * non-zero composition to scale. *

* * @param system thermodynamic system to inspect - * @param gasPhase {@code true} to sum gas-like phases, {@code false} to sum oil/liquid/aqueous phases + * @param gasPhase {@code true} to sum gas-like phases, {@code false} to sum oil/liquid/aqueous + * phases * @return component mole amounts contributed by the matching phases */ private double[] getPhaseFilteredComponentMoles(SystemInterface system, boolean gasPhase) { @@ -10802,18 +11017,18 @@ private double[] getPhaseFilteredComponentMoles(SystemInterface system, boolean String phaseName = system.getPhase(phaseIndex).getPhaseTypeName(); boolean matches; if (gasPhase) { - matches = "gas".equalsIgnoreCase(phaseName); + matches = "gas".equalsIgnoreCase(phaseName); } else { - matches = "oil".equalsIgnoreCase(phaseName) || "liquid".equalsIgnoreCase(phaseName) - || "aqueous".equalsIgnoreCase(phaseName); + matches = "oil".equalsIgnoreCase(phaseName) || "liquid".equalsIgnoreCase(phaseName) + || "aqueous".equalsIgnoreCase(phaseName); } if (!matches) { - continue; + continue; } anyMatch = true; for (int componentIndex = 0; componentIndex < componentMoles.length; componentIndex++) { - componentMoles[componentIndex] += system.getPhase(phaseIndex).getComponent(componentIndex) - .getNumberOfMolesInPhase(); + componentMoles[componentIndex] += + system.getPhase(phaseIndex).getComponent(componentIndex).getNumberOfMolesInPhase(); } } if (!anyMatch) { @@ -10880,8 +11095,9 @@ private void resetMatrixInsideOutDiagnostics() { } /** - * Prints a simple energy balance for each tray to the console. The method calculates the total enthalpy of all inlet - * streams and compares it with the outlet enthalpy in order to highlight any discrepancies in the column setup. + * Prints a simple energy balance for each tray to the console. The method calculates the total + * enthalpy of all inlet streams and compares it with the outlet enthalpy in order to highlight + * any discrepancies in the column setup. */ public void energyBalanceCheck() { double[] energyInput = new double[numberOfTrays]; @@ -10890,20 +11106,20 @@ public void energyBalanceCheck() { for (int i = 0; i < numberOfTrays; i++) { int numberOfInputStreams = trays.get(i).getNumberOfInputStreams(); for (int j = 0; j < numberOfInputStreams; j++) { - energyInput[i] += trays.get(i).getStream(j).getFluid().getEnthalpy(); + energyInput[i] += trays.get(i).getStream(j).getFluid().getEnthalpy(); } energyOutput[i] += trays.get(i).getGasOutStream().getFluid().getEnthalpy(); energyOutput[i] += trays.get(i).getLiquidOutStream().getFluid().getEnthalpy(); energyBalance[i] = energyInput[i] - energyOutput[i]; - System.out.println("Tray " + i + ", #in=" + numberOfInputStreams + ", eIn=" + energyInput[i] + ", eOut=" - + energyOutput[i] + ", balance=" + energyBalance[i]); + System.out.println("Tray " + i + ", #in=" + numberOfInputStreams + ", eIn=" + energyInput[i] + + ", eOut=" + energyOutput[i] + ", balance=" + energyBalance[i]); } } /** - * The main method demonstrates the creation and operation of a distillation column using the NeqSim library. It - * performs the following steps: + * The main method demonstrates the creation and operation of a distillation column using the + * NeqSim library. It performs the following steps: *
    *
  1. Creates a test thermodynamic system with methane, ethane, and propane components.
  2. *
  3. Performs a TP flash calculation on the test system.
  4. @@ -10911,7 +11127,8 @@ public void energyBalanceCheck() { *
  5. Constructs a distillation column with 5 trays, a reboiler, and a condenser.
  6. *
  7. Adds the two feed streams to the distillation column at tray 3.
  8. *
  9. Builds and runs the process system.
  10. - *
  11. Displays the results of the distillation column, including the gas and liquid output streams.
  12. + *
  13. Displays the results of the distillation column, including the gas and liquid output + * streams.
  14. *
* * @param args command line arguments (not used) @@ -10919,14 +11136,15 @@ public void energyBalanceCheck() { @ExcludeFromJacocoGeneratedReport public static void main(String[] args) { // Create a test system - neqsim.thermo.system.SystemInterface testSystem = new neqsim.thermo.system.SystemSrkEos(273.15 + 25.0, 15.0); + neqsim.thermo.system.SystemInterface testSystem = + new neqsim.thermo.system.SystemSrkEos(273.15 + 25.0, 15.0); testSystem.addComponent("methane", 10.00); testSystem.addComponent("ethane", 10.0); testSystem.addComponent("propane", 10.0); testSystem.createDatabase(true); testSystem.setMixingRule(2); - neqsim.thermodynamicoperations.ThermodynamicOperations ops = new neqsim.thermodynamicoperations.ThermodynamicOperations( - testSystem); + neqsim.thermodynamicoperations.ThermodynamicOperations ops = + new neqsim.thermodynamicoperations.ThermodynamicOperations(testSystem); ops.TPflash(); testSystem.display(); @@ -10942,7 +11160,8 @@ public static void main(String[] args) { column.addFeedStream(feed2, 3); // Build process - neqsim.process.processmodel.ProcessSystem operations = new neqsim.process.processmodel.ProcessSystem(); + neqsim.process.processmodel.ProcessSystem operations = + new neqsim.process.processmodel.ProcessSystem(); operations.add(feed1); operations.add(feed2); operations.add(column); @@ -10979,9 +11198,9 @@ public ValidationResult validateSetup() { * Validate only the configured top and bottom column specifications. * *

- * This focused validator is useful before a solve when users are scripting product-purity, component-recovery, - * flow-rate, reflux, or duty specifications and want actionable diagnostics without validating the full equipment - * setup. + * This focused validator is useful before a solve when users are scripting product-purity, + * component-recovery, flow-rate, reflux, or duty specifications and want actionable diagnostics + * without validating the full equipment setup. *

* * @return validation result containing specification errors and warnings @@ -11002,14 +11221,16 @@ private void validateCommercialActiveBounds(ValidationResult result) { validateSpecificationActiveBound(result, bottomSpecification); for (ColumnSideDrawSpecification specification : sideDrawSpecifications) { if (specification.getTargetFlowRate() > 1.0e8) { - result.addWarning("sidedraw.activeBound", "Side-draw target flow is far above typical column traffic", - "Use homotopy steps or check units before solving the column"); + result.addWarning("sidedraw.activeBound", + "Side-draw target flow is far above typical column traffic", + "Use homotopy steps or check units before solving the column"); } } for (ColumnPumparound pumparound : pumparounds) { if (pumparound.getDrawFraction() > 0.90) { - result.addWarning("pumparound.activeBound", "Pumparound draw fraction is close to the full tray liquid traffic", - "Reduce draw fraction or solve with staged continuation before rigorous refinement"); + result.addWarning("pumparound.activeBound", + "Pumparound draw fraction is close to the full tray liquid traffic", + "Reduce draw fraction or solve with staged continuation before rigorous refinement"); } } } @@ -11020,16 +11241,18 @@ private void validateCommercialActiveBounds(ValidationResult result) { * @param result validation result receiving active-bound warnings * @param specification column specification to screen */ - private void validateSpecificationActiveBound(ValidationResult result, ColumnSpecification specification) { + private void validateSpecificationActiveBound(ValidationResult result, + ColumnSpecification specification) { if (specification == null) { return; } ColumnSpecification.SpecificationType type = specification.getType(); if ((type == ColumnSpecification.SpecificationType.PRODUCT_PURITY - || type == ColumnSpecification.SpecificationType.COMPONENT_RECOVERY) - && isNearFractionBound(specification.getTargetValue())) { - result.addWarning("specification.activeBound", "Column specification target is close to a hard fraction bound", - "Relax the target for an initial homotopy solve and tighten it after convergence"); + || type == ColumnSpecification.SpecificationType.COMPONENT_RECOVERY) + && isNearFractionBound(specification.getTargetValue())) { + result.addWarning("specification.activeBound", + "Column specification target is close to a hard fraction bound", + "Relax the target for an initial homotopy solve and tighten it after convergence"); } } @@ -11040,7 +11263,8 @@ && isNearFractionBound(specification.getTargetValue())) { * @return {@code true} when the value lies close to either hard bound */ private boolean isNearFractionBound(double value) { - return value <= ACTIVE_BOUND_FRACTION_TOLERANCE || value >= 1.0 - ACTIVE_BOUND_FRACTION_TOLERANCE; + return value <= ACTIVE_BOUND_FRACTION_TOLERANCE + || value >= 1.0 - ACTIVE_BOUND_FRACTION_TOLERANCE; } /** @@ -11050,15 +11274,17 @@ private boolean isNearFractionBound(double value) { */ private void validateColumnGeometry(ValidationResult result) { if (numberOfTrays <= 0) { - result.addError("column.trays", "Column has no trays", "Create the column with at least one equilibrium tray"); + result.addError("column.trays", "Column has no trays", + "Create the column with at least one equilibrium tray"); } if (!Double.isFinite(internalDiameter) || internalDiameter <= 0.0) { result.addError("column.diameter", "Internal column diameter is not positive and finite", - "Set a positive diameter with column.setInternalDiameter(valueInMeters)"); + "Set a positive diameter with column.setInternalDiameter(valueInMeters)"); } if (!hasCondenser && !hasReboiler) { - result.addWarning("configuration", "Column has neither condenser nor reboiler - acting as a stripper/absorber", - "Set hasCondenser=true and/or hasReboiler=true in constructor if separation is needed"); + result.addWarning("configuration", + "Column has neither condenser nor reboiler - acting as a stripper/absorber", + "Set hasCondenser=true and/or hasReboiler=true in constructor if separation is needed"); } } @@ -11071,23 +11297,24 @@ private void validateColumnFeeds(ValidationResult result) { if (feedStreams.isEmpty() && unassignedFeedStreams.isEmpty()) { result.addError("stream", "No feed stream connected to distillation column", - "Add a feed stream: column.addFeedStream(stream, feedTrayNumber)"); + "Add a feed stream: column.addFeedStream(stream, feedTrayNumber)"); } for (Entry> feedEntry : feedStreams.entrySet()) { Integer trayNumber = feedEntry.getKey(); - if (trayNumber == null || trayNumber.intValue() < 0 || trayNumber.intValue() >= numberOfTrays) { - result.addError("stream.feedTray", "Feed tray index is outside the column tray range", - "Use a feed tray from 0 to numberOfTrays - 1"); + if (trayNumber == null || trayNumber.intValue() < 0 + || trayNumber.intValue() >= numberOfTrays) { + result.addError("stream.feedTray", "Feed tray index is outside the column tray range", + "Use a feed tray from 0 to numberOfTrays - 1"); } List streams = feedEntry.getValue(); if (streams == null || streams.isEmpty()) { - result.addWarning("stream.feedTray", "Feed tray has no streams assigned", - "Remove the empty tray assignment or add a feed stream"); - continue; + result.addWarning("stream.feedTray", "Feed tray has no streams assigned", + "Remove the empty tray assignment or add a feed stream"); + continue; } for (StreamInterface feedStream : streams) { - validateFeedStream(result, feedStream); + validateFeedStream(result, feedStream); } } @@ -11105,18 +11332,18 @@ private void validateColumnFeeds(ValidationResult result) { private void validateFeedStream(ValidationResult result, StreamInterface feedStream) { if (feedStream == null) { result.addError("stream.feed", "A null feed stream is connected", - "Remove the null feed or replace it with a Stream instance"); + "Remove the null feed or replace it with a Stream instance"); return; } SystemInterface fluid = feedStream.getFluid(); if (fluid == null) { result.addError("stream.feed", "Feed stream has no thermodynamic system", - "Construct the stream with a fluid before adding it to the column"); + "Construct the stream with a fluid before adding it to the column"); return; } if (fluid.getTotalNumberOfMoles() <= 0.0) { result.addWarning("stream.feed", "Feed stream has zero or negative total moles", - "Set a positive flow rate on the feed stream before running the column"); + "Set a positive flow rate on the feed stream before running the column"); } } @@ -11128,15 +11355,16 @@ private void validateFeedStream(ValidationResult result, StreamInterface feedStr private void validateColumnPressureProfile(ValidationResult result) { if (topTrayPressure == 0.0 || (Double.isFinite(topTrayPressure) && topTrayPressure < 0.0)) { result.addWarning("column.pressure", "Top pressure is not explicitly set", - "Call column.setTopPressure(value) or allow the solver to initialize from feed pressure"); + "Call column.setTopPressure(value) or allow the solver to initialize from feed pressure"); } - if (bottomTrayPressure == 0.0 || (Double.isFinite(bottomTrayPressure) && bottomTrayPressure < 0.0)) { + if (bottomTrayPressure == 0.0 + || (Double.isFinite(bottomTrayPressure) && bottomTrayPressure < 0.0)) { result.addWarning("column.pressure", "Bottom pressure is not explicitly set", - "Call column.setBottomPressure(value) or allow the solver to initialize from feed pressure"); + "Call column.setBottomPressure(value) or allow the solver to initialize from feed pressure"); } if (topTrayPressure > 0.0 && bottomTrayPressure > 0.0 && topTrayPressure > bottomTrayPressure) { result.addWarning("column.pressure", "Top pressure is higher than bottom pressure", - "Check the pressure profile; distillation columns normally have bottom pressure >= top pressure"); + "Check the pressure profile; distillation columns normally have bottom pressure >= top pressure"); } } @@ -11148,31 +11376,33 @@ private void validateColumnPressureProfile(ValidationResult result) { private void validateColumnNumerics(ValidationResult result) { if (solverType == null) { result.addError("solver", "Solver type is null", - "Use column.setSolverType(DistillationColumn.SolverType.AUTO) or another solver"); + "Use column.setSolverType(DistillationColumn.SolverType.AUTO) or another solver"); } if (maxNumberOfIterations <= 0) { result.addError("solver.iterations", "Maximum iteration count is not positive", - "Set a positive value with column.setMaxNumberOfIterations(iterations)"); + "Set a positive value with column.setMaxNumberOfIterations(iterations)"); } if (!isPositiveFiniteValue(temperatureTolerance)) { result.addError("solver.temperatureTolerance", "Temperature tolerance is not positive finite", - "Set a positive finite tolerance with column.setTemperatureTolerance(tol)"); + "Set a positive finite tolerance with column.setTemperatureTolerance(tol)"); } if (!isPositiveFiniteValue(massBalanceTolerance)) { - result.addError("solver.massBalanceTolerance", "Mass balance tolerance is not positive finite", - "Set a positive finite tolerance with column.setMassBalanceTolerance(tol)"); + result.addError("solver.massBalanceTolerance", + "Mass balance tolerance is not positive finite", + "Set a positive finite tolerance with column.setMassBalanceTolerance(tol)"); } if (!isPositiveFiniteValue(enthalpyBalanceTolerance)) { result.addError("solver.enthalpyTolerance", "Enthalpy tolerance is not positive finite", - "Set a positive finite tolerance with column.setEnthalpyBalanceTolerance(tol)"); + "Set a positive finite tolerance with column.setEnthalpyBalanceTolerance(tol)"); } if (!isPositiveFiniteValue(meshResidualTolerance)) { result.addError("solver.meshTolerance", "MESH residual tolerance is not positive finite", - "Set a positive finite tolerance with column.setMeshResidualTolerance(tol)"); + "Set a positive finite tolerance with column.setMeshResidualTolerance(tol)"); } - if (murphreeEfficiency < 0.0 || murphreeEfficiency > 1.0 || !Double.isFinite(murphreeEfficiency)) { + if (murphreeEfficiency < 0.0 || murphreeEfficiency > 1.0 + || !Double.isFinite(murphreeEfficiency)) { result.addError("efficiency", "Murphree efficiency is outside 0..1", - "Set tray efficiency with column.setMurphreeEfficiency(valueBetweenZeroAndOne)"); + "Set tray efficiency with column.setMurphreeEfficiency(valueBetweenZeroAndOne)"); } } @@ -11183,7 +11413,8 @@ private void validateColumnNumerics(ValidationResult result) { */ private void validateColumnSpecifications(ValidationResult result) { validateColumnSpecification(result, topSpecification, ColumnSpecification.ProductLocation.TOP); - validateColumnSpecification(result, bottomSpecification, ColumnSpecification.ProductLocation.BOTTOM); + validateColumnSpecification(result, bottomSpecification, + ColumnSpecification.ProductLocation.BOTTOM); validateProductFlowSpecificationsAgainstFeed(result); } @@ -11201,18 +11432,22 @@ private void validateProductFlowSpecificationsAgainstFeed(ValidationResult resul double topProductFlowTarget = getProductFlowSpecificationTarget(topSpecification); double bottomProductFlowTarget = getProductFlowSpecificationTarget(bottomSpecification); if (topProductFlowTarget > totalFeedFlow * (1.0 + 1.0e-12)) { - result.addError("specification.productFlow", "Product-flow target for the top product exceeds total feed flow", - "Use a top product-flow target below the total feed flow or check the mol/hr units"); + result.addError("specification.productFlow", + "Product-flow target for the top product exceeds total feed flow", + "Use a top product-flow target below the total feed flow or check the mol/hr units"); } if (bottomProductFlowTarget > totalFeedFlow * (1.0 + 1.0e-12)) { - result.addError("specification.productFlow", "Product-flow target for the bottom product exceeds total feed flow", - "Use a bottom product-flow target below the total feed flow or check the mol/hr units"); + result.addError("specification.productFlow", + "Product-flow target for the bottom product exceeds total feed flow", + "Use a bottom product-flow target below the total feed flow or check the mol/hr units"); } - double productFlowSum = positiveFiniteOrZero(topProductFlowTarget) + positiveFiniteOrZero(bottomProductFlowTarget); + double productFlowSum = + positiveFiniteOrZero(topProductFlowTarget) + positiveFiniteOrZero(bottomProductFlowTarget); if (productFlowSum > totalFeedFlow * (1.0 + 1.0e-12)) { - result.addError("specification.productFlow.sum", "Top and bottom product-flow targets exceed total feed flow", - "Reduce one product-flow target or replace one flow target with purity, " - + "recovery, duty, or reflux specification"); + result.addError("specification.productFlow.sum", + "Top and bottom product-flow targets exceed total feed flow", + "Reduce one product-flow target or replace one flow target with purity, " + + "recovery, duty, or reflux specification"); } } @@ -11220,10 +11455,12 @@ private void validateProductFlowSpecificationsAgainstFeed(ValidationResult resul * Get the target value for a product-flow specification. * * @param specification column specification to inspect - * @return product-flow target in mol/hr, or {@link Double#NaN} when the specification is not a product-flow target + * @return product-flow target in mol/hr, or {@link Double#NaN} when the specification is not a + * product-flow target */ private double getProductFlowSpecificationTarget(ColumnSpecification specification) { - if (specification == null || specification.getType() != ColumnSpecification.SpecificationType.PRODUCT_FLOW_RATE) { + if (specification == null + || specification.getType() != ColumnSpecification.SpecificationType.PRODUCT_FLOW_RATE) { return Double.NaN; } return specification.getTargetValue(); @@ -11248,11 +11485,11 @@ private double getTotalExternalFeedFlowMolPerHour() { double totalFeedFlow = 0.0; for (StreamInterface feed : getAllExternalFeedStreams()) { if (feed == null) { - continue; + continue; } double flow = feed.getFlowRate("mol/hr"); if (Double.isFinite(flow)) { - totalFeedFlow += Math.abs(flow); + totalFeedFlow += Math.abs(flow); } } return totalFeedFlow; @@ -11278,20 +11515,21 @@ private void validateColumnTearVariables(ValidationResult result) { private void validateSideDrawSpecifications(ValidationResult result) { for (ColumnSideDrawSpecification specification : sideDrawSpecifications) { if (specification.getTrayNumber() < 0 || specification.getTrayNumber() >= numberOfTrays) { - result.addError("sidedraw.tray", "Side-draw specification tray is outside the column", - "Use a tray number between 0 and column.getNumberOfTrays() - 1"); + result.addError("sidedraw.tray", "Side-draw specification tray is outside the column", + "Use a tray number between 0 and column.getNumberOfTrays() - 1"); } - if (!Double.isFinite(specification.getTargetFlowRate()) || specification.getTargetFlowRate() < 0.0) { - result.addError("sidedraw.flow", "Side-draw target flow is not finite and non-negative", - "Create the side-draw specification with a finite flow rate >= 0"); + if (!Double.isFinite(specification.getTargetFlowRate()) + || specification.getTargetFlowRate() < 0.0) { + result.addError("sidedraw.flow", "Side-draw target flow is not finite and non-negative", + "Create the side-draw specification with a finite flow rate >= 0"); } if (!isPositiveFiniteValue(specification.getTolerance())) { - result.addError("sidedraw.tolerance", "Side-draw tolerance is not positive finite", - "Set a positive finite tolerance on the side-draw specification"); + result.addError("sidedraw.tolerance", "Side-draw tolerance is not positive finite", + "Set a positive finite tolerance on the side-draw specification"); } if (specification.getMaxIterations() <= 0) { - result.addError("sidedraw.iterations", "Side-draw iteration limit is not positive", - "Set maxIterations to a positive value on the side-draw specification"); + result.addError("sidedraw.iterations", "Side-draw iteration limit is not positive", + "Set maxIterations to a positive value on the side-draw specification"); } } } @@ -11304,17 +11542,18 @@ private void validateSideDrawSpecifications(ValidationResult result) { private void validatePumparounds(ValidationResult result) { if (!isPositiveFiniteValue(pumparoundTolerance)) { result.addError("pumparound.tolerance", "Pumparound tolerance is not positive finite", - "Set a positive finite tolerance with column.setPumparoundTolerance(tolerance)"); + "Set a positive finite tolerance with column.setPumparoundTolerance(tolerance)"); } if (maxPumparoundIterations <= 0) { result.addError("pumparound.iterations", "Pumparound iteration limit is not positive", - "Set max pumparound iterations to a positive value"); + "Set max pumparound iterations to a positive value"); } for (ColumnPumparound pumparound : pumparounds) { if (pumparound.getDrawTrayNumber() < 0 || pumparound.getDrawTrayNumber() >= numberOfTrays - || pumparound.getReturnTrayNumber() < 0 || pumparound.getReturnTrayNumber() >= numberOfTrays) { - result.addError("pumparound.tray", "Pumparound tray is outside the column", - "Use draw and return tray numbers between 0 and column.getNumberOfTrays() - 1"); + || pumparound.getReturnTrayNumber() < 0 + || pumparound.getReturnTrayNumber() >= numberOfTrays) { + result.addError("pumparound.tray", "Pumparound tray is outside the column", + "Use draw and return tray numbers between 0 and column.getNumberOfTrays() - 1"); } } } @@ -11327,20 +11566,22 @@ private void validatePumparounds(ValidationResult result) { private void validateHydraulicPressureDropCoupling(ValidationResult result) { if (!isPositiveFiniteValue(columnTearTolerance)) { result.addError("columntear.tolerance", "Column tear tolerance is not positive finite", - "Set a positive finite tolerance with column.setColumnTearTolerance(tolerance)"); + "Set a positive finite tolerance with column.setColumnTearTolerance(tolerance)"); } if (maxColumnTearIterations <= 0) { result.addError("columntear.iterations", "Column tear iteration limit is not positive", - "Set max column tear iterations to a positive value"); + "Set max column tear iterations to a positive value"); } if (hydraulicPressureDropCouplingEnabled) { - if (hydraulicPressureDropInternalsType == null || hydraulicPressureDropInternalsType.trim().isEmpty()) { - result.addError("hydraulics.internals", "Hydraulic coupling has no internals type", - "Set the internals type with column.setHydraulicPressureDropInternalsType(type)"); + if (hydraulicPressureDropInternalsType == null + || hydraulicPressureDropInternalsType.trim().isEmpty()) { + result.addError("hydraulics.internals", "Hydraulic coupling has no internals type", + "Set the internals type with column.setHydraulicPressureDropInternalsType(type)"); } if (!isPositiveFinite(topTrayPressure) && !isPositiveFinite(bottomTrayPressure)) { - result.addWarning("hydraulics.pressure", "Hydraulic pressure-drop coupling has no pressure endpoint basis", - "Set either top or bottom pressure so hydraulic pressure drop can anchor the profile"); + result.addWarning("hydraulics.pressure", + "Hydraulic pressure-drop coupling has no pressure endpoint basis", + "Set either top or bottom pressure so hydraulic pressure drop can anchor the profile"); } } } @@ -11352,8 +11593,9 @@ private void validateHydraulicPressureDropCoupling(ValidationResult result) { */ private void validateDynamicColumnModel(ValidationResult result) { if (dynamicColumnEnabled && isDynamicColumnModelExperimental()) { - result.addWarning("dynamic.model", "Dynamic distillation model is experimental explicit-Euler holdup screening", - "Use it for qualitative transients only; rigorous industrial dynamics require a DAE formulation"); + result.addWarning("dynamic.model", + "Dynamic distillation model is experimental explicit-Euler holdup screening", + "Use it for qualitative transients only; rigorous industrial dynamics require a DAE formulation"); } } @@ -11364,26 +11606,26 @@ private void validateDynamicColumnModel(ValidationResult result) { * @param specification specification to validate * @param expectedLocation expected product location */ - private void validateColumnSpecification(ValidationResult result, ColumnSpecification specification, - ColumnSpecification.ProductLocation expectedLocation) { + private void validateColumnSpecification(ValidationResult result, + ColumnSpecification specification, ColumnSpecification.ProductLocation expectedLocation) { if (specification == null) { return; } if (specification.getLocation() != expectedLocation) { result.addError("specification.location", "Specification is assigned to the wrong column end", - "Use setTopSpecification() for TOP specs and setBottomSpecification() for BOTTOM specs"); + "Use setTopSpecification() for TOP specs and setBottomSpecification() for BOTTOM specs"); } if (!Double.isFinite(specification.getTargetValue())) { result.addError("specification.target", "Specification target is not finite", - "Create the specification with a finite target value"); + "Create the specification with a finite target value"); } if (!isPositiveFiniteValue(specification.getTolerance())) { result.addError("specification.tolerance", "Specification tolerance is not positive finite", - "Set a positive finite tolerance on the ColumnSpecification"); + "Set a positive finite tolerance on the ColumnSpecification"); } if (specification.getMaxIterations() <= 0) { result.addError("specification.iterations", "Specification iteration limit is not positive", - "Set maxIterations to a positive value on the ColumnSpecification"); + "Set maxIterations to a positive value on the ColumnSpecification"); } validateSpecificationHardware(result, specification); validateSpecificationComponent(result, specification); @@ -11395,19 +11637,22 @@ private void validateColumnSpecification(ValidationResult result, ColumnSpecific * @param result validation result receiving issues * @param specification specification to validate */ - private void validateSpecificationHardware(ValidationResult result, ColumnSpecification specification) { - boolean topSpecificationLocal = specification.getLocation() == ColumnSpecification.ProductLocation.TOP; + private void validateSpecificationHardware(ValidationResult result, + ColumnSpecification specification) { + boolean topSpecificationLocal = + specification.getLocation() == ColumnSpecification.ProductLocation.TOP; boolean hasControllerEnd = topSpecificationLocal ? hasCondenser : hasReboiler; if (!hasControllerEnd && needsAdjustment(specification)) { result.addWarning("specification.hardware", - "Adjustable specification has no condenser/reboiler handle on that column end", - "Add the matching condenser/reboiler or replace the spec with a directly set temperature/duty"); + "Adjustable specification has no condenser/reboiler handle on that column end", + "Add the matching condenser/reboiler or replace the spec with a directly set temperature/duty"); } - if (!hasControllerEnd && (specification.getType() == ColumnSpecification.SpecificationType.REFLUX_RATIO - || specification.getType() == ColumnSpecification.SpecificationType.DUTY)) { + if (!hasControllerEnd + && (specification.getType() == ColumnSpecification.SpecificationType.REFLUX_RATIO + || specification.getType() == ColumnSpecification.SpecificationType.DUTY)) { result.addWarning("specification.hardware", - "Direct reflux or duty specification has no matching condenser/reboiler", - "Enable the matching column end or remove the direct specification"); + "Direct reflux or duty specification has no matching condenser/reboiler", + "Enable the matching column end or remove the direct specification"); } } @@ -11417,21 +11662,22 @@ private void validateSpecificationHardware(ValidationResult result, ColumnSpecif * @param result validation result receiving issues * @param specification specification to validate */ - private void validateSpecificationComponent(ValidationResult result, ColumnSpecification specification) { + private void validateSpecificationComponent(ValidationResult result, + ColumnSpecification specification) { if (specification.getType() != ColumnSpecification.SpecificationType.PRODUCT_PURITY - && specification.getType() != ColumnSpecification.SpecificationType.COMPONENT_RECOVERY) { + && specification.getType() != ColumnSpecification.SpecificationType.COMPONENT_RECOVERY) { return; } String componentName = specification.getComponentName(); if (componentName == null || componentName.trim().isEmpty()) { result.addError("specification.component", "Component-based specification has no component", - "Pass the component name to the ColumnSpecification constructor"); + "Pass the component name to the ColumnSpecification constructor"); return; } if (!isComponentPresentInAnyFeed(componentName)) { result.addError("specification.component", - "Specification component is not present in any feed stream: " + componentName, - "Use a component name from the feed fluid or add the component to the feed"); + "Specification component is not present in any feed stream: " + componentName, + "Use a component name from the feed fluid or add the component to the feed"); } } @@ -11447,18 +11693,18 @@ private boolean isComponentPresentInAnyFeed(String componentName) { } for (StreamInterface feedStream : getAllExternalFeedStreams()) { if (feedStream == null || feedStream.getFluid() == null) { - continue; + continue; } if (fluidContainsComponent(feedStream.getFluid(), componentName)) { - return true; + return true; } } for (StreamInterface feedStream : unassignedFeedStreams) { if (feedStream == null || feedStream.getFluid() == null) { - continue; + continue; } if (fluidContainsComponent(feedStream.getFluid(), componentName)) { - return true; + return true; } } return false; @@ -11478,7 +11724,7 @@ private boolean fluidContainsComponent(SystemInterface fluid, String componentNa } for (String candidateName : componentNames) { if (componentName.equals(candidateName)) { - return true; + return true; } } return false; @@ -11498,7 +11744,7 @@ private boolean isPositiveFiniteValue(double value) { @Override public String toJson() { return new GsonBuilder().serializeSpecialFloatingPointValues().create() - .toJson(new DistillationColumnResponse(this)); + .toJson(new DistillationColumnResponse(this)); } /** {@inheritDoc} */ @@ -11527,8 +11773,8 @@ public int getNumerOfTrays() { * Get the number of stages in the column using the correctly spelled API name. * *

- * This method is an alias for the legacy {@link #getNumerOfTrays()} method and is kept separate to preserve backwards - * compatibility with existing scripts. + * This method is an alias for the legacy {@link #getNumerOfTrays()} method and is kept separate + * to preserve backwards compatibility with existing scripts. *

* * @return number of stages in the column including reboiler and condenser stages when present @@ -11541,13 +11787,14 @@ public int getNumberOfTrays() { * Set a per-stage seed temperature used as an initial guess by residual solvers. * *

- * A seed temperature is not a tray specification. Unlike {@link SimpleTray#setOutTemperature}, it does not pin the - * stage temperature or replace the energy balance. The current {@link SolverType#NAPHTALI_SANDHOLM} implementation - * uses finite seeds only when the same stage has no fixed output-temperature specification. + * A seed temperature is not a tray specification. Unlike {@link SimpleTray#setOutTemperature}, it + * does not pin the stage temperature or replace the energy balance. The current + * {@link SolverType#NAPHTALI_SANDHOLM} implementation uses finite seeds only when the same stage + * has no fixed output-temperature specification. *

* - * @param stageIndex bottom-up stage index, where zero is the reboiler when present and {@code numberOfTrays - 1} is - * the top stage + * @param stageIndex bottom-up stage index, where zero is the reboiler when present and + * {@code numberOfTrays - 1} is the top stage * @param temperatureK seed temperature in kelvin; pass {@link Double#NaN} to clear a stage seed */ public void setSeedTemperature(int stageIndex, double temperatureK) { @@ -11562,8 +11809,8 @@ public void setSeedTemperature(int stageIndex, double temperatureK) { * Get the seed temperature configured for one stage. * * @param stageIndex bottom-up stage index to inspect - * @return seed temperature in kelvin, or {@link Double#NaN} when no seed is configured or the stage index is outside - * the current column range + * @return seed temperature in kelvin, or {@link Double#NaN} when no seed is configured or the + * stage index is outside the current column range */ public double getSeedTemperature(int stageIndex) { if (seedTemperatures == null || stageIndex < 0 || stageIndex >= numberOfTrays) { @@ -11583,7 +11830,7 @@ public boolean hasSeedTemperatures() { } for (double seedTemperature : seedTemperatures) { if (Double.isFinite(seedTemperature)) { - return true; + return true; } } return false; @@ -11606,7 +11853,8 @@ private void ensureSeedTemperatureArray() { double[] resized = new double[numberOfTrays]; Arrays.fill(resized, Double.NaN); if (seedTemperatures != null) { - System.arraycopy(seedTemperatures, 0, resized, 0, Math.min(seedTemperatures.length, resized.length)); + System.arraycopy(seedTemperatures, 0, resized, 0, + Math.min(seedTemperatures.length, resized.length)); } seedTemperatures = resized; } @@ -11624,9 +11872,10 @@ public void setMurphreeEfficiency(double efficiency) { * Set the Murphree tray efficiency for a single stage. * *

- * Stage numbering follows the column internal order: stage 0 is the reboiler when present and the last stage is the - * condenser when present. Reboiler and condenser stages are still treated as equilibrium stages by the correction - * algorithm, but the value is stored so external style scripts can round-trip stage efficiency data consistently. + * Stage numbering follows the column internal order: stage 0 is the reboiler when present and the + * last stage is the condenser when present. Reboiler and condenser stages are still treated as + * equilibrium stages by the correction algorithm, but the value is stored so external style + * scripts can round-trip stage efficiency data consistently. *

* * @param stage 0-based stage index in the range {@code [0, numberOfTrays)} @@ -11642,8 +11891,9 @@ public void setMurphreeEfficiency(int stage, double efficiency) { * Set Murphree tray efficiencies for every stage in one call. * *

- * The array length must equal the total number of stages including reboiler and condenser if they are present. - * Entries equal to {@link Double#NaN} restore use of the column-wide default for that stage. + * The array length must equal the total number of stages including reboiler and condenser if they + * are present. Entries equal to {@link Double#NaN} restore use of the column-wide default for + * that stage. *

* * @param efficiencies per-stage Murphree efficiencies, or {@code null} to clear all overrides @@ -11654,12 +11904,13 @@ public void setMurphreeEfficiencies(double[] efficiencies) { return; } if (efficiencies.length != numberOfTrays) { - throw new IllegalArgumentException( - "efficiencies length " + efficiencies.length + " does not match number of stages " + numberOfTrays); + throw new IllegalArgumentException("efficiencies length " + efficiencies.length + + " does not match number of stages " + numberOfTrays); } double[] copy = new double[numberOfTrays]; for (int stage = 0; stage < numberOfTrays; stage++) { - copy[stage] = Double.isNaN(efficiencies[stage]) ? Double.NaN : clampMurphreeEfficiency(efficiencies[stage]); + copy[stage] = Double.isNaN(efficiencies[stage]) ? Double.NaN + : clampMurphreeEfficiency(efficiencies[stage]); } perStageMurphreeEfficiency = copy; } @@ -11700,9 +11951,10 @@ private void ensurePerStageMurphreeEfficiencyArray() { } double[] resized = new double[numberOfTrays]; for (int stage = 0; stage < numberOfTrays; stage++) { - resized[stage] = perStageMurphreeEfficiency != null && stage < perStageMurphreeEfficiency.length - ? perStageMurphreeEfficiency[stage] - : Double.NaN; + resized[stage] = + perStageMurphreeEfficiency != null && stage < perStageMurphreeEfficiency.length + ? perStageMurphreeEfficiency[stage] + : Double.NaN; } perStageMurphreeEfficiency = resized; } @@ -11714,10 +11966,11 @@ private void ensurePerStageMurphreeEfficiencyArray() { * @return per-stage override when finite, otherwise the column-wide Murphree efficiency */ private double getEffectiveMurphreeEfficiency(int stage) { - if (perStageMurphreeEfficiency != null && stage >= 0 && stage < perStageMurphreeEfficiency.length) { + if (perStageMurphreeEfficiency != null && stage >= 0 + && stage < perStageMurphreeEfficiency.length) { double value = perStageMurphreeEfficiency[stage]; if (!Double.isNaN(value)) { - return value; + return value; } } return murphreeEfficiency; @@ -11741,13 +11994,14 @@ private double clampMurphreeEfficiency(double efficiency) { */ private void validateStageIndex(int stage) { if (stage < 0 || stage >= numberOfTrays) { - throw new IndexOutOfBoundsException("stage index " + stage + " out of range [0, " + numberOfTrays + ")"); + throw new IndexOutOfBoundsException( + "stage index " + stage + " out of range [0, " + numberOfTrays + ")"); } } /** - * Set the number of simplified inner-loop iterations between rigorous flash updates in the IO solver. A value of 0 - * disables the simplified model (all iterations use rigorous flash). + * Set the number of simplified inner-loop iterations between rigorous flash updates in the IO + * solver. A value of 0 disables the simplified model (all iterations use rigorous flash). * * @param steps number of inner-loop steps (0 to disable, typically 2-5) */ @@ -11774,7 +12028,8 @@ public int getInnerLoopSteps() { * @return list of residual arrays, one per iteration; empty if no solve has been run */ public List getConvergenceHistory() { - return convergenceHistory != null ? Collections.unmodifiableList(convergenceHistory) : Collections.emptyList(); + return convergenceHistory != null ? Collections.unmodifiableList(convergenceHistory) + : Collections.emptyList(); } /** @@ -11790,8 +12045,9 @@ public SolverType getSolverType() { * Get the solver strategy that completed the latest run. * *

- * For explicitly selected solvers this is normally the same as {@link #getSolverType()}. When {@link SolverType#AUTO} - * is configured, this reports the concrete solver selected by the automatic solver factory. + * For explicitly selected solvers this is normally the same as {@link #getSolverType()}. When + * {@link SolverType#AUTO} is configured, this reports the concrete solver selected by the + * automatic solver factory. *

* * @return solver strategy used by the latest solve @@ -11804,10 +12060,11 @@ public SolverType getLastSolverTypeUsed() { * Get the concrete solver cached by the automatic solver for warm re-solves. * *

- * When {@link SolverType#AUTO} is configured, the first solve runs the full feasibility pre-screen and - * multi-candidate selection and caches the winning concrete solver. Subsequent warm re-solves (for example inside a - * recycle loop) reuse this cached solver directly, skipping the expensive selection step. Returns {@code null} before - * the first AUTO solve completes or after the column reverts to a cold start. + * When {@link SolverType#AUTO} is configured, the first solve runs the full feasibility + * pre-screen and multi-candidate selection and caches the winning concrete solver. Subsequent + * warm re-solves (for example inside a recycle loop) reuse this cached solver directly, skipping + * the expensive selection step. Returns {@code null} before the first AUTO solve completes or + * after the column reverts to a cold start. *

* * @return cached warm-start solver, or {@code null} if none is currently cached @@ -11837,8 +12094,8 @@ public String getLastSolveStatusReason() { /** * Get the latest automatic solver candidate trace. * - * @return candidate trace from {@link SolverType#AUTO}, or an empty string when automatic mode was not used in the - * latest solve + * @return candidate trace from {@link SolverType#AUTO}, or an empty string when automatic mode + * was not used in the latest solve */ public String getLastAutoSolverSummary() { return lastAutoSolverSummary; @@ -11885,8 +12142,8 @@ public List getTrays() { /** * Sets the reflux ratio on the condenser (if present). Also stores a - * {@link ColumnSpecification.SpecificationType#REFLUX_RATIO REFLUX_RATIO} top specification so that the column - * records the user's intent. + * {@link ColumnSpecification.SpecificationType#REFLUX_RATIO REFLUX_RATIO} top specification so + * that the column records the user's intent. * * @param refluxRatio the desired reflux ratio (L/D) */ @@ -11894,8 +12151,9 @@ public void setCondenserRefluxRatio(double refluxRatio) { if (hasCondenser) { getCondenser().setRefluxRatio(refluxRatio); } - this.topSpecification = new ColumnSpecification(ColumnSpecification.SpecificationType.REFLUX_RATIO, - ColumnSpecification.ProductLocation.TOP, refluxRatio); + this.topSpecification = + new ColumnSpecification(ColumnSpecification.SpecificationType.REFLUX_RATIO, + ColumnSpecification.ProductLocation.TOP, refluxRatio); } // ======================== Column specification convenience methods @@ -11927,7 +12185,8 @@ public ColumnSpecification getBottomSpecification() { */ public void setTopSpecification(ColumnSpecification spec) { if (spec != null && spec.getLocation() != ColumnSpecification.ProductLocation.TOP) { - throw new IllegalArgumentException("Top specification must have location TOP, got: " + spec.getLocation()); + throw new IllegalArgumentException( + "Top specification must have location TOP, got: " + spec.getLocation()); } this.topSpecification = spec; } @@ -11940,7 +12199,8 @@ public void setTopSpecification(ColumnSpecification spec) { */ public void setBottomSpecification(ColumnSpecification spec) { if (spec != null && spec.getLocation() != ColumnSpecification.ProductLocation.BOTTOM) { - throw new IllegalArgumentException("Bottom specification must have location BOTTOM, got: " + spec.getLocation()); + throw new IllegalArgumentException( + "Bottom specification must have location BOTTOM, got: " + spec.getLocation()); } this.bottomSpecification = spec; } @@ -11952,8 +12212,9 @@ public void setBottomSpecification(ColumnSpecification spec) { * @param purity the desired mole fraction (0 to 1) */ public void setTopProductPurity(String componentName, double purity) { - this.topSpecification = new ColumnSpecification(ColumnSpecification.SpecificationType.PRODUCT_PURITY, - ColumnSpecification.ProductLocation.TOP, purity, componentName); + this.topSpecification = + new ColumnSpecification(ColumnSpecification.SpecificationType.PRODUCT_PURITY, + ColumnSpecification.ProductLocation.TOP, purity, componentName); } /** @@ -11963,8 +12224,9 @@ public void setTopProductPurity(String componentName, double purity) { * @param purity the desired mole fraction (0 to 1) */ public void setBottomProductPurity(String componentName, double purity) { - this.bottomSpecification = new ColumnSpecification(ColumnSpecification.SpecificationType.PRODUCT_PURITY, - ColumnSpecification.ProductLocation.BOTTOM, purity, componentName); + this.bottomSpecification = + new ColumnSpecification(ColumnSpecification.SpecificationType.PRODUCT_PURITY, + ColumnSpecification.ProductLocation.BOTTOM, purity, componentName); } /** @@ -11976,8 +12238,9 @@ public void setReboilerBoilupRatio(double boilupRatio) { if (hasReboiler) { getReboiler().setRefluxRatio(boilupRatio); } - this.bottomSpecification = new ColumnSpecification(ColumnSpecification.SpecificationType.REFLUX_RATIO, - ColumnSpecification.ProductLocation.BOTTOM, boilupRatio); + this.bottomSpecification = + new ColumnSpecification(ColumnSpecification.SpecificationType.REFLUX_RATIO, + ColumnSpecification.ProductLocation.BOTTOM, boilupRatio); } /** @@ -11987,8 +12250,9 @@ public void setReboilerBoilupRatio(double boilupRatio) { * @param recovery the desired recovery fraction (0 to 1) */ public void setTopComponentRecovery(String componentName, double recovery) { - this.topSpecification = new ColumnSpecification(ColumnSpecification.SpecificationType.COMPONENT_RECOVERY, - ColumnSpecification.ProductLocation.TOP, recovery, componentName); + this.topSpecification = + new ColumnSpecification(ColumnSpecification.SpecificationType.COMPONENT_RECOVERY, + ColumnSpecification.ProductLocation.TOP, recovery, componentName); } /** @@ -11998,8 +12262,9 @@ public void setTopComponentRecovery(String componentName, double recovery) { * @param recovery the desired recovery fraction (0 to 1) */ public void setBottomComponentRecovery(String componentName, double recovery) { - this.bottomSpecification = new ColumnSpecification(ColumnSpecification.SpecificationType.COMPONENT_RECOVERY, - ColumnSpecification.ProductLocation.BOTTOM, recovery, componentName); + this.bottomSpecification = + new ColumnSpecification(ColumnSpecification.SpecificationType.COMPONENT_RECOVERY, + ColumnSpecification.ProductLocation.BOTTOM, recovery, componentName); } /** @@ -12009,8 +12274,9 @@ public void setBottomComponentRecovery(String componentName, double recovery) { * @param unit the flow rate unit (currently expected as {@code mol/hr}) */ public void setTopProductFlowRate(double flowRate, String unit) { - this.topSpecification = new ColumnSpecification(ColumnSpecification.SpecificationType.PRODUCT_FLOW_RATE, - ColumnSpecification.ProductLocation.TOP, flowRate); + this.topSpecification = + new ColumnSpecification(ColumnSpecification.SpecificationType.PRODUCT_FLOW_RATE, + ColumnSpecification.ProductLocation.TOP, flowRate); } /** @@ -12022,8 +12288,9 @@ public void setTopProductFlowRate(double flowRate, String unit) { public void setBottomProductFlowRate(double flowRate, String unit) { // Store the specification in mol/hr (the column evaluator uses mol/hr // internally) - this.bottomSpecification = new ColumnSpecification(ColumnSpecification.SpecificationType.PRODUCT_FLOW_RATE, - ColumnSpecification.ProductLocation.BOTTOM, flowRate); + this.bottomSpecification = + new ColumnSpecification(ColumnSpecification.SpecificationType.PRODUCT_FLOW_RATE, + ColumnSpecification.ProductLocation.BOTTOM, flowRate); } /** @@ -12033,7 +12300,7 @@ public void setBottomProductFlowRate(double flowRate, String unit) { */ public void setCondenserDutySpecification(double duty) { this.topSpecification = new ColumnSpecification(ColumnSpecification.SpecificationType.DUTY, - ColumnSpecification.ProductLocation.TOP, duty); + ColumnSpecification.ProductLocation.TOP, duty); } /** @@ -12043,7 +12310,7 @@ public void setCondenserDutySpecification(double duty) { */ public void setReboilerDutySpecification(double duty) { this.bottomSpecification = new ColumnSpecification(ColumnSpecification.SpecificationType.DUTY, - ColumnSpecification.ProductLocation.BOTTOM, duty); + ColumnSpecification.ProductLocation.BOTTOM, duty); } // ======================== Builder pattern ======================== @@ -12115,7 +12382,8 @@ public double getTrayWeirLength() { } /** - * Returns the liquid holdup array (moles per tray). May be null if dynamic model has not been initialized. + * Returns the liquid holdup array (moles per tray). May be null if dynamic model has not been + * initialized. * * @return array of liquid holdups indexed by tray number, or null */ @@ -12124,7 +12392,8 @@ public double[] getTrayLiquidHoldup() { } /** - * Returns the per-tray enthalpy array in J. May be null if energy balance has not been initialized. + * Returns the per-tray enthalpy array in J. May be null if energy balance has not been + * initialized. * * @return array of tray enthalpies indexed by tray number, or null */ @@ -12133,8 +12402,8 @@ public double[] getTrayEnthalpy() { } /** - * Sets the dry tray pressure drop per tray in Pa. Used in the dynamic vapor hydraulic model to compute vapor flow - * rate as a function of pressure difference between trays. + * Sets the dry tray pressure drop per tray in Pa. Used in the dynamic vapor hydraulic model to + * compute vapor flow rate as a function of pressure difference between trays. * * @param dpPa dry tray pressure drop in Pascals (positive value) */ @@ -12152,8 +12421,8 @@ public double getTrayDryPressureDrop() { } /** - * Enables or disables the per-tray energy balance in dynamic mode. When enabled, each tray's enthalpy is tracked and - * PH flash is used for re-equilibration instead of TP flash. + * Enables or disables the per-tray energy balance in dynamic mode. When enabled, each tray's + * enthalpy is tracked and PH flash is used for re-equilibration instead of TP flash. * * @param enabled true to enable energy-balanced trays */ @@ -12174,9 +12443,9 @@ public boolean isDynamicEnergyEnabled() { * {@inheritDoc} * *

- * Dynamic distillation column model. When {@code dynamicColumnEnabled} is true, performs a single forward-Euler - * integration step on each tray's liquid holdup using the MESH equations. Liquid leaving each tray is calculated - * using the Francis weir overflow formula. + * Dynamic distillation column model. When {@code dynamicColumnEnabled} is true, performs a single + * forward-Euler integration step on each tray's liquid holdup using the MESH equations. Liquid + * leaving each tray is calculated using the Francis weir overflow formula. *

* * @param dt time step in seconds @@ -12187,7 +12456,7 @@ public void runTransient(double dt, UUID id) { if (!dynamicColumnEnabled || trays.isEmpty()) { // Fall back to steady-state solve if (getCalculateSteadyState()) { - run(id); + run(id); } increaseTime(dt); return; @@ -12199,12 +12468,12 @@ public void runTransient(double dt, UUID id) { if (trayLiquidHoldup == null || trayLiquidHoldup.length != nTrays) { trayLiquidHoldup = new double[nTrays]; for (int i = 0; i < nTrays; i++) { - SystemInterface trayFluid = trays.get(i).getThermoSystem(); - if (trayFluid != null) { - trayLiquidHoldup[i] = trayFluid.getTotalNumberOfMoles(); - } else { - trayLiquidHoldup[i] = 100.0; // default - } + SystemInterface trayFluid = trays.get(i).getThermoSystem(); + if (trayFluid != null) { + trayLiquidHoldup[i] = trayFluid.getTotalNumberOfMoles(); + } else { + trayLiquidHoldup[i] = 100.0; // default + } } } @@ -12212,10 +12481,10 @@ public void runTransient(double dt, UUID id) { if (dynamicEnergyEnabled && (trayEnthalpy == null || trayEnthalpy.length != nTrays)) { trayEnthalpy = new double[nTrays]; for (int i = 0; i < nTrays; i++) { - SystemInterface trayFluid = trays.get(i).getThermoSystem(); - if (trayFluid != null) { - trayEnthalpy[i] = trayFluid.getEnthalpy(); - } + SystemInterface trayFluid = trays.get(i).getThermoSystem(); + if (trayFluid != null) { + trayEnthalpy[i] = trayFluid.getEnthalpy(); + } } } @@ -12231,11 +12500,11 @@ public void runTransient(double dt, UUID id) { SystemInterface trayFluid = trays.get(i).getThermoSystem(); liquidMolarVol[i] = 1.0e-4; // default m3/mol if (trayFluid != null && (trayFluid.hasPhaseType("aqueous") || trayFluid.hasPhaseType("oil") - || trayFluid.getNumberOfPhases() > 1)) { - double liquidDensity = trayFluid.getPhase(1).getDensity("mol/m3"); - if (liquidDensity > 0) { - liquidMolarVol[i] = 1.0 / liquidDensity; - } + || trayFluid.getNumberOfPhases() > 1)) { + double liquidDensity = trayFluid.getPhase(1).getDensity("mol/m3"); + if (liquidDensity > 0) { + liquidMolarVol[i] = 1.0 / liquidDensity; + } } double liquidVolume = trayLiquidHoldup[i] * liquidMolarVol[i]; double liquidHeight = trayArea > 0 ? liquidVolume / trayArea : 0.0; @@ -12249,7 +12518,7 @@ public void runTransient(double dt, UUID id) { SimpleTray tray = trays.get(i); SystemInterface trayFluid = tray.getThermoSystem(); if (trayFluid == null) { - continue; + continue; } double liquidOutRate = overflowMolRate[i]; @@ -12257,28 +12526,30 @@ public void runTransient(double dt, UUID id) { // Vapor in-flow from tray below double vaporInRate = 0.0; if (i < nTrays - 1) { - SystemInterface belowFluid = trays.get(i + 1).getThermoSystem(); - if (belowFluid != null && belowFluid.getNumberOfPhases() > 0) { - if (trayDryPressureDrop > 0.0) { - // Pressure-driven vapor hydraulic: vapor rises if pressure below exceeds - // pressure above by more than the tray resistance (dry DP + liquid head). - double liquidHeight = trayArea > 0 ? trayLiquidHoldup[i] * liquidMolarVol[i] / trayArea : 0.0; - double liquidHeadPa = liquidHeight * 9.81 * (liquidMolarVol[i] > 0 ? 1.0 / liquidMolarVol[i] : 800.0); - double totalTrayDp = trayDryPressureDrop + liquidHeadPa; - double pBelow = belowFluid.getPressure("Pa"); - double pAbove = trayFluid.getPressure("Pa"); - double dpAvailable = pBelow - pAbove; - if (dpAvailable > 0.0 && totalTrayDp > 0.0) { - // Vapor flow proportional to sqrt of available DP fraction - double vaporMoles = belowFluid.getPhase(0).getNumberOfMolesInPhase(); - double dpRatio = Math.min(dpAvailable / totalTrayDp, 2.0); - vaporInRate = vaporMoles * Math.sqrt(dpRatio) / Math.max(dt, 0.001); - } - } else { - // Original simplified model: all vapor rises in one timestep - vaporInRate = belowFluid.getPhase(0).getNumberOfMolesInPhase() / Math.max(dt, 0.001); - } - } + SystemInterface belowFluid = trays.get(i + 1).getThermoSystem(); + if (belowFluid != null && belowFluid.getNumberOfPhases() > 0) { + if (trayDryPressureDrop > 0.0) { + // Pressure-driven vapor hydraulic: vapor rises if pressure below exceeds + // pressure above by more than the tray resistance (dry DP + liquid head). + double liquidHeight = + trayArea > 0 ? trayLiquidHoldup[i] * liquidMolarVol[i] / trayArea : 0.0; + double liquidHeadPa = + liquidHeight * 9.81 * (liquidMolarVol[i] > 0 ? 1.0 / liquidMolarVol[i] : 800.0); + double totalTrayDp = trayDryPressureDrop + liquidHeadPa; + double pBelow = belowFluid.getPressure("Pa"); + double pAbove = trayFluid.getPressure("Pa"); + double dpAvailable = pBelow - pAbove; + if (dpAvailable > 0.0 && totalTrayDp > 0.0) { + // Vapor flow proportional to sqrt of available DP fraction + double vaporMoles = belowFluid.getPhase(0).getNumberOfMolesInPhase(); + double dpRatio = Math.min(dpAvailable / totalTrayDp, 2.0); + vaporInRate = vaporMoles * Math.sqrt(dpRatio) / Math.max(dt, 0.001); + } + } else { + // Original simplified model: all vapor rises in one timestep + vaporInRate = belowFluid.getPhase(0).getNumberOfMolesInPhase() / Math.max(dt, 0.001); + } + } } // Liquid in-flow from tray above (use pre-computed overflow) @@ -12289,21 +12560,22 @@ public void runTransient(double dt, UUID id) { double feedEnthalpy = 0.0; List trayFeeds = feedStreams.get(i); if (trayFeeds != null) { - for (StreamInterface feedStream : trayFeeds) { - if (feedStream.getThermoSystem() != null) { - double fMoles = feedStream.getThermoSystem().getTotalNumberOfMoles() / Math.max(dt, 0.001); - feedRate += fMoles; - if (dynamicEnergyEnabled) { - feedEnthalpy += feedStream.getThermoSystem().getEnthalpy() / Math.max(dt, 0.001); - } - } - } + for (StreamInterface feedStream : trayFeeds) { + if (feedStream.getThermoSystem() != null) { + double fMoles = + feedStream.getThermoSystem().getTotalNumberOfMoles() / Math.max(dt, 0.001); + feedRate += fMoles; + if (dynamicEnergyEnabled) { + feedEnthalpy += feedStream.getThermoSystem().getEnthalpy() / Math.max(dt, 0.001); + } + } + } } // Vapor production rate from this tray double vaporOutRate = 0.0; if (trayFluid.getNumberOfPhases() > 0) { - vaporOutRate = trayFluid.getPhase(0).getNumberOfMolesInPhase() / Math.max(dt, 0.001); + vaporOutRate = trayFluid.getPhase(0).getNumberOfMolesInPhase() / Math.max(dt, 0.001); } // Forward Euler holdup update: dn/dt = Lin + Vin + F - Lout - Vout @@ -12312,61 +12584,61 @@ public void runTransient(double dt, UUID id) { // --- Re-flash the tray --- if (dynamicEnergyEnabled && trayEnthalpy != null) { - // Energy-balance mode: compute enthalpy flows and use PH flash - double hLiqIn = 0.0; - if (i > 0 && liquidInRate > 0) { - SystemInterface aboveFluid = trays.get(i - 1).getThermoSystem(); - if (aboveFluid != null && aboveFluid.getNumberOfPhases() > 1) { - double molarH = aboveFluid.getPhase(1).getEnthalpy() - / Math.max(aboveFluid.getPhase(1).getNumberOfMolesInPhase(), 1.0); - hLiqIn = liquidInRate * molarH; - } - } - double hVapIn = 0.0; - if (i < nTrays - 1 && vaporInRate > 0) { - SystemInterface belowFluid = trays.get(i + 1).getThermoSystem(); - if (belowFluid != null && belowFluid.getNumberOfPhases() > 0) { - double molarH = belowFluid.getPhase(0).getEnthalpy() - / Math.max(belowFluid.getPhase(0).getNumberOfMolesInPhase(), 1.0); - hVapIn = vaporInRate * molarH; - } - } - double hLiqOut = 0.0; - if (liquidOutRate > 0 && trayFluid.getNumberOfPhases() > 1) { - double molarH = trayFluid.getPhase(1).getEnthalpy() - / Math.max(trayFluid.getPhase(1).getNumberOfMolesInPhase(), 1.0); - hLiqOut = liquidOutRate * molarH; - } - double hVapOut = 0.0; - if (vaporOutRate > 0 && trayFluid.getNumberOfPhases() > 0) { - double molarH = trayFluid.getPhase(0).getEnthalpy() - / Math.max(trayFluid.getPhase(0).getNumberOfMolesInPhase(), 1.0); - hVapOut = vaporOutRate * molarH; - } - double dEnthalpy = (hLiqIn + hVapIn + feedEnthalpy - hLiqOut - hVapOut) * dt; - trayEnthalpy[i] += dEnthalpy; - - // PH flash: set tray fluid to tracked enthalpy - try { - neqsim.thermodynamicoperations.ThermodynamicOperations trayOps = new neqsim.thermodynamicoperations.ThermodynamicOperations( - trayFluid); - trayOps.PHflash(trayEnthalpy[i]); - } catch (Exception ex) { - logger.warn("Dynamic tray " + i + " PH flash failed: " + ex.getMessage()); - // Fallback to TP flash - try { - tray.run(id); - } catch (Exception ex2) { - logger.warn("Dynamic tray " + i + " TP flash fallback failed: " + ex2.getMessage()); - } - } + // Energy-balance mode: compute enthalpy flows and use PH flash + double hLiqIn = 0.0; + if (i > 0 && liquidInRate > 0) { + SystemInterface aboveFluid = trays.get(i - 1).getThermoSystem(); + if (aboveFluid != null && aboveFluid.getNumberOfPhases() > 1) { + double molarH = aboveFluid.getPhase(1).getEnthalpy() + / Math.max(aboveFluid.getPhase(1).getNumberOfMolesInPhase(), 1.0); + hLiqIn = liquidInRate * molarH; + } + } + double hVapIn = 0.0; + if (i < nTrays - 1 && vaporInRate > 0) { + SystemInterface belowFluid = trays.get(i + 1).getThermoSystem(); + if (belowFluid != null && belowFluid.getNumberOfPhases() > 0) { + double molarH = belowFluid.getPhase(0).getEnthalpy() + / Math.max(belowFluid.getPhase(0).getNumberOfMolesInPhase(), 1.0); + hVapIn = vaporInRate * molarH; + } + } + double hLiqOut = 0.0; + if (liquidOutRate > 0 && trayFluid.getNumberOfPhases() > 1) { + double molarH = trayFluid.getPhase(1).getEnthalpy() + / Math.max(trayFluid.getPhase(1).getNumberOfMolesInPhase(), 1.0); + hLiqOut = liquidOutRate * molarH; + } + double hVapOut = 0.0; + if (vaporOutRate > 0 && trayFluid.getNumberOfPhases() > 0) { + double molarH = trayFluid.getPhase(0).getEnthalpy() + / Math.max(trayFluid.getPhase(0).getNumberOfMolesInPhase(), 1.0); + hVapOut = vaporOutRate * molarH; + } + double dEnthalpy = (hLiqIn + hVapIn + feedEnthalpy - hLiqOut - hVapOut) * dt; + trayEnthalpy[i] += dEnthalpy; + + // PH flash: set tray fluid to tracked enthalpy + try { + neqsim.thermodynamicoperations.ThermodynamicOperations trayOps = + new neqsim.thermodynamicoperations.ThermodynamicOperations(trayFluid); + trayOps.PHflash(trayEnthalpy[i]); + } catch (Exception ex) { + logger.warn("Dynamic tray " + i + " PH flash failed: " + ex.getMessage()); + // Fallback to TP flash + try { + tray.run(id); + } catch (Exception ex2) { + logger.warn("Dynamic tray " + i + " TP flash fallback failed: " + ex2.getMessage()); + } + } } else { - // Default: TP flash (original behavior) - try { - tray.run(id); - } catch (Exception ex) { - logger.warn("Dynamic tray " + i + " flash failed: " + ex.getMessage()); - } + // Default: TP flash (original behavior) + try { + tray.run(id); + } catch (Exception ex) { + logger.warn("Dynamic tray " + i + " flash failed: " + ex.getMessage()); + } } } @@ -12374,11 +12646,11 @@ public void runTransient(double dt, UUID id) { if (trays.size() > 0) { StreamInterface gasOut = trays.get(nTrays - 1).getGasOutStream(); if (gasOut != null && gasOut.getThermoSystem() != null) { - gasOutStream.setThermoSystem(gasOut.getThermoSystem().clone()); + gasOutStream.setThermoSystem(gasOut.getThermoSystem().clone()); } StreamInterface liqOut = trays.get(0).getLiquidOutStream(); if (liqOut != null && liqOut.getThermoSystem() != null) { - liquidOutStream.setThermoSystem(liqOut.getThermoSystem().clone()); + liquidOutStream.setThermoSystem(liqOut.getThermoSystem().clone()); } } @@ -12394,8 +12666,9 @@ public void runTransient(double dt, UUID id) { *

* *
-   * DistillationColumn col = DistillationColumn.builder("Deethanizer").numberOfTrays(7).withCondenserAndReboiler()
-   *     .topPressure(30.0, "bara").bottomPressure(31.0, "bara").insideOut().addFeedStream(feed, 4).build();
+   * DistillationColumn col = DistillationColumn.builder("Deethanizer").numberOfTrays(7)
+   *     .withCondenserAndReboiler().topPressure(30.0, "bara").bottomPressure(31.0, "bara")
+   *     .insideOut().addFeedStream(feed, 4).build();
    * 
* * @author esol @@ -12606,7 +12879,7 @@ public Builder internalDiameter(double diameter) { * @return this builder */ public Builder addFeedStream(StreamInterface feed, int trayIndex) { - this.feeds.add(new Object[] { feed, trayIndex }); + this.feeds.add(new Object[] {feed, trayIndex}); return this; } @@ -12619,7 +12892,7 @@ public Builder addFeedStream(StreamInterface feed, int trayIndex) { */ public Builder topProductPurity(String componentName, double purity) { this.topSpec = new ColumnSpecification(ColumnSpecification.SpecificationType.PRODUCT_PURITY, - ColumnSpecification.ProductLocation.TOP, purity, componentName); + ColumnSpecification.ProductLocation.TOP, purity, componentName); return this; } @@ -12642,37 +12915,37 @@ public Builder bottomSpecification(ColumnSpecification spec) { public DistillationColumn build() { DistillationColumn col = new DistillationColumn(name, numberOfTrays, condenser, reboiler); if (topPressure >= 0) { - col.setTopPressure(topPressure); + col.setTopPressure(topPressure); } if (bottomPressure >= 0) { - col.setBottomPressure(bottomPressure); + col.setBottomPressure(bottomPressure); } if (tempTol >= 0) { - col.setTemperatureTolerance(tempTol); + col.setTemperatureTolerance(tempTol); } if (massTol >= 0) { - col.setMassBalanceTolerance(massTol); + col.setMassBalanceTolerance(massTol); } if (maxIter >= 0) { - col.setMaxNumberOfIterations(maxIter); + col.setMaxNumberOfIterations(maxIter); } if (solver != null) { - col.setSolverType(solver); + col.setSolverType(solver); } if (relaxation >= 0) { - col.setRelaxationFactor(relaxation); + col.setRelaxationFactor(relaxation); } if (diameter >= 0) { - col.setInternalDiameter(diameter); + col.setInternalDiameter(diameter); } if (topSpec != null) { - col.topSpecification = topSpec; + col.topSpecification = topSpec; } if (bottomSpec != null) { - col.bottomSpecification = bottomSpec; + col.bottomSpecification = bottomSpec; } for (Object[] feedEntry : feeds) { - col.addFeedStream((StreamInterface) feedEntry[0], (Integer) feedEntry[1]); + col.addFeedStream((StreamInterface) feedEntry[0], (Integer) feedEntry[1]); } return col; } diff --git a/src/main/java/neqsim/process/equipment/distillation/NaphtaliSandholmSolver.java b/src/main/java/neqsim/process/equipment/distillation/NaphtaliSandholmSolver.java index 5e5523e21e..b2e0db57ac 100644 --- a/src/main/java/neqsim/process/equipment/distillation/NaphtaliSandholmSolver.java +++ b/src/main/java/neqsim/process/equipment/distillation/NaphtaliSandholmSolver.java @@ -11,24 +11,26 @@ import neqsim.thermo.phase.PhaseType; import neqsim.thermo.system.SystemInterface; import neqsim.thermodynamicoperations.ThermodynamicOperations; +import neqsim.util.math.LinearAlgebraOps; /** * Naphtali-Sandholm simultaneous correction solver for distillation columns. * *

- * Solves the full MESH (Material balance, Equilibrium, Summation, Heat balance) system of equations simultaneously - * using Newton-Raphson iteration with block-tridiagonal Jacobian structure. + * Solves the full MESH (Material balance, Equilibrium, Summation, Heat balance) system of equations + * simultaneously using Newton-Raphson iteration with block-tridiagonal Jacobian structure. *

* *

- * This solver handles wide-boiling-range systems where sequential substitution (tray-by-tray) methods diverge. The - * variable set per tray j is: liquid component flows l_{i,j} for each component i, temperature T_j, and vapor flow rate - * V_j. This gives (C+2) variables per tray for C components, and N*(C+2) total variables for N trays. + * This solver handles wide-boiling-range systems where sequential substitution (tray-by-tray) + * methods diverge. The variable set per tray j is: liquid component flows l_{i,j} for each + * component i, temperature T_j, and vapor flow rate V_j. This gives (C+2) variables per tray for C + * components, and N*(C+2) total variables for N trays. *

* *

- * Reference: Naphtali, L.M. and Sandholm, D.P. (1971). "Multicomponent Separation Calculations by Linearization", AIChE - * Journal, 17(1), 148-153. + * Reference: Naphtali, L.M. and Sandholm, D.P. (1971). "Multicomponent Separation Calculations by + * Linearization", AIChE Journal, 17(1), 148-153. *

* * @author NeqSim / AI-assisted @@ -87,15 +89,16 @@ public class NaphtaliSandholmSolver { private double[][] K; /** - * Per-stage Murphree efficiency cached at solver init time. trayEta[j] is forced to 1.0 for the reboiler (j=0) and - * the condenser (j=N-1 when present) so those stages remain rigorous equilibrium. Other stages use the value - * registered on the parent column via setMurphreeEfficiency(stage, value), defaulting to the global value when no - * per-stage override is set. + * Per-stage Murphree efficiency cached at solver init time. trayEta[j] is forced to 1.0 for the + * reboiler (j=0) and the condenser (j=N-1 when present) so those stages remain rigorous + * equilibrium. Other stages use the value registered on the parent column via + * setMurphreeEfficiency(stage, value), defaulting to the global value when no per-stage override + * is set. * - * The efficiency is embedded into the simultaneous MESH solution through the Edmister proxy K_eff[j][i] = - * K[j][i]^trayEta[j]. At trayEta = 1.0 the stage is rigorous equilibrium; at trayEta -> 0 the K-values approach - * 1.0 and the tray becomes passive (vapor passes through ~ unchanged), matching the behaviour of a heavily de-rated - * tray + * The efficiency is embedded into the simultaneous MESH solution through the Edmister proxy + * K_eff[j][i] = K[j][i]^trayEta[j]. At trayEta = 1.0 the stage is rigorous equilibrium; at + * trayEta -> 0 the K-values approach 1.0 and the tray becomes passive (vapor passes through ~ + * unchanged), matching the behaviour of a heavily de-rated tray */ private double[] trayEta; @@ -135,15 +138,16 @@ public class NaphtaliSandholmSolver { private double[] Q; /** - * Fixed temperature specification for each tray in K. NaN means the temperature is a free variable (solved by the - * energy balance). + * Fixed temperature specification for each tray in K. NaN means the temperature is a free + * variable (solved by the energy balance). */ private double[] fixedTemperature; /** - * Per-tray seed temperature [K] used by the initializer when the tray is NOT fixed-T (i.e. T[j] is a Newton - * variable). NaN means "no seed, use Wilson estimate". This lets a user pass a reboiler T value as a hint when they - * specify boilup ratio instead of T as the actual MESH constraint. + * Per-tray seed temperature [K] used by the initializer when the tray is NOT fixed-T (i.e. T[j] + * is a Newton variable). NaN means "no seed, use Wilson estimate". This lets a user pass a + * reboiler T value as a hint when they specify boilup ratio instead of T as the actual MESH + * constraint. */ private double[] seedTemperature; @@ -151,10 +155,11 @@ public class NaphtaliSandholmSolver { private SystemInterface referenceSystem; /** - * Enable Boston-Sullivan inside-out refinement of the seed when at least one tray temperature is pinned (e.g. T-spec - * reboiler). The smooth Wilson BP seed misses sharp T-pinches; B-S inner loop (per-component tridiag MB + bubble-T) - * captures them and gives SR / Newton a sharper starting point. Auto-detected: only fires when - * {@code fixedTemperature[j]} is set on at least one tray. Default true. + * Enable Boston-Sullivan inside-out refinement of the seed when at least one tray temperature is + * pinned (e.g. T-spec reboiler). The smooth Wilson BP seed misses sharp T-pinches; B-S inner loop + * (per-component tridiag MB + bubble-T) captures them and gives SR / Newton a sharper starting + * point. Auto-detected: only fires when {@code fixedTemperature[j]} is set on at least one tray. + * Default true. */ private boolean enableBostonSullivan = true; @@ -171,10 +176,11 @@ public class NaphtaliSandholmSolver { private double refluxRatio; /** - * When true, the column uses overall mass balance closure L[0] = totalFeed - V[N-1] instead of V[0] = - * boilupRatio*L[0]. Enabled automatically when the user pins reboiler temperature but does not explicitly set a - * boilup ratio (i.e. boilupRatio is still the NeqSim default of 0.1). With pinned T, V[0] must be free to be - * determined by physics, not constrained to an arbitrary default ratio. + * When true, the column uses overall mass balance closure L[0] = totalFeed - V[N-1] instead of + * V[0] = boilupRatio*L[0]. Enabled automatically when the user pins reboiler temperature but does + * not explicitly set a boilup ratio (i.e. boilupRatio is still the NeqSim default of 0.1). With + * pinned T, V[0] must be free to be determined by physics, not constrained to an arbitrary + * default ratio. */ private boolean useOverallMBClosure = false; @@ -224,14 +230,14 @@ public class NaphtaliSandholmSolver { private double lastLinearSolveTimeSeconds; /** - * Original feed thermo systems cloned before column init() corrupts them. Map of tray index to list of - * SystemInterface clones. Null if not provided. + * Original feed thermo systems cloned before column init() corrupts them. Map of tray index to + * list of SystemInterface clones. Null if not provided. */ private Map> originalFeedSystems; /** - * Original feed molar flow rates in mol/hr, saved before column init(). Map of tray index to list of flow rates. Null - * if not provided. + * Original feed molar flow rates in mol/hr, saved before column init(). Map of tray index to list + * of flow rates. Null if not provided. */ private Map> originalFeedFlowRates; @@ -367,16 +373,18 @@ public NaphtaliSandholmSolver(DistillationColumn column) { * Construct a solver with pre-saved original feed thermo systems and flow rates. * *

- * The column's init() method may modify feed streams in-place (shared object references with trays). This constructor - * accepts deep clones of the original feed thermo systems and their molar flow rates, taken before init() runs, so - * the solver always uses the correct feed T, P, composition, and total moles. + * The column's init() method may modify feed streams in-place (shared object references with + * trays). This constructor accepts deep clones of the original feed thermo systems and their + * molar flow rates, taken before init() runs, so the solver always uses the correct feed T, P, + * composition, and total moles. *

* * @param column the distillation column to solve * @param originalFeedSystems map of tray index to list of original feed SystemInterface clones * @param originalFeedFlowRates map of tray index to list of feed molar flow rates in mol/hr */ - public NaphtaliSandholmSolver(DistillationColumn column, Map> originalFeedSystems, + public NaphtaliSandholmSolver(DistillationColumn column, + Map> originalFeedSystems, Map> originalFeedFlowRates) { this.column = column; this.originalFeedSystems = originalFeedSystems; @@ -434,11 +442,11 @@ public boolean solve(UUID id) { // can converge from a reasonable CMO start. boolean bpConverged; if (useOverallMBClosure) { - logger.info("NS: bypassing BP/SR (useOverallMBClosure active) — direct Newton"); - seedSolutionForDirectNewton(); - bpConverged = true; + logger.info("NS: bypassing BP/SR (useOverallMBClosure active) — direct Newton"); + seedSolutionForDirectNewton(); + bpConverged = true; } else { - bpConverged = solveBubblePointMethod(); + bpConverged = solveBubblePointMethod(); } // Phase 1.5: Keep BP's V profile (the material balance V correction was wrong @@ -448,97 +456,102 @@ public boolean solve(UUID id) { // Evaluate residuals after BP + V correction evaluateThermo(); double[] residual = computeResidual(); - double norm = vectorNorm(residual); - logger.debug("NS after BP: ||F|| = {} converged={}", String.format("%.6e", norm), bpConverged); + double norm = LinearAlgebraOps.vectorNorm(residual); + logger.debug("NS after BP: ||F|| = {} converged={}", String.format("%.6e", norm), + bpConverged); // Log per-tray residual breakdown at debug level if (logger.isDebugEnabled()) { - for (int j = 0; j < N; j++) { - int base = j * varsPerTray; - double matNorm = 0; - for (int i = 0; i < C; i++) { - matNorm += residual[base + i] * residual[base + i]; - } - matNorm = Math.sqrt(matNorm); - logger.debug(" Tray {}: T={}C L={} V={} matRes={} hRes={} sumRes={}", j, - String.format("%.1f", T[j] - 273.15), String.format("%.4f", L[j]), String.format("%.4f", V[j]), - String.format("%.4e", matNorm), String.format("%.4e", Math.abs(residual[base + C])), - String.format("%.4e", Math.abs(residual[base + C + 1]))); - } + for (int j = 0; j < N; j++) { + int base = j * varsPerTray; + double matNorm = 0; + for (int i = 0; i < C; i++) { + matNorm += residual[base + i] * residual[base + i]; + } + matNorm = Math.sqrt(matNorm); + logger.debug(" Tray {}: T={}C L={} V={} matRes={} hRes={} sumRes={}", j, + String.format("%.1f", T[j] - 273.15), String.format("%.4f", L[j]), + String.format("%.4f", V[j]), String.format("%.4e", matNorm), + String.format("%.4e", Math.abs(residual[base + C])), + String.format("%.4e", Math.abs(residual[base + C + 1]))); + } } if (useOverallMBClosure && norm < tolerance) { - System.out.println("[NS] Full residual norm (" + norm + ") < tolerance — exiting early without energy check"); - applyResultsToColumn(id, 0, norm, startTime); - return true; + System.out.println("[NS] Full residual norm (" + norm + + ") < tolerance — exiting early without energy check"); + applyResultsToColumn(id, 0, norm, startTime); + return true; } // Check if mass AND energy balance are both acceptable double mbErrorBP = computeMassBalanceError(); double energyErrorBP = computeMaxRelativeEnergyError(); - logger.info("NS after BP+EOS: massBalErr={}% energyErr={}%", String.format("%.4f", mbErrorBP * 100), - String.format("%.2f", energyErrorBP * 100)); - System.out.println("[NS] after BP: mbErr=" + String.format("%.4f", mbErrorBP * 100) + "% energyErr=" - + String.format("%.2f", energyErrorBP * 100) + "%"); + logger.info("NS after BP+EOS: massBalErr={}% energyErr={}%", + String.format("%.4f", mbErrorBP * 100), String.format("%.2f", energyErrorBP * 100)); + System.out.println("[NS] after BP: mbErr=" + String.format("%.4f", mbErrorBP * 100) + + "% energyErr=" + String.format("%.2f", energyErrorBP * 100) + "%"); // Accept BP only if both mb and energy are very tight. If mb is OK but // energy is in the 1-5% range, run Sum-Rates (which adjusts T from the // energy balance) — that refines T without invoking Newton, which is // documented to diverge for the no-condenser/T-spec topology. if (useOverallMBClosure && mbErrorBP < 0.005 && energyErrorBP < 0.01) { - logger.info("NS: mass+energy balance OK (mb={}%, E={}%), accepting solution", - String.format("%.4f", mbErrorBP * 100), String.format("%.2f", energyErrorBP * 100)); - System.out.println("[NS] Both OK — accepting BP solution (no SR needed)"); - applyResultsToColumn(id, 0, norm, startTime); - return true; + logger.info("NS: mass+energy balance OK (mb={}%, E={}%), accepting solution", + String.format("%.4f", mbErrorBP * 100), String.format("%.2f", energyErrorBP * 100)); + System.out.println("[NS] Both OK — accepting BP solution (no SR needed)"); + applyResultsToColumn(id, 0, norm, startTime); + return true; } if (mbErrorBP < 0.005 && energyErrorBP >= 0.01) { - // Mass balance OK but energy not — use Sum-Rates method to correct T - // The BP method determines T from bubble-point (sum Kx = 1), which - // fails for wide-boiling / absorber columns. SR determines T from - // energy balance instead, which is more appropriate. - logger.info("NS: mass balance OK ({}%) but energy imbalance ({}%) — running Sum-Rates correction", - String.format("%.4f", mbErrorBP * 100), String.format("%.2f", energyErrorBP * 100)); - System.out.println("[NS] --> Calling solveSumRatesPhase()"); - solveSumRatesPhase(); - - // Re-evaluate after Sum-Rates and decide whether SR was sufficient. - evaluateThermo(); - residual = computeResidual(); - norm = vectorNorm(residual); - double mbAfterSR = computeMassBalanceError(); - double energyAfterSR = computeMaxRelativeEnergyError(); - if (mbAfterSR < 0.005 && energyAfterSR < 0.05 && norm < tolerance) { - System.out.println("[NS] SR fully converged (||F|| below tol) — accepting"); - applyResultsToColumn(id, 0, norm, startTime); - return true; - } - // For useOverallMBClosure (T-spec'd reboiler stripper / no condenser): - // Newton's full Jacobian is ill-conditioned for this topology - // (V[N-1] has no upstream anchor without a condenser, so K-value - // sensitivities propagate exponentially). When SR has already - // produced a result with mass balance closed AND energy balance - // below 5%, that is the best achievable answer — accept it and - // skip Newton entirely (Newton has been observed to diverge by - // 9 orders of magnitude on this topology). - // For useOverallMBClosure (T-spec'd reboiler stripper / no condenser): - // Newton's full Jacobian is ill-conditioned for this topology - // (V[N-1] has no upstream anchor without a condenser, so K-value - // sensitivities propagate exponentially — observed Newton blowup - // from ||F||=4 to 1e5 in one step). Once SR has closed mass balance - // and energy balance, accept that result and skip Newton. - if (useOverallMBClosure && mbAfterSR < 0.05 && energyAfterSR < 0.05) { - System.out.println("[NS] useOverallMBClosure path: accepting SR result " + "(mb=" - + String.format("%.4f%%", mbAfterSR * 100) + ", energy=" + String.format("%.2f%%", energyAfterSR * 100) - + ") — skipping Newton (ill-conditioned)."); - applyResultsToColumn(id, 0, norm, startTime); - return true; - } - // SR closed energy locally but the full Newton residual is still - // above tolerance — fall through to Newton so L and V get refined - // (SR only adjusts T; BP's L/V profile may be wrong and Newton is - // needed to fix it). - System.out.println("[NS] SR closed energy but ||F||=" + String.format("%.3e", norm) + " > tol=" + tolerance - + " — falling through to Newton"); + // Mass balance OK but energy not — use Sum-Rates method to correct T + // The BP method determines T from bubble-point (sum Kx = 1), which + // fails for wide-boiling / absorber columns. SR determines T from + // energy balance instead, which is more appropriate. + logger.info( + "NS: mass balance OK ({}%) but energy imbalance ({}%) — running Sum-Rates correction", + String.format("%.4f", mbErrorBP * 100), String.format("%.2f", energyErrorBP * 100)); + System.out.println("[NS] --> Calling solveSumRatesPhase()"); + solveSumRatesPhase(); + + // Re-evaluate after Sum-Rates and decide whether SR was sufficient. + evaluateThermo(); + residual = computeResidual(); + norm = LinearAlgebraOps.vectorNorm(residual); + double mbAfterSR = computeMassBalanceError(); + double energyAfterSR = computeMaxRelativeEnergyError(); + if (mbAfterSR < 0.005 && energyAfterSR < 0.05 && norm < tolerance) { + System.out.println("[NS] SR fully converged (||F|| below tol) — accepting"); + applyResultsToColumn(id, 0, norm, startTime); + return true; + } + // For useOverallMBClosure (T-spec'd reboiler stripper / no condenser): + // Newton's full Jacobian is ill-conditioned for this topology + // (V[N-1] has no upstream anchor without a condenser, so K-value + // sensitivities propagate exponentially). When SR has already + // produced a result with mass balance closed AND energy balance + // below 5%, that is the best achievable answer — accept it and + // skip Newton entirely (Newton has been observed to diverge by + // 9 orders of magnitude on this topology). + // For useOverallMBClosure (T-spec'd reboiler stripper / no condenser): + // Newton's full Jacobian is ill-conditioned for this topology + // (V[N-1] has no upstream anchor without a condenser, so K-value + // sensitivities propagate exponentially — observed Newton blowup + // from ||F||=4 to 1e5 in one step). Once SR has closed mass balance + // and energy balance, accept that result and skip Newton. + if (useOverallMBClosure && mbAfterSR < 0.05 && energyAfterSR < 0.05) { + System.out.println("[NS] useOverallMBClosure path: accepting SR result " + "(mb=" + + String.format("%.4f%%", mbAfterSR * 100) + ", energy=" + + String.format("%.2f%%", energyAfterSR * 100) + + ") — skipping Newton (ill-conditioned)."); + applyResultsToColumn(id, 0, norm, startTime); + return true; + } + // SR closed energy locally but the full Newton residual is still + // above tolerance — fall through to Newton so L and V get refined + // (SR only adjusts T; BP's L/V profile may be wrong and Newton is + // needed to fix it). + System.out.println("[NS] SR closed energy but ||F||=" + String.format("%.3e", norm) + + " > tol=" + tolerance + " — falling through to Newton"); } // Phase 2: Newton refinement from BP solution (includes energy equations) @@ -558,162 +571,168 @@ public boolean solve(UUID id) { double prevBestNorm = bestNorm; for (int iter = 1; iter <= maxIterations; iter++) { - // Time guard for Newton iterations - if (System.nanoTime() - newtonStart > maxNewtonTimeNs) { - logger.info("NS Newton: time limit reached at iter {}", iter); - restoreTrayState(bestLiq, bestT, bestV); - evaluateThermo(); - double mbErr = computeMassBalanceError(); - double eErr = computeMaxRelativeEnergyError(); - logger.info("NS Newton: bestNorm={} massBalErr={}% energyErr={}%", String.format("%.6e", bestNorm), - String.format("%.4f", mbErr * 100), String.format("%.2f", eErr * 100)); - if (mbErr < 0.01) { - applyResultsToColumn(id, iter, bestNorm, startTime); - return true; - } - break; - } - - if (norm < tolerance) { - logger.info("Naphtali-Sandholm converged in {} iterations, ||F|| = {}", iter - 1, - String.format("%.6e", norm)); - applyResultsToColumn(id, iter - 1, norm, startTime); - return true; - } - - // Compute Jacobian analytically - double[][] jacobian = computeJacobian(residual); - - // Solve J * dx = F using block-tridiagonal solver - double[] dx = solveBlockTridiagonal(jacobian, residual); - if (dx == null) { - // Fall back to full LU solve - dx = solveDenseLU(jacobian, residual); - if (dx == null) { - logger.error("Naphtali-Sandholm: linear solver failed at iteration {}", iter); - // Restore best state and try to continue with smaller steps - restoreTrayState(bestLiq, bestT, bestV); - evaluateThermo(); - residual = computeResidual(); - norm = vectorNorm(residual); - failedSteps++; - if (failedSteps > 5) { - applyResultsToColumn(id, iter, bestNorm, startTime); - return false; - } - continue; - } - } - - // Trust-region clamp: limit per-variable change so a single Newton - // step cannot leave the basin of attraction. For ill-conditioned - // topologies (no condenser, T-spec'd reboiler) this is the - // difference between converging and diverging by orders of - // magnitude in one iteration. - double trScale = applyTrustRegion(dx); - if (trScale < 1.0 && (iter <= 3 || iter % 10 == 0)) { - System.out.println("[Newton] iter " + iter + " trust-region scale=" + String.format("%.3e", trScale)); - } - - // Line search: backtrack if step increases residual norm - double alpha = lineSearch(dx, norm); - - // Apply update with step size alpha - applyUpdate(dx, alpha); - - // Re-evaluate thermodynamics at new state - evaluateThermo(); - - residual = computeResidual(); - double newNorm = vectorNorm(residual); - - logger.debug("NS iter {}: ||F|| = {} alpha={}", iter, String.format("%.6e", newNorm), - String.format("%.4f", alpha)); - - // Log every iteration while we diagnose the component-imbalance signal. - { - double mbIt = computeMassBalanceError(); - double eIt = computeMaxRelativeEnergyError(); - double compIt = computeMaxComponentImbalance(); - logger.info("NS Newton iter {}: ||F||={} mb={}% energy={}% maxComp={}% T[top]={}C alpha={}", iter, - String.format("%.4e", newNorm), String.format("%.4f", mbIt * 100), String.format("%.2f", eIt * 100), - String.format("%.4f", compIt * 100), String.format("%.1f", T[N - 1] - 273.15), - String.format("%.4f", alpha)); - System.out.println("[Newton] iter " + iter + ": ||F||=" + String.format("%.4e", newNorm) + " mb=" - + String.format("%.4f", mbIt * 100) + "% energy=" + String.format("%.2f", eIt * 100) + "% maxComp=" - + String.format("%.4f", compIt * 100) + "% T[top]=" + String.format("%.1f", T[N - 1] - 273.15) + "C T[0]=" - + String.format("%.1f", T[0] - 273.15) + "C alpha=" + String.format("%.4f", alpha)); - } - - if (Double.isNaN(newNorm) || Double.isInfinite(newNorm)) { - // Restore best state and continue - restoreTrayState(bestLiq, bestT, bestV); - evaluateThermo(); - residual = computeResidual(); - norm = vectorNorm(residual); - failedSteps++; - logger.warn("NS: NaN/Inf — reverting to best (||F||={})", String.format("%.6e", norm)); - if (failedSteps > 5) { - applyResultsToColumn(id, iter, bestNorm, startTime); - return false; - } - continue; - } - - // Strict descent enforcement: reject steps that increase residual too much - if (newNorm > 1.5 * norm && newNorm > tolerance) { - restoreTrayState(bestLiq, bestT, bestV); - evaluateThermo(); - residual = computeResidual(); - norm = vectorNorm(residual); - failedSteps++; - logger.debug("NS: rejected step (too large increase) — reverting to best (||F||={})", - String.format("%.6e", norm)); - if (failedSteps > 10) { - break; - } - continue; - } - - norm = newNorm; - failedSteps = 0; - - // Track best solution found - if (norm < bestNorm) { - bestNorm = norm; - saveTrayState(bestLiq, bestT, bestV); - } - - // Detect stagnation: if bestNorm hasn't improved significantly in 10 iters, - // stop - if (iter % 10 == 0) { - if (bestNorm > 0.95 * prevBestNorm) { - stagnationCount++; - if (stagnationCount >= 2) { - // Restore best state and check mass + energy balance - restoreTrayState(bestLiq, bestT, bestV); - evaluateThermo(); - double mbErr = computeMassBalanceError(); - double eErr = computeMaxRelativeEnergyError(); - logger.info("NS: stagnation detected (bestNorm={}, massBalErr={}%, energyErr={}%)", - String.format("%.6e", bestNorm), String.format("%.4f", mbErr * 100), - String.format("%.2f", eErr * 100)); - if (mbErr < 0.005) { - logger.info("NS: mass balance within 0.5%, accepting (energy={}%)", String.format("%.2f", eErr * 100)); - applyResultsToColumn(id, iter, bestNorm, startTime); - return true; - } - break; - } - } else { - stagnationCount = 0; - } - prevBestNorm = bestNorm; - } + // Time guard for Newton iterations + if (System.nanoTime() - newtonStart > maxNewtonTimeNs) { + logger.info("NS Newton: time limit reached at iter {}", iter); + restoreTrayState(bestLiq, bestT, bestV); + evaluateThermo(); + double mbErr = computeMassBalanceError(); + double eErr = computeMaxRelativeEnergyError(); + logger.info("NS Newton: bestNorm={} massBalErr={}% energyErr={}%", + String.format("%.6e", bestNorm), String.format("%.4f", mbErr * 100), + String.format("%.2f", eErr * 100)); + if (mbErr < 0.01) { + applyResultsToColumn(id, iter, bestNorm, startTime); + return true; + } + break; + } + + if (norm < tolerance) { + logger.info("Naphtali-Sandholm converged in {} iterations, ||F|| = {}", iter - 1, + String.format("%.6e", norm)); + applyResultsToColumn(id, iter - 1, norm, startTime); + return true; + } + + // Compute Jacobian analytically + double[][] jacobian = computeJacobian(residual); + + // Solve J * dx = F using block-tridiagonal solver + double[] dx = solveBlockTridiagonal(jacobian, residual); + if (dx == null) { + // Fall back to full LU solve + dx = solveDenseLU(jacobian, residual); + if (dx == null) { + logger.error("Naphtali-Sandholm: linear solver failed at iteration {}", iter); + // Restore best state and try to continue with smaller steps + restoreTrayState(bestLiq, bestT, bestV); + evaluateThermo(); + residual = computeResidual(); + norm = LinearAlgebraOps.vectorNorm(residual); + failedSteps++; + if (failedSteps > 5) { + applyResultsToColumn(id, iter, bestNorm, startTime); + return false; + } + continue; + } + } + + // Trust-region clamp: limit per-variable change so a single Newton + // step cannot leave the basin of attraction. For ill-conditioned + // topologies (no condenser, T-spec'd reboiler) this is the + // difference between converging and diverging by orders of + // magnitude in one iteration. + double trScale = applyTrustRegion(dx); + if (trScale < 1.0 && (iter <= 3 || iter % 10 == 0)) { + System.out.println( + "[Newton] iter " + iter + " trust-region scale=" + String.format("%.3e", trScale)); + } + + // Line search: backtrack if step increases residual norm + double alpha = lineSearch(dx, norm); + + // Apply update with step size alpha + applyUpdate(dx, alpha); + + // Re-evaluate thermodynamics at new state + evaluateThermo(); + + residual = computeResidual(); + double newNorm = LinearAlgebraOps.vectorNorm(residual); + + logger.debug("NS iter {}: ||F|| = {} alpha={}", iter, String.format("%.6e", newNorm), + String.format("%.4f", alpha)); + + // Log every iteration while we diagnose the component-imbalance signal. + { + double mbIt = computeMassBalanceError(); + double eIt = computeMaxRelativeEnergyError(); + double compIt = computeMaxComponentImbalance(); + logger.info( + "NS Newton iter {}: ||F||={} mb={}% energy={}% maxComp={}% T[top]={}C alpha={}", iter, + String.format("%.4e", newNorm), String.format("%.4f", mbIt * 100), + String.format("%.2f", eIt * 100), String.format("%.4f", compIt * 100), + String.format("%.1f", T[N - 1] - 273.15), String.format("%.4f", alpha)); + System.out.println("[Newton] iter " + iter + ": ||F||=" + String.format("%.4e", newNorm) + + " mb=" + String.format("%.4f", mbIt * 100) + "% energy=" + + String.format("%.2f", eIt * 100) + "% maxComp=" + + String.format("%.4f", compIt * 100) + "% T[top]=" + + String.format("%.1f", T[N - 1] - 273.15) + "C T[0]=" + + String.format("%.1f", T[0] - 273.15) + "C alpha=" + String.format("%.4f", alpha)); + } + + if (Double.isNaN(newNorm) || Double.isInfinite(newNorm)) { + // Restore best state and continue + restoreTrayState(bestLiq, bestT, bestV); + evaluateThermo(); + residual = computeResidual(); + norm = LinearAlgebraOps.vectorNorm(residual); + failedSteps++; + logger.warn("NS: NaN/Inf — reverting to best (||F||={})", String.format("%.6e", norm)); + if (failedSteps > 5) { + applyResultsToColumn(id, iter, bestNorm, startTime); + return false; + } + continue; + } + + // Strict descent enforcement: reject steps that increase residual too much + if (newNorm > 1.5 * norm && newNorm > tolerance) { + restoreTrayState(bestLiq, bestT, bestV); + evaluateThermo(); + residual = computeResidual(); + norm = LinearAlgebraOps.vectorNorm(residual); + failedSteps++; + logger.debug("NS: rejected step (too large increase) — reverting to best (||F||={})", + String.format("%.6e", norm)); + if (failedSteps > 10) { + break; + } + continue; + } + + norm = newNorm; + failedSteps = 0; + + // Track best solution found + if (norm < bestNorm) { + bestNorm = norm; + saveTrayState(bestLiq, bestT, bestV); + } + + // Detect stagnation: if bestNorm hasn't improved significantly in 10 iters, + // stop + if (iter % 10 == 0) { + if (bestNorm > 0.95 * prevBestNorm) { + stagnationCount++; + if (stagnationCount >= 2) { + // Restore best state and check mass + energy balance + restoreTrayState(bestLiq, bestT, bestV); + evaluateThermo(); + double mbErr = computeMassBalanceError(); + double eErr = computeMaxRelativeEnergyError(); + logger.info("NS: stagnation detected (bestNorm={}, massBalErr={}%, energyErr={}%)", + String.format("%.6e", bestNorm), String.format("%.4f", mbErr * 100), + String.format("%.2f", eErr * 100)); + if (mbErr < 0.005) { + logger.info("NS: mass balance within 0.5%, accepting (energy={}%)", + String.format("%.2f", eErr * 100)); + applyResultsToColumn(id, iter, bestNorm, startTime); + return true; + } + break; + } + } else { + stagnationCount = 0; + } + prevBestNorm = bestNorm; + } } logger.warn("Naphtali-Sandholm did not converge in {} iterations, ||F|| = {}", maxIterations, - String.format("%.6e", norm)); + String.format("%.6e", norm)); applyResultsToColumn(id, maxIterations, norm, startTime); return norm < tolerance * 100; // partial convergence } catch (Exception ex) { @@ -748,8 +767,8 @@ private void initialize() { StreamInterface firstFeed = null; for (List feeds : feedMap.values()) { if (!feeds.isEmpty()) { - firstFeed = feeds.get(0); - break; + firstFeed = feeds.get(0); + break; } } if (firstFeed == null) { @@ -780,12 +799,12 @@ private void initialize() { for (int j = 0; j < N; j++) { double eta = column.getMurphreeEfficiency(j); if (j == 0 || (hasCondenser && j == N - 1)) { - eta = 1.0; + eta = 1.0; } if (Double.isNaN(eta) || eta <= 0.0) { - eta = 1.0e-6; + eta = 1.0e-6; } else if (eta > 1.0) { - eta = 1.0; + eta = 1.0; } trayEta[j] = eta; } @@ -809,9 +828,9 @@ private void initialize() { for (int j = 0; j < N; j++) { SimpleTray tray = (SimpleTray) column.getTray(j); if (tray.trayPressure > 0) { - P[j] = tray.trayPressure * 1e5; // bara to Pa + P[j] = tray.trayPressure * 1e5; // bara to Pa } else { - P[j] = fallbackPressure; + P[j] = fallbackPressure; } } @@ -820,122 +839,124 @@ private void initialize() { for (Map.Entry> entry : feedMap.entrySet()) { int trayIdx = entry.getKey(); if (trayIdx < 0 || trayIdx >= N) { - continue; + continue; } List feeds = entry.getValue(); - List origSystems = (originalFeedSystems != null) ? originalFeedSystems.get(trayIdx) : null; + List origSystems = + (originalFeedSystems != null) ? originalFeedSystems.get(trayIdx) : null; for (int fi = 0; fi < feeds.size(); fi++) { - // Always start from a fresh clone of the *original* feed system if we - // have one (it has the user's T, P, composition before init() touched - // anything). Otherwise fall back to the current stream's system. - SystemInterface feedSys; - if (origSystems != null && fi < origSystems.size()) { - feedSys = origSystems.get(fi).clone(); - } else { - feedSys = feeds.get(fi).getThermoSystem().clone(); - } - // ALWAYS TPflash the feed before using it. Previously the origSystems - // path skipped re-flashing on the assumption that the upstream caller - // had already done a flash, but that assumption is brittle — a feed - // built with only init(0) reports beta=1.0 / nPhases=1 even when it - // is physically a subcooled liquid, which causes the solver to inject - // the feed as vapor and destroys the column's internal traffic. - feedSys.setMultiPhaseCheck(false); - feedSys.setNumberOfPhases(2); - ThermodynamicOperations feedOps = new ThermodynamicOperations(feedSys); - try { - feedOps.TPflash(); - } catch (Exception e) { - logger.warn("Feed TPflash failed on tray {}, using raw feed data", trayIdx); - } - feedSys.init(2); - - double feedMoles; - if (originalFeedFlowRates != null && originalFeedFlowRates.containsKey(trayIdx) - && fi < originalFeedFlowRates.get(trayIdx).size()) { - // Use the correct flow rate from the ORIGINAL stream (mol/hr) - feedMoles = originalFeedFlowRates.get(trayIdx).get(fi); - } else { - feedMoles = feedSys.getTotalNumberOfMoles(); - } - double beta = feedSys.getBeta(); // vapor fraction (only meaningful when 2 phases present) - // For single-phase feeds, getBeta() returns the fraction of phase(0) - // which is 1.0 regardless of phase type — use the actual phase type - // to decide vapor vs liquid. - boolean singlePhaseIsVapor = false; - if (feedSys.getNumberOfPhases() == 1) { - PhaseType pt = feedSys.getPhase(0).getType(); - singlePhaseIsVapor = (pt == PhaseType.GAS); - } - System.out.println("[NS-FEED] tray " + trayIdx + ": feedMoles=" + String.format("%.2f", feedMoles) + " beta=" - + String.format("%.4f", beta) + " nPhases=" + feedSys.getNumberOfPhases() + " phase0Type=" - + feedSys.getPhase(0).getType() + " singlePhaseIsVapor=" + singlePhaseIsVapor + " T=" - + String.format("%.1fC", feedSys.getTemperature() - 273.15) + " P=" - + String.format("%.2fbar", feedSys.getPressure()) + " totalH=" - + String.format("%.0f", feedSys.getEnthalpy())); - - for (int i = 0; i < C; i++) { - double zi = feedSys.getPhase(0).getComponent(i).getx(); // overall composition - if (feedSys.getNumberOfPhases() > 1) { - // Split feed into vapor and liquid portions - double yi = feedSys.getPhase(0).getComponent(i).getx(); - double xi = feedSys.getPhase(1).getComponent(i).getx(); - feedVap[trayIdx][i] += feedMoles * beta * yi; - feedLiq[trayIdx][i] += feedMoles * (1.0 - beta) * xi; - } else { - // Single phase feed — route by actual phase type, NOT beta. - if (singlePhaseIsVapor) { - feedVap[trayIdx][i] += feedMoles * zi; - } else { - feedLiq[trayIdx][i] += feedMoles * zi; - } - } - } - - // Feed enthalpies — use computeSinglePhaseEnthalpy() for the SAME - // reference state as tray enthalpies. The previous approach used - // feedSys.getPhase().getEnthalpy() which has a different EOS init path - // and produces incompatible absolute enthalpy values. - double feedTempK = feedSys.getTemperature(); - double feedPressBar = feedSys.getPressure(); - - if (feedSys.getNumberOfPhases() > 1) { - double nVap = feedMoles * beta; - double nLiq = feedMoles * (1.0 - beta); - - if (nVap > 0) { - // Extract vapor composition from the flash - double[] yFeed = new double[C]; - for (int i = 0; i < C; i++) { - yFeed[i] = feedSys.getPhase(0).getComponent(i).getx(); - } - feedHV[trayIdx] = computeSinglePhaseEnthalpy(yFeed, feedTempK, feedPressBar, true); - } - if (nLiq > 0) { - // Extract liquid composition from the flash - double[] xFeed = new double[C]; - for (int i = 0; i < C; i++) { - xFeed[i] = feedSys.getPhase(1).getComponent(i).getx(); - } - feedHL[trayIdx] = computeSinglePhaseEnthalpy(xFeed, feedTempK, feedPressBar, false); - } - feedVTotal[trayIdx] += nVap; - feedLTotal[trayIdx] += nLiq; - } else { - // Single phase feed — route by actual phase type, NOT beta. - double[] zFeed = new double[C]; - for (int i = 0; i < C; i++) { - zFeed[i] = feedSys.getPhase(0).getComponent(i).getx(); - } - if (singlePhaseIsVapor) { - feedHV[trayIdx] = computeSinglePhaseEnthalpy(zFeed, feedTempK, feedPressBar, true); - feedVTotal[trayIdx] += feedMoles; - } else { - feedHL[trayIdx] = computeSinglePhaseEnthalpy(zFeed, feedTempK, feedPressBar, false); - feedLTotal[trayIdx] += feedMoles; - } - } + // Always start from a fresh clone of the *original* feed system if we + // have one (it has the user's T, P, composition before init() touched + // anything). Otherwise fall back to the current stream's system. + SystemInterface feedSys; + if (origSystems != null && fi < origSystems.size()) { + feedSys = origSystems.get(fi).clone(); + } else { + feedSys = feeds.get(fi).getThermoSystem().clone(); + } + // ALWAYS TPflash the feed before using it. Previously the origSystems + // path skipped re-flashing on the assumption that the upstream caller + // had already done a flash, but that assumption is brittle — a feed + // built with only init(0) reports beta=1.0 / nPhases=1 even when it + // is physically a subcooled liquid, which causes the solver to inject + // the feed as vapor and destroys the column's internal traffic. + feedSys.setMultiPhaseCheck(false); + feedSys.setNumberOfPhases(2); + ThermodynamicOperations feedOps = new ThermodynamicOperations(feedSys); + try { + feedOps.TPflash(); + } catch (Exception e) { + logger.warn("Feed TPflash failed on tray {}, using raw feed data", trayIdx); + } + feedSys.init(2); + + double feedMoles; + if (originalFeedFlowRates != null && originalFeedFlowRates.containsKey(trayIdx) + && fi < originalFeedFlowRates.get(trayIdx).size()) { + // Use the correct flow rate from the ORIGINAL stream (mol/hr) + feedMoles = originalFeedFlowRates.get(trayIdx).get(fi); + } else { + feedMoles = feedSys.getTotalNumberOfMoles(); + } + double beta = feedSys.getBeta(); // vapor fraction (only meaningful when 2 phases present) + // For single-phase feeds, getBeta() returns the fraction of phase(0) + // which is 1.0 regardless of phase type — use the actual phase type + // to decide vapor vs liquid. + boolean singlePhaseIsVapor = false; + if (feedSys.getNumberOfPhases() == 1) { + PhaseType pt = feedSys.getPhase(0).getType(); + singlePhaseIsVapor = (pt == PhaseType.GAS); + } + System.out.println("[NS-FEED] tray " + trayIdx + ": feedMoles=" + + String.format("%.2f", feedMoles) + " beta=" + String.format("%.4f", beta) + + " nPhases=" + feedSys.getNumberOfPhases() + " phase0Type=" + + feedSys.getPhase(0).getType() + " singlePhaseIsVapor=" + singlePhaseIsVapor + " T=" + + String.format("%.1fC", feedSys.getTemperature() - 273.15) + " P=" + + String.format("%.2fbar", feedSys.getPressure()) + " totalH=" + + String.format("%.0f", feedSys.getEnthalpy())); + + for (int i = 0; i < C; i++) { + double zi = feedSys.getPhase(0).getComponent(i).getx(); // overall composition + if (feedSys.getNumberOfPhases() > 1) { + // Split feed into vapor and liquid portions + double yi = feedSys.getPhase(0).getComponent(i).getx(); + double xi = feedSys.getPhase(1).getComponent(i).getx(); + feedVap[trayIdx][i] += feedMoles * beta * yi; + feedLiq[trayIdx][i] += feedMoles * (1.0 - beta) * xi; + } else { + // Single phase feed — route by actual phase type, NOT beta. + if (singlePhaseIsVapor) { + feedVap[trayIdx][i] += feedMoles * zi; + } else { + feedLiq[trayIdx][i] += feedMoles * zi; + } + } + } + + // Feed enthalpies — use computeSinglePhaseEnthalpy() for the SAME + // reference state as tray enthalpies. The previous approach used + // feedSys.getPhase().getEnthalpy() which has a different EOS init path + // and produces incompatible absolute enthalpy values. + double feedTempK = feedSys.getTemperature(); + double feedPressBar = feedSys.getPressure(); + + if (feedSys.getNumberOfPhases() > 1) { + double nVap = feedMoles * beta; + double nLiq = feedMoles * (1.0 - beta); + + if (nVap > 0) { + // Extract vapor composition from the flash + double[] yFeed = new double[C]; + for (int i = 0; i < C; i++) { + yFeed[i] = feedSys.getPhase(0).getComponent(i).getx(); + } + feedHV[trayIdx] = computeSinglePhaseEnthalpy(yFeed, feedTempK, feedPressBar, true); + } + if (nLiq > 0) { + // Extract liquid composition from the flash + double[] xFeed = new double[C]; + for (int i = 0; i < C; i++) { + xFeed[i] = feedSys.getPhase(1).getComponent(i).getx(); + } + feedHL[trayIdx] = computeSinglePhaseEnthalpy(xFeed, feedTempK, feedPressBar, false); + } + feedVTotal[trayIdx] += nVap; + feedLTotal[trayIdx] += nLiq; + } else { + // Single phase feed — route by actual phase type, NOT beta. + double[] zFeed = new double[C]; + for (int i = 0; i < C; i++) { + zFeed[i] = feedSys.getPhase(0).getComponent(i).getx(); + } + if (singlePhaseIsVapor) { + feedHV[trayIdx] = computeSinglePhaseEnthalpy(zFeed, feedTempK, feedPressBar, true); + feedVTotal[trayIdx] += feedMoles; + } else { + feedHL[trayIdx] = computeSinglePhaseEnthalpy(zFeed, feedTempK, feedPressBar, false); + feedLTotal[trayIdx] += feedMoles; + } + } } } @@ -944,7 +965,7 @@ private void initialize() { double totalFeedMoles = 0; for (int j = 0; j < N; j++) { for (int i = 0; i < C; i++) { - totalFeedMoles += feedLiq[j][i] + feedVap[j][i]; + totalFeedMoles += feedLiq[j][i] + feedVap[j][i]; } } flowScale = Math.max(totalFeedMoles / N, 1.0); @@ -956,9 +977,9 @@ private void initialize() { for (int j = 0; j < N; j++) { SimpleTray tray = (SimpleTray) column.getTray(j); if (tray.isSetOutTemperature() && !Double.isNaN(tray.getOutTemperature())) { - fixedTemperature[j] = tray.getOutTemperature(); - T[j] = fixedTemperature[j]; // pin temperature to specified value - logger.info("Tray {} has fixed temperature: {} K", j, fixedTemperature[j]); + fixedTemperature[j] = tray.getOutTemperature(); + T[j] = fixedTemperature[j]; // pin temperature to specified value + logger.info("Tray {} has fixed temperature: {} K", j, fixedTemperature[j]); } } @@ -967,11 +988,11 @@ private void initialize() { // Newton initializer so the solver enters the right basin of attraction. if (column.hasSeedTemperatures()) { for (int j = 0; j < N; j++) { - double seed = column.getSeedTemperature(j); - if (!Double.isNaN(seed) && Double.isNaN(fixedTemperature[j])) { - seedTemperature[j] = seed; - logger.info("Tray {} has seed temperature: {} K", j, seed); - } + double seed = column.getSeedTemperature(j); + if (!Double.isNaN(seed) && Double.isNaN(fixedTemperature[j])) { + seedTemperature[j] = seed; + logger.info("Tray {} has seed temperature: {} K", j, seed); + } } } @@ -991,12 +1012,12 @@ private void initialize() { // Wilson bubble T of the mixed feed yields a wildly wrong value for // wide-boiling systems like ethane + n-pentane). if (Math.abs(boilupRatio - 0.1) > 1e-9 && !Double.isNaN(fixedTemperature[0])) { - logger.info("NS: B={} (non-default) — treating reboiler T={} K as seed, not constraint", boilupRatio, - fixedTemperature[0]); - // Stash the user-provided T as an initializer seed and clear the - // fixed-T flag so MESH residuals enforce V[0]=B*L[0] instead. - seedTemperature[0] = fixedTemperature[0]; - fixedTemperature[0] = Double.NaN; + logger.info("NS: B={} (non-default) — treating reboiler T={} K as seed, not constraint", + boilupRatio, fixedTemperature[0]); + // Stash the user-provided T as an initializer seed and clear the + // fixed-T flag so MESH residuals enforce V[0]=B*L[0] instead. + seedTemperature[0] = fixedTemperature[0]; + fixedTemperature[0] = Double.NaN; } } if (hasCondenser) { @@ -1013,27 +1034,32 @@ private void initialize() { // balance L[0] = totalFeed - V[N-1]. The Newton phase has enough residual // equations (C component MB + T-spec + sumKx=1) to fully determine V[0] // and L[0] at the reboiler tray once a sensible initial guess is in place. - useOverallMBClosure = hasReboiler && !Double.isNaN(fixedTemperature[0]) && Math.abs(boilupRatio - 0.1) < 1e-9; + useOverallMBClosure = + hasReboiler && !Double.isNaN(fixedTemperature[0]) && Math.abs(boilupRatio - 0.1) < 1e-9; if (useOverallMBClosure) { logger.info("NS: T-spec at reboiler with default boilup — using overall MB closure " - + "(L[0] = totalFeed - V[N-1]) in BP/SR init"); + + "(L[0] = totalFeed - V[N-1]) in BP/SR init"); } initializeTrayState(); - logger.info("Naphtali-Sandholm initialized: N={}, C={}, totalFeedMoles={}, " + "hasReboiler={}, hasCondenser={}", N, - C, String.format("%.4f", totalFeedMoles), hasReboiler, hasCondenser); + logger.info( + "Naphtali-Sandholm initialized: N={}, C={}, totalFeedMoles={}, " + + "hasReboiler={}, hasCondenser={}", + N, C, String.format("%.4f", totalFeedMoles), hasReboiler, hasCondenser); if (logger.isDebugEnabled()) { for (int j = 0; j < N; j++) { - logger.debug(" Tray {}: T={}K P={}bara L={} V={}{}", j, String.format("%.2f", T[j]), - String.format("%.2f", P[j] / 1e5), String.format("%.4f", L[j]), String.format("%.4f", V[j]), - (Double.isNaN(fixedTemperature[j]) ? "" : " FIXED_T=" + fixedTemperature[j])); + logger.debug(" Tray {}: T={}K P={}bara L={} V={}{}", j, String.format("%.2f", T[j]), + String.format("%.2f", P[j] / 1e5), String.format("%.4f", L[j]), + String.format("%.4f", V[j]), + (Double.isNaN(fixedTemperature[j]) ? "" : " FIXED_T=" + fixedTemperature[j])); } } } /** - * Initialize tray temperatures, flows, and compositions using Wilson K-values and constant molar overflow estimates. + * Initialize tray temperatures, flows, and compositions using Wilson K-values and constant molar + * overflow estimates. */ private void initializeTrayState() { // Compute total feed flow and average feed composition @@ -1041,9 +1067,9 @@ private void initializeTrayState() { double[] feedComp = new double[C]; for (int j = 0; j < N; j++) { for (int i = 0; i < C; i++) { - double fi = feedLiq[j][i] + feedVap[j][i]; - feedComp[i] += fi; - totalFeedFlow += fi; + double fi = feedLiq[j][i] + feedVap[j][i]; + feedComp[i] += fi; + totalFeedFlow += fi; } } for (int i = 0; i < C; i++) { @@ -1056,11 +1082,11 @@ private void initializeTrayState() { for (int j = 0; j < N; j++) { double trayFeed = 0; for (int i = 0; i < C; i++) { - trayFeed += feedLiq[j][i] + feedVap[j][i]; + trayFeed += feedLiq[j][i] + feedVap[j][i]; } if (trayFeed > maxFeed) { - maxFeed = trayFeed; - feedTray = j; + maxFeed = trayFeed; + feedTray = j; } } @@ -1076,28 +1102,30 @@ private void initializeTrayState() { for (Map.Entry> entry : feedMap.entrySet()) { int trayIdx = entry.getKey(); List feeds = entry.getValue(); - List origSystems = (originalFeedSystems != null) ? originalFeedSystems.get(trayIdx) : null; + List origSystems = + (originalFeedSystems != null) ? originalFeedSystems.get(trayIdx) : null; for (int fi = 0; fi < feeds.size(); fi++) { - StreamInterface feed = feeds.get(fi); - double tFeed; - double nFeed; - if (origSystems != null && fi < origSystems.size()) { - SystemInterface os = origSystems.get(fi); - tFeed = os.getTemperature(); - nFeed = os.getTotalNumberOfMoles(); - } else { - tFeed = feed.getTemperature(); - nFeed = feed.getFlowRate("mole/sec"); - if (!(nFeed > 0)) { - nFeed = feed.getThermoSystem().getTotalNumberOfMoles(); - } - } - feedTemp += tFeed * nFeed; - feedTempWeight += nFeed; + StreamInterface feed = feeds.get(fi); + double tFeed; + double nFeed; + if (origSystems != null && fi < origSystems.size()) { + SystemInterface os = origSystems.get(fi); + tFeed = os.getTemperature(); + nFeed = os.getTotalNumberOfMoles(); + } else { + tFeed = feed.getTemperature(); + nFeed = feed.getFlowRate("mole/sec"); + if (!(nFeed > 0)) { + nFeed = feed.getThermoSystem().getTotalNumberOfMoles(); + } + } + feedTemp += tFeed * nFeed; + feedTempWeight += nFeed; } } feedTemp /= Math.max(feedTempWeight, 1e-20); - System.out.println("[NS-INIT] computed feedTemp=" + String.format("%.2f", feedTemp - 273.15) + "C"); + System.out + .println("[NS-INIT] computed feedTemp=" + String.format("%.2f", feedTemp - 273.15) + "C"); // Better temperature profile: compute bubble/dew point estimates using Wilson // K. @@ -1130,20 +1158,21 @@ private void initializeTrayState() { // Ensure reasonable temperature range (guards in Kelvin) topTemp = Math.max(topTemp, 150.0); botTemp = Math.max(botTemp, topTemp + 10.0); - System.out.println("[NS-INIT] bubbleT@bot=" + String.format("%.2f", bubbleTatBot - 273.15) + "C dewT@top=" - + String.format("%.2f", dewTatTop - 273.15) + "C feedT=" + String.format("%.2f", feedTemp - 273.15) - + "C => botTemp=" + String.format("%.2f", botTemp - 273.15) + "C topTemp=" - + String.format("%.2f", topTemp - 273.15) + "C"); + System.out.println("[NS-INIT] bubbleT@bot=" + String.format("%.2f", bubbleTatBot - 273.15) + + "C dewT@top=" + String.format("%.2f", dewTatTop - 273.15) + "C feedT=" + + String.format("%.2f", feedTemp - 273.15) + "C => botTemp=" + + String.format("%.2f", botTemp - 273.15) + "C topTemp=" + + String.format("%.2f", topTemp - 273.15) + "C"); for (int j = 0; j < N; j++) { if (!Double.isNaN(fixedTemperature[j])) { - T[j] = fixedTemperature[j]; + T[j] = fixedTemperature[j]; } else if (!Double.isNaN(seedTemperature[j])) { - // User-supplied warm-start guess (not a constraint). - T[j] = seedTemperature[j]; + // User-supplied warm-start guess (not a constraint). + T[j] = seedTemperature[j]; } else { - double frac = (double) j / Math.max(N - 1, 1); - T[j] = botTemp + frac * (topTemp - botTemp); + double frac = (double) j / Math.max(N - 1, 1); + T[j] = botTemp + frac * (topTemp - botTemp); } } @@ -1192,12 +1221,12 @@ private void initializeTrayState() { for (int j = 0; j < N; j++) { double[] Kw = new double[C]; for (int i = 0; i < C; i++) { - ComponentInterface comp = referenceSystem.getPhase(0).getComponent(i); - double Tc = comp.getTC(); - double Pc = comp.getPC(); - double omega = comp.getAcentricFactor(); - double Pbar = P[j] / 1e5; - Kw[i] = (Pc / Pbar) * Math.exp(5.37 * (1.0 + omega) * (1.0 - Tc / T[j])); + ComponentInterface comp = referenceSystem.getPhase(0).getComponent(i); + double Tc = comp.getTC(); + double Pc = comp.getPC(); + double omega = comp.getAcentricFactor(); + double Pbar = P[j] / 1e5; + Kw[i] = (Pc / Pbar) * Math.exp(5.37 * (1.0 + omega) * (1.0 - Tc / T[j])); } // Rachford-Rice: solve for the vapor fraction psi such that @@ -1205,30 +1234,30 @@ private void initializeTrayState() { // For tray initialization, use feed composition double psi = feedBeta; for (int rrIter = 0; rrIter < 20; rrIter++) { - double f = 0, df = 0; - for (int i = 0; i < C; i++) { - double denom = 1.0 + psi * (Kw[i] - 1.0); - f += feedComp[i] * (Kw[i] - 1.0) / denom; - df -= feedComp[i] * (Kw[i] - 1.0) * (Kw[i] - 1.0) / (denom * denom); - } - if (Math.abs(df) < 1e-30 || Math.abs(f) < 1e-12) { - break; - } - psi -= f / df; - psi = Math.max(0.01, Math.min(0.99, psi)); + double f = 0, df = 0; + for (int i = 0; i < C; i++) { + double denom = 1.0 + psi * (Kw[i] - 1.0); + f += feedComp[i] * (Kw[i] - 1.0) / denom; + df -= feedComp[i] * (Kw[i] - 1.0) * (Kw[i] - 1.0) / (denom * denom); + } + if (Math.abs(df) < 1e-30 || Math.abs(f) < 1e-12) { + break; + } + psi -= f / df; + psi = Math.max(0.01, Math.min(0.99, psi)); } // Compute liquid composition from Rachford-Rice solution double sumX = 0; for (int i = 0; i < C; i++) { - double xi = feedComp[i] / (1.0 + psi * (Kw[i] - 1.0)); - xi = Math.max(xi, 1e-15); - liq[j][i] = xi * L[j]; - sumX += xi; + double xi = feedComp[i] / (1.0 + psi * (Kw[i] - 1.0)); + xi = Math.max(xi, 1e-15); + liq[j][i] = xi * L[j]; + sumX += xi; } // Normalize for (int i = 0; i < C; i++) { - liq[j][i] /= sumX; + liq[j][i] /= sumX; } } } @@ -1237,8 +1266,8 @@ private void initializeTrayState() { * Evaluate thermodynamic properties (K-values, enthalpies) at current state for all trays. * *

- * V[j] is NOT recomputed here — it is a free variable of the solver. The vapor component flows v_{i,j} = K_{i,j} * - * x_{i,j} * V_j are computed from the current V[j] and K-values. + * V[j] is NOT recomputed here — it is a free variable of the solver. The vapor component flows + * v_{i,j} = K_{i,j} * x_{i,j} * V_j are computed from the current V[j] and K-values. *

*/ private void evaluateThermo() { @@ -1251,21 +1280,23 @@ private void evaluateThermo() { * Seed a mass-balanced initial guess for direct Newton entry. * *

- * Used when {@code useOverallMBClosure} is active (T-spec'd reboiler with no boilup specification). Bypasses the - * BP-Wilson V-cascade — which is unstable for wide-boiling stripper topologies — and replaces it with a sweep that: + * Used when {@code useOverallMBClosure} is active (T-spec'd reboiler with no boilup + * specification). Bypasses the BP-Wilson V-cascade — which is unstable for wide-boiling stripper + * topologies — and replaces it with a sweep that: *

*
    - *
  1. Anchors V[N-1] (top vapor leaving column) to total feed minus a sensible bottoms estimate, so the overall MB - * closes.
  2. + *
  3. Anchors V[N-1] (top vapor leaving column) to total feed minus a sensible bottoms estimate, + * so the overall MB closes.
  4. *
  5. Distributes V[j] approximately linearly between feed trays.
  6. - *
  7. Computes L[j] from per-tray material balance: L[j] = L[j+1] + V[j-1] - V[j] + F_j (top down).
  8. - *
  9. Updates liq[j][i] from x[i] = z_i preserved-ratio normalization against L[j] (preserves the Rachford-Rice flash - * compositions set by initializeTrayState).
  10. + *
  11. Computes L[j] from per-tray material balance: L[j] = L[j+1] + V[j-1] - V[j] + F_j (top + * down).
  12. + *
  13. Updates liq[j][i] from x[i] = z_i preserved-ratio normalization against L[j] (preserves the + * Rachford-Rice flash compositions set by initializeTrayState).
  14. *
* *

- * Mass conservation is satisfied at the initial guess to within numerical tolerance; Newton then refines energy - * balance and equilibrium. + * Mass conservation is satisfied at the initial guess to within numerical tolerance; Newton then + * refines energy balance and equilibrium. *

*/ private void seedSolutionForDirectNewton() { @@ -1282,11 +1313,11 @@ private void seedSolutionForDirectNewton() { double fjv = 0; double fjl = 0; for (int i = 0; i < C; i++) { - double fij = feedLiq[j][i] + feedVap[j][i]; - fj += fij; - fjv += feedVap[j][i]; - fjl += feedLiq[j][i]; - Fi[i] += fij; + double fij = feedLiq[j][i] + feedVap[j][i]; + fj += fij; + fjv += feedVap[j][i]; + fjl += feedLiq[j][i]; + Fi[i] += fij; } feedTotalPerTray[j] = fj; feedVapPerTray[j] = fjv; @@ -1334,10 +1365,10 @@ private void seedSolutionForDirectNewton() { double S = Kmean[i] * Vavg / Math.max(Lavg, 1e-20); double frac; if (Math.abs(S - 1.0) < 1e-3) { - frac = (double) N / (N + 1.0); + frac = (double) N / (N + 1.0); } else { - double Sn = Math.pow(S, N + 1); - frac = (Sn - S) / (Sn - 1.0); + double Sn = Math.pow(S, N + 1); + frac = (Sn - S) / (Sn - 1.0); } frac = Math.max(1e-6, Math.min(1.0 - 1e-6, frac)); overhead_i[i] = frac * Fi[i]; @@ -1381,11 +1412,11 @@ private void seedSolutionForDirectNewton() { for (int j = 0; j < N; j++) { double Pj = P[j] / 1e5; for (int i = 0; i < C; i++) { - ComponentInterface comp = referenceSystem.getPhase(0).getComponent(i); - double Tc = comp.getTC(); - double Pc = comp.getPC(); - double om = comp.getAcentricFactor(); - Kseed[j][i] = (Pc / Pj) * Math.exp(5.37 * (1.0 + om) * (1.0 - Tc / T[j])); + ComponentInterface comp = referenceSystem.getPhase(0).getComponent(i); + double Tc = comp.getTC(); + double Pc = comp.getPC(); + double om = comp.getAcentricFactor(); + Kseed[j][i] = (Pc / Pj) * Math.exp(5.37 * (1.0 + om) * (1.0 - Tc / T[j])); } } @@ -1393,23 +1424,23 @@ private void seedSolutionForDirectNewton() { // y_i ∝ K_ij × x_i[j]; renormalize to sum=1; then V_i[j] = y_i × V[j]. double sumKx = 0; for (int i = 0; i < C; i++) { - sumKx += Kseed[j][i] * Liloc[j][i] / Math.max(L[j], 1e-20); + sumKx += Kseed[j][i] * Liloc[j][i] / Math.max(L[j], 1e-20); } double norm = Math.max(sumKx, 1e-20); for (int i = 0; i < C; i++) { - double yi = (Kseed[j][i] * Liloc[j][i] / Math.max(L[j], 1e-20)) / norm; - Viloc[j][i] = yi * V[j]; + double yi = (Kseed[j][i] * Liloc[j][i] / Math.max(L[j], 1e-20)) / norm; + Viloc[j][i] = yi * V[j]; } if (j < N - 1) { - // Component MB on tray j (steady state): V_i[j-1] + L_i[j+1] + F_i[j] = V_i[j] - // + L_i[j] - for (int i = 0; i < C; i++) { - double Fij = feedLiq[j][i] + feedVap[j][i]; - double Lnext = Viloc[j][i] + Liloc[j][i] - VimPrev[i] - Fij; - Liloc[j + 1][i] = Math.max(Lnext, 1e-15); - } - System.arraycopy(Viloc[j], 0, VimPrev, 0, C); + // Component MB on tray j (steady state): V_i[j-1] + L_i[j+1] + F_i[j] = V_i[j] + // + L_i[j] + for (int i = 0; i < C; i++) { + double Fij = feedLiq[j][i] + feedVap[j][i]; + double Lnext = Viloc[j][i] + Liloc[j][i] - VimPrev[i] - Fij; + Liloc[j + 1][i] = Math.max(Lnext, 1e-15); + } + System.arraycopy(Viloc[j], 0, VimPrev, 0, C); } } @@ -1441,112 +1472,112 @@ private void seedSolutionForDirectNewton() { for (int bpIter = 0; bpIter < 12; bpIter++) { double maxDT = 0.0; for (int j = 0; j < N; j++) { - if (!Double.isNaN(fixedTemperature[j])) { - continue; // pinned (e.g. reboiler) - } - double sumL = 0; - for (int i = 0; i < C; i++) { - sumL += Liloc[j][i]; - } - if (sumL < 1e-12) { - continue; - } - double Tj = T[j]; - double Pj = P[j] / 1e5; - for (int newt = 0; newt < 40; newt++) { - double f = -1.0; - double df = 0.0; - for (int i = 0; i < C; i++) { - ComponentInterface comp = referenceSystem.getPhase(0).getComponent(i); - double Tc = comp.getTC(); - double Pc = comp.getPC(); - double om = comp.getAcentricFactor(); - double Ki = (Pc / Pj) * Math.exp(5.37 * (1.0 + om) * (1.0 - Tc / Tj)); - double xi = Liloc[j][i] / sumL; - f += Ki * xi; - double dKdT = Ki * 5.37 * (1.0 + om) * Tc / (Tj * Tj); - df += dKdT * xi; - } - if (Math.abs(df) < 1e-30) { - break; - } - double dT = -f / df; - if (dT > 15.0) { - dT = 15.0; - } else if (dT < -15.0) { - dT = -15.0; - } - Tj += dT; - if (Tj < 150.0) { - Tj = 150.0; - } else if (Tj > 800.0) { - Tj = 800.0; - } - if (Math.abs(dT) < 1e-3) { - break; - } - } - double change = Math.abs(Tj - T[j]); - if (change > maxDT) { - maxDT = change; - } - T[j] = Tj; + if (!Double.isNaN(fixedTemperature[j])) { + continue; // pinned (e.g. reboiler) + } + double sumL = 0; + for (int i = 0; i < C; i++) { + sumL += Liloc[j][i]; + } + if (sumL < 1e-12) { + continue; + } + double Tj = T[j]; + double Pj = P[j] / 1e5; + for (int newt = 0; newt < 40; newt++) { + double f = -1.0; + double df = 0.0; + for (int i = 0; i < C; i++) { + ComponentInterface comp = referenceSystem.getPhase(0).getComponent(i); + double Tc = comp.getTC(); + double Pc = comp.getPC(); + double om = comp.getAcentricFactor(); + double Ki = (Pc / Pj) * Math.exp(5.37 * (1.0 + om) * (1.0 - Tc / Tj)); + double xi = Liloc[j][i] / sumL; + f += Ki * xi; + double dKdT = Ki * 5.37 * (1.0 + om) * Tc / (Tj * Tj); + df += dKdT * xi; + } + if (Math.abs(df) < 1e-30) { + break; + } + double dT = -f / df; + if (dT > 15.0) { + dT = 15.0; + } else if (dT < -15.0) { + dT = -15.0; + } + Tj += dT; + if (Tj < 150.0) { + Tj = 150.0; + } else if (Tj > 800.0) { + Tj = 800.0; + } + if (Math.abs(dT) < 1e-3) { + break; + } + } + double change = Math.abs(Tj - T[j]); + if (change > maxDT) { + maxDT = change; + } + T[j] = Tj; } // Refresh Wilson K at new T. for (int j = 0; j < N; j++) { - double Pj = P[j] / 1e5; - for (int i = 0; i < C; i++) { - ComponentInterface comp = referenceSystem.getPhase(0).getComponent(i); - double Tc = comp.getTC(); - double Pc = comp.getPC(); - double om = comp.getAcentricFactor(); - Kseed[j][i] = (Pc / Pj) * Math.exp(5.37 * (1.0 + om) * (1.0 - Tc / T[j])); - } + double Pj = P[j] / 1e5; + for (int i = 0; i < C; i++) { + ComponentInterface comp = referenceSystem.getPhase(0).getComponent(i); + double Tc = comp.getTC(); + double Pc = comp.getPC(); + double om = comp.getAcentricFactor(); + Kseed[j][i] = (Pc / Pj) * Math.exp(5.37 * (1.0 + om) * (1.0 - Tc / T[j])); + } } // Re-shoot per-component flows with updated K-values. for (int i = 0; i < C; i++) { - VimPrev[i] = 0.0; + VimPrev[i] = 0.0; } for (int j = 0; j < N; j++) { - double sumKxJ = 0; - for (int i = 0; i < C; i++) { - sumKxJ += Kseed[j][i] * Liloc[j][i] / Math.max(L[j], 1e-20); - } - double normJ = Math.max(sumKxJ, 1e-20); - for (int i = 0; i < C; i++) { - double yi = (Kseed[j][i] * Liloc[j][i] / Math.max(L[j], 1e-20)) / normJ; - Viloc[j][i] = yi * V[j]; - } - if (j < N - 1) { - for (int i = 0; i < C; i++) { - double Fij = feedLiq[j][i] + feedVap[j][i]; - double Lnext = Viloc[j][i] + Liloc[j][i] - VimPrev[i] - Fij; - Liloc[j + 1][i] = Math.max(Lnext, 1e-15); - } - System.arraycopy(Viloc[j], 0, VimPrev, 0, C); - } + double sumKxJ = 0; + for (int i = 0; i < C; i++) { + sumKxJ += Kseed[j][i] * Liloc[j][i] / Math.max(L[j], 1e-20); + } + double normJ = Math.max(sumKxJ, 1e-20); + for (int i = 0; i < C; i++) { + double yi = (Kseed[j][i] * Liloc[j][i] / Math.max(L[j], 1e-20)) / normJ; + Viloc[j][i] = yi * V[j]; + } + if (j < N - 1) { + for (int i = 0; i < C; i++) { + double Fij = feedLiq[j][i] + feedVap[j][i]; + double Lnext = Viloc[j][i] + Liloc[j][i] - VimPrev[i] - Fij; + Liloc[j + 1][i] = Math.max(Lnext, 1e-15); + } + System.arraycopy(Viloc[j], 0, VimPrev, 0, C); + } } // Re-enforce overhead boundary. for (int i = 0; i < C; i++) { - Viloc[N - 1][i] = overhead_i[i]; - double Fij = feedLiq[N - 1][i] + feedVap[N - 1][i]; - double VimN2 = (N >= 2) ? Viloc[N - 2][i] : 0.0; - double Lnew = VimN2 + Fij - Viloc[N - 1][i]; - Liloc[N - 1][i] = Math.max(Lnew, 1e-15); + Viloc[N - 1][i] = overhead_i[i]; + double Fij = feedLiq[N - 1][i] + feedVap[N - 1][i]; + double VimN2 = (N >= 2) ? Viloc[N - 2][i] : 0.0; + double Lnew = VimN2 + Fij - Viloc[N - 1][i]; + Liloc[N - 1][i] = Math.max(Lnew, 1e-15); } // Refresh L[j] totals from new component flows (V[j] is held; it carries // the boilup / overhead anchor). for (int j = 0; j < N; j++) { - double sL = 0; - for (int i = 0; i < C; i++) { - sL += Liloc[j][i]; - } - L[j] = Math.max(sL, 1e-20); + double sL = 0; + for (int i = 0; i < C; i++) { + sL += Liloc[j][i]; + } + L[j] = Math.max(sL, 1e-20); } if (maxDT < 0.05) { - break; + break; } } @@ -1561,14 +1592,14 @@ private void seedSolutionForDirectNewton() { // Copy into solver state and refresh total L[j], V[j] from the components. for (int j = 0; j < N; j++) { for (int i = 0; i < C; i++) { - liq[j][i] = Liloc[j][i]; - vap[j][i] = Viloc[j][i]; + liq[j][i] = Liloc[j][i]; + vap[j][i] = Viloc[j][i]; } double sL = 0; double sV = 0; for (int i = 0; i < C; i++) { - sL += liq[j][i]; - sV += vap[j][i]; + sL += liq[j][i]; + sV += vap[j][i]; } L[j] = Math.max(sL, 1e-20); V[j] = Math.max(sV, 1e-20); @@ -1591,25 +1622,26 @@ private void seedSolutionForDirectNewton() { double out = vap[N - 1][i] + liq[0][i]; double err = Math.abs(out - Fi[i]) / Math.max(Fi[i], 1e-20); if (err > maxCompMBerr) { - maxCompMBerr = err; + maxCompMBerr = err; } } logger.info( - "NS seed: totalFeed={} overheadTotal={} bottomsTotal={} boilup={} " - + "V[0]={} V[N-1]={} L[0]={} L[N-1]={} maxCompMBerr={}", - String.format("%.4f", totalFeed), String.format("%.4f", overheadTotal), String.format("%.4f", bottomsTotal), - String.format("%.4f", boilup), String.format("%.4f", V[0]), String.format("%.4f", V[N - 1]), - String.format("%.4f", L[0]), String.format("%.4f", L[N - 1]), String.format("%.4e", maxCompMBerr)); + "NS seed: totalFeed={} overheadTotal={} bottomsTotal={} boilup={} " + + "V[0]={} V[N-1]={} L[0]={} L[N-1]={} maxCompMBerr={}", + String.format("%.4f", totalFeed), String.format("%.4f", overheadTotal), + String.format("%.4f", bottomsTotal), String.format("%.4f", boilup), + String.format("%.4f", V[0]), String.format("%.4f", V[N - 1]), String.format("%.4f", L[0]), + String.format("%.4f", L[N - 1]), String.format("%.4e", maxCompMBerr)); } /** * Solve the column using the Bubble Point (BP) method (Wang-Henke, 1966). * *

- * Decomposes the MESH equations into sequential subproblems: 1. Material balance: tridiagonal Thomas algorithm for - * each component 2. Summation: bubble point calculation for temperature on each tray 3. Energy: energy balance for - * vapor flow on each tray + * Decomposes the MESH equations into sequential subproblems: 1. Material balance: tridiagonal + * Thomas algorithm for each component 2. Summation: bubble point calculation for temperature on + * each tray 3. Energy: energy balance for vapor flow on each tray *

* * @return true if converged @@ -1640,31 +1672,31 @@ private boolean solveBubblePointMethod() { for (int iter = 0; iter < 80; iter++) { // Compute Wilson K-values at current T,P for (int j = 0; j < N; j++) { - double Pbar = P[j] / 1e5; - for (int i = 0; i < C; i++) { - K[j][i] = (Pc[i] / Pbar) * Math.exp(5.37 * (1.0 + omega[i]) * (1.0 - Tc[i] / T[j])); - K[j][i] = Math.max(K[j][i], 1e-20); - } - applyMurphreeEfficiencyToK(j); - // Update vapor flows from K-values - for (int i = 0; i < C; i++) { - double xi = liq[j][i] / Math.max(L[j], 1e-20); - vap[j][i] = K[j][i] * xi * V[j]; - } + double Pbar = P[j] / 1e5; + for (int i = 0; i < C; i++) { + K[j][i] = (Pc[i] / Pbar) * Math.exp(5.37 * (1.0 + omega[i]) * (1.0 - Tc[i] / T[j])); + K[j][i] = Math.max(K[j][i], 1e-20); + } + applyMurphreeEfficiencyToK(j); + // Update vapor flows from K-values + for (int i = 0; i < C; i++) { + double xi = liq[j][i] / Math.max(L[j], 1e-20); + vap[j][i] = K[j][i] * xi * V[j]; + } } // Step A: Solve tridiagonal material balance for each component for (int comp = 0; comp < C; comp++) { - solveTridiagonalForComponent(comp); + solveTridiagonalForComponent(comp); } // Step B: Update L totals for (int j = 0; j < N; j++) { - double sumL = 0; - for (int i = 0; i < C; i++) { - sumL += liq[j][i]; - } - L[j] = Math.max(sumL, 1e-20); + double sumL = 0; + for (int i = 0; i < C; i++) { + sumL += liq[j][i]; + } + L[j] = Math.max(sumL, 1e-20); } // Step C: T correction using analytical Wilson dK/dT @@ -1672,61 +1704,62 @@ private boolean solveBubblePointMethod() { // dK/dT = K * 5.37*(1+w)*Tc/T^2 double maxDeltaT = 0; for (int j = 0; j < N; j++) { - if (!Double.isNaN(fixedTemperature[j])) { - continue; - } - double Pbar = P[j] / 1e5; - double sumKx = 0; - double dsumKxdT = 0; - for (int i = 0; i < C; i++) { - double xi = liq[j][i] / Math.max(L[j], 1e-20); - double Ki = (Pc[i] / Pbar) * Math.exp(5.37 * (1.0 + omega[i]) * (1.0 - Tc[i] / T[j])); - double dKidT = Ki * 5.37 * (1.0 + omega[i]) * Tc[i] / (T[j] * T[j]); - sumKx += Ki * xi; - dsumKxdT += dKidT * xi; - } - if (Math.abs(dsumKxdT) > 1e-15) { - double deltaT = -(sumKx - 1.0) / dsumKxdT; - deltaT = Math.max(-50.0, Math.min(50.0, deltaT)); - maxDeltaT = Math.max(maxDeltaT, Math.abs(dampT * deltaT)); - T[j] += dampT * deltaT; - T[j] = Math.max(T[j], 100.0); - T[j] = Math.min(T[j], 1000.0); - } + if (!Double.isNaN(fixedTemperature[j])) { + continue; + } + double Pbar = P[j] / 1e5; + double sumKx = 0; + double dsumKxdT = 0; + for (int i = 0; i < C; i++) { + double xi = liq[j][i] / Math.max(L[j], 1e-20); + double Ki = (Pc[i] / Pbar) * Math.exp(5.37 * (1.0 + omega[i]) * (1.0 - Tc[i] / T[j])); + double dKidT = Ki * 5.37 * (1.0 + omega[i]) * Tc[i] / (T[j] * T[j]); + sumKx += Ki * xi; + dsumKxdT += dKidT * xi; + } + if (Math.abs(dsumKxdT) > 1e-15) { + double deltaT = -(sumKx - 1.0) / dsumKxdT; + deltaT = Math.max(-50.0, Math.min(50.0, deltaT)); + maxDeltaT = Math.max(maxDeltaT, Math.abs(dampT * deltaT)); + T[j] += dampT * deltaT; + T[j] = Math.max(T[j], 100.0); + T[j] = Math.min(T[j], 1000.0); + } } // Step E: V from sum-rates (only after initial T convergence) double maxDeltaV = 0; if (iter > 10 || maxDeltaT < 5.0) { - for (int j = 0; j < N; j++) { - double Pbar = P[j] / 1e5; - double sumKx = 0; - for (int i = 0; i < C; i++) { - double xi = liq[j][i] / Math.max(L[j], 1e-20); - double Ki = (Pc[i] / Pbar) * Math.exp(5.37 * (1.0 + omega[i]) * (1.0 - Tc[i] / T[j])); - sumKx += Ki * xi; - } - double newV = V[j] * sumKx; - newV = Math.max(newV, 1e-10); - double deltaV = Math.abs(newV - V[j]); - maxDeltaV = Math.max(maxDeltaV, deltaV); - V[j] = newV; - } - // Closure at the reboiler: prefer overall MB when T is pinned. - if (hasReboiler && useOverallMBClosure) { - L[0] = Math.max(totalFeedMolesField - V[N - 1], totalFeedMolesField * 0.001); - } else if (hasReboiler && boilupRatio > 0) { - V[0] = boilupRatio * L[0]; - } + for (int j = 0; j < N; j++) { + double Pbar = P[j] / 1e5; + double sumKx = 0; + for (int i = 0; i < C; i++) { + double xi = liq[j][i] / Math.max(L[j], 1e-20); + double Ki = (Pc[i] / Pbar) * Math.exp(5.37 * (1.0 + omega[i]) * (1.0 - Tc[i] / T[j])); + sumKx += Ki * xi; + } + double newV = V[j] * sumKx; + newV = Math.max(newV, 1e-10); + double deltaV = Math.abs(newV - V[j]); + maxDeltaV = Math.max(maxDeltaV, deltaV); + V[j] = newV; + } + // Closure at the reboiler: prefer overall MB when T is pinned. + if (hasReboiler && useOverallMBClosure) { + L[0] = Math.max(totalFeedMolesField - V[N - 1], totalFeedMolesField * 0.001); + } else if (hasReboiler && boilupRatio > 0) { + V[0] = boilupRatio * L[0]; + } } if (iter % 10 == 0) { - logger.debug("BP-Wilson iter {}: maxDeltaT={} maxDeltaV={}", iter, String.format("%.4f", maxDeltaT), - String.format("%.4f", maxDeltaV)); + logger.debug("BP-Wilson iter {}: maxDeltaT={} maxDeltaV={}", iter, + String.format("%.4f", maxDeltaT), String.format("%.4f", maxDeltaV)); } if (maxDeltaT < 0.1 && (maxDeltaV < 0.1 || iter <= 10)) { - logger.info("BP-Wilson converged at iter {}: maxDeltaT={}", iter, String.format("%.4f", maxDeltaT)); - break; + logger.info("BP-Wilson converged at iter {}: maxDeltaT={}", iter, + String.format("%.4f", maxDeltaT)); + break; } } @@ -1735,7 +1768,7 @@ private boolean solveBubblePointMethod() { // Here we use the same BP tridiagonal structure but with rigorous EOS K-values, // blending gradually from Wilson to EOS to avoid divergence. logger.info("BP phase 2: EOS K-value successive substitution. Reboiler T={} Top T={}", - String.format("%.2f", T[0] - 273.15), String.format("%.2f", T[N - 1] - 273.15)); + String.format("%.2f", T[0] - 273.15), String.format("%.2f", T[N - 1] - 273.15)); // Save Wilson solution as fallback double[][] wilsonLiq = new double[N][C]; @@ -1752,8 +1785,8 @@ private boolean solveBubblePointMethod() { for (int j = 0; j < N; j++) { double Pbar = P[j] / 1e5; for (int i = 0; i < C; i++) { - Kwilson[j][i] = (Pc[i] / Pbar) * Math.exp(5.37 * (1.0 + omega[i]) * (1.0 - Tc[i] / T[j])); - Kwilson[j][i] = Math.max(Kwilson[j][i], 1e-20); + Kwilson[j][i] = (Pc[i] / Pbar) * Math.exp(5.37 * (1.0 + omega[i]) * (1.0 - Tc[i] / T[j])); + Kwilson[j][i] = Math.max(Kwilson[j][i], 1e-20); } } @@ -1770,105 +1803,105 @@ private boolean solveBubblePointMethod() { for (int eosIter = 0; eosIter < maxEosIter; eosIter++) { if (System.nanoTime() - eosStart > maxEosTimeNs) { - logger.info("BP-EOS: time limit reached at iter {}", eosIter); - break; + logger.info("BP-EOS: time limit reached at iter {}", eosIter); + break; } // Compute EOS K-values with TPflash on each tray for (int j = 0; j < N; j++) { - double sumLiq = 0; - for (int i = 0; i < C; i++) { - sumLiq += liq[j][i]; - } - L[j] = Math.max(sumLiq, 1e-20); - - double[] x = new double[C]; - for (int i = 0; i < C; i++) { - x[i] = liq[j][i] / L[j]; - } - - // EOS K-values from TPflash - SystemInterface traySystem = referenceSystem.clone(); - traySystem.setTemperature(T[j]); - traySystem.setPressure(P[j] / 1e5); - traySystem.setTotalNumberOfMoles(1.0); - traySystem.setMolarComposition(x); - traySystem.setNumberOfPhases(2); - traySystem.init(0); - - ThermodynamicOperations ops = new ThermodynamicOperations(traySystem); - boolean eosOk = false; - try { - ops.TPflash(); - traySystem.init(2); - if (traySystem.getNumberOfPhases() >= 2) { - eosOk = true; - // Identify phases by density (lighter = vapor, heavier = liquid) - int vapIdx = 0; - int liqIdx = 1; - if (traySystem.getPhase(0).getDensity() > traySystem.getPhase(1).getDensity()) { - vapIdx = 1; - liqIdx = 0; - } - for (int i = 0; i < C; i++) { - double fugL = traySystem.getPhase(liqIdx).getComponent(i).getFugacityCoefficient(); - double fugV = traySystem.getPhase(vapIdx).getComponent(i).getFugacityCoefficient(); - K[j][i] = fugL / Math.max(fugV, 1e-30); - // Clamp extreme K-values to prevent divergence - K[j][i] = Math.max(K[j][i], 1e-15); - K[j][i] = Math.min(K[j][i], 1e15); - } - } - } catch (Exception e) { - // keep eosOk = false - } - - if (!eosOk) { - // Fall back to Wilson - double Pbar = P[j] / 1e5; - for (int i = 0; i < C; i++) { - K[j][i] = (Pc[i] / Pbar) * Math.exp(5.37 * (1.0 + omega[i]) * (1.0 - Tc[i] / T[j])); - K[j][i] = Math.max(K[j][i], 1e-20); - } - } - - // Apply Murphree-efficiency correction (Edmister K^eta proxy) so the - // simultaneous solve reflects any per-stage efficiency overrides. - applyMurphreeEfficiencyToK(j); - - // Compute actual vapor composition y[j] = K*x / sum(K*x) for correct enthalpy - double sumKxJ = 0; - for (int i = 0; i < C; i++) { - sumKxJ += K[j][i] * x[i]; - } - double[] yJ = new double[C]; - for (int i = 0; i < C; i++) { - yJ[i] = (sumKxJ > 1e-20) ? K[j][i] * x[i] / sumKxJ : x[i]; - } - - // Enthalpies at actual stream compositions (single-phase EOS evaluation) - hL[j] = computeSinglePhaseEnthalpy(x, T[j], P[j] / 1e5, false); - hV[j] = computeSinglePhaseEnthalpy(yJ, T[j], P[j] / 1e5, true); - - // Update vapor from K - for (int i = 0; i < C; i++) { - double xi = liq[j][i] / Math.max(L[j], 1e-20); - vap[j][i] = K[j][i] * xi * V[j]; - } + double sumLiq = 0; + for (int i = 0; i < C; i++) { + sumLiq += liq[j][i]; + } + L[j] = Math.max(sumLiq, 1e-20); + + double[] x = new double[C]; + for (int i = 0; i < C; i++) { + x[i] = liq[j][i] / L[j]; + } + + // EOS K-values from TPflash + SystemInterface traySystem = referenceSystem.clone(); + traySystem.setTemperature(T[j]); + traySystem.setPressure(P[j] / 1e5); + traySystem.setTotalNumberOfMoles(1.0); + traySystem.setMolarComposition(x); + traySystem.setNumberOfPhases(2); + traySystem.init(0); + + ThermodynamicOperations ops = new ThermodynamicOperations(traySystem); + boolean eosOk = false; + try { + ops.TPflash(); + traySystem.init(2); + if (traySystem.getNumberOfPhases() >= 2) { + eosOk = true; + // Identify phases by density (lighter = vapor, heavier = liquid) + int vapIdx = 0; + int liqIdx = 1; + if (traySystem.getPhase(0).getDensity() > traySystem.getPhase(1).getDensity()) { + vapIdx = 1; + liqIdx = 0; + } + for (int i = 0; i < C; i++) { + double fugL = traySystem.getPhase(liqIdx).getComponent(i).getFugacityCoefficient(); + double fugV = traySystem.getPhase(vapIdx).getComponent(i).getFugacityCoefficient(); + K[j][i] = fugL / Math.max(fugV, 1e-30); + // Clamp extreme K-values to prevent divergence + K[j][i] = Math.max(K[j][i], 1e-15); + K[j][i] = Math.min(K[j][i], 1e15); + } + } + } catch (Exception e) { + // keep eosOk = false + } + + if (!eosOk) { + // Fall back to Wilson + double Pbar = P[j] / 1e5; + for (int i = 0; i < C; i++) { + K[j][i] = (Pc[i] / Pbar) * Math.exp(5.37 * (1.0 + omega[i]) * (1.0 - Tc[i] / T[j])); + K[j][i] = Math.max(K[j][i], 1e-20); + } + } + + // Apply Murphree-efficiency correction (Edmister K^eta proxy) so the + // simultaneous solve reflects any per-stage efficiency overrides. + applyMurphreeEfficiencyToK(j); + + // Compute actual vapor composition y[j] = K*x / sum(K*x) for correct enthalpy + double sumKxJ = 0; + for (int i = 0; i < C; i++) { + sumKxJ += K[j][i] * x[i]; + } + double[] yJ = new double[C]; + for (int i = 0; i < C; i++) { + yJ[i] = (sumKxJ > 1e-20) ? K[j][i] * x[i] / sumKxJ : x[i]; + } + + // Enthalpies at actual stream compositions (single-phase EOS evaluation) + hL[j] = computeSinglePhaseEnthalpy(x, T[j], P[j] / 1e5, false); + hV[j] = computeSinglePhaseEnthalpy(yJ, T[j], P[j] / 1e5, true); + + // Update vapor from K + for (int i = 0; i < C; i++) { + double xi = liq[j][i] / Math.max(L[j], 1e-20); + vap[j][i] = K[j][i] * xi * V[j]; + } } // Step A: Solve tridiagonal material balance for each component for (int comp = 0; comp < C; comp++) { - solveTridiagonalForComponent(comp); + solveTridiagonalForComponent(comp); } // Step B: Update L totals for (int j = 0; j < N; j++) { - double sumL = 0; - for (int i = 0; i < C; i++) { - sumL += liq[j][i]; - } - L[j] = Math.max(sumL, 1e-20); + double sumL = 0; + for (int i = 0; i < C; i++) { + sumL += liq[j][i]; + } + L[j] = Math.max(sumL, 1e-20); } // Step C: T correction using numerical EOS dK/dT @@ -1876,86 +1909,86 @@ private boolean solveBubblePointMethod() { double maxDeltaT = 0; double dTpert = 0.1; // K for (int j = 0; j < N; j++) { - if (!Double.isNaN(fixedTemperature[j])) { - continue; - } - - // sumKx at current K (already computed above) - double sumKx = 0; - for (int i = 0; i < C; i++) { - double xi = liq[j][i] / Math.max(L[j], 1e-20); - sumKx += K[j][i] * xi; - } - - // Perturbed EOS K at T+dT - double[] x = new double[C]; - for (int i = 0; i < C; i++) { - x[i] = liq[j][i] / Math.max(L[j], 1e-20); - } - SystemInterface pertSystem = referenceSystem.clone(); - pertSystem.setTemperature(T[j] + dTpert); - pertSystem.setPressure(P[j] / 1e5); - pertSystem.setTotalNumberOfMoles(1.0); - pertSystem.setMolarComposition(x); - pertSystem.setNumberOfPhases(2); - pertSystem.init(0); - - ThermodynamicOperations pertOps = new ThermodynamicOperations(pertSystem); - double sumKxPert = 0; - boolean pertOk = false; - try { - pertOps.TPflash(); - pertSystem.init(2); - if (pertSystem.getNumberOfPhases() >= 2) { - pertOk = true; - // Identify phases by density - int vapIdxP = 0; - int liqIdxP = 1; - if (pertSystem.getPhase(0).getDensity() > pertSystem.getPhase(1).getDensity()) { - vapIdxP = 1; - liqIdxP = 0; - } - for (int i = 0; i < C; i++) { - double fugL = pertSystem.getPhase(liqIdxP).getComponent(i).getFugacityCoefficient(); - double fugV = pertSystem.getPhase(vapIdxP).getComponent(i).getFugacityCoefficient(); - double Kpert = fugL / Math.max(fugV, 1e-30); - Kpert = Math.max(Kpert, 1e-15); - Kpert = Math.min(Kpert, 1e15); - sumKxPert += Kpert * x[i]; - } - } - } catch (Exception e) { - // pertOk stays false - } - - if (pertOk) { - double dsumKxdT = (sumKxPert - sumKx) / dTpert; - if (Math.abs(dsumKxdT) > 1e-15) { - double deltaT = -(sumKx - 1.0) / dsumKxdT; - double eosDampT = 0.5; - deltaT = Math.max(-30.0, Math.min(30.0, deltaT)); - maxDeltaT = Math.max(maxDeltaT, Math.abs(eosDampT * deltaT)); - T[j] += eosDampT * deltaT; - T[j] = Math.max(T[j], 100.0); - T[j] = Math.min(T[j], 1000.0); - } - } else { - // Fall back to Wilson dK/dT - double Pbar = P[j] / 1e5; - double dsumKxdT = 0; - for (int i = 0; i < C; i++) { - double dKidT = K[j][i] * 5.37 * (1.0 + omega[i]) * Tc[i] / (T[j] * T[j]); - dsumKxdT += dKidT * x[i]; - } - if (Math.abs(dsumKxdT) > 1e-15) { - double deltaT = -(sumKx - 1.0) / dsumKxdT; - deltaT = Math.max(-20.0, Math.min(20.0, deltaT)); - maxDeltaT = Math.max(maxDeltaT, Math.abs(0.3 * deltaT)); - T[j] += 0.3 * deltaT; - T[j] = Math.max(T[j], 100.0); - T[j] = Math.min(T[j], 1000.0); - } - } + if (!Double.isNaN(fixedTemperature[j])) { + continue; + } + + // sumKx at current K (already computed above) + double sumKx = 0; + for (int i = 0; i < C; i++) { + double xi = liq[j][i] / Math.max(L[j], 1e-20); + sumKx += K[j][i] * xi; + } + + // Perturbed EOS K at T+dT + double[] x = new double[C]; + for (int i = 0; i < C; i++) { + x[i] = liq[j][i] / Math.max(L[j], 1e-20); + } + SystemInterface pertSystem = referenceSystem.clone(); + pertSystem.setTemperature(T[j] + dTpert); + pertSystem.setPressure(P[j] / 1e5); + pertSystem.setTotalNumberOfMoles(1.0); + pertSystem.setMolarComposition(x); + pertSystem.setNumberOfPhases(2); + pertSystem.init(0); + + ThermodynamicOperations pertOps = new ThermodynamicOperations(pertSystem); + double sumKxPert = 0; + boolean pertOk = false; + try { + pertOps.TPflash(); + pertSystem.init(2); + if (pertSystem.getNumberOfPhases() >= 2) { + pertOk = true; + // Identify phases by density + int vapIdxP = 0; + int liqIdxP = 1; + if (pertSystem.getPhase(0).getDensity() > pertSystem.getPhase(1).getDensity()) { + vapIdxP = 1; + liqIdxP = 0; + } + for (int i = 0; i < C; i++) { + double fugL = pertSystem.getPhase(liqIdxP).getComponent(i).getFugacityCoefficient(); + double fugV = pertSystem.getPhase(vapIdxP).getComponent(i).getFugacityCoefficient(); + double Kpert = fugL / Math.max(fugV, 1e-30); + Kpert = Math.max(Kpert, 1e-15); + Kpert = Math.min(Kpert, 1e15); + sumKxPert += Kpert * x[i]; + } + } + } catch (Exception e) { + // pertOk stays false + } + + if (pertOk) { + double dsumKxdT = (sumKxPert - sumKx) / dTpert; + if (Math.abs(dsumKxdT) > 1e-15) { + double deltaT = -(sumKx - 1.0) / dsumKxdT; + double eosDampT = 0.5; + deltaT = Math.max(-30.0, Math.min(30.0, deltaT)); + maxDeltaT = Math.max(maxDeltaT, Math.abs(eosDampT * deltaT)); + T[j] += eosDampT * deltaT; + T[j] = Math.max(T[j], 100.0); + T[j] = Math.min(T[j], 1000.0); + } + } else { + // Fall back to Wilson dK/dT + double Pbar = P[j] / 1e5; + double dsumKxdT = 0; + for (int i = 0; i < C; i++) { + double dKidT = K[j][i] * 5.37 * (1.0 + omega[i]) * Tc[i] / (T[j] * T[j]); + dsumKxdT += dKidT * x[i]; + } + if (Math.abs(dsumKxdT) > 1e-15) { + double deltaT = -(sumKx - 1.0) / dsumKxdT; + deltaT = Math.max(-20.0, Math.min(20.0, deltaT)); + maxDeltaT = Math.max(maxDeltaT, Math.abs(0.3 * deltaT)); + T[j] += 0.3 * deltaT; + T[j] = Math.max(T[j], 100.0); + T[j] = Math.min(T[j], 1000.0); + } + } } // Step E: V from energy balance (Wang-Henke method) @@ -1967,113 +2000,116 @@ private boolean solveBubblePointMethod() { double maxDeltaV = 0; double dampV = 0.5; for (int j = 0; j < N; j++) { - if (j == 0 && hasReboiler && useOverallMBClosure) { - // Reboiler: with T-spec and no explicit boilup, close via overall MB. - L[0] = Math.max(totalFeedMolesField - V[N - 1], totalFeedMolesField * 0.001); - continue; - } - if (j == 0 && hasReboiler && boilupRatio > 0) { - // Reboiler: V fixed by boilup specification - V[0] = boilupRatio * L[0]; - continue; - } - - double denominator = hV[j] - hL[j]; - if (Math.abs(denominator) < 1e-3) { - // hV ≈ hL means no phase split enthalpy difference — fall back to sum-rates - double sumKx = 0; - for (int i = 0; i < C; i++) { - double xi = liq[j][i] / Math.max(L[j], 1e-20); - sumKx += K[j][i] * xi; - } - double newV = V[j] * sumKx; - newV = Math.max(newV, 1e-10); - double deltaV = Math.abs(newV - V[j]); - maxDeltaV = Math.max(maxDeltaV, deltaV); - V[j] = V[j] + dampV * (newV - V[j]); - continue; - } - - // H_in from tray below (vapor) and tray above (liquid) - double numerator = 0; - if (j < N - 1) { - // Liquid from tray above: L_{j+1} * (hL_{j+1} - hL_j) - numerator += L[j + 1] * (hL[j + 1] - hL[j]); - } - if (j > 0) { - // Vapor from tray below enters with hV_{j-1}, already mixed - // The energy balance formulation for BP method: - // V_j * hV_j + L_j * hL_j = V_{j-1} * hV_{j-1} + L_{j+1} * hL_{j+1} + F - // Rearranging: V_j = [V_{j-1}*hV_{j-1} + L_{j+1}*hL_{j+1} + F - L_j*hL_j] / - // hV_j - // But L_j = V_{j-1} + L_{j+1} + F_j - V_j from total mass balance - // This creates a coupling. Standard BP uses a simpler form. - } - - // Feed enthalpy: F_liq * (hF_liq - hL_j) + F_vap * (hF_vap - hL_j) - numerator += feedLTotal[j] * (feedHL[j] - hL[j]); - numerator += feedVTotal[j] * (feedHV[j] - hL[j]); - - // Heat duty on tray (Q_j) - numerator += Q[j]; - - // Also add V_{j-1} * (hV_{j-1} - hL_j) to numerator - // This is the standard formulation: Energy balance gives - // V_j = [L_{j+1}*(hL_{j+1}-hL_j) + V_{j-1}*(hV_{j-1}-hL_j) + F*hF - F*hL_j + Q] - // / (hV_j - hL_j) - if (j > 0) { - numerator += V[j - 1] * (hV[j - 1] - hL[j]); - } - - double newV = numerator / denominator; - newV = Math.max(newV, 1e-10); - - double deltaV = Math.abs(newV - V[j]); - maxDeltaV = Math.max(maxDeltaV, deltaV); - V[j] = V[j] + dampV * (newV - V[j]); + if (j == 0 && hasReboiler && useOverallMBClosure) { + // Reboiler: with T-spec and no explicit boilup, close via overall MB. + L[0] = Math.max(totalFeedMolesField - V[N - 1], totalFeedMolesField * 0.001); + continue; + } + if (j == 0 && hasReboiler && boilupRatio > 0) { + // Reboiler: V fixed by boilup specification + V[0] = boilupRatio * L[0]; + continue; + } + + double denominator = hV[j] - hL[j]; + if (Math.abs(denominator) < 1e-3) { + // hV ≈ hL means no phase split enthalpy difference — fall back to sum-rates + double sumKx = 0; + for (int i = 0; i < C; i++) { + double xi = liq[j][i] / Math.max(L[j], 1e-20); + sumKx += K[j][i] * xi; + } + double newV = V[j] * sumKx; + newV = Math.max(newV, 1e-10); + double deltaV = Math.abs(newV - V[j]); + maxDeltaV = Math.max(maxDeltaV, deltaV); + V[j] = V[j] + dampV * (newV - V[j]); + continue; + } + + // H_in from tray below (vapor) and tray above (liquid) + double numerator = 0; + if (j < N - 1) { + // Liquid from tray above: L_{j+1} * (hL_{j+1} - hL_j) + numerator += L[j + 1] * (hL[j + 1] - hL[j]); + } + if (j > 0) { + // Vapor from tray below enters with hV_{j-1}, already mixed + // The energy balance formulation for BP method: + // V_j * hV_j + L_j * hL_j = V_{j-1} * hV_{j-1} + L_{j+1} * hL_{j+1} + F + // Rearranging: V_j = [V_{j-1}*hV_{j-1} + L_{j+1}*hL_{j+1} + F - L_j*hL_j] / + // hV_j + // But L_j = V_{j-1} + L_{j+1} + F_j - V_j from total mass balance + // This creates a coupling. Standard BP uses a simpler form. + } + + // Feed enthalpy: F_liq * (hF_liq - hL_j) + F_vap * (hF_vap - hL_j) + numerator += feedLTotal[j] * (feedHL[j] - hL[j]); + numerator += feedVTotal[j] * (feedHV[j] - hL[j]); + + // Heat duty on tray (Q_j) + numerator += Q[j]; + + // Also add V_{j-1} * (hV_{j-1} - hL_j) to numerator + // This is the standard formulation: Energy balance gives + // V_j = [L_{j+1}*(hL_{j+1}-hL_j) + V_{j-1}*(hV_{j-1}-hL_j) + F*hF - F*hL_j + Q] + // / (hV_j - hL_j) + if (j > 0) { + numerator += V[j - 1] * (hV[j - 1] - hL[j]); + } + + double newV = numerator / denominator; + newV = Math.max(newV, 1e-10); + + double deltaV = Math.abs(newV - V[j]); + maxDeltaV = Math.max(maxDeltaV, deltaV); + V[j] = V[j] + dampV * (newV - V[j]); } // Check mass balance and equilibrium error (sumKx deviation from 1.0) double mbErr = computeMassBalanceError(); double maxSumKxErr = 0; for (int j = 0; j < N; j++) { - if (!Double.isNaN(fixedTemperature[j])) { - continue; - } - double sumKx = 0; - for (int i = 0; i < C; i++) { - double xi = liq[j][i] / Math.max(L[j], 1e-20); - sumKx += K[j][i] * xi; - } - maxSumKxErr = Math.max(maxSumKxErr, Math.abs(sumKx - 1.0)); + if (!Double.isNaN(fixedTemperature[j])) { + continue; + } + double sumKx = 0; + for (int i = 0; i < C; i++) { + double xi = liq[j][i] / Math.max(L[j], 1e-20); + sumKx += K[j][i] * xi; + } + maxSumKxErr = Math.max(maxSumKxErr, Math.abs(sumKx - 1.0)); } if (eosIter % 5 == 0 || eosIter < 5) { - double eErr = computeMaxRelativeEnergyError(); - logger.info("BP-EOS iter {}: dT={} dV={} mbErr={}% energy={}% sumKxErr={} T[0]={} T[N-1]={}", eosIter, - String.format("%.2f", maxDeltaT), String.format("%.2f", maxDeltaV), String.format("%.4f", mbErr * 100), - String.format("%.2f", eErr * 100), String.format("%.6f", maxSumKxErr), String.format("%.1f", T[0] - 273.15), - String.format("%.1f", T[N - 1] - 273.15)); + double eErr = computeMaxRelativeEnergyError(); + logger.info( + "BP-EOS iter {}: dT={} dV={} mbErr={}% energy={}% sumKxErr={} T[0]={} T[N-1]={}", + eosIter, String.format("%.2f", maxDeltaT), String.format("%.2f", maxDeltaV), + String.format("%.4f", mbErr * 100), String.format("%.2f", eErr * 100), + String.format("%.6f", maxSumKxErr), String.format("%.1f", T[0] - 273.15), + String.format("%.1f", T[N - 1] - 273.15)); } // Track best solution if (mbErr < bestEosMassBalErr) { - bestEosMassBalErr = mbErr; - saveTrayState(bestEosLiq, bestEosT, bestEosV); + bestEosMassBalErr = mbErr; + saveTrayState(bestEosLiq, bestEosT, bestEosV); } // Check convergence — require both mass balance AND equilibrium satisfied if (maxDeltaT < 0.1 && maxDeltaV < 0.5 && mbErr < 0.005 && maxSumKxErr < 0.01) { - logger.info("BP-EOS converged at iter {}: massBalErr={}% sumKxErr={}", eosIter, - String.format("%.4f", mbErr * 100), String.format("%.6f", maxSumKxErr)); - break; + logger.info("BP-EOS converged at iter {}: massBalErr={}% sumKxErr={}", eosIter, + String.format("%.4f", mbErr * 100), String.format("%.6f", maxSumKxErr)); + break; } // Divergence detection: if mass balance gets much worse than Wilson, revert if (mbErr > 0.5) { - logger.warn("BP-EOS diverging (massBalErr={}%), reverting to best", String.format("%.2f", mbErr * 100)); - restoreTrayState(bestEosLiq, bestEosT, bestEosV); - break; + logger.warn("BP-EOS diverging (massBalErr={}%), reverting to best", + String.format("%.2f", mbErr * 100)); + restoreTrayState(bestEosLiq, bestEosT, bestEosV); + break; } } @@ -2083,7 +2119,8 @@ private boolean solveBubblePointMethod() { // Final EOS evaluation evaluateThermo(); - logger.info("BP-EOS complete: bestMassBalErr={}%", String.format("%.4f", bestEosMassBalErr * 100)); + logger.info("BP-EOS complete: bestMassBalErr={}%", + String.format("%.4f", bestEosMassBalErr * 100)); // Phase 3: Energy balance temperature correction (Option B) // DISABLED: Sequential BP→energy correction is unconditionally unstable for @@ -2101,16 +2138,17 @@ private boolean solveBubblePointMethod() { * Phase 3: PH-flash temperature correction for energy balance. * *

- * After the BP method converges on material balance/equilibrium, tray temperatures may be off because the BP method - * only solves MESH equations without the E (energy balance). This method computes the enthalpy of all streams - * entering each tray, mixes them, then PH-flashes to find the energy-consistent temperature. The resulting K-values - * and flows are fed back to the tridiagonal solver, and the process iterates until the temperature profile - * stabilizes. + * After the BP method converges on material balance/equilibrium, tray temperatures may be off + * because the BP method only solves MESH equations without the E (energy balance). This method + * computes the enthalpy of all streams entering each tray, mixes them, then PH-flashes to find + * the energy-consistent temperature. The resulting K-values and flows are fed back to the + * tridiagonal solver, and the process iterates until the temperature profile stabilizes. *

*/ private void phaseThreePHflashCorrection() { - System.out.println("Phase 3: Newton-E T correction. T[0]=" + String.format("%.2f", T[0] - 273.15) + " T[N-1]=" - + String.format("%.2f", T[N - 1] - 273.15)); + System.out + .println("Phase 3: Newton-E T correction. T[0]=" + String.format("%.2f", T[0] - 273.15) + + " T[N-1]=" + String.format("%.2f", T[N - 1] - 273.15)); int maxIter = 40; double dampE = 0.3; @@ -2138,90 +2176,92 @@ private void phaseThreePHflashCorrection() { double maxEErr = 0; for (int j = 0; j < N; j++) { - maxEErr = Math.max(maxEErr, Math.abs(energyErr[j])); - if (!Double.isNaN(fixedTemperature[j])) { - continue; - } - - // Numerical dE/dT: perturb T[j], recompute, get derivative - double t0 = T[j]; - T[j] = t0 + pertDT; - evaluateThermoForTray(j); - double[] energyErrP = computeEnergyErrors(); - double dEdT = (energyErrP[j] - energyErr[j]) / pertDT; - T[j] = t0; // restore - evaluateThermoForTray(j); - - if (Math.abs(dEdT) < 1e-10) { - continue; - } - - double deltaT = -energyErr[j] / dEdT; - deltaT = Math.max(-maxClamp, Math.min(maxClamp, deltaT)); - deltaT *= dampE; - maxDeltaT = Math.max(maxDeltaT, Math.abs(deltaT)); - - if (iter == 0) { - System.out.println(" Tray " + j + ": T=" + String.format("%.1f", T[j] - 273.15) + " E=" - + String.format("%.0f", energyErr[j]) + " dE/dT=" + String.format("%.0f", dEdT) + " dT=" - + String.format("%.2f", deltaT)); - } - - T[j] += deltaT; - T[j] = Math.max(T[j], 200.0); - T[j] = Math.min(T[j], 800.0); + maxEErr = Math.max(maxEErr, Math.abs(energyErr[j])); + if (!Double.isNaN(fixedTemperature[j])) { + continue; + } + + // Numerical dE/dT: perturb T[j], recompute, get derivative + double t0 = T[j]; + T[j] = t0 + pertDT; + evaluateThermoForTray(j); + double[] energyErrP = computeEnergyErrors(); + double dEdT = (energyErrP[j] - energyErr[j]) / pertDT; + T[j] = t0; // restore + evaluateThermoForTray(j); + + if (Math.abs(dEdT) < 1e-10) { + continue; + } + + double deltaT = -energyErr[j] / dEdT; + deltaT = Math.max(-maxClamp, Math.min(maxClamp, deltaT)); + deltaT *= dampE; + maxDeltaT = Math.max(maxDeltaT, Math.abs(deltaT)); + + if (iter == 0) { + System.out.println(" Tray " + j + ": T=" + String.format("%.1f", T[j] - 273.15) + " E=" + + String.format("%.0f", energyErr[j]) + " dE/dT=" + String.format("%.0f", dEdT) + + " dT=" + String.format("%.2f", deltaT)); + } + + T[j] += deltaT; + T[j] = Math.max(T[j], 200.0); + T[j] = Math.min(T[j], 800.0); } // Inner BP loop: reconverge material balance at new temperatures evaluateThermo(); for (int inner = 0; inner < innerBP; inner++) { - for (int comp = 0; comp < C; comp++) { - solveTridiagonalForComponent(comp); - } - for (int j = 0; j < N; j++) { - double sumL = 0; - for (int i = 0; i < C; i++) { - sumL += liq[j][i]; - } - L[j] = Math.max(sumL, 1e-20); - } - for (int j = 0; j < N; j++) { - double sumKx = 0; - for (int i = 0; i < C; i++) { - double xi = liq[j][i] / Math.max(L[j], 1e-20); - sumKx += K[j][i] * xi; - } - V[j] = Math.max(V[j] * sumKx, 1e-10); - } - if (hasReboiler && useOverallMBClosure) { - L[0] = Math.max(totalFeedMolesField - V[N - 1], totalFeedMolesField * 0.001); - } else if (hasReboiler && boilupRatio > 0 && Double.isNaN(fixedTemperature[0])) { - V[0] = boilupRatio * L[0]; - } - evaluateThermo(); + for (int comp = 0; comp < C; comp++) { + solveTridiagonalForComponent(comp); + } + for (int j = 0; j < N; j++) { + double sumL = 0; + for (int i = 0; i < C; i++) { + sumL += liq[j][i]; + } + L[j] = Math.max(sumL, 1e-20); + } + for (int j = 0; j < N; j++) { + double sumKx = 0; + for (int i = 0; i < C; i++) { + double xi = liq[j][i] / Math.max(L[j], 1e-20); + sumKx += K[j][i] * xi; + } + V[j] = Math.max(V[j] * sumKx, 1e-10); + } + if (hasReboiler && useOverallMBClosure) { + L[0] = Math.max(totalFeedMolesField - V[N - 1], totalFeedMolesField * 0.001); + } else if (hasReboiler && boilupRatio > 0 && Double.isNaN(fixedTemperature[0])) { + V[0] = boilupRatio * L[0]; + } + evaluateThermo(); } double mbErr = computeMassBalanceError(); System.out.println("E-iter " + iter + ": maxDT=" + String.format("%.3f", maxDeltaT) + " maxE=" - + String.format("%.0f", maxEErr) + " mb=" + String.format("%.4f%%", mbErr * 100) + " T[0]=" - + String.format("%.1f", T[0] - 273.15) + " T[N-1]=" + String.format("%.1f", T[N - 1] - 273.15)); + + String.format("%.0f", maxEErr) + " mb=" + String.format("%.4f%%", mbErr * 100) + + " T[0]=" + String.format("%.1f", T[0] - 273.15) + " T[N-1]=" + + String.format("%.1f", T[N - 1] - 273.15)); if (mbErr < bestMbErr || (mbErr < 0.005 && maxDeltaT < 1.0)) { - bestMbErr = mbErr; - saveTrayState(bestLiq, bestT, bestV); + bestMbErr = mbErr; + saveTrayState(bestLiq, bestT, bestV); } if (maxDeltaT < 0.1 && mbErr < 0.005) { - System.out.println("Newton-E CONVERGED at iter " + iter); - break; + System.out.println("Newton-E CONVERGED at iter " + iter); + break; } if (mbErr > 1.0) { - System.out.println("Newton-E diverging (mb=" + String.format("%.1f%%", mbErr * 100) + "), reducing damping"); - restoreTrayState(bestLiq, bestT, bestV); - evaluateThermo(); - dampE *= 0.5; - maxClamp = Math.max(maxClamp * 0.5, 2.0); + System.out.println("Newton-E diverging (mb=" + String.format("%.1f%%", mbErr * 100) + + "), reducing damping"); + restoreTrayState(bestLiq, bestT, bestV); + evaluateThermo(); + dampE *= 0.5; + maxClamp = Math.max(maxClamp * 0.5, 2.0); } } @@ -2229,16 +2269,17 @@ private void phaseThreePHflashCorrection() { evaluateThermo(); System.out.println("Phase 3 done: T[0]=" + String.format("%.2f", T[0] - 273.15) + " T[N-1]=" - + String.format("%.2f", T[N - 1] - 273.15) + " mb=" + String.format("%.4f%%", bestMbErr * 100)); + + String.format("%.2f", T[N - 1] - 273.15) + " mb=" + + String.format("%.4f%%", bestMbErr * 100)); } /** * Sum-Rates (SR) energy balance correction phase. * *

- * After the BP method converges on material balance, tray temperatures may be off because BP determines T from - * bubble-point (sum Kx = 1), which is inappropriate for wide-boiling / absorber-type columns. The SR method instead - * determines T from the energy balance: + * After the BP method converges on material balance, tray temperatures may be off because BP + * determines T from bubble-point (sum Kx = 1), which is inappropriate for wide-boiling / + * absorber-type columns. The SR method instead determines T from the energy balance: *

*
    *
  1. Compute EOS K-values and enthalpies at current T, x
  2. @@ -2250,28 +2291,30 @@ private void phaseThreePHflashCorrection() { *
* *

- * Reference: Seader, Henley & Roper, "Separation Process Principles", Chapter 10.4 — Sum-Rates method for - * absorber/stripper columns. + * Reference: Seader, Henley & Roper, "Separation Process Principles", Chapter 10.4 — + * Sum-Rates method for absorber/stripper columns. *

*/ private void solveSumRatesPhase() { - System.out.println("[SR] Starting Sum-Rates energy correction. T[0]=" + String.format("%.1f", T[0] - 273.15) - + "C T[N-1]=" + String.format("%.1f", T[N - 1] - 273.15) + "C"); + System.out.println( + "[SR] Starting Sum-Rates energy correction. T[0]=" + String.format("%.1f", T[0] - 273.15) + + "C T[N-1]=" + String.format("%.1f", T[N - 1] - 273.15) + "C"); // Print feed enthalpy diagnostics for (int j = 0; j < N; j++) { double feedMolesJ = 0; for (int i = 0; i < C; i++) { - feedMolesJ += feedLiq[j][i] + feedVap[j][i]; + feedMolesJ += feedLiq[j][i] + feedVap[j][i]; } if (feedMolesJ > 1e-10) { - System.out.println("[SR-FEED] tray " + j + ": feedLTotal=" + String.format("%.1f", feedLTotal[j]) + " feedHL=" - + String.format("%.1f", feedHL[j]) + " feedVTotal=" + String.format("%.1f", feedVTotal[j]) + " feedHV=" - + String.format("%.1f", feedHV[j]) + " feedEnthalpy=" - + String.format("%.0f", feedLTotal[j] * feedHL[j] + feedVTotal[j] * feedHV[j])); + System.out.println("[SR-FEED] tray " + j + ": feedLTotal=" + + String.format("%.1f", feedLTotal[j]) + " feedHL=" + String.format("%.1f", feedHL[j]) + + " feedVTotal=" + String.format("%.1f", feedVTotal[j]) + " feedHV=" + + String.format("%.1f", feedHV[j]) + " feedEnthalpy=" + + String.format("%.0f", feedLTotal[j] * feedHL[j] + feedVTotal[j] * feedHV[j])); } } - logger.info("SR: starting Sum-Rates energy correction. T[0]={} T[N-1]={}", String.format("%.1f", T[0] - 273.15), - String.format("%.1f", T[N - 1] - 273.15)); + logger.info("SR: starting Sum-Rates energy correction. T[0]={} T[N-1]={}", + String.format("%.1f", T[0] - 273.15), String.format("%.1f", T[N - 1] - 273.15)); // Save BP solution as fallback double[][] bpLiq = new double[N][C]; @@ -2300,17 +2343,17 @@ private void solveSumRatesPhase() { double maxTrayTemp = 0.0; for (int j = 0; j < N; j++) { if (!Double.isNaN(fixedTemperature[j])) { - maxTrayTemp = Math.max(maxTrayTemp, fixedTemperature[j]); + maxTrayTemp = Math.max(maxTrayTemp, fixedTemperature[j]); } double feedMoles = 0; for (int i = 0; i < C; i++) { - feedMoles += feedLiq[j][i] + feedVap[j][i]; + feedMoles += feedLiq[j][i] + feedVap[j][i]; } if (feedMoles > 1e-10) { - // Use feed enthalpy to estimate feed temperature - // For now, use the BP starting temperatures as bounds - minTrayTemp = Math.min(minTrayTemp, bpT[j]); - maxTrayTemp = Math.max(maxTrayTemp, bpT[j]); + // Use feed enthalpy to estimate feed temperature + // For now, use the BP starting temperatures as bounds + minTrayTemp = Math.min(minTrayTemp, bpT[j]); + maxTrayTemp = Math.max(maxTrayTemp, bpT[j]); } } // Add margins @@ -2321,7 +2364,7 @@ private void solveSumRatesPhase() { double bpBotT = bpT[0]; minTrayTemp = Math.max(bpTopT - 30.0, 200.0); logger.info("SR: T bounds: min={}C max={}C", String.format("%.1f", minTrayTemp - 273.15), - String.format("%.1f", maxTrayTemp - 273.15)); + String.format("%.1f", maxTrayTemp - 273.15)); for (int iter = 0; iter < maxIter; iter++) { // Step 1: Update K-values and enthalpies at current T, x @@ -2329,7 +2372,7 @@ private void solveSumRatesPhase() { // Step 2: Tridiagonal M-solve for each component (BP-style) for (int comp = 0; comp < C; comp++) { - solveTridiagonalForComponent(comp); + solveTridiagonalForComponent(comp); } // Step 3: Proper Sum-Rates L/V update. @@ -2349,11 +2392,11 @@ private void solveSumRatesPhase() { double[] newV = new double[N]; double srLVDamp = 0.6; // blending factor for L/V updates for (int j = 0; j < N; j++) { - double sumL = 0; - for (int i = 0; i < C; i++) { - sumL += liq[j][i]; - } - newL[j] = Math.max(sumL, 1e-20); + double sumL = 0; + for (int i = 0; i < C; i++) { + sumL += liq[j][i]; + } + newL[j] = Math.max(sumL, 1e-20); } // Note: do NOT anchor newL[0] from overall-MB. // M-equations are component-conservative; sum_i gives newL[j] that @@ -2361,36 +2404,37 @@ private void solveSumRatesPhase() { // (totalFeed - V[N-1]) creates a conflict that prevents the cascade // from converging (forces newV[N-1] == V_curr[N-1]). if (hasReboiler && boilupRatio > 0) { - // V[0] = boilupRatio * L[0] gets enforced below by cascade + // V[0] = boilupRatio * L[0] gets enforced below by cascade } // Reboiler vapor anchor: V[0] = L[1] + feed[0] - L[0] newV[0] = Math.max(newL[1] + feedLTotal[0] + feedVTotal[0] - newL[0], 1e-10); // Cascade up: V[j] = V[j-1] + L[j+1] - L[j] + feed[j], j=1..N-2 for (int j = 1; j < N - 1; j++) { - double netFeed = feedLTotal[j] + feedVTotal[j]; - newV[j] = Math.max(newV[j - 1] + newL[j + 1] - newL[j] + netFeed, 1e-10); + double netFeed = feedLTotal[j] + feedVTotal[j]; + newV[j] = Math.max(newV[j - 1] + newL[j + 1] - newL[j] + netFeed, 1e-10); } // Top: V[N-1] = V[N-2] + feed[N-1] - L[N-1] (no L[N]) - newV[N - 1] = Math.max(newV[N - 2] + feedLTotal[N - 1] + feedVTotal[N - 1] - newL[N - 1], 1e-10); + newV[N - 1] = + Math.max(newV[N - 2] + feedLTotal[N - 1] + feedVTotal[N - 1] - newL[N - 1], 1e-10); // Blend with current to damp the change (stability) for (int j = 0; j < N; j++) { - double blendedL = (1.0 - srLVDamp) * L[j] + srLVDamp * newL[j]; - double scale = blendedL / Math.max(L[j], 1e-20); - L[j] = Math.max(blendedL, 1e-20); - // Scale component liquid flows to match new L[j] - for (int i = 0; i < C; i++) { - liq[j][i] *= scale; - } - V[j] = Math.max((1.0 - srLVDamp) * V[j] + srLVDamp * newV[j], 1e-10); + double blendedL = (1.0 - srLVDamp) * L[j] + srLVDamp * newL[j]; + double scale = blendedL / Math.max(L[j], 1e-20); + L[j] = Math.max(blendedL, 1e-20); + // Scale component liquid flows to match new L[j] + for (int i = 0; i < C; i++) { + liq[j][i] *= scale; + } + V[j] = Math.max((1.0 - srLVDamp) * V[j] + srLVDamp * newV[j], 1e-10); } // Update vapor compositions for (int j = 0; j < N; j++) { - for (int i = 0; i < C; i++) { - double xi = liq[j][i] / Math.max(L[j], 1e-20); - vap[j][i] = K[j][i] * xi * V[j]; - } + for (int i = 0; i < C; i++) { + double xi = liq[j][i] / Math.max(L[j], 1e-20); + vap[j][i] = K[j][i] * xi * V[j]; + } } // Step 4: Refresh enthalpies at updated compositions @@ -2402,65 +2446,66 @@ private void solveSumRatesPhase() { double maxAbsEErr = 0; for (int j = 0; j < N; j++) { - maxAbsEErr = Math.max(maxAbsEErr, Math.abs(energyErr[j])); - - if (!Double.isNaN(fixedTemperature[j])) { - continue; // skip fixed-T trays (reboiler) - } - - // Numerical dE/dT by perturbation - double t0 = T[j]; - T[j] = t0 + pertDT; - evaluateThermoForTray(j); - double[] energyErrP = computeEnergyErrors(); - double dEdT = (energyErrP[j] - energyErr[j]) / pertDT; - T[j] = t0; - evaluateThermoForTray(j); // restore - - if (Math.abs(dEdT) < 1e-6) { - if (iter == 0) { - System.out - .println("[SR-DIAG] tray " + j + ": dEdT too small (" + String.format("%.6f", dEdT) + "), skipping"); - } - continue; - } - - double rawDeltaT = -energyErr[j] / dEdT; - - // Diagnostic: print full details on first iteration - if (iter == 0) { - double hOutJ = V[j] * hV[j] + L[j] * hL[j]; - double hInJ = 0; - if (j > 0) { - hInJ += V[j - 1] * hV[j - 1]; - } - if (j < N - 1) { - hInJ += L[j + 1] * hL[j + 1]; - } - double feedH = feedLTotal[j] * feedHL[j] + feedVTotal[j] * feedHV[j]; - double hInTotal = hInJ + feedH; - System.out.println("[SR-DIAG] tray " + j + ": T=" + String.format("%.1fC", T[j] - 273.15) + " hV=" - + String.format("%.1f", hV[j]) + " hL=" + String.format("%.1f", hL[j]) + " V=" - + String.format("%.1f", V[j]) + " L=" + String.format("%.1f", L[j]) + " Hout=" - + String.format("%.0f", hOutJ) + " Hin(noFeed)=" + String.format("%.0f", hInJ) + " feedH=" - + String.format("%.0f", feedH) + " Hin(total)=" + String.format("%.0f", hInTotal) + " Ej=" - + String.format("%.0f", energyErr[j]) + " dEdT=" + String.format("%.1f", dEdT) + " rawDT=" - + String.format("%.2f", rawDeltaT)); - } - - double deltaT = Math.max(-maxClampT, Math.min(maxClampT, rawDeltaT)); - deltaT *= dampT; - maxDeltaT = Math.max(maxDeltaT, Math.abs(deltaT)); - - T[j] += deltaT; - T[j] = Math.max(T[j], minTrayTemp); - T[j] = Math.min(T[j], maxTrayTemp); + maxAbsEErr = Math.max(maxAbsEErr, Math.abs(energyErr[j])); + + if (!Double.isNaN(fixedTemperature[j])) { + continue; // skip fixed-T trays (reboiler) + } + + // Numerical dE/dT by perturbation + double t0 = T[j]; + T[j] = t0 + pertDT; + evaluateThermoForTray(j); + double[] energyErrP = computeEnergyErrors(); + double dEdT = (energyErrP[j] - energyErr[j]) / pertDT; + T[j] = t0; + evaluateThermoForTray(j); // restore + + if (Math.abs(dEdT) < 1e-6) { + if (iter == 0) { + System.out.println("[SR-DIAG] tray " + j + ": dEdT too small (" + + String.format("%.6f", dEdT) + "), skipping"); + } + continue; + } + + double rawDeltaT = -energyErr[j] / dEdT; + + // Diagnostic: print full details on first iteration + if (iter == 0) { + double hOutJ = V[j] * hV[j] + L[j] * hL[j]; + double hInJ = 0; + if (j > 0) { + hInJ += V[j - 1] * hV[j - 1]; + } + if (j < N - 1) { + hInJ += L[j + 1] * hL[j + 1]; + } + double feedH = feedLTotal[j] * feedHL[j] + feedVTotal[j] * feedHV[j]; + double hInTotal = hInJ + feedH; + System.out.println("[SR-DIAG] tray " + j + ": T=" + String.format("%.1fC", T[j] - 273.15) + + " hV=" + String.format("%.1f", hV[j]) + " hL=" + String.format("%.1f", hL[j]) + + " V=" + String.format("%.1f", V[j]) + " L=" + String.format("%.1f", L[j]) + " Hout=" + + String.format("%.0f", hOutJ) + " Hin(noFeed)=" + String.format("%.0f", hInJ) + + " feedH=" + String.format("%.0f", feedH) + " Hin(total)=" + + String.format("%.0f", hInTotal) + " Ej=" + String.format("%.0f", energyErr[j]) + + " dEdT=" + String.format("%.1f", dEdT) + " rawDT=" + + String.format("%.2f", rawDeltaT)); + } + + double deltaT = Math.max(-maxClampT, Math.min(maxClampT, rawDeltaT)); + deltaT *= dampT; + maxDeltaT = Math.max(maxDeltaT, Math.abs(deltaT)); + + T[j] += deltaT; + T[j] = Math.max(T[j], minTrayTemp); + T[j] = Math.min(T[j], maxTrayTemp); } // Adaptive damping: increase damping if making progress steadily if (iter > 20 && maxDeltaT < maxClampT * 0.5) { - dampT = Math.min(dampT * 1.05, 0.5); - maxClampT = Math.min(maxClampT * 1.05, 8.0); + dampT = Math.min(dampT * 1.05, 0.5); + maxClampT = Math.min(maxClampT * 1.05, 8.0); } // Check convergence @@ -2468,43 +2513,45 @@ private void solveSumRatesPhase() { double eErr = computeMaxRelativeEnergyError(); if (iter % 10 == 0 || iter < 5) { - System.out.println("[SR] iter " + iter + ": dT=" + String.format("%.3f", maxDeltaT) + " mb=" - + String.format("%.4f%%", mbErr * 100) + " energy=" + String.format("%.1f%%", eErr * 100) + " T[0]=" - + String.format("%.1f", T[0] - 273.15) + " T[top]=" + String.format("%.1f", T[N - 1] - 273.15) + " damp=" - + String.format("%.3f", dampT)); - logger.info("SR iter {}: maxDT={} mbErr={}% energy={}% T[0]={} T[N-1]={} damp={}", iter, - String.format("%.3f", maxDeltaT), String.format("%.4f", mbErr * 100), String.format("%.2f", eErr * 100), - String.format("%.1f", T[0] - 273.15), String.format("%.1f", T[N - 1] - 273.15), - String.format("%.3f", dampT)); + System.out.println("[SR] iter " + iter + ": dT=" + String.format("%.3f", maxDeltaT) + " mb=" + + String.format("%.4f%%", mbErr * 100) + " energy=" + + String.format("%.1f%%", eErr * 100) + " T[0]=" + String.format("%.1f", T[0] - 273.15) + + " T[top]=" + String.format("%.1f", T[N - 1] - 273.15) + " damp=" + + String.format("%.3f", dampT)); + logger.info("SR iter {}: maxDT={} mbErr={}% energy={}% T[0]={} T[N-1]={} damp={}", iter, + String.format("%.3f", maxDeltaT), String.format("%.4f", mbErr * 100), + String.format("%.2f", eErr * 100), String.format("%.1f", T[0] - 273.15), + String.format("%.1f", T[N - 1] - 273.15), String.format("%.3f", dampT)); } // Track best solution (minimize energy error while keeping mass OK) if (eErr < bestEnergyErr || (mbErr < 0.01 && eErr < bestEnergyErr * 1.05)) { - bestEnergyErr = eErr; - bestMbErr = mbErr; - saveTrayState(bestLiq, bestT, bestV); + bestEnergyErr = eErr; + bestMbErr = mbErr; + saveTrayState(bestLiq, bestT, bestV); } // Convergence: T stable and mass balance acceptable if (maxDeltaT < 0.05 && mbErr < 0.01) { - logger.info("SR converged at iter {}: mbErr={}% energy={}%", iter, String.format("%.4f", mbErr * 100), - String.format("%.2f", eErr * 100)); - break; + logger.info("SR converged at iter {}: mbErr={}% energy={}%", iter, + String.format("%.4f", mbErr * 100), String.format("%.2f", eErr * 100)); + break; } // Divergence check: if mass balance deteriorates badly, reduce damping if (mbErr > 5.0) { - System.out - .println("[SR] mass balance degraded to " + String.format("%.1f%%", mbErr * 100) + " — reducing damping"); - logger.warn("SR: mass balance degraded to {}%, reducing damping", String.format("%.1f", mbErr * 100)); - restoreTrayState(bestLiq, bestT, bestV); - evaluateThermo(); - dampT *= 0.5; - maxClampT = Math.max(maxClampT * 0.5, 1.0); - if (dampT < 0.01) { - logger.info("SR: damping too low ({} ), stopping", String.format("%.3f", dampT)); - break; - } + System.out.println("[SR] mass balance degraded to " + String.format("%.1f%%", mbErr * 100) + + " — reducing damping"); + logger.warn("SR: mass balance degraded to {}%, reducing damping", + String.format("%.1f", mbErr * 100)); + restoreTrayState(bestLiq, bestT, bestV); + evaluateThermo(); + dampT *= 0.5; + maxClampT = Math.max(maxClampT * 0.5, 1.0); + if (dampT < 0.01) { + logger.info("SR: damping too low ({} ), stopping", String.format("%.3f", dampT)); + break; + } } } @@ -2514,12 +2561,12 @@ private void solveSumRatesPhase() { double finalMb = computeMassBalanceError(); double finalE = computeMaxRelativeEnergyError(); - System.out.println( - "[SR] Done: mb=" + String.format("%.4f%%", finalMb * 100) + " energy=" + String.format("%.1f%%", finalE * 100) - + " T[0]=" + String.format("%.1f", T[0] - 273.15) + " T[top]=" + String.format("%.1f", T[N - 1] - 273.15)); - logger.info("SR Phase done: mbErr={}% energy={}% T[0]={} T[N-1]={}", String.format("%.4f", finalMb * 100), - String.format("%.2f", finalE * 100), String.format("%.1f", T[0] - 273.15), - String.format("%.1f", T[N - 1] - 273.15)); + System.out.println("[SR] Done: mb=" + String.format("%.4f%%", finalMb * 100) + " energy=" + + String.format("%.1f%%", finalE * 100) + " T[0]=" + String.format("%.1f", T[0] - 273.15) + + " T[top]=" + String.format("%.1f", T[N - 1] - 273.15)); + logger.info("SR Phase done: mbErr={}% energy={}% T[0]={} T[N-1]={}", + String.format("%.4f", finalMb * 100), String.format("%.2f", finalE * 100), + String.format("%.1f", T[0] - 273.15), String.format("%.1f", T[N - 1] - 273.15)); // If SR solution is worse than BP in both metrics, revert to BP if (finalMb > bpMbErr * 5 && finalE > bpEnergyErr) { @@ -2534,8 +2581,8 @@ private void solveSumRatesPhase() { * Compute the maximum relative energy balance error across all non-fixed trays. * *

- * The relative error for each tray is |E_j| / max(|H_out_j|, |H_in_j|). This gives a dimensionless metric comparable - * to the mass balance error fraction. + * The relative error for each tray is |E_j| / max(|H_out_j|, |H_in_j|). This gives a + * dimensionless metric comparable to the mass balance error fraction. *

* * @return maximum relative energy error (0.0 = perfect, 1.0 = 100% imbalance) @@ -2545,20 +2592,20 @@ private double computeMaxRelativeEnergyError() { double maxRelErr = 0; for (int j = 0; j < N; j++) { if (!Double.isNaN(fixedTemperature[j])) { - continue; // skip fixed-T trays (reboiler) — they have a duty + continue; // skip fixed-T trays (reboiler) — they have a duty } double hOut = Math.abs(V[j] * hV[j] + L[j] * hL[j]); double hIn = 0; if (j > 0) { - hIn += Math.abs(V[j - 1] * hV[j - 1]); + hIn += Math.abs(V[j - 1] * hV[j - 1]); } if (j < N - 1) { - hIn += Math.abs(L[j + 1] * hL[j + 1]); + hIn += Math.abs(L[j + 1] * hL[j + 1]); } double scale = Math.max(hOut, hIn); if (scale > 1e-10) { - double relErr = Math.abs(eErr[j]) / scale; - maxRelErr = Math.max(maxRelErr, relErr); + double relErr = Math.abs(eErr[j]) / scale; + maxRelErr = Math.max(maxRelErr, relErr); } } return maxRelErr; @@ -2568,8 +2615,8 @@ private double computeMaxRelativeEnergyError() { * Compute energy balance error for each tray. * *

- * E_j = H_out_j - H_in_j where H_out_j = V[j]*hV[j] + L[j]*hL[j] and H_in_j = V[j-1]*hV[j-1] + L[j+1]*hL[j+1] + - * feedEnthalpy[j]. + * E_j = H_out_j - H_in_j where H_out_j = V[j]*hV[j] + L[j]*hL[j] and H_in_j = V[j-1]*hV[j-1] + + * L[j+1]*hL[j+1] + feedEnthalpy[j]. *

* * @return array of energy errors per tray in J @@ -2580,18 +2627,18 @@ private double[] computeEnergyErrors() { double hOut = V[j] * hV[j] + L[j] * hL[j]; double hIn = 0; if (j > 0) { - hIn += V[j - 1] * hV[j - 1]; + hIn += V[j - 1] * hV[j - 1]; } if (j < N - 1) { - hIn += L[j + 1] * hL[j + 1]; + hIn += L[j + 1] * hL[j + 1]; } // Feed enthalpy double feedMolesJ = 0; for (int i = 0; i < C; i++) { - feedMolesJ += feedLiq[j][i] + feedVap[j][i]; + feedMolesJ += feedLiq[j][i] + feedVap[j][i]; } if (feedMolesJ > 1e-10) { - hIn += feedLTotal[j] * feedHL[j] + feedVTotal[j] * feedHV[j]; + hIn += feedLTotal[j] * feedHL[j] + feedVTotal[j] * feedHV[j]; } eErr[j] = hOut - hIn; } @@ -2602,8 +2649,8 @@ private double[] computeEnergyErrors() { * Solve the tridiagonal material balance for a single component. * *

- * For component i, the material balance on tray j gives: a_j * l_{i,j-1} + b_j * l_{i,j} + c_j * l_{i,j+1} = d_j - * where a_j = -K_{i,j-1}*V_{j-1}/L_{j-1}, b_j = 1 + K_{i,j}*V_j/L_j, c_j = -1 + * For component i, the material balance on tray j gives: a_j * l_{i,j-1} + b_j * l_{i,j} + c_j * + * l_{i,j+1} = d_j where a_j = -K_{i,j-1}*V_{j-1}/L_{j-1}, b_j = 1 + K_{i,j}*V_j/L_j, c_j = -1 *

* * @param comp component index @@ -2629,7 +2676,7 @@ private void solveTridiagonalForComponent(int comp) { for (int j = 1; j < N; j++) { double m = b[j] - a[j] * cp[j - 1]; if (Math.abs(m) < 1e-30) { - m = 1e-30; + m = 1e-30; } cp[j] = c[j] / m; dp[j] = (d[j] - a[j] * dp[j - 1]) / m; @@ -2676,15 +2723,15 @@ private double solveBubblePointTemperature(int j) { double fMid = sumKxWilson(x, Tmid, P[j]) - 1.0; if (Math.abs(fMid) < 1e-8 || (Thigh - Tlow) < 0.001) { - return Tmid; + return Tmid; } if (fMid * fLow < 0) { - Thigh = Tmid; - fHigh = fMid; + Thigh = Tmid; + fHigh = fMid; } else { - Tlow = Tmid; - fLow = fMid; + Tlow = Tmid; + fLow = fMid; } } @@ -2728,12 +2775,13 @@ private double sumKxWilson(double[] x, double temp, double press) { * Variable ordering per tray: [l_{0,j}, l_{1,j}, ..., l_{C-1,j}, T_j, V_j] * *

- * Material balance for component i on tray j: M_{i,j} = l_{i,j}(1 + s_{i,j}) - l_{i,j-1} - v_{i,j+1} - f_{i,j} = 0 - * where s_{i,j} = K_{i,j} * V_j / L_j is the stripping factor, and v_{i,j} = K_{i,j} * l_{i,j} * V_j / L_j + * Material balance for component i on tray j: M_{i,j} = l_{i,j}(1 + s_{i,j}) - l_{i,j-1} - + * v_{i,j+1} - f_{i,j} = 0 where s_{i,j} = K_{i,j} * V_j / L_j is the stripping factor, and + * v_{i,j} = K_{i,j} * l_{i,j} * V_j / L_j * *

- * Energy balance for tray j: H_j = L_j*h_L_j + V_j*h_V_j - L_{j-1}*h_L_{j-1} - V_{j+1}*h_V_{j+1} - F_L_j*h_FL_j - - * F_V_j*h_FV_j - Q_j = 0 + * Energy balance for tray j: H_j = L_j*h_L_j + V_j*h_V_j - L_{j-1}*h_L_{j-1} - V_{j+1}*h_V_{j+1} + * - F_L_j*h_FL_j - F_V_j*h_FV_j - Q_j = 0 * *

* Summation: S_j = sum_i(y_{i,j}) - 1 = sum_i(K_{i,j}*x_{i,j}) - 1 = 0 @@ -2751,58 +2799,58 @@ private double[] computeResidual() { // Material balances: C equations for (int i = 0; i < C; i++) { - double Mij = liq[j][i] + vap[j][i]; // leaving tray j + double Mij = liq[j][i] + vap[j][i]; // leaving tray j - // Liquid from tray above (j+1) - if (j < N - 1) { - Mij -= liq[j + 1][i]; - } + // Liquid from tray above (j+1) + if (j < N - 1) { + Mij -= liq[j + 1][i]; + } - // Vapor from tray below (j-1) - if (j > 0) { - Mij -= vap[j - 1][i]; - } + // Vapor from tray below (j-1) + if (j > 0) { + Mij -= vap[j - 1][i]; + } - // Feed - Mij -= feedLiq[j][i] + feedVap[j][i]; + // Feed + Mij -= feedLiq[j][i] + feedVap[j][i]; - F[base + i] = Mij / flowScale; + F[base + i] = Mij / flowScale; } // Energy balance (or fixed temperature specification) if (!Double.isNaN(fixedTemperature[j])) { - // Fixed temperature: residual is T_j - T_spec - F[base + C] = (T[j] - fixedTemperature[j]) / tempScale; + // Fixed temperature: residual is T_j - T_spec + F[base + C] = (T[j] - fixedTemperature[j]) / tempScale; } else { - double Hj = Lj * hL[j] + V[j] * hV[j]; + double Hj = Lj * hL[j] + V[j] * hV[j]; - if (j < N - 1) { - Hj -= L[j + 1] * hL[j + 1]; - } - if (j > 0) { - Hj -= V[j - 1] * hV[j - 1]; - } + if (j < N - 1) { + Hj -= L[j + 1] * hL[j + 1]; + } + if (j > 0) { + Hj -= V[j - 1] * hV[j - 1]; + } - // Feed enthalpies - Hj -= feedLTotal[j] * feedHL[j] + feedVTotal[j] * feedHV[j]; + // Feed enthalpies + Hj -= feedLTotal[j] * feedHL[j] + feedVTotal[j] * feedHV[j]; - // Heat duty - Hj -= Q[j]; + // Heat duty + Hj -= Q[j]; - // Scale energy equation: total tray throughput * latent heat gives the - // characteristic energy magnitude. This brings H residuals to ~O(1) - // consistent with scaled material balances. - double trayFlow = Math.max(Lj + V[j], flowScale); - double latentHeat = Math.max(Math.abs(hV[j] - hL[j]), 1e3); - double energyScale = trayFlow * latentHeat; - F[base + C] = Hj / energyScale; + // Scale energy equation: total tray throughput * latent heat gives the + // characteristic energy magnitude. This brings H residuals to ~O(1) + // consistent with scaled material balances. + double trayFlow = Math.max(Lj + V[j], flowScale); + double latentHeat = Math.max(Math.abs(hV[j] - hL[j]), 1e3); + double energyScale = trayFlow * latentHeat; + F[base + C] = Hj / energyScale; } // Summation equation: sum(K_{i,j} * x_{i,j}) - 1 = 0 double sumKx = 0; for (int i = 0; i < C; i++) { - double xi = liq[j][i] / Math.max(Lj, 1e-20); - sumKx += K[j][i] * xi; + double xi = liq[j][i] / Math.max(Lj, 1e-20); + sumKx += K[j][i] * xi; } F[base + C + 1] = sumKx - 1.0; } @@ -2814,9 +2862,10 @@ private double[] computeResidual() { * Compute the Jacobian matrix numerically using finite differences. * *

- * The Jacobian has block-tridiagonal structure. Only entries in the tri-diagonal bands are computed. For variable k - * on tray jj, we compute d(residual_j)/d(var_{jj,k}) for j in {jj-1, jj, jj+1}. After perturbing a variable on tray - * jj, only tray jj's thermo is re-evaluated (K-values, enthalpies depend only on local T and x). + * The Jacobian has block-tridiagonal structure. Only entries in the tri-diagonal bands are + * computed. For variable k on tray jj, we compute d(residual_j)/d(var_{jj,k}) for j in {jj-1, jj, + * jj+1}. After perturbing a variable on tray jj, only tray jj's thermo is re-evaluated (K-values, + * enthalpies depend only on local T and x). *

* * @param F0 current residual vector @@ -2830,32 +2879,32 @@ private double[][] computeJacobian(double[] F0) { for (int jj = 0; jj < N; jj++) { for (int k = 0; k < varsPerTray; k++) { - int varIdx = jj * varsPerTray + k; + int varIdx = jj * varsPerTray + k; - // Save original value - double origVal = getVariable(jj, k); - double h = Math.max(Math.abs(origVal) * pertSize, minPert); + // Save original value + double origVal = getVariable(jj, k); + double h = Math.max(Math.abs(origVal) * pertSize, minPert); - // Perturb - setVariable(jj, k, origVal + h); + // Perturb + setVariable(jj, k, origVal + h); - // Re-evaluate thermo ONLY for the tray whose variable changed - evaluateThermoForTray(jj); + // Re-evaluate thermo ONLY for the tray whose variable changed + evaluateThermoForTray(jj); - // Compute perturbed residuals for affected trays (j-1, j, j+1) - int jStart = Math.max(0, jj - 1); - int jEnd = Math.min(N - 1, jj + 1); - for (int j = jStart; j <= jEnd; j++) { - double[] Fpert = computeResidualForTray(j); - int rowBase = j * varsPerTray; - for (int eq = 0; eq < varsPerTray; eq++) { - J[rowBase + eq][varIdx] = (Fpert[eq] - F0[rowBase + eq]) / h; - } - } + // Compute perturbed residuals for affected trays (j-1, j, j+1) + int jStart = Math.max(0, jj - 1); + int jEnd = Math.min(N - 1, jj + 1); + for (int j = jStart; j <= jEnd; j++) { + double[] Fpert = computeResidualForTray(j); + int rowBase = j * varsPerTray; + for (int eq = 0; eq < varsPerTray; eq++) { + J[rowBase + eq][varIdx] = (Fpert[eq] - F0[rowBase + eq]) / h; + } + } - // Restore - setVariable(jj, k, origVal); - evaluateThermoForTray(jj); + // Restore + setVariable(jj, k, origVal); + evaluateThermoForTray(jj); } } @@ -2881,10 +2930,10 @@ private double[] computeResidualForTray(int j) { double Mij = liq[j][i] + vap[j][i]; if (j < N - 1) { - Mij -= liq[j + 1][i]; + Mij -= liq[j + 1][i]; } if (j > 0) { - Mij -= vap[j - 1][i]; + Mij -= vap[j - 1][i]; } Mij -= feedLiq[j][i] + feedVap[j][i]; @@ -2897,10 +2946,10 @@ private double[] computeResidualForTray(int j) { } else { double Hj = Lj * hL[j] + V[j] * hV[j]; if (j < N - 1) { - Hj -= L[j + 1] * hL[j + 1]; + Hj -= L[j + 1] * hL[j + 1]; } if (j > 0) { - Hj -= V[j - 1] * hV[j - 1]; + Hj -= V[j - 1] * hV[j - 1]; } Hj -= feedLTotal[j] * feedHL[j] + feedVTotal[j] * feedHV[j]; Hj -= Q[j]; @@ -2923,8 +2972,8 @@ private double[] computeResidualForTray(int j) { } /** - * Evaluate thermodynamic properties for a single tray. Used during Jacobian perturbation to avoid full column - * re-evaluation. V[j] is NOT overwritten — it is a free variable. + * Evaluate thermodynamic properties for a single tray. Used during Jacobian perturbation to avoid + * full column re-evaluation. V[j] is NOT overwritten — it is a free variable. * * @param j tray index */ @@ -2932,20 +2981,20 @@ private double[] computeResidualForTray(int j) { * Apply the Edmister Murphree-efficiency proxy to the K-values of tray j. * *

- * The exact Murphree definition mixes the actual outlet vapor with the vapor entering from below: y_actual = y_in + - * eta * (y_eq - y_in). Embedding that into the simultaneous MESH solve requires the residual on tray j to couple to - * vap[j-1], which would expand the Jacobian bandwidth. + * The exact Murphree definition mixes the actual outlet vapor with the vapor entering from below: + * y_actual = y_in + eta * (y_eq - y_in). Embedding that into the simultaneous MESH solve requires + * the residual on tray j to couple to vap[j-1], which would expand the Jacobian bandwidth. *

* * The Edmister approximation K_eff = K^eta has the correct limits: *
    *
  • eta = 1.0 -> K_eff = K (rigorous equilibrium)
  • - *
  • eta -> 0 -> K_eff -> 1.0, so y = K_eff*x -> x and the stage becomes passive (vapor leaves with the - * same composition as the liquid).
  • + *
  • eta -> 0 -> K_eff -> 1.0, so y = K_eff*x -> x and the stage becomes passive + * (vapor leaves with the same composition as the liquid).
  • *
- * This is the same correction used by many shortcut and equation-tearing column codes (e.g. ChemSep documentation, - * Edmister 1957) and is mass- and energy-balance consistent because the residual equations always use the scaled - * K[j][i]. + * This is the same correction used by many shortcut and equation-tearing column codes (e.g. + * ChemSep documentation, Edmister 1957) and is mass- and energy-balance consistent because the + * residual equations always use the scaled K[j][i]. * * @param j tray index (0 = reboiler, N-1 = condenser when present) */ @@ -2957,7 +3006,7 @@ private void applyMurphreeEfficiencyToK(int j) { for (int i = 0; i < C; i++) { double kEq = K[j][i]; if (kEq <= 0.0) { - continue; + continue; } K[j][i] = Math.pow(kEq, eta); } @@ -3002,7 +3051,7 @@ private void evaluateThermoForTray(int j) { for (int i = 0; i < C; i++) { double Kguess = K[j][i]; if (!(Kguess > 1e-20 && Kguess < 1e15) || !phiOk) { - Kguess = wilsonK(i, T[j], Pbar); + Kguess = wilsonK(i, T[j], Pbar); } y[i] = Kguess * x[i]; sumKxGuess += y[i]; @@ -3017,29 +3066,29 @@ private void evaluateThermoForTray(int j) { boolean kOk = phiOk; if (kOk) { for (int sweep = 0; sweep < 2; sweep++) { - if (!computeSinglePhaseFugacityCoefficients(y, T[j], Pbar, true, phiV)) { - kOk = false; - break; - } - double sumKxLocal = 0; - for (int i = 0; i < C; i++) { - double Knew = phiL[i] / Math.max(phiV[i], 1e-30); - Knew = Math.max(Knew, 1e-15); - Knew = Math.min(Knew, 1e15); - K[j][i] = Knew; - y[i] = Knew * x[i]; - sumKxLocal += y[i]; - } - for (int i = 0; i < C; i++) { - y[i] = (sumKxLocal > 1e-20) ? y[i] / sumKxLocal : x[i]; - } + if (!computeSinglePhaseFugacityCoefficients(y, T[j], Pbar, true, phiV)) { + kOk = false; + break; + } + double sumKxLocal = 0; + for (int i = 0; i < C; i++) { + double Knew = phiL[i] / Math.max(phiV[i], 1e-30); + Knew = Math.max(Knew, 1e-15); + Knew = Math.min(Knew, 1e15); + K[j][i] = Knew; + y[i] = Knew * x[i]; + sumKxLocal += y[i]; + } + for (int i = 0; i < C; i++) { + y[i] = (sumKxLocal > 1e-20) ? y[i] / sumKxLocal : x[i]; + } } } if (!kOk) { // Wilson K-values fallback (matches the previous flash-failure branch). for (int i = 0; i < C; i++) { - K[j][i] = wilsonK(i, T[j], Pbar); + K[j][i] = wilsonK(i, T[j], Pbar); } applyMurphreeEfficiencyToK(j); hL[j] = 0; @@ -3076,14 +3125,15 @@ private void evaluateThermoForTray(int j) { } /** - * Compute molar enthalpy for a single phase (liquid or vapor) at given composition, temperature and pressure using - * the EOS. + * Compute molar enthalpy for a single phase (liquid or vapor) at given composition, temperature + * and pressure using the EOS. * *

- * This forces the EOS to evaluate at the specified phase root (liquid or vapor) rather than doing a flash. This is - * essential for energy balance in the BP method: hL must be evaluated at the actual liquid composition x[j] using the - * liquid root, and hV at the actual vapor composition y[j] using the vapor root. Using the enthalpies from a TPflash - * of x[j] gives wrong hV because the flash vapor has a different composition than the actual column vapor. + * This forces the EOS to evaluate at the specified phase root (liquid or vapor) rather than doing + * a flash. This is essential for energy balance in the BP method: hL must be evaluated at the + * actual liquid composition x[j] using the liquid root, and hV at the actual vapor composition + * y[j] using the vapor root. Using the enthalpies from a TPflash of x[j] gives wrong hV because + * the flash vapor has a different composition than the actual column vapor. *

* * @param composition molar composition array (must sum to ~1.0) @@ -3096,23 +3146,26 @@ private void evaluateThermoForTray(int j) { * Boston-Sullivan inside-out refinement of the seed state. * *

- * Activates when at least one tray has a fixed temperature spec — the case where the smooth Wilson BP cascade - * under-resolves a sharp T-pinch. The algorithm follows Boston & Sullivan (1974): + * Activates when at least one tray has a fixed temperature spec — the case where the smooth + * Wilson BP cascade under-resolves a sharp T-pinch. The algorithm follows Boston & Sullivan + * (1974): *

*
    - *
  1. Outer loop: compute base K-value {@code Kb[j]} as the x-weighted geometric mean of {@code K[j][i]}, relative - * volatilities {@code alpha[j][i] = K[j][i] / Kb[j]}, and an Antoine-style fit {@code ln(Kb) = A - B/T} via a - * Wilson-K perturbation at {@code T + 5K}.
  2. - *
  3. Inner loop (fixed {@code alpha}, {@code A}, {@code B}): solve one tridiagonal MB per component for - * {@code x[j][i]}; apply the bubble-point criterion {@code sum(alpha * x) * Kb = 1} to get {@code Kb_new}; invert the - * Antoine fit for {@code T_new}; clamp T-pinned trays to {@code fixedTemperature[j]}; update {@code liq[j][i]} and - * {@code vap[j][i]} preserving the current {@code L[j]} and {@code V[j]} totals.
  4. - *
  5. Repeat until the relative change in {@code alpha} between outer iterations falls below tolerance.
  6. + *
  7. Outer loop: compute base K-value {@code Kb[j]} as the x-weighted geometric mean of + * {@code K[j][i]}, relative volatilities {@code alpha[j][i] = K[j][i] / Kb[j]}, and an + * Antoine-style fit {@code ln(Kb) = A - B/T} via a Wilson-K perturbation at {@code T + 5K}.
  8. + *
  9. Inner loop (fixed {@code alpha}, {@code A}, {@code B}): solve one tridiagonal MB per + * component for {@code x[j][i]}; apply the bubble-point criterion {@code sum(alpha * x) * Kb = 1} + * to get {@code Kb_new}; invert the Antoine fit for {@code T_new}; clamp T-pinned trays to + * {@code fixedTemperature[j]}; update {@code liq[j][i]} and {@code vap[j][i]} preserving the + * current {@code L[j]} and {@code V[j]} totals.
  10. + *
  11. Repeat until the relative change in {@code alpha} between outer iterations falls below + * tolerance.
  12. *
* *

- * Mass balance is preserved by the tridiagonal solve. V[j] and L[j] are held at their BP-Wilson values; the - * downstream SR / Newton phase polishes the energy balance. + * Mass balance is preserved by the tridiagonal solve. V[j] and L[j] are held at their BP-Wilson + * values; the downstream SR / Newton phase polishes the energy balance. *

*/ private void runBostonSullivanRefinement() { @@ -3122,8 +3175,8 @@ private void runBostonSullivanRefinement() { boolean hasFixedT = false; for (int j = 0; j < N; j++) { if (!Double.isNaN(fixedTemperature[j])) { - hasFixedT = true; - break; + hasFixedT = true; + break; } } if (!hasFixedT) { @@ -3171,179 +3224,181 @@ private void runBostonSullivanRefinement() { // (A) Compute Kb (x-weighted geomean of K) and alpha[j][i] from rigorous K. double maxAlphaChange = 0.0; for (int j = 0; j < N; j++) { - double Lsum = 0; - for (int i = 0; i < C; i++) { - Lsum += liq[j][i]; - } - Lsum = Math.max(Lsum, 1e-20); - double lnKb = 0; - for (int i = 0; i < C; i++) { - double xi = liq[j][i] / Lsum; - double Ki = Math.max(K[j][i], 1e-30); - lnKb += xi * Math.log(Ki); - } - Kb[j] = Math.max(Math.exp(lnKb), 1e-30); - for (int i = 0; i < C; i++) { - double Ki = Math.max(K[j][i], 1e-30); - alpha[j][i] = Ki / Kb[j]; - if (outer > 0) { - double aOld = alphaOld[j][i]; - if (aOld > 1e-30) { - double rel = Math.abs(alpha[j][i] - aOld) / aOld; - if (rel > maxAlphaChange) { - maxAlphaChange = rel; - } - } - } - } - - // (B) Antoine fit: ln(Kb) = A - B/T via Wilson K-perturbation at T+5K. - // ln(Kb_pert) = ln(Kb) + sum_i x_i * ln(K_i_pert / K_i_base) - double Tj = T[j]; - double Tpert = Tj + 5.0; - double Pbar = Math.max(P[j] / 1e5, 1e-12); - double shift = 0; - for (int i = 0; i < C; i++) { - double KiPert = (Pc[i] / Pbar) * Math.exp(5.37 * (1.0 + omega[i]) * (1.0 - Tc[i] / Tpert)); - double KiBase = (Pc[i] / Pbar) * Math.exp(5.37 * (1.0 + omega[i]) * (1.0 - Tc[i] / Tj)); - double xi = liq[j][i] / Lsum; - shift += xi * Math.log(Math.max(KiPert, 1e-30) / Math.max(KiBase, 1e-30)); - } - double lnKbAtPert = Math.log(Kb[j]) + shift; - double inv1 = 1.0 / Tj; - double inv2 = 1.0 / Tpert; - double Bv; - if (Math.abs(inv2 - inv1) > 1e-14) { - Bv = (Math.log(Kb[j]) - lnKbAtPert) / (inv2 - inv1); - } else { - Bv = 2500.0; - } - // Sanity bound on B (typical 1000..5000 K for hydrocarbons). - if (!Double.isFinite(Bv) || Bv < 200.0) { - Bv = 2500.0; - } else if (Bv > 15000.0) { - Bv = 15000.0; - } - Bant[j] = Bv; - Aant[j] = Math.log(Kb[j]) + Bv / Tj; + double Lsum = 0; + for (int i = 0; i < C; i++) { + Lsum += liq[j][i]; + } + Lsum = Math.max(Lsum, 1e-20); + double lnKb = 0; + for (int i = 0; i < C; i++) { + double xi = liq[j][i] / Lsum; + double Ki = Math.max(K[j][i], 1e-30); + lnKb += xi * Math.log(Ki); + } + Kb[j] = Math.max(Math.exp(lnKb), 1e-30); + for (int i = 0; i < C; i++) { + double Ki = Math.max(K[j][i], 1e-30); + alpha[j][i] = Ki / Kb[j]; + if (outer > 0) { + double aOld = alphaOld[j][i]; + if (aOld > 1e-30) { + double rel = Math.abs(alpha[j][i] - aOld) / aOld; + if (rel > maxAlphaChange) { + maxAlphaChange = rel; + } + } + } + } + + // (B) Antoine fit: ln(Kb) = A - B/T via Wilson K-perturbation at T+5K. + // ln(Kb_pert) = ln(Kb) + sum_i x_i * ln(K_i_pert / K_i_base) + double Tj = T[j]; + double Tpert = Tj + 5.0; + double Pbar = Math.max(P[j] / 1e5, 1e-12); + double shift = 0; + for (int i = 0; i < C; i++) { + double KiPert = + (Pc[i] / Pbar) * Math.exp(5.37 * (1.0 + omega[i]) * (1.0 - Tc[i] / Tpert)); + double KiBase = (Pc[i] / Pbar) * Math.exp(5.37 * (1.0 + omega[i]) * (1.0 - Tc[i] / Tj)); + double xi = liq[j][i] / Lsum; + shift += xi * Math.log(Math.max(KiPert, 1e-30) / Math.max(KiBase, 1e-30)); + } + double lnKbAtPert = Math.log(Kb[j]) + shift; + double inv1 = 1.0 / Tj; + double inv2 = 1.0 / Tpert; + double Bv; + if (Math.abs(inv2 - inv1) > 1e-14) { + Bv = (Math.log(Kb[j]) - lnKbAtPert) / (inv2 - inv1); + } else { + Bv = 2500.0; + } + // Sanity bound on B (typical 1000..5000 K for hydrocarbons). + if (!Double.isFinite(Bv) || Bv < 200.0) { + Bv = 2500.0; + } else if (Bv > 15000.0) { + Bv = 15000.0; + } + Bant[j] = Bv; + Aant[j] = Math.log(Kb[j]) + Bv / Tj; } if (outer > 0 && maxAlphaChange < alphaTol) { - logger.info("BS outer {} converged on alpha (maxRelChange={})", outer, String.format("%.3e", maxAlphaChange)); - // Snapshot alphaOld for next call (no-op here since we're breaking) - break; + logger.info("BS outer {} converged on alpha (maxRelChange={})", outer, + String.format("%.3e", maxAlphaChange)); + // Snapshot alphaOld for next call (no-op here since we're breaking) + break; } // (C) Inner loop: tridiag MB per component + bubble-T per tray. double maxDTinner = 0; int innerUsed = 0; for (int inner = 0; inner < maxInner; inner++) { - innerUsed = inner + 1; - // Build & solve C tridiagonal systems for x[*][i]. - for (int i = 0; i < C; i++) { - for (int j = 0; j < N; j++) { - double Kij = alpha[j][i] * Kb[j]; - bDiag[j] = L[j] + V[j] * Kij; - if (j > 0) { - double Kjm1 = alpha[j - 1][i] * Kb[j - 1]; - aDiag[j] = -V[j - 1] * Kjm1; - } else { - aDiag[j] = 0; - } - if (j < N - 1) { - cDiag[j] = -L[j + 1]; - } else { - cDiag[j] = 0; - } - rhs[j] = feedLiq[j][i] + feedVap[j][i]; - } - thomas(aDiag, bDiag, cDiag, rhs, xCol); - for (int j = 0; j < N; j++) { - xUpd[j][i] = Math.max(xCol[j], 1e-25); - } - } - - // Normalize x per tray; bubble criterion -> Kb_new -> T_new -> Kb at T_new. - maxDTinner = 0; - for (int j = 0; j < N; j++) { - double sumX = 0; - for (int i = 0; i < C; i++) { - sumX += xUpd[j][i]; - } - if (sumX <= 0) { - sumX = 1.0; - } - for (int i = 0; i < C; i++) { - xUpd[j][i] /= sumX; - } - double sumAX = 0; - for (int i = 0; i < C; i++) { - sumAX += alpha[j][i] * xUpd[j][i]; - } - double KbBubble = 1.0 / Math.max(sumAX, 1e-30); - - double tNew; - if (!Double.isNaN(fixedTemperature[j])) { - tNew = fixedTemperature[j]; - } else { - double denom = Aant[j] - Math.log(Math.max(KbBubble, 1e-30)); - if (Math.abs(denom) < 1e-10) { - tNew = T[j]; - } else { - tNew = Bant[j] / denom; - } - // Guard against runaway estimates. - if (!Double.isFinite(tNew) || tNew < 150.0 || tNew > 1500.0) { - tNew = T[j]; - } - } - double dT = tNew - T[j]; - if (dT > tStepLimit) { - dT = tStepLimit; - } - if (dT < -tStepLimit) { - dT = -tStepLimit; - } - double tFinal = T[j] + tDamp * dT; - if (!Double.isNaN(fixedTemperature[j])) { - tFinal = fixedTemperature[j]; - } - if (Math.abs(tFinal - T[j]) > maxDTinner) { - maxDTinner = Math.abs(tFinal - T[j]); - } - T[j] = tFinal; - Kb[j] = Math.exp(Aant[j] - Bant[j] / tFinal); - } - - // Update liq[j][i] and vap[j][i] from xUpd and current Kb. - // Preserve total L[j], V[j] (energy balance polished by downstream SR). - for (int j = 0; j < N; j++) { - double sumY = 0; - double[] y = new double[C]; - for (int i = 0; i < C; i++) { - y[i] = alpha[j][i] * Kb[j] * xUpd[j][i]; - sumY += y[i]; - } - if (sumY <= 0) { - sumY = 1.0; - } - for (int i = 0; i < C; i++) { - y[i] /= sumY; - liq[j][i] = L[j] * xUpd[j][i]; - vap[j][i] = V[j] * y[i]; - } - } - - if (maxDTinner < tTol) { - break; - } + innerUsed = inner + 1; + // Build & solve C tridiagonal systems for x[*][i]. + for (int i = 0; i < C; i++) { + for (int j = 0; j < N; j++) { + double Kij = alpha[j][i] * Kb[j]; + bDiag[j] = L[j] + V[j] * Kij; + if (j > 0) { + double Kjm1 = alpha[j - 1][i] * Kb[j - 1]; + aDiag[j] = -V[j - 1] * Kjm1; + } else { + aDiag[j] = 0; + } + if (j < N - 1) { + cDiag[j] = -L[j + 1]; + } else { + cDiag[j] = 0; + } + rhs[j] = feedLiq[j][i] + feedVap[j][i]; + } + thomas(aDiag, bDiag, cDiag, rhs, xCol); + for (int j = 0; j < N; j++) { + xUpd[j][i] = Math.max(xCol[j], 1e-25); + } + } + + // Normalize x per tray; bubble criterion -> Kb_new -> T_new -> Kb at T_new. + maxDTinner = 0; + for (int j = 0; j < N; j++) { + double sumX = 0; + for (int i = 0; i < C; i++) { + sumX += xUpd[j][i]; + } + if (sumX <= 0) { + sumX = 1.0; + } + for (int i = 0; i < C; i++) { + xUpd[j][i] /= sumX; + } + double sumAX = 0; + for (int i = 0; i < C; i++) { + sumAX += alpha[j][i] * xUpd[j][i]; + } + double KbBubble = 1.0 / Math.max(sumAX, 1e-30); + + double tNew; + if (!Double.isNaN(fixedTemperature[j])) { + tNew = fixedTemperature[j]; + } else { + double denom = Aant[j] - Math.log(Math.max(KbBubble, 1e-30)); + if (Math.abs(denom) < 1e-10) { + tNew = T[j]; + } else { + tNew = Bant[j] / denom; + } + // Guard against runaway estimates. + if (!Double.isFinite(tNew) || tNew < 150.0 || tNew > 1500.0) { + tNew = T[j]; + } + } + double dT = tNew - T[j]; + if (dT > tStepLimit) { + dT = tStepLimit; + } + if (dT < -tStepLimit) { + dT = -tStepLimit; + } + double tFinal = T[j] + tDamp * dT; + if (!Double.isNaN(fixedTemperature[j])) { + tFinal = fixedTemperature[j]; + } + if (Math.abs(tFinal - T[j]) > maxDTinner) { + maxDTinner = Math.abs(tFinal - T[j]); + } + T[j] = tFinal; + Kb[j] = Math.exp(Aant[j] - Bant[j] / tFinal); + } + + // Update liq[j][i] and vap[j][i] from xUpd and current Kb. + // Preserve total L[j], V[j] (energy balance polished by downstream SR). + for (int j = 0; j < N; j++) { + double sumY = 0; + double[] y = new double[C]; + for (int i = 0; i < C; i++) { + y[i] = alpha[j][i] * Kb[j] * xUpd[j][i]; + sumY += y[i]; + } + if (sumY <= 0) { + sumY = 1.0; + } + for (int i = 0; i < C; i++) { + y[i] /= sumY; + liq[j][i] = L[j] * xUpd[j][i]; + vap[j][i] = V[j] * y[i]; + } + } + + if (maxDTinner < tTol) { + break; + } } // Snapshot alpha for outer-loop convergence check next pass. for (int j = 0; j < N; j++) { - for (int i = 0; i < C; i++) { - alphaOld[j][i] = alpha[j][i]; - } + for (int i = 0; i < C; i++) { + alphaOld[j][i] = alpha[j][i]; + } } // (D) Refresh rigorous K for next outer iteration. @@ -3356,34 +3411,34 @@ private void runBostonSullivanRefinement() { // — without it, V/L stays at the BP-Wilson seed (too-low boilup) and // SR later reverses the T improvement to match the low V cascade. for (int j = 0; j < N; j++) { - double Lsum = 0; - double Vsum = 0; - for (int i = 0; i < C; i++) { - Lsum += liq[j][i]; - Vsum += vap[j][i]; - } - double[] xj = new double[C]; - double[] yj = new double[C]; - for (int i = 0; i < C; i++) { - xj[i] = (Lsum > 1e-20) ? liq[j][i] / Lsum : 1.0 / C; - yj[i] = (Vsum > 1e-20) ? vap[j][i] / Vsum : 1.0 / C; - } - double Pbar = P[j] / 1e5; - hL[j] = computeSinglePhaseEnthalpy(xj, T[j], Pbar, false); - hV[j] = computeSinglePhaseEnthalpy(yj, T[j], Pbar, true); + double Lsum = 0; + double Vsum = 0; + for (int i = 0; i < C; i++) { + Lsum += liq[j][i]; + Vsum += vap[j][i]; + } + double[] xj = new double[C]; + double[] yj = new double[C]; + for (int i = 0; i < C; i++) { + xj[i] = (Lsum > 1e-20) ? liq[j][i] / Lsum : 1.0 / C; + yj[i] = (Vsum > 1e-20) ? vap[j][i] / Vsum : 1.0 / C; + } + double Pbar = P[j] / 1e5; + hL[j] = computeSinglePhaseEnthalpy(xj, T[j], Pbar, false); + hV[j] = computeSinglePhaseEnthalpy(yj, T[j], Pbar, true); } double vDamp = 0.25; double[] Vold = new double[N]; double[] Lold = new double[N]; for (int j = 0; j < N; j++) { - Vold[j] = V[j]; - Lold[j] = L[j]; + Vold[j] = V[j]; + Lold[j] = L[j]; } // Pre-compute Fmol (used both for V[0] closure and L rebuild below). double[] FmolPre = new double[N]; for (int j = 0; j < N; j++) { - FmolPre[j] = feedLTotal[j] + feedVTotal[j]; + FmolPre[j] = feedLTotal[j] + feedVTotal[j]; } // For T-pin reboiler stripper: Q[0] (reboiler duty) is the IMPLICIT // free variable that holds T[0] at its pinned value. Compute it from @@ -3396,114 +3451,114 @@ private void runBostonSullivanRefinement() { double qRebOverall = 0.0; boolean useOverallQreb = hasReboiler && useOverallMBClosure; if (useOverallQreb) { - qRebOverall = L[0] * hL[0] + V[N - 1] * hV[N - 1]; - for (int jj = 0; jj < N; jj++) { - qRebOverall -= feedLTotal[jj] * feedHL[jj]; - qRebOverall -= feedVTotal[jj] * feedHV[jj]; - if (jj != 0) { - qRebOverall -= Q[jj]; - } - } + qRebOverall = L[0] * hL[0] + V[N - 1] * hV[N - 1]; + for (int jj = 0; jj < N; jj++) { + qRebOverall -= feedLTotal[jj] * feedHL[jj]; + qRebOverall -= feedVTotal[jj] * feedHV[jj]; + if (jj != 0) { + qRebOverall -= Q[jj]; + } + } } for (int j = 0; j < N; j++) { - // Boundary trays: keep current closure. - if (j == 0 && hasReboiler && useOverallMBClosure) { - // T-pin reboiler: V[0] free, fixed by tray-0 EB with Q[0] - // taken from overall column EB (not the input Q[0]=0). - // Tray-0 EB: - // L[1]*hL[1] + F_L[0]*hF_L[0] + F_V[0]*hF_V[0] + Q_reb - // = V[0]*hV[0] + L[0]*hL[0] - if (Math.abs(hV[0]) < 1e-3) { - continue; - } - double num = -L[0] * hL[0]; - if (N > 1) { - num += L[1] * hL[1]; - } - num += feedLTotal[0] * feedHL[0]; - num += feedVTotal[0] * feedHV[0]; - num += qRebOverall; - double newV0 = num / hV[0]; - if (newV0 > 0 && Double.isFinite(newV0)) { - // V[0] (boilup) gets aggressive damping/cap: it's the master - // driver of the bottom-section T-cliff in T-pin strippers. - // Without this, vDamp=0.25 keeps V[0] stuck near the BP-Wilson - // seed (way too low) and the bottom section can't warm up. - double cap = 2.0 * Math.max(Vold[0], 1.0); - double dv = newV0 - V[0]; - if (dv > cap) { - dv = cap; - } else if (dv < -cap) { - dv = -cap; - } - V[0] = V[0] + 0.6 * dv; - if (V[0] < 1e-10) { - V[0] = 1e-10; - } - } - continue; - } - if (j == 0 && hasReboiler && boilupRatio > 0) { - V[0] = boilupRatio * L[0]; - continue; - } - // Skip V[N-1] update: with no condenser and T-pin reboiler, V[N-1] - // is the overhead product stream, fixed by overall MB closure - // (V[N-1] = totalFeed - L[0]). The tray-EB at j=N-1 lacks a - // closure equation (Q_reboiler is the free parameter, not V[N-1]) - // so its computed value runs to the cap. Leave V[N-1] at its - // current value (BP-Wilson seed already satisfies overall MB). - if (j == N - 1) { - continue; - } - // Internal tray energy balance: - // V_j*(hV_j-hL_j) = L_{j+1}*(hL_{j+1}-hL_j) + V_{j-1}*(hV_{j-1}-hL_j) - // + F_L*(hF_L-hL_j) + F_V*(hF_V-hL_j) + Q_j - double den = hV[j] - hL[j]; - if (Math.abs(den) < 1e-3) { - continue; - } - double num = 0; - if (j < N - 1) { - num += L[j + 1] * (hL[j + 1] - hL[j]); - } - if (j > 0) { - num += V[j - 1] * (hV[j - 1] - hL[j]); - } - num += feedLTotal[j] * (feedHL[j] - hL[j]); - num += feedVTotal[j] * (feedHV[j] - hL[j]); - num += Q[j]; - double newV = num / den; - if (newV > 0 && Double.isFinite(newV)) { - // Hard-clamp the step: at most ±25% of the prior V per outer pass - // (smaller than V[0]'s ±200% — internal trays must propagate - // gradually to avoid oscillation while V[0] catches up). - double cap = 0.25 * Vold[j]; - double dv = newV - V[j]; - if (dv > cap) { - dv = cap; - } else if (dv < -cap) { - dv = -cap; - } - V[j] = V[j] + 0.15 * dv; - if (V[j] < 1e-10) { - V[j] = 1e-10; - } - // Also bound to [0.25, 4.0] of prior V to prevent runaway. - if (V[j] < 0.25 * Vold[j]) { - V[j] = 0.25 * Vold[j]; - } else if (V[j] > 4.0 * Vold[j]) { - V[j] = 4.0 * Vold[j]; - } - // Hard physical cap: no internal vapor can exceed total feed - // (overall vapor mass balance: V[N-1] ≤ totalFeed). Apply to - // every tray since internal V can't exceed cumulative feed - // either. Leave a small margin for numerical headroom. - double Vcap = 0.98 * totalFeedMolesField; - if (V[j] > Vcap) { - V[j] = Vcap; - } - } + // Boundary trays: keep current closure. + if (j == 0 && hasReboiler && useOverallMBClosure) { + // T-pin reboiler: V[0] free, fixed by tray-0 EB with Q[0] + // taken from overall column EB (not the input Q[0]=0). + // Tray-0 EB: + // L[1]*hL[1] + F_L[0]*hF_L[0] + F_V[0]*hF_V[0] + Q_reb + // = V[0]*hV[0] + L[0]*hL[0] + if (Math.abs(hV[0]) < 1e-3) { + continue; + } + double num = -L[0] * hL[0]; + if (N > 1) { + num += L[1] * hL[1]; + } + num += feedLTotal[0] * feedHL[0]; + num += feedVTotal[0] * feedHV[0]; + num += qRebOverall; + double newV0 = num / hV[0]; + if (newV0 > 0 && Double.isFinite(newV0)) { + // V[0] (boilup) gets aggressive damping/cap: it's the master + // driver of the bottom-section T-cliff in T-pin strippers. + // Without this, vDamp=0.25 keeps V[0] stuck near the BP-Wilson + // seed (way too low) and the bottom section can't warm up. + double cap = 2.0 * Math.max(Vold[0], 1.0); + double dv = newV0 - V[0]; + if (dv > cap) { + dv = cap; + } else if (dv < -cap) { + dv = -cap; + } + V[0] = V[0] + 0.6 * dv; + if (V[0] < 1e-10) { + V[0] = 1e-10; + } + } + continue; + } + if (j == 0 && hasReboiler && boilupRatio > 0) { + V[0] = boilupRatio * L[0]; + continue; + } + // Skip V[N-1] update: with no condenser and T-pin reboiler, V[N-1] + // is the overhead product stream, fixed by overall MB closure + // (V[N-1] = totalFeed - L[0]). The tray-EB at j=N-1 lacks a + // closure equation (Q_reboiler is the free parameter, not V[N-1]) + // so its computed value runs to the cap. Leave V[N-1] at its + // current value (BP-Wilson seed already satisfies overall MB). + if (j == N - 1) { + continue; + } + // Internal tray energy balance: + // V_j*(hV_j-hL_j) = L_{j+1}*(hL_{j+1}-hL_j) + V_{j-1}*(hV_{j-1}-hL_j) + // + F_L*(hF_L-hL_j) + F_V*(hF_V-hL_j) + Q_j + double den = hV[j] - hL[j]; + if (Math.abs(den) < 1e-3) { + continue; + } + double num = 0; + if (j < N - 1) { + num += L[j + 1] * (hL[j + 1] - hL[j]); + } + if (j > 0) { + num += V[j - 1] * (hV[j - 1] - hL[j]); + } + num += feedLTotal[j] * (feedHL[j] - hL[j]); + num += feedVTotal[j] * (feedHV[j] - hL[j]); + num += Q[j]; + double newV = num / den; + if (newV > 0 && Double.isFinite(newV)) { + // Hard-clamp the step: at most ±25% of the prior V per outer pass + // (smaller than V[0]'s ±200% — internal trays must propagate + // gradually to avoid oscillation while V[0] catches up). + double cap = 0.25 * Vold[j]; + double dv = newV - V[j]; + if (dv > cap) { + dv = cap; + } else if (dv < -cap) { + dv = -cap; + } + V[j] = V[j] + 0.15 * dv; + if (V[j] < 1e-10) { + V[j] = 1e-10; + } + // Also bound to [0.25, 4.0] of prior V to prevent runaway. + if (V[j] < 0.25 * Vold[j]) { + V[j] = 0.25 * Vold[j]; + } else if (V[j] > 4.0 * Vold[j]) { + V[j] = 4.0 * Vold[j]; + } + // Hard physical cap: no internal vapor can exceed total feed + // (overall vapor mass balance: V[N-1] ≤ totalFeed). Apply to + // every tray since internal V can't exceed cumulative feed + // either. Leave a small margin for numerical headroom. + double Vcap = 0.98 * totalFeedMolesField; + if (V[j] > Vcap) { + V[j] = Vcap; + } + } } // Rebuild L from total mass balance (bottom-up cumulative): @@ -3525,53 +3580,55 @@ private void runBostonSullivanRefinement() { // L[j+1] = L[j] + V[j] - V[j-1] - Fmol[j] (rearranged tray-j MB) // starting from L[0] from overall MB closure. if (hasReboiler && useOverallMBClosure) { - L[0] = Math.max(totalFeedMolesField - V[N - 1], totalFeedMolesField * 0.001); + L[0] = Math.max(totalFeedMolesField - V[N - 1], totalFeedMolesField * 0.001); } for (int j = 0; j < N - 1; j++) { - // tray-j MB: V[j-1] + L[j+1] + F[j] = V[j] + L[j] - // -> L[j+1] = L[j] + V[j] - (j>0 ? V[j-1] : 0) - F[j] - double Vprev = (j > 0) ? V[j - 1] : 0.0; - double newLjp1 = L[j] + V[j] - Vprev - Fmol[j]; - if (newLjp1 < 1e-10) { - newLjp1 = 1e-10; - } - L[j + 1] = L[j + 1] + vDamp * (newLjp1 - L[j + 1]); - if (L[j + 1] < 1e-10) { - L[j + 1] = 1e-10; - } + // tray-j MB: V[j-1] + L[j+1] + F[j] = V[j] + L[j] + // -> L[j+1] = L[j] + V[j] - (j>0 ? V[j-1] : 0) - F[j] + double Vprev = (j > 0) ? V[j - 1] : 0.0; + double newLjp1 = L[j] + V[j] - Vprev - Fmol[j]; + if (newLjp1 < 1e-10) { + newLjp1 = 1e-10; + } + L[j + 1] = L[j + 1] + vDamp * (newLjp1 - L[j + 1]); + if (L[j + 1] < 1e-10) { + L[j + 1] = 1e-10; + } } // Recompute per-component liq/vap from current x,y and new V,L. for (int j = 0; j < N; j++) { - double Lsum = 0; - double Vsum = 0; - for (int i = 0; i < C; i++) { - Lsum += liq[j][i]; - Vsum += vap[j][i]; - } - for (int i = 0; i < C; i++) { - double xij = (Lsum > 1e-20) ? liq[j][i] / Lsum : 1.0 / C; - double yij = (Vsum > 1e-20) ? vap[j][i] / Vsum : 1.0 / C; - liq[j][i] = L[j] * xij; - vap[j][i] = V[j] * yij; - } + double Lsum = 0; + double Vsum = 0; + for (int i = 0; i < C; i++) { + Lsum += liq[j][i]; + Vsum += vap[j][i]; + } + for (int i = 0; i < C; i++) { + double xij = (Lsum > 1e-20) ? liq[j][i] / Lsum : 1.0 / C; + double yij = (Vsum > 1e-20) ? vap[j][i] / Vsum : 1.0 / C; + liq[j][i] = L[j] * xij; + vap[j][i] = V[j] * yij; + } } logger.info( - "BS outer {}: innerUsed={} maxDTinner={}K maxAlphaChange={} " - + "T[0]={}C T[mid]={}C T[N-1]={}C V[0]={} V[N-1]={}", - outer + 1, innerUsed, String.format("%.3f", maxDTinner), String.format("%.3e", maxAlphaChange), - String.format("%.2f", T[0] - 273.15), String.format("%.2f", T[N / 2] - 273.15), - String.format("%.2f", T[N - 1] - 273.15), String.format("%.1f", V[0]), String.format("%.1f", V[N - 1])); + "BS outer {}: innerUsed={} maxDTinner={}K maxAlphaChange={} " + + "T[0]={}C T[mid]={}C T[N-1]={}C V[0]={} V[N-1]={}", + outer + 1, innerUsed, String.format("%.3f", maxDTinner), + String.format("%.3e", maxAlphaChange), String.format("%.2f", T[0] - 273.15), + String.format("%.2f", T[N / 2] - 273.15), String.format("%.2f", T[N - 1] - 273.15), + String.format("%.1f", V[0]), String.format("%.1f", V[N - 1])); } - logger.info("BS done: T[0]={}C->{}C T[N-1]={}C->{}C", String.format("%.2f", initialT0 - 273.15), - String.format("%.2f", T[0] - 273.15), String.format("%.2f", initialTtop - 273.15), - String.format("%.2f", T[N - 1] - 273.15)); + logger.info("BS done: T[0]={}C->{}C T[N-1]={}C->{}C", + String.format("%.2f", initialT0 - 273.15), String.format("%.2f", T[0] - 273.15), + String.format("%.2f", initialTtop - 273.15), String.format("%.2f", T[N - 1] - 273.15)); } /** - * Thomas algorithm for tridiagonal systems {@code a[j] * x[j-1] + b[j] * x[j] + c[j] * x[j+1] = d[j]}. + * Thomas algorithm for tridiagonal systems + * {@code a[j] * x[j-1] + b[j] * x[j] + c[j] * x[j+1] = d[j]}. * * @param a sub-diagonal (a[0] ignored), length n * @param b main diagonal, length n @@ -3592,7 +3649,7 @@ private void thomas(double[] a, double[] b, double[] c, double[] d, double[] x) for (int j = 1; j < n; j++) { double m = b[j] - a[j] * cp[j - 1]; if (Math.abs(m) < 1e-30) { - m = (m >= 0) ? 1e-30 : -1e-30; + m = (m >= 0) ? 1e-30 : -1e-30; } cp[j] = (j < n - 1) ? c[j] / m : 0; dp[j] = (d[j] - a[j] * dp[j - 1]) / m; @@ -3604,8 +3661,9 @@ private void thomas(double[] a, double[] b, double[] c, double[] d, double[] x) } /** - * Wilson-K bubble-point temperature for a composition at a given pressure. Solves sum(z_i * K_i(T,P)) = 1 by Newton - * iteration. Used only as an initial-guess generator for the BP method T-profile. + * Wilson-K bubble-point temperature for a composition at a given pressure. Solves sum(z_i * + * K_i(T,P)) = 1 by Newton iteration. Used only as an initial-guess generator for the BP method + * T-profile. * * @param z molar composition * @param pBar pressure in bara @@ -3618,24 +3676,24 @@ private double wilsonBubbleTemperature(double[] z, double pBar, double tGuess) { double f = -1.0; double dfdt = 0.0; for (int i = 0; i < C; i++) { - ComponentInterface comp = referenceSystem.getPhase(0).getComponent(i); - double tc = comp.getTC(); - double pc = comp.getPC(); - double omega = comp.getAcentricFactor(); - double exponent = 5.37 * (1.0 + omega) * (1.0 - tc / t); - double k = (pc / pBar) * Math.exp(exponent); - double dkdt = k * 5.37 * (1.0 + omega) * tc / (t * t); - f += z[i] * k; - dfdt += z[i] * dkdt; + ComponentInterface comp = referenceSystem.getPhase(0).getComponent(i); + double tc = comp.getTC(); + double pc = comp.getPC(); + double omega = comp.getAcentricFactor(); + double exponent = 5.37 * (1.0 + omega) * (1.0 - tc / t); + double k = (pc / pBar) * Math.exp(exponent); + double dkdt = k * 5.37 * (1.0 + omega) * tc / (t * t); + f += z[i] * k; + dfdt += z[i] * dkdt; } if (Math.abs(f) < 1e-8 || Math.abs(dfdt) < 1e-30) { - break; + break; } double dt = -f / dfdt; if (dt > 30.0) - dt = 30.0; + dt = 30.0; if (dt < -30.0) - dt = -30.0; + dt = -30.0; t += dt; t = Math.max(100.0, Math.min(1000.0, t)); } @@ -3643,8 +3701,9 @@ private double wilsonBubbleTemperature(double[] z, double pBar, double tGuess) { } /** - * Wilson-K dew-point temperature for a composition at a given pressure. Solves sum(z_i / K_i(T,P)) = 1 by Newton - * iteration. Used only as an initial-guess generator for the BP method T-profile. + * Wilson-K dew-point temperature for a composition at a given pressure. Solves sum(z_i / + * K_i(T,P)) = 1 by Newton iteration. Used only as an initial-guess generator for the BP method + * T-profile. * * @param z molar composition * @param pBar pressure in bara @@ -3657,24 +3716,24 @@ private double wilsonDewTemperature(double[] z, double pBar, double tGuess) { double f = -1.0; double dfdt = 0.0; for (int i = 0; i < C; i++) { - ComponentInterface comp = referenceSystem.getPhase(0).getComponent(i); - double tc = comp.getTC(); - double pc = comp.getPC(); - double omega = comp.getAcentricFactor(); - double exponent = 5.37 * (1.0 + omega) * (1.0 - tc / t); - double k = (pc / pBar) * Math.exp(exponent); - double dkdt = k * 5.37 * (1.0 + omega) * tc / (t * t); - f += z[i] / k; - dfdt += -z[i] * dkdt / (k * k); + ComponentInterface comp = referenceSystem.getPhase(0).getComponent(i); + double tc = comp.getTC(); + double pc = comp.getPC(); + double omega = comp.getAcentricFactor(); + double exponent = 5.37 * (1.0 + omega) * (1.0 - tc / t); + double k = (pc / pBar) * Math.exp(exponent); + double dkdt = k * 5.37 * (1.0 + omega) * tc / (t * t); + f += z[i] / k; + dfdt += -z[i] * dkdt / (k * k); } if (Math.abs(f) < 1e-8 || Math.abs(dfdt) < 1e-30) { - break; + break; } double dt = -f / dfdt; if (dt > 30.0) - dt = 30.0; + dt = 30.0; if (dt < -30.0) - dt = -30.0; + dt = -30.0; t += dt; t = Math.max(100.0, Math.min(1000.0, t)); } @@ -3682,14 +3741,14 @@ private double wilsonDewTemperature(double[] z, double pBar, double tGuess) { } /** - * Compute fugacity coefficients for a single phase (liquid or vapor) at given composition, temperature and pressure - * using a forced single-phase EOS root. + * Compute fugacity coefficients for a single phase (liquid or vapor) at given composition, + * temperature and pressure using a forced single-phase EOS root. * *

- * Mirrors {@link #computeSinglePhaseEnthalpy} — sets one phase, forces the requested phase type (GAS or LIQUID), and - * reads the fugacity coefficients directly. This is the correct way to evaluate K-values inside a column MESH - * residual, because K_i = phi_L(x) / phi_V(y) must be evaluated at the tray's actual compositions x and y, NOT at a - * flash composition. + * Mirrors {@link #computeSinglePhaseEnthalpy} — sets one phase, forces the requested phase type + * (GAS or LIQUID), and reads the fugacity coefficients directly. This is the correct way to + * evaluate K-values inside a column MESH residual, because K_i = phi_L(x) / phi_V(y) must be + * evaluated at the tray's actual compositions x and y, NOT at a flash composition. *

* * @param composition mole fractions (length C, normalised by caller) @@ -3699,8 +3758,8 @@ private double wilsonDewTemperature(double[] z, double pBar, double tGuess) { * @param phiOut output array of length C to be populated with phi_i * @return true on success, false if the forced-phase calculation failed */ - private boolean computeSinglePhaseFugacityCoefficients(double[] composition, double tempK, double pressBar, - boolean isVapor, double[] phiOut) { + private boolean computeSinglePhaseFugacityCoefficients(double[] composition, double tempK, + double pressBar, boolean isVapor, double[] phiOut) { try { SystemInterface sys = referenceSystem.clone(); sys.setTemperature(tempK); @@ -3713,22 +3772,23 @@ private boolean computeSinglePhaseFugacityCoefficients(double[] composition, dou sys.setForcePhaseTypes(true); sys.init(2); for (int i = 0; i < C; i++) { - double phi = sys.getPhase(0).getComponent(i).getFugacityCoefficient(); - if (!(phi > 0.0) || Double.isNaN(phi) || Double.isInfinite(phi)) { - return false; - } - phiOut[i] = phi; + double phi = sys.getPhase(0).getComponent(i).getFugacityCoefficient(); + if (!(phi > 0.0) || Double.isNaN(phi) || Double.isInfinite(phi)) { + return false; + } + phiOut[i] = phi; } return true; } catch (Exception e) { - logger.debug("Single-phase fugacity failed for {} phase at T={}K P={}bar", isVapor ? "vapor" : "liquid", tempK, - pressBar); + logger.debug("Single-phase fugacity failed for {} phase at T={}K P={}bar", + isVapor ? "vapor" : "liquid", tempK, pressBar); return false; } } /** - * Wilson correlation for K-values, used as a fallback / initialisation when an EOS evaluation fails. + * Wilson correlation for K-values, used as a fallback / initialisation when an EOS evaluation + * fails. * * @param i component index in the reference system * @param tempK temperature in Kelvin @@ -3743,7 +3803,8 @@ private double wilsonK(int i, double tempK, double pressBar) { return (Pc / pressBar) * Math.exp(5.37 * (1.0 + omega) * (1.0 - Tc / tempK)); } - private double computeSinglePhaseEnthalpy(double[] composition, double tempK, double pressBar, boolean isVapor) { + private double computeSinglePhaseEnthalpy(double[] composition, double tempK, double pressBar, + boolean isVapor) { try { SystemInterface sys = referenceSystem.clone(); sys.setTemperature(tempK); @@ -3760,10 +3821,11 @@ private double computeSinglePhaseEnthalpy(double[] composition, double tempK, do sys.setPhaseType(0, isVapor ? PhaseType.GAS : PhaseType.LIQUID); sys.setForcePhaseTypes(true); sys.init(2); - return sys.getPhase(0).getEnthalpy() / Math.max(sys.getPhase(0).getNumberOfMolesInPhase(), 1e-20); + return sys.getPhase(0).getEnthalpy() + / Math.max(sys.getPhase(0).getNumberOfMolesInPhase(), 1e-20); } catch (Exception e) { - logger.debug("Single-phase enthalpy failed for {} phase at T={}K P={}bar", isVapor ? "vapor" : "liquid", tempK, - pressBar); + logger.debug("Single-phase enthalpy failed for {} phase at T={}K P={}bar", + isVapor ? "vapor" : "liquid", tempK, pressBar); return 0.0; } } @@ -3772,7 +3834,8 @@ private double computeSinglePhaseEnthalpy(double[] composition, double tempK, do * Get the value of a variable by tray and intra-tray index. * * @param tray tray index - * @param k intra-tray variable index: 0..C-1 are liquid flows, C is temperature, C+1 is vapor flow + * @param k intra-tray variable index: 0..C-1 are liquid flows, C is temperature, C+1 is vapor + * flow * @return variable value */ private double getVariable(int tray, int k) { @@ -3806,8 +3869,8 @@ private void setVariable(int tray, int k, double value) { * Compute the overall mass balance error as a fraction of total feed. * *

- * The overall mass balance is: V[N-1] + L[0] = total_feed. This method returns the absolute relative error |V[N-1] + - * L[0] - feed| / feed. + * The overall mass balance is: V[N-1] + L[0] = total_feed. This method returns the absolute + * relative error |V[N-1] + L[0] - feed| / feed. *

* * @return mass balance error as a fraction (0.005 = 0.5%) @@ -3816,7 +3879,7 @@ private double computeMassBalanceError() { double totalFeedFlow = 0; for (int j = 0; j < N; j++) { for (int i = 0; i < C; i++) { - totalFeedFlow += feedLiq[j][i] + feedVap[j][i]; + totalFeedFlow += feedLiq[j][i] + feedVap[j][i]; } } double topFlow = V[N - 1]; // vapor leaving top tray @@ -3825,14 +3888,17 @@ private double computeMassBalanceError() { } /** - * Maximum per-tray, per-component molar imbalance, expressed relative to the total feed molar flow. + * Maximum per-tray, per-component molar imbalance, expressed relative to the total feed molar + * flow. * *

- * For each tray j and component i this evaluates the MESH M-residual {@code feed_{j,i} + liq_{j+1,i} + vap_{j-1,i} - - * liq_{j,i} - vap_{j,i}} (the same equation that {@link #computeResidual()} packs into the F vector), takes the - * absolute value, and divides by the total feed flow. Returning the worst value across all (j, i) pairs exposes - * leakage on individual species, which the scalar {@link #computeMassBalanceError()} (overall column closure) and the - * L2 norm {@code ||F||} can both mask. + * For each tray j and component i this evaluates the MESH M-residual + * {@code feed_{j,i} + liq_{j+1,i} + vap_{j-1,i} - + * liq_{j,i} - vap_{j,i}} (the same equation that {@link #computeResidual()} packs into the F + * vector), takes the absolute value, and divides by the total feed flow. Returning the worst + * value across all (j, i) pairs exposes leakage on individual species, which the scalar + * {@link #computeMassBalanceError()} (overall column closure) and the L2 norm {@code ||F||} can + * both mask. *

* * @return maximum relative component imbalance (1.0e-3 = 0.1%) @@ -3841,25 +3907,25 @@ private double computeMaxComponentImbalance() { double totalFeedFlow = 0.0; for (int j = 0; j < N; j++) { for (int i = 0; i < C; i++) { - totalFeedFlow += feedLiq[j][i] + feedVap[j][i]; + totalFeedFlow += feedLiq[j][i] + feedVap[j][i]; } } double denom = Math.max(totalFeedFlow, 1.0e-20); double worst = 0.0; for (int j = 0; j < N; j++) { for (int i = 0; i < C; i++) { - double mij = liq[j][i] + vap[j][i]; - if (j < N - 1) { - mij -= liq[j + 1][i]; - } - if (j > 0) { - mij -= vap[j - 1][i]; - } - mij -= feedLiq[j][i] + feedVap[j][i]; - double rel = Math.abs(mij) / denom; - if (rel > worst) { - worst = rel; - } + double mij = liq[j][i] + vap[j][i]; + if (j < N - 1) { + mij -= liq[j + 1][i]; + } + if (j > 0) { + mij -= vap[j - 1][i]; + } + mij -= feedLiq[j][i] + feedVap[j][i]; + double rel = Math.abs(mij) / denom; + if (rel > worst) { + worst = rel; + } } } return worst; @@ -3869,13 +3935,14 @@ private double computeMaxComponentImbalance() { * Apply a trust-region clamp to the Newton direction dx (in-place). * *

- * Limits each variable's per-iteration change to a physically reasonable magnitude to prevent Newton from leaving the - * basin of attraction. The whole step vector is scaled by the smallest acceptable ratio so the descent direction is - * preserved. + * Limits each variable's per-iteration change to a physically reasonable magnitude to prevent + * Newton from leaving the basin of attraction. The whole step vector is scaled by the smallest + * acceptable ratio so the descent direction is preserved. *

* *

- * Bounds: |dT| ≤ 10 K, |dV| ≤ 0.5*V + 0.05*flowScale, |d(liq_ij)| ≤ 0.5*liq_ij + 1e-3*flowScale. + * Bounds: |dT| ≤ 10 K, |dV| ≤ 0.5*V + 0.05*flowScale, |d(liq_ij)| ≤ 0.5*liq_ij + + * 1e-3*flowScale. *

* * @param dx the Newton step (will be scaled in place) @@ -3887,27 +3954,27 @@ private double applyTrustRegion(double[] dx) { for (int j = 0; j < N; j++) { int base = j * varsPerTray; for (int i = 0; i < C; i++) { - double cap = 0.5 * liq[j][i] + 1.0e-3 * flowScale; - double step = Math.abs(dx[base + i]); - if (step > cap && cap > 0.0) { - scale = Math.min(scale, cap / step); - } + double cap = 0.5 * liq[j][i] + 1.0e-3 * flowScale; + double step = Math.abs(dx[base + i]); + if (step > cap && cap > 0.0) { + scale = Math.min(scale, cap / step); + } } if (Double.isNaN(fixedTemperature[j])) { - double step = Math.abs(dx[base + C]); - if (step > maxDT) { - scale = Math.min(scale, maxDT / step); - } + double step = Math.abs(dx[base + C]); + if (step > maxDT) { + scale = Math.min(scale, maxDT / step); + } } double capV = 0.5 * V[j] + 0.05 * flowScale; double stepV = Math.abs(dx[base + C + 1]); if (stepV > capV && capV > 0.0) { - scale = Math.min(scale, capV / stepV); + scale = Math.min(scale, capV / stepV); } } if (scale < 1.0) { for (int k = 0; k < dx.length; k++) { - dx[k] *= scale; + dx[k] *= scale; } } return scale; @@ -3917,7 +3984,8 @@ private double applyTrustRegion(double[] dx) { * Apply the Newton update dx with step size alpha to the current state. * *

- * Includes bounds enforcement: temperatures must remain positive, component flows must remain non-negative. + * Includes bounds enforcement: temperatures must remain positive, component flows must remain + * non-negative. *

* * @param dx correction vector @@ -3929,15 +3997,15 @@ private void applyUpdate(double[] dx, double alpha) { // Update liquid component flows with non-negativity bounds for (int i = 0; i < C; i++) { - double newVal = liq[j][i] - alpha * dx[base + i]; - liq[j][i] = Math.max(newVal, 1e-20); + double newVal = liq[j][i] - alpha * dx[base + i]; + liq[j][i] = Math.max(newVal, 1e-20); } // Update temperature with physical bounds (skip if fixed) if (Double.isNaN(fixedTemperature[j])) { - double newT = T[j] - alpha * dx[base + C]; - T[j] = Math.max(newT, 100.0); // minimum 100K - T[j] = Math.min(T[j], 1000.0); // maximum 1000K + double newT = T[j] - alpha * dx[base + C]; + T[j] = Math.max(newT, 100.0); // minimum 100K + T[j] = Math.min(T[j], 1000.0); // maximum 1000K } // Update vapor flow @@ -4004,23 +4072,23 @@ private double lineSearch(double[] dx, double currentNorm) { for (int bt = 0; bt < maxBacktrack; bt++) { // Trial update for (int j = 0; j < N; j++) { - for (int i = 0; i < C; i++) { - liq[j][i] = Math.max(saveLiq[j][i] - alpha * dx[j * varsPerTray + i], 1e-20); - } - if (Double.isNaN(fixedTemperature[j])) { - T[j] = Math.max(saveT[j] - alpha * dx[j * varsPerTray + C], 100.0); - T[j] = Math.min(T[j], 1000.0); - } - V[j] = Math.max(saveV[j] - alpha * dx[j * varsPerTray + C + 1], 0.0); + for (int i = 0; i < C; i++) { + liq[j][i] = Math.max(saveLiq[j][i] - alpha * dx[j * varsPerTray + i], 1e-20); + } + if (Double.isNaN(fixedTemperature[j])) { + T[j] = Math.max(saveT[j] - alpha * dx[j * varsPerTray + C], 100.0); + T[j] = Math.min(T[j], 1000.0); + } + V[j] = Math.max(saveV[j] - alpha * dx[j * varsPerTray + C + 1], 0.0); } evaluateThermo(); double[] Ftrial = computeResidual(); - double trialNorm = vectorNorm(Ftrial); + double trialNorm = LinearAlgebraOps.vectorNorm(Ftrial); if (trialNorm < (1.0 - c * alpha) * currentNorm || alpha < 0.01) { - bestAlpha = alpha; - break; + bestAlpha = alpha; + break; } alpha *= rho; @@ -4042,8 +4110,8 @@ private double lineSearch(double[] dx, double currentNorm) { * Solve the block-tridiagonal linear system J * dx = -F. * *

- * The Jacobian J is block-tridiagonal with block size (C+2). Blocks: A_j (sub-diagonal, coupling to tray j-1), B_j - * (diagonal, tray j), C_j (super-diagonal, coupling to tray j+1). + * The Jacobian J is block-tridiagonal with block size (C+2). Blocks: A_j (sub-diagonal, coupling + * to tray j-1), B_j (diagonal, tray j), C_j (super-diagonal, coupling to tray j+1). *

* *

@@ -4069,28 +4137,28 @@ private double[] solveBlockTridiagonal(double[][] J, double[] F) { // Diagonal block: columns from tray j for (int r = 0; r < m; r++) { - for (int c = 0; c < m; c++) { - Bdiag[j][r][c] = J[rowBase + r][j * m + c]; - } - rhs[j][r] = F[rowBase + r]; + for (int c = 0; c < m; c++) { + Bdiag[j][r][c] = J[rowBase + r][j * m + c]; + } + rhs[j][r] = F[rowBase + r]; } // Sub-diagonal block: columns from tray j-1 if (j > 0) { - for (int r = 0; r < m; r++) { - for (int c = 0; c < m; c++) { - Asub[j][r][c] = J[rowBase + r][(j - 1) * m + c]; - } - } + for (int r = 0; r < m; r++) { + for (int c = 0; c < m; c++) { + Asub[j][r][c] = J[rowBase + r][(j - 1) * m + c]; + } + } } // Super-diagonal block: columns from tray j+1 if (j < N - 1) { - for (int r = 0; r < m; r++) { - for (int c = 0; c < m; c++) { - Csup[j][r][c] = J[rowBase + r][(j + 1) * m + c]; - } - } + for (int r = 0; r < m; r++) { + for (int c = 0; c < m; c++) { + Csup[j][r][c] = J[rowBase + r][(j + 1) * m + c]; + } + } } } @@ -4109,7 +4177,7 @@ private double[] solveBlockTridiagonal(double[][] J, double[] F) { // Compute: fac = A_j * inv(B'_{j-1}) double[][] invBprev = invertBlock(Bprime[j - 1]); if (invBprev == null) { - return null; // singular + return null; // singular } double[][] fac = multiplyBlocks(Asub[j], invBprev); @@ -4117,15 +4185,15 @@ private double[] solveBlockTridiagonal(double[][] J, double[] F) { // B'_j = B_j - fac * C_{j-1} double[][] facC = multiplyBlocks(fac, Csup[j - 1]); for (int r = 0; r < m; r++) { - for (int c = 0; c < m; c++) { - Bprime[j][r][c] = Bdiag[j][r][c] - facC[r][c]; - } + for (int c = 0; c < m; c++) { + Bprime[j][r][c] = Bdiag[j][r][c] - facC[r][c]; + } } // rhs'_j = rhs_j - fac * rhs'_{j-1} double[] facRhs = multiplyBlockVec(fac, rhsPrime[j - 1]); for (int r = 0; r < m; r++) { - rhsPrime[j][r] = rhs[j][r] - facRhs[r]; + rhsPrime[j][r] = rhs[j][r] - facRhs[r]; } } @@ -4144,11 +4212,11 @@ private double[] solveBlockTridiagonal(double[][] J, double[] F) { double[] CxNext = multiplyBlockVec(Csup[j], xBlocks[j + 1]); double[] rhsAdj = new double[m]; for (int r = 0; r < m; r++) { - rhsAdj[r] = rhsPrime[j][r] - CxNext[r]; + rhsAdj[r] = rhsPrime[j][r] - CxNext[r]; } double[][] invBj = invertBlock(Bprime[j]); if (invBj == null) { - return null; + return null; } xBlocks[j] = multiplyBlockVec(invBj, rhsAdj); } @@ -4186,37 +4254,37 @@ private double[][] invertBlock(double[][] A) { int maxRow = col; double maxVal = Math.abs(aug[col][col]); for (int row = col + 1; row < m; row++) { - if (Math.abs(aug[row][col]) > maxVal) { - maxVal = Math.abs(aug[row][col]); - maxRow = row; - } + if (Math.abs(aug[row][col]) > maxVal) { + maxVal = Math.abs(aug[row][col]); + maxRow = row; + } } if (maxVal < 1e-30) { - return null; // singular + return null; // singular } // Swap rows if (maxRow != col) { - double[] tmp = aug[col]; - aug[col] = aug[maxRow]; - aug[maxRow] = tmp; + double[] tmp = aug[col]; + aug[col] = aug[maxRow]; + aug[maxRow] = tmp; } // Scale pivot row double pivot = aug[col][col]; for (int k = 0; k < 2 * m; k++) { - aug[col][k] /= pivot; + aug[col][k] /= pivot; } // Eliminate column for (int row = 0; row < m; row++) { - if (row != col) { - double factor = aug[row][col]; - for (int k = 0; k < 2 * m; k++) { - aug[row][k] -= factor * aug[col][k]; - } - } + if (row != col) { + double factor = aug[row][col]; + for (int k = 0; k < 2 * m; k++) { + aug[row][k] -= factor * aug[col][k]; + } + } } } @@ -4241,11 +4309,11 @@ private double[][] multiplyBlocks(double[][] A, double[][] B) { double[][] result = new double[m][m]; for (int i = 0; i < m; i++) { for (int j = 0; j < m; j++) { - double sum = 0; - for (int k = 0; k < m; k++) { - sum += A[i][k] * B[k][j]; - } - result[i][j] = sum; + double sum = 0; + for (int k = 0; k < m; k++) { + sum += A[i][k] * B[k][j]; + } + result[i][j] = sum; } } return result; @@ -4264,7 +4332,7 @@ private double[] multiplyBlockVec(double[][] A, double[] v) { for (int i = 0; i < m; i++) { double sum = 0; for (int k = 0; k < m; k++) { - sum += A[i][k] * v[k]; + sum += A[i][k] * v[k]; } result[i] = sum; } @@ -4294,27 +4362,27 @@ private double[] solveDenseLU(double[][] J, double[] F) { int maxRow = col; double maxVal = Math.abs(aug[col][col]); for (int row = col + 1; row < n; row++) { - if (Math.abs(aug[row][col]) > maxVal) { - maxVal = Math.abs(aug[row][col]); - maxRow = row; - } + if (Math.abs(aug[row][col]) > maxVal) { + maxVal = Math.abs(aug[row][col]); + maxRow = row; + } } if (maxVal < 1e-30) { - return null; + return null; } if (maxRow != col) { - double[] tmp = aug[col]; - aug[col] = aug[maxRow]; - aug[maxRow] = tmp; + double[] tmp = aug[col]; + aug[col] = aug[maxRow]; + aug[maxRow] = tmp; } for (int row = col + 1; row < n; row++) { - double factor = aug[row][col] / aug[col][col]; - for (int k = col; k <= n; k++) { - aug[row][k] -= factor * aug[col][k]; - } + double factor = aug[row][col] / aug[col][col]; + for (int k = col; k <= n; k++) { + aug[row][k] -= factor * aug[col][k]; + } } } @@ -4323,7 +4391,7 @@ private double[] solveDenseLU(double[][] J, double[] F) { for (int i = n - 1; i >= 0; i--) { double sum = aug[i][n]; for (int j = i + 1; j < n; j++) { - sum -= aug[i][j] * dx[j]; + sum -= aug[i][j] * dx[j]; } dx[i] = sum / aug[i][i]; } @@ -4333,27 +4401,14 @@ private double[] solveDenseLU(double[][] J, double[] F) { return dx; } - /** - * Compute the L2 norm of a vector. - * - * @param v vector - * @return ||v||_2 - */ - private double vectorNorm(double[] v) { - double sum = 0; - for (double vi : v) { - sum += vi * vi; - } - return Math.sqrt(sum); - } - /** * Apply the converged solution back to the DistillationColumn trays. * *

- * For each tray, creates a two-phase thermo system at the converged T, P, and overall composition (from liquid + - * vapor flows), performs a TPflash, then extracts the gas phase via {@code phaseToSystem(0)} and the liquid phase via - * {@code phaseToSystem(1)}. The phase flow rates are set to V[j] and L[j] from the solver variables. + * For each tray, creates a two-phase thermo system at the converged T, P, and overall composition + * (from liquid + vapor flows), performs a TPflash, then extracts the gas phase via + * {@code phaseToSystem(0)} and the liquid phase via {@code phaseToSystem(1)}. The phase flow + * rates are set to V[j] and L[j] from the solver variables. *

* * @param id calculation identifier @@ -4368,36 +4423,36 @@ private void applyResultsToColumn(UUID id, int iterations, double finalNorm, lon // Recompute L[j] from solved liquid flows double sumLiq = 0; for (int i = 0; i < C; i++) { - sumLiq += liq[j][i]; + sumLiq += liq[j][i]; } L[j] = Math.max(sumLiq, 1e-20); // Liquid composition x double[] x = new double[C]; for (int i = 0; i < C; i++) { - x[i] = liq[j][i] / L[j]; + x[i] = liq[j][i] / L[j]; } // Vapor composition y from K-values double[] y = new double[C]; double sumY = 0; for (int i = 0; i < C; i++) { - y[i] = K[j][i] * x[i]; - sumY += y[i]; + y[i] = K[j][i] * x[i]; + sumY += y[i]; } if (sumY > 1e-20) { - for (int i = 0; i < C; i++) { - y[i] /= sumY; - } + for (int i = 0; i < C; i++) { + y[i] /= sumY; + } } else { - System.arraycopy(x, 0, y, 0, C); + System.arraycopy(x, 0, y, 0, C); } // Overall composition z from liquid + vapor component flows double totalMoles = L[j] + V[j]; double[] z = new double[C]; for (int i = 0; i < C; i++) { - z[i] = (liq[j][i] + vap[j][i]) / Math.max(totalMoles, 1e-20); + z[i] = (liq[j][i] + vap[j][i]) / Math.max(totalMoles, 1e-20); } // Create overall tray system and flash to get proper two-phase equilibrium @@ -4412,16 +4467,16 @@ private void applyResultsToColumn(UUID id, int iterations, double finalNorm, lon ThermodynamicOperations ops = new ThermodynamicOperations(traySystem); try { - ops.TPflash(); - traySystem.init(2); - traySystem.initPhysicalProperties(); + ops.TPflash(); + traySystem.init(2); + traySystem.initPhysicalProperties(); } catch (Exception e) { - logger.warn("Final TPflash failed on tray {}", j); + logger.warn("Final TPflash failed on tray {}", j); } // Set the tray's mixed stream if (tray.getOutletStream() != null) { - tray.getOutletStream().setThermoSystem(traySystem); + tray.getOutletStream().setThermoSystem(traySystem); } tray.setTemperature(T[j]); @@ -4452,19 +4507,21 @@ private void applyResultsToColumn(UUID id, int iterations, double finalNorm, lon liqSystem.setNumberOfPhases(1); liqSystem.init(0); liqSystem.init(2); - tray.setCachedLiquidOutStream(new neqsim.process.equipment.stream.Stream("liq_" + j, liqSystem)); + tray.setCachedLiquidOutStream( + new neqsim.process.equipment.stream.Stream("liq_" + j, liqSystem)); } // Compute mass balance error for diagnostics double totalFeedFlow = 0; for (int j = 0; j < N; j++) { for (int i = 0; i < C; i++) { - totalFeedFlow += feedLiq[j][i] + feedVap[j][i]; + totalFeedFlow += feedLiq[j][i] + feedVap[j][i]; } } double topFlow = V[N - 1]; // vapor leaving top tray double botFlow = L[0]; // liquid leaving bottom tray - double massBalErr = Math.abs(topFlow + botFlow - totalFeedFlow) / Math.max(totalFeedFlow, 1e-20); + double massBalErr = + Math.abs(topFlow + botFlow - totalFeedFlow) / Math.max(totalFeedFlow, 1e-20); double solveTime = (System.nanoTime() - startTime) / 1.0e9; @@ -4474,9 +4531,10 @@ private void applyResultsToColumn(UUID id, int iterations, double finalNorm, lon lastSolveTimeSeconds = solveTime; logger.info( - "Naphtali-Sandholm results: iter={}, ||F||={}, " - + "massBalErr={}, topFlow={}, botFlow={}, feedFlow={}, time={}s", - iterations, String.format("%.6e", finalNorm), String.format("%.6e", massBalErr), String.format("%.4f", topFlow), - String.format("%.4f", botFlow), String.format("%.4f", totalFeedFlow), String.format("%.2f", solveTime)); + "Naphtali-Sandholm results: iter={}, ||F||={}, " + + "massBalErr={}, topFlow={}, botFlow={}, feedFlow={}, time={}s", + iterations, String.format("%.6e", finalNorm), String.format("%.6e", massBalErr), + String.format("%.4f", topFlow), String.format("%.4f", botFlow), + String.format("%.4f", totalFeedFlow), String.format("%.2f", solveTime)); } } diff --git a/src/main/java/neqsim/process/equipment/distillation/RateBasedPackedColumn.java b/src/main/java/neqsim/process/equipment/distillation/RateBasedPackedColumn.java index 8fe23f8cbc..7dd6e2d83c 100644 --- a/src/main/java/neqsim/process/equipment/distillation/RateBasedPackedColumn.java +++ b/src/main/java/neqsim/process/equipment/distillation/RateBasedPackedColumn.java @@ -22,17 +22,18 @@ import neqsim.thermo.system.SystemInterface; import neqsim.thermodynamicoperations.ThermodynamicOperations; import neqsim.util.ExcludeFromJacocoGeneratedReport; +import neqsim.util.math.LinearAlgebraOps; import neqsim.util.validation.ValidationResult; /** * Counter-current rate-based packed column for non-reactive absorption and stripping. * *

- * The column is divided into axial segments. In each segment, NeqSim phase-equilibrium calculations provide the - * interfacial equilibrium driving force, {@link PhysicalProperties} provides effective diffusivities, and - * {@link PackingHydraulicsCalculator} provides packing hydraulics, wetted area, and film mass-transfer coefficients. - * Component transfer is bidirectional, so the same equipment can model gas-to-liquid absorption and liquid-to-gas - * stripping. + * The column is divided into axial segments. In each segment, NeqSim phase-equilibrium calculations + * provide the interfacial equilibrium driving force, {@link PhysicalProperties} provides effective + * diffusivities, and {@link PackingHydraulicsCalculator} provides packing hydraulics, wetted area, + * and film mass-transfer coefficients. Component transfer is bidirectional, so the same equipment + * can model gas-to-liquid absorption and liquid-to-gas stripping. *

* * @author NeqSim @@ -103,7 +104,8 @@ public class RateBasedPackedColumn extends ProcessEquipmentBaseClass { private double massTransferCorrectionFactor = 1.0; /** Packing specification used in all segments. */ - private PackingSpecification packingSpecification = PackingSpecificationLibrary.getOrDefault("Pall-Ring-50"); + private PackingSpecification packingSpecification = + PackingSpecificationLibrary.getOrDefault("Pall-Ring-50"); /** Optional transfer component whitelist. Empty means all components are considered. */ private final List transferComponents = new ArrayList(); @@ -242,7 +244,8 @@ public RateBasedPackedColumn(String name) { * @param gasInStream gas inlet stream entering the bottom of the packing * @param liquidInStream liquid inlet stream entering the top of the packing */ - public RateBasedPackedColumn(String name, StreamInterface gasInStream, StreamInterface liquidInStream) { + public RateBasedPackedColumn(String name, StreamInterface gasInStream, + StreamInterface liquidInStream) { super(name); setGasInStream(gasInStream); setLiquidInStream(liquidInStream); @@ -475,7 +478,8 @@ public double getConvergenceTolerance() { */ public void setMaxTransferFractionPerSegment(double fraction) { if (!Double.isFinite(fraction) || fraction <= 0.0 || fraction > 1.0) { - throw new IllegalArgumentException("fraction must be greater than zero and less than or equal to one"); + throw new IllegalArgumentException( + "fraction must be greater than zero and less than or equal to one"); } this.maxTransferFractionPerSegment = fraction; } @@ -678,7 +682,8 @@ public double getHeatTransferCorrectionFactor() { */ public void setMaxHeatTransferFractionPerSegment(double fraction) { if (!Double.isFinite(fraction) || fraction <= 0.0 || fraction > 1.0) { - throw new IllegalArgumentException("fraction must be greater than zero and less than or equal to one"); + throw new IllegalArgumentException( + "fraction must be greater than zero and less than or equal to one"); } this.maxHeatTransferFractionPerSegment = fraction; } @@ -859,7 +864,7 @@ public void setTransferComponents(String... componentNames) { } for (int i = 0; i < componentNames.length; i++) { if (componentNames[i] != null && !componentNames[i].trim().isEmpty()) { - transferComponents.add(componentNames[i].trim()); + transferComponents.add(componentNames[i].trim()); } } } @@ -965,32 +970,33 @@ public ValidationResult validateSetup() { } if (gasInStream == null) { result.addError("gasInStream", "No gas inlet stream connected", - "Call setGasInStream(stream) before running the column"); + "Call setGasInStream(stream) before running the column"); } else if (gasInStream.getThermoSystem() == null) { result.addError("gasInStream", "Gas inlet stream has no thermodynamic system", - "Create the gas inlet stream with a valid fluid"); + "Create the gas inlet stream with a valid fluid"); } if (liquidInStream == null) { result.addError("liquidInStream", "No liquid inlet stream connected", - "Call setLiquidInStream(stream) before running the column"); + "Call setLiquidInStream(stream) before running the column"); } else if (liquidInStream.getThermoSystem() == null) { result.addError("liquidInStream", "Liquid inlet stream has no thermodynamic system", - "Create the liquid inlet stream with a valid fluid"); + "Create the liquid inlet stream with a valid fluid"); } if (columnDiameter <= 0.0) { result.addError("columnDiameter", "Column diameter must be positive", - "Set column diameter in metres with setColumnDiameter"); + "Set column diameter in metres with setColumnDiameter"); } if (packedHeight < 0.0) { result.addError("packedHeight", "Packed height can not be negative", - "Set a non-negative packed height in metres"); + "Set a non-negative packed height in metres"); } if (numberOfSegments < 1) { - result.addError("numberOfSegments", "At least one segment is required", "Set numberOfSegments to one or more"); + result.addError("numberOfSegments", "At least one segment is required", + "Set numberOfSegments to one or more"); } if (packingSpecification == null) { result.addError("packingSpecification", "No packing specification configured", - "Use setPackingType or setPackingSpecification"); + "Use setPackingType or setPackingSpecification"); } return result; } @@ -1003,7 +1009,7 @@ public ValidationResult validateSetup() { @Override public String toJson() { return new GsonBuilder().setPrettyPrinting().serializeSpecialFloatingPointValues().create() - .toJson(new ColumnReport(this)); + .toJson(new ColumnReport(this)); } /** @@ -1014,7 +1020,8 @@ public String toJson() { private void validateRuntimeSetup() { ValidationResult result = validateSetup(); if (!result.isValid()) { - throw new IllegalStateException("RateBasedPackedColumn setup is invalid: " + result.toString()); + throw new IllegalStateException( + "RateBasedPackedColumn setup is invalid: " + result.toString()); } } @@ -1025,7 +1032,8 @@ private void validateRuntimeSetup() { * @param liquidIn liquid inlet system * @return converged counter-current solution */ - private CounterCurrentSolution solveCounterCurrentProfile(SystemInterface gasIn, SystemInterface liquidIn) { + private CounterCurrentSolution solveCounterCurrentProfile(SystemInterface gasIn, + SystemInterface liquidIn) { if (columnSolver == ColumnSolver.EQUATION_ORIENTED && packedHeight > 0.0) { return solveEquationOrientedProfile(gasIn, liquidIn); } @@ -1039,7 +1047,8 @@ private CounterCurrentSolution solveCounterCurrentProfile(SystemInterface gasIn, * @param liquidIn liquid inlet system * @return converged counter-current solution */ - private CounterCurrentSolution solveFixedPointProfile(SystemInterface gasIn, SystemInterface liquidIn) { + private CounterCurrentSolution solveFixedPointProfile(SystemInterface gasIn, + SystemInterface liquidIn) { resetColumnResidualDiagnostics(); List liquidEntering = initializeLiquidProfile(liquidIn); SystemInterface previousGasOutlet = null; @@ -1047,13 +1056,13 @@ private CounterCurrentSolution solveFixedPointProfile(SystemInterface gasIn, Sys CounterCurrentSolution solution = null; for (int iteration = 1; iteration <= maxIterations; iteration++) { solution = runOneProfileIteration(gasIn, liquidIn, liquidEntering); - double residual = calculateOutletResidual(previousGasOutlet, solution.gasOutlet, previousLiquidOutlet, - solution.liquidOutlet); + double residual = calculateOutletResidual(previousGasOutlet, solution.gasOutlet, + previousLiquidOutlet, solution.liquidOutlet); lastIterationCount = iteration; lastConvergenceResidual = residual; if (residual <= convergenceTolerance || packedHeight == 0.0) { - acceptSolution(solution); - return solution; + acceptSolution(solution); + return solution; } previousGasOutlet = solution.gasOutlet.clone(); previousLiquidOutlet = solution.liquidOutlet.clone(); @@ -1081,7 +1090,8 @@ private void resetColumnResidualDiagnostics() { * @param liquidIn liquid inlet system * @return equation-oriented counter-current solution */ - private CounterCurrentSolution solveEquationOrientedProfile(SystemInterface gasIn, SystemInterface liquidIn) { + private CounterCurrentSolution solveEquationOrientedProfile(SystemInterface gasIn, + SystemInterface liquidIn) { CounterCurrentSolution seed = solveFixedPointProfile(gasIn, liquidIn); List components = getTransferComponentList(gasIn, liquidIn); if (components.isEmpty()) { @@ -1096,7 +1106,8 @@ private CounterCurrentSolution solveEquationOrientedProfile(SystemInterface gasI unknowns = evaluation.unknowns; totalIterations += evaluation.iterations; } - evaluation = evaluateColumnResidual(gasIn, liquidIn, components, unknowns, 1.0, totalIterations); + evaluation = + evaluateColumnResidual(gasIn, liquidIn, components, unknowns, 1.0, totalIterations); lastColumnResidualNorm = evaluation.norm; lastColumnResidualIterations = totalIterations; lastGasComponentBalanceResidual = evaluation.maxGasComponentBalanceResidual; @@ -1113,23 +1124,26 @@ private CounterCurrentSolution solveEquationOrientedProfile(SystemInterface gasI * * @param seed seed profile solution * @param components active transfer components - * @return unknown vector containing segment fluxes, interface temperatures, and outlet temperatures + * @return unknown vector containing segment fluxes, interface temperatures, and outlet + * temperatures */ private double[] createColumnUnknowns(CounterCurrentSolution seed, List components) { int blockSize = columnUnknownBlockSize(components); double[] unknowns = new double[numberOfSegments * blockSize]; for (int segment = 0; segment < numberOfSegments; segment++) { - SegmentResult result = seed.segmentResults.get(Math.min(segment, seed.segmentResults.size() - 1)); + SegmentResult result = + seed.segmentResults.get(Math.min(segment, seed.segmentResults.size() - 1)); for (int componentIndex = 0; componentIndex < components.size(); componentIndex++) { - Double transfer = result.componentMoleTransfer.get(components.get(componentIndex)); - unknowns[columnFluxIndex(segment, componentIndex, components)] = transfer == null ? 0.0 - : transfer.doubleValue(); + Double transfer = result.componentMoleTransfer.get(components.get(componentIndex)); + unknowns[columnFluxIndex(segment, componentIndex, components)] = + transfer == null ? 0.0 : transfer.doubleValue(); } - unknowns[columnInterfaceTemperatureIndex(segment, components)] = finitePositive(result.interfaceTemperatureK, - 0.5 * (result.gasTemperatureK + result.liquidTemperatureK)); - unknowns[columnGasOutletTemperatureIndex(segment, components)] = finitePositive(result.gasTemperatureK, 300.0); - unknowns[columnLiquidOutletTemperatureIndex(segment, components)] = finitePositive(result.liquidTemperatureK, - 300.0); + unknowns[columnInterfaceTemperatureIndex(segment, components)] = finitePositive( + result.interfaceTemperatureK, 0.5 * (result.gasTemperatureK + result.liquidTemperatureK)); + unknowns[columnGasOutletTemperatureIndex(segment, components)] = + finitePositive(result.gasTemperatureK, 300.0); + unknowns[columnLiquidOutletTemperatureIndex(segment, components)] = + finitePositive(result.liquidTemperatureK, 300.0); } return unknowns; } @@ -1144,36 +1158,40 @@ private double[] createColumnUnknowns(CounterCurrentSolution seed, List * @param homotopyFactor continuation factor from zero to one * @return best residual evaluation found */ - private ColumnResidualEvaluation solveColumnResiduals(SystemInterface gasIn, SystemInterface liquidIn, - List components, double[] initialUnknowns, double homotopyFactor) { + private ColumnResidualEvaluation solveColumnResiduals(SystemInterface gasIn, + SystemInterface liquidIn, List components, double[] initialUnknowns, + double homotopyFactor) { double[] unknowns = clampColumnUnknowns(initialUnknowns, gasIn, liquidIn, components); - ColumnResidualEvaluation best = evaluateColumnResidual(gasIn, liquidIn, components, unknowns, homotopyFactor, 0); + ColumnResidualEvaluation best = + evaluateColumnResidual(gasIn, liquidIn, components, unknowns, homotopyFactor, 0); for (int iteration = 0; iteration < maxColumnResidualIterations; iteration++) { if (best.norm <= columnResidualTolerance) { - return best.withIterations(iteration); + return best.withIterations(iteration); } - Matrix step = calculateColumnResidualStep(gasIn, liquidIn, components, unknowns, best, homotopyFactor); + Matrix step = + calculateColumnResidualStep(gasIn, liquidIn, components, unknowns, best, homotopyFactor); if (step == null) { - return best.withIterations(iteration); + return best.withIterations(iteration); } boolean improved = false; double[] bestUnknowns = unknowns; ColumnResidualEvaluation bestCandidate = best; double damping = 1.0; for (int lineSearch = 0; lineSearch < 10; lineSearch++) { - double[] candidateUnknowns = applyColumnResidualStep(unknowns, step, damping, gasIn, liquidIn, components); - ColumnResidualEvaluation candidate = evaluateColumnResidual(gasIn, liquidIn, components, candidateUnknowns, - homotopyFactor, iteration + 1); - if (candidate.norm < bestCandidate.norm) { - bestUnknowns = candidateUnknowns; - bestCandidate = candidate; - improved = true; - break; - } - damping *= 0.5; + double[] candidateUnknowns = + applyColumnResidualStep(unknowns, step, damping, gasIn, liquidIn, components); + ColumnResidualEvaluation candidate = evaluateColumnResidual(gasIn, liquidIn, components, + candidateUnknowns, homotopyFactor, iteration + 1); + if (candidate.norm < bestCandidate.norm) { + bestUnknowns = candidateUnknowns; + bestCandidate = candidate; + improved = true; + break; + } + damping *= 0.5; } if (!improved) { - return best.withIterations(iteration); + return best.withIterations(iteration); } unknowns = bestUnknowns; best = bestCandidate; @@ -1192,8 +1210,9 @@ private ColumnResidualEvaluation solveColumnResiduals(SystemInterface gasIn, Sys * @param iterations iteration count represented by this evaluation * @return column residual evaluation */ - private ColumnResidualEvaluation evaluateColumnResidual(SystemInterface gasIn, SystemInterface liquidIn, - List components, double[] unknowns, double homotopyFactor, int iterations) { + private ColumnResidualEvaluation evaluateColumnResidual(SystemInterface gasIn, + SystemInterface liquidIn, List components, double[] unknowns, double homotopyFactor, + int iterations) { double[] boundedUnknowns = clampColumnUnknowns(unknowns, gasIn, liquidIn, components); ColumnState state = buildColumnState(gasIn, liquidIn, components, boundedUnknowns); List residuals = new ArrayList(); @@ -1208,87 +1227,105 @@ private ColumnResidualEvaluation evaluateColumnResidual(SystemInterface gasIn, S SystemInterface gas = state.gasEntering.get(segment).clone(); SystemInterface liquid = state.liquidEntering.get(segment).clone(); TransportSnapshot snapshot = calculateTransportSnapshot(gas, liquid, segmentHeight); - double interfaceTemperature = boundedUnknowns[columnInterfaceTemperatureIndex(segment, components)]; - InterfaceEquilibrium equilibrium = calculateInterfaceEquilibrium(gas, liquid, interfaceTemperature); + double interfaceTemperature = + boundedUnknowns[columnInterfaceTemperatureIndex(segment, components)]; + InterfaceEquilibrium equilibrium = + calculateInterfaceEquilibrium(gas, liquid, interfaceTemperature); Map componentTransfers = new LinkedHashMap(); double gasMassEnthalpy = 0.0; double liquidMassEnthalpy = 0.0; for (int componentIndex = 0; componentIndex < components.size(); componentIndex++) { - String component = components.get(componentIndex); - double transfer = boundedUnknowns[columnFluxIndex(segment, componentIndex, components)]; - double predictedTransfer = calculateUnboundedComponentTransfer(component, gas, liquid, snapshot, equilibrium) - * homotopyFactor; - predictedTransfer = limitTransfer(component, predictedTransfer, gas, liquid); - double fluxResidual = transfer - predictedTransfer; - residuals.add(Double.valueOf(fluxResidual / transferResidualScale(component, predictedTransfer, gas, liquid))); - maxFluxResidual = Math.max(maxFluxResidual, Math.abs(fluxResidual)); - componentTransfers.put(component, transfer); - gasMassEnthalpy += transfer * equilibrium.getGasMolarEnthalpy(component); - liquidMassEnthalpy += transfer * equilibrium.getLiquidMolarEnthalpy(component); + String component = components.get(componentIndex); + double transfer = boundedUnknowns[columnFluxIndex(segment, componentIndex, components)]; + double predictedTransfer = + calculateUnboundedComponentTransfer(component, gas, liquid, snapshot, equilibrium) + * homotopyFactor; + predictedTransfer = limitTransfer(component, predictedTransfer, gas, liquid); + double fluxResidual = transfer - predictedTransfer; + residuals.add(Double.valueOf( + fluxResidual / transferResidualScale(component, predictedTransfer, gas, liquid))); + maxFluxResidual = Math.max(maxFluxResidual, Math.abs(fluxResidual)); + componentTransfers.put(component, transfer); + gasMassEnthalpy += transfer * equilibrium.getGasMolarEnthalpy(component); + liquidMassEnthalpy += transfer * equilibrium.getLiquidMolarEnthalpy(component); } - double segmentVolume = Math.PI * columnDiameter * columnDiameter / 4.0 * packedHeight / numberOfSegments; + double segmentVolume = + Math.PI * columnDiameter * columnDiameter / 4.0 * packedHeight / numberOfSegments; double gasSensibleHeat = snapshot.gasHeatTransferCoefficient * segmentVolume - * (gas.getTemperature() - interfaceTemperature); + * (gas.getTemperature() - interfaceTemperature); double liquidSensibleHeat = snapshot.liquidHeatTransferCoefficient * segmentVolume - * (interfaceTemperature - liquid.getTemperature()); - double heatResidual = gasSensibleHeat + gasMassEnthalpy - liquidSensibleHeat - liquidMassEnthalpy; - residuals.add(Double.valueOf( - heatResidual / heatResidualScale(gasSensibleHeat, liquidSensibleHeat, gasMassEnthalpy, liquidMassEnthalpy))); + * (interfaceTemperature - liquid.getTemperature()); + double heatResidual = + gasSensibleHeat + gasMassEnthalpy - liquidSensibleHeat - liquidMassEnthalpy; + residuals.add(Double.valueOf(heatResidual / heatResidualScale(gasSensibleHeat, + liquidSensibleHeat, gasMassEnthalpy, liquidMassEnthalpy))); maxHeatResidual = Math.max(maxHeatResidual, Math.abs(heatResidual)); SystemInterface gasOutletTarget = state.gasLeaving.get(segment).clone(); SystemInterface liquidOutletTarget = state.liquidLeaving.get(segment).clone(); double gasTargetEnthalpy = gas.getEnthalpy() - gasSensibleHeat - gasMassEnthalpy; double liquidTargetEnthalpy = liquid.getEnthalpy() + liquidSensibleHeat + liquidMassEnthalpy; - double gasTargetTemperature = estimateTemperatureForTargetEnthalpy(gasOutletTarget, gasTargetEnthalpy); - double liquidTargetTemperature = estimateTemperatureForTargetEnthalpy(liquidOutletTarget, liquidTargetEnthalpy); - double gasTemperatureResidual = state.gasLeaving.get(segment).getTemperature() - gasTargetTemperature; - double liquidTemperatureResidual = state.liquidLeaving.get(segment).getTemperature() - liquidTargetTemperature; + double gasTargetTemperature = + estimateTemperatureForTargetEnthalpy(gasOutletTarget, gasTargetEnthalpy); + double liquidTargetTemperature = + estimateTemperatureForTargetEnthalpy(liquidOutletTarget, liquidTargetEnthalpy); + double gasTemperatureResidual = + state.gasLeaving.get(segment).getTemperature() - gasTargetTemperature; + double liquidTemperatureResidual = + state.liquidLeaving.get(segment).getTemperature() - liquidTargetTemperature; double gasEnthalpyResidual = state.gasLeaving.get(segment).getEnthalpy() - gasTargetEnthalpy; - double liquidEnthalpyResidual = state.liquidLeaving.get(segment).getEnthalpy() - liquidTargetEnthalpy; + double liquidEnthalpyResidual = + state.liquidLeaving.get(segment).getEnthalpy() - liquidTargetEnthalpy; residuals.add(Double.valueOf(gasTemperatureResidual / 10.0)); residuals.add(Double.valueOf(liquidTemperatureResidual / 10.0)); maxEnergyResidual = Math.max(maxEnergyResidual, - Math.max(Math.abs(gasEnthalpyResidual), Math.abs(liquidEnthalpyResidual))); + Math.max(Math.abs(gasEnthalpyResidual), Math.abs(liquidEnthalpyResidual))); for (int componentIndex = 0; componentIndex < components.size(); componentIndex++) { - String component = components.get(componentIndex); - double transfer = componentTransfers.get(component).doubleValue(); - double gasBalance = componentMoles(state.gasLeaving.get(segment), component) - - (componentMoles(gas, component) - transfer); - double liquidBalance = componentMoles(state.liquidLeaving.get(segment), component) - - (componentMoles(liquid, component) + transfer); - residuals.add(Double.valueOf(gasBalance / transferResidualScale(component, transfer, gas, liquid))); - residuals.add(Double.valueOf(liquidBalance / transferResidualScale(component, transfer, gas, liquid))); - maxGasBalanceResidual = Math.max(maxGasBalanceResidual, Math.abs(gasBalance)); - maxLiquidBalanceResidual = Math.max(maxLiquidBalanceResidual, Math.abs(liquidBalance)); + String component = components.get(componentIndex); + double transfer = componentTransfers.get(component).doubleValue(); + double gasBalance = componentMoles(state.gasLeaving.get(segment), component) + - (componentMoles(gas, component) - transfer); + double liquidBalance = componentMoles(state.liquidLeaving.get(segment), component) + - (componentMoles(liquid, component) + transfer); + residuals.add( + Double.valueOf(gasBalance / transferResidualScale(component, transfer, gas, liquid))); + residuals.add(Double + .valueOf(liquidBalance / transferResidualScale(component, transfer, gas, liquid))); + maxGasBalanceResidual = Math.max(maxGasBalanceResidual, Math.abs(gasBalance)); + maxLiquidBalanceResidual = Math.max(maxLiquidBalanceResidual, Math.abs(liquidBalance)); } double totalTransfer = 0.0; for (Double value : componentTransfers.values()) { - totalTransfer += value.doubleValue(); + totalTransfer += value.doubleValue(); } double segmentEnergyResidual = state.gasLeaving.get(segment).getEnthalpy() - + state.liquidLeaving.get(segment).getEnthalpy() - gas.getEnthalpy() - liquid.getEnthalpy(); + + state.liquidLeaving.get(segment).getEnthalpy() - gas.getEnthalpy() + - liquid.getEnthalpy(); SegmentResult result = new SegmentResult(segment + 1, (segment + 0.5) * segmentHeight, - state.gasLeaving.get(segment).getTemperature(), state.liquidLeaving.get(segment).getTemperature(), - state.gasLeaving.get(segment).getPressure(), state.liquidLeaving.get(segment).getPressure(), - state.gasLeaving.get(segment).getTotalNumberOfMoles(), - state.liquidLeaving.get(segment).getTotalNumberOfMoles(), snapshot.gasDensity, snapshot.liquidDensity, - snapshot.gasViscosity, snapshot.liquidViscosity, snapshot.gasDiffusivity, snapshot.liquidDiffusivity, - snapshot.wettedArea, snapshot.kGa, snapshot.kLa, snapshot.gasHeatTransferCoefficient, - snapshot.liquidHeatTransferCoefficient, snapshot.overallHeatTransferCoefficient, interfaceTemperature, - liquidSensibleHeat, snapshot.pressureDropPerMeter, snapshot.percentFlood, totalTransfer, componentTransfers, - equilibrium.gasMoleFractions, equilibrium.liquidMoleFractions, equilibrium.equilibriumRatios, - ColumnSolver.EQUATION_ORIENTED.name(), iterations, maxFluxResidual, heatResidual, segmentEnergyResidual); + state.gasLeaving.get(segment).getTemperature(), + state.liquidLeaving.get(segment).getTemperature(), + state.gasLeaving.get(segment).getPressure(), + state.liquidLeaving.get(segment).getPressure(), + state.gasLeaving.get(segment).getTotalNumberOfMoles(), + state.liquidLeaving.get(segment).getTotalNumberOfMoles(), snapshot.gasDensity, + snapshot.liquidDensity, snapshot.gasViscosity, snapshot.liquidViscosity, + snapshot.gasDiffusivity, snapshot.liquidDiffusivity, snapshot.wettedArea, snapshot.kGa, + snapshot.kLa, snapshot.gasHeatTransferCoefficient, snapshot.liquidHeatTransferCoefficient, + snapshot.overallHeatTransferCoefficient, interfaceTemperature, liquidSensibleHeat, + snapshot.pressureDropPerMeter, snapshot.percentFlood, totalTransfer, componentTransfers, + equilibrium.gasMoleFractions, equilibrium.liquidMoleFractions, + equilibrium.equilibriumRatios, ColumnSolver.EQUATION_ORIENTED.name(), iterations, + maxFluxResidual, heatResidual, segmentEnergyResidual); results.add(result); } double[] residualArray = toPrimitiveArray(residuals); - CounterCurrentSolution solution = new CounterCurrentSolution(state.gasOutlet, state.liquidOutlet, - state.liquidLeaving, results); - return new ColumnResidualEvaluation(boundedUnknowns, residualArray, residualNorm(residualArray), solution, - iterations, maxFluxResidual, maxHeatResidual, maxEnergyResidual, maxGasBalanceResidual, - maxLiquidBalanceResidual); + CounterCurrentSolution solution = new CounterCurrentSolution(state.gasOutlet, + state.liquidOutlet, state.liquidLeaving, results); + return new ColumnResidualEvaluation(boundedUnknowns, residualArray, residualNorm(residualArray), + solution, iterations, maxFluxResidual, maxHeatResidual, maxEnergyResidual, + maxGasBalanceResidual, maxLiquidBalanceResidual); } /** @@ -1302,8 +1339,9 @@ private ColumnResidualEvaluation evaluateColumnResidual(SystemInterface gasIn, S * @param homotopyFactor continuation factor from zero to one * @return Newton step, or null if the least-squares solve fails */ - private Matrix calculateColumnResidualStep(SystemInterface gasIn, SystemInterface liquidIn, List components, - double[] unknowns, ColumnResidualEvaluation evaluation, double homotopyFactor) { + private Matrix calculateColumnResidualStep(SystemInterface gasIn, SystemInterface liquidIn, + List components, double[] unknowns, ColumnResidualEvaluation evaluation, + double homotopyFactor) { int residualCount = evaluation.normalizedResiduals.length; int variableCount = unknowns.length; SparseJacobian sparseJacobian = new SparseJacobian(residualCount, variableCount); @@ -1314,32 +1352,34 @@ private Matrix calculateColumnResidualStep(SystemInterface gasIn, SystemInterfac shifted = clampColumnUnknowns(shifted, gasIn, liquidIn, components); double actualStep = shifted[variable] - unknowns[variable]; if (Math.abs(actualStep) < 1.0e-20) { - shifted = unknowns.clone(); - shifted[variable] -= step; - shifted = clampColumnUnknowns(shifted, gasIn, liquidIn, components); - actualStep = shifted[variable] - unknowns[variable]; + shifted = unknowns.clone(); + shifted[variable] -= step; + shifted = clampColumnUnknowns(shifted, gasIn, liquidIn, components); + actualStep = shifted[variable] - unknowns[variable]; } if (Math.abs(actualStep) < 1.0e-20) { - sparseJacobian.set(variable % residualCount, variable, 1.0); + sparseJacobian.set(variable % residualCount, variable, 1.0); } else { - ColumnResidualEvaluation shiftedEvaluation = evaluateColumnResidual(gasIn, liquidIn, components, shifted, - homotopyFactor, evaluation.iterations); - for (int row = 0; row < residualCount; row++) { - double derivative = (shiftedEvaluation.normalizedResiduals[row] - evaluation.normalizedResiduals[row]) - / actualStep; - if (Math.abs(derivative) > 1.0e-14 && Double.isFinite(derivative)) { - sparseJacobian.set(row, variable, derivative); - } - } + ColumnResidualEvaluation shiftedEvaluation = evaluateColumnResidual(gasIn, liquidIn, + components, shifted, homotopyFactor, evaluation.iterations); + for (int row = 0; row < residualCount; row++) { + double derivative = + (shiftedEvaluation.normalizedResiduals[row] - evaluation.normalizedResiduals[row]) + / actualStep; + if (Math.abs(derivative) > 1.0e-14 && Double.isFinite(derivative)) { + sparseJacobian.set(row, variable, derivative); + } + } } } try { - Matrix jacobian = sparseJacobian.toDenseMatrix(); + Matrix jacobian = LinearAlgebraOps.toDenseMatrix(sparseJacobian.rows, sparseJacobian.columns, + sparseJacobian.values); Matrix normalMatrix = jacobian.transpose().times(jacobian) - .plus(Matrix.identity(variableCount, variableCount).times(1.0e-10)); + .plus(Matrix.identity(variableCount, variableCount).times(1.0e-10)); double[][] rhsValues = new double[residualCount][1]; for (int row = 0; row < residualCount; row++) { - rhsValues[row][0] = -evaluation.normalizedResiduals[row]; + rhsValues[row][0] = -evaluation.normalizedResiduals[row]; } Matrix normalRhs = jacobian.transpose().times(new Matrix(rhsValues)); return normalMatrix.solve(normalRhs); @@ -1359,8 +1399,8 @@ private Matrix calculateColumnResidualStep(SystemInterface gasIn, SystemInterfac * @param components active transfer components * @return bounded candidate unknowns */ - private double[] applyColumnResidualStep(double[] unknowns, Matrix step, double damping, SystemInterface gasIn, - SystemInterface liquidIn, List components) { + private double[] applyColumnResidualStep(double[] unknowns, Matrix step, double damping, + SystemInterface gasIn, SystemInterface liquidIn, List components) { double[] candidate = unknowns.clone(); for (int i = 0; i < candidate.length; i++) { candidate[i] += damping * step.get(i, 0); @@ -1377,8 +1417,8 @@ private double[] applyColumnResidualStep(double[] unknowns, Matrix step, double * @param unknowns bounded column unknown vector * @return reconstructed column state */ - private ColumnState buildColumnState(SystemInterface gasIn, SystemInterface liquidIn, List components, - double[] unknowns) { + private ColumnState buildColumnState(SystemInterface gasIn, SystemInterface liquidIn, + List components, double[] unknowns) { List gasEntering = new ArrayList(); List gasLeaving = new ArrayList(); SystemInterface gasCurrent = gasIn.clone(); @@ -1387,8 +1427,8 @@ private ColumnState buildColumnState(SystemInterface gasIn, SystemInterface liqu gasEntering.add(gasCurrent.clone()); SystemInterface gasOutlet = gasCurrent.clone(); for (int componentIndex = 0; componentIndex < components.size(); componentIndex++) { - double transfer = unknowns[columnFluxIndex(segment, componentIndex, components)]; - addComponentDelta(gasOutlet, components.get(componentIndex), -transfer); + double transfer = unknowns[columnFluxIndex(segment, componentIndex, components)]; + addComponentDelta(gasOutlet, components.get(componentIndex), -transfer); } gasOutlet.setTemperature(unknowns[columnGasOutletTemperatureIndex(segment, components)]); flashAndInitialize(gasOutlet); @@ -1408,16 +1448,17 @@ private ColumnState buildColumnState(SystemInterface gasIn, SystemInterface liqu liquidEntering.set(segment, liquidCurrent.clone()); SystemInterface liquidOutlet = liquidCurrent.clone(); for (int componentIndex = 0; componentIndex < components.size(); componentIndex++) { - double transfer = unknowns[columnFluxIndex(segment, componentIndex, components)]; - addComponentDelta(liquidOutlet, components.get(componentIndex), transfer); + double transfer = unknowns[columnFluxIndex(segment, componentIndex, components)]; + addComponentDelta(liquidOutlet, components.get(componentIndex), transfer); } - liquidOutlet.setTemperature(unknowns[columnLiquidOutletTemperatureIndex(segment, components)]); + liquidOutlet + .setTemperature(unknowns[columnLiquidOutletTemperatureIndex(segment, components)]); flashAndInitialize(liquidOutlet); liquidLeaving.set(segment, liquidOutlet); liquidCurrent = liquidOutlet.clone(); } return new ColumnState(gasEntering, gasLeaving, liquidEntering, liquidLeaving, - gasLeaving.get(numberOfSegments - 1).clone(), liquidLeaving.get(0).clone()); + gasLeaving.get(numberOfSegments - 1).clone(), liquidLeaving.get(0).clone()); } /** @@ -1429,43 +1470,47 @@ private ColumnState buildColumnState(SystemInterface gasIn, SystemInterface liqu * @param components active transfer components * @return clamped unknown vector */ - private double[] clampColumnUnknowns(double[] unknowns, SystemInterface gasIn, SystemInterface liquidIn, - List components) { + private double[] clampColumnUnknowns(double[] unknowns, SystemInterface gasIn, + SystemInterface liquidIn, List components) { double[] bounded = unknowns.clone(); Map gasAvailable = componentInventoryMap(gasIn, components); for (int segment = 0; segment < numberOfSegments; segment++) { for (int componentIndex = 0; componentIndex < components.size(); componentIndex++) { - int index = columnFluxIndex(segment, componentIndex, components); - if (!Double.isFinite(bounded[index])) { - bounded[index] = 0.0; - } - String component = components.get(componentIndex); - if (bounded[index] > 0.0) { - double available = Math.max(0.0, gasAvailable.get(component).doubleValue() * maxTransferFractionPerSegment); - bounded[index] = Math.min(bounded[index], available); - gasAvailable.put(component, - Double.valueOf(Math.max(0.0, gasAvailable.get(component).doubleValue() - bounded[index]))); - } + int index = columnFluxIndex(segment, componentIndex, components); + if (!Double.isFinite(bounded[index])) { + bounded[index] = 0.0; + } + String component = components.get(componentIndex); + if (bounded[index] > 0.0) { + double available = Math.max(0.0, + gasAvailable.get(component).doubleValue() * maxTransferFractionPerSegment); + bounded[index] = Math.min(bounded[index], available); + gasAvailable.put(component, Double + .valueOf(Math.max(0.0, gasAvailable.get(component).doubleValue() - bounded[index]))); + } } } Map liquidAvailable = componentInventoryMap(liquidIn, components); for (int segment = numberOfSegments - 1; segment >= 0; segment--) { for (int componentIndex = 0; componentIndex < components.size(); componentIndex++) { - int index = columnFluxIndex(segment, componentIndex, components); - String component = components.get(componentIndex); - if (bounded[index] < 0.0) { - double available = Math.max(0.0, - liquidAvailable.get(component).doubleValue() * maxTransferFractionPerSegment); - bounded[index] = -Math.min(-bounded[index], available); - liquidAvailable.put(component, - Double.valueOf(Math.max(0.0, liquidAvailable.get(component).doubleValue() + bounded[index]))); - } + int index = columnFluxIndex(segment, componentIndex, components); + String component = components.get(componentIndex); + if (bounded[index] < 0.0) { + double available = Math.max(0.0, + liquidAvailable.get(component).doubleValue() * maxTransferFractionPerSegment); + bounded[index] = -Math.min(-bounded[index], available); + liquidAvailable.put(component, Double.valueOf( + Math.max(0.0, liquidAvailable.get(component).doubleValue() + bounded[index]))); + } } } for (int segment = 0; segment < numberOfSegments; segment++) { - clampColumnTemperatureUnknown(bounded, columnInterfaceTemperatureIndex(segment, components), gasIn, liquidIn); - clampColumnTemperatureUnknown(bounded, columnGasOutletTemperatureIndex(segment, components), gasIn, liquidIn); - clampColumnTemperatureUnknown(bounded, columnLiquidOutletTemperatureIndex(segment, components), gasIn, liquidIn); + clampColumnTemperatureUnknown(bounded, columnInterfaceTemperatureIndex(segment, components), + gasIn, liquidIn); + clampColumnTemperatureUnknown(bounded, columnGasOutletTemperatureIndex(segment, components), + gasIn, liquidIn); + clampColumnTemperatureUnknown(bounded, + columnLiquidOutletTemperatureIndex(segment, components), gasIn, liquidIn); } return bounded; } @@ -1483,7 +1528,8 @@ private void clampColumnTemperatureUnknown(double[] unknowns, int index, SystemI if (!Double.isFinite(unknowns[index])) { unknowns[index] = 0.5 * (gasIn.getTemperature() + liquidIn.getTemperature()); } - double minTemperature = Math.max(1.0, Math.min(gasIn.getTemperature(), liquidIn.getTemperature()) - 150.0); + double minTemperature = + Math.max(1.0, Math.min(gasIn.getTemperature(), liquidIn.getTemperature()) - 150.0); double maxTemperature = Math.max(gasIn.getTemperature(), liquidIn.getTemperature()) + 150.0; unknowns[index] = clamp(unknowns[index], minTemperature, maxTemperature); } @@ -1495,7 +1541,8 @@ private void clampColumnTemperatureUnknown(double[] unknowns, int index, SystemI * @param components active transfer components * @return component moles by component name */ - private Map componentInventoryMap(SystemInterface system, List components) { + private Map componentInventoryMap(SystemInterface system, + List components) { Map inventory = new LinkedHashMap(); for (int i = 0; i < components.size(); i++) { String component = components.get(i); @@ -1628,8 +1675,8 @@ private List initializeLiquidProfile(SystemInterface liquidIn) * @param liquidEntering liquid systems entering each segment * @return one profile iteration solution */ - private CounterCurrentSolution runOneProfileIteration(SystemInterface gasIn, SystemInterface liquidIn, - List liquidEntering) { + private CounterCurrentSolution runOneProfileIteration(SystemInterface gasIn, + SystemInterface liquidIn, List liquidEntering) { SystemInterface gasCurrent = gasIn.clone(); List liquidLeaving = new ArrayList(); List iterationResults = new ArrayList(); @@ -1651,13 +1698,14 @@ private CounterCurrentSolution runOneProfileIteration(SystemInterface gasIn, Sys * @param liquidLeaving liquid systems leaving each segment from the previous iteration * @return updated segment liquid inlet profile */ - private List updateLiquidProfile(SystemInterface liquidIn, List liquidLeaving) { + private List updateLiquidProfile(SystemInterface liquidIn, + List liquidLeaving) { List updated = new ArrayList(); for (int segment = 0; segment < numberOfSegments; segment++) { if (segment == numberOfSegments - 1) { - updated.add(liquidIn.clone()); + updated.add(liquidIn.clone()); } else { - updated.add(liquidLeaving.get(segment + 1).clone()); + updated.add(liquidLeaving.get(segment + 1).clone()); } } return updated; @@ -1679,10 +1727,10 @@ private void acceptSolution(CounterCurrentSolution solution) { for (int i = 0; i < solution.segmentResults.size(); i++) { SegmentResult result = solution.segmentResults.get(i); for (Map.Entry entry : result.getComponentMoleTransfer().entrySet()) { - Double oldValue = componentTransferTotals.get(entry.getKey()); - double newValue = (oldValue == null ? 0.0 : oldValue.doubleValue()) + entry.getValue(); - componentTransferTotals.put(entry.getKey(), newValue); - totalAbsoluteMolarTransfer += Math.abs(entry.getValue()); + Double oldValue = componentTransferTotals.get(entry.getKey()); + double newValue = (oldValue == null ? 0.0 : oldValue.doubleValue()) + entry.getValue(); + componentTransferTotals.put(entry.getKey(), newValue); + totalAbsoluteMolarTransfer += Math.abs(entry.getValue()); } } } @@ -1695,7 +1743,8 @@ private void acceptSolution(CounterCurrentSolution solution) { * @param liquidIn liquid system entering the segment * @return segment computation with outlet systems and result data */ - private SegmentComputation calculateSegment(int segment, SystemInterface gasIn, SystemInterface liquidIn) { + private SegmentComputation calculateSegment(int segment, SystemInterface gasIn, + SystemInterface liquidIn) { SystemInterface gas = gasIn.clone(); SystemInterface liquid = liquidIn.clone(); flashAndInitialize(gas); @@ -1703,24 +1752,26 @@ private SegmentComputation calculateSegment(int segment, SystemInterface gasIn, double segmentHeight = packedHeight / numberOfSegments; if (segmentHeight > 0.0 && segmentSolver == SegmentSolver.SIMULTANEOUS_RESIDUAL - && heatTransferModel != HeatTransferModel.NONE) { + && heatTransferModel != HeatTransferModel.NONE) { return calculateSimultaneousResidualSegment(segment, gas, liquid, segmentHeight); } double inletTotalEnthalpy = gas.getEnthalpy() + liquid.getEnthalpy(); Map componentTransfers = new LinkedHashMap(); TransportSnapshot snapshot = calculateTransportSnapshot(gas, liquid, segmentHeight); - InterfaceEquilibrium interfaceEquilibrium = calculateInterfaceEquilibrium(gas, liquid, snapshot); + InterfaceEquilibrium interfaceEquilibrium = + calculateInterfaceEquilibrium(gas, liquid, snapshot); double heatTransferRate = 0.0; if (segmentHeight > 0.0) { List components = getTransferComponentList(gas, liquid); for (int i = 0; i < components.size(); i++) { - String component = components.get(i); - double transfer = calculateComponentTransfer(component, gas, liquid, snapshot, interfaceEquilibrium); - if (Math.abs(transfer) > 0.0) { - applyComponentTransfer(component, transfer, gas, liquid); - componentTransfers.put(component, transfer); - } + String component = components.get(i); + double transfer = + calculateComponentTransfer(component, gas, liquid, snapshot, interfaceEquilibrium); + if (Math.abs(transfer) > 0.0) { + applyComponentTransfer(component, transfer, gas, liquid); + componentTransfers.put(component, transfer); + } } heatTransferRate = applyInterphaseHeatTransfer(gas, liquid, snapshot); flashAndInitialize(gas); @@ -1731,16 +1782,18 @@ private SegmentComputation calculateSegment(int segment, SystemInterface gasIn, for (Double value : componentTransfers.values()) { totalTransfer += value.doubleValue(); } - SegmentResult result = new SegmentResult(segment + 1, (segment + 0.5) * segmentHeight, gas.getTemperature(), - liquid.getTemperature(), gas.getPressure(), liquid.getPressure(), gas.getTotalNumberOfMoles(), - liquid.getTotalNumberOfMoles(), snapshot.gasDensity, snapshot.liquidDensity, snapshot.gasViscosity, - snapshot.liquidViscosity, snapshot.gasDiffusivity, snapshot.liquidDiffusivity, snapshot.wettedArea, - snapshot.kGa, snapshot.kLa, snapshot.gasHeatTransferCoefficient, snapshot.liquidHeatTransferCoefficient, - snapshot.overallHeatTransferCoefficient, interfaceEquilibrium.interfaceTemperatureK, heatTransferRate, - snapshot.pressureDropPerMeter, snapshot.percentFlood, totalTransfer, componentTransfers, - interfaceEquilibrium.gasMoleFractions, interfaceEquilibrium.liquidMoleFractions, - interfaceEquilibrium.equilibriumRatios, SegmentSolver.SEQUENTIAL_EXPLICIT.name(), 0, 0.0, 0.0, - gas.getEnthalpy() + liquid.getEnthalpy() - inletTotalEnthalpy); + SegmentResult result = new SegmentResult(segment + 1, (segment + 0.5) * segmentHeight, + gas.getTemperature(), liquid.getTemperature(), gas.getPressure(), liquid.getPressure(), + gas.getTotalNumberOfMoles(), liquid.getTotalNumberOfMoles(), snapshot.gasDensity, + snapshot.liquidDensity, snapshot.gasViscosity, snapshot.liquidViscosity, + snapshot.gasDiffusivity, snapshot.liquidDiffusivity, snapshot.wettedArea, snapshot.kGa, + snapshot.kLa, snapshot.gasHeatTransferCoefficient, snapshot.liquidHeatTransferCoefficient, + snapshot.overallHeatTransferCoefficient, interfaceEquilibrium.interfaceTemperatureK, + heatTransferRate, snapshot.pressureDropPerMeter, snapshot.percentFlood, totalTransfer, + componentTransfers, interfaceEquilibrium.gasMoleFractions, + interfaceEquilibrium.liquidMoleFractions, interfaceEquilibrium.equilibriumRatios, + SegmentSolver.SEQUENTIAL_EXPLICIT.name(), 0, 0.0, 0.0, + gas.getEnthalpy() + liquid.getEnthalpy() - inletTotalEnthalpy); return new SegmentComputation(gas, liquid, result); } @@ -1766,7 +1819,8 @@ private TransportSnapshot calculateTransportSnapshot(SystemInterface gas, System double gasHeatCapacity = heatCapacityMass(gasPhase, DEFAULT_GAS_HEAT_CAPACITY); double liquidHeatCapacity = heatCapacityMass(liquidPhase, DEFAULT_LIQUID_HEAT_CAPACITY); double gasConductivity = thermalConductivity(gasPhase, DEFAULT_GAS_THERMAL_CONDUCTIVITY); - double liquidConductivity = thermalConductivity(liquidPhase, DEFAULT_LIQUID_THERMAL_CONDUCTIVITY); + double liquidConductivity = + thermalConductivity(liquidPhase, DEFAULT_LIQUID_THERMAL_CONDUCTIVITY); PackingHydraulicsCalculator hydraulics = new PackingHydraulicsCalculator(); hydraulics.setPackingSpecification(packingSpecification); @@ -1789,21 +1843,24 @@ private TransportSnapshot calculateTransportSnapshot(SystemInterface gas, System gasMultiplier = Math.max(0.1, packingSpecification.getBilletGasConstant() / 0.4); liquidMultiplier = Math.max(0.1, packingSpecification.getBilletLiquidConstant()); } - double kGa = finiteNonNegative(hydraulics.getKGa(), 0.0) * gasMultiplier * massTransferCorrectionFactor; - double kLa = finiteNonNegative(hydraulics.getKLa(), 0.0) * liquidMultiplier * massTransferCorrectionFactor; - double gasHeatTransferCoefficient = calculateVolumetricHeatTransferCoefficient(kGa, gasDensity, gasHeatCapacity, - gasViscosity, gasDiffusivity, gasConductivity); - double liquidHeatTransferCoefficient = calculateVolumetricHeatTransferCoefficient(kLa, liquidDensity, - liquidHeatCapacity, liquidViscosity, liquidDiffusivity, liquidConductivity); - double overallHeatTransferCoefficient = combineHeatTransferCoefficients(gasHeatTransferCoefficient, - liquidHeatTransferCoefficient); - double interfaceTemperature = calculateInterfaceTemperature(gas.getTemperature(), liquid.getTemperature(), - gasHeatTransferCoefficient, liquidHeatTransferCoefficient); - return new TransportSnapshot(gasDensity, liquidDensity, gasViscosity, liquidViscosity, gasDiffusivity, - liquidDiffusivity, finiteNonNegative(hydraulics.getWettedArea(), 0.0), kGa, kLa, gasHeatCapacity, - liquidHeatCapacity, gasHeatTransferCoefficient, liquidHeatTransferCoefficient, overallHeatTransferCoefficient, - interfaceTemperature, finiteNonNegative(hydraulics.getPressureDropPerMeter(), 0.0), - finiteNonNegative(hydraulics.getPercentFlood(), 0.0)); + double kGa = + finiteNonNegative(hydraulics.getKGa(), 0.0) * gasMultiplier * massTransferCorrectionFactor; + double kLa = finiteNonNegative(hydraulics.getKLa(), 0.0) * liquidMultiplier + * massTransferCorrectionFactor; + double gasHeatTransferCoefficient = calculateVolumetricHeatTransferCoefficient(kGa, gasDensity, + gasHeatCapacity, gasViscosity, gasDiffusivity, gasConductivity); + double liquidHeatTransferCoefficient = calculateVolumetricHeatTransferCoefficient(kLa, + liquidDensity, liquidHeatCapacity, liquidViscosity, liquidDiffusivity, liquidConductivity); + double overallHeatTransferCoefficient = + combineHeatTransferCoefficients(gasHeatTransferCoefficient, liquidHeatTransferCoefficient); + double interfaceTemperature = calculateInterfaceTemperature(gas.getTemperature(), + liquid.getTemperature(), gasHeatTransferCoefficient, liquidHeatTransferCoefficient); + return new TransportSnapshot(gasDensity, liquidDensity, gasViscosity, liquidViscosity, + gasDiffusivity, liquidDiffusivity, finiteNonNegative(hydraulics.getWettedArea(), 0.0), kGa, + kLa, gasHeatCapacity, liquidHeatCapacity, gasHeatTransferCoefficient, + liquidHeatTransferCoefficient, overallHeatTransferCoefficient, interfaceTemperature, + finiteNonNegative(hydraulics.getPressureDropPerMeter(), 0.0), + finiteNonNegative(hydraulics.getPercentFlood(), 0.0)); } /** @@ -1816,9 +1873,11 @@ interfaceTemperature, finiteNonNegative(hydraulics.getPressureDropPerMeter(), 0. * @param interfaceEquilibrium interface equilibrium data * @return transfer rate in mol/s, positive from gas to liquid */ - private double calculateComponentTransfer(String component, SystemInterface gas, SystemInterface liquid, - TransportSnapshot snapshot, InterfaceEquilibrium interfaceEquilibrium) { - double transfer = calculateUnboundedComponentTransfer(component, gas, liquid, snapshot, interfaceEquilibrium); + private double calculateComponentTransfer(String component, SystemInterface gas, + SystemInterface liquid, TransportSnapshot snapshot, + InterfaceEquilibrium interfaceEquilibrium) { + double transfer = + calculateUnboundedComponentTransfer(component, gas, liquid, snapshot, interfaceEquilibrium); return limitTransfer(component, transfer, gas, liquid); } @@ -1832,36 +1891,40 @@ private double calculateComponentTransfer(String component, SystemInterface gas, * @param interfaceEquilibrium interface equilibrium data * @return unbounded transfer rate in mol/s, positive from gas to liquid */ - private double calculateUnboundedComponentTransfer(String component, SystemInterface gas, SystemInterface liquid, - TransportSnapshot snapshot, InterfaceEquilibrium interfaceEquilibrium) { + private double calculateUnboundedComponentTransfer(String component, SystemInterface gas, + SystemInterface liquid, TransportSnapshot snapshot, + InterfaceEquilibrium interfaceEquilibrium) { PhaseInterface gasPhase = getGasPhase(gas); PhaseInterface liquidPhase = getLiquidPhase(liquid); double kValue = interfaceEquilibrium.getEquilibriumRatio(component); double gasFraction = moleFraction(gasPhase, component); double liquidFraction = moleFraction(liquidPhase, component); double gasInterfaceFraction = interfaceEquilibrium.getGasMoleFraction(component, - clamp(kValue * liquidFraction, 0.0, 0.999999)); + clamp(kValue * liquidFraction, 0.0, 0.999999)); double liquidInterfaceFraction = interfaceEquilibrium.getLiquidMoleFraction(component, - kValue > 1.0e-12 ? clamp(gasInterfaceFraction / kValue, 0.0, 0.999999) : liquidFraction); + kValue > 1.0e-12 ? clamp(gasInterfaceFraction / kValue, 0.0, 0.999999) : liquidFraction); double gasDrivingForce = gasFraction - gasInterfaceFraction; double liquidDrivingForce = liquidInterfaceFraction - liquidFraction; if (Math.abs(gasDrivingForce) < 1.0e-12 || snapshot.kGa <= 0.0 || snapshot.kLa <= 0.0) { return 0.0; } - double gasFilmCoefficient = calculateFilmCoefficient(gasPhase, component, snapshot.kGa, snapshot.gasDiffusivity, - true); + double gasFilmCoefficient = + calculateFilmCoefficient(gasPhase, component, snapshot.kGa, snapshot.gasDiffusivity, true); double liquidFilmCoefficient = calculateFilmCoefficient(liquidPhase, component, snapshot.kLa, - snapshot.liquidDiffusivity, false); + snapshot.liquidDiffusivity, false); double gasFluxDensity = gasFilmCoefficient * molarConcentration(gasPhase) * gasDrivingForce; - double liquidFluxDensity = liquidFilmCoefficient * molarConcentration(liquidPhase) * liquidDrivingForce; + double liquidFluxDensity = + liquidFilmCoefficient * molarConcentration(liquidPhase) * liquidDrivingForce; double transferDensity = combineFilmFluxes(gasFluxDensity, liquidFluxDensity); if (Math.abs(transferDensity) <= 0.0) { double yStar = clamp(kValue * liquidFraction, 0.0, 0.999999); double drivingForce = gasFraction - yStar; - double overallCoefficient = 1.0 / (1.0 / gasFilmCoefficient + Math.max(kValue, 1.0e-12) / liquidFilmCoefficient); + double overallCoefficient = + 1.0 / (1.0 / gasFilmCoefficient + Math.max(kValue, 1.0e-12) / liquidFilmCoefficient); transferDensity = overallCoefficient * molarConcentration(gasPhase) * drivingForce; } - double segmentVolume = Math.PI * columnDiameter * columnDiameter / 4.0 * packedHeight / numberOfSegments; + double segmentVolume = + Math.PI * columnDiameter * columnDiameter / 4.0 * packedHeight / numberOfSegments; return transferDensity * segmentVolume; } @@ -1887,8 +1950,8 @@ private SegmentComputation calculateSimultaneousResidualSegment(int segment, Sys Double transferValue = evaluation.componentTransfers.get(component); double transfer = transferValue == null ? 0.0 : transferValue.doubleValue(); if (Math.abs(transfer) > 0.0) { - applyComponentTransfer(component, transfer, gas, liquid); - componentTransfers.put(component, transfer); + applyComponentTransfer(component, transfer, gas, liquid); + componentTransfers.put(component, transfer); } } flashAndInitialize(gas); @@ -1901,17 +1964,21 @@ private SegmentComputation calculateSimultaneousResidualSegment(int segment, Sys totalTransfer += value.doubleValue(); } double enthalpyBalanceResidual = gas.getEnthalpy() + liquid.getEnthalpy() - inletTotalEnthalpy; - SegmentResult result = new SegmentResult(segment + 1, (segment + 0.5) * segmentHeight, gas.getTemperature(), - liquid.getTemperature(), gas.getPressure(), liquid.getPressure(), gas.getTotalNumberOfMoles(), - liquid.getTotalNumberOfMoles(), snapshot.gasDensity, snapshot.liquidDensity, snapshot.gasViscosity, - snapshot.liquidViscosity, snapshot.gasDiffusivity, snapshot.liquidDiffusivity, snapshot.wettedArea, - snapshot.kGa, snapshot.kLa, snapshot.gasHeatTransferCoefficient, snapshot.liquidHeatTransferCoefficient, - snapshot.overallHeatTransferCoefficient, evaluation.interfaceEquilibrium.interfaceTemperatureK, - evaluation.heatTransferRateW, snapshot.pressureDropPerMeter, snapshot.percentFlood, totalTransfer, - componentTransfers, evaluation.interfaceEquilibrium.gasMoleFractions, - evaluation.interfaceEquilibrium.liquidMoleFractions, evaluation.interfaceEquilibrium.equilibriumRatios, - SegmentSolver.SIMULTANEOUS_RESIDUAL.name(), evaluation.iterations, evaluation.maxFluxResidualMolPerSec, - evaluation.heatBalanceResidualW, enthalpyBalanceResidual); + SegmentResult result = new SegmentResult(segment + 1, (segment + 0.5) * segmentHeight, + gas.getTemperature(), liquid.getTemperature(), gas.getPressure(), liquid.getPressure(), + gas.getTotalNumberOfMoles(), liquid.getTotalNumberOfMoles(), snapshot.gasDensity, + snapshot.liquidDensity, snapshot.gasViscosity, snapshot.liquidViscosity, + snapshot.gasDiffusivity, snapshot.liquidDiffusivity, snapshot.wettedArea, snapshot.kGa, + snapshot.kLa, snapshot.gasHeatTransferCoefficient, snapshot.liquidHeatTransferCoefficient, + snapshot.overallHeatTransferCoefficient, + evaluation.interfaceEquilibrium.interfaceTemperatureK, evaluation.heatTransferRateW, + snapshot.pressureDropPerMeter, snapshot.percentFlood, totalTransfer, componentTransfers, + evaluation.interfaceEquilibrium.gasMoleFractions, + evaluation.interfaceEquilibrium.liquidMoleFractions, + evaluation.interfaceEquilibrium.equilibriumRatios, + SegmentSolver.SIMULTANEOUS_RESIDUAL.name(), evaluation.iterations, + evaluation.maxFluxResidualMolPerSec, evaluation.heatBalanceResidualW, + enthalpyBalanceResidual); return new SegmentComputation(gas, liquid, result); } @@ -1924,36 +1991,38 @@ private SegmentComputation calculateSimultaneousResidualSegment(int segment, Sys * @param components active transfer components * @return best residual evaluation found */ - private SegmentResidualEvaluation solveSegmentResiduals(SystemInterface gas, SystemInterface liquid, - TransportSnapshot snapshot, List components) { + private SegmentResidualEvaluation solveSegmentResiduals(SystemInterface gas, + SystemInterface liquid, TransportSnapshot snapshot, List components) { double[] unknowns = createInitialResidualUnknowns(gas, liquid, snapshot, components); - SegmentResidualEvaluation best = evaluateSegmentResidual(gas, liquid, snapshot, components, unknowns, 0); + SegmentResidualEvaluation best = + evaluateSegmentResidual(gas, liquid, snapshot, components, unknowns, 0); for (int iteration = 0; iteration < maxSegmentResidualIterations; iteration++) { if (best.norm <= segmentResidualTolerance) { - return best; + return best; } Matrix step = calculateResidualStep(gas, liquid, snapshot, components, unknowns, best); if (step == null) { - return best; + return best; } boolean improved = false; double[] bestUnknowns = unknowns; SegmentResidualEvaluation bestCandidate = best; double damping = 1.0; for (int lineSearch = 0; lineSearch < 8; lineSearch++) { - double[] candidateUnknowns = applyResidualStep(unknowns, step, damping, gas, liquid, components); - SegmentResidualEvaluation candidate = evaluateSegmentResidual(gas, liquid, snapshot, components, - candidateUnknowns, iteration + 1); - if (candidate.norm < bestCandidate.norm) { - bestUnknowns = candidateUnknowns; - bestCandidate = candidate; - improved = true; - break; - } - damping *= 0.5; + double[] candidateUnknowns = + applyResidualStep(unknowns, step, damping, gas, liquid, components); + SegmentResidualEvaluation candidate = evaluateSegmentResidual(gas, liquid, snapshot, + components, candidateUnknowns, iteration + 1); + if (candidate.norm < bestCandidate.norm) { + bestUnknowns = candidateUnknowns; + bestCandidate = candidate; + improved = true; + break; + } + damping *= 0.5; } if (!improved) { - return best; + return best; } unknowns = bestUnknowns; best = bestCandidate; @@ -1975,7 +2044,8 @@ private double[] createInitialResidualUnknowns(SystemInterface gas, SystemInterf double[] unknowns = new double[components.size() + 1]; InterfaceEquilibrium equilibrium = calculateInterfaceEquilibrium(gas, liquid, snapshot); for (int i = 0; i < components.size(); i++) { - unknowns[i] = calculateComponentTransfer(components.get(i), gas, liquid, snapshot, equilibrium); + unknowns[i] = + calculateComponentTransfer(components.get(i), gas, liquid, snapshot, equilibrium); } unknowns[components.size()] = snapshot.interfaceTemperatureK; return clampResidualUnknowns(unknowns, gas, liquid, components); @@ -1992,30 +2062,35 @@ private double[] createInitialResidualUnknowns(SystemInterface gas, SystemInterf * @param iterations iteration count represented by the evaluation * @return residual evaluation */ - private SegmentResidualEvaluation evaluateSegmentResidual(SystemInterface gas, SystemInterface liquid, - TransportSnapshot snapshot, List components, double[] unknowns, int iterations) { + private SegmentResidualEvaluation evaluateSegmentResidual(SystemInterface gas, + SystemInterface liquid, TransportSnapshot snapshot, List components, + double[] unknowns, int iterations) { double[] boundedUnknowns = clampResidualUnknowns(unknowns, gas, liquid, components); double interfaceTemperature = boundedUnknowns[components.size()]; - InterfaceEquilibrium equilibrium = calculateInterfaceEquilibrium(gas, liquid, interfaceTemperature); + InterfaceEquilibrium equilibrium = + calculateInterfaceEquilibrium(gas, liquid, interfaceTemperature); double[] residuals = new double[components.size() + 1]; Map componentTransfers = new LinkedHashMap(); double maxFluxResidual = 0.0; for (int i = 0; i < components.size(); i++) { String component = components.get(i); double proposedTransfer = boundedUnknowns[i]; - double predictedTransfer = calculateUnboundedComponentTransfer(component, gas, liquid, snapshot, equilibrium); + double predictedTransfer = + calculateUnboundedComponentTransfer(component, gas, liquid, snapshot, equilibrium); predictedTransfer = limitTransfer(component, predictedTransfer, gas, liquid); double fluxResidual = proposedTransfer - predictedTransfer; - residuals[i] = fluxResidual / transferResidualScale(component, predictedTransfer, gas, liquid); + residuals[i] = + fluxResidual / transferResidualScale(component, predictedTransfer, gas, liquid); maxFluxResidual = Math.max(maxFluxResidual, Math.abs(fluxResidual)); componentTransfers.put(component, proposedTransfer); } - double segmentVolume = Math.PI * columnDiameter * columnDiameter / 4.0 * packedHeight / numberOfSegments; + double segmentVolume = + Math.PI * columnDiameter * columnDiameter / 4.0 * packedHeight / numberOfSegments; double gasSensibleHeat = snapshot.gasHeatTransferCoefficient * segmentVolume - * (gas.getTemperature() - interfaceTemperature); + * (gas.getTemperature() - interfaceTemperature); double liquidSensibleHeat = snapshot.liquidHeatTransferCoefficient * segmentVolume - * (interfaceTemperature - liquid.getTemperature()); + * (interfaceTemperature - liquid.getTemperature()); double gasMassEnthalpy = 0.0; double liquidMassEnthalpy = 0.0; for (int i = 0; i < components.size(); i++) { @@ -2024,13 +2099,15 @@ private SegmentResidualEvaluation evaluateSegmentResidual(SystemInterface gas, S gasMassEnthalpy += transfer * equilibrium.getGasMolarEnthalpy(component); liquidMassEnthalpy += transfer * equilibrium.getLiquidMolarEnthalpy(component); } - double heatBalanceResidual = gasSensibleHeat + gasMassEnthalpy - liquidSensibleHeat - liquidMassEnthalpy; - residuals[components.size()] = heatBalanceResidual - / heatResidualScale(gasSensibleHeat, liquidSensibleHeat, gasMassEnthalpy, liquidMassEnthalpy); + double heatBalanceResidual = + gasSensibleHeat + gasMassEnthalpy - liquidSensibleHeat - liquidMassEnthalpy; + residuals[components.size()] = heatBalanceResidual / heatResidualScale(gasSensibleHeat, + liquidSensibleHeat, gasMassEnthalpy, liquidMassEnthalpy); double gasTargetEnthalpy = gas.getEnthalpy() - gasSensibleHeat - gasMassEnthalpy; double liquidTargetEnthalpy = liquid.getEnthalpy() + liquidSensibleHeat + liquidMassEnthalpy; - return new SegmentResidualEvaluation(equilibrium, componentTransfers, residuals, residualNorm(residuals), - maxFluxResidual, heatBalanceResidual, liquidSensibleHeat, gasTargetEnthalpy, liquidTargetEnthalpy, iterations); + return new SegmentResidualEvaluation(equilibrium, componentTransfers, residuals, + residualNorm(residuals), maxFluxResidual, heatBalanceResidual, liquidSensibleHeat, + gasTargetEnthalpy, liquidTargetEnthalpy, iterations); } /** @@ -2044,8 +2121,9 @@ private SegmentResidualEvaluation evaluateSegmentResidual(SystemInterface gas, S * @param evaluation current residual evaluation * @return Newton step, or null if the linear solve fails */ - private Matrix calculateResidualStep(SystemInterface gas, SystemInterface liquid, TransportSnapshot snapshot, - List components, double[] unknowns, SegmentResidualEvaluation evaluation) { + private Matrix calculateResidualStep(SystemInterface gas, SystemInterface liquid, + TransportSnapshot snapshot, List components, double[] unknowns, + SegmentResidualEvaluation evaluation) { int dimension = unknowns.length; double[][] jacobian = new double[dimension][dimension]; for (int variable = 0; variable < dimension; variable++) { @@ -2055,20 +2133,21 @@ private Matrix calculateResidualStep(SystemInterface gas, SystemInterface liquid shifted = clampResidualUnknowns(shifted, gas, liquid, components); double actualStep = shifted[variable] - unknowns[variable]; if (Math.abs(actualStep) < 1.0e-20) { - shifted = unknowns.clone(); - shifted[variable] -= step; - shifted = clampResidualUnknowns(shifted, gas, liquid, components); - actualStep = shifted[variable] - unknowns[variable]; + shifted = unknowns.clone(); + shifted[variable] -= step; + shifted = clampResidualUnknowns(shifted, gas, liquid, components); + actualStep = shifted[variable] - unknowns[variable]; } if (Math.abs(actualStep) < 1.0e-20) { - jacobian[variable][variable] = 1.0; + jacobian[variable][variable] = 1.0; } else { - SegmentResidualEvaluation shiftedEvaluation = evaluateSegmentResidual(gas, liquid, snapshot, components, - shifted, evaluation.iterations); - for (int row = 0; row < dimension; row++) { - jacobian[row][variable] = (shiftedEvaluation.normalizedResiduals[row] - evaluation.normalizedResiduals[row]) - / actualStep; - } + SegmentResidualEvaluation shiftedEvaluation = evaluateSegmentResidual(gas, liquid, snapshot, + components, shifted, evaluation.iterations); + for (int row = 0; row < dimension; row++) { + jacobian[row][variable] = + (shiftedEvaluation.normalizedResiduals[row] - evaluation.normalizedResiduals[row]) + / actualStep; + } } } double[][] rhsValues = new double[dimension][1]; @@ -2093,8 +2172,8 @@ private Matrix calculateResidualStep(SystemInterface gas, SystemInterface liquid * @param components active transfer components * @return bounded candidate unknowns */ - private double[] applyResidualStep(double[] unknowns, Matrix step, double damping, SystemInterface gas, - SystemInterface liquid, List components) { + private double[] applyResidualStep(double[] unknowns, Matrix step, double damping, + SystemInterface gas, SystemInterface liquid, List components) { double[] candidate = unknowns.clone(); for (int i = 0; i < candidate.length; i++) { candidate[i] += damping * step.get(i, 0); @@ -2111,21 +2190,23 @@ private double[] applyResidualStep(double[] unknowns, Matrix step, double dampin * @param components active transfer components * @return clamped unknown vector */ - private double[] clampResidualUnknowns(double[] unknowns, SystemInterface gas, SystemInterface liquid, - List components) { + private double[] clampResidualUnknowns(double[] unknowns, SystemInterface gas, + SystemInterface liquid, List components) { double[] bounded = unknowns.clone(); for (int i = 0; i < components.size(); i++) { if (!Double.isFinite(bounded[i])) { - bounded[i] = 0.0; + bounded[i] = 0.0; } bounded[i] = limitTransfer(components.get(i), bounded[i], gas, liquid); } - double minimumTemperature = Math.max(1.0, Math.min(gas.getTemperature(), liquid.getTemperature()) - 100.0); + double minimumTemperature = + Math.max(1.0, Math.min(gas.getTemperature(), liquid.getTemperature()) - 100.0); double maximumTemperature = Math.max(gas.getTemperature(), liquid.getTemperature()) + 100.0; if (!Double.isFinite(bounded[components.size()])) { bounded[components.size()] = 0.5 * (gas.getTemperature() + liquid.getTemperature()); } - bounded[components.size()] = clamp(bounded[components.size()], minimumTemperature, maximumTemperature); + bounded[components.size()] = + clamp(bounded[components.size()], minimumTemperature, maximumTemperature); return bounded; } @@ -2153,10 +2234,11 @@ private double residualVariableStep(double[] unknowns, int variable, int compone * @param liquid liquid system * @return positive residual scaling in mol/s */ - private double transferResidualScale(String component, double predictedTransfer, SystemInterface gas, - SystemInterface liquid) { + private double transferResidualScale(String component, double predictedTransfer, + SystemInterface gas, SystemInterface liquid) { double inventory = Math.max(componentMoles(gas, component), componentMoles(liquid, component)); - return Math.max(1.0e-10, Math.max(Math.abs(predictedTransfer), inventory * maxTransferFractionPerSegment * 1.0e-4)); + return Math.max(1.0e-10, + Math.max(Math.abs(predictedTransfer), inventory * maxTransferFractionPerSegment * 1.0e-4)); } /** @@ -2168,10 +2250,10 @@ private double transferResidualScale(String component, double predictedTransfer, * @param liquidMassEnthalpy liquid-side transferred component enthalpy rate in W * @return positive residual scaling in W */ - private double heatResidualScale(double gasSensibleHeat, double liquidSensibleHeat, double gasMassEnthalpy, - double liquidMassEnthalpy) { - return Math.max(1.0, Math.abs(gasSensibleHeat) + Math.abs(liquidSensibleHeat) + Math.abs(gasMassEnthalpy) - + Math.abs(liquidMassEnthalpy)); + private double heatResidualScale(double gasSensibleHeat, double liquidSensibleHeat, + double gasMassEnthalpy, double liquidMassEnthalpy) { + return Math.max(1.0, Math.abs(gasSensibleHeat) + Math.abs(liquidSensibleHeat) + + Math.abs(gasMassEnthalpy) + Math.abs(liquidMassEnthalpy)); } /** @@ -2203,11 +2285,11 @@ private void applyEnthalpyTarget(SystemInterface system, double targetEnthalpy) double estimatedTemperature = estimateTemperatureForTargetEnthalpy(system, targetEnthalpy); system.setTemperature(estimatedTemperature); try { - flashAndInitialize(system); + flashAndInitialize(system); } catch (RuntimeException innerException) { - system.setTemperature(clamp(system.getTemperature(), 250.0, 500.0)); - system.init(3); - system.initProperties(); + system.setTemperature(clamp(system.getTemperature(), 250.0, 500.0)); + system.init(3); + system.initProperties(); } } } @@ -2219,9 +2301,11 @@ private void applyEnthalpyTarget(SystemInterface system, double targetEnthalpy) * @param targetEnthalpy target total enthalpy in J or W-equivalent stream basis * @return estimated temperature in kelvin */ - private double estimateTemperatureForTargetEnthalpy(SystemInterface system, double targetEnthalpy) { + private double estimateTemperatureForTargetEnthalpy(SystemInterface system, + double targetEnthalpy) { double heatCapacity = Math.max(1.0, system.getCp("J/K")); - double estimatedTemperature = system.getTemperature() + (targetEnthalpy - system.getEnthalpy()) / heatCapacity; + double estimatedTemperature = + system.getTemperature() + (targetEnthalpy - system.getEnthalpy()) / heatCapacity; if (!Double.isFinite(estimatedTemperature)) { return system.getTemperature(); } @@ -2236,7 +2320,8 @@ private double estimateTemperatureForTargetEnthalpy(SystemInterface system, doub * @param gas gas system to update * @param liquid liquid system to update */ - private void applyComponentTransfer(String component, double transfer, SystemInterface gas, SystemInterface liquid) { + private void applyComponentTransfer(String component, double transfer, SystemInterface gas, + SystemInterface liquid) { gas.addComponent(component, -transfer); liquid.addComponent(component, transfer); } @@ -2250,7 +2335,8 @@ private void applyComponentTransfer(String component, double transfer, SystemInt * @param liquid liquid system * @return bounded transfer in mol/s */ - private double limitTransfer(String component, double proposedTransfer, SystemInterface gas, SystemInterface liquid) { + private double limitTransfer(String component, double proposedTransfer, SystemInterface gas, + SystemInterface liquid) { if (proposedTransfer > 0.0) { double available = componentMoles(gas, component) * maxTransferFractionPerSegment; return Math.min(proposedTransfer, Math.max(0.0, available)); @@ -2269,8 +2355,8 @@ private double limitTransfer(String component, double proposedTransfer, SystemIn * @param snapshot transport snapshot containing the interfacial temperature estimate * @return interface equilibrium data */ - private InterfaceEquilibrium calculateInterfaceEquilibrium(SystemInterface gas, SystemInterface liquid, - TransportSnapshot snapshot) { + private InterfaceEquilibrium calculateInterfaceEquilibrium(SystemInterface gas, + SystemInterface liquid, TransportSnapshot snapshot) { Map gasFractions = new LinkedHashMap(); Map liquidFractions = new LinkedHashMap(); Map ratios = new LinkedHashMap(); @@ -2297,18 +2383,18 @@ private InterfaceEquilibrium calculateInterfaceEquilibrium(SystemInterface gas, double y = moleFraction(gasPhase, component); gasFractions.put(component, y); liquidFractions.put(component, x); - gasMolarEnthalpies.put(component, - componentMolarEnthalpy(gasPhase, component, snapshot.interfaceTemperatureK, phaseMolarEnthalpy(gasPhase))); + gasMolarEnthalpies.put(component, componentMolarEnthalpy(gasPhase, component, + snapshot.interfaceTemperatureK, phaseMolarEnthalpy(gasPhase))); liquidMolarEnthalpies.put(component, componentMolarEnthalpy(liquidPhase, component, - snapshot.interfaceTemperatureK, phaseMolarEnthalpy(liquidPhase))); + snapshot.interfaceTemperatureK, phaseMolarEnthalpy(liquidPhase))); if (x > 1.0e-12 && y >= 0.0) { - ratios.put(component, Math.max(1.0e-12, y / x)); + ratios.put(component, Math.max(1.0e-12, y / x)); } else { - ratios.put(component, 1.0); + ratios.put(component, 1.0); } } - return new InterfaceEquilibrium(snapshot.interfaceTemperatureK, gasFractions, liquidFractions, ratios, - gasMolarEnthalpies, liquidMolarEnthalpies); + return new InterfaceEquilibrium(snapshot.interfaceTemperatureK, gasFractions, liquidFractions, + ratios, gasMolarEnthalpies, liquidMolarEnthalpies); } /** @@ -2319,11 +2405,11 @@ private InterfaceEquilibrium calculateInterfaceEquilibrium(SystemInterface gas, * @param interfaceTemperatureK interface temperature in kelvin * @return interface equilibrium data */ - private InterfaceEquilibrium calculateInterfaceEquilibrium(SystemInterface gas, SystemInterface liquid, - double interfaceTemperatureK) { + private InterfaceEquilibrium calculateInterfaceEquilibrium(SystemInterface gas, + SystemInterface liquid, double interfaceTemperatureK) { TransportSnapshot snapshot = new TransportSnapshot(0.0, 0.0, 0.0, 0.0, DEFAULT_GAS_DIFFUSIVITY, - DEFAULT_LIQUID_DIFFUSIVITY, 0.0, 0.0, 0.0, DEFAULT_GAS_HEAT_CAPACITY, DEFAULT_LIQUID_HEAT_CAPACITY, 0.0, 0.0, - 0.0, interfaceTemperatureK, 0.0, 0.0); + DEFAULT_LIQUID_DIFFUSIVITY, 0.0, 0.0, 0.0, DEFAULT_GAS_HEAT_CAPACITY, + DEFAULT_LIQUID_HEAT_CAPACITY, 0.0, 0.0, 0.0, interfaceTemperatureK, 0.0, 0.0); return calculateInterfaceEquilibrium(gas, liquid, snapshot); } @@ -2337,12 +2423,13 @@ private InterfaceEquilibrium calculateInterfaceEquilibrium(SystemInterface gas, * @param gasPhase true when the phase is gas * @return component film coefficient in 1/s */ - private double calculateFilmCoefficient(PhaseInterface phase, String component, double baseCoefficient, - double referenceDiffusivity, boolean gasPhase) { + private double calculateFilmCoefficient(PhaseInterface phase, String component, + double baseCoefficient, double referenceDiffusivity, boolean gasPhase) { if (filmModel != FilmModel.MAXWELL_STEFAN_MATRIX || phase == null) { return baseCoefficient; } - return maxwellStefanFilmCoefficient(phase, component, baseCoefficient, referenceDiffusivity, gasPhase); + return maxwellStefanFilmCoefficient(phase, component, baseCoefficient, referenceDiffusivity, + gasPhase); } /** @@ -2355,50 +2442,54 @@ private double calculateFilmCoefficient(PhaseInterface phase, String component, * @param gasPhase true when the phase is gas * @return Maxwell-Stefan corrected film coefficient in 1/s */ - private double maxwellStefanFilmCoefficient(PhaseInterface phase, String component, double baseCoefficient, - double referenceDiffusivity, boolean gasPhase) { + private double maxwellStefanFilmCoefficient(PhaseInterface phase, String component, + double baseCoefficient, double referenceDiffusivity, boolean gasPhase) { if (!isFinitePositive(baseCoefficient)) { return 0.0; } int componentIndex = componentIndex(phase, component); int componentCount = phase.getNumberOfComponents(); if (componentIndex < 0 || componentCount <= 2) { - double diffusivity = mixtureDiffusivityForComponent(phase, componentIndex, referenceDiffusivity, gasPhase); + double diffusivity = + mixtureDiffusivityForComponent(phase, componentIndex, referenceDiffusivity, gasPhase); return scaleFilmCoefficient(baseCoefficient, diffusivity, referenceDiffusivity); } int reducedDimension = componentCount - 1; if (componentIndex >= reducedDimension) { - double diffusivity = mixtureDiffusivityForComponent(phase, componentIndex, referenceDiffusivity, gasPhase); + double diffusivity = + mixtureDiffusivityForComponent(phase, componentIndex, referenceDiffusivity, gasPhase); return scaleFilmCoefficient(baseCoefficient, diffusivity, referenceDiffusivity); } try { Matrix resistanceMatrix = new Matrix(reducedDimension, reducedDimension); for (int row = 0; row < reducedDimension; row++) { - double rowSum = 0.0; - double referenceCoefficient = binaryFilmCoefficient(phase, row, reducedDimension, baseCoefficient, - referenceDiffusivity, gasPhase); - for (int column = 0; column < componentCount; column++) { - double binaryCoefficient = binaryFilmCoefficient(phase, row, column, baseCoefficient, referenceDiffusivity, - gasPhase); - if (row != column) { - rowSum += moleFraction(phase, column) / binaryCoefficient; - } - if (column < reducedDimension) { - double value = -moleFraction(phase, row) * (1.0 / binaryCoefficient - 1.0 / referenceCoefficient); - resistanceMatrix.set(row, column, value); - } - } - resistanceMatrix.set(row, row, - resistanceMatrix.get(row, row) + rowSum + moleFraction(phase, row) / referenceCoefficient); + double rowSum = 0.0; + double referenceCoefficient = binaryFilmCoefficient(phase, row, reducedDimension, + baseCoefficient, referenceDiffusivity, gasPhase); + for (int column = 0; column < componentCount; column++) { + double binaryCoefficient = binaryFilmCoefficient(phase, row, column, baseCoefficient, + referenceDiffusivity, gasPhase); + if (row != column) { + rowSum += moleFraction(phase, column) / binaryCoefficient; + } + if (column < reducedDimension) { + double value = + -moleFraction(phase, row) * (1.0 / binaryCoefficient - 1.0 / referenceCoefficient); + resistanceMatrix.set(row, column, value); + } + } + resistanceMatrix.set(row, row, resistanceMatrix.get(row, row) + rowSum + + moleFraction(phase, row) / referenceCoefficient); } Matrix coefficientMatrix = resistanceMatrix.inverse(); double coefficient = coefficientMatrix.get(componentIndex, componentIndex); if (isFinitePositive(coefficient)) { - return clamp(coefficient, baseCoefficient * 0.02, baseCoefficient * 50.0); + return clamp(coefficient, baseCoefficient * 0.02, baseCoefficient * 50.0); } } catch (RuntimeException ex) { return scaleFilmCoefficient(baseCoefficient, - mixtureDiffusivityForComponent(phase, componentIndex, referenceDiffusivity, gasPhase), referenceDiffusivity); + mixtureDiffusivityForComponent(phase, componentIndex, referenceDiffusivity, gasPhase), + referenceDiffusivity); } return baseCoefficient; } @@ -2432,8 +2523,10 @@ private double combineFilmFluxes(double gasFluxDensity, double liquidFluxDensity * @param snapshot transport snapshot for the segment * @return heat-transfer rate in W, positive from gas to liquid */ - private double applyInterphaseHeatTransfer(SystemInterface gas, SystemInterface liquid, TransportSnapshot snapshot) { - if (heatTransferModel == HeatTransferModel.NONE || !isFinitePositive(snapshot.overallHeatTransferCoefficient)) { + private double applyInterphaseHeatTransfer(SystemInterface gas, SystemInterface liquid, + TransportSnapshot snapshot) { + if (heatTransferModel == HeatTransferModel.NONE + || !isFinitePositive(snapshot.overallHeatTransferCoefficient)) { return 0.0; } double temperatureDifference = gas.getTemperature() - liquid.getTemperature(); @@ -2441,20 +2534,24 @@ private double applyInterphaseHeatTransfer(SystemInterface gas, SystemInterface return 0.0; } double gasHeatCapacityRate = heatCapacityRate(getGasPhase(gas), snapshot.gasHeatCapacity); - double liquidHeatCapacityRate = heatCapacityRate(getLiquidPhase(liquid), snapshot.liquidHeatCapacity); + double liquidHeatCapacityRate = + heatCapacityRate(getLiquidPhase(liquid), snapshot.liquidHeatCapacity); if (!isFinitePositive(gasHeatCapacityRate) || !isFinitePositive(liquidHeatCapacityRate)) { return 0.0; } - double segmentVolume = Math.PI * columnDiameter * columnDiameter / 4.0 * packedHeight / numberOfSegments; - double heatRate = snapshot.overallHeatTransferCoefficient * segmentVolume * temperatureDifference; - double maximumHeatRate = Math.min(gasHeatCapacityRate, liquidHeatCapacityRate) * Math.abs(temperatureDifference) - * maxHeatTransferFractionPerSegment; + double segmentVolume = + Math.PI * columnDiameter * columnDiameter / 4.0 * packedHeight / numberOfSegments; + double heatRate = + snapshot.overallHeatTransferCoefficient * segmentVolume * temperatureDifference; + double maximumHeatRate = Math.min(gasHeatCapacityRate, liquidHeatCapacityRate) + * Math.abs(temperatureDifference) * maxHeatTransferFractionPerSegment; heatRate = Math.signum(heatRate) * Math.min(Math.abs(heatRate), maximumHeatRate); if (Math.abs(heatRate) <= 0.0) { return 0.0; } gas.setTemperature(Math.max(1.0, gas.getTemperature() - heatRate / gasHeatCapacityRate)); - liquid.setTemperature(Math.max(1.0, liquid.getTemperature() + heatRate / liquidHeatCapacityRate)); + liquid + .setTemperature(Math.max(1.0, liquid.getTemperature() + heatRate / liquidHeatCapacityRate)); return heatRate; } @@ -2469,11 +2566,13 @@ private double applyInterphaseHeatTransfer(SystemInterface gas, SystemInterface * @param thermalConductivity thermal conductivity in W/(m K) * @return volumetric heat-transfer coefficient in W/(m3 K) */ - private double calculateVolumetricHeatTransferCoefficient(double massTransferCoefficient, double density, - double heatCapacity, double viscosity, double diffusivity, double thermalConductivity) { + private double calculateVolumetricHeatTransferCoefficient(double massTransferCoefficient, + double density, double heatCapacity, double viscosity, double diffusivity, + double thermalConductivity) { if (heatTransferModel == HeatTransferModel.NONE || !isFinitePositive(massTransferCoefficient) - || !isFinitePositive(density) || !isFinitePositive(heatCapacity) || !isFinitePositive(viscosity) - || !isFinitePositive(diffusivity) || !isFinitePositive(thermalConductivity)) { + || !isFinitePositive(density) || !isFinitePositive(heatCapacity) + || !isFinitePositive(viscosity) || !isFinitePositive(diffusivity) + || !isFinitePositive(thermalConductivity)) { return 0.0; } double prandtlNumber = heatCapacity * viscosity / thermalConductivity; @@ -2482,7 +2581,8 @@ private double calculateVolumetricHeatTransferCoefficient(double massTransferCoe return 0.0; } double analogyFactor = Math.pow(schmidtNumber / prandtlNumber, 2.0 / 3.0); - return massTransferCoefficient * density * heatCapacity * analogyFactor * heatTransferCorrectionFactor; + return massTransferCoefficient * density * heatCapacity * analogyFactor + * heatTransferCorrectionFactor; } /** @@ -2508,13 +2608,13 @@ private double combineHeatTransferCoefficients(double gasCoefficient, double liq * @param liquidCoefficient liquid-side heat-transfer coefficient in W/(m3 K) * @return estimated interface temperature in K */ - private double calculateInterfaceTemperature(double gasTemperature, double liquidTemperature, double gasCoefficient, - double liquidCoefficient) { + private double calculateInterfaceTemperature(double gasTemperature, double liquidTemperature, + double gasCoefficient, double liquidCoefficient) { if (!isFinitePositive(gasCoefficient) || !isFinitePositive(liquidCoefficient)) { return 0.5 * (gasTemperature + liquidTemperature); } return (gasCoefficient * gasTemperature + liquidCoefficient * liquidTemperature) - / (gasCoefficient + liquidCoefficient); + / (gasCoefficient + liquidCoefficient); } /** @@ -2555,7 +2655,8 @@ private double phaseMolarEnthalpy(PhaseInterface phase) { * @param fallback fallback molar enthalpy in J/mol * @return component molar enthalpy in J/mol */ - private double componentMolarEnthalpy(PhaseInterface phase, String component, double temperature, double fallback) { + private double componentMolarEnthalpy(PhaseInterface phase, String component, double temperature, + double fallback) { if (phase == null || component == null || !phase.hasComponent(component)) { return fallback; } @@ -2606,7 +2707,7 @@ private int componentIndex(PhaseInterface phase, String component) { } for (int i = 0; i < phase.getNumberOfComponents(); i++) { if (component.equals(phase.getComponent(i).getComponentName())) { - return i; + return i; } } return -1; @@ -2637,14 +2738,14 @@ private double moleFraction(PhaseInterface phase, int componentIndex) { * @param gasPhase true for gas fallback diffusivity * @return binary film coefficient in 1/s */ - private double binaryFilmCoefficient(PhaseInterface phase, int firstComponent, int secondComponent, - double baseCoefficient, double referenceDiffusivity, boolean gasPhase) { + private double binaryFilmCoefficient(PhaseInterface phase, int firstComponent, + int secondComponent, double baseCoefficient, double referenceDiffusivity, boolean gasPhase) { if (firstComponent == secondComponent) { return baseCoefficient; } return scaleFilmCoefficient(baseCoefficient, - binaryDiffusivity(phase, firstComponent, secondComponent, referenceDiffusivity, gasPhase), - referenceDiffusivity); + binaryDiffusivity(phase, firstComponent, secondComponent, referenceDiffusivity, gasPhase), + referenceDiffusivity); } /** @@ -2655,7 +2756,8 @@ private double binaryFilmCoefficient(PhaseInterface phase, int firstComponent, i * @param referenceDiffusivity reference diffusivity in m2/s * @return scaled film coefficient in 1/s */ - private double scaleFilmCoefficient(double baseCoefficient, double diffusivity, double referenceDiffusivity) { + private double scaleFilmCoefficient(double baseCoefficient, double diffusivity, + double referenceDiffusivity) { double reference = finitePositive(referenceDiffusivity, diffusivity); double scaled = baseCoefficient * finitePositive(diffusivity, reference) / reference; return clamp(scaled, baseCoefficient * 0.02, baseCoefficient * 50.0); @@ -2670,16 +2772,17 @@ private double scaleFilmCoefficient(double baseCoefficient, double diffusivity, * @param gasPhase true for gas fallback diffusivity * @return mixture diffusivity in m2/s */ - private double mixtureDiffusivityForComponent(PhaseInterface phase, int componentIndex, double referenceDiffusivity, - boolean gasPhase) { + private double mixtureDiffusivityForComponent(PhaseInterface phase, int componentIndex, + double referenceDiffusivity, boolean gasPhase) { if (componentIndex < 0 || phase.getNumberOfComponents() <= 1) { return referenceDiffusivity; } double resistance = 0.0; for (int i = 0; i < phase.getNumberOfComponents(); i++) { if (i != componentIndex) { - double diffusivity = binaryDiffusivity(phase, componentIndex, i, referenceDiffusivity, gasPhase); - resistance += moleFraction(phase, i) / diffusivity; + double diffusivity = + binaryDiffusivity(phase, componentIndex, i, referenceDiffusivity, gasPhase); + resistance += moleFraction(phase, i) / diffusivity; } } if (isFinitePositive(resistance)) { @@ -2701,9 +2804,10 @@ private double mixtureDiffusivityForComponent(PhaseInterface phase, int componen private double binaryDiffusivity(PhaseInterface phase, int firstComponent, int secondComponent, double referenceDiffusivity, boolean gasPhase) { try { - double value = phase.getPhysicalProperties().getDiffusionCoefficient(firstComponent, secondComponent); + double value = + phase.getPhysicalProperties().getDiffusionCoefficient(firstComponent, secondComponent); if (isFinitePositive(value)) { - return value; + return value; } } catch (RuntimeException ex) { // Fallback below uses effective component diffusivity or robust defaults. @@ -2711,12 +2815,13 @@ private double binaryDiffusivity(PhaseInterface phase, int firstComponent, int s try { double value = phase.getPhysicalProperties().getEffectiveDiffusionCoefficient(firstComponent); if (isFinitePositive(value)) { - return value; + return value; } } catch (RuntimeException ex) { // Fallback below keeps Maxwell-Stefan correction robust for sparse property models. } - return finitePositive(referenceDiffusivity, gasPhase ? DEFAULT_GAS_DIFFUSIVITY : DEFAULT_LIQUID_DIFFUSIVITY); + return finitePositive(referenceDiffusivity, + gasPhase ? DEFAULT_GAS_DIFFUSIVITY : DEFAULT_LIQUID_DIFFUSIVITY); } /** @@ -2739,9 +2844,9 @@ private double calculateOutletResidual(SystemInterface previousGas, SystemInterf components.addAll(getComponentNames(currentLiquid)); for (String component : components) { residual = Math.max(residual, - Math.abs(componentMoles(previousGas, component) - componentMoles(currentGas, component))); - residual = Math.max(residual, - Math.abs(componentMoles(previousLiquid, component) - componentMoles(currentLiquid, component))); + Math.abs(componentMoles(previousGas, component) - componentMoles(currentGas, component))); + residual = Math.max(residual, Math.abs( + componentMoles(previousLiquid, component) - componentMoles(currentLiquid, component))); } return residual; } @@ -2792,7 +2897,7 @@ private PhaseInterface getLiquidPhase(SystemInterface system) { } for (int phase = 0; phase < system.getNumberOfPhases(); phase++) { if (system.getPhase(phase).getType() != PhaseType.GAS) { - return system.getPhase(phase); + return system.getPhase(phase); } } return system.getPhase(0); @@ -2811,13 +2916,13 @@ private double averageDiffusivity(PhaseInterface phase, boolean gasPhase) { PhysicalProperties properties = phase.getPhysicalProperties(); for (int component = 0; component < phase.getNumberOfComponents(); component++) { try { - double value = properties.getEffectiveDiffusionCoefficient(component); - if (isFinitePositive(value)) { - sum += value; - count++; - } + double value = properties.getEffectiveDiffusionCoefficient(component); + if (isFinitePositive(value)) { + sum += value; + count++; + } } catch (RuntimeException ex) { - // Fallback below keeps the column robust when a diffusion model is unavailable. + // Fallback below keeps the column robust when a diffusion model is unavailable. } } if (count > 0) { @@ -2839,14 +2944,15 @@ private double estimateSurfaceTension(SystemInterface gas, SystemInterface liqui flashAndInitialize(mixed); try { if (mixed.hasPhaseType(PhaseType.GAS) && mixed.getNumberOfPhases() > 1) { - int gasPhaseNumber = mixed.getPhaseNumberOfPhase(PhaseType.GAS); - int liquidPhaseNumber = getLiquidPhaseNumber(mixed); - if (liquidPhaseNumber >= 0 && liquidPhaseNumber != gasPhaseNumber) { - double value = mixed.getInterphaseProperties().getSurfaceTension(gasPhaseNumber, liquidPhaseNumber); - if (isFinitePositive(value)) { - return value; - } - } + int gasPhaseNumber = mixed.getPhaseNumberOfPhase(PhaseType.GAS); + int liquidPhaseNumber = getLiquidPhaseNumber(mixed); + if (liquidPhaseNumber >= 0 && liquidPhaseNumber != gasPhaseNumber) { + double value = + mixed.getInterphaseProperties().getSurfaceTension(gasPhaseNumber, liquidPhaseNumber); + if (isFinitePositive(value)) { + return value; + } + } } } catch (RuntimeException ex) { return DEFAULT_SURFACE_TENSION; @@ -2867,8 +2973,8 @@ private void addSystemComponents(SystemInterface target, SystemInterface source) String component = components.get(i); double moles = componentMoles(source, component); if (moles > 0.0) { - target.addComponent(component, moles); - addedComponent = true; + target.addComponent(component, moles); + addedComponent = true; } } if (addedComponent) { @@ -2886,11 +2992,11 @@ private void refreshComponentDatabase(SystemInterface system) { try { system.createDatabase(true); if (mixingRuleName != null && !mixingRuleName.trim().isEmpty()) { - system.setMixingRule(mixingRuleName); + system.setMixingRule(mixingRuleName); } } catch (RuntimeException ex) { if (mixingRuleName == null || mixingRuleName.trim().isEmpty()) { - system.setMixingRule("classic"); + system.setMixingRule("classic"); } } } @@ -2913,7 +3019,7 @@ private int getLiquidPhaseNumber(SystemInterface system) { } for (int phase = 0; phase < system.getNumberOfPhases(); phase++) { if (system.getPhase(phase).getType() != PhaseType.GAS) { - return phase; + return phase; } } return -1; @@ -3210,18 +3316,23 @@ public static class SegmentResult implements java.io.Serializable { * @param residualIterations residual solver iterations * @param maxFluxResidualMolPerSec maximum component flux residual in mol/s * @param heatBalanceResidualW interfacial heat-balance residual in W - * @param enthalpyBalanceResidualW total outlet enthalpy-balance residual in W-equivalent stream basis + * @param enthalpyBalanceResidualW total outlet enthalpy-balance residual in W-equivalent stream + * basis */ - public SegmentResult(int segmentNumber, double heightFromBottom, double gasTemperatureK, double liquidTemperatureK, - double gasPressureBar, double liquidPressureBar, double gasMolarFlow, double liquidMolarFlow, double gasDensity, - double liquidDensity, double gasViscosity, double liquidViscosity, double gasDiffusivity, - double liquidDiffusivity, double wettedArea, double kGa, double kLa, double gasHeatTransferCoefficient, - double liquidHeatTransferCoefficient, double overallHeatTransferCoefficient, double interfaceTemperatureK, - double heatTransferRateW, double pressureDropPerMeter, double percentFlood, double netMolarTransfer, - Map componentMoleTransfer, Map interfaceGasMoleFractions, - Map interfaceLiquidMoleFractions, Map interfaceEquilibriumRatios, - String segmentSolver, int residualIterations, double maxFluxResidualMolPerSec, double heatBalanceResidualW, - double enthalpyBalanceResidualW) { + public SegmentResult(int segmentNumber, double heightFromBottom, double gasTemperatureK, + double liquidTemperatureK, double gasPressureBar, double liquidPressureBar, + double gasMolarFlow, double liquidMolarFlow, double gasDensity, double liquidDensity, + double gasViscosity, double liquidViscosity, double gasDiffusivity, + double liquidDiffusivity, double wettedArea, double kGa, double kLa, + double gasHeatTransferCoefficient, double liquidHeatTransferCoefficient, + double overallHeatTransferCoefficient, double interfaceTemperatureK, + double heatTransferRateW, double pressureDropPerMeter, double percentFlood, + double netMolarTransfer, Map componentMoleTransfer, + Map interfaceGasMoleFractions, + Map interfaceLiquidMoleFractions, + Map interfaceEquilibriumRatios, String segmentSolver, + int residualIterations, double maxFluxResidualMolPerSec, double heatBalanceResidualW, + double enthalpyBalanceResidualW) { this.segmentNumber = segmentNumber; this.heightFromBottom = heightFromBottom; this.gasTemperatureK = gasTemperatureK; @@ -3249,8 +3360,10 @@ public SegmentResult(int segmentNumber, double heightFromBottom, double gasTempe this.netMolarTransfer = netMolarTransfer; this.componentMoleTransfer = new LinkedHashMap(componentMoleTransfer); this.interfaceGasMoleFractions = new LinkedHashMap(interfaceGasMoleFractions); - this.interfaceLiquidMoleFractions = new LinkedHashMap(interfaceLiquidMoleFractions); - this.interfaceEquilibriumRatios = new LinkedHashMap(interfaceEquilibriumRatios); + this.interfaceLiquidMoleFractions = + new LinkedHashMap(interfaceLiquidMoleFractions); + this.interfaceEquilibriumRatios = + new LinkedHashMap(interfaceEquilibriumRatios); this.segmentSolver = segmentSolver; this.residualIterations = residualIterations; this.maxFluxResidualMolPerSec = maxFluxResidualMolPerSec; @@ -3581,7 +3694,8 @@ private static class SegmentComputation { * @param liquidOutlet segment liquid outlet system * @param result segment result */ - private SegmentComputation(SystemInterface gasOutlet, SystemInterface liquidOutlet, SegmentResult result) { + private SegmentComputation(SystemInterface gasOutlet, SystemInterface liquidOutlet, + SegmentResult result) { this.gasOutlet = gasOutlet; this.liquidOutlet = liquidOutlet; this.result = result; @@ -3608,7 +3722,7 @@ private static class CounterCurrentSolution { * @param segmentResults segment profile results */ private CounterCurrentSolution(SystemInterface gasOutlet, SystemInterface liquidOutlet, - List liquidLeavingSegments, List segmentResults) { + List liquidLeavingSegments, List segmentResults) { this.gasOutlet = gasOutlet; this.liquidOutlet = liquidOutlet; this.liquidLeavingSegments = liquidLeavingSegments; @@ -3674,11 +3788,12 @@ private static class TransportSnapshot { * @param pressureDropPerMeter pressure drop per metre in Pa/m * @param percentFlood percent flooding */ - private TransportSnapshot(double gasDensity, double liquidDensity, double gasViscosity, double liquidViscosity, - double gasDiffusivity, double liquidDiffusivity, double wettedArea, double kGa, double kLa, - double gasHeatCapacity, double liquidHeatCapacity, double gasHeatTransferCoefficient, - double liquidHeatTransferCoefficient, double overallHeatTransferCoefficient, double interfaceTemperatureK, - double pressureDropPerMeter, double percentFlood) { + private TransportSnapshot(double gasDensity, double liquidDensity, double gasViscosity, + double liquidViscosity, double gasDiffusivity, double liquidDiffusivity, double wettedArea, + double kGa, double kLa, double gasHeatCapacity, double liquidHeatCapacity, + double gasHeatTransferCoefficient, double liquidHeatTransferCoefficient, + double overallHeatTransferCoefficient, double interfaceTemperatureK, + double pressureDropPerMeter, double percentFlood) { this.gasDensity = gasDensity; this.liquidDensity = liquidDensity; this.gasViscosity = gasViscosity; @@ -3725,8 +3840,8 @@ private static class InterfaceEquilibrium { * @param liquidMolarEnthalpies liquid-side component molar enthalpies by component */ private InterfaceEquilibrium(double interfaceTemperatureK, Map gasMoleFractions, - Map liquidMoleFractions, Map equilibriumRatios, - Map gasMolarEnthalpies, Map liquidMolarEnthalpies) { + Map liquidMoleFractions, Map equilibriumRatios, + Map gasMolarEnthalpies, Map liquidMolarEnthalpies) { this.interfaceTemperatureK = interfaceTemperatureK; this.gasMoleFractions = new LinkedHashMap(gasMoleFractions); this.liquidMoleFractions = new LinkedHashMap(liquidMoleFractions); @@ -3819,8 +3934,8 @@ private static class ColumnState { * @param liquidOutlet column liquid outlet system */ private ColumnState(List gasEntering, List gasLeaving, - List liquidEntering, List liquidLeaving, SystemInterface gasOutlet, - SystemInterface liquidOutlet) { + List liquidEntering, List liquidLeaving, + SystemInterface gasOutlet, SystemInterface liquidOutlet) { this.gasEntering = gasEntering; this.gasLeaving = gasLeaving; this.liquidEntering = liquidEntering; @@ -3837,7 +3952,8 @@ private static class SparseJacobian { /** Column count. */ private final int columns; /** Sparse matrix values keyed by row then column. */ - private final Map> values = new LinkedHashMap>(); + private final Map> values = + new LinkedHashMap>(); /** * Create a sparse Jacobian. @@ -3860,27 +3976,12 @@ private SparseJacobian(int rows, int columns) { private void set(int row, int column, double value) { Map rowValues = values.get(Integer.valueOf(row)); if (rowValues == null) { - rowValues = new LinkedHashMap(); - values.put(Integer.valueOf(row), rowValues); + rowValues = new LinkedHashMap(); + values.put(Integer.valueOf(row), rowValues); } rowValues.put(Integer.valueOf(column), Double.valueOf(value)); } - /** - * Convert the sparse matrix to a dense matrix for the available linear solver. - * - * @return dense matrix representation - */ - private Matrix toDenseMatrix() { - double[][] dense = new double[rows][columns]; - for (Map.Entry> rowEntry : values.entrySet()) { - int row = rowEntry.getKey().intValue(); - for (Map.Entry columnEntry : rowEntry.getValue().entrySet()) { - dense[row][columnEntry.getKey().intValue()] = columnEntry.getValue().doubleValue(); - } - } - return new Matrix(dense); - } } /** Internal residual evaluation container for the equation-oriented column solver. */ @@ -3921,9 +4022,9 @@ private static class ColumnResidualEvaluation { * @param maxLiquidComponentBalanceResidual maximum liquid component-balance residual in mol/s */ private ColumnResidualEvaluation(double[] unknowns, double[] normalizedResiduals, double norm, - CounterCurrentSolution solution, int iterations, double maxFluxResidual, double maxHeatResidual, - double maxEnergyBalanceResidual, double maxGasComponentBalanceResidual, - double maxLiquidComponentBalanceResidual) { + CounterCurrentSolution solution, int iterations, double maxFluxResidual, + double maxHeatResidual, double maxEnergyBalanceResidual, + double maxGasComponentBalanceResidual, double maxLiquidComponentBalanceResidual) { this.unknowns = unknowns.clone(); this.normalizedResiduals = normalizedResiduals.clone(); this.norm = norm; @@ -3943,8 +4044,9 @@ private ColumnResidualEvaluation(double[] unknowns, double[] normalizedResiduals * @return residual evaluation with updated iteration count */ private ColumnResidualEvaluation withIterations(int iterations) { - return new ColumnResidualEvaluation(unknowns, normalizedResiduals, norm, solution, iterations, maxFluxResidual, - maxHeatResidual, maxEnergyBalanceResidual, maxGasComponentBalanceResidual, maxLiquidComponentBalanceResidual); + return new ColumnResidualEvaluation(unknowns, normalizedResiduals, norm, solution, iterations, + maxFluxResidual, maxHeatResidual, maxEnergyBalanceResidual, + maxGasComponentBalanceResidual, maxLiquidComponentBalanceResidual); } } @@ -3985,9 +4087,10 @@ private static class SegmentResidualEvaluation { * @param liquidTargetEnthalpy liquid outlet enthalpy target in J or W-equivalent stream basis * @param iterations residual iterations used for this evaluation */ - private SegmentResidualEvaluation(InterfaceEquilibrium interfaceEquilibrium, Map componentTransfers, - double[] normalizedResiduals, double norm, double maxFluxResidualMolPerSec, double heatBalanceResidualW, - double heatTransferRateW, double gasTargetEnthalpy, double liquidTargetEnthalpy, int iterations) { + private SegmentResidualEvaluation(InterfaceEquilibrium interfaceEquilibrium, + Map componentTransfers, double[] normalizedResiduals, double norm, + double maxFluxResidualMolPerSec, double heatBalanceResidualW, double heatTransferRateW, + double gasTargetEnthalpy, double liquidTargetEnthalpy, int iterations) { this.interfaceEquilibrium = interfaceEquilibrium; this.componentTransfers = new LinkedHashMap(componentTransfers); this.normalizedResiduals = normalizedResiduals.clone(); @@ -4074,7 +4177,8 @@ private ColumnReport(RateBasedPackedColumn column) { this.liquidComponentBalanceResidualMolPerSec = column.getLastLiquidComponentBalanceResidual(); this.columnEnergyBalanceResidualW = column.getLastColumnEnergyBalanceResidual(); this.totalAbsoluteMolarTransferMolPerSec = column.getTotalAbsoluteMolarTransfer(); - this.componentTransferMolPerSec = new LinkedHashMap(column.getComponentTransferTotals()); + this.componentTransferMolPerSec = + new LinkedHashMap(column.getComponentTransferTotals()); this.segments = new ArrayList(column.getSegmentResults()); } } diff --git a/src/main/java/neqsim/process/equipment/network/NetworkLinearSolver.java b/src/main/java/neqsim/process/equipment/network/NetworkLinearSolver.java index e127d58008..7d01381cf5 100644 --- a/src/main/java/neqsim/process/equipment/network/NetworkLinearSolver.java +++ b/src/main/java/neqsim/process/equipment/network/NetworkLinearSolver.java @@ -1,32 +1,28 @@ package neqsim.process.equipment.network; -import org.ejml.data.DMatrixRMaj; -import org.ejml.data.DMatrixSparseCSC; -import org.ejml.dense.row.factory.LinearSolverFactory_DDRM; -import org.ejml.interfaces.linsol.LinearSolverDense; -import org.ejml.sparse.csc.factory.LinearSolverFactory_DSCC; -import org.ejml.interfaces.linsol.LinearSolverSparse; import org.apache.logging.log4j.LogManager; import org.apache.logging.log4j.Logger; +import neqsim.util.math.LinearAlgebraOps; /** * Sparse and dense linear system solvers for pipeline network equations. * *

- * Replaces the O(n³) Gaussian elimination in the Newton-Raphson GGA solver with efficient alternatives from the EJML - * library (already a NeqSim dependency): + * Replaces the O(n³) Gaussian elimination in the Newton-Raphson GGA solver with efficient + * alternatives from ojAlgo: *

*
    - *
  • Sparse LU: For large networks (>50 nodes), uses compressed sparse column (CSC) format with EJML sparse - * LU. Complexity depends on the number of non-zeros in the Schur complement matrix.
  • - *
  • Dense LU: For small-to-medium networks (≤50 nodes), uses EJML's optimized dense LU with partial - * pivoting. Faster than hand-coded Gaussian for n > 10.
  • + *
  • Sparse path: For large sparse networks, routes through the sparse decision path and + * currently uses the dense ojAlgo LU backend for robustness.
  • + *
  • Dense LU: For small-to-medium networks (≤50 nodes), uses ojAlgo's dense LU with + * partial pivoting. Faster than hand-coded Gaussian for n > 10.
  • *
* *

- * The Schur complement matrix in the Todini-Pilati GGA is structurally sparse: entry (i,j) is nonzero only if nodes i - * and j are connected by at least one pipe. For a gas gathering network with n=100 nodes and average degree 3, matrix - * density is ~6%, making sparse solvers 10–50x faster than dense Gaussian. + * The Schur complement matrix in the Todini-Pilati GGA is structurally sparse: entry (i,j) is + * nonzero only if nodes i and j are connected by at least one pipe. For a gas gathering network + * with n=100 nodes and average degree 3, matrix density is ~6%, making sparse solvers 10–50x faster + * than dense Gaussian. *

* * @author Even Solbraa @@ -38,14 +34,15 @@ public class NetworkLinearSolver { private static final Logger logger = LogManager.getLogger(NetworkLinearSolver.class); /** - * Threshold for switching from Gaussian to EJML solvers. Networks with more free nodes than this threshold use EJML - * (dense LU for n ≤ 100, sparse CSC LU for n > 100). Below this threshold, Gaussian elimination is used for - * backward compatibility with existing solver convergence behavior. + * Threshold for switching from Gaussian to EJML solvers. Networks with more free nodes than this + * threshold use ojAlgo (dense LU for n ≤ 100, sparse path for n > 100). Below this + * threshold, Gaussian elimination is used for backward compatibility with existing solver + * convergence behavior. */ private static final int EJML_THRESHOLD = 30; /** - * Threshold for switching from dense to sparse EJML solver within the EJML path. + * Threshold for switching from dense to sparse-path solver within the ojAlgo path. */ private static final int SPARSE_THRESHOLD = 100; @@ -53,9 +50,10 @@ public class NetworkLinearSolver { * Solve the linear system Ax = b using the most appropriate method. * *

- * For small systems (n ≤ {@value #EJML_THRESHOLD}), uses Gaussian elimination with partial pivoting for backward - * compatibility. For medium systems, uses EJML dense LU. For large systems (n > {@value #SPARSE_THRESHOLD}), uses - * EJML sparse CSC LU. Falls back to Gaussian elimination if EJML solvers fail. + * For small systems (n ≤ {@value #EJML_THRESHOLD}), uses Gaussian elimination with partial + * pivoting for backward compatibility. For medium systems, uses ojAlgo dense LU. For large + * systems (n > {@value #SPARSE_THRESHOLD}), uses sparse-path solve. Falls back to Gaussian + * elimination if EJML solvers fail. *

* * @param matA coefficient matrix (n x n) @@ -80,22 +78,23 @@ public static double[] solve(double[][] matA, double[] vecB, int n) { try { if (n > SPARSE_THRESHOLD) { - return solveSparse(matA, vecB, n); + return solveSparse(matA, vecB, n); } else { - return solveDense(matA, vecB, n); + return solveDense(matA, vecB, n); } } catch (Exception e) { - logger.warn("EJML solver failed (n=" + n + "), falling back to Gaussian: " + e.getMessage()); + logger + .warn("ojAlgo solver failed (n=" + n + "), falling back to Gaussian: " + e.getMessage()); return solveGaussian(matA, vecB, n); } } /** - * Solve using EJML dense LU decomposition with partial pivoting. + * Solve using ojAlgo dense LU decomposition with partial pivoting. * *

- * Uses EJML LinearSolverFactory_DDRM.lu() which provides O(n³/3) factorization with BLAS-optimized operations. For - * n=50, approximately 5x faster than hand-coded Gaussian elimination. + * Uses ojAlgo LU decomposition which provides O(n³/3) factorization with BLAS-optimized + * operations. For n=50, approximately 5x faster than hand-coded Gaussian elimination. *

* * @param matA coefficient matrix (n x n) @@ -104,40 +103,20 @@ public static double[] solve(double[][] matA, double[] vecB, int n) { * @return solution vector x */ public static double[] solveDense(double[][] matA, double[] vecB, int n) { - DMatrixRMaj denseA = new DMatrixRMaj(n, n); - DMatrixRMaj denseB = new DMatrixRMaj(n, 1); - DMatrixRMaj denseX = new DMatrixRMaj(n, 1); - - // Copy data to EJML matrices - for (int i = 0; i < n; i++) { - for (int j = 0; j < n; j++) { - denseA.set(i, j, matA[i][j]); - } - denseB.set(i, 0, vecB[i]); - } - - LinearSolverDense solver = LinearSolverFactory_DDRM.lu(n); - if (!solver.setA(denseA)) { + double[] result = new double[n]; + if (!LinearAlgebraOps.solveLinearSystem(matA, vecB, result)) { logger.warn("Dense LU setA failed (singular?), falling back to Gaussian"); return solveGaussian(matA, vecB, n); } - solver.solve(denseB, denseX); - - double[] result = new double[n]; - for (int i = 0; i < n; i++) { - result[i] = denseX.get(i, 0); - } return result; } /** - * Solve using EJML sparse CSC LU decomposition. + * Solve using sparse-path dispatch. * *

- * Converts the dense Schur complement to compressed sparse column (CSC) format, discarding structural zeros. Uses - * EJML sparse LU with natural ordering; EJML 0.41/0.45 do not expose a deterministic AMD/COLAMD fill-reducing option - * through this factory. For a 100x100 matrix with 6% density, sparse storage can be significantly faster than dense - * Gaussian. + * Converts through the sparsity decision path. The current backend uses dense ojAlgo LU for + * robustness and deterministic behavior; dense fallback remains in place. *

* * @param matA coefficient matrix (n x n) — may have many zeros @@ -150,59 +129,32 @@ public static double[] solveSparse(double[][] matA, double[] vecB, int n) { int nnz = 0; for (int i = 0; i < n; i++) { for (int j = 0; j < n; j++) { - if (Math.abs(matA[i][j]) > 1e-30) { - nnz++; - } + if (Math.abs(matA[i][j]) > 1e-30) { + nnz++; + } } } double density = (double) nnz / (n * n); if (density > 0.5) { // Matrix is too dense for sparse solver benefit; use dense - logger.debug("Sparse matrix density " + String.format("%.1f%%", density * 100) + " too high, using dense solver"); - return solveDense(matA, vecB, n); - } - - // Build sparse CSC matrix - DMatrixSparseCSC sparseA = new DMatrixSparseCSC(n, n, nnz); - for (int j = 0; j < n; j++) { - for (int i = 0; i < n; i++) { - if (Math.abs(matA[i][j]) > 1e-30) { - sparseA.set(i, j, matA[i][j]); - } - } - } - - DMatrixRMaj denseB = new DMatrixRMaj(n, 1); - DMatrixRMaj denseX = new DMatrixRMaj(n, 1); - for (int i = 0; i < n; i++) { - denseB.set(i, 0, vecB[i]); - } - - LinearSolverSparse solver = LinearSolverFactory_DSCC - .lu(org.ejml.sparse.FillReducing.NONE); - if (!solver.setA(sparseA)) { - logger.warn("Sparse LU setA failed (singular?), falling back to dense"); + logger.debug("Sparse matrix density " + String.format("%.1f%%", density * 100) + + " too high, using dense solver"); return solveDense(matA, vecB, n); } - solver.solve(denseB, denseX); - double[] result = new double[n]; - for (int i = 0; i < n; i++) { - result[i] = denseX.get(i, 0); - } - - logger.debug(String.format("Sparse solve: n=%d, nnz=%d, density=%.1f%%", n, nnz, density * 100)); - - return result; + logger.debug( + String.format("Sparse path dispatch using dense backend: n=%d, nnz=%d, density=%.1f%%", n, + nnz, density * 100)); + return solveDense(matA, vecB, n); } /** * Fallback: Gaussian elimination with partial pivoting. * *

- * This is the original O(n³) solver, kept as a robust fallback when EJML solvers encounter issues (singular matrices, - * numerical edge cases). Makes a defensive copy of the input arrays. + * This is the original O(n³) solver, kept as a robust fallback when EJML solvers encounter issues + * (singular matrices, numerical edge cases). Makes a defensive copy of the input arrays. *

* * @param matAOrig coefficient matrix (n x n) — not modified @@ -225,9 +177,9 @@ public static double[] solveGaussian(double[][] matAOrig, double[] vecBOrig, int for (int k = 0; k < n; k++) { int maxRow = k; for (int i = k + 1; i < n; i++) { - if (Math.abs(matA[i][k]) > Math.abs(matA[maxRow][k])) { - maxRow = i; - } + if (Math.abs(matA[i][k]) > Math.abs(matA[maxRow][k])) { + maxRow = i; + } } double[] tempRow = matA[k]; matA[k] = matA[maxRow]; @@ -237,15 +189,15 @@ public static double[] solveGaussian(double[][] matAOrig, double[] vecBOrig, int vecB[maxRow] = tempB; if (Math.abs(matA[k][k]) < 1e-20) { - continue; + continue; } for (int i = k + 1; i < n; i++) { - double factor = matA[i][k] / matA[k][k]; - for (int j = k + 1; j < n; j++) { - matA[i][j] -= factor * matA[k][j]; - } - vecB[i] -= factor * vecB[k]; + double factor = matA[i][k] / matA[k][k]; + for (int j = k + 1; j < n; j++) { + matA[i][j] -= factor * matA[k][j]; + } + vecB[i] -= factor * vecB[k]; } } @@ -253,10 +205,10 @@ public static double[] solveGaussian(double[][] matAOrig, double[] vecBOrig, int for (int i = n - 1; i >= 0; i--) { x[i] = vecB[i]; for (int j = i + 1; j < n; j++) { - x[i] -= matA[i][j] * x[j]; + x[i] -= matA[i][j] * x[j]; } if (Math.abs(matA[i][i]) > 1e-20) { - x[i] /= matA[i][i]; + x[i] /= matA[i][i]; } } return x; @@ -271,8 +223,8 @@ public static double[] solveGaussian(double[][] matAOrig, double[] vecBOrig, int * * @param nodeCount number of free nodes * @param pipeCount number of pipe elements - * @return array [density, nonzeros, recommended_threshold] where recommended_threshold is 0 for dense and 1 for - * sparse + * @return array [density, nonzeros, recommended_threshold] where recommended_threshold is 0 for + * dense and 1 for sparse */ public static double[] estimateSparsity(int nodeCount, int pipeCount) { // In a pipe network, each pipe connects exactly 2 nodes. @@ -283,6 +235,6 @@ public static double[] estimateSparsity(int nodeCount, int pipeCount) { int totalEntries = nodeCount * nodeCount; double density = (totalEntries > 0) ? (double) estimatedNnz / totalEntries : 1.0; double usesSparse = (nodeCount > SPARSE_THRESHOLD && density < 0.5) ? 1.0 : 0.0; - return new double[] { density, estimatedNnz, usesSparse }; + return new double[] {density, estimatedNnz, usesSparse}; } } diff --git a/src/main/java/neqsim/process/equipment/reactor/GibbsReactor.java b/src/main/java/neqsim/process/equipment/reactor/GibbsReactor.java index 3db756aec2..6e0c203800 100644 --- a/src/main/java/neqsim/process/equipment/reactor/GibbsReactor.java +++ b/src/main/java/neqsim/process/equipment/reactor/GibbsReactor.java @@ -1,5 +1,10 @@ package neqsim.process.equipment.reactor; +import neqsim.process.equipment.TwoPortEquipment; +import neqsim.process.equipment.stream.StreamInterface; +import neqsim.thermo.system.SystemInterface; +import neqsim.util.math.LinearAlgebraOps; + import java.io.InputStream; import java.util.ArrayList; import java.util.Collections; @@ -9,20 +14,17 @@ import java.util.Scanner; import java.util.UUID; import java.util.regex.Pattern; + import org.apache.logging.log4j.LogManager; import org.apache.logging.log4j.Logger; -import org.ejml.simple.SimpleMatrix; -import neqsim.process.equipment.TwoPortEquipment; -import neqsim.process.equipment.stream.StreamInterface; -import neqsim.thermo.system.SystemInterface; /** * Gibbs reactor for chemical equilibrium calculations using Gibbs free energy minimization. * *

- * This reactor computes chemical equilibrium compositions by minimizing the total Gibbs free energy of the system - * subject to elemental mass balance constraints. The implementation uses the Newton-Raphson method with Lagrange - * multipliers to solve the constrained optimization problem. + * This reactor computes chemical equilibrium compositions by minimizing the total Gibbs free energy + * of the system subject to elemental mass balance constraints. The implementation uses the + * Newton-Raphson method with Lagrange multipliers to solve the constrained optimization problem. *

* *

Key Features

@@ -42,8 +44,9 @@ * G = Σ nᵢ(μᵢ⁰ + RT ln(φᵢyᵢP)) - Σ λⱼ(Σ aᵢⱼnᵢ - bⱼ) * *

- * where nᵢ are molar amounts, μᵢ⁰ is standard chemical potential, φᵢ is fugacity coefficient, yᵢ is mole fraction, λⱼ - * are Lagrange multipliers, aᵢⱼ are stoichiometric coefficients, and bⱼ are element totals. + * where nᵢ are molar amounts, μᵢ⁰ is standard chemical potential, φᵢ is fugacity coefficient, yᵢ is + * mole fraction, λⱼ are Lagrange multipliers, aᵢⱼ are stoichiometric coefficients, and bⱼ are + * element totals. *

* *

Usage Example

@@ -68,7 +71,8 @@ */ public class GibbsReactor extends TwoPortEquipment { /** - * Get the relative mass balance error as a percentage. Computed as {@code 100 * |massIn - massOut| / massIn}. + * Get the relative mass balance error as a percentage. Computed as + * {@code 100 * |massIn - massOut| / massIn}. * * @return relative mass balance error in percent (e.g., 0.001 means 0.001%) */ @@ -94,7 +98,8 @@ public boolean getMassBalanceConverged() { } // Thread-local reusable system for fugacity calculations to minimize cloning - private transient ThreadLocal tempFugacitySystem = new ThreadLocal<>(); + private transient ThreadLocal tempFugacitySystem = + new ThreadLocal<>(); /** * Ensures tempFugacitySystem is initialized (handles deserialization case). @@ -143,13 +148,13 @@ public double getPower(String unit) { return getPower(); } switch (unit.trim().toLowerCase()) { - case "kw": - return -enthalpyOfReactions; - case "mw": - return -enthalpyOfReactions / 1000.0; - case "w": - default: - return -enthalpyOfReactions * 1000.0; + case "kw": + return -enthalpyOfReactions; + case "mw": + return -enthalpyOfReactions / 1000.0; + case "w": + default: + return -enthalpyOfReactions * 1000.0; } } @@ -169,9 +174,9 @@ public double calculateMixtureEnthalpy(List componentNames, List String compName = componentNames.get(i); GibbsComponent comp = componentMap.get(compName.toLowerCase()); if (comp == null) { - logger.warn( - "Component '" + compName + "' not found in gibbsReactDatabase. Neglecting from enthalpy calculation."); - continue; + logger.warn("Component '" + compName + + "' not found in gibbsReactDatabase. Neglecting from enthalpy calculation."); + continue; } totalH += n.get(i) * comp.calculateEnthalpy(T, i); } @@ -195,9 +200,9 @@ public double calculateMixtureGibbsEnergy(List componentNames, List componentNames, List String compName = componentNames.get(i); GibbsComponent comp = componentMap.get(compName.toLowerCase()); if (comp == null) { - logger.warn("Component '" + compName - + "' not found in gibbsReactDatabase. Neglecting from standard enthalpy calculation."); - continue; + logger.warn("Component '" + compName + + "' not found in gibbsReactDatabase. Neglecting from standard enthalpy calculation."); + continue; } totalH += n.get(i) * comp.calculateEnthalpy(REFERENCE_TEMPERATURE, i); // Use reference - // temperature for - // standard enthalpy + // temperature for + // standard enthalpy } return totalH; } @@ -267,9 +272,9 @@ public double calculateMixtureEnthalpy(List componentNames, List String compName = componentNames.get(i); GibbsComponent comp = componentMap.get(compName.toLowerCase()); if (comp == null) { - logger.warn( - "Component '" + compName + "' not found in gibbsReactDatabase. Neglecting from enthalpy calculation."); - continue; + logger.warn("Component '" + compName + + "' not found in gibbsReactDatabase. Neglecting from enthalpy calculation."); + continue; } totalH += n.get(i) * comp.calculateEnthalpy(T, i); // Use 298.15K for standard enthalpy } @@ -326,8 +331,8 @@ public void setEnergyMode(EnergyMode mode) { } /** - * Set the energy mode of the reactor using a string (case-insensitive). Accepts "adiabatic" or "isothermal" - * (case-insensitive). + * Set the energy mode of the reactor using a string (case-insensitive). Accepts "adiabatic" or + * "isothermal" (case-insensitive). * * @param mode String representing the energy mode * @throws java.lang.IllegalArgumentException if the mode is not recognized @@ -337,14 +342,14 @@ public void setEnergyMode(String mode) { throw new IllegalArgumentException("Energy mode string cannot be null"); } switch (mode.trim().toLowerCase()) { - case "adiabatic": - setEnergyMode(EnergyMode.ADIABATIC); - break; - case "isothermal": - setEnergyMode(EnergyMode.ISOTHERMAL); - break; - default: - throw new IllegalArgumentException("Unknown energy mode: " + mode); + case "adiabatic": + setEnergyMode(EnergyMode.ADIABATIC); + break; + case "isothermal": + setEnergyMode(EnergyMode.ISOTHERMAL); + break; + default: + throw new IllegalArgumentException("Unknown energy mode: " + mode); } } @@ -379,7 +384,7 @@ public EnergyMode getEnergyMode() { // Results from the last calculation private double[] lambda = new double[7]; // O, N, C, H, S, Ar, Z private Map lagrangeContributions = new HashMap<>(); - private String[] elementNames = { "O", "N", "C", "H", "S", "Ar", "Z" }; + private String[] elementNames = {"O", "N", "C", "H", "S", "Ar", "Z"}; private List processedComponents = new ArrayList<>(); private Map objectiveFunctionValues = new HashMap<>(); // Set of inert components (names in lowercase). Inert components are present in the @@ -457,15 +462,15 @@ public boolean isComponentInert(String componentName) { } /** - * Check whether a component has been automatically excluded from the optimization matrix because one or more of its - * constituent elements is not available in the feed. + * Check whether a component has been automatically excluded from the optimization matrix because + * one or more of its constituent elements is not available in the feed. * *

- * A species can only form if every element it contains is present in the feed. When an element has zero total feed - * availability, the corresponding element mass-balance constraint row is degenerate (its "determinator" is zero) and - * every species requiring that element is frozen at its feed amount and removed as an optimization variable. This - * prevents the solver from creating spurious trace amounts of species (for example sulfuric acid when no sulfur is - * fed). + * A species can only form if every element it contains is present in the feed. When an element + * has zero total feed availability, the corresponding element mass-balance constraint row is + * degenerate (its "determinator" is zero) and every species requiring that element is frozen at + * its feed amount and removed as an optimization variable. This prevents the solver from creating + * spurious trace amounts of species (for example sulfuric acid when no sulfur is fed). *

* * @param componentName the name of the component to check @@ -476,13 +481,14 @@ public boolean isComponentExcludedByFeed(String componentName) { } /** - * Determine which components must be excluded from the optimization matrix for the current run because at least one - * of their constituent elements is not available in the feed. + * Determine which components must be excluded from the optimization matrix for the current run + * because at least one of their constituent elements is not available in the feed. * *

- * Uses the already-computed inlet element mole balance ({@link #elementMoleBalanceIn}). For each component, if it has - * a non-zero coefficient for an element whose total feed availability is at or below {@link #ELEMENT_ZERO_THRESHOLD}, - * the component is added to {@link #feedExcludedComponents}. The result is recomputed on every run. + * Uses the already-computed inlet element mole balance ({@link #elementMoleBalanceIn}). For each + * component, if it has a non-zero coefficient for an element whose total feed availability is at + * or below {@link #ELEMENT_ZERO_THRESHOLD}, the component is added to + * {@link #feedExcludedComponents}. The result is recomputed on every run. *

* * @param system the thermodynamic system whose components are checked @@ -493,17 +499,17 @@ private void determineFeedExcludedComponents(SystemInterface system) { String compName = system.getComponent(i).getComponentName(); GibbsComponent comp = componentMap.get(compName.toLowerCase()); if (comp == null) { - continue; + continue; } double[] elements = comp.getElements(); for (int j = 0; j < elementNames.length; j++) { - if (Math.abs(elements[j]) > ELEMENT_ZERO_THRESHOLD - && Math.abs(elementMoleBalanceIn[j]) <= ELEMENT_ZERO_THRESHOLD) { - feedExcludedComponents.add(compName.toLowerCase()); - logger.debug("Excluding component '" + compName + "' from Gibbs matrix: element '" + elementNames[j] - + "' not available in feed."); - break; - } + if (Math.abs(elements[j]) > ELEMENT_ZERO_THRESHOLD + && Math.abs(elementMoleBalanceIn[j]) <= ELEMENT_ZERO_THRESHOLD) { + feedExcludedComponents.add(compName.toLowerCase()); + logger.debug("Excluding component '" + compName + "' from Gibbs matrix: element '" + + elementNames[j] + "' not available in feed."); + break; + } } } } @@ -524,9 +530,9 @@ private void determineFeedExcludedComponents(SystemInterface system) { * Enable Armijo backtracking line search for guaranteed Gibbs energy decrease. * *

- * When enabled, the Newton step is scaled by alpha in (0, 1] such that the Armijo sufficient decrease condition is - * satisfied: G(n + alpha * dn) <= G(n) + c1 * alpha * grad^T * dn. This replaces fixed damping with an adaptive, - * globally convergent strategy. + * When enabled, the Newton step is scaled by alpha in (0, 1] such that the Armijo sufficient + * decrease condition is satisfied: G(n + alpha * dn) <= G(n) + c1 * alpha * grad^T * dn. This + * replaces fixed damping with an adaptive, globally convergent strategy. *

* *

@@ -554,9 +560,9 @@ private void determineFeedExcludedComponents(SystemInterface system) { * Enable Tikhonov regularization for ill-conditioned Jacobians. * *

- * When the condition number of the Jacobian exceeds {@code regularizationThreshold}, a regularization term tau * I is - * added to the Hessian block: H_reg = H + tau * I. This prevents divergence near phase boundaries where the Hessian - * becomes singular. + * When the condition number of the Jacobian exceeds {@code regularizationThreshold}, a + * regularization term tau * I is added to the Hessian block: H_reg = H + tau * I. This prevents + * divergence near phase boundaries where the Hessian becomes singular. *

* *

@@ -596,15 +602,15 @@ private void determineFeedExcludedComponents(SystemInterface system) { private transient List elementBalanceErrorHistory = new ArrayList<>(); /** - * Column scaling factors from the NASA CEA log-mole transformation applied to the Jacobian. Each entry j stores the - * factor by which Jacobian column j was multiplied. The Newton step deltaX_scaled must be divided by these factors to - * obtain the true deltaX in mole units. + * Column scaling factors from the NASA CEA log-mole transformation applied to the Jacobian. Each + * entry j stores the factor by which Jacobian column j was multiplied. The Newton step + * deltaX_scaled must be divided by these factors to obtain the true deltaX in mole units. */ private transient double[] columnScaleFactors = null; /** - * Maximum relative change per step in moles, following NASA CEA step limiting (Gordon & McBride, 1994, NASA - * RP-1311). Prevents overshooting in Newton-Raphson iterations: + * Maximum relative change per step in moles, following NASA CEA step limiting (Gordon & + * McBride, 1994, NASA RP-1311). Prevents overshooting in Newton-Raphson iterations: * {@code max |alpha * deltaX[i]| / n_i < MAX_STEP_LIMIT}. */ private static final double MAX_STEP_LIMIT = Math.log(5.0); @@ -704,10 +710,10 @@ public class GibbsComponent { * @param coeffEh coefficient Eh for Gibbs calculations * @param coeffGh coefficient Gh for Gibbs calculations */ - public GibbsComponent(String molecule, double[] elements, double[] heatCapacityCoeffs, double deltaHf298, - double deltaGf298, double deltaSf298, double coeffAg, double coeffBg, double coeffCg, double coeffDg, - double coeffEg, double coeffFg, double coeffAh, double coeffBh, double coeffCh, double coeffDh, double coeffEh, - double coeffGh) { + public GibbsComponent(String molecule, double[] elements, double[] heatCapacityCoeffs, + double deltaHf298, double deltaGf298, double deltaSf298, double coeffAg, double coeffBg, + double coeffCg, double coeffDg, double coeffEg, double coeffFg, double coeffAh, + double coeffBh, double coeffCh, double coeffDh, double coeffEh, double coeffGh) { this.molecule = molecule; this.elements = elements.clone(); this.heatCapacityCoeffs = heatCapacityCoeffs.clone(); @@ -768,14 +774,14 @@ public double getDeltaGf298() { public double calculateGibbsEnergy(double temperature, int compNumber) { double T = temperature; // If any Gibbs coefficients are not NaN, use polynomial formula - if (!Double.isNaN(coeffAg) || !Double.isNaN(coeffBg) || !Double.isNaN(coeffCg) || !Double.isNaN(coeffDg) - || !Double.isNaN(coeffEg) || !Double.isNaN(coeffFg)) { - double gibbsPoly = (Double.isNaN(coeffAg) ? 0.0 : coeffAg) * Math.pow(T, 5) - + (Double.isNaN(coeffBg) ? 0.0 : coeffBg) * Math.pow(T, 4) - + (Double.isNaN(coeffCg) ? 0.0 : coeffCg) * Math.pow(T, 3) - + (Double.isNaN(coeffDg) ? 0.0 : coeffDg) * Math.pow(T, 2) + (Double.isNaN(coeffEg) ? 0.0 : coeffEg) * T - + (Double.isNaN(coeffFg) ? 0.0 : coeffFg); - return gibbsPoly; + if (!Double.isNaN(coeffAg) || !Double.isNaN(coeffBg) || !Double.isNaN(coeffCg) + || !Double.isNaN(coeffDg) || !Double.isNaN(coeffEg) || !Double.isNaN(coeffFg)) { + double gibbsPoly = (Double.isNaN(coeffAg) ? 0.0 : coeffAg) * Math.pow(T, 5) + + (Double.isNaN(coeffBg) ? 0.0 : coeffBg) * Math.pow(T, 4) + + (Double.isNaN(coeffCg) ? 0.0 : coeffCg) * Math.pow(T, 3) + + (Double.isNaN(coeffDg) ? 0.0 : coeffDg) * Math.pow(T, 2) + + (Double.isNaN(coeffEg) ? 0.0 : coeffEg) * T + (Double.isNaN(coeffFg) ? 0.0 : coeffFg); + return gibbsPoly; } // Otherwise, use standard calculation with I and J functions // ΔG°f/RT = ΔG°f/RTR + (1/R)[J/T - ΔA×ln(T) - ΔB/2×T - ΔC/6×T² - ΔD/12×T³] @@ -795,16 +801,16 @@ public double calculateGibbsEnergy(double temperature, int compNumber) { double dC = correctedCoeffs[2]; double dD = correctedCoeffs[3]; - double deltaGf_RT = deltaGf_RT_ref + I + (1 / R) * (J / T - + (-dA * Math.log(T) - dB / 2.0 * T - dC / 6.0 * Math.pow(T, 2) - dD / 12.0 * Math.pow(T, 3)) / 1000); + double deltaGf_RT = deltaGf_RT_ref + I + (1 / R) * (J / T + (-dA * Math.log(T) - dB / 2.0 * T + - dC / 6.0 * Math.pow(T, 2) - dD / 12.0 * Math.pow(T, 3)) / 1000); double deltaGf = deltaGf_RT * R * T; return deltaGf; } /** - * Calculate corrected heat capacity coefficients dA, dB, dC, dD by subtracting elemental contributions. dA = A - - * nO*AO - nN*AN - nC*AC - nH*AH - nS*AS + * Calculate corrected heat capacity coefficients dA, dB, dC, dD by subtracting elemental + * contributions. dA = A - nO*AO - nN*AN - nC*AC - nH*AH - nS*AS * * @param compNumber component index * @return array of corrected coefficients [dA, dB, dC, dD] @@ -817,32 +823,33 @@ public double[] calculateCorrectedHeatCapacityCoeffs(int compNumber) { double D = system.getComponent(compNumber).getCpD(); // Element heat capacity coefficients [A, B, C, D] - double[] cpO = { 12.73, 7.60E-03, -3.58E-06, 6.56E-10 }; - double[] cpN = { 14.4415, -7.85E-04, 4.04E-06, -1.44E-09 }; - double[] cpC = { 8.43, 0.00E+00, 0.00E+00, 0.00E+00 }; - double[] cpH = { 14.544, -9.60E-04, 2.00E-06, -4.35E-10 }; - double[] cpS = { 17.815, 0.001, 0.000, 0.000 }; + double[] cpO = {12.73, 7.60E-03, -3.58E-06, 6.56E-10}; + double[] cpN = {14.4415, -7.85E-04, 4.04E-06, -1.44E-09}; + double[] cpC = {8.43, 0.00E+00, 0.00E+00, 0.00E+00}; + double[] cpH = {14.544, -9.60E-04, 2.00E-06, -4.35E-10}; + double[] cpS = {17.815, 0.001, 0.000, 0.000}; // Calculate dA, dB, dC, dD by subtracting elemental contributions // dA = A - nO*AO - nN*AN - nC*AC - nH*AH - nS*AS - double dA = A - (elements[0] * cpO[0]) - (elements[1] * cpN[0]) - (elements[2] * cpC[0]) - (elements[3] * cpH[0]) - - (elements[4] * cpS[0]); + double dA = A - (elements[0] * cpO[0]) - (elements[1] * cpN[0]) - (elements[2] * cpC[0]) + - (elements[3] * cpH[0]) - (elements[4] * cpS[0]); - double dB = B - (elements[0] * cpO[1]) - (elements[1] * cpN[1]) - (elements[2] * cpC[1]) - (elements[3] * cpH[1]) - - (elements[4] * cpS[1]); + double dB = B - (elements[0] * cpO[1]) - (elements[1] * cpN[1]) - (elements[2] * cpC[1]) + - (elements[3] * cpH[1]) - (elements[4] * cpS[1]); - double dC = C - (elements[0] * cpO[2]) - (elements[1] * cpN[2]) - (elements[2] * cpC[2]) - (elements[3] * cpH[2]) - - (elements[4] * cpS[2]); + double dC = C - (elements[0] * cpO[2]) - (elements[1] * cpN[2]) - (elements[2] * cpC[2]) + - (elements[3] * cpH[2]) - (elements[4] * cpS[2]); - double dD = D - (elements[0] * cpO[3]) - (elements[1] * cpN[3]) - (elements[2] * cpC[3]) - (elements[3] * cpH[3]) - - (elements[4] * cpS[3]); + double dD = D - (elements[0] * cpO[3]) - (elements[1] * cpN[3]) - (elements[2] * cpC[3]) + - (elements[3] * cpH[3]) - (elements[4] * cpS[3]); - return new double[] { dA, dB, dC, dD }; + return new double[] {dA, dB, dC, dD}; } /** - * Calculate the corrected formation enthalpy term J. J = ΔH°f - ΔA*TR - ΔB/2*TR² - ΔC/3*TR³ - ΔD/4*TR⁴ where TR is - * the reference temperature and ΔA, ΔB, ΔC, ΔD are corrected heat capacity coefficients. + * Calculate the corrected formation enthalpy term J. J = ΔH°f - ΔA*TR - ΔB/2*TR² - ΔC/3*TR³ - + * ΔD/4*TR⁴ where TR is the reference temperature and ΔA, ΔB, ΔC, ΔD are corrected heat capacity + * coefficients. * * @param compNumber component index * @return corrected formation enthalpy term J @@ -856,16 +863,18 @@ public double calculateJ(int compNumber) { double dD = correctedCoeffs[3]; // Calculate J = ΔH°f - ΔA*TR - ΔB/2*TR² - ΔC/3*TR³ - ΔD/4*TR⁴ - double J = deltaHf298 - (dA * REFERENCE_TEMPERATURE - dB / 2.0 * Math.pow(REFERENCE_TEMPERATURE, 2) - - dC / 3.0 * Math.pow(REFERENCE_TEMPERATURE, 3) - dD / 4.0 * Math.pow(REFERENCE_TEMPERATURE, 4)) / 1000; + double J = + deltaHf298 - (dA * REFERENCE_TEMPERATURE - dB / 2.0 * Math.pow(REFERENCE_TEMPERATURE, 2) + - dC / 3.0 * Math.pow(REFERENCE_TEMPERATURE, 3) + - dD / 4.0 * Math.pow(REFERENCE_TEMPERATURE, 4)) / 1000; return J; } /** - * Calculate the I term for thermodynamic calculations. I = (1/R) × [J/TR + ΔA×ln(TR) + ΔB/2×TR + ΔC/6×TR² + - * ΔD/12×TR³] where R is the gas constant, TR is the reference temperature, and J is the corrected formation - * enthalpy term. + * Calculate the I term for thermodynamic calculations. I = (1/R) × [J/TR + ΔA×ln(TR) + ΔB/2×TR + * + ΔC/6×TR² + ΔD/12×TR³] where R is the gas constant, TR is the reference temperature, and J + * is the corrected formation enthalpy term. * * @param compNumber component index * @return the I term @@ -885,9 +894,9 @@ public double calculateI(int compNumber) { double J = calculateJ(compNumber); // Calculate I = (1/R) × [J/TR + ΔA×ln(TR) + ΔB/2×TR + ΔC/6×TR² + ΔD/12×TR³] - double I = (1.0 / R) - * (-(J / REFERENCE_TEMPERATURE) + (dA * Math.log(REFERENCE_TEMPERATURE) + dB / 2.0 * REFERENCE_TEMPERATURE - + dC / 6.0 * Math.pow(REFERENCE_TEMPERATURE, 2) + dD / 12.0 * Math.pow(REFERENCE_TEMPERATURE, 3)) / 1000); + double I = (1.0 / R) * (-(J / REFERENCE_TEMPERATURE) + (dA * Math.log(REFERENCE_TEMPERATURE) + + dB / 2.0 * REFERENCE_TEMPERATURE + dC / 6.0 * Math.pow(REFERENCE_TEMPERATURE, 2) + + dD / 12.0 * Math.pow(REFERENCE_TEMPERATURE, 3)) / 1000); return I; } @@ -902,14 +911,15 @@ public double calculateI(int compNumber) { public double calculateEnthalpy(double temperature, int compNumber) { double T = temperature; // If any enthalpy coefficients are not NaN, use polynomial formula - if (!Double.isNaN(coeffAh) || !Double.isNaN(coeffBh) || !Double.isNaN(coeffCh) || !Double.isNaN(coeffDh) - || !Double.isNaN(coeffEh) || !Double.isNaN(coeffGh)) { - double enthalpyPoly = (Double.isNaN(coeffAh) ? 0.0 : coeffAh) * Math.pow(T, 5) - + (Double.isNaN(coeffBh) ? 0.0 : coeffBh) * Math.pow(T, 4) - + (Double.isNaN(coeffCh) ? 0.0 : coeffCh) * Math.pow(T, 3) - + (Double.isNaN(coeffDh) ? 0.0 : coeffDh) * Math.pow(T, 2) - + (Double.isNaN(coeffEh) ? 0.0 : coeffEh) * Math.pow(T, 1) + (Double.isNaN(coeffGh) ? 0.0 : coeffGh); - return enthalpyPoly; + if (!Double.isNaN(coeffAh) || !Double.isNaN(coeffBh) || !Double.isNaN(coeffCh) + || !Double.isNaN(coeffDh) || !Double.isNaN(coeffEh) || !Double.isNaN(coeffGh)) { + double enthalpyPoly = (Double.isNaN(coeffAh) ? 0.0 : coeffAh) * Math.pow(T, 5) + + (Double.isNaN(coeffBh) ? 0.0 : coeffBh) * Math.pow(T, 4) + + (Double.isNaN(coeffCh) ? 0.0 : coeffCh) * Math.pow(T, 3) + + (Double.isNaN(coeffDh) ? 0.0 : coeffDh) * Math.pow(T, 2) + + (Double.isNaN(coeffEh) ? 0.0 : coeffEh) * Math.pow(T, 1) + + (Double.isNaN(coeffGh) ? 0.0 : coeffGh); + return enthalpyPoly; } // Otherwise, use standard calculation @@ -921,10 +931,10 @@ public double calculateEnthalpy(double temperature, int compNumber) { double dC = correctedCoeffs[2]; double dD = correctedCoeffs[3]; - double deltaH = deltaHf298 - + (dA * (T - REFERENCE_TEMPERATURE) + dB / 2.0 * (Math.pow(T, 2) - Math.pow(REFERENCE_TEMPERATURE, 2)) - + dC / 3.0 * (Math.pow(T, 3) - Math.pow(REFERENCE_TEMPERATURE, 3)) - + dD / 4.0 * (Math.pow(T, 4) - Math.pow(REFERENCE_TEMPERATURE, 4))) / 1000; + double deltaH = deltaHf298 + (dA * (T - REFERENCE_TEMPERATURE) + + dB / 2.0 * (Math.pow(T, 2) - Math.pow(REFERENCE_TEMPERATURE, 2)) + + dC / 3.0 * (Math.pow(T, 3) - Math.pow(REFERENCE_TEMPERATURE, 3)) + + dD / 4.0 * (Math.pow(T, 4) - Math.pow(REFERENCE_TEMPERATURE, 4))) / 1000; return deltaH; } @@ -968,114 +978,123 @@ public double calculateHeatCapacity(double temperature, int compNumber) { private void loadGibbsDatabase() { try { // Load main Gibbs database - InputStream inputStream = getClass().getResourceAsStream("/data/GibbsReactDatabase/GibbsReactDatabase.csv"); + InputStream inputStream = + getClass().getResourceAsStream("/data/GibbsReactDatabase/GibbsReactDatabase.csv"); if (inputStream == null) { - inputStream = getClass().getResourceAsStream("/neqsim/data/GibbsReactDatabase/GibbsReactDatabase.csv"); + inputStream = getClass() + .getResourceAsStream("/neqsim/data/GibbsReactDatabase/GibbsReactDatabase.csv"); } if (inputStream == null) { - inputStream = getClass().getResourceAsStream("/neqsim/data/GibbsReactDatabase.csv"); + inputStream = getClass().getResourceAsStream("/neqsim/data/GibbsReactDatabase.csv"); } if (inputStream == null) { - logger.warn("Could not find GibbsReactDatabase.csv in resources"); - return; + logger.warn("Could not find GibbsReactDatabase.csv in resources"); + return; } // Load extra coefficients from DatabaseGibbsFreeEnergyCoeff.csv Map extraCoeffMap = new HashMap<>(); try { - InputStream coeffStream = getClass() - .getResourceAsStream("/data/GibbsReactDatabase/DatabaseGibbsFreeEnergyCoeff.csv"); - if (coeffStream != null) { - Scanner coeffScanner = new Scanner(coeffStream); - if (coeffScanner.hasNextLine()) { - coeffScanner.nextLine(); // skip header - } - while (coeffScanner.hasNextLine()) { - String line = coeffScanner.nextLine().trim(); - if (line.isEmpty() || line.startsWith("#")) { - continue; - } - String[] parts = line.split(";"); - if (parts.length == 13) { - String compName = parts[0].trim().toLowerCase(); - double[] coeffs = new double[12]; - // Initialize all coefficients with NaN - for (int j = 0; j < 12; j++) { - coeffs[j] = Double.NaN; - } - for (int i = 0; i < 12; i++) { - coeffs[i] = Double.parseDouble(parts[i + 1].replace(",", ".")); - } - extraCoeffMap.put(compName, coeffs); - } - } - coeffScanner.close(); - } + InputStream coeffStream = getClass() + .getResourceAsStream("/data/GibbsReactDatabase/DatabaseGibbsFreeEnergyCoeff.csv"); + if (coeffStream != null) { + Scanner coeffScanner = new Scanner(coeffStream); + if (coeffScanner.hasNextLine()) { + coeffScanner.nextLine(); // skip header + } + while (coeffScanner.hasNextLine()) { + String line = coeffScanner.nextLine().trim(); + if (line.isEmpty() || line.startsWith("#")) { + continue; + } + String[] parts = line.split(";"); + if (parts.length == 13) { + String compName = parts[0].trim().toLowerCase(); + double[] coeffs = new double[12]; + // Initialize all coefficients with NaN + for (int j = 0; j < 12; j++) { + coeffs[j] = Double.NaN; + } + for (int i = 0; i < 12; i++) { + coeffs[i] = Double.parseDouble(parts[i + 1].replace(",", ".")); + } + extraCoeffMap.put(compName, coeffs); + } + } + coeffScanner.close(); + } } catch (Exception e) { - logger.warn("Could not load extra Gibbs coefficients: " + e.getMessage()); + logger.warn("Could not load extra Gibbs coefficients: " + e.getMessage()); } Scanner scanner = new Scanner(inputStream); if (scanner.hasNextLine()) { - scanner.nextLine(); // skip header + scanner.nextLine(); // skip header } while (scanner.hasNextLine()) { - String line = scanner.nextLine().trim(); - if (line.isEmpty() || line.startsWith("#")) { - continue; - } - String[] parts = line.split(";"); - // Handle actual format: molecule + 8 elements + 4 Cp + 3 thermo = 16 columns - if (parts.length >= 16) { - try { - final String molecule = parts[0].trim(); - double[] elements = new double[8]; - for (int i = 0; i < 8; i++) { - elements[i] = Double.parseDouble(parts[i + 1].trim().replace(",", ".")); - } - double[] heatCapCoeffs = new double[4]; - int heatCapStartIndex = 9; // after 8 elements - for (int i = 0; i < 4; i++) { - heatCapCoeffs[i] = Double.parseDouble(parts[i + heatCapStartIndex].trim().replace(",", ".")); - } - int thermoStartIndex = heatCapStartIndex + 4; // 13 - String deltaHf298Str = parts[thermoStartIndex].trim().replace(",", "."); - String deltaGf298Str = parts[thermoStartIndex + 1].trim().replace(",", "."); - String deltaSf298Str = parts[thermoStartIndex + 2].trim().replace(",", "."); - double deltaHf298 = Double.parseDouble(deltaHf298Str); - double deltaGf298 = Double.parseDouble(deltaGf298Str); - double deltaSf298 = Double.parseDouble(deltaSf298Str); - // Get extra coefficients if available, default to NaN array if not found - double[] defaultCoeffs = new double[12]; - for (int k = 0; k < 12; k++) { - defaultCoeffs[k] = Double.NaN; - } - double[] coeffs = extraCoeffMap.getOrDefault(molecule.toLowerCase(), defaultCoeffs); - GibbsComponent component = new GibbsComponent(molecule, elements, heatCapCoeffs, deltaHf298, deltaGf298, - deltaSf298, coeffs.length > 0 ? coeffs[0] : Double.NaN, coeffs.length > 1 ? coeffs[1] : Double.NaN, - coeffs.length > 2 ? coeffs[2] : Double.NaN, coeffs.length > 3 ? coeffs[3] : Double.NaN, - coeffs.length > 4 ? coeffs[4] : Double.NaN, coeffs.length > 5 ? coeffs[5] : Double.NaN, - coeffs.length > 6 ? coeffs[6] : Double.NaN, coeffs.length > 7 ? coeffs[7] : Double.NaN, - coeffs.length > 8 ? coeffs[8] : Double.NaN, coeffs.length > 9 ? coeffs[9] : Double.NaN, - coeffs.length > 10 ? coeffs[10] : Double.NaN, coeffs.length > 11 ? coeffs[11] : Double.NaN); - gibbsDatabase.add(component); - componentMap.put(molecule.toLowerCase(), component); - componentMap.put(molecule.trim().toLowerCase(), component); - logger.debug("Loaded component: " + molecule); - } catch (NumberFormatException e) { - logger.warn("Error parsing line: " + line + " - " + e.getMessage()); - } - } + String line = scanner.nextLine().trim(); + if (line.isEmpty() || line.startsWith("#")) { + continue; + } + String[] parts = line.split(";"); + // Handle actual format: molecule + 8 elements + 4 Cp + 3 thermo = 16 columns + if (parts.length >= 16) { + try { + final String molecule = parts[0].trim(); + double[] elements = new double[8]; + for (int i = 0; i < 8; i++) { + elements[i] = Double.parseDouble(parts[i + 1].trim().replace(",", ".")); + } + double[] heatCapCoeffs = new double[4]; + int heatCapStartIndex = 9; // after 8 elements + for (int i = 0; i < 4; i++) { + heatCapCoeffs[i] = + Double.parseDouble(parts[i + heatCapStartIndex].trim().replace(",", ".")); + } + int thermoStartIndex = heatCapStartIndex + 4; // 13 + String deltaHf298Str = parts[thermoStartIndex].trim().replace(",", "."); + String deltaGf298Str = parts[thermoStartIndex + 1].trim().replace(",", "."); + String deltaSf298Str = parts[thermoStartIndex + 2].trim().replace(",", "."); + double deltaHf298 = Double.parseDouble(deltaHf298Str); + double deltaGf298 = Double.parseDouble(deltaGf298Str); + double deltaSf298 = Double.parseDouble(deltaSf298Str); + // Get extra coefficients if available, default to NaN array if not found + double[] defaultCoeffs = new double[12]; + for (int k = 0; k < 12; k++) { + defaultCoeffs[k] = Double.NaN; + } + double[] coeffs = extraCoeffMap.getOrDefault(molecule.toLowerCase(), defaultCoeffs); + GibbsComponent component = new GibbsComponent(molecule, elements, heatCapCoeffs, + deltaHf298, deltaGf298, deltaSf298, coeffs.length > 0 ? coeffs[0] : Double.NaN, + coeffs.length > 1 ? coeffs[1] : Double.NaN, + coeffs.length > 2 ? coeffs[2] : Double.NaN, + coeffs.length > 3 ? coeffs[3] : Double.NaN, + coeffs.length > 4 ? coeffs[4] : Double.NaN, + coeffs.length > 5 ? coeffs[5] : Double.NaN, + coeffs.length > 6 ? coeffs[6] : Double.NaN, + coeffs.length > 7 ? coeffs[7] : Double.NaN, + coeffs.length > 8 ? coeffs[8] : Double.NaN, + coeffs.length > 9 ? coeffs[9] : Double.NaN, + coeffs.length > 10 ? coeffs[10] : Double.NaN, + coeffs.length > 11 ? coeffs[11] : Double.NaN); + gibbsDatabase.add(component); + componentMap.put(molecule.toLowerCase(), component); + componentMap.put(molecule.trim().toLowerCase(), component); + logger.debug("Loaded component: " + molecule); + } catch (NumberFormatException e) { + logger.warn("Error parsing line: " + line + " - " + e.getMessage()); + } + } } scanner.close(); // Add aliases to componentMap for (Map.Entry entry : componentAliases.entrySet()) { - String alias = entry.getKey(); - String target = entry.getValue(); - if (componentMap.containsKey(target)) { - componentMap.put(alias, componentMap.get(target)); - } + String alias = entry.getKey(); + String target = entry.getValue(); + if (componentMap.containsKey(target)) { + componentMap.put(alias, componentMap.get(target)); + } } logger.info("Loaded " + gibbsDatabase.size() + " components from Gibbs database"); @@ -1162,11 +1181,12 @@ public void run(UUID id) { if (useAllDatabaseSpecies) { // Add all database species to system for (GibbsComponent component : gibbsDatabase) { - try { - system.addComponent(component.getMolecule(), 1E-6); - } catch (Exception e) { - logger.debug("Could not add component " + component.getMolecule() + ": " + e.getMessage()); - } + try { + system.addComponent(component.getMolecule(), 1E-6); + } catch (Exception e) { + logger + .debug("Could not add component " + component.getMolecule() + ": " + e.getMessage()); + } } } @@ -1199,7 +1219,7 @@ public void run(UUID id) { // Only add to variableComponents if not inert and not excluded because of a missing element if (!inertComponents.contains(compName.toLowerCase()) && !excludedByFeed) { - variableComponents.add(compName); + variableComponents.add(compName); } } @@ -1214,8 +1234,8 @@ public void run(UUID id) { // Debug logging for element balance logger.debug("=== Element Balance (mol/sec) ==="); for (int i = 0; i < elementNames.length; i++) { - logger.debug(String.format("%s: IN=%.6e, OUT=%.6e, DIFF=%.6e", elementNames[i], elementMoleBalanceIn[i], - elementMoleBalanceOut[i], elementMoleBalanceDiff[i])); + logger.debug(String.format("%s: IN=%.6e, OUT=%.6e, DIFF=%.6e", elementNames[i], + elementMoleBalanceIn[i], elementMoleBalanceOut[i], elementMoleBalanceDiff[i])); } // Calculate objective function values @@ -1230,7 +1250,7 @@ public void run(UUID id) { // Mass balance check at the end if (!getMassBalanceConverged()) { logger.debug( - "WARNING: Mass balance not converged in GibbsReactor. Consider decreasing the iteration step (damping factor) for better closure."); + "WARNING: Mass balance not converged in GibbsReactor. Consider decreasing the iteration step (damping factor) for better closure."); } } @@ -1257,14 +1277,14 @@ private void performGibbsMinimization(SystemInterface system) { // Do not seed a minimum amount for species that cannot form (a required element is absent // from the feed). They must stay at their feed amount. if (feedExcludedComponents.contains(compName.toLowerCase())) { - continue; + continue; } if (initialGuess.containsKey(compName)) { - double currentMoles = system.getComponent(i).getNumberOfMolesInPhase(); - if (currentMoles < 1E-6) { - system.addComponent(i, 1E-6 - currentMoles, 0); - } + double currentMoles = system.getComponent(i).getNumberOfMolesInPhase(); + if (currentMoles < 1E-6) { + system.addComponent(i, 1E-6 - currentMoles, 0); + } } } @@ -1278,7 +1298,8 @@ private void performGibbsMinimization(SystemInterface system) { * @param elementBalance Array to store the element balance * @param isInput true if this is input balance, false if output balance */ - private void calculateElementMoleBalance(SystemInterface system, double[] elementBalance, boolean isInput) { + private void calculateElementMoleBalance(SystemInterface system, double[] elementBalance, + boolean isInput) { // Reset balance for (int i = 0; i < elementBalance.length; i++) { elementBalance[i] = 0.0; @@ -1291,23 +1312,24 @@ private void calculateElementMoleBalance(SystemInterface system, double[] elemen // Use inlet_mole for input, outlet_mole for output double moles; if (isInput) { - moles = (i < inlet_mole.size()) ? inlet_mole.get(i) : 0.0; + moles = (i < inlet_mole.size()) ? inlet_mole.get(i) : 0.0; } else { - moles = (i < outlet_mole.size()) ? outlet_mole.get(i) : 0.0; + moles = (i < outlet_mole.size()) ? outlet_mole.get(i) : 0.0; } // Get element composition from database GibbsComponent comp = componentMap.get(compName.toLowerCase()); if (comp == null) { - logger.debug("WARNING: Component '" + compName - + "' not found in gibbsReactDatabase. Skipping element balance for this component."); - continue; + logger.debug("WARNING: Component '" + compName + + "' not found in gibbsReactDatabase. Skipping element balance for this component."); + continue; } double[] elements = comp.getElements(); - logger.debug("Component " + compName + " elements: O=" + elements[0] + ", N=" + elements[1] + ", C=" + elements[2] - + ", H=" + elements[3] + ", S=" + elements[4] + ", Ar=" + elements[5] + ", Z=" + elements[6]); + logger.debug("Component " + compName + " elements: O=" + elements[0] + ", N=" + elements[1] + + ", C=" + elements[2] + ", H=" + elements[3] + ", S=" + elements[4] + ", Ar=" + + elements[5] + ", Z=" + elements[6]); for (int j = 0; j < elementNames.length; j++) { - elementBalance[j] += elements[j] * moles; + elementBalance[j] += elements[j] * moles; } } } @@ -1339,10 +1361,10 @@ private void calculateObjectiveFunctionValues(SystemInterface system) { // Get Gibbs component GibbsComponent comp = componentMap.get(compName.toLowerCase()); if (comp == null) { - // System.err.println("WARNING: Component '" + compName - // + "' not found in gibbsReactDatabase. Skipping objective function value for this - // component."); - continue; + // System.err.println("WARNING: Component '" + compName + // + "' not found in gibbsReactDatabase. Skipping objective function value for this + // component."); + continue; } // Calculate Gibbs energy of formation double Gf0 = comp.calculateGibbsEnergy(T, i); @@ -1357,13 +1379,13 @@ private void calculateObjectiveFunctionValues(SystemInterface system) { double lagrangeSum = 0.0; double[] elements = comp.getElements(); for (int j = 0; j < lambda.length; j++) { - lagrangeSum += lambda[j] * elements[j]; + lagrangeSum += lambda[j] * elements[j]; } // Calculate objective function: F = Gf0 + RT*ln(phi) + RT*ln(yi) + RT*ln(P/Pref) - // lagrangeSum - double F = Gf0 + RT * Math.log(phi[i]) + RT * Math.log(yi) + RT * Math.log(system.getPressure("bara") / 1.0) - - lagrangeSum; + double F = Gf0 + RT * Math.log(phi[i]) + RT * Math.log(yi) + + RT * Math.log(system.getPressure("bara") / 1.0) - lagrangeSum; objectiveFunctionValues.put(compName, F); } } @@ -1407,8 +1429,8 @@ public Map> getLagrangeMultiplierContributions() { Map> contributions = new HashMap<>(); // Use the components that were actually processed in the last run - List componentsToProcess = processedComponents.isEmpty() ? new ArrayList<>(finalMoles.keySet()) - : processedComponents; + List componentsToProcess = + processedComponents.isEmpty() ? new ArrayList<>(finalMoles.keySet()) : processedComponents; for (String compName : componentsToProcess) { Map compContributions = new HashMap<>(); @@ -1416,18 +1438,18 @@ public Map> getLagrangeMultiplierContributions() { // Get element composition from database GibbsComponent comp = componentMap.get(compName.toLowerCase()); if (comp == null) { - // System.err.println("WARNING: Component '" + compName - // + "' not found in gibbsReactDatabase. Skipping Lagrange multiplier contributions for this - // component."); - continue; + // System.err.println("WARNING: Component '" + compName + // + "' not found in gibbsReactDatabase. Skipping Lagrange multiplier contributions for this + // component."); + continue; } double[] elements = comp.getElements(); double totalContribution = 0.0; for (int i = 0; i < elementNames.length; i++) { - double contribution = lambda[i] * elements[i]; - compContributions.put(elementNames[i], contribution); - totalContribution += contribution; + double contribution = lambda[i] * elements[i]; + compContributions.put(elementNames[i], contribution); + totalContribution += contribution; } compContributions.put("TOTAL", totalContribution); @@ -1490,10 +1512,10 @@ public Map> getDetailedMoleBalance() { Double molesOut = (i < outlet_mole.size()) ? outlet_mole.get(i) : 1E-6; if (comp == null) { - // System.err.println("WARNING: Component '" + compName - // + "' not found in gibbsReactDatabase. Skipping detailed mole balance for this - // component."); - continue; + // System.err.println("WARNING: Component '" + compName + // + "' not found in gibbsReactDatabase. Skipping detailed mole balance for this + // component."); + continue; } Map componentBalance = new HashMap<>(); final double[] elements = comp.getElements(); // [O, N, C, H, S, Ar] @@ -1505,13 +1527,13 @@ public Map> getDetailedMoleBalance() { // Calculate element contributions using outlet - inlet difference for (int j = 0; j < elementNames.length; j++) { - double elementIn = elements[j] * molesIn; - double elementOut = elements[j] * molesOut; - double elementDiff = elementOut - elementIn; // outlet - inlet + double elementIn = elements[j] * molesIn; + double elementOut = elements[j] * molesOut; + double elementDiff = elementOut - elementIn; // outlet - inlet - componentBalance.put(elementNames[j] + "_IN", elementIn); - componentBalance.put(elementNames[j] + "_OUT", elementOut); - componentBalance.put(elementNames[j] + "_DIFF", elementDiff); + componentBalance.put(elementNames[j] + "_IN", elementIn); + componentBalance.put(elementNames[j] + "_OUT", elementOut); + componentBalance.put(elementNames[j] + "_DIFF", elementDiff); } detailedBalance.put(compName, componentBalance); @@ -1521,9 +1543,9 @@ public Map> getDetailedMoleBalance() { } /** - * Calculate the objective minimization vector. This vector contains the F values for each component and the mass - * balance constraints. The system is in equilibrium when this vector is zero. Only includes elements that are - * actually present in the system. + * Calculate the objective minimization vector. This vector contains the F values for each + * component and the mass balance constraints. The system is in equilibrium when this vector is + * zero. Only includes elements that are actually present in the system. */ private void calculateObjectiveMinimizationVector() { // Find which elements are actually present in the system @@ -1562,8 +1584,8 @@ public double[] getObjectiveMinimizationVector() { } /** - * Build objective vector matching the optimization variables (variableComponents) and active element balances. This - * excludes inert components from the variable part but keeps balances. + * Build objective vector matching the optimization variables (variableComponents) and active + * element balances. This excludes inert components from the variable part but keeps balances. * * @return the objective vector for optimization variables */ @@ -1596,9 +1618,9 @@ public List getObjectiveMinimizationVectorLabels() { } /** - * Calculate the Jacobian matrix for the Newton-Raphson method. The Jacobian represents the derivatives of the - * objective function with respect to the variables. Only includes elements that are actually present in the system to - * avoid singular matrices. + * Calculate the Jacobian matrix for the Newton-Raphson method. The Jacobian represents the + * derivatives of the objective function with respect to the variables. Only includes elements + * that are actually present in the system to avoid singular matrices. */ private void calculateJacobian() { // If there are no optimization variables (all components inert or none present), nothing to do @@ -1648,31 +1670,33 @@ private void calculateJacobian() { // Use outlet_mole for calculations, but with a minimum value to avoid numerical issues // Find outlet mole corresponding to this variable component (use processedComponentIndexMap) int globalIdx = processedComponentIndexMap.getOrDefault(compI, -1); - double ni = (globalIdx >= 0 && globalIdx < outlet_mole.size()) ? outlet_mole.get(globalIdx) : MIN_MOLES; + double ni = (globalIdx >= 0 && globalIdx < outlet_mole.size()) ? outlet_mole.get(globalIdx) + : MIN_MOLES; double niForJacobian = Math.max(ni, MIN_JACOBIAN_MOLES); // Use minimum of 1e-6 for Jacobian - // calculation + // calculation for (int j = 0; j < numComponents; j++) { - String compJ = variableComponents.get(j); - int globalJ = processedComponentIndexMap.getOrDefault(compJ, -1); - double dfugdn = (globalIdx >= 0 && globalJ >= 0) ? system.getPhase(0).getComponent(globalIdx).getdfugdn(globalJ) - : 0.0; - if (i == j) { - // Diagonal: ∂F_i/∂n_i = RT * (1/n_i - 1/N + ∂ln(φ_i)/∂n_i) - jacobianMatrix[i][j] = RT * (1.0 / niForJacobian - 1.0 / totalMoles + dfugdn); - } else { - // Off-diagonal: ∂F_i/∂n_j = RT * (-1/N + ∂ln(φ_i)/∂n_j) - jacobianMatrix[i][j] = RT * (-1.0 / totalMoles + dfugdn); - } + String compJ = variableComponents.get(j); + int globalJ = processedComponentIndexMap.getOrDefault(compJ, -1); + double dfugdn = (globalIdx >= 0 && globalJ >= 0) + ? system.getPhase(0).getComponent(globalIdx).getdfugdn(globalJ) + : 0.0; + if (i == j) { + // Diagonal: ∂F_i/∂n_i = RT * (1/n_i - 1/N + ∂ln(φ_i)/∂n_i) + jacobianMatrix[i][j] = RT * (1.0 / niForJacobian - 1.0 / totalMoles + dfugdn); + } else { + // Off-diagonal: ∂F_i/∂n_j = RT * (-1/N + ∂ln(φ_i)/∂n_j) + jacobianMatrix[i][j] = RT * (-1.0 / totalMoles + dfugdn); + } } // Derivatives with respect to Lagrange multipliers (only active elements) GibbsComponent gibbsComp = componentMap.get(compI.toLowerCase()); if (gibbsComp != null) { - double[] elements = gibbsComp.getElements(); - for (int k = 0; k < numActiveElements; k++) { - int elementIndex = activeElements.get(k); - jacobianMatrix[i][numComponents + k] = -elements[elementIndex]; - } + double[] elements = gibbsComp.getElements(); + for (int k = 0; k < numActiveElements; k++) { + int elementIndex = activeElements.get(k); + jacobianMatrix[i][numComponents + k] = -elements[elementIndex]; + } } } @@ -1680,17 +1704,17 @@ private void calculateJacobian() { for (int i = 0; i < numActiveElements; i++) { int elementIndex = activeElements.get(i); for (int j = 0; j < numComponents; j++) { - String compName = variableComponents.get(j); - GibbsComponent gibbsComp = componentMap.get(compName.toLowerCase()); - if (gibbsComp != null) { - double[] elements = gibbsComp.getElements(); - jacobianMatrix[numComponents + i][j] = elements[elementIndex]; - } + String compName = variableComponents.get(j); + GibbsComponent gibbsComp = componentMap.get(compName.toLowerCase()); + if (gibbsComp != null) { + double[] elements = gibbsComp.getElements(); + jacobianMatrix[numComponents + i][j] = elements[elementIndex]; + } } // Derivatives with respect to Lagrange multipliers are zero for (int k = 0; k < numActiveElements; k++) { - jacobianMatrix[numComponents + i][numComponents + k] = 0.0; + jacobianMatrix[numComponents + i][numComponents + k] = 0.0; } } @@ -1771,7 +1795,22 @@ public double[][] getJacobianInverse() { } /** - * Calculate the inverse of the Jacobian matrix using EJML. + * Solve a linear system with LU decomposition. + * + * @param matrix coefficient matrix + * @param rhs right-hand side column vector + * @return solution column vector as primitive array + */ + private double[] solveWithLU(double[][] matrix, double[] rhs) { + double[] solution = new double[rhs.length]; + if (!LinearAlgebraOps.solveLinearSystem(matrix, rhs, solution)) { + throw new RuntimeException("LU decomposition failed"); + } + return solution; + } + + /** + * Calculate the inverse of the Jacobian matrix using ojAlgo. * * @return Inverse matrix, or null if matrix is singular */ @@ -1780,52 +1819,30 @@ private double[][] calculateJacobianInverse() { return null; } try { - // First try standard inversion - SimpleMatrix ejmlMatrix = new SimpleMatrix(jacobianMatrix); - - // Standard inversion for well-conditioned matrices - SimpleMatrix inverseMatrix = ejmlMatrix.invert(); - int nRows = inverseMatrix.numRows(); - int nCols = inverseMatrix.numCols(); - double[][] result = new double[nRows][nCols]; - double[] data = inverseMatrix.getDDRM().getData(); - for (int i = 0; i < nRows; i++) { - for (int j = 0; j < nCols; j++) { - result[i][j] = data[i * nCols + j]; - } - } - return result; + return LinearAlgebraOps.inverse(jacobianMatrix); } catch (RuntimeException e) { - logger.warn("Jacobian matrix inversion failed: " + e.getMessage() + ". Trying pseudo-inverse..."); + logger.warn( + "Jacobian matrix inversion failed: " + e.getMessage() + ". Trying pseudo-inverse..."); // Fallback to pseudo-inverse try { - SimpleMatrix ejmlMatrix = new SimpleMatrix(jacobianMatrix); - SimpleMatrix inverseMatrix = ejmlMatrix.pseudoInverse(); - int nRows = inverseMatrix.numRows(); - int nCols = inverseMatrix.numCols(); - double[][] result = new double[nRows][nCols]; - double[] data = inverseMatrix.getDDRM().getData(); - for (int i = 0; i < nRows; i++) { - for (int j = 0; j < nCols; j++) { - result[i][j] = data[i * nCols + j]; - } - } - logger.info("Successfully computed pseudo-inverse"); - return result; + double[][] result = LinearAlgebraOps.pseudoInverse(jacobianMatrix); + logger.info("Successfully computed pseudo-inverse"); + return result; } catch (RuntimeException e2) { - logger.error("Pseudo-inverse also failed: " + e2.getMessage()); - return null; + logger.error("Pseudo-inverse also failed: " + e2.getMessage()); + return null; } } } /** - * Solve the Newton-Raphson linear system J_scaled * deltaX_scaled = -F using LU decomposition, then unscale using the - * column scaling factors from the NASA CEA log-mole transformation. + * Solve the Newton-Raphson linear system J_scaled * deltaX_scaled = -F using LU decomposition, + * then unscale using the column scaling factors from the NASA CEA log-mole transformation. * *

- * The Jacobian was column-scaled: J_scaled[:,j] = J[:,j] * n_j. Solving gives deltaX_scaled, and the true step is - * deltaX[j] = deltaX_scaled[j] / n_j. Falls back to pseudo-inverse if LU solve fails (e.g., singular matrix). + * The Jacobian was column-scaled: J_scaled[:,j] = J[:,j] * n_j. Solving gives deltaX_scaled, and + * the true step is deltaX[j] = deltaX_scaled[j] / n_j. Falls back to pseudo-inverse if LU solve + * fails (e.g., singular matrix). *

* * @param objectiveVector the right-hand side vector F @@ -1836,48 +1853,39 @@ private double[] solveNewtonSystem(double[] objectiveVector) { return null; } try { - SimpleMatrix jMatrix = new SimpleMatrix(jacobianMatrix); - SimpleMatrix fVector = new SimpleMatrix(objectiveVector.length, 1, true, objectiveVector).scale(-1.0); - - // Use EJML's solve() which internally uses LU decomposition — O(n^3/3) vs O(n^3) for - // explicit inverse. See Nocedal & Wright, Numerical Optimization (2000), Ch. 3. - SimpleMatrix deltaX = jMatrix.solve(fVector); - - int nRows = deltaX.numRows(); - double[] result = new double[nRows]; - double[] data = deltaX.getDDRM().getData(); - for (int i = 0; i < nRows; i++) { - result[i] = data[i]; + double[] rhs = objectiveVector.clone(); + for (int i = 0; i < rhs.length; i++) { + rhs[i] *= -1.0; } + double[] result = solveWithLU(jacobianMatrix, rhs); // Unscale: deltaX_true[j] = deltaX_scaled[j] / columnScaleFactors[j] unscaleNewtonStep(result); return result; } catch (RuntimeException e) { logger.warn("LU solve failed: " + e.getMessage() + ". Trying pseudo-inverse fallback..."); try { - SimpleMatrix jMatrix = new SimpleMatrix(jacobianMatrix); - SimpleMatrix fVector = new SimpleMatrix(objectiveVector.length, 1, true, objectiveVector); - SimpleMatrix jInv = jMatrix.pseudoInverse(); - SimpleMatrix deltaX = jInv.mult(fVector).scale(-1.0); - int nRows = deltaX.numRows(); - double[] result = new double[nRows]; - double[] data = deltaX.getDDRM().getData(); - for (int i = 0; i < nRows; i++) { - result[i] = data[i]; - } - unscaleNewtonStep(result); - return result; + double[] rhs = objectiveVector.clone(); + for (int i = 0; i < rhs.length; i++) { + rhs[i] *= -1.0; + } + double[] result = new double[rhs.length]; + if (!LinearAlgebraOps.pseudoInverseSolve(jacobianMatrix, rhs, result)) { + throw new RuntimeException("SVD decomposition failed"); + } + unscaleNewtonStep(result); + return result; } catch (RuntimeException e2) { - logger.error("Pseudo-inverse fallback also failed: " + e2.getMessage()); - return null; + logger.error("Pseudo-inverse fallback also failed: " + e2.getMessage()); + return null; } } } /** - * Unscale a Newton step vector using the column scaling factors from the log-mole transformation. The scaled system - * solves J_s * Δξ = -F where J_s = J * D (D = diag(n_j)). Recovering the original step: Δn = D * Δξ, so Δn_j = n_j * - * Δξ_j. Lagrange multiplier entries have scale factor 1.0 (no change). + * Unscale a Newton step vector using the column scaling factors from the log-mole transformation. + * The scaled system solves J_s * Δξ = -F where J_s = J * D (D = diag(n_j)). Recovering the + * original step: Δn = D * Δξ, so Δn_j = n_j * Δξ_j. Lagrange multiplier entries have scale factor + * 1.0 (no change). * * @param deltaX the Newton step vector to unscale in place */ @@ -1891,8 +1899,8 @@ private void unscaleNewtonStep(double[] deltaX) { } /** - * Enforce minimum concentration threshold to prevent numerical issues. If any component has moles less than - * MIN_MOLES, it will be set to MIN_MOLES. + * Enforce minimum concentration threshold to prevent numerical issues. If any component has moles + * less than MIN_MOLES, it will be set to MIN_MOLES. * * @param system The thermodynamic system to check and modify */ @@ -1903,20 +1911,20 @@ private void enforceMinimumConcentrations(SystemInterface system) { String compName = system.getComponent(i).getComponentName(); // Only enforce minimum if component is in the Gibbs database if (componentMap.get(compName.toLowerCase()) == null) { - continue; + continue; } // Do not enforce a minimum for species that cannot form (a required element is absent from // the feed); they must stay at their feed amount (typically zero). if (feedExcludedComponents.contains(compName.toLowerCase())) { - continue; + continue; } double currentMoles = system.getComponent(i).getNumberOfMolesInPhase(); if (currentMoles < MIN_MOLES) { - logger.info("Component {} has very low concentration ({}), setting to minimum: {}", compName, currentMoles, - MIN_MOLES); - system.addComponent(i, MIN_MOLES - currentMoles, 0); - modified = true; + logger.info("Component {} has very low concentration ({}), setting to minimum: {}", + compName, currentMoles, MIN_MOLES); + system.addComponent(i, MIN_MOLES - currentMoles, 0); + modified = true; } } @@ -1972,8 +1980,8 @@ public void printDatabaseComponents() { for (GibbsComponent comp : gibbsDatabase) { String molecule = comp.getMolecule(); double[] elements = comp.getElements(); - logger.debug(" {}: O={}, N={}, C={}, H={}, S={}, Ar={}", molecule, elements[0], elements[1], elements[2], - elements[3], elements[4], elements[5]); + logger.debug(" {}: O={}, N={}, C={}, H={}, S={}, Ar={}", molecule, elements[0], elements[1], + elements[2], elements[3], elements[4], elements[5]); } logger.debug("Component map keys:"); @@ -1996,25 +2004,25 @@ private List findActiveElements() { // degenerate (zero "determinator"), so the corresponding Lagrange multiplier and constraint // are dropped until the element is actually present. if (Math.abs(elementMoleBalanceIn[elementIndex]) <= ELEMENT_ZERO_THRESHOLD) { - continue; + continue; } boolean elementPresent = false; for (String compName : processedComponents) { - GibbsComponent comp = componentMap.get(compName.toLowerCase()); - if (comp == null) { - continue; - } - double[] elements = comp.getElements(); - if (Math.abs(elements[elementIndex]) > ELEMENT_ZERO_THRESHOLD) { - elementPresent = true; - break; - } + GibbsComponent comp = componentMap.get(compName.toLowerCase()); + if (comp == null) { + continue; + } + double[] elements = comp.getElements(); + if (Math.abs(elements[elementIndex]) > ELEMENT_ZERO_THRESHOLD) { + elementPresent = true; + break; + } } if (elementPresent) { - activeElements.add(elementIndex); + activeElements.add(elementIndex); } } @@ -2032,28 +2040,28 @@ private List getActiveElementIndices() { for (int elementIndex = 0; elementIndex < elementNames.length; elementIndex++) { // Skip elements absent from the feed (degenerate constraint row / zero "determinator"). if (Math.abs(elementMoleBalanceIn[elementIndex]) <= ELEMENT_ZERO_THRESHOLD) { - continue; + continue; } boolean hasNonZero = false; for (String compName : processedComponents) { - GibbsComponent comp = componentMap.get(compName.toLowerCase()); - if (comp == null) { - // System.err.println("WARNING: Component '" + compName - // + "' not found in gibbsReactDatabase. Skipping active element index check for this - // component."); - continue; - } - double[] elements = comp.getElements(); - if (Math.abs(elements[elementIndex]) > 1e-10) { - hasNonZero = true; - break; - } + GibbsComponent comp = componentMap.get(compName.toLowerCase()); + if (comp == null) { + // System.err.println("WARNING: Component '" + compName + // + "' not found in gibbsReactDatabase. Skipping active element index check for this + // component."); + continue; + } + double[] elements = comp.getElements(); + if (Math.abs(elements[elementIndex]) > 1e-10) { + hasNonZero = true; + break; + } } if (hasNonZero) { - activeIndices.add(elementIndex); + activeIndices.add(elementIndex); } } @@ -2061,55 +2069,10 @@ private List getActiveElementIndices() { } /** - * Verify that the Jacobian inverse is correct by multiplying J * J^-1. Should return the identity matrix if the - * inverse is correct. - * - * @return True if the inverse is correct (within tolerance) - */ - public boolean verifyJacobianInverse() { - if (jacobianMatrix == null) { - calculateJacobian(); - } - if (jacobianMatrix == null) { - return false; - } - if (jacobianInverse == null) { - jacobianInverse = calculateJacobianInverse(); - } - if (jacobianInverse == null) { - return false; - } - - try { - // Only create SimpleMatrix objects once per call, not in a loop - SimpleMatrix jacobianMatrixEJML = new SimpleMatrix(jacobianMatrix); - SimpleMatrix jacobianInverseEJML = new SimpleMatrix(jacobianInverse); - SimpleMatrix resultMatrix = jacobianMatrixEJML.mult(jacobianInverseEJML); - double[] resultData = resultMatrix.getDDRM().getData(); - double tolerance = 1e-10; - int n = jacobianMatrix.length; - for (int i = 0; i < n; i++) { - for (int j = 0; j < n; j++) { - double value = resultData[i * n + j]; - double expected = (i == j) ? 1.0 : 0.0; - if (Math.abs(value - expected) > tolerance) { - logger.warn("Jacobian inverse verification failed at [" + i + "," + j + "]: " + "expected " + expected - + ", got " + value); - return false; - } - } - } - return true; - } catch (RuntimeException e) { - logger.warn("Error during Jacobian inverse verification: " + e.getMessage()); - return false; - } - } - - /** - * Perform one Newton-Raphson iteration step to calculate the delta vector (dX). Uses LU decomposition to solve J * dX - * = -F directly (Nocedal & Wright, 2000), falling back to J^{-1} * F if LU solve is not available. The LU - * approach is ~3x faster and more numerically stable than explicit matrix inversion. + * Perform one Newton-Raphson iteration step to calculate the delta vector (dX). Uses LU + * decomposition to solve J * dX = -F directly (Nocedal & Wright, 2000), falling back to + * J^{-1} * F if LU solve is not available. The LU approach is ~3x faster and more numerically + * stable than explicit matrix inversion. * * @return The delta vector (dX) for updating variables, or null if calculation fails */ @@ -2138,19 +2101,11 @@ public double[] performNewtonRaphsonIteration() { // Fallback to explicit inverse multiplication if LU solve failed if (jacobianInverse != null) { try { - SimpleMatrix jacobianInverseEJML = new SimpleMatrix(jacobianInverse); - SimpleMatrix objectiveVectorEJML = new SimpleMatrix(objectiveVector.length, 1, true, objectiveVector); - SimpleMatrix deltaXMatrix = jacobianInverseEJML.mult(objectiveVectorEJML).scale(-1.0); - int nRows = deltaXMatrix.numRows(); - double[] fallbackResult = new double[nRows]; - double[] data = deltaXMatrix.getDDRM().getData(); - for (int i = 0; i < nRows; i++) { - fallbackResult[i] = data[i]; - } - return fallbackResult; + double[] fallbackResult = LinearAlgebraOps.multiply(jacobianInverse, objectiveVector, -1.0); + return fallbackResult; } catch (RuntimeException e) { - logger.warn("Error during Newton-Raphson iteration calculation: " + e.getMessage()); - return null; + logger.warn("Error during Newton-Raphson iteration calculation: " + e.getMessage()); + return null; } } @@ -2159,8 +2114,8 @@ public double[] performNewtonRaphsonIteration() { } /** - * Perform a Newton-Raphson iteration update. Updates outlet compositions with damping factor and Lagrange multipliers - * directly. + * Perform a Newton-Raphson iteration update. Updates outlet compositions with damping factor and + * Lagrange multipliers directly. * * @param deltaX The delta vector from Newton-Raphson iteration * @param alphaComposition Damping factor for composition updates (e.g., 0.0001) @@ -2177,8 +2132,8 @@ public boolean performIterationUpdate(double[] deltaX, double alphaComposition) int numActiveElements = activeElementIndices.size(); if (deltaX.length != numComponents + numActiveElements) { - logger.warn( - "Delta vector size mismatch: expected " + (numComponents + numActiveElements) + ", got " + deltaX.length); + logger.warn("Delta vector size mismatch: expected " + (numComponents + numActiveElements) + + ", got " + deltaX.length); return false; } @@ -2188,12 +2143,12 @@ public boolean performIterationUpdate(double[] deltaX, double alphaComposition) String compName = variableComponents.get(i); // Only update if component is in the Gibbs database if (componentMap.get(compName.toLowerCase()) == null) { - continue; + continue; } // Find global index in processedComponents/outlet_mole int globalIdx = processedComponentIndexMap.getOrDefault(compName, -1); if (globalIdx < 0 || globalIdx >= outlet_mole.size()) { - continue; + continue; } double oldValue = outlet_mole.get(globalIdx); double deltaComposition = deltaX[i]; @@ -2244,36 +2199,36 @@ private boolean updateSystemWithNewCompositions() { // Update component moles in the system for (int i = 0; i < processedComponents.size(); i++) { - String compName = processedComponents.get(i); - // Only update if component is in the Gibbs database - if (componentMap.get(compName.toLowerCase()) == null) { - // Not in database, skip update to keep moles unchanged - continue; - } - double newMoles = outlet_mole.get(i); - - // Find component index in system - int compIndex = -1; - for (int j = 0; j < system.getNumberOfComponents(); j++) { - if (compName.equals(system.getComponent(j).getComponentName())) { - compIndex = j; - break; - } - } - - if (compIndex >= 0) { - // Set new moles - double currentMoles = system.getComponent(compIndex).getNumberOfMolesInPhase(); - double molesToAdd = newMoles - currentMoles; - if ((molesToAdd < 0.0) && (Math.abs(Math.abs(molesToAdd) - currentMoles)) < 1e-6) { - // Prevent removing more moles than present - molesToAdd = -currentMoles + 1e-6; // leave a tiny amount to avoid zero - } - if (Math.abs(molesToAdd) > 1e-15) { - system.addComponent(compIndex, molesToAdd, 0); - } - } - getOutletStream().setThermoSystem(system); + String compName = processedComponents.get(i); + // Only update if component is in the Gibbs database + if (componentMap.get(compName.toLowerCase()) == null) { + // Not in database, skip update to keep moles unchanged + continue; + } + double newMoles = outlet_mole.get(i); + + // Find component index in system + int compIndex = -1; + for (int j = 0; j < system.getNumberOfComponents(); j++) { + if (compName.equals(system.getComponent(j).getComponentName())) { + compIndex = j; + break; + } + } + + if (compIndex >= 0) { + // Set new moles + double currentMoles = system.getComponent(compIndex).getNumberOfMolesInPhase(); + double molesToAdd = newMoles - currentMoles; + if ((molesToAdd < 0.0) && (Math.abs(Math.abs(molesToAdd) - currentMoles)) < 1e-6) { + // Prevent removing more moles than present + molesToAdd = -currentMoles + 1e-6; // leave a tiny amount to avoid zero + } + if (Math.abs(molesToAdd) > 1e-15) { + system.addComponent(compIndex, molesToAdd, 0); + } + } + getOutletStream().setThermoSystem(system); } // Recalculate objective function values with new compositions and Lagrange multipliers @@ -2282,17 +2237,17 @@ private boolean updateSystemWithNewCompositions() { // Recalculate element mole balances calculateElementMoleBalance(system, elementMoleBalanceOut, false); for (int i = 0; i < elementNames.length; i++) { - elementMoleBalanceDiff[i] = elementMoleBalanceOut[i] - elementMoleBalanceIn[i]; + elementMoleBalanceDiff[i] = elementMoleBalanceOut[i] - elementMoleBalanceIn[i]; } // Debug logging for element balance during iterations logger.debug("--- Element Balance During Iteration ---"); for (int i = 0; i < elementNames.length; i++) { - // Always show Z element, and others only if significant differences - if (elementNames[i].equals("Z") || Math.abs(elementMoleBalanceDiff[i]) > 1e-10) { - logger.debug(String.format("%s: IN=%.6e, OUT=%.6e, DIFF=%.6e", elementNames[i], elementMoleBalanceIn[i], - elementMoleBalanceOut[i], elementMoleBalanceDiff[i])); - } + // Always show Z element, and others only if significant differences + if (elementNames[i].equals("Z") || Math.abs(elementMoleBalanceDiff[i]) > 1e-10) { + logger.debug(String.format("%s: IN=%.6e, OUT=%.6e, DIFF=%.6e", elementNames[i], + elementMoleBalanceIn[i], elementMoleBalanceOut[i], elementMoleBalanceDiff[i])); + } } return true; @@ -2303,11 +2258,12 @@ private boolean updateSystemWithNewCompositions() { } /** - * Get the fugacity coefficient array for all components in a specified phase using the current outlet composition. - * Uses direct phase composition assignment for efficiency. + * Get the fugacity coefficient array for all components in a specified phase using the current + * outlet composition. Uses direct phase composition assignment for efficiency. * * @param phaseNameOrIndex Name or index of the phase (e.g., "gas", "oil", "aqueous", or 0/1/2) - * @return Fugacity coefficient (phi) array for all components in the specified phase, or Double.NaN if not found + * @return Fugacity coefficient (phi) array for all components in the specified phase, or + * Double.NaN if not found */ public double[] getFugacityCoefficient(Object phaseNameOrIndex) { int phaseIndex = 0; @@ -2316,11 +2272,11 @@ public double[] getFugacityCoefficient(Object phaseNameOrIndex) { } else if (phaseNameOrIndex instanceof String) { String phaseName = ((String) phaseNameOrIndex).toLowerCase(); for (int i = 0; i < system.getNumberOfPhases(); i++) { - String name = system.getPhase(i).getPhaseTypeName().toLowerCase(); - if (name.contains(phaseName)) { - phaseIndex = i; - break; - } + String name = system.getPhase(i).getPhaseTypeName().toLowerCase(); + if (name.contains(phaseName)) { + phaseIndex = i; + break; + } } } @@ -2388,12 +2344,13 @@ public double getDampingComposition() { } /** - * Enable the mathematically consistent off-diagonal Jacobian formulation. This is now always enabled (the - * RT-corrected formulation is the only implementation). This method is retained for backward compatibility but has no - * effect. + * Enable the mathematically consistent off-diagonal Jacobian formulation. This is now always + * enabled (the RT-corrected formulation is the only implementation). This method is retained for + * backward compatibility but has no effect. * * @param useConsistent ignored — consistent formulation is always active - * @deprecated The consistent off-diagonal formulation is now always enabled. This setter is a no-op. + * @deprecated The consistent off-diagonal formulation is now always enabled. This setter is a + * no-op. */ @Deprecated public void setUseConsistentOffDiagonal(boolean useConsistent) { @@ -2439,9 +2396,10 @@ public double getFinalConvergenceError() { } /** - * Set minimum number of iterations before convergence is checked. Default is 100. The solver will not declare - * convergence before completing this many iterations, even if the convergence criterion is satisfied. Setting this - * too high wastes iterations; too low may cause premature termination. + * Set minimum number of iterations before convergence is checked. Default is 100. The solver will + * not declare convergence before completing this many iterations, even if the convergence + * criterion is satisfied. Setting this too high wastes iterations; too low may cause premature + * termination. * * @param minIterations Minimum iterations before convergence check (must be at least 1) */ @@ -2459,10 +2417,11 @@ public int getMinIterations() { } /** - * Enable or disable NASA CEA-style adaptive step sizing (Gordon & McBride, 1994, NASA RP-1311). When enabled, the - * step size is automatically computed each iteration to limit the maximum relative change in component moles, - * allowing larger steps when safe and smaller steps near steep gradients. When disabled, the fixed - * {@code dampingComposition} factor is used for all iterations. + * Enable or disable NASA CEA-style adaptive step sizing (Gordon & McBride, 1994, NASA + * RP-1311). When enabled, the step size is automatically computed each iteration to limit the + * maximum relative change in component moles, allowing larger steps when safe and smaller steps + * near steep gradients. When disabled, the fixed {@code dampingComposition} factor is used for + * all iterations. * * @param useAdaptive true to enable adaptive step sizing */ @@ -2483,8 +2442,9 @@ public boolean isUseAdaptiveStepSize() { * Enable or disable Armijo backtracking line search. * *

- * When enabled, the Newton step is adaptively scaled to guarantee sufficient decrease in the total Gibbs free energy - * at each iteration. This provides a globally convergent algorithm replacing fixed damping. + * When enabled, the Newton step is adaptively scaled to guarantee sufficient decrease in the + * total Gibbs free energy at each iteration. This provides a globally convergent algorithm + * replacing fixed damping. *

* * @param useArmijo true to enable Armijo line search @@ -2644,20 +2604,20 @@ private double evaluateTotalGibbsEnergy() { String compName = processedComponents.get(i); GibbsComponent comp = componentMap.get(compName.toLowerCase()); if (comp == null) { - continue; + continue; } double ni = outlet_mole.get(i); if (ni < 1e-30) { - continue; + continue; } double yi = ni / totalMoles; double Gf0 = comp.calculateGibbsEnergy(T, i); int sysIdx = -1; for (int j = 0; j < sys.getNumberOfComponents(); j++) { - if (compName.equals(sys.getComponent(j).getComponentName())) { - sysIdx = j; - break; - } + if (compName.equals(sys.getComponent(j).getComponentName())) { + sysIdx = j; + break; + } } double lnPhi = (sysIdx >= 0 && sysIdx < phi.length) ? Math.log(phi[sysIdx]) : 0.0; double lnP = Math.log(sys.getPressure("bara") / 1.0); @@ -2667,12 +2627,12 @@ private double evaluateTotalGibbsEnergy() { } /** - * Perform Armijo backtracking line search to find a step size that guarantees sufficient decrease in the total Gibbs - * free energy. + * Perform Armijo backtracking line search to find a step size that guarantees sufficient decrease + * in the total Gibbs free energy. * *

- * Starting from alpha = alpha_max, the step is contracted by factor rho until the Armijo condition is satisfied: G(n - * + alpha*dn) <= G(n) + c1*alpha*grad^T*dn. + * Starting from alpha = alpha_max, the step is contracted by factor rho until the Armijo + * condition is satisfied: G(n + alpha*dn) <= G(n) + c1*alpha*grad^T*dn. *

* *

@@ -2693,15 +2653,15 @@ private double armijoLineSearch(double[] deltaX, double alphaMax, double current String compName = variableComponents.get(i); Double fi = fValues.get(compName); if (fi != null) { - directionalDerivative += fi * deltaX[i]; + directionalDerivative += fi * deltaX[i]; } } // If directional derivative is non-negative, Newton direction is not a descent direction; // fall back to the max alpha (steepest descent would be needed, but we just use the step) if (directionalDerivative >= 0.0) { - logger.debug("Armijo: directional derivative non-negative ({}), using alphaMax={}", directionalDerivative, - alphaMax); + logger.debug("Armijo: directional derivative non-negative ({}), using alphaMax={}", + directionalDerivative, alphaMax); return alphaMax; } @@ -2713,12 +2673,12 @@ private double armijoLineSearch(double[] deltaX, double alphaMax, double current for (int k = 0; k < armijoMaxBacktracks; k++) { // Trial update: n_trial = n + alpha * dn (composition only) for (int i = 0; i < numComponents; i++) { - String compName = variableComponents.get(i); - int globalIdx = processedComponentIndexMap.getOrDefault(compName, -1); - if (globalIdx >= 0 && globalIdx < outlet_mole.size()) { - double trialValue = savedMoles.get(globalIdx) + alpha * deltaX[i]; - outlet_mole.set(globalIdx, Math.max(trialValue, 1e-15)); - } + String compName = variableComponents.get(i); + int globalIdx = processedComponentIndexMap.getOrDefault(compName, -1); + if (globalIdx >= 0 && globalIdx < outlet_mole.size()) { + double trialValue = savedMoles.get(globalIdx) + alpha * deltaX[i]; + outlet_mole.set(globalIdx, Math.max(trialValue, 1e-15)); + } } // Update system and evaluate Gibbs energy at trial point @@ -2729,14 +2689,15 @@ private double armijoLineSearch(double[] deltaX, double alphaMax, double current // Armijo condition: G(trial) <= G(current) + c1 * alpha * grad^T * dn double sufficientDecrease = currentG + armijoC1 * alpha * directionalDerivative; if (trialG <= sufficientDecrease) { - logger.debug("Armijo accepted: alpha={}, G={} -> {}, backtracks={}", alpha, currentG, trialG, k); - // Restore moles (the actual update will happen in performIterationUpdate) - for (int i = 0; i < outlet_mole.size(); i++) { - outlet_mole.set(i, savedMoles.get(i)); - } - System.arraycopy(savedLambda, 0, lambda, 0, lambda.length); - updateSystemWithNewCompositions(); - return alpha; + logger.debug("Armijo accepted: alpha={}, G={} -> {}, backtracks={}", alpha, currentG, + trialG, k); + // Restore moles (the actual update will happen in performIterationUpdate) + for (int i = 0; i < outlet_mole.size(); i++) { + outlet_mole.set(i, savedMoles.get(i)); + } + System.arraycopy(savedLambda, 0, lambda, 0, lambda.length); + updateSystemWithNewCompositions(); + return alpha; } // Contract step @@ -2754,12 +2715,12 @@ private double armijoLineSearch(double[] deltaX, double alphaMax, double current } /** - * Apply Tikhonov regularization to the Jacobian matrix if the condition number exceeds the threshold. Adds tau*I to - * the Hessian (composition-composition) block of the Jacobian. + * Apply Tikhonov regularization to the Jacobian matrix if the condition number exceeds the + * threshold. Adds tau*I to the Hessian (composition-composition) block of the Jacobian. * *

- * This converts the saddle-point system into a positive-definite system when the Hessian is nearly singular, ensuring - * the Newton direction remains well-defined. + * This converts the saddle-point system into a positive-definite system when the Hessian is + * nearly singular, ensuring the Newton direction remains well-defined. *

* * @return the condition number of the (possibly regularized) Jacobian @@ -2769,8 +2730,7 @@ private double applyRegularization() { return Double.NaN; } - SimpleMatrix jMat = new SimpleMatrix(jacobianMatrix); - double condNum = jMat.conditionP2(); + double condNum = LinearAlgebraOps.conditionP2(jacobianMatrix); if (useRegularization && condNum > regularizationThreshold) { int numComp = variableComponents.size(); @@ -2778,20 +2738,19 @@ private double applyRegularization() { // Scale tau to the magnitude of the diagonal double maxDiag = 0.0; for (int i = 0; i < numComp; i++) { - maxDiag = Math.max(maxDiag, Math.abs(jacobianMatrix[i][i])); + maxDiag = Math.max(maxDiag, Math.abs(jacobianMatrix[i][i])); } if (maxDiag > 0.0) { - tau = Math.max(tau, maxDiag * 1e-8); + tau = Math.max(tau, maxDiag * 1e-8); } logger.info("Applying Tikhonov regularization: condNum={}, tau={}", condNum, tau); for (int i = 0; i < numComp; i++) { - jacobianMatrix[i][i] += tau; + jacobianMatrix[i][i] += tau; } // Recompute condition number after regularization - jMat = new SimpleMatrix(jacobianMatrix); - condNum = jMat.conditionP2(); + condNum = LinearAlgebraOps.conditionP2(jacobianMatrix); logger.info("After regularization: condNum={}", condNum); } @@ -2799,10 +2758,11 @@ private double applyRegularization() { } /** - * Calculate adaptive step size using NASA CEA-style step limiting (Gordon & McBride, 1994). Limits the maximum - * relative mole change so that no major component changes by more than a factor of {@code e^MAX_STEP_LIMIT} - * (~5x) in a single step. Components with moles below a significance threshold are excluded from the limiting (they - * are growing from near-zero and need large relative steps). + * Calculate adaptive step size using NASA CEA-style step limiting (Gordon & McBride, 1994). + * Limits the maximum relative mole change so that no major component changes by more + * than a factor of {@code e^MAX_STEP_LIMIT} (~5x) in a single step. Components with moles below a + * significance threshold are excluded from the limiting (they are growing from near-zero and need + * large relative steps). * * @param deltaX The raw Newton step vector * @param requestedAlpha The starting step size (typically 1.0) @@ -2824,18 +2784,18 @@ private double calculateAdaptiveAlpha(double[] deltaX, double requestedAlpha) { String compName = variableComponents.get(i); int globalIdx = processedComponentIndexMap.getOrDefault(compName, -1); if (globalIdx < 0 || globalIdx >= outlet_mole.size()) { - continue; + continue; } double currentMole = outlet_mole.get(globalIdx); // Skip near-zero components — they need large relative steps to grow if (currentMole < significanceThreshold) { - continue; + continue; } double absDelta = Math.abs(deltaX[i]); if (absDelta < 1e-30) { - continue; + continue; } // Limit: |alpha * deltaX[i]| / currentMole < MAX_STEP_LIMIT @@ -2848,14 +2808,14 @@ private double calculateAdaptiveAlpha(double[] deltaX, double requestedAlpha) { String compName = variableComponents.get(i); int globalIdx = processedComponentIndexMap.getOrDefault(compName, -1); if (globalIdx < 0 || globalIdx >= outlet_mole.size()) { - continue; + continue; } double currentMole = outlet_mole.get(globalIdx); double delta = deltaX[i]; // If step would make moles negative, limit alpha if (delta < 0 && currentMole > 1e-15) { - double maxAlpha = 0.9 * currentMole / (-delta); - alpha = Math.min(alpha, maxAlpha); + double maxAlpha = 0.9 * currentMole / (-delta); + alpha = Math.min(alpha, maxAlpha); } } @@ -2900,7 +2860,8 @@ public boolean solveGibbsEquilibrium(double alphaComposition) { logger.info("Armijo line search enabled: c1={}, rho={}", armijoC1, armijoRho); } if (useRegularization) { - logger.info("Tikhonov regularization enabled: threshold={}, tau={}", regularizationThreshold, regularizationTau); + logger.info("Tikhonov regularization enabled: threshold={}, tau={}", regularizationThreshold, + regularizationTau); } for (int iteration = 1; iteration <= maxIterations; iteration++) { @@ -2913,24 +2874,24 @@ public boolean solveGibbsEquilibrium(double alphaComposition) { // Debug log component, Gibbs energy, enthalpy, and entropy for every iteration logger.debug("Iteration {} component properties:", iteration); for (int i = 0; i < outletSystem.getNumberOfComponents(); i++) { - String compName = outletSystem.getComponent(i).getComponentName(); - GibbsComponent comp = componentMap.get(compName.toLowerCase()); - if (comp != null) { - double T = outletSystem.getTemperature(); - double gibbs = comp.calculateGibbsEnergy(T, i); - double enthalpy = comp.calculateEnthalpy(T, i); - double entropy = comp.calculateEntropy(T, i); - logger.debug( - String.format("Component: %s, GibbsEnergy: %.2f kJ/mol, Enthalpy: %.2f kJ/mol, Entropy: %.2f kJ/(mol·K)", - compName, gibbs, enthalpy, entropy)); - } + String compName = outletSystem.getComponent(i).getComponentName(); + GibbsComponent comp = componentMap.get(compName.toLowerCase()); + if (comp != null) { + double T = outletSystem.getTemperature(); + double gibbs = comp.calculateGibbsEnergy(T, i); + double enthalpy = comp.calculateEnthalpy(T, i); + double entropy = comp.calculateEntropy(T, i); + logger.debug(String.format( + "Component: %s, GibbsEnergy: %.2f kJ/mol, Enthalpy: %.2f kJ/mol, Entropy: %.2f kJ/(mol·K)", + compName, gibbs, enthalpy, entropy)); + } } // Calculate F vector norm for convergence check Map fValues = getObjectiveFunctionValues(); double fNorm = 0.0; for (Double value : fValues.values()) { - fNorm += value * value; + fNorm += value * value; } fNorm = Math.sqrt(fNorm); @@ -2940,33 +2901,33 @@ public boolean solveGibbsEquilibrium(double alphaComposition) { double[] deltaX = performNewtonRaphsonIteration(); if (deltaX == null) { - logger.warn("Newton-Raphson iteration failed at iteration " + iteration); - finalConvergenceError = fNorm; - return false; + logger.warn("Newton-Raphson iteration failed at iteration " + iteration); + finalConvergenceError = fNorm; + return false; } // Apply Tikhonov regularization if enabled, and record condition number if (useRegularization || !conditionNumberHistory.isEmpty() || iteration <= 5) { - double condNum = applyRegularization(); - conditionNumberHistory.add(condNum); - - // If regularization changed the Jacobian, recompute the Newton step - if (useRegularization && condNum < regularizationThreshold) { - // Jacobian was regularized; resolve the linear system - double[] objectiveVector = getObjectiveVectorForVariables(); - if (objectiveVector != null) { - double[] recomputed = solveNewtonSystem(objectiveVector); - if (recomputed != null) { - deltaX = recomputed; - } - } - } + double condNum = applyRegularization(); + conditionNumberHistory.add(condNum); + + // If regularization changed the Jacobian, recompute the Newton step + if (useRegularization && condNum < regularizationThreshold) { + // Jacobian was regularized; resolve the linear system + double[] objectiveVector = getObjectiveVectorForVariables(); + if (objectiveVector != null) { + double[] recomputed = solveNewtonSystem(objectiveVector); + if (recomputed != null) { + deltaX = recomputed; + } + } + } } // Calculate delta vector norm double deltaXNorm = 0.0; for (double value : deltaX) { - deltaXNorm += value * value; + deltaXNorm += value * value; } deltaXNorm = Math.sqrt(deltaXNorm); @@ -2974,72 +2935,75 @@ public boolean solveGibbsEquilibrium(double alphaComposition) { logger.debug("deltaXNorm (full update vector): {}", deltaXNorm); if (iteration == 1) { - G = calculateMixtureGibbsEnergy(processedComponents, outlet_mole, componentMap, system.getTemperature()); - logger.debug("Initial Gibbs energy G = " + G); - GOLD = G; + G = calculateMixtureGibbsEnergy(processedComponents, outlet_mole, componentMap, + system.getTemperature()); + logger.debug("Initial Gibbs energy G = " + G); + GOLD = G; } else { - GOLD = G; - G = calculateMixtureGibbsEnergy(processedComponents, outlet_mole, componentMap, system.getTemperature()); - dG = G - GOLD; - logger.debug("Gibbs energy change dG = " + dG); + GOLD = G; + G = calculateMixtureGibbsEnergy(processedComponents, outlet_mole, componentMap, + system.getTemperature()); + dG = G - GOLD; + logger.debug("Gibbs energy change dG = " + dG); } // Record Gibbs energy for diagnostics gibbsEnergyHistory.add(G); if (energyMode == EnergyMode.ADIABATIC) { - if (iteration == 1) { - inletEnthalpy = calculateMixtureEnthalpy(processedComponents, outlet_mole, componentMap, - system.getTemperature()); - enthalpyOld = inletEnthalpy; - } else { - outletEnthalpy = calculateMixtureEnthalpy(processedComponents, outlet_mole, componentMap, - system.getTemperature()); - double dH; - dH = outletEnthalpy - enthalpyOld; - enthalpyOfReactions += dH; - enthalpyOld = outletEnthalpy; - double T_out = system.getTemperature() - dH * 1000 / (system.getCp("J/K")); - dT = Math.abs(T_out - system.getTemperature()); - if (dT > 1000) { - throw new RuntimeException( - "Temperature change per iteration (dT) exceeded 1000 K. Please reduce the step of iteration (alphaComposition or damping factor)."); - } - temperatureChange += dT; - system.setTemperature(T_out); - system.init(3); - this.getOutletStream().getThermoSystem().setTemperature(system.getTemperature()); - } + if (iteration == 1) { + inletEnthalpy = calculateMixtureEnthalpy(processedComponents, outlet_mole, componentMap, + system.getTemperature()); + enthalpyOld = inletEnthalpy; + } else { + outletEnthalpy = calculateMixtureEnthalpy(processedComponents, outlet_mole, componentMap, + system.getTemperature()); + double dH; + dH = outletEnthalpy - enthalpyOld; + enthalpyOfReactions += dH; + enthalpyOld = outletEnthalpy; + double T_out = system.getTemperature() - dH * 1000 / (system.getCp("J/K")); + dT = Math.abs(T_out - system.getTemperature()); + if (dT > 1000) { + throw new RuntimeException( + "Temperature change per iteration (dT) exceeded 1000 K. Please reduce the step of iteration (alphaComposition or damping factor)."); + } + temperatureChange += dT; + system.setTemperature(T_out); + system.init(3); + this.getOutletStream().getThermoSystem().setTemperature(system.getTemperature()); + } } // Check convergence (require minimum iterations for stability) - if ((deltaXNorm < convergenceTolerance && iteration >= minIterations) || iteration == maxIterations) { - logger.info((deltaXNorm < convergenceTolerance ? "Converged" : "Max iterations reached") + " at iteration " - + iteration + " with delta norm = " + deltaXNorm); - converged = deltaXNorm < convergenceTolerance; - finalConvergenceError = deltaXNorm; - updateSystemWithNewCompositions(); - this.getOutletStream().getThermoSystem().setTemperature(system.getTemperature()); - if (iteration == maxIterations) { - logger.warn( - "Maximum number of iterations reached without convergence. Please increase the maximum number of iterations (maxIterations) and try again."); - } - return true; + if ((deltaXNorm < convergenceTolerance && iteration >= minIterations) + || iteration == maxIterations) { + logger.info((deltaXNorm < convergenceTolerance ? "Converged" : "Max iterations reached") + + " at iteration " + iteration + " with delta norm = " + deltaXNorm); + converged = deltaXNorm < convergenceTolerance; + finalConvergenceError = deltaXNorm; + updateSystemWithNewCompositions(); + this.getOutletStream().getThermoSystem().setTemperature(system.getTemperature()); + if (iteration == maxIterations) { + logger.warn( + "Maximum number of iterations reached without convergence. Please increase the maximum number of iterations (maxIterations) and try again."); + } + return true; } // Determine step size: Armijo line search, adaptive (NASA CEA-style), or fixed damping double effectiveAlpha = alphaComposition; if (useAdaptiveStepSize) { - effectiveAlpha = calculateAdaptiveAlpha(deltaX, 1.0); - if (iteration <= 5 || iteration % 100 == 0) { - logger.debug("Iteration " + iteration + ": adaptive alpha = " + effectiveAlpha); - } + effectiveAlpha = calculateAdaptiveAlpha(deltaX, 1.0); + if (iteration <= 5 || iteration % 100 == 0) { + logger.debug("Iteration " + iteration + ": adaptive alpha = " + effectiveAlpha); + } } // Apply Armijo backtracking line search if enabled if (useArmijoLineSearch) { - double currentG = evaluateTotalGibbsEnergy(); - effectiveAlpha = armijoLineSearch(deltaX, effectiveAlpha, currentG); + double currentG = evaluateTotalGibbsEnergy(); + effectiveAlpha = armijoLineSearch(deltaX, effectiveAlpha, currentG); } // Record step size for diagnostics @@ -3048,10 +3012,11 @@ public boolean solveGibbsEquilibrium(double alphaComposition) { // Perform iteration update boolean updateSuccess = performIterationUpdate(deltaX, effectiveAlpha); if (!updateSuccess) { - // Set final convergence error for diagnostics and throw an exception to signal failure - logger.warn("Iteration update failed at iteration " + iteration); - finalConvergenceError = deltaXNorm; - throw new RuntimeException("Iteration update failed at iteration " + iteration + ", deltaXNorm=" + deltaXNorm); + // Set final convergence error for diagnostics and throw an exception to signal failure + logger.warn("Iteration update failed at iteration " + iteration); + finalConvergenceError = deltaXNorm; + throw new RuntimeException( + "Iteration update failed at iteration " + iteration + ", deltaXNorm=" + deltaXNorm); } // Debug logging for element balance during iterations @@ -3060,13 +3025,13 @@ public boolean solveGibbsEquilibrium(double alphaComposition) { double elementErrorNorm = 0.0; logger.debug("Iteration " + iteration + " element balance:"); for (int i = 0; i < elementNames.length; i++) { - double diff = elementMoleBalanceOut[i] - elementMoleBalanceIn[i]; - elementErrorNorm += diff * diff; - // Always show Z element, and others only if significant differences - if (elementNames[i].equals("Z") || Math.abs(diff) > 1e-10) { - logger.debug(String.format(" %s: IN=%.6e, OUT=%.6e, DIFF=%.6e", elementNames[i], elementMoleBalanceIn[i], - elementMoleBalanceOut[i], diff)); - } + double diff = elementMoleBalanceOut[i] - elementMoleBalanceIn[i]; + elementErrorNorm += diff * diff; + // Always show Z element, and others only if significant differences + if (elementNames[i].equals("Z") || Math.abs(diff) > 1e-10) { + logger.debug(String.format(" %s: IN=%.6e, OUT=%.6e, DIFF=%.6e", elementNames[i], + elementMoleBalanceIn[i], elementMoleBalanceOut[i], diff)); + } } elementBalanceErrorHistory.add(Math.sqrt(elementErrorNorm)); @@ -3080,6 +3045,15 @@ public boolean solveGibbsEquilibrium(double alphaComposition) { return false; } + /** + * Solve Gibbs equilibrium using Newton-Raphson iterations with default damping factor. + * + * @return true if converged, false otherwise + */ + public boolean solveGibbsEquilibrium() { + return solveGibbsEquilibrium(dampingComposition); // Use configured damping factor + } + // --- Formula alias mapping for user-friendly reaction input --- private static final Map formulaToComponent = new HashMap<>(); static { @@ -3089,13 +3063,4 @@ public boolean solveGibbsEquilibrium(double alphaComposition) { formulaToComponent.put("H2O", "water"); // Add more mappings as needed } - - /** - * Solve Gibbs equilibrium using Newton-Raphson iterations with default damping factor. - * - * @return true if converged, false otherwise - */ - public boolean solveGibbsEquilibrium() { - return solveGibbsEquilibrium(dampingComposition); // Use configured damping factor - } } diff --git a/src/main/java/neqsim/process/equipment/util/BroydenAccelerator.java b/src/main/java/neqsim/process/equipment/util/BroydenAccelerator.java index 52beddd595..0eff0320f8 100644 --- a/src/main/java/neqsim/process/equipment/util/BroydenAccelerator.java +++ b/src/main/java/neqsim/process/equipment/util/BroydenAccelerator.java @@ -4,6 +4,7 @@ import java.util.Arrays; import org.apache.logging.log4j.LogManager; import org.apache.logging.log4j.Logger; +import neqsim.util.math.LinearAlgebraOps; /** * Broyden's quasi-Newton acceleration method for multi-variable recycle convergence. @@ -151,7 +152,7 @@ public double[] accelerate(double[] currentX, double[] functionOutput) { } // Check for sufficient change to update Jacobian - double deltaXNorm = vectorNorm(deltaX); + double deltaXNorm = LinearAlgebraOps.vectorNorm(deltaX); if (deltaXNorm > EPSILON) { // Update inverse Jacobian using Sherman-Morrison formula updateInverseJacobian(deltaX, deltaF); @@ -166,7 +167,7 @@ public double[] accelerate(double[] currentX, double[] functionOutput) { } // Limit step size if needed - double stepNorm = vectorNorm(step); + double stepNorm = LinearAlgebraOps.vectorNorm(step); if (stepNorm > maxStepSize) { double scale = maxStepSize / stepNorm; for (int i = 0; i < n; i++) { @@ -270,16 +271,6 @@ private double dotProduct(double[] a, double[] b) { return sum; } - /** - * Computes Euclidean norm of a vector. - * - * @param v the vector - * @return norm - */ - private double vectorNorm(double[] v) { - return Math.sqrt(dotProduct(v, v)); - } - /** * Gets the current iteration count. * @@ -381,6 +372,6 @@ public double getResidualNorm() { if (previousF == null) { return -1.0; } - return vectorNorm(previousF); + return LinearAlgebraOps.vectorNorm(previousF); } } diff --git a/src/main/java/neqsim/process/util/optimizer/SQPoptimizer.java b/src/main/java/neqsim/process/util/optimizer/SQPoptimizer.java index 1b51f35d93..374a7b840c 100644 --- a/src/main/java/neqsim/process/util/optimizer/SQPoptimizer.java +++ b/src/main/java/neqsim/process/util/optimizer/SQPoptimizer.java @@ -6,6 +6,7 @@ import java.util.List; import org.apache.logging.log4j.LogManager; import org.apache.logging.log4j.Logger; +import neqsim.util.math.LinearAlgebraOps; /** * Sequential Quadratic Programming (SQP) optimizer for constrained process optimization. @@ -30,9 +31,9 @@ * * *

- * At each iteration, a QP sub-problem is formed with a BFGS approximation of the Hessian of the Lagrangian, and the - * solution provides a search direction. An Armijo-backtracking line search on a merit function ensures global - * convergence. + * At each iteration, a QP sub-problem is formed with a BFGS approximation of the Hessian of the + * Lagrangian, and the solution provides a search direction. An Armijo-backtracking line search on a + * merit function ensures global convergence. *

* *

Features

@@ -51,14 +52,15 @@ * sqp.setObjectiveFunction(x -> computeNPV(x, process)); * sqp.addEqualityConstraint(x -> massBalance(x)); * sqp.addInequalityConstraint(x -> maxPressure - x[0]); - * sqp.setVariableBounds(new double[] { 50.0, 0.5 }, new double[] { 200.0, 1.0 }); - * sqp.setInitialPoint(new double[] { 100.0, 0.8 }); + * sqp.setVariableBounds(new double[] {50.0, 0.5}, new double[] {200.0, 1.0}); + * sqp.setInitialPoint(new double[] {100.0, 0.8}); * SQPoptimizer.OptimizationResult result = sqp.solve(); * * *

References

*
    - *
  • Nocedal, J. & Wright, S.J., "Numerical Optimization", 2nd ed., Springer (2006), Ch. 18
  • + *
  • Nocedal, J. & Wright, S.J., "Numerical Optimization", 2nd ed., Springer (2006), Ch. + * 18
  • *
  • Biegler, L.T., "Nonlinear Programming", SIAM (2010), Ch. 3-4
  • *
  • Boggs, P.T. & Tolle, J.W., "Sequential Quadratic Programming", Acta Numerica (1995)
  • *
@@ -150,8 +152,7 @@ public interface ConstraintFunc { /** * Default constructor. */ - public SQPoptimizer() { - } + public SQPoptimizer() {} /** * Constructor with number of variables. @@ -312,8 +313,8 @@ public OptimizationResult solve() { // Check KKT optimality (with Lagrange multiplier contributions) double kktError = computeKKTError(gradF, gEq, hIneq, jacEq, jacIneq, x); if (kktError < tolerance) { - converged = true; - break; + converged = true; + break; } // Solve QP sub-problem for search direction @@ -328,7 +329,7 @@ public OptimizationResult solve() { // Update x for (int i = 0; i < n; i++) { - x[i] = x[i] + alpha * dx[i]; + x[i] = x[i] + alpha * dx[i]; } projectToBounds(x); @@ -344,21 +345,21 @@ public OptimizationResult solve() { double[] gEqNew = evaluateConstraints(equalityConstraints, x); double[] hIneqNew = evaluateConstraints(inequalityConstraints, x); for (int i = 0; i < gEqNew.length; i++) { - if (Math.abs(gEqNew[i]) > tolerance * 100.0) { - feasible = false; - break; - } + if (Math.abs(gEqNew[i]) > tolerance * 100.0) { + feasible = false; + break; + } } if (feasible) { - for (int j = 0; j < hIneqNew.length; j++) { - if (hIneqNew[j] < -tolerance * 100.0) { - feasible = false; - break; - } - } + for (int j = 0; j < hIneqNew.length; j++) { + if (hIneqNew[j] < -tolerance * 100.0) { + feasible = false; + break; + } + } } if (feasible && fBest < objectiveFunction.evaluate(xBest)) { - xBest = Arrays.copyOf(x, n); + xBest = Arrays.copyOf(x, n); } } @@ -373,9 +374,11 @@ public OptimizationResult solve() { double fFinal = objectiveFunction.evaluate(xFinal); return new OptimizationResult(xFinal, fFinal, iterCount, converged, - computeKKTError(computeGradient(objectiveFunction, xFinal), evaluateConstraints(equalityConstraints, xFinal), - evaluateConstraints(inequalityConstraints, xFinal), computeJacobian(equalityConstraints, xFinal), - computeJacobian(inequalityConstraints, xFinal), xFinal)); + computeKKTError(computeGradient(objectiveFunction, xFinal), + evaluateConstraints(equalityConstraints, xFinal), + evaluateConstraints(inequalityConstraints, xFinal), + computeJacobian(equalityConstraints, xFinal), + computeJacobian(inequalityConstraints, xFinal), xFinal)); } /** @@ -386,8 +389,8 @@ public OptimizationResult solve() { private void projectToBounds(double[] x) { if (lowerBounds != null && upperBounds != null) { for (int i = 0; i < n; i++) { - x[i] = Math.max(x[i], lowerBounds[i]); - x[i] = Math.min(x[i], upperBounds[i]); + x[i] = Math.max(x[i], lowerBounds[i]); + x[i] = Math.min(x[i], upperBounds[i]); } } } @@ -446,7 +449,7 @@ private double[][] computeJacobian(List constraints, double[] x) double[] cp = evaluateConstraints(constraints, xp); double[] cm = evaluateConstraints(constraints, xm); for (int i = 0; i < m; i++) { - jac[i][j] = (cp[i] - cm[i]) / (2.0 * step); + jac[i][j] = (cp[i] - cm[i]) / (2.0 * step); } } return jac; @@ -463,24 +466,24 @@ private double[][] computeJacobian(List constraints, double[] x) * @param x current point * @return KKT error (inf-norm) */ - private double computeKKTError(double[] gradF, double[] gEq, double[] hIneq, double[][] jacEq, double[][] jacIneq, - double[] x) { + private double computeKKTError(double[] gradF, double[] gEq, double[] hIneq, double[][] jacEq, + double[][] jacIneq, double[] x) { double maxError = 0.0; // Compute Lagrangian gradient: grad_L = gradF - jacEq^T * lambdaEq - jacIneq^T * lambdaIneq double[] gradL = Arrays.copyOf(gradF, n); if (lambdaEq != null && jacEq != null) { for (int j = 0; j < lambdaEq.length; j++) { - for (int i = 0; i < n; i++) { - gradL[i] -= jacEq[j][i] * lambdaEq[j]; - } + for (int i = 0; i < n; i++) { + gradL[i] -= jacEq[j][i] * lambdaEq[j]; + } } } if (lambdaIneq != null && jacIneq != null) { for (int j = 0; j < lambdaIneq.length; j++) { - for (int i = 0; i < n; i++) { - gradL[i] -= jacIneq[j][i] * lambdaIneq[j]; - } + for (int i = 0; i < n; i++) { + gradL[i] -= jacIneq[j][i] * lambdaIneq[j]; + } } } @@ -492,10 +495,10 @@ private double computeKKTError(double[] gradF, double[] gEq, double[] hIneq, dou boolean atUpper = upperBounds != null && Math.abs(x[i] - upperBounds[i]) < tolerance * 10.0; if (atLower && g >= 0.0) { - continue; // KKT satisfied at lower bound (gradient pushes into bound) + continue; // KKT satisfied at lower bound (gradient pushes into bound) } if (atUpper && g <= 0.0) { - continue; // KKT satisfied at upper bound (gradient pushes into bound) + continue; // KKT satisfied at upper bound (gradient pushes into bound) } maxError = Math.max(maxError, Math.abs(g)); } @@ -508,19 +511,19 @@ private double computeKKTError(double[] gradF, double[] gEq, double[] hIneq, dou // Primal feasibility: inequality constraints h(x) >= 0, violation when h < 0 for (int j = 0; j < hIneq.length; j++) { if (hIneq[j] < 0) { - maxError = Math.max(maxError, -hIneq[j]); + maxError = Math.max(maxError, -hIneq[j]); } } // Bound feasibility if (lowerBounds != null && upperBounds != null) { for (int i = 0; i < n; i++) { - if (x[i] < lowerBounds[i]) { - maxError = Math.max(maxError, lowerBounds[i] - x[i]); - } - if (x[i] > upperBounds[i]) { - maxError = Math.max(maxError, x[i] - upperBounds[i]); - } + if (x[i] < lowerBounds[i]) { + maxError = Math.max(maxError, lowerBounds[i] - x[i]); + } + if (x[i] > upperBounds[i]) { + maxError = Math.max(maxError, x[i] - upperBounds[i]); + } } } @@ -531,8 +534,8 @@ private double computeKKTError(double[] gradF, double[] gEq, double[] hIneq, dou * Solve the QP sub-problem for the SQP search direction. * *

- * Minimizes: 0.5 * d^T * H * d + grad_f^T * d subject to linearized constraints. Uses a simplified projected gradient - * approach with active-set handling. + * Minimizes: 0.5 * d^T * H * d + grad_f^T * d subject to linearized constraints. Uses a + * simplified projected gradient approach with active-set handling. *

* * @param gradF gradient of objective @@ -543,8 +546,8 @@ private double computeKKTError(double[] gradF, double[] gEq, double[] hIneq, dou * @param x current point * @return search direction d */ - private double[] solveQPSubproblem(double[] gradF, double[] gEq, double[] hIneq, double[][] jacEq, double[][] jacIneq, - double[] x) { + private double[] solveQPSubproblem(double[] gradF, double[] gEq, double[] hIneq, double[][] jacEq, + double[][] jacIneq, double[] x) { int mEq = gEq.length; int mIneq = hIneq.length; @@ -562,7 +565,7 @@ private double[] solveQPSubproblem(double[] gradF, double[] gEq, double[] hIneq, List activeIneq = new ArrayList(); for (int j = 0; j < mIneq; j++) { if (hIneq[j] < tolerance * 10.0) { - activeIneq.add(j); + activeIneq.add(j); } } @@ -589,28 +592,36 @@ private double[] solveQPSubproblem(double[] gradF, double[] gEq, double[] hIneq, // d = H^{-1} * (-gradF + A^T * lambda) // A * H^{-1} * A^T * lambda = A * H^{-1} * gradF + c double[] dUnconstrained = solveLinearSystem(hessian, negateVector(gradF)); - double[][] hInv = invertMatrix(hessian); + double[][] hInv; + try { + hInv = LinearAlgebraOps.inverse(hessian); + } catch (RuntimeException e) { + hInv = new double[n][n]; + for (int i = 0; i < n; i++) { + hInv[i][i] = 1.0; + } + } // S = A * H^{-1} * A^T (Schur complement) double[][] hInvAt = new double[n][mActive]; for (int i = 0; i < n; i++) { for (int j = 0; j < mActive; j++) { - double sum = 0.0; - for (int k = 0; k < n; k++) { - sum += hInv[i][k] * aMatrix[j][k]; - } - hInvAt[i][j] = sum; + double sum = 0.0; + for (int k = 0; k < n; k++) { + sum += hInv[i][k] * aMatrix[j][k]; + } + hInvAt[i][j] = sum; } } double[][] schur = new double[mActive][mActive]; for (int i = 0; i < mActive; i++) { for (int j = 0; j < mActive; j++) { - double sum = 0.0; - for (int k = 0; k < n; k++) { - sum += aMatrix[i][k] * hInvAt[k][j]; - } - schur[i][j] = sum; + double sum = 0.0; + for (int k = 0; k < n; k++) { + sum += aMatrix[i][k] * hInvAt[k][j]; + } + schur[i][j] = sum; } } @@ -619,7 +630,7 @@ private double[] solveQPSubproblem(double[] gradF, double[] gEq, double[] hIneq, for (int i = 0; i < mActive; i++) { double aDu = 0.0; for (int k = 0; k < n; k++) { - aDu += aMatrix[i][k] * dUnconstrained[k]; + aDu += aMatrix[i][k] * dUnconstrained[k]; } rhs[i] = residual[i] - aDu; } @@ -640,7 +651,7 @@ private double[] solveQPSubproblem(double[] gradF, double[] gEq, double[] hIneq, double[] dx = Arrays.copyOf(dUnconstrained, n); for (int i = 0; i < n; i++) { for (int j = 0; j < mActive; j++) { - dx[i] += hInvAt[i][j] * lambdaActive[j]; + dx[i] += hInvAt[i][j] * lambdaActive[j]; } } @@ -664,7 +675,7 @@ private double lineSearch(double[] x, double[] dx, double f0, double[] gEq, doub for (int ls = 0; ls < maxLineSearchIterations; ls++) { double[] xTrial = new double[n]; for (int i = 0; i < n; i++) { - xTrial[i] = x[i] + alpha * dx[i]; + xTrial[i] = x[i] + alpha * dx[i]; } projectToBounds(xTrial); @@ -675,7 +686,7 @@ private double lineSearch(double[] x, double[] dx, double f0, double[] gEq, doub // Armijo condition on merit function if (meritTrial <= merit0 - armijoC1 * alpha * merit0) { - return alpha; + return alpha; } alpha *= 0.5; } @@ -698,7 +709,7 @@ private double computeMerit(double f, double[] gEq, double[] hIneq) { } for (int j = 0; j < hIneq.length; j++) { if (hIneq[j] < 0) { - merit += penaltyParameter * (-hIneq[j]); + merit += penaltyParameter * (-hIneq[j]); } } return merit; @@ -714,7 +725,8 @@ private double computeMerit(double f, double[] gEq, double[] hIneq) { * @param gEq equality constraints (for Lagrangian gradient) * @param hIneq inequality constraints (for Lagrangian gradient) */ - private void updateBFGS(double[] x, double[] xPrev, double[] gradF, double[] gradPrev, double[] gEq, double[] hIneq) { + private void updateBFGS(double[] x, double[] xPrev, double[] gradF, double[] gradPrev, + double[] gEq, double[] hIneq) { double[] s = new double[n]; // Step double[] y = new double[n]; // Gradient change @@ -724,7 +736,7 @@ private void updateBFGS(double[] x, double[] xPrev, double[] gradF, double[] gra } double sy = dotProduct(s, y); - double[] hs = matVecMult(hessian, s); + double[] hs = LinearAlgebraOps.multiply(hessian, s, 1.0); double shs = dotProduct(s, hs); // Powell's damped BFGS: ensure positive definiteness @@ -746,7 +758,7 @@ private void updateBFGS(double[] x, double[] xPrev, double[] gradF, double[] gra // BFGS update: H_new = H - (H*s*s^T*H)/(s^T*H*s) + (r*r^T)/(s^T*r) for (int i = 0; i < n; i++) { for (int j = 0; j < n; j++) { - hessian[i][j] = hessian[i][j] - hs[i] * hs[j] / shs + r[i] * r[j] / sr; + hessian[i][j] = hessian[i][j] - hs[i] * hs[j] / shs + r[i] * r[j] / sr; } } } @@ -762,117 +774,15 @@ private void updateBFGS(double[] x, double[] xPrev, double[] gradF, double[] gra */ private double[] solveLinearSystem(double[][] aMatrix, double[] b) { int dim = b.length; - // Augmented matrix - double[][] aug = new double[dim][dim + 1]; - for (int i = 0; i < dim; i++) { - System.arraycopy(aMatrix[i], 0, aug[i], 0, dim); - aug[i][dim] = b[i]; - } - - // Forward elimination with partial pivoting - for (int col = 0; col < dim; col++) { - // Find pivot - int maxRow = col; - double maxVal = Math.abs(aug[col][col]); - for (int row = col + 1; row < dim; row++) { - if (Math.abs(aug[row][col]) > maxVal) { - maxVal = Math.abs(aug[row][col]); - maxRow = row; - } - } - // Swap - double[] temp = aug[col]; - aug[col] = aug[maxRow]; - aug[maxRow] = temp; - - if (Math.abs(aug[col][col]) < 1e-14) { - // Near-singular: return gradient descent direction - double[] result = new double[dim]; - for (int i = 0; i < dim; i++) { - result[i] = -b[i]; - } - return result; - } - - // Eliminate - for (int row = col + 1; row < dim; row++) { - double factor = aug[row][col] / aug[col][col]; - for (int k = col; k <= dim; k++) { - aug[row][k] -= factor * aug[col][k]; - } - } - } - - // Back substitution double[] result = new double[dim]; - for (int i = dim - 1; i >= 0; i--) { - result[i] = aug[i][dim]; - for (int j = i + 1; j < dim; j++) { - result[i] -= aug[i][j] * result[j]; + if (!LinearAlgebraOps.solveLinearSystem(aMatrix, b, result)) { + for (int i = 0; i < dim; i++) { + result[i] = -b[i]; } - result[i] /= aug[i][i]; } return result; } - /** - * Invert a matrix using Gauss-Jordan elimination. - * - * @param matrix square matrix to invert - * @return inverse matrix - */ - private double[][] invertMatrix(double[][] matrix) { - int dim = matrix.length; - double[][] aug = new double[dim][2 * dim]; - - for (int i = 0; i < dim; i++) { - System.arraycopy(matrix[i], 0, aug[i], 0, dim); - aug[i][dim + i] = 1.0; - } - - for (int col = 0; col < dim; col++) { - // Pivot - int maxRow = col; - for (int row = col + 1; row < dim; row++) { - if (Math.abs(aug[row][col]) > Math.abs(aug[maxRow][col])) { - maxRow = row; - } - } - double[] temp = aug[col]; - aug[col] = aug[maxRow]; - aug[maxRow] = temp; - - double pivot = aug[col][col]; - if (Math.abs(pivot) < 1e-14) { - // Return identity for near-singular - double[][] identity = new double[dim][dim]; - for (int i = 0; i < dim; i++) { - identity[i][i] = 1.0; - } - return identity; - } - - for (int k = 0; k < 2 * dim; k++) { - aug[col][k] /= pivot; - } - - for (int row = 0; row < dim; row++) { - if (row != col) { - double factor = aug[row][col]; - for (int k = 0; k < 2 * dim; k++) { - aug[row][k] -= factor * aug[col][k]; - } - } - } - } - - double[][] inv = new double[dim][dim]; - for (int i = 0; i < dim; i++) { - System.arraycopy(aug[i], dim, inv[i], 0, dim); - } - return inv; - } - /** * Negate a vector. * @@ -902,26 +812,6 @@ private double dotProduct(double[] a, double[] b) { return sum; } - /** - * Matrix-vector multiplication. - * - * @param matrix the matrix - * @param vec the vector - * @return result vector - */ - private double[] matVecMult(double[][] matrix, double[] vec) { - int rows = matrix.length; - double[] result = new double[rows]; - for (int i = 0; i < rows; i++) { - double sum = 0.0; - for (int j = 0; j < vec.length; j++) { - sum += matrix[i][j] * vec[j]; - } - result[i] = sum; - } - return result; - } - /** * Optimization result container. */ @@ -953,8 +843,8 @@ public static class OptimizationResult implements Serializable { * @param converged whether converged * @param kktError final KKT error */ - public OptimizationResult(double[] optimalPoint, double optimalValue, int iterations, boolean converged, - double kktError) { + public OptimizationResult(double[] optimalPoint, double optimalValue, int iterations, + boolean converged, double kktError) { this.optimalPoint = Arrays.copyOf(optimalPoint, optimalPoint.length); this.optimalValue = optimalValue; this.iterations = iterations; diff --git a/src/main/java/neqsim/process/util/reconciliation/DataReconciliationEngine.java b/src/main/java/neqsim/process/util/reconciliation/DataReconciliationEngine.java index f70a4d076c..ae840c3008 100644 --- a/src/main/java/neqsim/process/util/reconciliation/DataReconciliationEngine.java +++ b/src/main/java/neqsim/process/util/reconciliation/DataReconciliationEngine.java @@ -6,7 +6,9 @@ import java.util.List; import org.apache.logging.log4j.LogManager; import org.apache.logging.log4j.Logger; -import org.ejml.simple.SimpleMatrix; +import org.ojalgo.matrix.store.MatrixStore; +import org.ojalgo.matrix.store.Primitive64Store; +import neqsim.util.math.LinearAlgebraOps; /** * Data reconciliation engine using weighted least squares (WLS) with linear constraints. @@ -268,20 +270,20 @@ public ReconciliationResult reconcile() { */ private ReconciliationResult solveWLS(int n, int m, long startTime) { // Build measurement vector y (n x 1) - SimpleMatrix y = new SimpleMatrix(n, 1); + Primitive64Store y = Primitive64Store.FACTORY.make(n, 1); for (int i = 0; i < n; i++) { y.set(i, 0, variables.get(i).getMeasuredValue()); } // Build covariance matrix V = diag(sigma^2) (n x n) - SimpleMatrix bigV = new SimpleMatrix(n, n); + Primitive64Store bigV = Primitive64Store.FACTORY.make(n, n); for (int i = 0; i < n; i++) { double sigma = variables.get(i).getUncertainty(); bigV.set(i, i, sigma * sigma); } // Build constraint matrix A (m x n) - SimpleMatrix bigA = new SimpleMatrix(m, n); + Primitive64Store bigA = Primitive64Store.FACTORY.make(m, n); for (int i = 0; i < m; i++) { double[] row = constraintRows.get(i); for (int j = 0; j < n; j++) { @@ -290,7 +292,7 @@ private ReconciliationResult solveWLS(int n, int m, long startTime) { } // Constraint residuals before: r_before = A * y - SimpleMatrix rBefore = bigA.mult(y); + MatrixStore rBefore = bigA.multiply(y); double[] residualsBefore = new double[m]; for (int i = 0; i < m; i++) { residualsBefore[i] = rBefore.get(i, 0); @@ -298,21 +300,24 @@ private ReconciliationResult solveWLS(int n, int m, long startTime) { // Solve: x_adj = y - V * A^T * solve(A * V * A^T, A * y) // Step 1: A * V * A^T (m x m) - SimpleMatrix aTimesV = bigA.mult(bigV); - SimpleMatrix avat = aTimesV.mult(bigA.transpose()); + MatrixStore aTimesV = bigA.multiply(bigV); + MatrixStore avat = aTimesV.multiply(bigA.transpose()); // Step 2: solve(AVAT, A*y) without forming AVAT^-1 - SimpleMatrix lagrangeMultipliers = avat.solve(rBefore); + MatrixStore lagrangeMultipliers = LinearAlgebraOps.solveLinearSystem(avat, rBefore); // Step 3: correction = V * A^T * solve(AVAT, A*y) - SimpleMatrix vAt = bigV.mult(bigA.transpose()); - SimpleMatrix correction = vAt.mult(lagrangeMultipliers); + MatrixStore vAt = bigV.multiply(bigA.transpose()); + MatrixStore correction = vAt.multiply(lagrangeMultipliers); // Step 4: reconciled = y - correction - SimpleMatrix xAdj = y.minus(correction); + Primitive64Store xAdj = Primitive64Store.FACTORY.make(n, 1); + for (int i = 0; i < n; i++) { + xAdj.set(i, 0, y.get(i, 0) - correction.get(i, 0)); + } // Constraint residuals after: r_after = A * x_adj (should be ~0) - SimpleMatrix rAfter = bigA.mult(xAdj); + MatrixStore rAfter = bigA.multiply(xAdj); double[] residualsAfter = new double[m]; for (int i = 0; i < m; i++) { residualsAfter[i] = rAfter.get(i, 0); @@ -333,9 +338,14 @@ private ReconciliationResult solveWLS(int n, int m, long startTime) { // Gross error detection via normalized residuals // Covariance of adjustments: V_adj = V - V * A^T * solve(AVAT, A * V) - SimpleMatrix solvedAv = avat.solve(aTimesV); - SimpleMatrix projectionM = vAt.mult(solvedAv); - SimpleMatrix vAdj = bigV.minus(projectionM); + MatrixStore solvedAv = LinearAlgebraOps.solveLinearSystem(avat, aTimesV); + MatrixStore projectionM = vAt.multiply(solvedAv); + Primitive64Store vAdj = Primitive64Store.FACTORY.make(n, n); + for (int i = 0; i < n; i++) { + for (int j = 0; j < n; j++) { + vAdj.set(i, j, bigV.get(i, j) - projectionM.get(i, j)); + } + } detectGrossErrors(n, vAdj); @@ -384,7 +394,7 @@ private ReconciliationResult solveWLS(int n, int m, long startTime) { * @param n number of variables * @param vAdj covariance matrix of reconciled adjustments (V - V*A^T*(AVA^T)^-1*A*V) */ - private void detectGrossErrors(int n, SimpleMatrix vAdj) { + private void detectGrossErrors(int n, MatrixStore vAdj) { for (int i = 0; i < n; i++) { ReconciliationVariable v = variables.get(i); double sigma = v.getUncertainty(); diff --git a/src/main/java/neqsim/thermo/phase/PhaseElectrolyteCPA.java b/src/main/java/neqsim/thermo/phase/PhaseElectrolyteCPA.java index 4ddeac7979..06b136b552 100644 --- a/src/main/java/neqsim/thermo/phase/PhaseElectrolyteCPA.java +++ b/src/main/java/neqsim/thermo/phase/PhaseElectrolyteCPA.java @@ -2,17 +2,14 @@ import org.apache.logging.log4j.LogManager; import org.apache.logging.log4j.Logger; -import org.ejml.data.DMatrixRMaj; -import org.ejml.dense.row.CommonOps_DDRM; -import org.ejml.dense.row.NormOps_DDRM; -import org.ejml.dense.row.factory.LinearSolverFactory_DDRM; -import org.ejml.interfaces.linsol.LinearSolverDense; -import org.ejml.simple.SimpleMatrix; +import org.ojalgo.matrix.decomposition.LU; import neqsim.thermo.component.ComponentCPAInterface; import neqsim.thermo.component.ComponentElectrolyteCPA; import neqsim.thermo.mixingrule.CPAMixingRuleHandler; import neqsim.thermo.mixingrule.CPAMixingRulesInterface; import neqsim.thermo.mixingrule.MixingRuleTypeInterface; +import neqsim.util.math.LinearAlgebraOps; +import neqsim.util.math.LinearAlgebraOps.DenseMatrix; /** *

@@ -22,7 +19,8 @@ * @author Even Solbraa * @version $Id: $Id */ -public class PhaseElectrolyteCPA extends PhaseModifiedFurstElectrolyteEos implements PhaseCPAInterface { +public class PhaseElectrolyteCPA extends PhaseModifiedFurstElectrolyteEos + implements PhaseCPAInterface { /** Serialization version UID. */ private static final long serialVersionUID = 1000; /** Logger object for class. */ @@ -55,31 +53,31 @@ public class PhaseElectrolyteCPA extends PhaseModifiedFurstElectrolyteEos implem private double[][] deltadT = null; private double[][] deltadTdT = null; private double[][][] Klkni = null; - private SimpleMatrix KlkTVMatrix = null; - private SimpleMatrix KlkTTMatrix = null; - private SimpleMatrix KlkTMatrix = null; - private SimpleMatrix udotTimesmMatrix = null; - private SimpleMatrix mVector = null; - private SimpleMatrix udotMatrix = null; - private SimpleMatrix uMatrix = null; - private SimpleMatrix QMatksiksiksi = null; - private SimpleMatrix KlkVVVMatrix = null; - private SimpleMatrix KlkVVMatrix = null; - private SimpleMatrix udotTimesmiMatrix = null; - private SimpleMatrix ksiMatrix = null; - private SimpleMatrix KlkMatrix = null; - private SimpleMatrix hessianMatrix = null; - private SimpleMatrix hessianInvers = null; + private DenseMatrix KlkTVMatrix = null; + private DenseMatrix KlkTTMatrix = null; + private DenseMatrix KlkTMatrix = null; + private DenseMatrix udotTimesmMatrix = null; + private DenseMatrix mVector = null; + private DenseMatrix udotMatrix = null; + private DenseMatrix uMatrix = null; + private DenseMatrix QMatksiksiksi = null; + private DenseMatrix KlkVVVMatrix = null; + private DenseMatrix KlkVVMatrix = null; + private DenseMatrix udotTimesmiMatrix = null; + private DenseMatrix ksiMatrix = null; + private DenseMatrix KlkMatrix = null; + private DenseMatrix hessianMatrix = null; + private DenseMatrix hessianInvers = null; /** Cached LU factorization of {@code hessianMatrix} for repeated Hessian backsolves. */ - private transient LinearSolverDense hessianLU = null; + private transient LU hessianLU = null; /** Scratch matrix passed to EJML LU because the solver may decompose its input in place. */ - private transient DMatrixRMaj hessianLUinput = null; + private transient DenseMatrix hessianLUinput = null; /** Matrix size associated with the cached Hessian LU factorization. */ private transient int hessianLUSize = -1; - private SimpleMatrix KlkVMatrix = null; - DMatrixRMaj corr2Matrix = null; - DMatrixRMaj corr3Matrix = null; - DMatrixRMaj corr4Matrix = null; + private DenseMatrix KlkVMatrix = null; + DenseMatrix corr2Matrix = null; + DenseMatrix corr3Matrix = null; + DenseMatrix corr4Matrix = null; private double[] lngi; /** @@ -87,8 +85,7 @@ public class PhaseElectrolyteCPA extends PhaseModifiedFurstElectrolyteEos implem * Constructor for PhaseElectrolyteCPA. *

*/ - public PhaseElectrolyteCPA() { - } + public PhaseElectrolyteCPA() {} /** {@inheritDoc} */ @Override @@ -119,63 +116,78 @@ public void setMixingRule(MixingRuleTypeInterface mr) { /** {@inheritDoc} */ @Override - public void init(double totalNumberOfMoles, int numberOfComponents, int initType, PhaseType pt, double beta) { + public void init(double totalNumberOfMoles, int numberOfComponents, int initType, PhaseType pt, + double beta) { if (initType == 0) { setTotalNumberOfAccociationSites(0); selfAccociationScheme = new int[numberOfComponents][0][0]; crossAccociationScheme = new int[numberOfComponents][numberOfComponents][0][0]; for (int i = 0; i < numberOfComponents; i++) { - if (getComponent(i).getNumberOfmoles() < 1e-100) { - getComponent(i).setNumberOfAssociationSites(0); - } else { - getComponent(i).setNumberOfAssociationSites(getComponent(i).getOrginalNumberOfAssociationSites()); - setTotalNumberOfAccociationSites( - getTotalNumberOfAccociationSites() + getComponent(i).getNumberOfAssociationSites()); - selfAccociationScheme[i] = cpaSelect.setAssociationScheme(i, this); - for (int j = 0; j < numberOfComponents; j++) { - crossAccociationScheme[i][j] = cpaSelect.setCrossAssociationScheme(i, j, this); - } - } + if (getComponent(i).getNumberOfmoles() < 1e-100) { + getComponent(i).setNumberOfAssociationSites(0); + } else { + getComponent(i) + .setNumberOfAssociationSites(getComponent(i).getOrginalNumberOfAssociationSites()); + setTotalNumberOfAccociationSites( + getTotalNumberOfAccociationSites() + getComponent(i).getNumberOfAssociationSites()); + selfAccociationScheme[i] = cpaSelect.setAssociationScheme(i, this); + for (int j = 0; j < numberOfComponents; j++) { + crossAccociationScheme[i][j] = cpaSelect.setCrossAssociationScheme(i, j, this); + } + } } if (getTotalNumberOfAccociationSites() != oldTotalNumberOfAccociationSites) { - mVector = new SimpleMatrix(getTotalNumberOfAccociationSites(), 1); - KlkMatrix = new SimpleMatrix(getTotalNumberOfAccociationSites(), getTotalNumberOfAccociationSites()); - KlkVMatrix = new SimpleMatrix(getTotalNumberOfAccociationSites(), getTotalNumberOfAccociationSites()); - KlkVVMatrix = new SimpleMatrix(getTotalNumberOfAccociationSites(), getTotalNumberOfAccociationSites()); - KlkVVVMatrix = new SimpleMatrix(getTotalNumberOfAccociationSites(), getTotalNumberOfAccociationSites()); - hessianMatrix = new SimpleMatrix(getTotalNumberOfAccociationSites(), getTotalNumberOfAccociationSites()); - KlkTMatrix = new SimpleMatrix(getTotalNumberOfAccociationSites(), getTotalNumberOfAccociationSites()); - KlkTTMatrix = new SimpleMatrix(getTotalNumberOfAccociationSites(), getTotalNumberOfAccociationSites()); - KlkTVMatrix = new SimpleMatrix(getTotalNumberOfAccociationSites(), getTotalNumberOfAccociationSites()); - corr2Matrix = new DMatrixRMaj(getTotalNumberOfAccociationSites(), 1); - corr3Matrix = new DMatrixRMaj(getTotalNumberOfAccociationSites(), 1); - corr4Matrix = new DMatrixRMaj(getTotalNumberOfAccociationSites(), 1); - Klkni = new double[numberOfComponents][getTotalNumberOfAccociationSites()][getTotalNumberOfAccociationSites()]; - ksiMatrix = new SimpleMatrix(getTotalNumberOfAccociationSites(), 1); - uMatrix = new SimpleMatrix(getTotalNumberOfAccociationSites(), 1); - udotMatrix = new SimpleMatrix(getTotalNumberOfAccociationSites(), 1); - moleculeNumber = new int[getTotalNumberOfAccociationSites()]; - assSiteNumber = new int[getTotalNumberOfAccociationSites()]; - gvector = new double[getTotalNumberOfAccociationSites()][1]; - udotTimesmMatrix = new SimpleMatrix(getTotalNumberOfAccociationSites(), 1); - udotTimesmiMatrix = new SimpleMatrix(getNumberOfComponents(), getTotalNumberOfAccociationSites()); - delta = new double[getTotalNumberOfAccociationSites()][getTotalNumberOfAccociationSites()]; - deltaNog = new double[getTotalNumberOfAccociationSites()][getTotalNumberOfAccociationSites()]; - deltadT = new double[getTotalNumberOfAccociationSites()][getTotalNumberOfAccociationSites()]; - deltadTdT = new double[getTotalNumberOfAccociationSites()][getTotalNumberOfAccociationSites()]; - QMatksiksiksi = new SimpleMatrix(getTotalNumberOfAccociationSites(), 1); - lngi = new double[numberOfComponents]; + mVector = new DenseMatrix(getTotalNumberOfAccociationSites(), 1); + KlkMatrix = + new DenseMatrix(getTotalNumberOfAccociationSites(), getTotalNumberOfAccociationSites()); + KlkVMatrix = + new DenseMatrix(getTotalNumberOfAccociationSites(), getTotalNumberOfAccociationSites()); + KlkVVMatrix = + new DenseMatrix(getTotalNumberOfAccociationSites(), getTotalNumberOfAccociationSites()); + KlkVVVMatrix = + new DenseMatrix(getTotalNumberOfAccociationSites(), getTotalNumberOfAccociationSites()); + hessianMatrix = + new DenseMatrix(getTotalNumberOfAccociationSites(), getTotalNumberOfAccociationSites()); + KlkTMatrix = + new DenseMatrix(getTotalNumberOfAccociationSites(), getTotalNumberOfAccociationSites()); + KlkTTMatrix = + new DenseMatrix(getTotalNumberOfAccociationSites(), getTotalNumberOfAccociationSites()); + KlkTVMatrix = + new DenseMatrix(getTotalNumberOfAccociationSites(), getTotalNumberOfAccociationSites()); + corr2Matrix = new DenseMatrix(getTotalNumberOfAccociationSites(), 1); + corr3Matrix = new DenseMatrix(getTotalNumberOfAccociationSites(), 1); + corr4Matrix = new DenseMatrix(getTotalNumberOfAccociationSites(), 1); + Klkni = + new double[numberOfComponents][getTotalNumberOfAccociationSites()][getTotalNumberOfAccociationSites()]; + ksiMatrix = new DenseMatrix(getTotalNumberOfAccociationSites(), 1); + uMatrix = new DenseMatrix(getTotalNumberOfAccociationSites(), 1); + udotMatrix = new DenseMatrix(getTotalNumberOfAccociationSites(), 1); + moleculeNumber = new int[getTotalNumberOfAccociationSites()]; + assSiteNumber = new int[getTotalNumberOfAccociationSites()]; + gvector = new double[getTotalNumberOfAccociationSites()][1]; + udotTimesmMatrix = new DenseMatrix(getTotalNumberOfAccociationSites(), 1); + udotTimesmiMatrix = + new DenseMatrix(getNumberOfComponents(), getTotalNumberOfAccociationSites()); + delta = new double[getTotalNumberOfAccociationSites()][getTotalNumberOfAccociationSites()]; + deltaNog = + new double[getTotalNumberOfAccociationSites()][getTotalNumberOfAccociationSites()]; + deltadT = + new double[getTotalNumberOfAccociationSites()][getTotalNumberOfAccociationSites()]; + deltadTdT = + new double[getTotalNumberOfAccociationSites()][getTotalNumberOfAccociationSites()]; + QMatksiksiksi = new DenseMatrix(getTotalNumberOfAccociationSites(), 1); + lngi = new double[numberOfComponents]; } oldTotalNumberOfAccociationSites = getTotalNumberOfAccociationSites(); int temp = 0; for (int i = 0; i < numberOfComponents; i++) { - for (int j = 0; j < getComponent(i).getNumberOfAssociationSites(); j++) { - moleculeNumber[temp + j] = i; - assSiteNumber[temp + j] = j; - } - temp += getComponent(i).getNumberOfAssociationSites(); + for (int j = 0; j < getComponent(i).getNumberOfAssociationSites(); j++) { + moleculeNumber[temp + j] = i; + assSiteNumber[temp + j] = j; + } + temp += getComponent(i).getNumberOfAssociationSites(); } } @@ -218,9 +230,9 @@ public void init(double totalNumberOfMoles, int numberOfComponents, int initType public void calcDelta() { for (int i = 0; i < getTotalNumberOfAccociationSites(); i++) { for (int j = i; j < getTotalNumberOfAccociationSites(); j++) { - deltaNog[i][j] = cpamix.calcDeltaNog(assSiteNumber[i], assSiteNumber[j], moleculeNumber[i], moleculeNumber[j], - this, getTemperature(), getPressure(), numberOfComponents); - deltaNog[j][i] = deltaNog[i][j]; + deltaNog[i][j] = cpamix.calcDeltaNog(assSiteNumber[i], assSiteNumber[j], moleculeNumber[i], + moleculeNumber[j], this, getTemperature(), getPressure(), numberOfComponents); + deltaNog[j][i] = deltaNog[i][j]; } } } @@ -249,39 +261,40 @@ public void initCPAMatrix(int type) { double tempVar2; for (int i = 0; i < numberOfComponents; i++) { for (int j = 0; j < componentArray[i].getNumberOfAssociationSites(); j++) { - tempVar1 = ksiMatrix.get(temp + j, 0); - tempVar2 = udotMatrix.get(temp + j, 0); - uMatrix.set(temp + j, 0, Math.log(tempVar1) - tempVar1 + 1.0); - gvector[temp + j][0] = mVector.get(temp + j, 0) * tempVar2; - - if (moleculeNumber[temp + j] == i) { - udotTimesmiMatrix.set(i, temp + j, tempVar2); - } else { - udotTimesmiMatrix.set(i, temp + j, 0.0); - } + tempVar1 = ksiMatrix.unsafe_get(temp + j, 0); + tempVar2 = udotMatrix.unsafe_get(temp + j, 0); + uMatrix.unsafe_set(temp + j, 0, Math.log(tempVar1) - tempVar1 + 1.0); + gvector[temp + j][0] = mVector.unsafe_get(temp + j, 0) * tempVar2; + + if (moleculeNumber[temp + j] == i) { + udotTimesmiMatrix.unsafe_set(i, temp + j, tempVar2); + } else { + udotTimesmiMatrix.unsafe_set(i, temp + j, 0.0); + } } temp += componentArray[i].getNumberOfAssociationSites(); } if (type > 2) { for (int p = 0; p < numberOfComponents; p++) { - lngi[p] = ((ComponentElectrolyteCPA) componentArray[p]).calc_lngi(this); + lngi[p] = ((ComponentElectrolyteCPA) componentArray[p]).calc_lngi(this); } } for (int i = 0; i < totalNumberOfAccociationSites; i++) { for (int j = i; j < totalNumberOfAccociationSites; j++) { - delta[i][j] = deltaNog[i][j] * gcpa; - delta[j][i] = delta[i][j]; - if (type > 1) { - deltadT[i][j] = cpamix.calcDeltadT(assSiteNumber[i], assSiteNumber[j], moleculeNumber[i], moleculeNumber[j], - this, getTemperature(), getPressure(), numberOfComponents); - deltadT[j][i] = deltadT[i][j]; - - deltadTdT[i][j] = cpamix.calcDeltadTdT(assSiteNumber[i], assSiteNumber[j], moleculeNumber[i], - moleculeNumber[j], this, getTemperature(), getPressure(), numberOfComponents); - deltadTdT[j][i] = deltadTdT[i][j]; - } + delta[i][j] = deltaNog[i][j] * gcpa; + delta[j][i] = delta[i][j]; + if (type > 1) { + deltadT[i][j] = cpamix.calcDeltadT(assSiteNumber[i], assSiteNumber[j], moleculeNumber[i], + moleculeNumber[j], this, getTemperature(), getPressure(), numberOfComponents); + deltadT[j][i] = deltadT[i][j]; + + deltadTdT[i][j] = + cpamix.calcDeltadTdT(assSiteNumber[i], assSiteNumber[j], moleculeNumber[i], + moleculeNumber[j], this, getTemperature(), getPressure(), numberOfComponents); + deltadTdT[j][i] = deltadTdT[i][j]; + } } } @@ -296,87 +309,91 @@ public void initCPAMatrix(int type) { double tempKsiRead = 0.0; for (int i = 0; i < totalNumberOfAccociationSites; i++) { for (int j = i; j < totalNumberOfAccociationSites; j++) { - Klk = KlkMatrix.get(i, j); - tempVar = Klk * gdv1; - KlkVMatrix.set(i, j, tempVar); - KlkVMatrix.set(j, i, tempVar); - - tempVar = Klk * gdv2 + Klk * (gcpavv + 1.0 / totalVolume2); - KlkVVMatrix.set(i, j, tempVar); - KlkVVMatrix.set(j, i, tempVar); - - tempVar = Klk * gdv3 + 3.0 * Klk * (gcpav - 1.0 / totalVolume) * (gcpavv + 1.0 / totalVolume2) - + Klk * (gcpavvv - 2.0 / totalVolume3); - KlkVVVMatrix.set(i, j, tempVar); - KlkVVVMatrix.set(j, i, tempVar); - - if (type > 1) { - tempVar = deltadT[i][j] / delta[i][j]; - - if (Math.abs(tempVar) > 1e-50) { - double tempVardT = deltadTdT[i][j] / delta[i][j] - - (deltadT[i][j] * deltadT[i][j]) / (delta[i][j] * delta[i][j]); - - tempVar2 = Klk * tempVar; - KlkTMatrix.set(i, j, tempVar2); - KlkTMatrix.set(j, i, tempVar2); - - tempVar2 = Klk * tempVar * (gcpav - 1.0 / totalVolume); - KlkTVMatrix.set(i, j, tempVar2); - KlkTVMatrix.set(j, i, tempVar2); - - tempVar2 = Klk * (tempVar * tempVar + tempVardT); - KlkTTMatrix.set(i, j, tempVar2); - KlkTTMatrix.set(j, i, tempVar2); - } - - if (type > 2) { - for (int p = 0; p < numberOfComponents; p++) { - double t1 = 0.0; - double t2 = 0.0; - if (moleculeNumber[i] == p) { - t1 = 1.0 / mVector.get(i, 0); - } - if (moleculeNumber[j] == p) { - t2 = 1.0 / mVector.get(j, 0); - } - Klkni[p][i][j] = Klk * (t1 + t2 + lngi[p]); - Klkni[p][j][i] = Klkni[p][i][j]; - } - } - } + Klk = KlkMatrix.unsafe_get(i, j); + tempVar = Klk * gdv1; + KlkVMatrix.unsafe_set(i, j, tempVar); + KlkVMatrix.unsafe_set(j, i, tempVar); + + tempVar = Klk * gdv2 + Klk * (gcpavv + 1.0 / totalVolume2); + KlkVVMatrix.unsafe_set(i, j, tempVar); + KlkVVMatrix.unsafe_set(j, i, tempVar); + + tempVar = + Klk * gdv3 + 3.0 * Klk * (gcpav - 1.0 / totalVolume) * (gcpavv + 1.0 / totalVolume2) + + Klk * (gcpavvv - 2.0 / totalVolume3); + KlkVVVMatrix.unsafe_set(i, j, tempVar); + KlkVVVMatrix.unsafe_set(j, i, tempVar); + + if (type > 1) { + tempVar = deltadT[i][j] / delta[i][j]; + + if (Math.abs(tempVar) > 1e-50) { + double tempVardT = deltadTdT[i][j] / delta[i][j] + - (deltadT[i][j] * deltadT[i][j]) / (delta[i][j] * delta[i][j]); + + tempVar2 = Klk * tempVar; + KlkTMatrix.unsafe_set(i, j, tempVar2); + KlkTMatrix.unsafe_set(j, i, tempVar2); + + tempVar2 = Klk * tempVar * (gcpav - 1.0 / totalVolume); + KlkTVMatrix.unsafe_set(i, j, tempVar2); + KlkTVMatrix.unsafe_set(j, i, tempVar2); + + tempVar2 = Klk * (tempVar * tempVar + tempVardT); + KlkTTMatrix.unsafe_set(i, j, tempVar2); + KlkTTMatrix.unsafe_set(j, i, tempVar2); + } + + if (type > 2) { + for (int p = 0; p < numberOfComponents; p++) { + double t1 = 0.0; + double t2 = 0.0; + if (moleculeNumber[i] == p) { + t1 = 1.0 / mVector.unsafe_get(i, 0); + } + if (moleculeNumber[j] == p) { + t2 = 1.0 / mVector.unsafe_get(j, 0); + } + Klkni[p][i][j] = Klk * (t1 + t2 + lngi[p]); + Klkni[p][j][i] = Klkni[p][i][j]; + } + } + } } - tempKsiRead = ksiMatrix.get(i, 0); - QMatksiksiksi.set(i, 0, 2.0 * mVector.get(i, 0) / (tempKsiRead * tempKsiRead * tempKsiRead)); + tempKsiRead = ksiMatrix.unsafe_get(i, 0); + QMatksiksiksi.unsafe_set(i, 0, + 2.0 * mVector.unsafe_get(i, 0) / (tempKsiRead * tempKsiRead * tempKsiRead)); } - SimpleMatrix ksiMatrixTranspose = ksiMatrix.transpose(); + DenseMatrix ksiMatrixTranspose = transpose(ksiMatrix); // dXdV - SimpleMatrix KlkVMatrixksi = KlkVMatrix.mult(ksiMatrix); - SimpleMatrix XV = applyHessianInv(KlkVMatrixksi); - SimpleMatrix XVtranspose = XV.transpose(); - - SimpleMatrix QCPA = mVector.transpose().mult(uMatrix.minus(ksiMatrix.elementMult(udotMatrix).scale(0.5))); - FCPA = QCPA.get(0, 0); - - SimpleMatrix tempMatrix = ksiMatrixTranspose.mult(KlkVMatrixksi).scale(-0.5); - dFCPAdV = tempMatrix.get(0, 0); - SimpleMatrix KlkVVMatrixTImesKsi = KlkVVMatrix.mult(ksiMatrix); - SimpleMatrix tempMatrixVV = ksiMatrixTranspose.mult(KlkVVMatrixTImesKsi).scale(-0.5) - .minus(KlkVMatrixksi.transpose().mult(XV)); - dFCPAdVdV = tempMatrixVV.get(0, 0); - - SimpleMatrix QVVV = ksiMatrixTranspose.mult(KlkVVVMatrix.mult(ksiMatrix)).scale(-0.5); - SimpleMatrix QVVksi = KlkVVMatrixTImesKsi.scale(-1.0); - SimpleMatrix QksiVksi = KlkVMatrix.scale(-1.0); - - SimpleMatrix mat1 = QVVksi.transpose().mult(XV).scale(3.0); - SimpleMatrix mat2 = XVtranspose.mult(QksiVksi.mult(XV)).scale(3.0); - SimpleMatrix mat4 = XVtranspose.mult(QMatksiksiksi.mult(XVtranspose)).mult(XV); - - SimpleMatrix dFCPAdVdVdVMatrix = QVVV.plus(mat1).plus(mat2).plus(mat2).plus(mat4); - dFCPAdVdVdV = dFCPAdVdVdVMatrix.get(0, 0); + DenseMatrix KlkVMatrixksi = multiply(KlkVMatrix, ksiMatrix); + DenseMatrix XV = applyHessianInv(KlkVMatrixksi); + DenseMatrix XVtranspose = transpose(XV); + + DenseMatrix qCpa = multiply(transpose(mVector), + subtract(uMatrix, scale(elementMult(ksiMatrix, udotMatrix), 0.5))); + FCPA = qCpa.unsafe_get(0, 0); + + DenseMatrix tempMatrix = scale(multiply(ksiMatrixTranspose, KlkVMatrixksi), -0.5); + dFCPAdV = tempMatrix.unsafe_get(0, 0); + DenseMatrix klkVvMatrixTimesKsi = multiply(KlkVVMatrix, ksiMatrix); + DenseMatrix tempMatrixVV = + subtract(scale(multiply(ksiMatrixTranspose, klkVvMatrixTimesKsi), -0.5), + multiply(transpose(KlkVMatrixksi), XV)); + dFCPAdVdV = tempMatrixVV.unsafe_get(0, 0); + + DenseMatrix qVvv = scale(multiply(ksiMatrixTranspose, multiply(KlkVVVMatrix, ksiMatrix)), -0.5); + DenseMatrix qVvksi = scale(klkVvMatrixTimesKsi, -1.0); + DenseMatrix qKsiVksi = scale(KlkVMatrix, -1.0); + + DenseMatrix mat1 = scale(multiply(transpose(qVvksi), XV), 3.0); + DenseMatrix mat2 = scale(multiply(XVtranspose, multiply(qKsiVksi, XV)), 3.0); + DenseMatrix mat4 = multiply(multiply(XVtranspose, multiply(QMatksiksiksi, XVtranspose)), XV); + + DenseMatrix dFCPAdVdVdVMatrix = add(add(add(add(qVvv, mat1), mat2), mat2), mat4); + dFCPAdVdVdV = dFCPAdVdVdVMatrix.unsafe_get(0, 0); temp = 0; if (type == 1) { @@ -384,36 +401,38 @@ public void initCPAMatrix(int type) { } for (int p = 0; p < numberOfComponents; p++) { for (int kk = 0; kk < getComponent(p).getNumberOfAssociationSites(); kk++) { - ((ComponentCPAInterface) getComponent(p)).setXsitedV(kk, XV.get(temp + kk, 0)); + ((ComponentCPAInterface) getComponent(p)).setXsitedV(kk, XV.unsafe_get(temp + kk, 0)); } temp += getComponent(p).getNumberOfAssociationSites(); } - // KlkTMatrix = new SimpleMatrix(KlkdT); - SimpleMatrix KlkTMatrixTImesKsi = KlkTMatrix.mult(ksiMatrix); + // KlkTMatrix corresponds to dKlk/dT + DenseMatrix klkTMatrixTimesKsi = multiply(KlkTMatrix, ksiMatrix); // dQdT - SimpleMatrix tempMatrix2 = ksiMatrixTranspose.mult(KlkTMatrixTImesKsi).scale(-0.5); - dFCPAdT = tempMatrix2.get(0, 0); + DenseMatrix tempMatrix2 = scale(multiply(ksiMatrixTranspose, klkTMatrixTimesKsi), -0.5); + dFCPAdT = tempMatrix2.unsafe_get(0, 0); - // SimpleMatrix KlkTVMatrix = new SimpleMatrix(KlkdTdV); - // SimpleMatrix tempMatrixTV = + // KlkTVMatrix corresponds to d2Klk/(dTdV) + // tempMatrixTV = // ksiMatrixTranspose.mult(KlkTVMatrix.mult(ksiMatrix)).scale(-0.5).minus(KlkTMatrixTImesKsi.transpose().mult(XV)); // dFCPAdTdV = tempMatrixTV.get(0, 0); // dXdT - SimpleMatrix XT = applyHessianInv(KlkTMatrixTImesKsi); + DenseMatrix XT = applyHessianInv(klkTMatrixTimesKsi); // dQdTdT - SimpleMatrix tempMatrixTT = ksiMatrixTranspose.mult(KlkTTMatrix.mult(ksiMatrix)).scale(-0.5) - .minus(KlkTMatrixTImesKsi.transpose().mult(XT)); - dFCPAdTdT = tempMatrixTT.get(0, 0); + DenseMatrix tempMatrixTT = + subtract(scale(multiply(ksiMatrixTranspose, multiply(KlkTTMatrix, ksiMatrix)), -0.5), + multiply(transpose(klkTMatrixTimesKsi), XT)); + dFCPAdTdT = tempMatrixTT.unsafe_get(0, 0); - SimpleMatrix tempMatrixTV = ksiMatrixTranspose.mult(KlkTVMatrix.mult(ksiMatrix)).scale(-0.5) - .minus(KlkTMatrixTImesKsi.transpose().mult(XV)); - dFCPAdTdV = tempMatrixTV.get(0, 0); + DenseMatrix tempMatrixTV = + subtract(scale(multiply(ksiMatrixTranspose, multiply(KlkTVMatrix, ksiMatrix)), -0.5), + multiply(transpose(klkTMatrixTimesKsi), XV)); + dFCPAdTdV = tempMatrixTV.unsafe_get(0, 0); temp = 0; for (int p = 0; p < numberOfComponents; p++) { for (int kk = 0; kk < getComponent(p).getNumberOfAssociationSites(); kk++) { - ((ComponentCPAInterface) getComponent(p)).setXsitedT(kk, XT.get(temp + kk, 0)); + ((ComponentCPAInterface) getComponent(p)).setXsitedT(kk, XT.unsafe_get(temp + kk, 0)); } temp += getComponent(p).getNumberOfAssociationSites(); } @@ -424,7 +443,7 @@ public void initCPAMatrix(int type) { // if(true) return; for (int p = 0; p < numberOfComponents; p++) { - SimpleMatrix KiMatrix = new SimpleMatrix(Klkni[p]); + DenseMatrix kiMatrix = new DenseMatrix(Klkni[p]); // KiMatrix.print(10,10); // Matrix dQdniMatrix = // (ksiMatrix.transpose().times(KiMatrix.times(ksiMatrix)).times(-0.5)); // this @@ -439,10 +458,11 @@ public void initCPAMatrix(int type) { // ksiMatrix.transpose().times(KlkTMatrix.times(ksiMatrix)).times(-0.5); // System.out.println("dQdn "); // tempMatrix20.print(10, 10); - SimpleMatrix tempMatrix4 = KiMatrix.mult(ksiMatrix); + DenseMatrix tempMatrix4 = multiply(kiMatrix, ksiMatrix); // udotTimesmiMatrix.getMatrix(assSites, assSites, 0, // totalNumberOfAccociationSites - 1).print(10, 10); - SimpleMatrix tempMatrix5 = udotTimesmiMatrix.extractVector(true, p).transpose().minus(tempMatrix4); + DenseMatrix tempMatrix5 = + subtract(transpose(extractVector(udotTimesmiMatrix, true, p)), tempMatrix4); // tempMki[0] = mki[p]; // Matrix amatrix = new Matrix(croeneckerProduct(tempMki, // udotMatrix.getArray())); @@ -451,7 +471,7 @@ public void initCPAMatrix(int type) { // System.out.println("temp4 matrix"); // tempMatrix4.print(10, 10); // Matrix tempMatrix5 = amatrix.minus(tempMatrix4); - SimpleMatrix tempMatrix6 = applyHessianInv(tempMatrix5); // .scale(-1.0); + DenseMatrix tempMatrix6 = applyHessianInv(tempMatrix5); // .scale(-1.0); // System.out.println("dXdni"); // tempMatrix4.print(10, 10); // tempMatrix5.print(10, 10); @@ -459,10 +479,11 @@ public void initCPAMatrix(int type) { // tempMatrix6.print(10, 10); int temp2 = 0; for (int compp = 0; compp < numberOfComponents; compp++) { - for (int kk = 0; kk < getComponent(compp).getNumberOfAssociationSites(); kk++) { - ((ComponentCPAInterface) getComponent(compp)).setXsitedni(kk, p, -1.0 * tempMatrix6.get(temp2 + kk, 0)); - } - temp2 += getComponent(compp).getNumberOfAssociationSites(); + for (int kk = 0; kk < getComponent(compp).getNumberOfAssociationSites(); kk++) { + ((ComponentCPAInterface) getComponent(compp)).setXsitedni(kk, p, + -1.0 * tempMatrix6.unsafe_get(temp2 + kk, 0)); + } + temp2 += getComponent(compp).getNumberOfAssociationSites(); } // assSites += getComponent(p).getNumberOfAssociationSites(); } @@ -527,10 +548,10 @@ public double dFdTdT() { */ public double FCPA() { /* - * double tot = 0.0; double ans = 0.0; for (int i = 0; i < numberOfComponents; i++) { tot = 0.0; for (int j = 0; j < - * getComponent(i).getNumberOfAssociationSites(); j++) { double xai = ((ComponentSrkCPA) - * getComponent(i)).getXsite()[j]; tot += (Math.log(xai) - 1.0 / 2.0 * xai + 1.0 / 2.0); } ans += - * getComponent(i).getNumberOfMolesInPhase() * tot; } return ans; + * double tot = 0.0; double ans = 0.0; for (int i = 0; i < numberOfComponents; i++) { tot = 0.0; + * for (int j = 0; j < getComponent(i).getNumberOfAssociationSites(); j++) { double xai = + * ((ComponentSrkCPA) getComponent(i)).getXsite()[j]; tot += (Math.log(xai) - 1.0 / 2.0 * xai + + * 1.0 / 2.0); } ans += getComponent(i).getNumberOfMolesInPhase() * tot; } return ans; */ return FCPA; } @@ -586,11 +607,12 @@ public double dFCPAdVdVdV() { */ public double dFCPAdT() { /* - * double tot = 0.0; double ans = 0.0; for (int i = 0; i < numberOfComponents; i++) { tot = 0.0; for (int j = 0; j < - * getComponent(i).getNumberOfAssociationSites(); j++) { double xai = ((ComponentSrkCPA) - * getComponent(i)).getXsite()[j]; double xaidT = ((ComponentSrkCPA) getComponent(i)).getXsitedT()[j]; tot += 1.0 / - * xai * xaidT - 0.5 * xaidT; // - 1.0 / 2.0 * xai + 1.0 / 2.0); } ans += getComponent(i).getNumberOfMolesInPhase() - * * tot; } System.out.println("dFCPAdT1 " + ans + " dfcpa2 " +dFCPAdT); return ans; + * double tot = 0.0; double ans = 0.0; for (int i = 0; i < numberOfComponents; i++) { tot = 0.0; + * for (int j = 0; j < getComponent(i).getNumberOfAssociationSites(); j++) { double xai = + * ((ComponentSrkCPA) getComponent(i)).getXsite()[j]; double xaidT = ((ComponentSrkCPA) + * getComponent(i)).getXsitedT()[j]; tot += 1.0 / xai * xaidT - 0.5 * xaidT; // - 1.0 / 2.0 * + * xai + 1.0 / 2.0); } ans += getComponent(i).getNumberOfMolesInPhase() * tot; } + * System.out.println("dFCPAdT1 " + ans + " dfcpa2 " +dFCPAdT); return ans; */ return dFCPAdT; } @@ -618,8 +640,8 @@ public double dFCPAdTdV() { // getTotalVolume()) * (1.0 - getTotalVolume() * getGcpav()) * hcpatotdT)); return dFCPAdTdV; /* - * if (totalNumberOfAccociationSites > 0) { return 1.0 / (2.0 * getTotalVolume()) * (1.0 - getTotalVolume() * - * getGcpav()) * hcpatotdT; } else { return 0; } + * if (totalNumberOfAccociationSites > 0) { return 1.0 / (2.0 * getTotalVolume()) * (1.0 - + * getTotalVolume() * getGcpav()) * hcpatotdT; } else { return 0; } */ } @@ -635,21 +657,21 @@ public void calcXsitedV() { } /** - * Applies the current Hessian inverse to a right-hand side without forming the inverse when a cached LU factorization - * is available. + * Applies the current Hessian inverse to a right-hand side without forming the inverse when a + * cached LU factorization is available. * * @param rhs right-hand-side matrix with one row per association site * @return solution of {@code hessianMatrix * x = rhs} */ - private SimpleMatrix applyHessianInv(SimpleMatrix rhs) { + private DenseMatrix applyHessianInv(DenseMatrix rhs) { if (hessianInvers != null) { - return hessianInvers.mult(rhs); + return multiply(hessianInvers, rhs); } if (hessianLU != null && hessianLUSize == totalNumberOfAccociationSites) { - DMatrixRMaj rhsMat = rhs.getDDRM(); - DMatrixRMaj out = new DMatrixRMaj(rhsMat.numRows, rhsMat.numCols); - hessianLU.solve(rhsMat, out); - return SimpleMatrix.wrap(out); + DenseMatrix out = new DenseMatrix(rhs.numRows, rhs.numCols); + LinearAlgebraOps.solveLu(hessianLU, rhs.numRows, rhs.numCols, (i, j) -> rhs.unsafe_get(i, j), + (i, j, value) -> out.unsafe_set(i, j, value)); + return out; } throw new IllegalStateException("Hessian factorization has not been initialized"); } @@ -668,19 +690,19 @@ public boolean solveX() { boolean solvedX = solveX2(15); - DMatrixRMaj mVectorMat = mVector.getMatrix(); - DMatrixRMaj ksiMatrixMat = ksiMatrix.getMatrix(); + DenseMatrix mVectorMat = mVector; + DenseMatrix ksiMatrixMat = ksiMatrix; int temp = 0; int iter = 0; for (int i = 0; i < numberOfComponents; i++) { for (int j = 0; j < componentArray[i].getNumberOfAssociationSites(); j++) { - mVectorMat.unsafe_set(temp + j, 0, componentArray[i].getNumberOfMolesInPhase()); + mVectorMat.unsafe_set(temp + j, 0, componentArray[i].getNumberOfMolesInPhase()); } temp += componentArray[i].getNumberOfAssociationSites(); } - DMatrixRMaj mat1 = KlkMatrix.getMatrix(); + DenseMatrix mat1 = KlkMatrix; double Klk = 0.0; double totvolume = getTotalVolume(); double tempVari; @@ -688,10 +710,10 @@ public boolean solveX() { for (int i = 0; i < getTotalNumberOfAccociationSites(); i++) { tempVari = mVectorMat.unsafe_get(i, 0); for (int j = i; j < getTotalNumberOfAccociationSites(); j++) { - tempVarj = mVectorMat.unsafe_get(j, 0); - Klk = tempVari * tempVarj / totvolume * delta[i][j]; - mat1.unsafe_set(i, j, Klk); - mat1.unsafe_set(j, i, Klk); + tempVarj = mVectorMat.unsafe_get(j, 0); + Klk = tempVari * tempVarj / totvolume * delta[i][j]; + mat1.unsafe_set(i, j, Klk); + mat1.unsafe_set(j, i, Klk); } } boolean solved = true; @@ -703,63 +725,65 @@ public boolean solveX() { double temp1; double temp2; for (int i = 0; i < numberOfComponents; i++) { - temp1 = componentArray[i].getNumberOfMolesInPhase(); - for (int j = 0; j < componentArray[i].getNumberOfAssociationSites(); j++) { - ksi = ((ComponentCPAInterface) componentArray[i]).getXsite()[j]; - ksiMatrixMat.unsafe_set(temp + j, 0, ksi); - tempVari = 1.0 / ksi - 1.0; - udotMatrix.set(temp + j, 0, tempVari); - udotTimesmMatrix.set(temp + j, 0, temp1 * tempVari); - } - temp += componentArray[i].getNumberOfAssociationSites(); + temp1 = componentArray[i].getNumberOfMolesInPhase(); + for (int j = 0; j < componentArray[i].getNumberOfAssociationSites(); j++) { + ksi = ((ComponentCPAInterface) componentArray[i]).getXsite()[j]; + ksiMatrixMat.unsafe_set(temp + j, 0, ksi); + tempVari = 1.0 / ksi - 1.0; + udotMatrix.unsafe_set(temp + j, 0, tempVari); + udotTimesmMatrix.unsafe_set(temp + j, 0, temp1 * tempVari); + } + temp += componentArray[i].getNumberOfAssociationSites(); } int krondelt; for (int i = 0; i < getTotalNumberOfAccociationSites(); i++) { - temp1 = mVectorMat.unsafe_get(i, 0); - temp2 = ksiMatrix.get(i, 0); - for (int j = i; j < getTotalNumberOfAccociationSites(); j++) { - krondelt = (i == j) ? 1 : 0; - tempVari = -temp1 / (temp2 * temp2) * krondelt - mat1.unsafe_get(i, j); - hessianMatrix.set(i, j, tempVari); - hessianMatrix.set(j, i, tempVari); - } + temp1 = mVectorMat.unsafe_get(i, 0); + temp2 = ksiMatrix.unsafe_get(i, 0); + for (int j = i; j < getTotalNumberOfAccociationSites(); j++) { + krondelt = (i == j) ? 1 : 0; + tempVari = -temp1 / (temp2 * temp2) * krondelt - mat1.unsafe_get(i, j); + hessianMatrix.unsafe_set(i, j, tempVari); + hessianMatrix.unsafe_set(j, i, tempVari); + } } int n = totalNumberOfAccociationSites; if (hessianLU == null || hessianLUSize != n) { - hessianLU = LinearSolverFactory_DDRM.lu(n); - hessianLUinput = new DMatrixRMaj(n, n); - hessianLUSize = n; + hessianLU = LU.PRIMITIVE.make(n, n); + hessianLUinput = new DenseMatrix(n, n); + hessianLUSize = n; } - System.arraycopy(hessianMatrix.getDDRM().getData(), 0, hessianLUinput.getData(), 0, n * n); - if (!hessianLU.setA(hessianLUinput)) { - return false; + System.arraycopy(hessianMatrix.getData(), 0, hessianLUinput.getData(), 0, n * n); + if (!LinearAlgebraOps.decomposeLu(hessianLU, n, (i, j) -> hessianLUinput.unsafe_get(i, j))) { + return false; } hessianInvers = null; if (solvedX) { - return true; + return true; } - DMatrixRMaj mat2 = ksiMatrix.getMatrix(); - CommonOps_DDRM.mult(mat1, mat2, corr2Matrix); - CommonOps_DDRM.subtract(udotTimesmMatrix.getDDRM(), corr2Matrix, corr3Matrix); - hessianLU.solve(corr3Matrix, corr4Matrix); + DenseMatrix mat2 = ksiMatrix; + LinearAlgebraOps.mult(mat1, mat2, corr2Matrix); + LinearAlgebraOps.subtract(udotTimesmMatrix, corr2Matrix, corr3Matrix); + LinearAlgebraOps.solveLu(hessianLU, corr3Matrix.numRows, corr3Matrix.numCols, + (i, j) -> corr3Matrix.unsafe_get(i, j), + (i, j, value) -> corr4Matrix.unsafe_set(i, j, value)); temp = 0; double newX; for (int i = 0; i < numberOfComponents; i++) { - for (int j = 0; j < componentArray[i].getNumberOfAssociationSites(); j++) { - newX = ksiMatrix.get(temp + j, 0) - corr4Matrix.unsafe_get((temp + j), 0); - if (newX < 0) { - newX = 1e-10; - solved = false; - } - ((ComponentCPAInterface) componentArray[i]).setXsite(j, newX); - } - temp += componentArray[i].getNumberOfAssociationSites(); + for (int j = 0; j < componentArray[i].getNumberOfAssociationSites(); j++) { + newX = ksiMatrix.unsafe_get(temp + j, 0) - corr4Matrix.unsafe_get((temp + j), 0); + if (newX < 0) { + newX = 1e-10; + solved = false; + } + ((ComponentCPAInterface) componentArray[i]).setXsite(j, newX); + } + temp += componentArray[i].getNumberOfAssociationSites(); } - } while ((NormOps_DDRM.normF(corr4Matrix) > 1e-12 || !solved) && iter < 100); + } while ((LinearAlgebraOps.normF(corr4Matrix) > 1e-12 || !solved) && iter < 100); return true; } @@ -782,15 +806,18 @@ public boolean solveX2(int maxIter) { iter++; err = 0.0; for (int i = 0; i < getTotalNumberOfAccociationSites(); i++) { - old = ((ComponentCPAInterface) componentArray[moleculeNumber[i]]).getXsite()[assSiteNumber[i]]; - neeval = 0.0; - for (int j = 0; j < getTotalNumberOfAccociationSites(); j++) { - neeval += componentArray[moleculeNumber[j]].getNumberOfMolesInPhase() * delta[i][j] - * ((ComponentCPAInterface) componentArray[moleculeNumber[j]]).getXsite()[assSiteNumber[j]]; - } - neeval = 1.0 / (1.0 + 1.0 / totalVolume * neeval); - ((ComponentCPAInterface) componentArray[moleculeNumber[i]]).setXsite(assSiteNumber[i], neeval); - err += Math.abs((old - neeval) / neeval); + old = ((ComponentCPAInterface) componentArray[moleculeNumber[i]]) + .getXsite()[assSiteNumber[i]]; + neeval = 0.0; + for (int j = 0; j < getTotalNumberOfAccociationSites(); j++) { + neeval += componentArray[moleculeNumber[j]].getNumberOfMolesInPhase() * delta[i][j] + * ((ComponentCPAInterface) componentArray[moleculeNumber[j]]) + .getXsite()[assSiteNumber[j]]; + } + neeval = 1.0 / (1.0 + 1.0 / totalVolume * neeval); + ((ComponentCPAInterface) componentArray[moleculeNumber[i]]).setXsite(assSiteNumber[i], + neeval); + err += Math.abs((old - neeval) / neeval); } } while (Math.abs(err) > 1e-12 && iter < maxIter); return Math.abs(err) < 1e-12; @@ -856,28 +883,29 @@ public double calcRootVolFinder(PhaseType pt) { volInit(); int solveXAttempts = 0; while (!solveX() && solveXAttempts < 50) { - solveXAttempts++; + solveXAttempts++; } if (solveXAttempts >= 50) { - // solveX failed to converge, skip this BonV value - oldh = h; - continue; + // solveX failed to converge, skip this BonV value + oldh = h; + continue; } - h = BonV - Btemp / numberOfMolesInPhase * dFdV() - pressure * Btemp / (numberOfMolesInPhase * R * temperature); + h = BonV - Btemp / numberOfMolesInPhase * dFdV() + - pressure * Btemp / (numberOfMolesInPhase * R * temperature); if (Math.signum(h) * Math.signum(oldh) < 0 && i > 2) { - if (solvedBonVlow < 1e-3) { - solvedBonVlow = (BonV + BonVold) / 2.0; - if (pt == PhaseType.GAS) { - break; - } - } else { - solvedBonVHigh = (BonV + BonVold) / 2.0; - if (pt == PhaseType.LIQUID || pt == PhaseType.AQUEOUS) { - break; - } - } + if (solvedBonVlow < 1e-3) { + solvedBonVlow = (BonV + BonVold) / 2.0; + if (pt == PhaseType.GAS) { + break; + } + } else { + solvedBonVHigh = (BonV + BonVold) / 2.0; + if (pt == PhaseType.LIQUID || pt == PhaseType.AQUEOUS) { + break; + } + } } solvedBonVHigh = (BonV + BonVold) / 2.0; oldh = h; @@ -902,7 +930,8 @@ public double calcRootVolFinder(PhaseType pt) { /** {@inheritDoc} */ @Override public double molarVolume2(double pressure, double temperature, double A, double B, PhaseType pt) - throws neqsim.util.exception.IsNaNException, neqsim.util.exception.TooManyIterationsException { + throws neqsim.util.exception.IsNaNException, + neqsim.util.exception.TooManyIterationsException { double BonV; // Use liquid-like initial guess for LIQUID or AQUEOUS phases if (pt == PhaseType.LIQUID || pt == PhaseType.AQUEOUS) { @@ -938,8 +967,8 @@ public double molarVolume2(double pressure, double temperature, double A, double iterations++; gcpa = calc_g(); if (gcpa < 0) { - setMolarVolume(1.0 / Btemp / numberOfMolesInPhase); - gcpa = calc_g(); + setMolarVolume(1.0 / Btemp / numberOfMolesInPhase); + gcpa = calc_g(); } // double lngcpa = Math.log(gcpa); @@ -955,79 +984,81 @@ public double molarVolume2(double pressure, double temperature, double A, double volInit(); double BonV2 = BonV * BonV; BonVold = BonV; - h = BonV - Btemp / numberOfMolesInPhase * dFdV() - pressure * Btemp / (numberOfMolesInPhase * R * temperature); + h = BonV - Btemp / numberOfMolesInPhase * dFdV() + - pressure * Btemp / (numberOfMolesInPhase * R * temperature); dh = 1.0 + Btemp / (BonV2) * (Btemp / numberOfMolesInPhase * dFdVdV()); dhh = -2.0 * Btemp / (BonV2 * BonV) * (Btemp / numberOfMolesInPhase * dFdVdV()) - - (Btemp * Btemp) / (BonV2 * BonV2) * (Btemp / numberOfMolesInPhase * dFdVdVdV()); + - (Btemp * Btemp) / (BonV2 * BonV2) * (Btemp / numberOfMolesInPhase * dFdVdVdV()); d1 = -h / dh; d2 = -dh / dhh; // System.out.println("d1" + d1 + " d2 " + d2 + " d1 / d2 " + (d1 / d2)); if (Double.isNaN(d1) || Double.isNaN(d2)) { - if (pt == PhaseType.GAS) { - BonV = 2.0 / (2.0 + temperature / getPseudoCriticalTemperature()); - } else { - BonV = pressure * getB() / (numberOfMolesInPhase * temperature * R); - } + if (pt == PhaseType.GAS) { + BonV = 2.0 / (2.0 + temperature / getPseudoCriticalTemperature()); + } else { + BonV = pressure * getB() / (numberOfMolesInPhase * temperature * R); + } } else if (Math.abs(d1 / d2) <= 1.0) { - BonV += d1 * (1.0 + 0.5 * d1 / d2); + BonV += d1 * (1.0 + 0.5 * d1 / d2); } else if (d1 / d2 < -1) { - BonV += 0.5 * d1; + BonV += 0.5 * d1; } else if (d1 > d2) { - BonV += d2; - double hnew = h + d2 * dh; - if (Math.abs(hnew) > Math.abs(h)) { - // Reset to appropriate initial guess based on phase type - if (pt == PhaseType.GAS) { - BonV = 2.0 / (2.0 + temperature / getPseudoCriticalTemperature()); - } else if (pt == PhaseType.AQUEOUS || pt == PhaseType.LIQUID) { - BonV = 0.99; - } else { - BonV = pressure * getB() / (numberOfMolesInPhase * temperature * R); - } - } + BonV += d2; + double hnew = h + d2 * dh; + if (Math.abs(hnew) > Math.abs(h)) { + // Reset to appropriate initial guess based on phase type + if (pt == PhaseType.GAS) { + BonV = 2.0 / (2.0 + temperature / getPseudoCriticalTemperature()); + } else if (pt == PhaseType.AQUEOUS || pt == PhaseType.LIQUID) { + BonV = 0.99; + } else { + BonV = pressure * getB() / (numberOfMolesInPhase * temperature * R); + } + } } else { - BonV += 0.5 * d1; + BonV += 0.5 * d1; } if (Math.abs((BonV - BonVold) / BonVold) > 0.1) { - BonV = BonVold + 0.1 * (BonV - BonVold); + BonV = BonVold + 0.1 * (BonV - BonVold); } if (BonV > 1.1) { - if (iterations < 3) { - BonV = (BonVold + BonV) / 2.0; - } else { - // Reset to appropriate initial guess based on phase type - if (pt == PhaseType.GAS) { - BonV = 2.0 / (2.0 + temperature / getPseudoCriticalTemperature()); - } else if (pt == PhaseType.AQUEOUS || pt == PhaseType.LIQUID) { - BonV = 0.99; - } else { - BonV = pressure * getB() / (numberOfMolesInPhase * temperature * R); - } - } + if (iterations < 3) { + BonV = (BonVold + BonV) / 2.0; + } else { + // Reset to appropriate initial guess based on phase type + if (pt == PhaseType.GAS) { + BonV = 2.0 / (2.0 + temperature / getPseudoCriticalTemperature()); + } else if (pt == PhaseType.AQUEOUS || pt == PhaseType.LIQUID) { + BonV = 0.99; + } else { + BonV = pressure * getB() / (numberOfMolesInPhase * temperature * R); + } + } } if (BonV < 0) { - if (iterations < 3) { - BonV = Math.abs(BonVold + BonV) / 2.0; - } else { - // Reset to appropriate initial guess based on phase type - if (pt == PhaseType.GAS) { - BonV = 2.0 / (2.0 + temperature / getPseudoCriticalTemperature()); - } else if (pt == PhaseType.AQUEOUS || pt == PhaseType.LIQUID) { - BonV = 0.99; - } else { - BonV = pressure * getB() / (numberOfMolesInPhase * temperature * R); - } - } + if (iterations < 3) { + BonV = Math.abs(BonVold + BonV) / 2.0; + } else { + // Reset to appropriate initial guess based on phase type + if (pt == PhaseType.GAS) { + BonV = 2.0 / (2.0 + temperature / getPseudoCriticalTemperature()); + } else if (pt == PhaseType.AQUEOUS || pt == PhaseType.LIQUID) { + BonV = 0.99; + } else { + BonV = pressure * getB() / (numberOfMolesInPhase * temperature * R); + } + } } setMolarVolume(1.0 / BonV * Btemp / numberOfMolesInPhase); Z = pressure * getMolarVolume() / (R * temperature); // System.out.println("Z" + Z); - } while ((Math.abs((BonV - BonVold) / BonV) > 1.0e-10 || Double.isNaN(BonV)) && iterations < 100); + } while ((Math.abs((BonV - BonVold) / BonV) > 1.0e-10 || Double.isNaN(BonV)) + && iterations < 100); // System.out.println("Z" + Z + " iterations " + iterations + " BonV " + BonV); // System.out.println("pressure " + Z*R*temperature/getMolarVolume()); // System.out.println("volume " + getTotalVolume() + " molar volume " + @@ -1054,7 +1085,8 @@ public double molarVolume2(double pressure, double temperature, double A, double /** {@inheritDoc} */ @Override public double molarVolume(double pressure, double temperature, double A, double B, PhaseType pt) - throws neqsim.util.exception.IsNaNException, neqsim.util.exception.TooManyIterationsException { + throws neqsim.util.exception.IsNaNException, + neqsim.util.exception.TooManyIterationsException { // For AQUEOUS and LIQUID phases, use a liquid-like initial guess (high BonV) // For GAS phase, use a gas-like initial guess (low BonV) double BonV; @@ -1087,8 +1119,8 @@ public double molarVolume(double pressure, double temperature, double A, double volInit(); gcpa = calc_g(); if (gcpa < 0) { - setMolarVolume(1.0 / Btemp / numberOfMolesInPhase); - gcpa = calc_g(); + setMolarVolume(1.0 / Btemp / numberOfMolesInPhase); + gcpa = calc_g(); } // lngcpa = @@ -1098,74 +1130,76 @@ public double molarVolume(double pressure, double temperature, double A, double gcpavvv = calc_lngVVV(); if (getTotalNumberOfAccociationSites() > 0) { - solveX(); + solveX(); } initCPAMatrix(1); double BonV2 = BonV * BonV; BonVold = BonV; - h = BonV - Btemp / numberOfMolesInPhase * dFdV() - pressure * Btemp / (numberOfMolesInPhase * R * temperature); + h = BonV - Btemp / numberOfMolesInPhase * dFdV() + - pressure * Btemp / (numberOfMolesInPhase * R * temperature); dh = 1.0 + Btemp / (BonV2) * (Btemp / numberOfMolesInPhase * dFdVdV()); dhh = -2.0 * Btemp / (BonV2 * BonV) * (Btemp / numberOfMolesInPhase * dFdVdV()) - - (Btemp * Btemp) / (BonV2 * BonV2) * (Btemp / numberOfMolesInPhase * dFdVdVdV()); + - (Btemp * Btemp) / (BonV2 * BonV2) * (Btemp / numberOfMolesInPhase * dFdVdVdV()); d1 = -h / dh; d2 = -dh / dhh; // System.out.println("h " + h + " iter " + iterations + " " + d1 + " d2 " + d2 // + " d1 / d2 " + (d1 / d2)); if (Double.isNaN(d1) || Double.isNaN(d2)) { - return molarVolumeChangePhase(pressure, temperature, A, B, pt); + return molarVolumeChangePhase(pressure, temperature, A, B, pt); } else if (Math.abs(d1 / d2) <= 1.0) { - BonV += d1 * (1.0 + 0.5 * d1 / d2); + BonV += d1 * (1.0 + 0.5 * d1 / d2); } else if (d1 / d2 < -1) { - BonV += 0.5 * d1; + BonV += 0.5 * d1; } else if (d1 > d2) { - BonV += d2; - double hnew = h + d2 * dh; - if (Math.abs(hnew) > Math.abs(h)) { - // Reset to appropriate initial guess based on phase type - if (pt == PhaseType.GAS) { - BonV = 2.0 / (2.0 + temperature / getPseudoCriticalTemperature()); - } else if (pt == PhaseType.AQUEOUS || pt == PhaseType.LIQUID) { - // For aqueous/liquid phases, reset to liquid-like guess - BonV = 0.99; - } else { - BonV = pressure * getB() / (numberOfMolesInPhase * temperature * R); - } - } + BonV += d2; + double hnew = h + d2 * dh; + if (Math.abs(hnew) > Math.abs(h)) { + // Reset to appropriate initial guess based on phase type + if (pt == PhaseType.GAS) { + BonV = 2.0 / (2.0 + temperature / getPseudoCriticalTemperature()); + } else if (pt == PhaseType.AQUEOUS || pt == PhaseType.LIQUID) { + // For aqueous/liquid phases, reset to liquid-like guess + BonV = 0.99; + } else { + BonV = pressure * getB() / (numberOfMolesInPhase * temperature * R); + } + } } else { - BonV += 0.5 * d1; + BonV += 0.5 * d1; } if (Math.abs((BonV - BonVold) / BonVold) > 0.1) { - BonV = BonVold + 0.1 * (BonV - BonVold); + BonV = BonVold + 0.1 * (BonV - BonVold); } if (Double.isNaN(BonV)) { - return molarVolumeChangePhase(pressure, temperature, A, B, pt); + return molarVolumeChangePhase(pressure, temperature, A, B, pt); } if (BonV > 0.9999) { - if (iterations < 3) { - BonV = (BonVold + BonV) / 2.0; - } else { - return molarVolumeChangePhase(pressure, temperature, A, B, pt); - // BonV = 0.9999; - // BonV = pt == 1 ? 2.0 / (2.0 + temperature / - // getPseudoCriticalTemperature()) : pressure * getB() / (numberOfMolesInPhase * - // temperature * R); - } + if (iterations < 3) { + BonV = (BonVold + BonV) / 2.0; + } else { + return molarVolumeChangePhase(pressure, temperature, A, B, pt); + // BonV = 0.9999; + // BonV = pt == 1 ? 2.0 / (2.0 + temperature / + // getPseudoCriticalTemperature()) : pressure * getB() / (numberOfMolesInPhase * + // temperature * R); + } } else if (BonV < 0) { - if (iterations < 3) { - BonV = Math.abs(BonVold + BonV) / 2.0; - } else { - return molarVolumeChangePhase(pressure, temperature, A, B, pt); - // BonV = pt == 1 ? 2.0 / (2.0 + temperature / - // getPseudoCriticalTemperature()) : pressure * getB() / (numberOfMolesInPhase * - // temperature * R); - } + if (iterations < 3) { + BonV = Math.abs(BonVold + BonV) / 2.0; + } else { + return molarVolumeChangePhase(pressure, temperature, A, B, pt); + // BonV = pt == 1 ? 2.0 / (2.0 + temperature / + // getPseudoCriticalTemperature()) : pressure * getB() / (numberOfMolesInPhase * + // temperature * R); + } } setMolarVolume(1.0 / BonV * Btemp / numberOfMolesInPhase); Z = pressure * getMolarVolume() / (R * temperature); - } while ((Math.abs((BonV - BonVold) / BonV) > 1.0e-10 || Math.abs(h) > 1e-9) && iterations < 100); + } while ((Math.abs((BonV - BonVold) / BonV) > 1.0e-10 || Math.abs(h) > 1e-9) + && iterations < 100); if (Math.abs(h) > 1e-9 || Double.isNaN(h)) { // System.out.println("h failed " + "Z" + Z + " iterations " + iterations + " @@ -1208,8 +1242,9 @@ public double molarVolume(double pressure, double temperature, double A, double * @throws neqsim.util.exception.IsNaNException if any. * @throws neqsim.util.exception.TooManyIterationsException if any. */ - public double molarVolumeChangePhase(double pressure, double temperature, double A, double B, PhaseType pt) - throws neqsim.util.exception.IsNaNException, neqsim.util.exception.TooManyIterationsException { + public double molarVolumeChangePhase(double pressure, double temperature, double A, double B, + PhaseType pt) throws neqsim.util.exception.IsNaNException, + neqsim.util.exception.TooManyIterationsException { // double BonV = pt == 1 ? 2.0 / (2.0 + temperature / // getPseudoCriticalTemperature()) : pressure * getB() / (numberOfMolesInPhase * // temperature * R); @@ -1243,8 +1278,8 @@ public double molarVolumeChangePhase(double pressure, double temperature, double iterations++; gcpa = calc_g(); if (gcpa < 0) { - setMolarVolume(1.0 / Btemp / numberOfMolesInPhase); - gcpa = calc_g(); + setMolarVolume(1.0 / Btemp / numberOfMolesInPhase); + gcpa = calc_g(); } // lngcpa = @@ -1258,79 +1293,80 @@ public double molarVolumeChangePhase(double pressure, double temperature, double initCPAMatrix(1); double BonV2 = BonV * BonV; BonVold = BonV; - h = BonV - Btemp / numberOfMolesInPhase * dFdV() - pressure * Btemp / (numberOfMolesInPhase * R * temperature); + h = BonV - Btemp / numberOfMolesInPhase * dFdV() + - pressure * Btemp / (numberOfMolesInPhase * R * temperature); dh = 1.0 + Btemp / (BonV2) * (Btemp / numberOfMolesInPhase * dFdVdV()); dhh = -2.0 * Btemp / (BonV2 * BonV) * (Btemp / numberOfMolesInPhase * dFdVdV()) - - (Btemp * Btemp) / (BonV2 * BonV2) * (Btemp / numberOfMolesInPhase * dFdVdVdV()); + - (Btemp * Btemp) / (BonV2 * BonV2) * (Btemp / numberOfMolesInPhase * dFdVdVdV()); d1 = -h / dh; d2 = -dh / dhh; // System.out.println("d1" + d1 + " d2 " + d2 + " d1 / d2 " + (d1 / d2)); if (Double.isNaN(d1) || Double.isNaN(d2)) { - if (pt == PhaseType.GAS) { - BonV = 2.0 / (2.0 + temperature / getPseudoCriticalTemperature()); - } else { - BonV = pressure * getB() / (numberOfMolesInPhase * temperature * R); - } + if (pt == PhaseType.GAS) { + BonV = 2.0 / (2.0 + temperature / getPseudoCriticalTemperature()); + } else { + BonV = pressure * getB() / (numberOfMolesInPhase * temperature * R); + } } else if (Math.abs(d1 / d2) <= 1.0) { - BonV += d1 * (1.0 + 0.5 * d1 / d2); + BonV += d1 * (1.0 + 0.5 * d1 / d2); } else if (d1 / d2 < -1) { - BonV += 0.5 * d1; + BonV += 0.5 * d1; } else if (d1 > d2) { - BonV += d2; - double hnew = h + d2 * dh; - if (Math.abs(hnew) > Math.abs(h)) { - // Reset to appropriate initial guess based on phase type - if (pt == PhaseType.GAS) { - BonV = 2.0 / (2.0 + temperature / getPseudoCriticalTemperature()); - } else if (pt == PhaseType.AQUEOUS || pt == PhaseType.LIQUID) { - BonV = 0.99; - } else { - BonV = pressure * getB() / (numberOfMolesInPhase * temperature * R); - } - } + BonV += d2; + double hnew = h + d2 * dh; + if (Math.abs(hnew) > Math.abs(h)) { + // Reset to appropriate initial guess based on phase type + if (pt == PhaseType.GAS) { + BonV = 2.0 / (2.0 + temperature / getPseudoCriticalTemperature()); + } else if (pt == PhaseType.AQUEOUS || pt == PhaseType.LIQUID) { + BonV = 0.99; + } else { + BonV = pressure * getB() / (numberOfMolesInPhase * temperature * R); + } + } } else { - BonV += 0.5 * d1; + BonV += 0.5 * d1; } if (Math.abs((BonV - BonVold) / BonVold) > 0.1) { - BonV = BonVold + 0.1 * (BonV - BonVold); + BonV = BonVold + 0.1 * (BonV - BonVold); } if (Double.isNaN(BonV)) { - if (pt == PhaseType.GAS) { - BonV = 2.0 / (2.0 + temperature / getPseudoCriticalTemperature()); - } else { - BonV = pressure * getB() / (numberOfMolesInPhase * temperature * R); - } + if (pt == PhaseType.GAS) { + BonV = 2.0 / (2.0 + temperature / getPseudoCriticalTemperature()); + } else { + BonV = pressure * getB() / (numberOfMolesInPhase * temperature * R); + } } if (BonV > 1.1) { - if (iterations < 3) { - BonV = (BonVold + BonV) / 2.0; - } else { - // Reset to appropriate initial guess based on phase type - if (pt == PhaseType.GAS) { - BonV = 2.0 / (2.0 + temperature / getPseudoCriticalTemperature()); - } else if (pt == PhaseType.AQUEOUS || pt == PhaseType.LIQUID) { - BonV = 0.99; - } else { - BonV = pressure * getB() / (numberOfMolesInPhase * temperature * R); - } - } + if (iterations < 3) { + BonV = (BonVold + BonV) / 2.0; + } else { + // Reset to appropriate initial guess based on phase type + if (pt == PhaseType.GAS) { + BonV = 2.0 / (2.0 + temperature / getPseudoCriticalTemperature()); + } else if (pt == PhaseType.AQUEOUS || pt == PhaseType.LIQUID) { + BonV = 0.99; + } else { + BonV = pressure * getB() / (numberOfMolesInPhase * temperature * R); + } + } } if (BonV < 0) { - if (iterations < 3) { - BonV = Math.abs(BonVold + BonV) / 2.0; - } else { - // Reset to appropriate initial guess based on phase type - if (pt == PhaseType.GAS) { - BonV = 2.0 / (2.0 + temperature / getPseudoCriticalTemperature()); - } else if (pt == PhaseType.AQUEOUS || pt == PhaseType.LIQUID) { - BonV = 0.99; - } else { - BonV = pressure * getB() / (numberOfMolesInPhase * temperature * R); - } - } + if (iterations < 3) { + BonV = Math.abs(BonVold + BonV) / 2.0; + } else { + // Reset to appropriate initial guess based on phase type + if (pt == PhaseType.GAS) { + BonV = 2.0 / (2.0 + temperature / getPseudoCriticalTemperature()); + } else if (pt == PhaseType.AQUEOUS || pt == PhaseType.LIQUID) { + BonV = 0.99; + } else { + BonV = pressure * getB() / (numberOfMolesInPhase * temperature * R); + } + } } setMolarVolume(1.0 / BonV * Btemp / numberOfMolesInPhase); @@ -1338,11 +1374,12 @@ public double molarVolumeChangePhase(double pressure, double temperature, double // System.out.println("Z " + Z + "h " + h + " BONV " + (Math.abs((BonV - // BonVold) / BonV))); - } while ((Math.abs((BonV - BonVold) / BonV) > 1.0e-10 || Double.isNaN(BonV)) && iterations < 100); + } while ((Math.abs((BonV - BonVold) / BonV) > 1.0e-10 || Double.isNaN(BonV)) + && iterations < 100); /* - * if (Math.abs(h) > 1e-8) { if (pt == 0) { molarVolume(pressure, temperature, A, B, 1); } else { - * molarVolume(pressure, temperature, A, B, 0); } return getMolarVolume(); } + * if (Math.abs(h) > 1e-8) { if (pt == 0) { molarVolume(pressure, temperature, A, B, 1); } else + * { molarVolume(pressure, temperature, A, B, 0); } return getMolarVolume(); } */ // System.out.println("Z" + Z + " iterations " + iterations + " BonV " + BonV); // System.out.println("pressure " + Z*R*temperature/getMolarVolume()); @@ -1357,7 +1394,8 @@ public double molarVolumeChangePhase(double pressure, double temperature, double // " +dh + " B // " + Btemp + " gv" + gV() + " fv " + fv() + " fvv" + fVV()); if (Double.isNaN(getMolarVolume())) { - throw new neqsim.util.exception.IsNaNException(this, "molarVolumeChangePhase", "Molar volume"); + throw new neqsim.util.exception.IsNaNException(this, "molarVolumeChangePhase", + "Molar volume"); } return getMolarVolume(); @@ -1429,11 +1467,11 @@ public double[][] croeneckerProduct(double[][] a, double[][] b) { double[][] result = new double[aLength * bLength][(aCols) * (bCols)]; for (int z = 0; z < aLength; z++) { for (int i = 0; i < aCols; i++) { - for (int j = 0; j < bLength; j++) { - for (int k = 0; k < bCols; k++) { - result[j + (z * bLength)][k + (i * bCols)] = a[z][i] * b[j][k]; - } - } + for (int j = 0; j < bLength; j++) { + for (int k = 0; k < bCols; k++) { + result[j + (z * bLength)][k + (i * bCols)] = a[z][i] * b[j][k]; + } + } } } return result; @@ -1450,4 +1488,121 @@ public int getTotalNumberOfAccociationSites() { public void setTotalNumberOfAccociationSites(int totalNumberOfAccociationSites) { this.totalNumberOfAccociationSites = totalNumberOfAccociationSites; } + + /** + * Returns the transpose of a dense matrix. + * + * @param matrix input matrix + * @return transposed matrix + */ + private static DenseMatrix transpose(DenseMatrix matrix) { + DenseMatrix out = new DenseMatrix(matrix.numCols, matrix.numRows); + for (int i = 0; i < matrix.numRows; i++) { + for (int j = 0; j < matrix.numCols; j++) { + out.unsafe_set(j, i, matrix.unsafe_get(i, j)); + } + } + return out; + } + + /** + * Multiplies two dense matrices. + * + * @param left left matrix + * @param right right matrix + * @return matrix product + */ + private static DenseMatrix multiply(DenseMatrix left, DenseMatrix right) { + DenseMatrix out = new DenseMatrix(left.numRows, right.numCols); + LinearAlgebraOps.mult(left, right, out); + return out; + } + + /** + * Adds two dense matrices. + * + * @param left left matrix + * @param right right matrix + * @return sum matrix + */ + private static DenseMatrix add(DenseMatrix left, DenseMatrix right) { + DenseMatrix out = new DenseMatrix(left.numRows, left.numCols); + for (int i = 0; i < left.numRows; i++) { + for (int j = 0; j < left.numCols; j++) { + out.unsafe_set(i, j, left.unsafe_get(i, j) + right.unsafe_get(i, j)); + } + } + return out; + } + + /** + * Subtracts two dense matrices. + * + * @param left left matrix + * @param right right matrix + * @return difference matrix + */ + private static DenseMatrix subtract(DenseMatrix left, DenseMatrix right) { + DenseMatrix out = new DenseMatrix(left.numRows, left.numCols); + LinearAlgebraOps.subtract(left, right, out); + return out; + } + + /** + * Scales all elements in a dense matrix. + * + * @param matrix input matrix + * @param factor scale factor + * @return scaled matrix + */ + private static DenseMatrix scale(DenseMatrix matrix, double factor) { + DenseMatrix out = new DenseMatrix(matrix.numRows, matrix.numCols); + for (int i = 0; i < matrix.numRows; i++) { + for (int j = 0; j < matrix.numCols; j++) { + out.unsafe_set(i, j, matrix.unsafe_get(i, j) * factor); + } + } + return out; + } + + /** + * Element-wise multiplication of two dense matrices. + * + * @param left left matrix + * @param right right matrix + * @return Hadamard product + */ + private static DenseMatrix elementMult(DenseMatrix left, DenseMatrix right) { + DenseMatrix out = new DenseMatrix(left.numRows, left.numCols); + for (int i = 0; i < left.numRows; i++) { + for (int j = 0; j < left.numCols; j++) { + out.unsafe_set(i, j, left.unsafe_get(i, j) * right.unsafe_get(i, j)); + } + } + return out; + } + + /** + * Extracts one row or column as a matrix view copy. + * + * @param matrix source matrix + * @param extractRow true to extract a row, false to extract a column + * @param index row/column index + * @return extracted row/column matrix + */ + private static DenseMatrix extractVector(DenseMatrix matrix, boolean extractRow, int index) { + if (extractRow) { + DenseMatrix row = new DenseMatrix(1, matrix.numCols); + for (int j = 0; j < matrix.numCols; j++) { + row.unsafe_set(0, j, matrix.unsafe_get(index, j)); + } + return row; + } + DenseMatrix col = new DenseMatrix(matrix.numRows, 1); + for (int i = 0; i < matrix.numRows; i++) { + col.unsafe_set(i, 0, matrix.unsafe_get(i, index)); + } + return col; + } + } diff --git a/src/main/java/neqsim/thermo/phase/PhaseSrkCPA.java b/src/main/java/neqsim/thermo/phase/PhaseSrkCPA.java index 835d98a6b3..3778c8dd7f 100644 --- a/src/main/java/neqsim/thermo/phase/PhaseSrkCPA.java +++ b/src/main/java/neqsim/thermo/phase/PhaseSrkCPA.java @@ -2,17 +2,14 @@ import org.apache.logging.log4j.LogManager; import org.apache.logging.log4j.Logger; -import org.ejml.data.DMatrixRMaj; -import org.ejml.dense.row.CommonOps_DDRM; -import org.ejml.dense.row.NormOps_DDRM; -import org.ejml.dense.row.factory.LinearSolverFactory_DDRM; -import org.ejml.interfaces.linsol.LinearSolverDense; -import org.ejml.simple.SimpleMatrix; +import org.ojalgo.matrix.decomposition.LU; import neqsim.thermo.component.ComponentCPAInterface; import neqsim.thermo.component.ComponentSrkCPA; import neqsim.thermo.mixingrule.CPAMixingRuleHandler; import neqsim.thermo.mixingrule.CPAMixingRulesInterface; import neqsim.thermo.mixingrule.MixingRuleTypeInterface; +import neqsim.util.math.LinearAlgebraOps; +import neqsim.util.math.LinearAlgebraOps.DenseMatrix; /** *

@@ -42,7 +39,7 @@ public class PhaseSrkCPA extends PhaseSrkEos implements PhaseCPAInterface { double dFCPAdVdV = 0.0; double dFCPAdVdVdV = 0.0; double gcpav = 0.0; - protected double[] dFdNtemp = { 0, 0 }; + protected double[] dFdNtemp = {0, 0}; int cpaon = 1; int oldTotalNumberOfAccociationSites = 0; int totalNumberOfAccociationSites = 0; @@ -58,36 +55,38 @@ public class PhaseSrkCPA extends PhaseSrkEos implements PhaseCPAInterface { private double[][] deltadT = null; private double[][] deltadTdT = null; double[][][] Klkni = null; - private SimpleMatrix KlkTVMatrix = null; - private SimpleMatrix KlkTTMatrix = null; - private SimpleMatrix KlkTMatrix = null; - private SimpleMatrix udotTimesmMatrix = null; - private SimpleMatrix mVector = null; - private SimpleMatrix udotMatrix = null; - private SimpleMatrix uMatrix = null; - private SimpleMatrix QMatksiksiksi = null; - private SimpleMatrix KlkVVVMatrix = null; - private SimpleMatrix KlkVVMatrix = null; - private SimpleMatrix udotTimesmiMatrix = null; - private SimpleMatrix ksiMatrix = null; - private SimpleMatrix KlkMatrix = null; - private SimpleMatrix hessianMatrix = null; - private SimpleMatrix hessianInvers = null; + private DenseMatrix KlkTVMatrix = null; + private DenseMatrix KlkTTMatrix = null; + private DenseMatrix KlkTMatrix = null; + private DenseMatrix udotTimesmMatrix = null; + private DenseMatrix mVector = null; + private DenseMatrix udotMatrix = null; + private DenseMatrix uMatrix = null; + private DenseMatrix QMatksiksiksi = null; + private DenseMatrix KlkVVVMatrix = null; + private DenseMatrix KlkVVMatrix = null; + private DenseMatrix udotTimesmiMatrix = null; + private DenseMatrix ksiMatrix = null; + private DenseMatrix KlkMatrix = null; + private DenseMatrix hessianMatrix = null; + private DenseMatrix hessianInvers = null; /** - * Cached LU factorization of {@code hessianMatrix}. Reused by {@link #applyHessianInv} instead of multiplying by an - * explicit inverse when available. The solver is rebuilt when the number of association sites changes. + * Cached LU factorization of {@code hessianMatrix}. Reused by {@link #applyHessianInv} instead of + * multiplying by an explicit inverse when available. The solver is rebuilt when the number of + * association sites changes. */ - private transient LinearSolverDense hessianLU = null; + private transient LU hessianLU = null; /** - * Reusable scratch copy of {@code hessianMatrix.getMatrix()} passed into the LU solver. EJML factories may decompose - * in place, so we always feed the solver a copy to keep {@code hessianMatrix} readable by other call sites. + * Reusable scratch copy of {@code hessianMatrix.getMatrix()} passed into the LU solver. EJML + * factories may decompose in place, so we always feed the solver a copy to keep + * {@code hessianMatrix} readable by other call sites. */ - private transient DMatrixRMaj hessianLUinput = null; + private transient DenseMatrix hessianLUinput = null; private transient int hessianLUSize = -1; - private SimpleMatrix KlkVMatrix = null; - private DMatrixRMaj corr2Matrix = null; - private DMatrixRMaj corr3Matrix = null; - private DMatrixRMaj corr4Matrix = null; + private DenseMatrix KlkVMatrix = null; + private DenseMatrix corr2Matrix = null; + private DenseMatrix corr3Matrix = null; + private DenseMatrix corr4Matrix = null; /** *

@@ -109,7 +108,8 @@ public PhaseSrkCPA clone() { } if (activeAccosComp != null) { clonedPhase.activeAccosComp = activeAccosComp.clone(); - System.arraycopy(this.activeAccosComp, 0, clonedPhase.activeAccosComp, 0, activeAccosComp.length); + System.arraycopy(this.activeAccosComp, 0, clonedPhase.activeAccosComp, 0, + activeAccosComp.length); } // clonedPhase.cpaSelect = (CPAMixing) cpaSelect.clone(); // clonedPhase.cpamix = (CPAMixingInterface) cpamix.clone(); @@ -120,92 +120,106 @@ public PhaseSrkCPA clone() { /** {@inheritDoc} */ @Override - public void init(double totalNumberOfMoles, int numberOfComponents, int initType, PhaseType pt, double beta) { + public void init(double totalNumberOfMoles, int numberOfComponents, int initType, PhaseType pt, + double beta) { boolean changedAssosiationStatus = false; if (initType == 0) { activeAccosComp = new int[numberOfComponents]; for (int i = 0; i < numberOfComponents; i++) { - if (componentArray[i].getNumberOfmoles() < 1e-50) { - componentArray[i].setNumberOfAssociationSites(0); - if (activeAccosComp[i] == 1) { - activeAccosComp[i] = 0; - changedAssosiationStatus = true; - } - } else { - if (activeAccosComp[i] == 0) { - changedAssosiationStatus = true; - activeAccosComp[i] = 1; - } - } + if (componentArray[i].getNumberOfmoles() < 1e-50) { + componentArray[i].setNumberOfAssociationSites(0); + if (activeAccosComp[i] == 1) { + activeAccosComp[i] = 0; + changedAssosiationStatus = true; + } + } else { + if (activeAccosComp[i] == 0) { + changedAssosiationStatus = true; + activeAccosComp[i] = 1; + } + } } if (changedAssosiationStatus || lngi == null) { - setTotalNumberOfAccociationSites(0); - selfAccociationScheme = new int[numberOfComponents][0][0]; - crossAccociationScheme = new int[numberOfComponents][numberOfComponents][0][0]; - for (int i = 0; i < numberOfComponents; i++) { - if (componentArray[i].getNumberOfmoles() < 1e-50) { - componentArray[i].setNumberOfAssociationSites(0); - } else { - componentArray[i].setNumberOfAssociationSites(componentArray[i].getOrginalNumberOfAssociationSites()); - setTotalNumberOfAccociationSites( - getTotalNumberOfAccociationSites() + componentArray[i].getNumberOfAssociationSites()); - selfAccociationScheme[i] = cpaSelect.setAssociationScheme(i, this); - for (int j = 0; j < numberOfComponents; j++) { - crossAccociationScheme[i][j] = cpaSelect.setCrossAssociationScheme(i, j, this); - } - } - } + setTotalNumberOfAccociationSites(0); + selfAccociationScheme = new int[numberOfComponents][0][0]; + crossAccociationScheme = new int[numberOfComponents][numberOfComponents][0][0]; + for (int i = 0; i < numberOfComponents; i++) { + if (componentArray[i].getNumberOfmoles() < 1e-50) { + componentArray[i].setNumberOfAssociationSites(0); + } else { + componentArray[i].setNumberOfAssociationSites( + componentArray[i].getOrginalNumberOfAssociationSites()); + setTotalNumberOfAccociationSites(getTotalNumberOfAccociationSites() + + componentArray[i].getNumberOfAssociationSites()); + selfAccociationScheme[i] = cpaSelect.setAssociationScheme(i, this); + for (int j = 0; j < numberOfComponents; j++) { + crossAccociationScheme[i][j] = cpaSelect.setCrossAssociationScheme(i, j, this); + } + } + } } for (int i = 0; i < numberOfComponents; i++) { - for (int j = 0; j < componentArray[i].getNumberOfAssociationSites(); j++) { - ((ComponentSrkCPA) componentArray[i]).setXsite(j, 1.0); - ((ComponentSrkCPA) componentArray[i]).setXsitedV(j, 0.0); - ((ComponentSrkCPA) componentArray[i]).setXsitedT(j, 0.0); - } + for (int j = 0; j < componentArray[i].getNumberOfAssociationSites(); j++) { + ((ComponentSrkCPA) componentArray[i]).setXsite(j, 1.0); + ((ComponentSrkCPA) componentArray[i]).setXsitedV(j, 0.0); + ((ComponentSrkCPA) componentArray[i]).setXsitedT(j, 0.0); + } } if (changedAssosiationStatus || lngi == null || mVector == null) { - lngi = new double[numberOfComponents]; - mVector = new SimpleMatrix(getTotalNumberOfAccociationSites(), 1); - KlkMatrix = new SimpleMatrix(getTotalNumberOfAccociationSites(), getTotalNumberOfAccociationSites()); - KlkVMatrix = new SimpleMatrix(getTotalNumberOfAccociationSites(), getTotalNumberOfAccociationSites()); - KlkVVMatrix = new SimpleMatrix(getTotalNumberOfAccociationSites(), getTotalNumberOfAccociationSites()); - KlkVVVMatrix = new SimpleMatrix(getTotalNumberOfAccociationSites(), getTotalNumberOfAccociationSites()); - hessianMatrix = new SimpleMatrix(getTotalNumberOfAccociationSites(), getTotalNumberOfAccociationSites()); - KlkTMatrix = new SimpleMatrix(getTotalNumberOfAccociationSites(), getTotalNumberOfAccociationSites()); - KlkTTMatrix = new SimpleMatrix(getTotalNumberOfAccociationSites(), getTotalNumberOfAccociationSites()); - KlkTVMatrix = new SimpleMatrix(getTotalNumberOfAccociationSites(), getTotalNumberOfAccociationSites()); - corr2Matrix = new DMatrixRMaj(getTotalNumberOfAccociationSites(), 1); - corr3Matrix = new DMatrixRMaj(getTotalNumberOfAccociationSites(), 1); - corr4Matrix = new DMatrixRMaj(getTotalNumberOfAccociationSites(), 1); - Klkni = new double[numberOfComponents][getTotalNumberOfAccociationSites()][getTotalNumberOfAccociationSites()]; - ksiMatrix = new SimpleMatrix(getTotalNumberOfAccociationSites(), 1); - uMatrix = new SimpleMatrix(getTotalNumberOfAccociationSites(), 1); - udotMatrix = new SimpleMatrix(getTotalNumberOfAccociationSites(), 1); - moleculeNumber = new int[getTotalNumberOfAccociationSites()]; - assSiteNumber = new int[getTotalNumberOfAccociationSites()]; - gvector = new double[getTotalNumberOfAccociationSites()][1]; - udotTimesmMatrix = new SimpleMatrix(getTotalNumberOfAccociationSites(), 1); - delta = new double[getTotalNumberOfAccociationSites()][getTotalNumberOfAccociationSites()]; - deltaNog = new double[getTotalNumberOfAccociationSites()][getTotalNumberOfAccociationSites()]; - deltadT = new double[getTotalNumberOfAccociationSites()][getTotalNumberOfAccociationSites()]; - deltadTdT = new double[getTotalNumberOfAccociationSites()][getTotalNumberOfAccociationSites()]; - QMatksiksiksi = new SimpleMatrix(getTotalNumberOfAccociationSites(), 1); - udotTimesmiMatrix = new SimpleMatrix(numberOfComponents, getTotalNumberOfAccociationSites()); - - oldTotalNumberOfAccociationSites = getTotalNumberOfAccociationSites(); - - int temp = 0; - for (int i = 0; i < numberOfComponents; i++) { - for (int j = 0; j < componentArray[i].getNumberOfAssociationSites(); j++) { - moleculeNumber[temp + j] = i; - assSiteNumber[temp + j] = j; - } - temp += componentArray[i].getNumberOfAssociationSites(); - } + lngi = new double[numberOfComponents]; + mVector = new DenseMatrix(getTotalNumberOfAccociationSites(), 1); + KlkMatrix = + new DenseMatrix(getTotalNumberOfAccociationSites(), getTotalNumberOfAccociationSites()); + KlkVMatrix = + new DenseMatrix(getTotalNumberOfAccociationSites(), getTotalNumberOfAccociationSites()); + KlkVVMatrix = + new DenseMatrix(getTotalNumberOfAccociationSites(), getTotalNumberOfAccociationSites()); + KlkVVVMatrix = + new DenseMatrix(getTotalNumberOfAccociationSites(), getTotalNumberOfAccociationSites()); + hessianMatrix = + new DenseMatrix(getTotalNumberOfAccociationSites(), getTotalNumberOfAccociationSites()); + KlkTMatrix = + new DenseMatrix(getTotalNumberOfAccociationSites(), getTotalNumberOfAccociationSites()); + KlkTTMatrix = + new DenseMatrix(getTotalNumberOfAccociationSites(), getTotalNumberOfAccociationSites()); + KlkTVMatrix = + new DenseMatrix(getTotalNumberOfAccociationSites(), getTotalNumberOfAccociationSites()); + corr2Matrix = new DenseMatrix(getTotalNumberOfAccociationSites(), 1); + corr3Matrix = new DenseMatrix(getTotalNumberOfAccociationSites(), 1); + corr4Matrix = new DenseMatrix(getTotalNumberOfAccociationSites(), 1); + Klkni = + new double[numberOfComponents][getTotalNumberOfAccociationSites()][getTotalNumberOfAccociationSites()]; + ksiMatrix = new DenseMatrix(getTotalNumberOfAccociationSites(), 1); + uMatrix = new DenseMatrix(getTotalNumberOfAccociationSites(), 1); + udotMatrix = new DenseMatrix(getTotalNumberOfAccociationSites(), 1); + moleculeNumber = new int[getTotalNumberOfAccociationSites()]; + assSiteNumber = new int[getTotalNumberOfAccociationSites()]; + gvector = new double[getTotalNumberOfAccociationSites()][1]; + udotTimesmMatrix = new DenseMatrix(getTotalNumberOfAccociationSites(), 1); + delta = new double[getTotalNumberOfAccociationSites()][getTotalNumberOfAccociationSites()]; + deltaNog = + new double[getTotalNumberOfAccociationSites()][getTotalNumberOfAccociationSites()]; + deltadT = + new double[getTotalNumberOfAccociationSites()][getTotalNumberOfAccociationSites()]; + deltadTdT = + new double[getTotalNumberOfAccociationSites()][getTotalNumberOfAccociationSites()]; + QMatksiksiksi = new DenseMatrix(getTotalNumberOfAccociationSites(), 1); + udotTimesmiMatrix = new DenseMatrix(numberOfComponents, getTotalNumberOfAccociationSites()); + + oldTotalNumberOfAccociationSites = getTotalNumberOfAccociationSites(); + + int temp = 0; + for (int i = 0; i < numberOfComponents; i++) { + for (int j = 0; j < componentArray[i].getNumberOfAssociationSites(); j++) { + moleculeNumber[temp + j] = i; + assSiteNumber[temp + j] = j; + } + temp += componentArray[i].getNumberOfAssociationSites(); + } } } @@ -262,46 +276,47 @@ public void initCPAMatrix(int type) { double tempVar2; for (int i = 0; i < numberOfComponents; i++) { for (int j = 0; j < componentArray[i].getNumberOfAssociationSites(); j++) { - tempVar1 = ksiMatrix.get(temp + j, 0); - tempVar2 = udotMatrix.get(temp + j, 0); - uMatrix.set(temp + j, 0, Math.log(tempVar1) - tempVar1 + 1.0); - gvector[temp + j][0] = mVector.get(temp + j, 0) * tempVar2; - - if (moleculeNumber[temp + j] == i) { - udotTimesmiMatrix.set(i, temp + j, tempVar2); - } else { - udotTimesmiMatrix.set(i, temp + j, 0.0); - } + tempVar1 = ksiMatrix.unsafe_get(temp + j, 0); + tempVar2 = udotMatrix.unsafe_get(temp + j, 0); + uMatrix.unsafe_set(temp + j, 0, Math.log(tempVar1) - tempVar1 + 1.0); + gvector[temp + j][0] = mVector.unsafe_get(temp + j, 0) * tempVar2; + + if (moleculeNumber[temp + j] == i) { + udotTimesmiMatrix.unsafe_set(i, temp + j, tempVar2); + } else { + udotTimesmiMatrix.unsafe_set(i, temp + j, 0.0); + } } temp += componentArray[i].getNumberOfAssociationSites(); } if (type > 2) { for (int p = 0; p < numberOfComponents; p++) { - lngi[p] = ((ComponentSrkCPA) componentArray[p]).calc_lngi(this); + lngi[p] = ((ComponentSrkCPA) componentArray[p]).calc_lngi(this); } } for (int i = 0; i < totalNumberOfAccociationSites; i++) { for (int j = i; j < totalNumberOfAccociationSites; j++) { - delta[i][j] = deltaNog[i][j] * gcpa; - delta[j][i] = delta[i][j]; - if (type > 1) { - deltadT[i][j] = cpamix.calcDeltadT(assSiteNumber[i], assSiteNumber[j], moleculeNumber[i], moleculeNumber[j], - this, getTemperature(), getPressure(), numberOfComponents); - deltadT[j][i] = deltadT[i][j]; - - deltadTdT[i][j] = cpamix.calcDeltadTdT(assSiteNumber[i], assSiteNumber[j], moleculeNumber[i], - moleculeNumber[j], this, getTemperature(), getPressure(), numberOfComponents); - deltadTdT[j][i] = deltadTdT[i][j]; - } + delta[i][j] = deltaNog[i][j] * gcpa; + delta[j][i] = delta[i][j]; + if (type > 1) { + deltadT[i][j] = cpamix.calcDeltadT(assSiteNumber[i], assSiteNumber[j], moleculeNumber[i], + moleculeNumber[j], this, getTemperature(), getPressure(), numberOfComponents); + deltadT[j][i] = deltadT[i][j]; + + deltadTdT[i][j] = + cpamix.calcDeltadTdT(assSiteNumber[i], assSiteNumber[j], moleculeNumber[i], + moleculeNumber[j], this, getTemperature(), getPressure(), numberOfComponents); + deltadTdT[j][i] = deltadTdT[i][j]; + } } } double totalVolume = getTotalVolume(); double totalVolume2 = totalVolume * totalVolume; double totalVolume3 = totalVolume2 * totalVolume; - double gdv1 = getGcpav() - 1.0 / totalVolume; + double gdv1 = gcpav - 1.0 / totalVolume; double gdv2 = gdv1 * gdv1; double gdv3 = gdv2 * gdv1; double Klk = 0.0; @@ -309,84 +324,90 @@ public void initCPAMatrix(int type) { double tempKsiRead = 0.0; for (int i = 0; i < totalNumberOfAccociationSites; i++) { for (int j = i; j < totalNumberOfAccociationSites; j++) { - Klk = KlkMatrix.get(i, j); - tempVar = Klk * gdv1; - KlkVMatrix.set(i, j, tempVar); - KlkVMatrix.set(j, i, tempVar); - - tempVar = Klk * gdv2 + Klk * (gcpavv + 1.0 / totalVolume2); - KlkVVMatrix.set(i, j, tempVar); - KlkVVMatrix.set(j, i, tempVar); - - tempVar = Klk * gdv3 + 3.0 * Klk * (gcpav - 1.0 / totalVolume) * (gcpavv + 1.0 / (totalVolume2)) - + Klk * (gcpavvv - 2.0 / (totalVolume3)); - KlkVVVMatrix.set(i, j, tempVar); - KlkVVVMatrix.set(j, i, tempVar); - - if (type > 1) { - tempVar = deltadT[i][j] / delta[i][j]; - - if (Math.abs(tempVar) > 1e-50) { - double tempVardT = deltadTdT[i][j] / delta[i][j] - - (deltadT[i][j] * deltadT[i][j]) / (delta[i][j] * delta[i][j]); - - tempVar2 = Klk * tempVar; - KlkTMatrix.set(i, j, tempVar2); - KlkTMatrix.set(j, i, tempVar2); - - tempVar2 = Klk * tempVar * (gcpav - 1.0 / totalVolume); - KlkTVMatrix.set(i, j, tempVar2); - KlkTVMatrix.set(j, i, tempVar2); - - tempVar2 = Klk * (tempVar * tempVar + tempVardT); - KlkTTMatrix.set(i, j, tempVar2); - KlkTTMatrix.set(j, i, tempVar2); - } - - if (type > 2) { - for (int p = 0; p < numberOfComponents; p++) { - double t1 = 0.0; - double t2 = 0.0; - if (moleculeNumber[i] == p) { - t1 = 1.0 / mVector.get(i, 0); - } - if (moleculeNumber[j] == p) { - t2 = 1.0 / mVector.get(j, 0); - } - Klkni[p][i][j] = Klk * (t1 + t2 + lngi[p]); // ((ComponentSrkCPA) - // getComponent(p)).calc_lngi(this)); - Klkni[p][j][i] = Klkni[p][i][j]; - } - } - } + Klk = KlkMatrix.unsafe_get(i, j); + tempVar = Klk * gdv1; + KlkVMatrix.unsafe_set(i, j, tempVar); + KlkVMatrix.unsafe_set(j, i, tempVar); + + tempVar = Klk * gdv2 + Klk * (gcpavv + 1.0 / totalVolume2); + KlkVVMatrix.unsafe_set(i, j, tempVar); + KlkVVMatrix.unsafe_set(j, i, tempVar); + + tempVar = + Klk * gdv3 + 3.0 * Klk * (gcpav - 1.0 / totalVolume) * (gcpavv + 1.0 / totalVolume2) + + Klk * (gcpavvv - 2.0 / totalVolume3); + KlkVVVMatrix.unsafe_set(i, j, tempVar); + KlkVVVMatrix.unsafe_set(j, i, tempVar); + + if (type > 1) { + tempVar = deltadT[i][j] / delta[i][j]; + + if (Math.abs(tempVar) > 1e-50) { + double tempVardT = deltadTdT[i][j] / delta[i][j] + - (deltadT[i][j] * deltadT[i][j]) / (delta[i][j] * delta[i][j]); + + tempVar2 = Klk * tempVar; + KlkTMatrix.unsafe_set(i, j, tempVar2); + KlkTMatrix.unsafe_set(j, i, tempVar2); + + tempVar2 = Klk * tempVar * (gcpav - 1.0 / totalVolume); + KlkTVMatrix.unsafe_set(i, j, tempVar2); + KlkTVMatrix.unsafe_set(j, i, tempVar2); + + tempVar2 = Klk * (tempVar * tempVar + tempVardT); + KlkTTMatrix.unsafe_set(i, j, tempVar2); + KlkTTMatrix.unsafe_set(j, i, tempVar2); + } + + if (type > 2) { + for (int p = 0; p < numberOfComponents; p++) { + double t1 = 0.0; + double t2 = 0.0; + if (moleculeNumber[i] == p) { + t1 = 1.0 / mVector.unsafe_get(i, 0); + } + if (moleculeNumber[j] == p) { + t2 = 1.0 / mVector.unsafe_get(j, 0); + } + Klkni[p][i][j] = Klk * (t1 + t2 + lngi[p]); + Klkni[p][j][i] = Klkni[p][i][j]; + } + } + } } - tempKsiRead = ksiMatrix.get(i, 0); - QMatksiksiksi.set(i, 0, 2.0 * mVector.get(i, 0) / (tempKsiRead * tempKsiRead * tempKsiRead)); + tempKsiRead = ksiMatrix.unsafe_get(i, 0); + QMatksiksiksi.unsafe_set(i, 0, + 2.0 * mVector.unsafe_get(i, 0) / (tempKsiRead * tempKsiRead * tempKsiRead)); } - SimpleMatrix ksiMatrixTranspose = ksiMatrix.transpose(); + DenseMatrix ksiMatrixTranspose = transpose(ksiMatrix); - // dXdV - SimpleMatrix KlkVMatrixksi = KlkVMatrix.mult(ksiMatrix); - SimpleMatrix XV = applyHessianInv(KlkVMatrixksi); - SimpleMatrix XVtranspose = XV.transpose(); + DenseMatrix klkVMatrixksi = multiply(KlkVMatrix, ksiMatrix); + DenseMatrix XV = applyHessianInv(klkVMatrixksi); + DenseMatrix XVtranspose = transpose(XV); - FCPA = mVector.transpose().mult(uMatrix.minus(ksiMatrix.elementMult(udotMatrix).scale(0.5))).get(0, 0); // QCPA.get(0, - // 0); - // //*0.5; + DenseMatrix qCpa = multiply(transpose(mVector), + subtract(uMatrix, scale(elementMult(ksiMatrix, udotMatrix), 0.5))); + FCPA = qCpa.unsafe_get(0, 0); - dFCPAdV = ksiMatrixTranspose.mult(KlkVMatrixksi).get(0, 0) * (-0.5); - SimpleMatrix KlkVVMatrixTImesKsi = KlkVVMatrix.mult(ksiMatrix); - dFCPAdVdV = ksiMatrixTranspose.mult(KlkVVMatrixTImesKsi).scale(-0.5).minus(KlkVMatrixksi.transpose().mult(XV)) - .get(0, 0); + DenseMatrix tempMatrix = scale(multiply(ksiMatrixTranspose, klkVMatrixksi), -0.5); + dFCPAdV = tempMatrix.unsafe_get(0, 0); + DenseMatrix klkVvMatrixTimesKsi = multiply(KlkVVMatrix, ksiMatrix); + DenseMatrix tempMatrixVV = + subtract(scale(multiply(ksiMatrixTranspose, klkVvMatrixTimesKsi), -0.5), + multiply(transpose(klkVMatrixksi), XV)); + dFCPAdVdV = tempMatrixVV.unsafe_get(0, 0); - SimpleMatrix QVVV = ksiMatrixTranspose.mult(KlkVVVMatrix.mult(ksiMatrix)); // .scale(-0.5); - SimpleMatrix QVVksi = KlkVVMatrixTImesKsi.scale(-1.0); - SimpleMatrix QksiVksi = KlkVMatrix.scale(-1.0); + DenseMatrix qVvv = multiply(ksiMatrixTranspose, multiply(KlkVVVMatrix, ksiMatrix)); + DenseMatrix qVvksi = scale(klkVvMatrixTimesKsi, -1.0); + DenseMatrix qKsiVksi = scale(KlkVMatrix, -1.0); - dFCPAdVdVdV = -0.5 * QVVV.get(0, 0) + QVVksi.transpose().mult(XV).get(0, 0) * 3.0 - + XVtranspose.mult(QksiVksi.mult(XV)).get(0, 0) * 3.0 - + XVtranspose.mult(QMatksiksiksi.mult(XVtranspose)).mult(XV).get(0, 0); + DenseMatrix mat1 = scale(multiply(transpose(qVvksi), XV), 3.0); + DenseMatrix mat2 = scale(multiply(XVtranspose, multiply(qKsiVksi, XV)), 3.0); + DenseMatrix mat4 = multiply(multiply(XVtranspose, multiply(QMatksiksiksi, XVtranspose)), XV); + + dFCPAdVdVdV = -0.5 * qVvv.unsafe_get(0, 0) + mat1.unsafe_get(0, 0) + mat2.unsafe_get(0, 0) + + mat4.unsafe_get(0, 0); if (type == 1) { return; @@ -395,36 +416,30 @@ public void initCPAMatrix(int type) { temp = 0; for (int p = 0; p < numberOfComponents; p++) { for (int kk = 0; kk < getComponent(p).getNumberOfAssociationSites(); kk++) { - ((ComponentCPAInterface) getComponent(p)).setXsitedV(kk, XV.get(temp + kk, 0)); + ((ComponentCPAInterface) getComponent(p)).setXsitedV(kk, XV.unsafe_get(temp + kk, 0)); } temp += getComponent(p).getNumberOfAssociationSites(); } - // KlkTMatrix = new SimpleMatrix(KlkdT); - SimpleMatrix KlkTMatrixTImesKsi = KlkTMatrix.mult(ksiMatrix); - // dQdT - SimpleMatrix tempMatrix2 = ksiMatrixTranspose.mult(KlkTMatrixTImesKsi); // .scale(-0.5); - dFCPAdT = tempMatrix2.get(0, 0) * (-0.5); - - // SimpleMatrix KlkTVMatrix = new SimpleMatrix(KlkdTdV); - // SimpleMatrix tempMatrixTV = - // ksiMatrixTranspose.mult(KlkTVMatrix.mult(ksiMatrix)).scale(-0.5).minus(KlkTMatrixTImesKsi.transpose().mult(XV)); - // dFCPAdTdV = tempMatrixTV.get(0, 0); - // dXdT - SimpleMatrix XT = applyHessianInv(KlkTMatrixTImesKsi); - // dQdTdT - SimpleMatrix tempMatrixTT = ksiMatrixTranspose.mult(KlkTTMatrix.mult(ksiMatrix)).scale(-0.5) - .minus(KlkTMatrixTImesKsi.transpose().mult(XT)); - dFCPAdTdT = tempMatrixTT.get(0, 0); - - SimpleMatrix tempMatrixTV = ksiMatrixTranspose.mult(KlkTVMatrix.mult(ksiMatrix)).scale(-0.5) - .minus(KlkTMatrixTImesKsi.transpose().mult(XV)); - dFCPAdTdV = tempMatrixTV.get(0, 0); + DenseMatrix klkTMatrixTimesKsi = multiply(KlkTMatrix, ksiMatrix); + DenseMatrix tempMatrix2 = scale(multiply(ksiMatrixTranspose, klkTMatrixTimesKsi), -0.5); + dFCPAdT = tempMatrix2.unsafe_get(0, 0); + + DenseMatrix XT = applyHessianInv(klkTMatrixTimesKsi); + DenseMatrix tempMatrixTT = + subtract(scale(multiply(ksiMatrixTranspose, multiply(KlkTTMatrix, ksiMatrix)), -0.5), + multiply(transpose(klkTMatrixTimesKsi), XT)); + dFCPAdTdT = tempMatrixTT.unsafe_get(0, 0); + + DenseMatrix tempMatrixTV = + subtract(scale(multiply(ksiMatrixTranspose, multiply(KlkTVMatrix, ksiMatrix)), -0.5), + multiply(transpose(klkTMatrixTimesKsi), XV)); + dFCPAdTdV = tempMatrixTV.unsafe_get(0, 0); temp = 0; for (int p = 0; p < numberOfComponents; p++) { for (int kk = 0; kk < getComponent(p).getNumberOfAssociationSites(); kk++) { - ((ComponentCPAInterface) getComponent(p)).setXsitedT(kk, XT.get(temp + kk, 0)); + ((ComponentCPAInterface) getComponent(p)).setXsitedT(kk, XT.unsafe_get(temp + kk, 0)); } temp += getComponent(p).getNumberOfAssociationSites(); } @@ -433,51 +448,20 @@ public void initCPAMatrix(int type) { return; } - // int assSites = 0; - // if(true) return; for (int p = 0; p < numberOfComponents; p++) { - SimpleMatrix KiMatrix = new SimpleMatrix(Klkni[p]); - // KiMatrix.print(10,10); - // Matrix dQdniMatrix = - // (ksiMatrix.transpose().times(KiMatrix.times(ksiMatrix)).times(-0.5)); // this - // methods misses one part of .... - // dQdniMatrix.print(10,10); - // KiMatrix.print(10, 10); - // miMatrix.getMatrix(assSites, assSites, 0, totalNumberOfAccociationSites - - // 1).print(10, 10); - // Matrix tempMatrix20 = miMatrix.getMatrix(assSites, assSites, 0, - // totalNumberOfAccociationSites - - // 1).times(uMatrix).minus(ksiMatrix.transpose().times(KiMatrix.times(ksiMatrix)).times(-0.5)); - // - // ksiMatrix.transpose().times(KlkTMatrix.times(ksiMatrix)).times(-0.5); - // System.out.println("dQdn "); - // tempMatrix20.print(10, 10); - SimpleMatrix tempMatrix4 = KiMatrix.mult(ksiMatrix); - // udotTimesmiMatrix.getMatrix(assSites, assSites, 0, - // totalNumberOfAccociationSites - 1).print(10, 10); - SimpleMatrix tempMatrix5 = udotTimesmiMatrix.extractVector(true, p).transpose().minus(tempMatrix4); - // tempMki[0] = mki[p]; - // Matrix amatrix = new Matrix(croeneckerProduct(tempMki, - // udotMatrix.getArray())); - // System.out.println("aMatrix "); - // amatrix.transpose().print(10, 10); - // System.out.println("temp4 matrix"); - // tempMatrix4.print(10, 10); - // Matrix tempMatrix5 = amatrix.minus(tempMatrix4); - SimpleMatrix tempMatrix6 = applyHessianInv(tempMatrix5); // .scale(-1.0); - // System.out.println("dXdni"); - // tempMatrix4.print(10, 10); - // tempMatrix5.print(10, 10); - // System.out.println("dXdn "); - // tempMatrix6.print(10, 10); + DenseMatrix kiMatrix = new DenseMatrix(Klkni[p]); + DenseMatrix tempMatrix4 = multiply(kiMatrix, ksiMatrix); + DenseMatrix tempMatrix5 = + subtract(transpose(extractVector(udotTimesmiMatrix, true, p)), tempMatrix4); + DenseMatrix tempMatrix6 = applyHessianInv(tempMatrix5); int temp2 = 0; for (int compp = 0; compp < numberOfComponents; compp++) { - for (int kk = 0; kk < getComponent(compp).getNumberOfAssociationSites(); kk++) { - ((ComponentCPAInterface) getComponent(compp)).setXsitedni(kk, p, -1.0 * tempMatrix6.get(temp2 + kk, 0)); - } - temp2 += getComponent(compp).getNumberOfAssociationSites(); + for (int kk = 0; kk < getComponent(compp).getNumberOfAssociationSites(); kk++) { + ((ComponentCPAInterface) getComponent(compp)).setXsitedni(kk, p, + -1.0 * tempMatrix6.unsafe_get(temp2 + kk, 0)); + } + temp2 += getComponent(compp).getNumberOfAssociationSites(); } - // assSites += getComponent(p).getNumberOfAssociationSites(); } } @@ -498,9 +482,9 @@ public void setMixingRule(MixingRuleTypeInterface mr) { public void calcDelta() { for (int i = 0; i < getTotalNumberOfAccociationSites(); i++) { for (int j = i; j < getTotalNumberOfAccociationSites(); j++) { - deltaNog[i][j] = cpamix.calcDeltaNog(assSiteNumber[i], assSiteNumber[j], moleculeNumber[i], moleculeNumber[j], - this, getTemperature(), getPressure(), numberOfComponents); - deltaNog[j][i] = deltaNog[i][j]; + deltaNog[i][j] = cpamix.calcDeltaNog(assSiteNumber[i], assSiteNumber[j], moleculeNumber[i], + moleculeNumber[j], this, getTemperature(), getPressure(), numberOfComponents); + deltaNog[j][i] = deltaNog[i][j]; } } } @@ -512,7 +496,7 @@ public void addComponent(String name, double moles, double molesInPhase, int com componentArray[compNumber] = new ComponentSrkCPA(name, moles, molesInPhase, compNumber, this); for (int i = 0; i < numberOfComponents; i++) { if (componentArray[i] instanceof ComponentSrkCPA) { - ((ComponentSrkCPA) componentArray[i]).resizeXsitedni(numberOfComponents); + ((ComponentSrkCPA) componentArray[i]).resizeXsitedni(numberOfComponents); } } } @@ -570,10 +554,10 @@ public double dFdTdT() { */ public double FCPA() { /* - * double tot = 0.0; double ans = 0.0; for (int i = 0; i < numberOfComponents; i++) { tot = 0.0; for (int j = 0; j < - * componentArray[i].getNumberOfAssociationSites(); j++) { double xai = ((ComponentSrkCPA) - * componentArray[i]).getXsite()[j]; tot += (Math.log(xai) - 1.0 / 2.0 * xai + 1.0 / 2.0); } ans += - * componentArray[i].getNumberOfMolesInPhase() * tot; } return ans; + * double tot = 0.0; double ans = 0.0; for (int i = 0; i < numberOfComponents; i++) { tot = 0.0; + * for (int j = 0; j < componentArray[i].getNumberOfAssociationSites(); j++) { double xai = + * ((ComponentSrkCPA) componentArray[i]).getXsite()[j]; tot += (Math.log(xai) - 1.0 / 2.0 * xai + * + 1.0 / 2.0); } ans += componentArray[i].getNumberOfMolesInPhase() * tot; } return ans; */ return FCPA; } @@ -629,12 +613,12 @@ public double dFCPAdVdVdV() { */ public double dFCPAdT() { /* - * double tot = 0.0; double ans = 0.0; for (int i = 0; i < numberOfComponents; i++) { tot = 0.0; for (int j = 0; j < - * componentArray[i].getNumberOfAssociationSites(); j++) { double xai = ((ComponentSrkCPA) - * componentArray[i]).getXsite()[j]; double xaidT = ((ComponentSrkCPA) componentArray[i]).getXsitedT()[j]; tot += - * 1.0 / xai * xaidT - 0.5 * xaidT; // - 1.0 / 2.0 * xai + 1.0 / 2.0); } ans += - * componentArray[i].getNumberOfMolesInPhase() * tot; } System.out.println("dFCPAdT1 " + ans + " dfcpa2 " - * +dFCPAdT); return ans; + * double tot = 0.0; double ans = 0.0; for (int i = 0; i < numberOfComponents; i++) { tot = 0.0; + * for (int j = 0; j < componentArray[i].getNumberOfAssociationSites(); j++) { double xai = + * ((ComponentSrkCPA) componentArray[i]).getXsite()[j]; double xaidT = ((ComponentSrkCPA) + * componentArray[i]).getXsitedT()[j]; tot += 1.0 / xai * xaidT - 0.5 * xaidT; // - 1.0 / 2.0 * + * xai + 1.0 / 2.0); } ans += componentArray[i].getNumberOfMolesInPhase() * tot; } + * System.out.println("dFCPAdT1 " + ans + " dfcpa2 " +dFCPAdT); return ans; */ return dFCPAdT; } @@ -662,8 +646,8 @@ public double dFCPAdTdV() { // getTotalVolume()) * (1.0 - getTotalVolume() * getGcpav()) * hcpatotdT)); return dFCPAdTdV; /* - * if (totalNumberOfAccociationSites > 0) { return 1.0 / (2.0 * getTotalVolume()) * (1.0 - getTotalVolume() * - * getGcpav()) * hcpatotdT; } else { return 0; } + * if (totalNumberOfAccociationSites > 0) { return 1.0 / (2.0 * getTotalVolume()) * (1.0 - + * getTotalVolume() * getGcpav()) * hcpatotdT; } else { return 0; } */ } @@ -684,13 +668,13 @@ protected double[] calcdFdNtemp() { // temp = ((ComponentSrkCPA) getComponent(k)).calc_lngi(this); // temp2 = ((ComponentSrkCPA) getComponent(k)).calc_lngidV(this); for (int i = 0; i < getComponent(k).getNumberOfAssociationSites(); i++) { - tot2 -= 1.0 * ((ComponentSrkCPA) getComponent(k)).getXsitedV()[i]; - tot3 += (1.0 - ((ComponentSrkCPA) getComponent(k)).getXsite()[i]) * 1.0; + tot2 -= 1.0 * ((ComponentSrkCPA) getComponent(k)).getXsitedV()[i]; + tot3 += (1.0 - ((ComponentSrkCPA) getComponent(k)).getXsite()[i]) * 1.0; } tot1 += 1.0 / 2.0 * tot2 * getComponent(k).getNumberOfMolesInPhase(); tot4 += 0.5 * getComponent(k).getNumberOfMolesInPhase() * tot3; } - return new double[] { -tot1, -tot4 }; + return new double[] {-tot1, -tot4}; } /** @@ -705,21 +689,24 @@ public void calcXsitedV() { } /** - * Apply the inverse of the current Hessian to a right-hand side. If an LU factorization is available (cached by - * {@link #solveX}), it is used to back-solve; otherwise, the explicit {@code hessianInvers} matrix is used (e.g. when - * called from legacy code paths). + * Apply the inverse of the current Hessian to a right-hand side. If an LU factorization is + * available (cached by {@link #solveX}), it is used to back-solve; otherwise, the explicit + * {@code hessianInvers} matrix is used (e.g. when called from legacy code paths). * * @param rhs right-hand side - * @return {@code H^{-1} * rhs} as a SimpleMatrix + * @return {@code H^{-1} * rhs} as a dense matrix */ - private SimpleMatrix applyHessianInv(SimpleMatrix rhs) { + private DenseMatrix applyHessianInv(DenseMatrix rhs) { + if (hessianInvers != null) { + return multiply(hessianInvers, rhs); + } if (hessianLU != null && hessianLUSize == totalNumberOfAccociationSites) { - DMatrixRMaj rhsMat = rhs.getDDRM(); - DMatrixRMaj out = new DMatrixRMaj(rhsMat.numRows, rhsMat.numCols); - hessianLU.solve(rhsMat, out); - return SimpleMatrix.wrap(out); + DenseMatrix out = new DenseMatrix(rhs.numRows, rhs.numCols); + LinearAlgebraOps.solveLu(hessianLU, rhs.numRows, rhs.numCols, (i, j) -> rhs.unsafe_get(i, j), + (i, j, value) -> out.unsafe_set(i, j, value)); + return out; } - return hessianInvers.mult(rhs); + throw new IllegalStateException("Hessian factorization has not been initialized"); } /** @@ -736,8 +723,8 @@ public boolean solveX() { boolean solvedX = solveX2(15); - DMatrixRMaj mVectorMat = mVector.getMatrix(); - DMatrixRMaj ksiMatrixMat = ksiMatrix.getMatrix(); + DenseMatrix mVectorMat = mVector; + DenseMatrix ksiMatrixMat = ksiMatrix; // ksiMatrix.print(); // second order method not working correctly and not used t the moment b ecause of numerical @@ -746,12 +733,12 @@ public boolean solveX() { int iter = 0; for (int i = 0; i < numberOfComponents; i++) { for (int j = 0; j < componentArray[i].getNumberOfAssociationSites(); j++) { - mVectorMat.unsafe_set(temp + j, 0, componentArray[i].getNumberOfMolesInPhase()); + mVectorMat.unsafe_set(temp + j, 0, componentArray[i].getNumberOfMolesInPhase()); } temp += componentArray[i].getNumberOfAssociationSites(); } - DMatrixRMaj mat1 = KlkMatrix.getMatrix(); + DenseMatrix mat1 = KlkMatrix; double Klk = 0.0; double totvolume = getTotalVolume(); double tempVari; @@ -759,14 +746,13 @@ public boolean solveX() { for (int i = 0; i < getTotalNumberOfAccociationSites(); i++) { tempVari = mVectorMat.unsafe_get(i, 0); for (int j = i; j < getTotalNumberOfAccociationSites(); j++) { - tempVarj = mVectorMat.unsafe_get(j, 0); - Klk = tempVari * tempVarj / totvolume * delta[i][j]; - mat1.unsafe_set(i, j, Klk); - mat1.unsafe_set(j, i, Klk); + tempVarj = mVectorMat.unsafe_get(j, 0); + Klk = tempVari * tempVarj / totvolume * delta[i][j]; + mat1.unsafe_set(i, j, Klk); + mat1.unsafe_set(j, i, Klk); } } boolean solved = true; - // SimpleMatrix corrMatrix = null; do { solved = true; iter++; @@ -776,87 +762,83 @@ public boolean solveX() { double temp1; double temp2; for (int i = 0; i < numberOfComponents; i++) { - temp1 = componentArray[i].getNumberOfMolesInPhase(); - for (int j = 0; j < componentArray[i].getNumberOfAssociationSites(); j++) { - ksi = ((ComponentSrkCPA) componentArray[i]).getXsite()[j]; - ksiMatrixMat.unsafe_set(temp + j, 0, ksi); - // ksiMatrix.getMatrix().unsafe_set(temp + j, 0, - // ksiMatrix.getMatrix().unsafe_get(temp + j, 0)); - tempVari = 1.0 / ksi - 1.0; - udotMatrix.set(temp + j, 0, tempVari); - udotTimesmMatrix.set(temp + j, 0, temp1 * tempVari); - } - temp += componentArray[i].getNumberOfAssociationSites(); + temp1 = componentArray[i].getNumberOfMolesInPhase(); + for (int j = 0; j < componentArray[i].getNumberOfAssociationSites(); j++) { + ksi = ((ComponentSrkCPA) componentArray[i]).getXsite()[j]; + ksiMatrixMat.unsafe_set(temp + j, 0, ksi); + // ksiMatrix.getMatrix().unsafe_set(temp + j, 0, + // ksiMatrix.getMatrix().unsafe_get(temp + j, 0)); + tempVari = 1.0 / ksi - 1.0; + udotMatrix.unsafe_set(temp + j, 0, tempVari); + udotTimesmMatrix.unsafe_set(temp + j, 0, temp1 * tempVari); + } + temp += componentArray[i].getNumberOfAssociationSites(); } int krondelt; for (int i = 0; i < getTotalNumberOfAccociationSites(); i++) { - temp1 = mVectorMat.unsafe_get(i, 0); - temp2 = ksiMatrix.get(i, 0); - for (int j = i; j < getTotalNumberOfAccociationSites(); j++) { - krondelt = 0; - if (i == j) { - krondelt = 1; - } - tempVari = -temp1 / (temp2 * temp2) * krondelt - mat1.unsafe_get(i, j); - hessianMatrix.set(i, j, tempVari); - hessianMatrix.set(j, i, tempVari); - } + temp1 = mVectorMat.unsafe_get(i, 0); + temp2 = ksiMatrix.unsafe_get(i, 0); + for (int j = i; j < getTotalNumberOfAccociationSites(); j++) { + krondelt = 0; + if (i == j) { + krondelt = 1; + } + tempVari = -temp1 / (temp2 * temp2) * krondelt - mat1.unsafe_get(i, j); + hessianMatrix.unsafe_set(i, j, tempVari); + hessianMatrix.unsafe_set(j, i, tempVari); + } } - // ksiMatrix = new SimpleMatrix(ksi); - // SimpleMatrix hessianMatrix = new SimpleMatrix(hessian); int n = totalNumberOfAccociationSites; if (hessianLU == null || hessianLUSize != n) { - hessianLU = LinearSolverFactory_DDRM.lu(n); - hessianLUinput = new DMatrixRMaj(n, n); - hessianLUSize = n; + hessianLU = LU.PRIMITIVE.make(n, n); + hessianLUinput = new DenseMatrix(n, n); + hessianLUSize = n; } // Defensive copy: EJML's LU solver may decompose in place, which would corrupt // hessianMatrix for any later reader. We keep hessianMatrix intact by feeding the // solver a reusable scratch copy. - System.arraycopy(hessianMatrix.getDDRM().getData(), 0, hessianLUinput.getData(), 0, n * n); - if (!hessianLU.setA(hessianLUinput)) { - return false; + System.arraycopy(hessianMatrix.getData(), 0, hessianLUinput.getData(), 0, n * n); + if (!LinearAlgebraOps.decomposeLu(hessianLU, n, (i, j) -> hessianLUinput.unsafe_get(i, j))) { + return false; } hessianInvers = null; if (solvedX) { - // System.out.println("solvedX "); - return true; + // System.out.println("solvedX "); + return true; } - DMatrixRMaj mat2 = ksiMatrix.getMatrix(); - CommonOps_DDRM.mult(mat1, mat2, corr2Matrix); - CommonOps_DDRM.subtract(udotTimesmMatrix.getDDRM(), corr2Matrix, corr3Matrix); - hessianLU.solve(corr3Matrix, corr4Matrix); - // SimpleMatrix gMatrix = udotTimesmMatrix.minus(KlkMatrix.mult(ksiMatrix)); + DenseMatrix mat2 = ksiMatrix; + LinearAlgebraOps.mult(mat1, mat2, corr2Matrix); + LinearAlgebraOps.subtract(udotTimesmMatrix, corr2Matrix, corr3Matrix); + LinearAlgebraOps.solveLu(hessianLU, corr3Matrix.numRows, corr3Matrix.numCols, + (i, j) -> corr3Matrix.unsafe_get(i, j), + (i, j, value) -> corr4Matrix.unsafe_set(i, j, value)); // corrMatrix = // hessianInvers.mult(udotTimesmMatrix.minus(KlkMatrix.mult(ksiMatrix))); // //.scale(-1.0); temp = 0; - // System.out.println("print SimpleMatrix ..."); // corrMatrix.print(10, 10); - // SimpleMatrix simp = new SimpleMatrix(corr4Matrix); // System.out.println("print CommonOps ..."); - // simp.print(10,10); double newX; for (int i = 0; i < numberOfComponents; i++) { - for (int j = 0; j < componentArray[i].getNumberOfAssociationSites(); j++) { - newX = ksiMatrix.get(temp + j, 0) - corr4Matrix.unsafe_get((temp + j), 0); - if (newX < 0) { - newX = 1e-10; - solved = false; - } - ((ComponentCPAInterface) componentArray[i]).setXsite(j, newX); - } - temp += componentArray[i].getNumberOfAssociationSites(); + for (int j = 0; j < componentArray[i].getNumberOfAssociationSites(); j++) { + newX = ksiMatrix.unsafe_get(temp + j, 0) - corr4Matrix.unsafe_get((temp + j), 0); + if (newX < 0) { + newX = 1e-10; + solved = false; + } + ((ComponentCPAInterface) componentArray[i]).setXsite(j, newX); + } + temp += componentArray[i].getNumberOfAssociationSites(); } // System.out.println("corrmatrix error " ); - // System.out.println("error " + NormOps_DDRM.normF(corr4Matrix)); - } while ((NormOps_DDRM.normF(corr4Matrix) > 1e-12 || !solved) && iter < 100); + // System.out.println("error " + LinearAlgebraOps.normF(corr4Matrix)); + } while ((LinearAlgebraOps.normF(corr4Matrix) > 1e-12 || !solved) && iter < 100); // System.out.println("iter " + iter + " error " + - // NormOps_DDRM.normF(corr4Matrix)); // corrMatrix.print(10, 10); + // LinearAlgebraOps.normF(corr4Matrix)); // corrMatrix.print(10, 10); // ksiMatrix.print(10, 10); return true; } @@ -898,15 +880,15 @@ public boolean solveX2(int maxIter) { iter++; err = 0.0; for (int i = 0; i < n; i++) { - final double old = xArr[i]; - final double[] deltaRow = delta[i]; - double sum = 0.0; - for (int j = 0; j < n; j++) { - sum += nMoles[j] * deltaRow[j] * xArr[j]; - } - double neeval = 1.0 / (1.0 + invV * sum); - xArr[i] = neeval; - err += Math.abs((old - neeval) / neeval); + final double old = xArr[i]; + final double[] deltaRow = delta[i]; + double sum = 0.0; + for (int j = 0; j < n; j++) { + sum += nMoles[j] * deltaRow[j] * xArr[j]; + } + double neeval = 1.0 / (1.0 + invV * sum); + xArr[i] = neeval; + err += Math.abs((old - neeval) / neeval); } } while (Math.abs(err) > 1e-12 && iter < maxIter); @@ -995,28 +977,29 @@ public double calcRootVolFinder(PhaseType pt) { int solveXAttempts = 0; while (!solveX() && solveXAttempts < 50) { - solveXAttempts++; + solveXAttempts++; } if (solveXAttempts >= 50) { - // solveX failed to converge, skip this BonV value - oldh = h; - continue; + // solveX failed to converge, skip this BonV value + oldh = h; + continue; } - h = BonV - Btemp / numberOfMolesInPhase * dFdV() - pressure * Btemp / (numberOfMolesInPhase * R * temperature); + h = BonV - Btemp / numberOfMolesInPhase * dFdV() + - pressure * Btemp / (numberOfMolesInPhase * R * temperature); if (Math.signum(h) * Math.signum(oldh) < 0 && i > 2) { - if (solvedBonVlow < 1e-3) { - solvedBonVlow = (BonV + BonVold) / 2.0; - if (pt == PhaseType.GAS) { - break; - } - } else { - solvedBonVHigh = (BonV + BonVold) / 2.0; - if (pt == PhaseType.LIQUID) { - break; - } - } + if (solvedBonVlow < 1e-3) { + solvedBonVlow = (BonV + BonVold) / 2.0; + if (pt == PhaseType.GAS) { + break; + } + } else { + solvedBonVHigh = (BonV + BonVold) / 2.0; + if (pt == PhaseType.LIQUID) { + break; + } + } } solvedBonVHigh = (BonV + BonVold) / 2.0; oldh = h; @@ -1041,9 +1024,10 @@ public double calcRootVolFinder(PhaseType pt) { /** {@inheritDoc} */ @Override public double molarVolume(double pressure, double temperature, double A, double B, PhaseType pt) - throws neqsim.util.exception.IsNaNException, neqsim.util.exception.TooManyIterationsException { + throws neqsim.util.exception.IsNaNException, + neqsim.util.exception.TooManyIterationsException { double BonV = pt == PhaseType.GAS ? pressure * getB() / (numberOfMolesInPhase * temperature * R) - : 2.0 / (2.0 + temperature / getPseudoCriticalTemperature()); + : 2.0 / (2.0 + temperature / getPseudoCriticalTemperature()); BonV = Math.max(1.0e-8, Math.min(1.0 - 1.0e-8, BonV)); double BonVold; double BonV2; @@ -1064,8 +1048,8 @@ public double molarVolume(double pressure, double temperature, double A, double iterations++; gcpa = calc_g(); if (gcpa < 0) { - setMolarVolume(1.0 / Btemp / numberOfMolesInPhase); - gcpa = calc_g(); + setMolarVolume(1.0 / Btemp / numberOfMolesInPhase); + gcpa = calc_g(); } // lngcpa = @@ -1075,74 +1059,78 @@ public double molarVolume(double pressure, double temperature, double A, double gcpavvv = calc_lngVVV(); if (totalNumberOfAccociationSites > 0) { - solveX(); + solveX(); } initCPAMatrix(1); BonV2 = BonV * BonV; BonVold = BonV; - h = BonV - Btemp / numberOfMolesInPhase * dFdV() - pressure * Btemp / (numberOfMolesInPhase * R * temperature); + h = BonV - Btemp / numberOfMolesInPhase * dFdV() + - pressure * Btemp / (numberOfMolesInPhase * R * temperature); dh = 1.0 + Btemp / (BonV2) * (Btemp / numberOfMolesInPhase * dFdVdV()); dhh = -2.0 * Btemp / (BonV2 * BonV) * (Btemp / numberOfMolesInPhase * dFdVdV()) - - (Btemp * Btemp) / (BonV2 * BonV2) * (Btemp / numberOfMolesInPhase * dFdVdVdV()); + - (Btemp * Btemp) / (BonV2 * BonV2) * (Btemp / numberOfMolesInPhase * dFdVdVdV()); d1 = -h / dh; d2 = -dh / dhh; // System.out.println("h " + h + " iter " + iterations + " " + d1 + " d2 " + d2 // + " d1 / d2 " + (d1 / d2)); if (Double.isNaN(d1) || Double.isNaN(d2)) { - return molarVolumeChangePhase(pressure, temperature, A, B, pt); + return molarVolumeChangePhase(pressure, temperature, A, B, pt); } else if (Math.abs(d1 / d2) <= 1.0) { - BonV += d1 * (1.0 + 0.5 * d1 / d2); + BonV += d1 * (1.0 + 0.5 * d1 / d2); } else if (d1 / d2 < -1) { - BonV += 0.5 * d1; + BonV += 0.5 * d1; } else if (d1 > d2) { - return molarVolumeChangePhase(pressure, temperature, A, B, pt); - // BonV += d2; - // double hnew = h + d2 * dh; - // if (Math.abs(hnew) > Math.abs(h)) { - // BonV = pt == 1 ? 2.0 / (2.0 + temperature / - // getPseudoCriticalTemperature()) : pressure * getB() / (numberOfMolesInPhase * - // temperature * R); - // } + return molarVolumeChangePhase(pressure, temperature, A, B, pt); + // BonV += d2; + // double hnew = h + d2 * dh; + // if (Math.abs(hnew) > Math.abs(h)) { + // BonV = pt == 1 ? 2.0 / (2.0 + temperature / + // getPseudoCriticalTemperature()) : pressure * getB() / (numberOfMolesInPhase * + // temperature * R); + // } } else { - BonV += 0.5 * d1; + BonV += 0.5 * d1; } if (Math.abs((BonV - BonVold) / BonV) > 0.1) { - BonV = BonVold + 0.1 * (BonV - BonVold); + BonV = BonVold + 0.1 * (BonV - BonVold); } if (Double.isNaN(BonV)) { - return molarVolumeChangePhase(pressure, temperature, A, B, pt); + return molarVolumeChangePhase(pressure, temperature, A, B, pt); } if (BonV < 0) { - if (iterations < 10) { - // System.out.println(iterations + " BonV " + BonV); - BonV = (BonVold + BonV) / 2.0; - } else { - return molarVolumeChangePhase(pressure, temperature, A, B, pt); - } + if (iterations < 10) { + // System.out.println(iterations + " BonV " + BonV); + BonV = (BonVold + BonV) / 2.0; + } else { + return molarVolumeChangePhase(pressure, temperature, A, B, pt); + } } if (BonV >= 1.0) { - if (iterations < 10) { - BonV = (BonVold + BonV) / 2.0; - } else { - return molarVolumeChangePhase(pressure, temperature, A, B, pt); - } + if (iterations < 10) { + BonV = (BonVold + BonV) / 2.0; + } else { + return molarVolumeChangePhase(pressure, temperature, A, B, pt); + } } /* - * if (BonV > 0.9999) { if (iterations < 10) { BonV = (BonVold + BonV) / 2.0; } else { // BonV = - * calcRootVolFinder(pt); // BonV = molarVolumeChangePhase(pressure, temperature, A, B, pt); // BonV = 0.9999; // - * BonV = pt == 1 ? 2.0 / (2.0 + temperature / getPseudoCriticalTemperature()) : pressure * getB() / - * (numberOfMolesInPhase * temperature * R); } } else if (BonV < 0) { if (iterations < 10) { BonV = - * Math.abs(BonVold + BonV) / 2.0; } else { // BonV = calcRootVolFinder(pt); // return - * molarVolumeChangePhase(pressure, temperature, A, B, pt); // BonV = pt == 1 ? 2.0 / (2.0 + temperature / - * getPseudoCriticalTemperature()) : pressure * getB() / (numberOfMolesInPhase * temperature * R); } } + * if (BonV > 0.9999) { if (iterations < 10) { BonV = (BonVold + BonV) / 2.0; } else { // BonV + * = calcRootVolFinder(pt); // BonV = molarVolumeChangePhase(pressure, temperature, A, B, pt); + * // BonV = 0.9999; // BonV = pt == 1 ? 2.0 / (2.0 + temperature / + * getPseudoCriticalTemperature()) : pressure * getB() / (numberOfMolesInPhase * temperature * + * R); } } else if (BonV < 0) { if (iterations < 10) { BonV = Math.abs(BonVold + BonV) / 2.0; + * } else { // BonV = calcRootVolFinder(pt); // return molarVolumeChangePhase(pressure, + * temperature, A, B, pt); // BonV = pt == 1 ? 2.0 / (2.0 + temperature / + * getPseudoCriticalTemperature()) : pressure * getB() / (numberOfMolesInPhase * temperature * + * R); } } */ setMolarVolume(1.0 / BonV * Btemp / numberOfMolesInPhase); Z = pressure * getMolarVolume() / (R * temperature); - } while ((Math.abs((BonV - BonVold) / BonV) > 1.0e-10 || Math.abs(h) > 1e-12) && iterations < maxIterations); + } while ((Math.abs((BonV - BonVold) / BonV) > 1.0e-10 || Math.abs(h) > 1e-12) + && iterations < maxIterations); // System.out.println("h failed " + h + " Z" + Z + " iterations " + iterations + // " BonV " + BonV); @@ -1187,10 +1175,11 @@ public double molarVolume(double pressure, double temperature, double A, double * @throws neqsim.util.exception.IsNaNException if any. * @throws neqsim.util.exception.TooManyIterationsException if any. */ - public double molarVolumeChangePhase(double pressure, double temperature, double A, double B, PhaseType pt) - throws neqsim.util.exception.IsNaNException, neqsim.util.exception.TooManyIterationsException { + public double molarVolumeChangePhase(double pressure, double temperature, double A, double B, + PhaseType pt) throws neqsim.util.exception.IsNaNException, + neqsim.util.exception.TooManyIterationsException { double BonV = pt == PhaseType.GAS ? 2.0 / (2.0 + temperature / getPseudoCriticalTemperature()) - : pressure * getB() / (numberOfMolesInPhase * temperature * R); + : pressure * getB() / (numberOfMolesInPhase * temperature * R); // double BonV = calcRootVolFinder(pt); // double BonVInit = BonV; if (BonV < 0) { @@ -1221,8 +1210,8 @@ public double molarVolumeChangePhase(double pressure, double temperature, double iterations++; gcpa = calc_g(); if (gcpa < 0) { - setMolarVolume(1.0 / Btemp / numberOfMolesInPhase); - gcpa = calc_g(); + setMolarVolume(1.0 / Btemp / numberOfMolesInPhase); + gcpa = calc_g(); } // lngcpa = @@ -1236,57 +1225,58 @@ public double molarVolumeChangePhase(double pressure, double temperature, double initCPAMatrix(1); double BonV2 = BonV * BonV; BonVold = BonV; - h = BonV - Btemp / numberOfMolesInPhase * dFdV() - pressure * Btemp / (numberOfMolesInPhase * R * temperature); + h = BonV - Btemp / numberOfMolesInPhase * dFdV() + - pressure * Btemp / (numberOfMolesInPhase * R * temperature); dh = 1.0 + Btemp / (BonV2) * (Btemp / numberOfMolesInPhase * dFdVdV()); dhh = -2.0 * Btemp / (BonV2 * BonV) * (Btemp / numberOfMolesInPhase * dFdVdV()) - - (Btemp * Btemp) / (BonV2 * BonV2) * (Btemp / numberOfMolesInPhase * dFdVdVdV()); + - (Btemp * Btemp) / (BonV2 * BonV2) * (Btemp / numberOfMolesInPhase * dFdVdVdV()); d1 = -h / dh; d2 = -dh / dhh; // System.out.println("d1" + d1 + " d2 " + d2 + " d1 / d2 " + (d1 / d2)); if (Double.isNaN(d1) || Double.isNaN(d2)) { - BonV = pt == PhaseType.GAS ? 2.0 / (2.0 + temperature / getPseudoCriticalTemperature()) - : pressure * getB() / (numberOfMolesInPhase * temperature * R); + BonV = pt == PhaseType.GAS ? 2.0 / (2.0 + temperature / getPseudoCriticalTemperature()) + : pressure * getB() / (numberOfMolesInPhase * temperature * R); } else if (Math.abs(d1 / d2) <= 1.0) { - BonV += d1 * (1.0 + 0.5 * d1 / d2); + BonV += d1 * (1.0 + 0.5 * d1 / d2); } else if (d1 / d2 < -1) { - BonV += 0.5 * d1; + BonV += 0.5 * d1; } else if (d1 > d2) { - BonV += d2; - double hnew = h + d2 * dh; - if (Math.abs(hnew) > Math.abs(h)) { - BonV = pt == PhaseType.GAS ? 2.0 / (2.0 + temperature / getPseudoCriticalTemperature()) - : pressure * getB() / (numberOfMolesInPhase * temperature * R); - } + BonV += d2; + double hnew = h + d2 * dh; + if (Math.abs(hnew) > Math.abs(h)) { + BonV = pt == PhaseType.GAS ? 2.0 / (2.0 + temperature / getPseudoCriticalTemperature()) + : pressure * getB() / (numberOfMolesInPhase * temperature * R); + } } else { - BonV += 0.5 * d1; + BonV += 0.5 * d1; } if (Math.abs((BonV - BonVold) / BonVold) > 0.1) { - BonV = BonVold + 0.1 * (BonV - BonVold); + BonV = BonVold + 0.1 * (BonV - BonVold); } if (Double.isNaN(BonV)) { - BonV = pt == PhaseType.GAS ? 2.0 / (2.0 + temperature / getPseudoCriticalTemperature()) - : pressure * getB() / (numberOfMolesInPhase * temperature * R); + BonV = pt == PhaseType.GAS ? 2.0 / (2.0 + temperature / getPseudoCriticalTemperature()) + : pressure * getB() / (numberOfMolesInPhase * temperature * R); } if (BonV > 1.1) { - if (iterations < 3) { - BonV = (BonVold + BonV) / 2.0; - } else { - BonV = pt == PhaseType.GAS ? 2.0 / (2.0 + temperature / getPseudoCriticalTemperature()) - : pressure * getB() / (numberOfMolesInPhase * temperature * R); - } + if (iterations < 3) { + BonV = (BonVold + BonV) / 2.0; + } else { + BonV = pt == PhaseType.GAS ? 2.0 / (2.0 + temperature / getPseudoCriticalTemperature()) + : pressure * getB() / (numberOfMolesInPhase * temperature * R); + } } if (BonV < 0) { - if (iterations < 3) { - BonV = Math.abs(BonVold + BonV) / 2.0; - } else { - BonV = pt == PhaseType.GAS ? 2.0 / (2.0 + temperature / getPseudoCriticalTemperature()) - : pressure * getB() / (numberOfMolesInPhase * temperature * R); - } + if (iterations < 3) { + BonV = Math.abs(BonVold + BonV) / 2.0; + } else { + BonV = pt == PhaseType.GAS ? 2.0 / (2.0 + temperature / getPseudoCriticalTemperature()) + : pressure * getB() / (numberOfMolesInPhase * temperature * R); + } } setMolarVolume(1.0 / BonV * Btemp / numberOfMolesInPhase); @@ -1297,8 +1287,8 @@ public double molarVolumeChangePhase(double pressure, double temperature, double } while ((Math.abs((BonV - BonVold) / BonV) > 1.0e-10) && iterations < 100); /* - * if (Math.abs(h) > 1e-8) { if (pt == 0) { molarVolume(pressure, temperature, A, B, 1); } else { - * molarVolume(pressure, temperature, A, B, 0); } return getMolarVolume(); } + * if (Math.abs(h) > 1e-8) { if (pt == 0) { molarVolume(pressure, temperature, A, B, 1); } else + * { molarVolume(pressure, temperature, A, B, 0); } return getMolarVolume(); } */ // System.out.println("Z" + Z + " iterations " + iterations + " BonV " + BonV); // System.out.println("pressure " + Z*R*temperature/getMolarVolume()); @@ -1312,7 +1302,8 @@ public double molarVolumeChangePhase(double pressure, double temperature, double // System.out.println("BonV: " + BonV + " "+" itert: " + iterations +" " +h + " " +dh + " B // " + Btemp + " gv" + gV() + " fv " + fv() + " fvv" + fVV()); if (Double.isNaN(getMolarVolume())) { - throw new neqsim.util.exception.IsNaNException(this, "molarVolumeChangePhase", "Molar volume"); + throw new neqsim.util.exception.IsNaNException(this, "molarVolumeChangePhase", + "Molar volume"); // System.out.println("BonV: " + BonV + " "+" itert: " + iterations +" " +h + " // " +dh + " B " + Btemp + " D " + Dtemp + " gv" + gV() + " fv " + fv() + " fvv" // + fVV()); @@ -1324,7 +1315,8 @@ public double molarVolumeChangePhase(double pressure, double temperature, double /** {@inheritDoc} */ @Override public double molarVolume2(double pressure, double temperature, double A, double B, PhaseType pt) - throws neqsim.util.exception.IsNaNException, neqsim.util.exception.TooManyIterationsException { + throws neqsim.util.exception.IsNaNException, + neqsim.util.exception.TooManyIterationsException { Z = pt == PhaseType.LIQUID ? 1.0 : 1.0e-5; setMolarVolume(Z * R * temperature / pressure); // super.molarVolume(pressure,temperature, A, B, phase); @@ -1347,7 +1339,8 @@ public double molarVolume2(double pressure, double temperature, double A, double // System.out.println("pressure " + -R * temperature * dFdV + " " + R * // temperature / getMolarVolume()); // -pressure; - dErrdV = -R * temperature * dFdVdV - R * temperature * numberOfMolesInPhase / Math.pow(getVolume(), 2.0); + dErrdV = -R * temperature * dFdVdV + - R * temperature * numberOfMolesInPhase / Math.pow(getVolume(), 2.0); // System.out.println("errdV " + dErrdV); // System.out.println("err " + err); @@ -1357,8 +1350,8 @@ public double molarVolume2(double pressure, double temperature, double A, double Z = pressure * getMolarVolume() / (R * temperature); if (Z < 0) { - Z = 1e-6; - setMolarVolume(Z * R * temperature / pressure); + Z = 1e-6; + setMolarVolume(Z * R * temperature / pressure); } // System.out.println("Z " + Z); } while (Math.abs(err) > 1.0e-8 || iterations < 100); @@ -1431,11 +1424,11 @@ public double[][] croeneckerProduct(double[][] a, double[][] b) { double[][] result = new double[aLength * bLength][(aCols) * (bCols)]; for (int z = 0; z < aLength; z++) { for (int i = 0; i < aCols; i++) { - for (int j = 0; j < bLength; j++) { - for (int k = 0; k < bCols; k++) { - result[j + (z * bLength)][k + (i * bCols)] = a[z][i] * b[j][k]; - } - } + for (int j = 0; j < bLength; j++) { + for (int k = 0; k < bCols; k++) { + result[j + (z * bLength)][k + (i * bCols)] = a[z][i] * b[j][k]; + } + } } } return result; @@ -1464,65 +1457,78 @@ public void setTotalNumberOfAccociationSites(int totalNumberOfAccociationSites) * @param pt the PhaseType of the phase * @param beta a double */ - public void initOld2(double totalNumberOfMoles, int numberOfComponents, int type, PhaseType pt, double beta) { + public void initOld2(double totalNumberOfMoles, int numberOfComponents, int type, PhaseType pt, + double beta) { // type = 0 start init, type = 1 gi nye betingelser if (type == 0) { setTotalNumberOfAccociationSites(0); selfAccociationScheme = new int[numberOfComponents][0][0]; crossAccociationScheme = new int[numberOfComponents][numberOfComponents][0][0]; for (int i = 0; i < numberOfComponents; i++) { - if (componentArray[i].getNumberOfmoles() < 1e-50) { - componentArray[i].setNumberOfAssociationSites(0); - } else { - componentArray[i].setNumberOfAssociationSites(componentArray[i].getOrginalNumberOfAssociationSites()); - setTotalNumberOfAccociationSites( - getTotalNumberOfAccociationSites() + componentArray[i].getNumberOfAssociationSites()); - selfAccociationScheme[i] = cpaSelect.setAssociationScheme(i, this); - for (int j = 0; j < numberOfComponents; j++) { - crossAccociationScheme[i][j] = cpaSelect.setCrossAssociationScheme(i, j, this); - } - } + if (componentArray[i].getNumberOfmoles() < 1e-50) { + componentArray[i].setNumberOfAssociationSites(0); + } else { + componentArray[i] + .setNumberOfAssociationSites(componentArray[i].getOrginalNumberOfAssociationSites()); + setTotalNumberOfAccociationSites( + getTotalNumberOfAccociationSites() + componentArray[i].getNumberOfAssociationSites()); + selfAccociationScheme[i] = cpaSelect.setAssociationScheme(i, this); + for (int j = 0; j < numberOfComponents; j++) { + crossAccociationScheme[i][j] = cpaSelect.setCrossAssociationScheme(i, j, this); + } + } } // had to remove if below - dont understand why.. Even // if (getTotalNumberOfAccociationSites() != oldTotalNumberOfAccociationSites) { - mVector = new SimpleMatrix(getTotalNumberOfAccociationSites(), 1); - KlkMatrix = new SimpleMatrix(getTotalNumberOfAccociationSites(), getTotalNumberOfAccociationSites()); - KlkVMatrix = new SimpleMatrix(getTotalNumberOfAccociationSites(), getTotalNumberOfAccociationSites()); - KlkVVMatrix = new SimpleMatrix(getTotalNumberOfAccociationSites(), getTotalNumberOfAccociationSites()); - KlkVVVMatrix = new SimpleMatrix(getTotalNumberOfAccociationSites(), getTotalNumberOfAccociationSites()); - hessianMatrix = new SimpleMatrix(getTotalNumberOfAccociationSites(), getTotalNumberOfAccociationSites()); - KlkTMatrix = new SimpleMatrix(getTotalNumberOfAccociationSites(), getTotalNumberOfAccociationSites()); - KlkTTMatrix = new SimpleMatrix(getTotalNumberOfAccociationSites(), getTotalNumberOfAccociationSites()); - KlkTVMatrix = new SimpleMatrix(getTotalNumberOfAccociationSites(), getTotalNumberOfAccociationSites()); - corr2Matrix = new DMatrixRMaj(getTotalNumberOfAccociationSites(), 1); - corr3Matrix = new DMatrixRMaj(getTotalNumberOfAccociationSites(), 1); - corr4Matrix = new DMatrixRMaj(getTotalNumberOfAccociationSites(), 1); - Klkni = new double[numberOfComponents][getTotalNumberOfAccociationSites()][getTotalNumberOfAccociationSites()]; - ksiMatrix = new SimpleMatrix(getTotalNumberOfAccociationSites(), 1); - uMatrix = new SimpleMatrix(getTotalNumberOfAccociationSites(), 1); - udotMatrix = new SimpleMatrix(getTotalNumberOfAccociationSites(), 1); + mVector = new DenseMatrix(getTotalNumberOfAccociationSites(), 1); + KlkMatrix = + new DenseMatrix(getTotalNumberOfAccociationSites(), getTotalNumberOfAccociationSites()); + KlkVMatrix = + new DenseMatrix(getTotalNumberOfAccociationSites(), getTotalNumberOfAccociationSites()); + KlkVVMatrix = + new DenseMatrix(getTotalNumberOfAccociationSites(), getTotalNumberOfAccociationSites()); + KlkVVVMatrix = + new DenseMatrix(getTotalNumberOfAccociationSites(), getTotalNumberOfAccociationSites()); + hessianMatrix = + new DenseMatrix(getTotalNumberOfAccociationSites(), getTotalNumberOfAccociationSites()); + KlkTMatrix = + new DenseMatrix(getTotalNumberOfAccociationSites(), getTotalNumberOfAccociationSites()); + KlkTTMatrix = + new DenseMatrix(getTotalNumberOfAccociationSites(), getTotalNumberOfAccociationSites()); + KlkTVMatrix = + new DenseMatrix(getTotalNumberOfAccociationSites(), getTotalNumberOfAccociationSites()); + corr2Matrix = new DenseMatrix(getTotalNumberOfAccociationSites(), 1); + corr3Matrix = new DenseMatrix(getTotalNumberOfAccociationSites(), 1); + corr4Matrix = new DenseMatrix(getTotalNumberOfAccociationSites(), 1); + Klkni = + new double[numberOfComponents][getTotalNumberOfAccociationSites()][getTotalNumberOfAccociationSites()]; + ksiMatrix = new DenseMatrix(getTotalNumberOfAccociationSites(), 1); + uMatrix = new DenseMatrix(getTotalNumberOfAccociationSites(), 1); + udotMatrix = new DenseMatrix(getTotalNumberOfAccociationSites(), 1); moleculeNumber = new int[getTotalNumberOfAccociationSites()]; assSiteNumber = new int[getTotalNumberOfAccociationSites()]; gvector = new double[getTotalNumberOfAccociationSites()][1]; - udotTimesmMatrix = new SimpleMatrix(getTotalNumberOfAccociationSites(), 1); + udotTimesmMatrix = new DenseMatrix(getTotalNumberOfAccociationSites(), 1); delta = new double[getTotalNumberOfAccociationSites()][getTotalNumberOfAccociationSites()]; deltaNog = new double[getTotalNumberOfAccociationSites()][getTotalNumberOfAccociationSites()]; deltadT = new double[getTotalNumberOfAccociationSites()][getTotalNumberOfAccociationSites()]; - deltadTdT = new double[getTotalNumberOfAccociationSites()][getTotalNumberOfAccociationSites()]; - QMatksiksiksi = new SimpleMatrix(getTotalNumberOfAccociationSites(), 1); + deltadTdT = + new double[getTotalNumberOfAccociationSites()][getTotalNumberOfAccociationSites()]; + QMatksiksiksi = new DenseMatrix(getTotalNumberOfAccociationSites(), 1); // } - udotTimesmiMatrix = new SimpleMatrix(getNumberOfComponents(), getTotalNumberOfAccociationSites()); + udotTimesmiMatrix = + new DenseMatrix(getNumberOfComponents(), getTotalNumberOfAccociationSites()); oldTotalNumberOfAccociationSites = getTotalNumberOfAccociationSites(); int temp = 0; for (int i = 0; i < numberOfComponents; i++) { - for (int j = 0; j < componentArray[i].getNumberOfAssociationSites(); j++) { - moleculeNumber[temp + j] = i; - assSiteNumber[temp + j] = j; - } - temp += componentArray[i].getNumberOfAssociationSites(); + for (int j = 0; j < componentArray[i].getNumberOfAssociationSites(); j++) { + moleculeNumber[temp + j] = i; + assSiteNumber[temp + j] = j; + } + temp += componentArray[i].getNumberOfAssociationSites(); } } if (cpamix == null) { @@ -1561,231 +1567,7 @@ public void initOld2(double totalNumberOfMoles, int numberOfComponents, int type * @param type a int */ public void initCPAMatrixOld(int type) { - if (getTotalNumberOfAccociationSites() == 0) { - FCPA = 0.0; - dFCPAdTdV = 0.0; - dFCPAdTdT = 0.0; - dFCPAdT = 0; - dFCPAdV = 0; - dFCPAdVdV = 0.0; - dFCPAdVdVdV = 0.0; - - return; - } - - int temp = 0; - for (int i = 0; i < numberOfComponents; i++) { - for (int j = 0; j < componentArray[i].getNumberOfAssociationSites(); j++) { - uMatrix.set(temp + j, 0, Math.log(ksiMatrix.get(temp + j, 0)) - ksiMatrix.get(temp + j, 0) + 1.0); - gvector[temp + j][0] = mVector.get(temp + j, 0) * udotMatrix.get(temp + j, 0); - } - temp += componentArray[i].getNumberOfAssociationSites(); - } - for (int i = 0; i < getNumberOfComponents(); i++) { - for (int j = 0; j < getTotalNumberOfAccociationSites(); j++) { - if (moleculeNumber[j] == i) { - udotTimesmiMatrix.set(i, j, udotMatrix.get(j, 0)); - } else { - udotTimesmiMatrix.set(i, j, 0.0); - } - } - } - - for (int i = 0; i < getTotalNumberOfAccociationSites(); i++) { - for (int j = i; j < getTotalNumberOfAccociationSites(); j++) { - delta[i][j] = deltaNog[i][j] * getGcpa(); - delta[j][i] = delta[i][j]; - if (type > 1) { - deltadT[i][j] = cpamix.calcDeltadT(assSiteNumber[i], assSiteNumber[j], moleculeNumber[i], moleculeNumber[j], - this, getTemperature(), getPressure(), numberOfComponents); - deltadT[j][i] = deltadT[i][j]; - - deltadTdT[i][j] = cpamix.calcDeltadTdT(assSiteNumber[i], assSiteNumber[j], moleculeNumber[i], - moleculeNumber[j], this, getTemperature(), getPressure(), numberOfComponents); - deltadTdT[j][i] = deltadTdT[i][j]; - } - } - } - - double totalVolume = getTotalVolume(); - double totalVolume2 = totalVolume * totalVolume; - double totalVolume3 = totalVolume2 * totalVolume; - double gdv1 = getGcpav() - 1.0 / totalVolume; - double gdv2 = gdv1 * gdv1; - double gdv3 = gdv2 * gdv1; - // double Klk = 0.0; - for (int i = 0; i < getTotalNumberOfAccociationSites(); i++) { - for (int j = i; j < getTotalNumberOfAccociationSites(); j++) { - KlkVMatrix.set(i, j, KlkMatrix.get(i, j) * gdv1); - KlkVMatrix.set(j, i, KlkVMatrix.get(i, j)); - - KlkVVMatrix.set(i, j, - KlkMatrix.get(i, j) * gdv2 + KlkMatrix.get(i, j) * (gcpavv + 1.0 / totalVolume / totalVolume)); - KlkVVMatrix.set(j, i, KlkVVMatrix.get(i, j)); - - KlkVVVMatrix.set(i, j, - KlkMatrix.get(i, j) * gdv3 - + 3.0 * KlkMatrix.get(i, j) * (getGcpav() - 1.0 / totalVolume) * (gcpavv + 1.0 / (totalVolume2)) - + KlkMatrix.get(i, j) * (gcpavvv - 2.0 / (totalVolume3))); - KlkVVVMatrix.set(j, i, KlkVVVMatrix.get(i, j)); - - if (type > 1) { - double tempVar = deltadT[i][j] / delta[i][j]; - double tempVardT = deltadTdT[i][j] / delta[i][j] - - (deltadT[i][j] * deltadT[i][j]) / (delta[i][j] * delta[i][j]); - - if (!Double.isNaN(tempVar)) { - // KlkdT[i][j] = KlkMatrix.getMatrix().unsafe_get(i, j) * tempVar; - // KlkdT[j][i] = KlkdT[i][j]; - - KlkTMatrix.set(i, j, KlkMatrix.get(i, j) * tempVar); - KlkTMatrix.set(j, i, KlkTMatrix.get(i, j)); - - KlkTVMatrix.set(i, j, KlkMatrix.get(i, j) * tempVar * (gcpav - 1.0 / totalVolume)); - KlkTVMatrix.set(j, i, KlkTVMatrix.get(i, j)); - - KlkTTMatrix.set(i, j, KlkMatrix.get(i, j) * (tempVar * tempVar + tempVardT)); - KlkTTMatrix.set(j, i, KlkTTMatrix.get(i, j)); - } - - if (type > 2) { - for (int p = 0; p < numberOfComponents; p++) { - double t1 = 0.0; - double t2 = 0.0; - if (moleculeNumber[i] == p) { - t1 = 1.0 / mVector.get(i, 0); - } - if (moleculeNumber[j] == p) { - t2 = 1.0 / mVector.get(j, 0); - } - Klkni[p][i][j] = KlkMatrix.get(i, j) * (t1 + t2 + ((ComponentSrkCPA) getComponent(p)).calc_lngi(this)); - Klkni[p][j][i] = Klkni[p][i][j]; - } - } - } - } - QMatksiksiksi.set(i, 0, - 2.0 * mVector.get(i, 0) / (ksiMatrix.get(i, 0) * ksiMatrix.get(i, 0) * ksiMatrix.get(i, 0))); - } - - SimpleMatrix ksiMatrixTranspose = ksiMatrix.transpose(); - - // dXdV - SimpleMatrix KlkVMatrixksi = KlkVMatrix.mult(ksiMatrix); - SimpleMatrix XV = hessianInvers.mult(KlkVMatrixksi); - SimpleMatrix XVtranspose = XV.transpose(); - - SimpleMatrix QCPA = mVector.transpose().mult(uMatrix.minus(ksiMatrix.elementMult(udotMatrix).scale(0.5))); - FCPA = QCPA.get(0, 0); - - SimpleMatrix tempMatrix = ksiMatrixTranspose.mult(KlkVMatrixksi).scale(-0.5); - dFCPAdV = tempMatrix.get(0, 0); - SimpleMatrix KlkVVMatrixTImesKsi = KlkVVMatrix.mult(ksiMatrix); - SimpleMatrix tempMatrixVV = ksiMatrixTranspose.mult(KlkVVMatrixTImesKsi).scale(-0.5) - .minus(KlkVMatrixksi.transpose().mult(XV)); - dFCPAdVdV = tempMatrixVV.get(0, 0); - - SimpleMatrix QVVV = ksiMatrixTranspose.mult(KlkVVVMatrix.mult(ksiMatrix)).scale(-0.5); - SimpleMatrix QVVksi = KlkVVMatrixTImesKsi.scale(-1.0); - SimpleMatrix QksiVksi = KlkVMatrix.scale(-1.0); - - SimpleMatrix mat1 = QVVksi.transpose().mult(XV).scale(3.0); - SimpleMatrix mat2 = XVtranspose.mult(QksiVksi.mult(XV)).scale(3.0); - SimpleMatrix mat4 = XVtranspose.mult(QMatksiksiksi.mult(XVtranspose)).mult(XV); - - SimpleMatrix dFCPAdVdVdVMatrix = QVVV.plus(mat1).plus(mat2).plus(mat2).plus(mat4); - dFCPAdVdVdV = dFCPAdVdVdVMatrix.get(0, 0); - temp = 0; - - if (type == 1) { - return; - } - for (int p = 0; p < numberOfComponents; p++) { - for (int kk = 0; kk < getComponent(p).getNumberOfAssociationSites(); kk++) { - ((ComponentCPAInterface) getComponent(p)).setXsitedV(kk, XV.get(temp + kk, 0)); - } - temp += getComponent(p).getNumberOfAssociationSites(); - } - - // KlkTMatrix = new SimpleMatrix(KlkdT); - SimpleMatrix KlkTMatrixTImesKsi = KlkTMatrix.mult(ksiMatrix); - // dQdT - SimpleMatrix tempMatrix2 = ksiMatrixTranspose.mult(KlkTMatrixTImesKsi).scale(-0.5); - dFCPAdT = tempMatrix2.get(0, 0); - - // SimpleMatrix KlkTVMatrix = new SimpleMatrix(KlkdTdV); - // SimpleMatrix tempMatrixTV = - // ksiMatrixTranspose.mult(KlkTVMatrix.mult(ksiMatrix)).scale(-0.5).minus(KlkTMatrixTImesKsi.transpose().mult(XV)); - // dFCPAdTdV = tempMatrixTV.get(0, 0); - // dXdT - SimpleMatrix XT = hessianInvers.mult(KlkTMatrixTImesKsi); - // dQdTdT - SimpleMatrix tempMatrixTT = ksiMatrixTranspose.mult(KlkTTMatrix.mult(ksiMatrix)).scale(-0.5) - .minus(KlkTMatrixTImesKsi.transpose().mult(XT)); - dFCPAdTdT = tempMatrixTT.get(0, 0); - - SimpleMatrix tempMatrixTV = ksiMatrixTranspose.mult(KlkTVMatrix.mult(ksiMatrix)).scale(-0.5) - .minus(KlkTMatrixTImesKsi.transpose().mult(XV)); - dFCPAdTdV = tempMatrixTV.get(0, 0); - - temp = 0; - for (int p = 0; p < numberOfComponents; p++) { - for (int kk = 0; kk < getComponent(p).getNumberOfAssociationSites(); kk++) { - ((ComponentCPAInterface) getComponent(p)).setXsitedT(kk, XT.get(temp + kk, 0)); - } - temp += getComponent(p).getNumberOfAssociationSites(); - } - - if (type == 2) { - return; - } - - // int assSites = 0; - // if(true) return; - for (int p = 0; p < numberOfComponents; p++) { - SimpleMatrix KiMatrix = new SimpleMatrix(Klkni[p]); - // KiMatrix.print(10,10); - // Matrix dQdniMatrix = - // (ksiMatrix.transpose().times(KiMatrix.times(ksiMatrix)).times(-0.5)); // this - // methods misses one part of .... - // dQdniMatrix.print(10,10); - // KiMatrix.print(10, 10); - // miMatrix.getMatrix(assSites, assSites, 0, totalNumberOfAccociationSites - - // 1).print(10, 10); - // Matrix tempMatrix20 = miMatrix.getMatrix(assSites, assSites, 0, - // totalNumberOfAccociationSites - - // 1).times(uMatrix).minus(ksiMatrix.transpose().times(KiMatrix.times(ksiMatrix)).times(-0.5)); - // - // ksiMatrix.transpose().times(KlkTMatrix.times(ksiMatrix)).times(-0.5); - // System.out.println("dQdn "); - // tempMatrix20.print(10, 10); - SimpleMatrix tempMatrix4 = KiMatrix.mult(ksiMatrix); - // udotTimesmiMatrix.getMatrix(assSites, assSites, 0, - // totalNumberOfAccociationSites - 1).print(10, 10); - SimpleMatrix tempMatrix5 = udotTimesmiMatrix.extractVector(true, p).transpose().minus(tempMatrix4); - // tempMki[0] = mki[p]; - // Matrix amatrix = new Matrix(croeneckerProduct(tempMki, - // udotMatrix.getArray())); - // System.out.println("aMatrix "); - // amatrix.transpose().print(10, 10); - // System.out.println("temp4 matrix"); - // tempMatrix4.print(10, 10); - // Matrix tempMatrix5 = amatrix.minus(tempMatrix4); - SimpleMatrix tempMatrix6 = hessianInvers.mult(tempMatrix5); // .scale(-1.0); - // System.out.println("dXdni"); - // tempMatrix4.print(10, 10); - // tempMatrix5.print(10, 10); - // System.out.println("dXdn "); - // tempMatrix6.print(10, 10); - int temp2 = 0; - for (int compp = 0; compp < numberOfComponents; compp++) { - for (int kk = 0; kk < getComponent(compp).getNumberOfAssociationSites(); kk++) { - ((ComponentCPAInterface) getComponent(compp)).setXsitedni(kk, p, -1.0 * tempMatrix6.get(temp2 + kk, 0)); - } - temp2 += getComponent(compp).getNumberOfAssociationSites(); - } - // assSites += getComponent(p).getNumberOfAssociationSites(); - } + initCPAMatrix(type); } /** @@ -1796,106 +1578,7 @@ public void initCPAMatrixOld(int type) { * @return a boolean */ public boolean solveXOld() { - if (getTotalNumberOfAccociationSites() == 0) { - return true; - } - - boolean solvedX = solveX2(5); - if (solvedX) { - // return true; - } - - DMatrixRMaj mat1 = KlkMatrix.getMatrix(); - DMatrixRMaj mat2 = ksiMatrix.getMatrix(); - // second order method not working correctly and not used t the moment b ecause of numerical - // stability - int temp = 0; - int iter = 0; - for (int i = 0; i < numberOfComponents; i++) { - for (int j = 0; j < componentArray[i].getNumberOfAssociationSites(); j++) { - mVector.set(temp + j, 0, componentArray[i].getNumberOfMolesInPhase()); - } - temp += componentArray[i].getNumberOfAssociationSites(); - } - double Klk = 0.0; - double totalVolume = getTotalVolume(); - for (int i = 0; i < getTotalNumberOfAccociationSites(); i++) { - for (int j = i; j < getTotalNumberOfAccociationSites(); j++) { - Klk = mVector.get(i, 0) * mVector.get(j, 0) / totalVolume * delta[i][j]; - KlkMatrix.set(i, j, Klk); - KlkMatrix.set(j, i, Klk); - } - } - boolean solved = true; - // SimpleMatrix corrMatrix = null; - do { - solved = true; - iter++; - temp = 0; - for (int i = 0; i < numberOfComponents; i++) { - for (int j = 0; j < componentArray[i].getNumberOfAssociationSites(); j++) { - ksiMatrix.set(temp + j, 0, ((ComponentSrkCPA) componentArray[i]).getXsite()[j]); - // ksiMatrix.getMatrix().unsafe_set(temp + j, 0, - // ksiMatrix.getMatrix().unsafe_get(temp + j, 0)); - udotMatrix.set(temp + j, 0, 1.0 / ksiMatrix.get(temp + j, 0) - 1.0); - udotTimesmMatrix.set(temp + j, 0, mVector.get(temp + j, 0) * udotMatrix.get(temp + j, 0)); - } - temp += componentArray[i].getNumberOfAssociationSites(); - } - - for (int i = 0; i < getTotalNumberOfAccociationSites(); i++) { - for (int j = i; j < getTotalNumberOfAccociationSites(); j++) { - int krondelt = 0; - if (i == j) { - krondelt = 1; - } - hessianMatrix.set(i, j, - -mVector.get(i, 0) / (ksiMatrix.get(i, 0) * ksiMatrix.get(i, 0)) * krondelt - KlkMatrix.get(i, j)); - hessianMatrix.set(j, i, hessianMatrix.get(i, j)); - } - } - - // ksiMatrix = new SimpleMatrix(ksi); - // SimpleMatrix hessianMatrix = new SimpleMatrix(hessian); - try { - hessianInvers = hessianMatrix.invert(); - } catch (Exception ex) { - logger.error(ex.getMessage(), ex); - return false; - } - - CommonOps_DDRM.mult(mat1, mat2, corr2Matrix); - CommonOps_DDRM.subtract(udotTimesmMatrix.getDDRM(), corr2Matrix, corr3Matrix); - CommonOps_DDRM.mult(hessianInvers.getDDRM(), corr3Matrix, corr4Matrix); - // SimpleMatrix gMatrix = udotTimesmMatrix.minus(KlkMatrix.mult(ksiMatrix)); - // corrMatrix = - // hessianInvers.mult(udotTimesmMatrix.minus(KlkMatrix.mult(ksiMatrix))); - // //.scale(-1.0); - temp = 0; - // System.out.println("print SimpleMatrix ..."); - // corrMatrix.print(10, 10); - // SimpleMatrix simp = new SimpleMatrix(corr4Matrix); - // System.out.println("print CommonOps ..."); - // simp.print(10,10); - for (int i = 0; i < numberOfComponents; i++) { - for (int j = 0; j < componentArray[i].getNumberOfAssociationSites(); j++) { - double newX = ksiMatrix.get(temp + j, 0) - corr4Matrix.unsafe_get((temp + j), 0); - if (newX < 0) { - newX = 1e-10; - solved = false; - } - ((ComponentCPAInterface) componentArray[i]).setXsite(j, newX); - } - temp += componentArray[i].getNumberOfAssociationSites(); - } - // System.out.println("corrmatrix error " ); - // System.out.println("error " + corrMatrix.norm1()); - } while ((NormOps_DDRM.normF(corr4Matrix) > 1e-12 || !solved) && iter < 100); - - // System.out.println("iter " + iter + " error " + NormOps.normF(corr4Matrix)); - // // corrMatrix.print(10, 10); - // ksiMatrix.print(10, 10); - return true; + return solveX(); } /** @@ -1907,37 +1590,7 @@ public boolean solveXOld() { * @return a boolean */ public boolean solveX2Old(int maxIter) { - double err = .0; - int iter = 0; - // if (delta == null) { - // initCPAMatrix(1); - double old = 0.0; - double neeval = 0.0; - double totalVolume = getTotalVolume(); - // } - do { - iter++; - err = 0.0; - for (int i = 0; i < getTotalNumberOfAccociationSites(); i++) { - old = ((ComponentSrkCPA) getComponent(moleculeNumber[i])).getXsite()[assSiteNumber[i]]; - neeval = 0; - for (int j = 0; j < getTotalNumberOfAccociationSites(); j++) { - neeval += getComponent(moleculeNumber[j]).getNumberOfMolesInPhase() * delta[i][j] - * ((ComponentSrkCPA) getComponent(moleculeNumber[j])).getXsite()[assSiteNumber[j]]; - } - neeval = 1.0 / (1.0 + 1.0 / totalVolume * neeval); - ((ComponentCPAInterface) getComponent(moleculeNumber[i])).setXsite(assSiteNumber[i], neeval); - err += Math.abs((old - neeval) / neeval); - } - } while (Math.abs(err) > 1e-10 && iter < maxIter); - // System.out.println("iter " + iter); - // if (Math.abs(err) - // < 1e-12) { - // return true; - // } else { - // System.out.println("did not solve for Xi in iterations: " + iter); - // System.out.println("error: " + err); - return false; + return solveX2(maxIter); } /** @@ -1954,10 +1607,12 @@ public boolean solveX2Old(int maxIter) { * @throws neqsim.util.exception.IsNaNException if any. * @throws neqsim.util.exception.TooManyIterationsException if any. */ - public double molarVolumeOld(double pressure, double temperature, double A, double B, PhaseType pt) - throws neqsim.util.exception.IsNaNException, neqsim.util.exception.TooManyIterationsException { - double BonV = pt == PhaseType.LIQUID ? 2.0 / (2.0 + temperature / getPseudoCriticalTemperature()) - : pressure * getB() / (numberOfMolesInPhase * temperature * R); + public double molarVolumeOld(double pressure, double temperature, double A, double B, + PhaseType pt) throws neqsim.util.exception.IsNaNException, + neqsim.util.exception.TooManyIterationsException { + double BonV = + pt == PhaseType.LIQUID ? 2.0 / (2.0 + temperature / getPseudoCriticalTemperature()) + : pressure * getB() / (numberOfMolesInPhase * temperature * R); // if (pressure > 1000) { // BonV = 0.9999; // } @@ -1991,8 +1646,8 @@ public double molarVolumeOld(double pressure, double temperature, double A, doub iterations++; gcpa = calc_g(); if (gcpa < 0) { - setMolarVolume(1.0 / Btemp / numberOfMolesInPhase); - gcpa = calc_g(); + setMolarVolume(1.0 / Btemp / numberOfMolesInPhase); + gcpa = calc_g(); } // lngcpa = @@ -2002,62 +1657,64 @@ public double molarVolumeOld(double pressure, double temperature, double A, doub gcpavvv = calc_lngVVV(); if (getTotalNumberOfAccociationSites() > 0) { - solveX(); + solveX(); } initCPAMatrix(1); double BonV2 = BonV * BonV; BonVold = BonV; - h = BonV - Btemp / numberOfMolesInPhase * dFdV() - pressure * Btemp / (numberOfMolesInPhase * R * temperature); + h = BonV - Btemp / numberOfMolesInPhase * dFdV() + - pressure * Btemp / (numberOfMolesInPhase * R * temperature); dh = 1.0 + Btemp / (BonV2) * (Btemp / numberOfMolesInPhase * dFdVdV()); dhh = -2.0 * Btemp / (BonV2 * BonV) * (Btemp / numberOfMolesInPhase * dFdVdV()) - - (Btemp * Btemp) / (BonV2 * BonV2) * (Btemp / numberOfMolesInPhase * dFdVdVdV()); + - (Btemp * Btemp) / (BonV2 * BonV2) * (Btemp / numberOfMolesInPhase * dFdVdVdV()); d1 = -h / dh; d2 = -dh / dhh; // System.out.println("h " + h + " iter " + iterations + " " + d1 + " d2 " + d2 // + " d1 / d2 " + (d1 / d2)); if (Math.abs(d1 / d2) <= 1.0) { - BonV += d1 * (1.0 + 0.5 * d1 / d2); + BonV += d1 * (1.0 + 0.5 * d1 / d2); } else if (d1 / d2 < -1) { - BonV += 0.5 * d1; + BonV += 0.5 * d1; } else if (d1 > d2) { - BonV += d2; - double hnew = h + d2 * dh; - if (Math.abs(hnew) > Math.abs(h)) { - BonV = pt == PhaseType.GAS ? 2.0 / (2.0 + temperature / getPseudoCriticalTemperature()) - : pressure * getB() / (numberOfMolesInPhase * temperature * R); - } + BonV += d2; + double hnew = h + d2 * dh; + if (Math.abs(hnew) > Math.abs(h)) { + BonV = pt == PhaseType.GAS ? 2.0 / (2.0 + temperature / getPseudoCriticalTemperature()) + : pressure * getB() / (numberOfMolesInPhase * temperature * R); + } } else { - BonV += 0.5 * d1; + BonV += 0.5 * d1; } if (Math.abs((BonV - BonVold) / BonVold) > 0.1) { - BonV = BonVold + 0.1 * (BonV - BonVold); + BonV = BonVold + 0.1 * (BonV - BonVold); } if (BonV > 0.9999) { - if (iterations < 3) { - BonV = (BonVold + BonV) / 2.0; - } else { - // return molarVolumeChangePhase(pressure, temperature, A, B, pt); - // BonV = 0.9999; - // BonV = pt == 1 ? 2.0 / (2.0 + temperature / - // getPseudoCriticalTemperature()) : pressure * getB() / (numberOfMolesInPhase * - // temperature * R); - } + if (iterations < 3) { + BonV = (BonVold + BonV) / 2.0; + } else { + // return molarVolumeChangePhase(pressure, temperature, A, B, pt); + // BonV = 0.9999; + // BonV = pt == 1 ? 2.0 / (2.0 + temperature / + // getPseudoCriticalTemperature()) : pressure * getB() / (numberOfMolesInPhase * + // temperature * R); + } } else if (BonV < 0) { - if (iterations < 3) { - BonV = Math.abs(BonVold + BonV) / 2.0; - } else { - // return molarVolumeChangePhase(pressure, temperature, A, B, pt); - // BonV = pt == 1 ? 2.0 / (2.0 + temperature / - // getPseudoCriticalTemperature()) : pressure * getB() / (numberOfMolesInPhase * - // temperature * R); - } + if (iterations < 3) { + BonV = Math.abs(BonVold + BonV) / 2.0; + } else { + // return molarVolumeChangePhase(pressure, temperature, A, B, pt); + // BonV = pt == 1 ? 2.0 / (2.0 + temperature / + // getPseudoCriticalTemperature()) : pressure * getB() / (numberOfMolesInPhase * + // temperature * R); + } } setMolarVolume(1.0 / BonV * Btemp / numberOfMolesInPhase); Z = pressure * getMolarVolume() / (R * temperature); - } while ((Math.abs((BonV - BonVold) / BonV) > 1.0e-10 || Math.abs(h) > 1e-12) && iterations < 100); + } while ((Math.abs((BonV - BonVold) / BonV) > 1.0e-10 || Math.abs(h) > 1e-12) + && iterations < 100); if (Math.abs(h) > 1e-12) { // System.out.println("h failed " + "Z" + Z + " iterations " + iterations + " @@ -2084,4 +1741,121 @@ public double molarVolumeOld(double pressure, double temperature, double A, doub } return getMolarVolume(); } + + /** + * Returns the transpose of a dense matrix. + * + * @param matrix input matrix + * @return transposed matrix + */ + private static DenseMatrix transpose(DenseMatrix matrix) { + DenseMatrix out = new DenseMatrix(matrix.numCols, matrix.numRows); + for (int i = 0; i < matrix.numRows; i++) { + for (int j = 0; j < matrix.numCols; j++) { + out.unsafe_set(j, i, matrix.unsafe_get(i, j)); + } + } + return out; + } + + /** + * Multiplies two dense matrices. + * + * @param left left matrix + * @param right right matrix + * @return matrix product + */ + private static DenseMatrix multiply(DenseMatrix left, DenseMatrix right) { + DenseMatrix out = new DenseMatrix(left.numRows, right.numCols); + LinearAlgebraOps.mult(left, right, out); + return out; + } + + /** + * Adds two dense matrices. + * + * @param left left matrix + * @param right right matrix + * @return sum matrix + */ + private static DenseMatrix add(DenseMatrix left, DenseMatrix right) { + DenseMatrix out = new DenseMatrix(left.numRows, left.numCols); + for (int i = 0; i < left.numRows; i++) { + for (int j = 0; j < left.numCols; j++) { + out.unsafe_set(i, j, left.unsafe_get(i, j) + right.unsafe_get(i, j)); + } + } + return out; + } + + /** + * Subtracts two dense matrices. + * + * @param left left matrix + * @param right right matrix + * @return difference matrix + */ + private static DenseMatrix subtract(DenseMatrix left, DenseMatrix right) { + DenseMatrix out = new DenseMatrix(left.numRows, left.numCols); + LinearAlgebraOps.subtract(left, right, out); + return out; + } + + /** + * Scales all elements in a dense matrix. + * + * @param matrix input matrix + * @param factor scale factor + * @return scaled matrix + */ + private static DenseMatrix scale(DenseMatrix matrix, double factor) { + DenseMatrix out = new DenseMatrix(matrix.numRows, matrix.numCols); + for (int i = 0; i < matrix.numRows; i++) { + for (int j = 0; j < matrix.numCols; j++) { + out.unsafe_set(i, j, matrix.unsafe_get(i, j) * factor); + } + } + return out; + } + + /** + * Element-wise multiplication of two dense matrices. + * + * @param left left matrix + * @param right right matrix + * @return Hadamard product + */ + private static DenseMatrix elementMult(DenseMatrix left, DenseMatrix right) { + DenseMatrix out = new DenseMatrix(left.numRows, left.numCols); + for (int i = 0; i < left.numRows; i++) { + for (int j = 0; j < left.numCols; j++) { + out.unsafe_set(i, j, left.unsafe_get(i, j) * right.unsafe_get(i, j)); + } + } + return out; + } + + /** + * Extracts one row or column as a matrix view copy. + * + * @param matrix source matrix + * @param extractRow true to extract a row, false to extract a column + * @param index row/column index + * @return extracted row/column matrix + */ + private static DenseMatrix extractVector(DenseMatrix matrix, boolean extractRow, int index) { + if (extractRow) { + DenseMatrix row = new DenseMatrix(1, matrix.numCols); + for (int j = 0; j < matrix.numCols; j++) { + row.unsafe_set(0, j, matrix.unsafe_get(index, j)); + } + return row; + } + DenseMatrix col = new DenseMatrix(matrix.numRows, 1); + for (int i = 0; i < matrix.numRows; i++) { + col.unsafe_set(i, 0, matrix.unsafe_get(i, index)); + } + return col; + } + } diff --git a/src/main/java/neqsim/thermo/phase/PhaseSrkCPAandersonReduced.java b/src/main/java/neqsim/thermo/phase/PhaseSrkCPAandersonReduced.java index 15826b03e8..72c87df056 100644 --- a/src/main/java/neqsim/thermo/phase/PhaseSrkCPAandersonReduced.java +++ b/src/main/java/neqsim/thermo/phase/PhaseSrkCPAandersonReduced.java @@ -4,6 +4,7 @@ import org.apache.logging.log4j.Logger; import neqsim.thermo.component.ComponentCPAInterface; import neqsim.thermo.component.ComponentSrkCPA; +import neqsim.util.math.LinearAlgebraOps; /** * Anderson-accelerated nested CPA phase solver with site symmetry reduction. @@ -12,21 +13,22 @@ * Combines two orthogonal acceleration strategies: *

*
    - *
  1. Site symmetry reduction: groups equivalent association sites (same bonding pattern on the same component) - * into unique site types, reducing the inner loop dimension from n_s to p.
  2. - *
  3. Anderson acceleration: replaces successive substitution with Anderson mixing (depth m=3) on the reduced - * p-dimensional site fraction vector, achieving superlinear convergence.
  4. + *
  5. Site symmetry reduction: groups equivalent association sites (same bonding pattern on + * the same component) into unique site types, reducing the inner loop dimension from n_s to p.
  6. + *
  7. Anderson acceleration: replaces successive substitution with Anderson mixing (depth + * m=3) on the reduced p-dimensional site fraction vector, achieving superlinear convergence.
  8. *
* *

- * The outer Halley iteration for molar volume and the volume derivative computation ({@code initCPAMatrix(1)}) also use - * the reduced dimension p, reducing the Hessian linear system from O(n_s^3) to O(p^3). + * The outer Halley iteration for molar volume and the volume derivative computation + * ({@code initCPAMatrix(1)}) also use the reduced dimension p, reducing the Hessian linear system + * from O(n_s^3) to O(p^3). *

* *

- * This is a nested-family solver: site fractions are fully converged at each volume step before computing exact - * volume derivatives via the implicit function theorem. It therefore avoids the coupled-family equilibrium sensitivity - * documented for the Broyden and fully implicit solvers. + * This is a nested-family solver: site fractions are fully converged at each volume step + * before computing exact volume derivatives via the implicit function theorem. It therefore avoids + * the coupled-family equilibrium sensitivity documented for the Broyden and fully implicit solvers. *

* *

@@ -133,8 +135,8 @@ public class PhaseSrkCPAandersonReduced extends PhaseSrkCPAs { private transient int workOuterNs = 0; /** - * When true, {@code initCPAMatrix(1)} skips the {@code updateDeltaWithG(ns)} call. Set by the Halley outer loop where - * delta has just been refreshed; cleared on exit. + * When true, {@code initCPAMatrix(1)} skips the {@code updateDeltaWithG(ns)} call. Set by the + * Halley outer loop where delta has just been refreshed; cleared on exit. */ private transient boolean skipDeltaUpdateInInitCPA = false; @@ -170,9 +172,10 @@ public static String getProfileSummary() { double avgOuter = callCount > 0 ? (double) totalOuterIters / callCount : 0; double avgInner = totalOuterIters > 0 ? (double) totalInnerIters / totalOuterIters : 0; return String.format( - "Calls=%d AvgOuterIters=%.1f AvgInnerIters=%.1f AndersonConverged=%d " - + "NewtonFallback=%d NumTypes(last)=%d", - callCount, avgOuter, avgInner, andersonConvergedCount, newtonFallbackCount, callCount > 0 ? 0 : -1); + "Calls=%d AvgOuterIters=%.1f AvgInnerIters=%.1f AndersonConverged=%d " + + "NewtonFallback=%d NumTypes(last)=%d", + callCount, avgOuter, avgInner, andersonConvergedCount, newtonFallbackCount, + callCount > 0 ? 0 : -1); } /** @@ -264,14 +267,15 @@ public void addComponent(String name, double moles, double molesInPhase, int com * {@inheritDoc} * *

- * Molar volume calculation using the Halley outer loop for volume and Anderson-accelerated successive substitution on - * reduced site type fractions. The Halley step uses the implicit function theorem to compute dF_CPA/dV - * derivatives in the reduced p-dimensional space. + * Molar volume calculation using the Halley outer loop for volume and Anderson-accelerated + * successive substitution on reduced site type fractions. The Halley step uses the + * implicit function theorem to compute dF_CPA/dV derivatives in the reduced p-dimensional space. *

*/ @Override public double molarVolume(double pressure, double temperature, double A, double B, PhaseType pt) - throws neqsim.util.exception.IsNaNException, neqsim.util.exception.TooManyIterationsException { + throws neqsim.util.exception.IsNaNException, + neqsim.util.exception.TooManyIterationsException { int ns = getTotalNumberOfAccociationSites(); @@ -354,20 +358,20 @@ public double molarVolume(double pressure, double temperature, double A, double for (int t = 0; t < p; t++) { double val = xSiteFull[typeRepSite[t]]; if (val <= 1.0e-15 || val >= 1.0 || Double.isNaN(val)) { - coldStart = true; - break; + coldStart = true; + break; } xType[t] = val; } if (coldStart) { for (int t = 0; t < p; t++) { - xType[t] = 0.5; + xType[t] = 0.5; } expandAndSetSiteFractions(xType, ns); solveX2(10); readXsiteFromComponents(xSiteFull, ns); for (int t = 0; t < p; t++) { - xType[t] = xSiteFull[typeRepSite[t]]; + xType[t] = xSiteFull[typeRepSite[t]]; } } expandAndSetSiteFractions(xType, ns); @@ -392,8 +396,8 @@ public double molarVolume(double pressure, double temperature, double A, double // Update g-function and association strengths gcpa = calc_g(); if (gcpa < 0) { - setMolarVolume(Btemp / numberOfMolesInPhase); - gcpa = calc_g(); + setMolarVolume(Btemp / numberOfMolesInPhase); + gcpa = calc_g(); } gcpav = calc_lngV(); gcpavv = calc_lngVV(); @@ -411,37 +415,38 @@ public double molarVolume(double pressure, double temperature, double A, double // duplicate call inside initCPAMatrix(1). skipDeltaUpdateInInitCPA = true; try { - initCPAMatrix(1); + initCPAMatrix(1); } finally { - skipDeltaUpdateInInitCPA = false; + skipDeltaUpdateInInitCPA = false; } // --- Outer step: Halley iteration for volume --- double h = BonV - Btemp / numberOfMolesInPhase * dFdV() - - pressure * Btemp / (numberOfMolesInPhase * R * temperature); + - pressure * Btemp / (numberOfMolesInPhase * R * temperature); double dh = 1.0 + Btemp / (BonV * BonV) * (Btemp / numberOfMolesInPhase * dFdVdV()); double dhh = -2.0 * Btemp / (BonV * BonV * BonV) * (Btemp / numberOfMolesInPhase * dFdVdV()) - + Btemp * Btemp * Btemp / (BonV * BonV * BonV * BonV) * (1.0 / numberOfMolesInPhase * dFdVdVdV()); + + Btemp * Btemp * Btemp / (BonV * BonV * BonV * BonV) + * (1.0 / numberOfMolesInPhase * dFdVdVdV()); double dBonV = -h / dh; // Halley correction double halleyCorrection = 1.0 - 0.5 * dBonV * dhh / dh; if (Math.abs(halleyCorrection) > 0.1) { - dBonV = dBonV / halleyCorrection; + dBonV = dBonV / halleyCorrection; } // Step limiting if (Math.abs(dBonV) > 0.1 * BonV) { - dBonV = Math.signum(dBonV) * 0.1 * BonV; + dBonV = Math.signum(dBonV) * 0.1 * BonV; } BonV += dBonV; BonV = Math.max(1.0e-10, Math.min(1.0 - 1.0e-10, BonV)); if (Math.abs((BonV - BonVold) / BonV) < OUTER_TOL && outer > 2) { - converged = true; - break; + converged = true; + break; } } @@ -489,8 +494,8 @@ public double molarVolume(double pressure, double temperature, double A, double * * *

- * Anderson acceleration (mixing depth m=3) is applied to this reduced map. After convergence, the p type fractions - * are expanded back to all n_s individual site fractions. + * Anderson acceleration (mixing depth m=3) is applied to this reduced map. After convergence, the + * p type fractions are expanded back to all n_s individual site fractions. *

* * @param p number of unique site types @@ -513,7 +518,7 @@ private int solveXAndersonReduced(int p, double[] tMoles, int ns) { for (int t = 0; t < p; t++) { xCurr[t] = xSiteFull[typeRepSite[t]]; if (xCurr[t] <= 1.0e-15 || xCurr[t] >= 1.0 || Double.isNaN(xCurr[t])) { - xCurr[t] = 0.5; + xCurr[t] = 0.5; } } @@ -521,7 +526,7 @@ private int solveXAndersonReduced(int p, double[] tMoles, int ns) { double[][] klk = workInnerKlk; for (int a = 0; a < p; a++) { for (int b = 0; b < p; b++) { - klk[a][b] = typeMult[b] * tMoles[b] * delta[typeRepSite[a]][typeRepSite[b]] * invV; + klk[a][b] = typeMult[b] * tMoles[b] * delta[typeRepSite[a]][typeRepSite[b]] * invV; } } @@ -544,55 +549,55 @@ private int solveXAndersonReduced(int p, double[] tMoles, int ns) { // One SS step on reduced types: compute f(x) for (int a = 0; a < p; a++) { - double sumAB = 0.0; - for (int b = 0; b < p; b++) { - sumAB += klk[a][b] * xCurr[b]; - } - fX[a] = 1.0 / (1.0 + sumAB); + double sumAB = 0.0; + for (int b = 0; b < p; b++) { + sumAB += klk[a][b] * xCurr[b]; + } + fX[a] = 1.0 / (1.0 + sumAB); } // Residual: g = f(x) - x double maxG = 0.0; for (int a = 0; a < p; a++) { - gCurr[a] = fX[a] - xCurr[a]; - maxG = Math.max(maxG, Math.abs(gCurr[a])); + gCurr[a] = fX[a] - xCurr[a]; + maxG = Math.max(maxG, Math.abs(gCurr[a])); } if (maxG < INNER_TOL) { - andersonConvergedCount++; - expandAndSetSiteFractions(xCurr, ns); - return iterations; + andersonConvergedCount++; + expandAndSetSiteFractions(xCurr, ns); + return iterations; } // Anderson mixing double[] xNew; if (hasPrev) { - // Store history (circular buffer) - int slot = histLen < m ? histLen : (histLen % m); - for (int a = 0; a < p; a++) { - gHist[slot][a] = gCurr[a] - gPrev[a]; - xHist[slot][a] = xCurr[a] - xPrev[a]; - } - if (histLen < m) { - histLen++; - } - - // Solve least-squares: min ||g_curr - G * gamma||^2 - double[] gamma = solveAndersonLeastSquares(gHist, gCurr, p, histLen); - - // x_{k+1} = (x_curr + g_curr) - sum_i gamma_i * (xHist[i] + gHist[i]) - for (int a = 0; a < p; a++) { - xNewBuf[a] = xCurr[a] + gCurr[a]; - for (int i = 0; i < histLen; i++) { - xNewBuf[a] -= gamma[i] * (xHist[i][a] + gHist[i][a]); - } - // Clamp to valid range - xNewBuf[a] = Math.max(1.0e-15, Math.min(1.0, xNewBuf[a])); - } - xNew = xNewBuf; + // Store history (circular buffer) + int slot = histLen < m ? histLen : (histLen % m); + for (int a = 0; a < p; a++) { + gHist[slot][a] = gCurr[a] - gPrev[a]; + xHist[slot][a] = xCurr[a] - xPrev[a]; + } + if (histLen < m) { + histLen++; + } + + // Solve least-squares: min ||g_curr - G * gamma||^2 + double[] gamma = solveAndersonLeastSquares(gHist, gCurr, p, histLen); + + // x_{k+1} = (x_curr + g_curr) - sum_i gamma_i * (xHist[i] + gHist[i]) + for (int a = 0; a < p; a++) { + xNewBuf[a] = xCurr[a] + gCurr[a]; + for (int i = 0; i < histLen; i++) { + xNewBuf[a] -= gamma[i] * (xHist[i][a] + gHist[i][a]); + } + // Clamp to valid range + xNewBuf[a] = Math.max(1.0e-15, Math.min(1.0, xNewBuf[a])); + } + xNew = xNewBuf; } else { - // First iteration: plain SS step - xNew = fX; + // First iteration: plain SS step + xNew = fX; } // Save current as previous @@ -616,8 +621,8 @@ private int solveXAndersonReduced(int p, double[] tMoles, int ns) { * Solve the Anderson least-squares problem: min ||g - G * gamma||^2. * *

- * Uses the normal equations: (G^T G) gamma = G^T g. The system is at most m x m (typically 3x3) so a direct solve via - * Gaussian elimination is efficient and stable. + * Uses the normal equations: (G^T G) gamma = G^T g. The system is at most m x m (typically 3x3) + * so a direct solve via Gaussian elimination is efficient and stable. *

* * @param gMatrix history of residual differences (m x p, using rows 0..histLen-1) @@ -626,17 +631,18 @@ private int solveXAndersonReduced(int p, double[] tMoles, int ns) { * @param histLen number of stored history vectors * @return mixing coefficients gamma (length histLen) */ - private static double[] solveAndersonLeastSquares(double[][] gMatrix, double[] gVec, int p, int histLen) { + private static double[] solveAndersonLeastSquares(double[][] gMatrix, double[] gVec, int p, + int histLen) { // Build G^T G (histLen x histLen) double[][] gtg = new double[histLen][histLen]; for (int i = 0; i < histLen; i++) { for (int j = i; j < histLen; j++) { - double dot = 0.0; - for (int k = 0; k < p; k++) { - dot += gMatrix[i][k] * gMatrix[j][k]; - } - gtg[i][j] = dot; - gtg[j][i] = dot; + double dot = 0.0; + for (int k = 0; k < p; k++) { + dot += gMatrix[i][k] * gMatrix[j][k]; + } + gtg[i][j] = dot; + gtg[j][i] = dot; } } @@ -645,7 +651,7 @@ private static double[] solveAndersonLeastSquares(double[][] gMatrix, double[] g for (int i = 0; i < histLen; i++) { double dot = 0.0; for (int k = 0; k < p; k++) { - dot += gMatrix[i][k] * gVec[k]; + dot += gMatrix[i][k] * gVec[k]; } gtgVec[i] = dot; } @@ -660,30 +666,30 @@ private static double[] solveAndersonLeastSquares(double[][] gMatrix, double[] g int maxRow = col; double maxVal = Math.abs(gtg[col][col]); for (int row = col + 1; row < histLen; row++) { - double val = Math.abs(gtg[row][col]); - if (val > maxVal) { - maxVal = val; - maxRow = row; - } + double val = Math.abs(gtg[row][col]); + if (val > maxVal) { + maxVal = val; + maxRow = row; + } } if (maxVal < 1.0e-30) { - return new double[histLen]; + return new double[histLen]; } if (maxRow != col) { - double[] tempRow = gtg[col]; - gtg[col] = gtg[maxRow]; - gtg[maxRow] = tempRow; - double tempB = gtgVec[col]; - gtgVec[col] = gtgVec[maxRow]; - gtgVec[maxRow] = tempB; + double[] tempRow = gtg[col]; + gtg[col] = gtg[maxRow]; + gtg[maxRow] = tempRow; + double tempB = gtgVec[col]; + gtgVec[col] = gtgVec[maxRow]; + gtgVec[maxRow] = tempB; } double pivot = gtg[col][col]; for (int row = col + 1; row < histLen; row++) { - double factor = gtg[row][col] / pivot; - for (int k = col + 1; k < histLen; k++) { - gtg[row][k] -= factor * gtg[col][k]; - } - gtgVec[row] -= factor * gtgVec[col]; + double factor = gtg[row][col] / pivot; + for (int k = col + 1; k < histLen; k++) { + gtg[row][k] -= factor * gtg[col][k]; + } + gtgVec[row] -= factor * gtgVec[col]; } } // Back substitution @@ -691,7 +697,7 @@ private static double[] solveAndersonLeastSquares(double[][] gMatrix, double[] g for (int row = histLen - 1; row >= 0; row--) { double sum = gtgVec[row]; for (int k = row + 1; k < histLen; k++) { - sum -= gtg[row][k] * gamma[k]; + sum -= gtg[row][k] * gamma[k]; } gamma[row] = sum / gtg[row][row]; } @@ -707,8 +713,9 @@ private static double[] solveAndersonLeastSquares(double[][] gMatrix, double[] g * Build the site type map by grouping equivalent association sites. * *

- * Two individual sites are equivalent if they belong to the same component and have identical deltaNog rows (same - * bonding pattern to all other sites). This corresponds to sites with the same charge in the CPA association scheme. + * Two individual sites are equivalent if they belong to the same component and have identical + * deltaNog rows (same bonding pattern to all other sites). This corresponds to sites with the + * same charge in the CPA association scheme. *

* * @param ns total number of individual association sites @@ -723,30 +730,30 @@ private void buildSiteTypeMap(int ns) { for (int i = 0; i < ns; i++) { boolean matched = false; for (int t = 0; t < numTypes; t++) { - if (moleculeNumber[i] != moleculeNumber[typeRepSite[t]]) { - continue; - } - // Same component — check if deltaNog rows are identical - boolean identical = true; - for (int j = 0; j < ns; j++) { - if (Math.abs(deltaNog[i][j] - deltaNog[typeRepSite[t]][j]) > 1.0e-30) { - identical = false; - break; - } - } - if (identical) { - siteToType[i] = t; - typeMult[t]++; - matched = true; - break; - } + if (moleculeNumber[i] != moleculeNumber[typeRepSite[t]]) { + continue; + } + // Same component — check if deltaNog rows are identical + boolean identical = true; + for (int j = 0; j < ns; j++) { + if (Math.abs(deltaNog[i][j] - deltaNog[typeRepSite[t]][j]) > 1.0e-30) { + identical = false; + break; + } + } + if (identical) { + siteToType[i] = t; + typeMult[t]++; + matched = true; + break; + } } if (!matched) { - typeRepSite[numTypes] = i; - siteToType[i] = numTypes; - typeMult[numTypes] = 1; - typeCompIdx[numTypes] = moleculeNumber[i]; - numTypes++; + typeRepSite[numTypes] = i; + siteToType[i] = numTypes; + typeMult[numTypes] = 1; + typeCompIdx[numTypes] = moleculeNumber[i]; + numTypes++; } } } @@ -761,9 +768,9 @@ private void expandAndSetSiteFractions(double[] xType, int ns) { int idx = 0; for (int i = 0; i < numberOfComponents; i++) { for (int j = 0; j < componentArray[i].getNumberOfAssociationSites(); j++) { - int typeIdx = siteToType[idx]; - ((ComponentCPAInterface) componentArray[i]).setXsite(j, xType[typeIdx]); - idx++; + int typeIdx = siteToType[idx]; + ((ComponentCPAInterface) componentArray[i]).setXsite(j, xType[typeIdx]); + idx++; } } } @@ -778,8 +785,8 @@ private void readXsiteFromComponents(double[] xSite, int ns) { int idx = 0; for (int i = 0; i < numberOfComponents; i++) { for (int j = 0; j < componentArray[i].getNumberOfAssociationSites(); j++) { - xSite[idx] = ((ComponentSrkCPA) componentArray[i]).getXsite()[j]; - idx++; + xSite[idx] = ((ComponentSrkCPA) componentArray[i]).getXsite()[j]; + idx++; } } } @@ -809,9 +816,9 @@ private void ensureWorkArrays(int p) { * {@inheritDoc} * *

- * Override type 1 initialization to use reduced-dimension site type computation. Volume derivatives (FCPA, dFCPAdV, - * dFCPAdVdV, dFCPAdVdVdV) are computed using the type grouping, reducing the Hessian linear system from n_s to p - * dimensions. + * Override type 1 initialization to use reduced-dimension site type computation. Volume + * derivatives (FCPA, dFCPAdV, dFCPAdVdV, dFCPAdVdVdV) are computed using the type grouping, + * reducing the Hessian linear system from n_s to p dimensions. *

*/ @Override @@ -857,10 +864,12 @@ public void initCPAMatrix(int type) { double gv = getGcpav(); double fV = gv - 1.0 / totalVolume; double fVV = fV * fV + gcpavv + 1.0 / totalVolume2; - double fVVV = fV * fV * fV + 3.0 * fV * (gcpavv + 1.0 / totalVolume2) + gcpavvv - 2.0 / totalVolume3; + double fVVV = + fV * fV * fV + 3.0 * fV * (gcpavv + 1.0 / totalVolume2) + gcpavvv - 2.0 / totalVolume3; // Read reduced site fractions and moles (reuse cached buffer if available) - double[] xSiteFull = (workXSiteFull != null && workXSiteFull.length == ns) ? workXSiteFull : new double[ns]; + double[] xSiteFull = + (workXSiteFull != null && workXSiteFull.length == ns) ? workXSiteFull : new double[ns]; readXsiteFromComponents(xSiteFull, ns); for (int t = 0; t < p; t++) { workKsi[t] = xSiteFull[typeRepSite[t]]; @@ -872,9 +881,9 @@ public void initCPAMatrix(int type) { for (int a = 0; a < p; a++) { double maInvV = typeMult[a] * workM[a] * invV; for (int b = a; b < p; b++) { - double k = maInvV * typeMult[b] * workM[b] * delta[typeRepSite[a]][typeRepSite[b]]; - workKlk[a][b] = k; - workKlk[b][a] = k; + double k = maInvV * typeMult[b] * workM[b] * delta[typeRepSite[a]][typeRepSite[b]]; + workKlk[a][b] = k; + workKlk[b][a] = k; } } @@ -882,7 +891,7 @@ public void initCPAMatrix(int type) { for (int a = 0; a < p; a++) { double s = 0.0; for (int b = 0; b < p; b++) { - s += workKlk[a][b] * workKsi[b]; + s += workKlk[a][b] * workKsi[b]; } workKlkKsi[a] = s; } @@ -890,7 +899,7 @@ public void initCPAMatrix(int type) { // Build reduced Hessian for XV linear system for (int a = 0; a < p; a++) { for (int b = 0; b < p; b++) { - workHess[a][b] = -workKlk[a][b]; + workHess[a][b] = -workKlk[a][b]; } workHess[a][a] -= typeMult[a] * workM[a] / (workKsi[a] * workKsi[a]); } @@ -899,7 +908,7 @@ public void initCPAMatrix(int type) { for (int a = 0; a < p; a++) { workXV[a] = fV * workKlkKsi[a]; } - solveLinearSystem(workHess, workXV, p); + LinearAlgebraOps.solveLinearSystemInPlace(workHess, workXV, p); // Compute scalar quantities double dotKsiKlkKsi = 0.0; @@ -919,7 +928,7 @@ public void initCPAMatrix(int type) { for (int a = 0; a < p; a++) { double s = 0.0; for (int b = 0; b < p; b++) { - s += workKlk[a][b] * workXV[b]; + s += workKlk[a][b] * workXV[b]; } dotXVKlkXV += workXV[a] * s; } @@ -931,59 +940,8 @@ public void initCPAMatrix(int type) { sumXV2 += typeMult[a] * workXV[a] * workXV[a]; } - dFCPAdVdVdV = -0.5 * fVVV * dotKsiKlkKsi - 3.0 * fVV * dotKlkKsiXV - 3.0 * fV * dotXVKlkXV + sumQXV * sumXV2; - } - - // =========================================================================== - // Linear algebra utilities - // =========================================================================== - - /** - * Solve the linear system A*x = b in-place (b overwritten with solution) using Gaussian elimination with partial - * pivoting. - * - * @param a coefficient matrix (modified in-place) - * @param b right-hand side vector (overwritten with solution) - * @param n system dimension - */ - private static void solveLinearSystem(double[][] a, double[] b, int n) { - for (int col = 0; col < n; col++) { - int maxRow = col; - double maxVal = Math.abs(a[col][col]); - for (int row = col + 1; row < n; row++) { - double val = Math.abs(a[row][col]); - if (val > maxVal) { - maxVal = val; - maxRow = row; - } - } - if (maxRow != col) { - double[] tmpRow = a[col]; - a[col] = a[maxRow]; - a[maxRow] = tmpRow; - double tmpVal = b[col]; - b[col] = b[maxRow]; - b[maxRow] = tmpVal; - } - double pivot = a[col][col]; - if (Math.abs(pivot) < 1.0e-30) { - continue; - } - for (int row = col + 1; row < n; row++) { - double factor = a[row][col] / pivot; - for (int k = col + 1; k < n; k++) { - a[row][k] -= factor * a[col][k]; - } - b[row] -= factor * b[col]; - } - } - for (int row = n - 1; row >= 0; row--) { - double s = b[row]; - for (int k = row + 1; k < n; k++) { - s -= a[row][k] * b[k]; - } - b[row] = s / a[row][row]; - } + dFCPAdVdVdV = -0.5 * fVVV * dotKsiKlkKsi - 3.0 * fVV * dotKlkKsiXV - 3.0 * fV * dotXVKlkXV + + sumQXV * sumXV2; } // --- Helper methods --- @@ -996,16 +954,17 @@ private static void solveLinearSystem(double[][] a, double[] b, int n) { private void updateDeltaWithG(int ns) { for (int i = 0; i < ns; i++) { for (int j = i; j < ns; j++) { - delta[i][j] = deltaNog[i][j] * gcpa; - delta[j][i] = delta[i][j]; + delta[i][j] = deltaNog[i][j] * gcpa; + delta[j][i] = delta[i][j]; } } } /** {@inheritDoc} */ @Override - public double molarVolumeChangePhase(double pressure, double temperature, double A, double B, PhaseType pt) - throws neqsim.util.exception.IsNaNException, neqsim.util.exception.TooManyIterationsException { + public double molarVolumeChangePhase(double pressure, double temperature, double A, double B, + PhaseType pt) throws neqsim.util.exception.IsNaNException, + neqsim.util.exception.TooManyIterationsException { return super.molarVolumeChangePhase(pressure, temperature, A, B, pt); } } diff --git a/src/main/java/neqsim/thermo/phase/PhaseSrkCPAreduced.java b/src/main/java/neqsim/thermo/phase/PhaseSrkCPAreduced.java index 0ac24954da..024005a668 100644 --- a/src/main/java/neqsim/thermo/phase/PhaseSrkCPAreduced.java +++ b/src/main/java/neqsim/thermo/phase/PhaseSrkCPAreduced.java @@ -2,20 +2,22 @@ import neqsim.thermo.component.ComponentCPAInterface; import neqsim.thermo.component.ComponentSrkCPA; +import neqsim.util.math.LinearAlgebraOps; /** * CPA phase class with site symmetry reduction and Broyden quasi-Newton acceleration. * *

- * Reduces the coupled (n_s + 1)-dimensional Newton system to (p + 1) dimensions, where p is the number of unique - * association site types. Sites on the same component with identical bonding patterns (same delta row in the - * association matrix) are grouped into a single "type" with a multiplicity factor. This exploits the mathematical - * theorem that equivalent sites converge to equal site fractions at equilibrium. + * Reduces the coupled (n_s + 1)-dimensional Newton system to (p + 1) dimensions, where p is the + * number of unique association site types. Sites on the same component with identical bonding + * patterns (same delta row in the association matrix) are grouped into a single "type" with a + * multiplicity factor. This exploits the mathematical theorem that equivalent sites converge to + * equal site fractions at equilibrium. *

* *

- * Combined with Broyden rank-1 updates of the inverse Jacobian after initial convergence, this yields compounded - * speedup from both dimension reduction and reduced per-iteration cost. + * Combined with Broyden rank-1 updates of the inverse Jacobian after initial convergence, this + * yields compounded speedup from both dimension reduction and reduced per-iteration cost. *

* *

@@ -117,8 +119,7 @@ public class PhaseSrkCPAreduced extends PhaseSrkCPAs { /** * Construct a PhaseSrkCPAreduced phase. */ - public PhaseSrkCPAreduced() { - } + public PhaseSrkCPAreduced() {} /** {@inheritDoc} */ @Override @@ -155,14 +156,15 @@ public void addComponent(String name, double moles, double molesInPhase, int com * {@inheritDoc} * *

- * Reduced-dimension coupled Newton/Broyden molar volume solver. Exploits association site symmetry to work in (p+1) - * dimensions where p is the number of unique site types, combined with Broyden rank-1 inverse-Jacobian updates after - * the initial Newton phase. + * Reduced-dimension coupled Newton/Broyden molar volume solver. Exploits association site + * symmetry to work in (p+1) dimensions where p is the number of unique site types, combined with + * Broyden rank-1 inverse-Jacobian updates after the initial Newton phase. *

*/ @Override public double molarVolume(double pressure, double temperature, double A, double B, PhaseType pt) - throws neqsim.util.exception.IsNaNException, neqsim.util.exception.TooManyIterationsException { + throws neqsim.util.exception.IsNaNException, + neqsim.util.exception.TooManyIterationsException { int ns = getTotalNumberOfAccociationSites(); @@ -225,20 +227,20 @@ public double molarVolume(double pressure, double temperature, double A, double for (int t = 0; t < p; t++) { double val = xSiteFull[typeRepSite[t]]; if (val <= 1.0e-15 || val >= 1.0 || Double.isNaN(val)) { - needsInit = true; - break; + needsInit = true; + break; } xType[t] = val; } if (needsInit) { for (int t = 0; t < p; t++) { - xType[t] = 0.5; + xType[t] = 0.5; } expandAndSetSiteFractions(xType, ns); solveX2(10); readXsiteFromComponents(xSiteFull, ns); for (int t = 0; t < p; t++) { - xType[t] = xSiteFull[typeRepSite[t]]; + xType[t] = xSiteFull[typeRepSite[t]]; } } @@ -276,11 +278,11 @@ public double molarVolume(double pressure, double temperature, double A, double // --- Update g-function and delta --- gcpa = calc_g(); if (gcpa < 0) { - setMolarVolume(Btemp / numberOfMolesInPhase); - gcpa = calc_g(); - totalVol = getMolarVolume() * numberOfMolesInPhase; - zeta = Btemp / (numberOfMolesInPhase * getMolarVolume()); - molarVol = getMolarVolume(); + setMolarVolume(Btemp / numberOfMolesInPhase); + gcpa = calc_g(); + totalVol = getMolarVolume() * numberOfMolesInPhase; + zeta = Btemp / (numberOfMolesInPhase * getMolarVolume()); + molarVol = getMolarVolume(); } gcpav = calc_lngV(); gcpavv = calc_lngVV(); @@ -294,15 +296,15 @@ public double molarVolume(double pressure, double temperature, double A, double double sumFV = 0.0; double sumFVV = 0.0; for (int a = 0; a < p; a++) { - for (int b = a; b < p; b++) { - double dab = delta[typeRepSite[a]][typeRepSite[b]]; - double kRed = tMoles[a] * tMoles[b] / totalVol * dab * typeMult[a] * typeMult[b]; - double xaxb = xType[a] * xType[b]; - double kx = kRed * xaxb; - double sym = (a == b) ? 1.0 : 2.0; - sumFV += sym * kx * gdv1; - sumFVV += sym * kx * (gdv1 * gdv1 + gcpavv + 1.0 / totalVol2); - } + for (int b = a; b < p; b++) { + double dab = delta[typeRepSite[a]][typeRepSite[b]]; + double kRed = tMoles[a] * tMoles[b] / totalVol * dab * typeMult[a] * typeMult[b]; + double xaxb = xType[a] * xType[b]; + double kx = kRed * xaxb; + double sym = (a == b) ? 1.0 : 2.0; + sumFV += sym * kx * gdv1; + sumFVV += sym * kx * (gdv1 * gdv1 + gcpavv + 1.0 / totalVol2); + } } dFCPAdV = -0.5 * sumFV; dFCPAdVdV = -0.5 * sumFVV; @@ -310,145 +312,147 @@ public double molarVolume(double pressure, double temperature, double A, double // --- Build reduced residual --- double[] redSum = new double[p]; for (int a = 0; a < p; a++) { - double s = 0.0; - for (int b = 0; b < p; b++) { - s += typeMult[b] * tMoles[b] * delta[typeRepSite[a]][typeRepSite[b]] * xType[b]; - } - redSum[a] = s; - residual[a] = xType[a] - 1.0 / (1.0 + redSum[a] / totalVol); + double s = 0.0; + for (int b = 0; b < p; b++) { + s += typeMult[b] * tMoles[b] * delta[typeRepSite[a]][typeRepSite[b]] * xType[b]; + } + redSum[a] = s; + residual[a] = xType[a] - 1.0 / (1.0 + redSum[a] / totalVol); } double h = zeta - Btemp / numberOfMolesInPhase * dFdV() - - pressure * Btemp / (numberOfMolesInPhase * R * temperature); + - pressure * Btemp / (numberOfMolesInPhase * R * temperature); residual[p] = h; // --- Check convergence --- double maxResidual = 0.0; for (int i = 0; i < dim; i++) { - maxResidual = Math.max(maxResidual, Math.abs(residual[i])); + maxResidual = Math.max(maxResidual, Math.abs(residual[i])); } if (maxResidual < CONVERGENCE_TOL && iterations > 1) { - converged = true; - break; + converged = true; + break; } // --- Newton or Broyden step --- if (!useBroyden || iterations <= MIN_NEWTON_STEPS || maxResidual > BROYDEN_SWITCH_TOL) { - buildReducedJacobian(jacobian, xType, tMoles, redSum, totalVol, gdv1, zeta, Btemp, dim, p); - boolean ok = invertMatrix(jacobian, invJac, dim); - if (!ok) { - fallbackCount++; - return super.molarVolume(pressure, temperature, A, B, pt); - } - jacobianEvals++; - if (maxResidual <= BROYDEN_SWITCH_TOL && iterations > MIN_NEWTON_STEPS) { - useBroyden = true; - } - stallCount = 0; + buildReducedJacobian(jacobian, xType, tMoles, redSum, totalVol, gdv1, zeta, Btemp, dim, p); + boolean ok = invertMatrix(jacobian, invJac, dim); + if (!ok) { + fallbackCount++; + return super.molarVolume(pressure, temperature, A, B, pt); + } + jacobianEvals++; + if (maxResidual <= BROYDEN_SWITCH_TOL && iterations > MIN_NEWTON_STEPS) { + useBroyden = true; + } + stallCount = 0; } else { - // Broyden rank-1 update - for (int i = 0; i < dim; i++) { - df[i] = residual[i] - residualOld[i]; - } - double[] dxPrev = new double[dim]; - for (int t = 0; t < p; t++) { - dxPrev[t] = xType[t] - xOld[t]; - } - dxPrev[p] = zeta - xOld[p]; - - broydenUpdate(invJac, dxPrev, df, dim); - broydenUpdates++; - - if (maxResidual > prevResNorm) { - buildReducedJacobian(jacobian, xType, tMoles, redSum, totalVol, gdv1, zeta, Btemp, dim, p); - boolean ok = invertMatrix(jacobian, invJac, dim); - if (!ok) { - fallbackCount++; - return super.molarVolume(pressure, temperature, A, B, pt); - } - jacobianEvals++; - stallCount = 0; - } else if (maxResidual > prevResNorm * STALL_RATIO) { - stallCount++; - if (stallCount >= 2) { - buildReducedJacobian(jacobian, xType, tMoles, redSum, totalVol, gdv1, zeta, Btemp, dim, p); - boolean ok = invertMatrix(jacobian, invJac, dim); - if (!ok) { - fallbackCount++; - return super.molarVolume(pressure, temperature, A, B, pt); - } - jacobianEvals++; - stallCount = 0; - } - } else { - stallCount = 0; - } + // Broyden rank-1 update + for (int i = 0; i < dim; i++) { + df[i] = residual[i] - residualOld[i]; + } + double[] dxPrev = new double[dim]; + for (int t = 0; t < p; t++) { + dxPrev[t] = xType[t] - xOld[t]; + } + dxPrev[p] = zeta - xOld[p]; + + broydenUpdate(invJac, dxPrev, df, dim); + broydenUpdates++; + + if (maxResidual > prevResNorm) { + buildReducedJacobian(jacobian, xType, tMoles, redSum, totalVol, gdv1, zeta, Btemp, dim, + p); + boolean ok = invertMatrix(jacobian, invJac, dim); + if (!ok) { + fallbackCount++; + return super.molarVolume(pressure, temperature, A, B, pt); + } + jacobianEvals++; + stallCount = 0; + } else if (maxResidual > prevResNorm * STALL_RATIO) { + stallCount++; + if (stallCount >= 2) { + buildReducedJacobian(jacobian, xType, tMoles, redSum, totalVol, gdv1, zeta, Btemp, dim, + p); + boolean ok = invertMatrix(jacobian, invJac, dim); + if (!ok) { + fallbackCount++; + return super.molarVolume(pressure, temperature, A, B, pt); + } + jacobianEvals++; + stallCount = 0; + } + } else { + stallCount = 0; + } } prevResNorm = maxResidual; // Save state for Broyden update for (int t = 0; t < p; t++) { - xOld[t] = xType[t]; + xOld[t] = xType[t]; } xOld[p] = zeta; System.arraycopy(residual, 0, residualOld, 0, dim); // --- Compute step: dx = -H * R --- for (int i = 0; i < dim; i++) { - double s = 0.0; - for (int j = 0; j < dim; j++) { - s += invJac[i][j] * residual[j]; - } - dx[i] = -s; + double s = 0.0; + for (int j = 0; j < dim; j++) { + s += invJac[i][j] * residual[j]; + } + dx[i] = -s; } // --- Step limiting --- double maxStep = 1.0; for (int t = 0; t < p; t++) { - double proposed = xType[t] + maxStep * dx[t]; - if (proposed < 1.0e-15) { - double limit = MAX_REL_STEP * xType[t] / Math.abs(dx[t]); - maxStep = Math.min(maxStep, limit); - } - if (proposed > 1.0) { - double limit = (1.0 - xType[t]) / dx[t]; - maxStep = Math.min(maxStep, Math.max(0.1, limit)); - } + double proposed = xType[t] + maxStep * dx[t]; + if (proposed < 1.0e-15) { + double limit = MAX_REL_STEP * xType[t] / Math.abs(dx[t]); + maxStep = Math.min(maxStep, limit); + } + if (proposed > 1.0) { + double limit = (1.0 - xType[t]) / dx[t]; + maxStep = Math.min(maxStep, Math.max(0.1, limit)); + } } double proposedZeta = zeta + maxStep * dx[p]; if (proposedZeta < 1.0e-10) { - double limit = MAX_REL_STEP * zeta / Math.abs(dx[p]); - maxStep = Math.min(maxStep, limit); + double limit = MAX_REL_STEP * zeta / Math.abs(dx[p]); + maxStep = Math.min(maxStep, limit); } if (proposedZeta > 1.0 - 1.0e-10) { - double limit = (1.0 - 1.0e-10 - zeta) / dx[p]; - maxStep = Math.min(maxStep, Math.max(0.1, limit)); + double limit = (1.0 - 1.0e-10 - zeta) / dx[p]; + maxStep = Math.min(maxStep, Math.max(0.1, limit)); } if (Math.abs(dx[p]) / Math.max(zeta, 1.0e-10) > 0.3) { - maxStep = Math.min(maxStep, 0.3 * zeta / Math.abs(dx[p])); + maxStep = Math.min(maxStep, 0.3 * zeta / Math.abs(dx[p])); } // --- Apply update --- for (int t = 0; t < p; t++) { - xType[t] += maxStep * dx[t]; - xType[t] = Math.max(1.0e-15, Math.min(1.0, xType[t])); + xType[t] += maxStep * dx[t]; + xType[t] = Math.max(1.0e-15, Math.min(1.0, xType[t])); } zeta += maxStep * dx[p]; zeta = Math.max(1.0e-10, Math.min(1.0 - 1.0e-10, zeta)); // --- Restart criterion --- if (iterations > 20 && maxResidual > 0.1 && !restartTriggered) { - restartTriggered = true; - useBroyden = false; - if (pt == PhaseType.GAS) { - zeta = 2.0 / (2.0 + temperature / getPseudoCriticalTemperature()); - } else { - zeta = pressure * Btemp / (numberOfMolesInPhase * temperature * R); - } - zeta = Math.max(1.0e-8, Math.min(1.0 - 1.0e-8, zeta)); - for (int t = 0; t < p; t++) { - xType[t] = 0.5; - } + restartTriggered = true; + useBroyden = false; + if (pt == PhaseType.GAS) { + zeta = 2.0 / (2.0 + temperature / getPseudoCriticalTemperature()); + } else { + zeta = pressure * Btemp / (numberOfMolesInPhase * temperature * R); + } + zeta = Math.max(1.0e-8, Math.min(1.0 - 1.0e-8, zeta)); + for (int t = 0; t < p; t++) { + xType[t] = 0.5; + } } } while (iterations < MAX_ITERATIONS); @@ -490,9 +494,10 @@ public double molarVolume(double pressure, double temperature, double A, double * Build the site type map by grouping equivalent association sites. * *

- * Two individual sites are equivalent if they belong to the same component and have identical deltaNog rows (same - * bonding pattern to all other sites). This corresponds to sites with the same charge in the CPA association scheme — - * e.g., the two electron-donor sites of water (4C) have identical interactions with all other sites. + * Two individual sites are equivalent if they belong to the same component and have identical + * deltaNog rows (same bonding pattern to all other sites). This corresponds to sites with the + * same charge in the CPA association scheme — e.g., the two electron-donor sites of water (4C) + * have identical interactions with all other sites. *

* * @param ns total number of individual association sites @@ -507,30 +512,30 @@ private void buildSiteTypeMap(int ns) { for (int i = 0; i < ns; i++) { boolean matched = false; for (int t = 0; t < numTypes; t++) { - if (moleculeNumber[i] != moleculeNumber[typeRepSite[t]]) { - continue; - } - // Same component — check if deltaNog rows are identical - boolean identical = true; - for (int j = 0; j < ns; j++) { - if (Math.abs(deltaNog[i][j] - deltaNog[typeRepSite[t]][j]) > 1.0e-30) { - identical = false; - break; - } - } - if (identical) { - siteToType[i] = t; - typeMult[t]++; - matched = true; - break; - } + if (moleculeNumber[i] != moleculeNumber[typeRepSite[t]]) { + continue; + } + // Same component — check if deltaNog rows are identical + boolean identical = true; + for (int j = 0; j < ns; j++) { + if (Math.abs(deltaNog[i][j] - deltaNog[typeRepSite[t]][j]) > 1.0e-30) { + identical = false; + break; + } + } + if (identical) { + siteToType[i] = t; + typeMult[t]++; + matched = true; + break; + } } if (!matched) { - typeRepSite[numTypes] = i; - siteToType[i] = numTypes; - typeMult[numTypes] = 1; - typeCompIdx[numTypes] = moleculeNumber[i]; - numTypes++; + typeRepSite[numTypes] = i; + siteToType[i] = numTypes; + typeMult[numTypes] = 1; + typeCompIdx[numTypes] = moleculeNumber[i]; + numTypes++; } } } @@ -545,9 +550,9 @@ private void expandAndSetSiteFractions(double[] xType, int ns) { int idx = 0; for (int i = 0; i < numberOfComponents; i++) { for (int j = 0; j < componentArray[i].getNumberOfAssociationSites(); j++) { - int typeIdx = siteToType[idx]; - ((ComponentCPAInterface) componentArray[i]).setXsite(j, xType[typeIdx]); - idx++; + int typeIdx = siteToType[idx]; + ((ComponentCPAInterface) componentArray[i]).setXsite(j, xType[typeIdx]); + idx++; } } } @@ -562,8 +567,8 @@ private void readXsiteFromComponents(double[] xSite, int ns) { int idx = 0; for (int i = 0; i < numberOfComponents; i++) { for (int j = 0; j < componentArray[i].getNumberOfAssociationSites(); j++) { - xSite[idx] = ((ComponentSrkCPA) componentArray[i]).getXsite()[j]; - idx++; + xSite[idx] = ((ComponentSrkCPA) componentArray[i]).getXsite()[j]; + idx++; } } } @@ -596,16 +601,16 @@ private void readXsiteFromComponents(double[] xSite, int ns) { * @param dim system dimension (p+1) * @param p number of unique site types */ - private void buildReducedJacobian(double[][] jac, double[] xType, double[] tMoles, double[] redSum, double totalVol, - double gdv1, double zeta, double btemp, int dim, int p) { + private void buildReducedJacobian(double[][] jac, double[] xType, double[] tMoles, + double[] redSum, double totalVol, double gdv1, double zeta, double btemp, int dim, int p) { // J_XX block: dR_a/dX_b = delta_ab + X_a^2 * mult_b * n_b * delta(rep_a,rep_b) / V for (int a = 0; a < p; a++) { double xa2 = xType[a] * xType[a]; for (int b = 0; b < p; b++) { - double dab = (a == b) ? 1.0 : 0.0; - double dlt = delta[typeRepSite[a]][typeRepSite[b]]; - jac[a][b] = dab + xa2 * typeMult[b] * tMoles[b] * dlt / totalVol; + double dab = (a == b) ? 1.0 : 0.0; + double dlt = delta[typeRepSite[a]][typeRepSite[b]]; + jac[a][b] = dab + xa2 * typeMult[b] * tMoles[b] * dlt / totalVol; } } @@ -616,16 +621,17 @@ private void buildReducedJacobian(double[][] jac, double[] xType, double[] tMole double xa2 = xType[a] * xType[a]; double sumDeriv = 0.0; for (int b = 0; b < p; b++) { - double dlt = delta[typeRepSite[a]][typeRepSite[b]]; - double dDeltadV = dlt * gcpav; - sumDeriv += typeMult[b] * tMoles[b] * xType[b] * (dDeltadV * totalVol - dlt) / totalVol2; + double dlt = delta[typeRepSite[a]][typeRepSite[b]]; + double dDeltadV = dlt * gcpav; + sumDeriv += typeMult[b] * tMoles[b] * xType[b] * (dDeltadV * totalVol - dlt) / totalVol2; } jac[a][p] = xa2 * sumDeriv * dVdZeta; } // j_zeta_X row: dR_zeta/dX_a for (int a = 0; a < p; a++) { - jac[p][a] = btemp / numberOfMolesInPhase * cpaon * typeMult[a] * (tMoles[a] / totalVol) * gdv1 * redSum[a]; + jac[p][a] = btemp / numberOfMolesInPhase * cpaon * typeMult[a] * (tMoles[a] / totalVol) * gdv1 + * redSum[a]; } // j_zeta_zeta scalar @@ -641,8 +647,8 @@ private void buildReducedJacobian(double[][] jac, double[] xType, double[] tMole private void updateDeltaWithG(int ns) { for (int i = 0; i < ns; i++) { for (int j = i; j < ns; j++) { - delta[i][j] = deltaNog[i][j] * gcpa; - delta[j][i] = delta[i][j]; + delta[i][j] = deltaNog[i][j] * gcpa; + delta[j][i] = delta[i][j]; } } } @@ -658,7 +664,7 @@ private void updateDeltaWithG(int ns) { private static boolean invertMatrix(double[][] a, double[][] ainv, int n) { for (int i = 0; i < n; i++) { for (int j = 0; j < n; j++) { - ainv[i][j] = (i == j) ? 1.0 : 0.0; + ainv[i][j] = (i == j) ? 1.0 : 0.0; } } double[][] work = new double[n][n]; @@ -669,37 +675,37 @@ private static boolean invertMatrix(double[][] a, double[][] ainv, int n) { int maxRow = col; double maxVal = Math.abs(work[col][col]); for (int row = col + 1; row < n; row++) { - double val = Math.abs(work[row][col]); - if (val > maxVal) { - maxVal = val; - maxRow = row; - } + double val = Math.abs(work[row][col]); + if (val > maxVal) { + maxVal = val; + maxRow = row; + } } if (maxVal < 1.0e-30) { - return false; + return false; } if (maxRow != col) { - double[] tempRow = work[col]; - work[col] = work[maxRow]; - work[maxRow] = tempRow; - tempRow = ainv[col]; - ainv[col] = ainv[maxRow]; - ainv[maxRow] = tempRow; + double[] tempRow = work[col]; + work[col] = work[maxRow]; + work[maxRow] = tempRow; + tempRow = ainv[col]; + ainv[col] = ainv[maxRow]; + ainv[maxRow] = tempRow; } double pivot = work[col][col]; for (int k = 0; k < n; k++) { - work[col][k] /= pivot; - ainv[col][k] /= pivot; + work[col][k] /= pivot; + ainv[col][k] /= pivot; } for (int row = 0; row < n; row++) { - if (row == col) { - continue; - } - double factor = work[row][col]; - for (int k = 0; k < n; k++) { - work[row][k] -= factor * work[col][k]; - ainv[row][k] -= factor * ainv[col][k]; - } + if (row == col) { + continue; + } + double factor = work[row][col]; + for (int k = 0; k < n; k++) { + work[row][k] -= factor * work[col][k]; + ainv[row][k] -= factor * ainv[col][k]; + } } } return true; @@ -722,7 +728,7 @@ private static void broydenUpdate(double[][] invJ, double[] dxVec, double[] dfVe for (int i = 0; i < n; i++) { double s = 0.0; for (int j = 0; j < n; j++) { - s += invJ[i][j] * dfVec[j]; + s += invJ[i][j] * dfVec[j]; } hdf[i] = s; } @@ -730,7 +736,7 @@ private static void broydenUpdate(double[][] invJ, double[] dxVec, double[] dfVe for (int j = 0; j < n; j++) { double s = 0.0; for (int i = 0; i < n; i++) { - s += dxVec[i] * invJ[i][j]; + s += dxVec[i] * invJ[i][j]; } dxH[j] = s; } @@ -749,7 +755,7 @@ private static void broydenUpdate(double[][] invJ, double[] dxVec, double[] dfVe for (int i = 0; i < n; i++) { double numI = numVec[i] * invDenom; for (int j = 0; j < n; j++) { - invJ[i][j] += numI * dxH[j]; + invJ[i][j] += numI * dxH[j]; } } } @@ -779,9 +785,9 @@ private void ensureWorkArrays(int p) { * {@inheritDoc} * *

- * Override type 1 initialization to use reduced-dimension site type computation. Higher-order volume derivatives - * (FCPA, dFCPAdV, dFCPAdVdV, dFCPAdVdVdV) are computed using the type grouping, reducing linear system size from n_s - * to p. + * Override type 1 initialization to use reduced-dimension site type computation. Higher-order + * volume derivatives (FCPA, dFCPAdV, dFCPAdVdV, dFCPAdVdVdV) are computed using the type + * grouping, reducing linear system size from n_s to p. *

*/ @Override @@ -825,7 +831,8 @@ public void initCPAMatrix(int type) { double gv = getGcpav(); double fV = gv - 1.0 / totalVolume; double fVV = fV * fV + gcpavv + 1.0 / totalVolume2; - double fVVV = fV * fV * fV + 3.0 * fV * (gcpavv + 1.0 / totalVolume2) + gcpavvv - 2.0 / totalVolume3; + double fVVV = + fV * fV * fV + 3.0 * fV * (gcpavv + 1.0 / totalVolume2) + gcpavvv - 2.0 / totalVolume3; // Read reduced site fractions and moles double[] xSiteFull = new double[ns]; @@ -840,9 +847,9 @@ public void initCPAMatrix(int type) { for (int a = 0; a < p; a++) { double maInvV = typeMult[a] * workM[a] * invV; for (int b = a; b < p; b++) { - double k = maInvV * typeMult[b] * workM[b] * delta[typeRepSite[a]][typeRepSite[b]]; - workKlk[a][b] = k; - workKlk[b][a] = k; + double k = maInvV * typeMult[b] * workM[b] * delta[typeRepSite[a]][typeRepSite[b]]; + workKlk[a][b] = k; + workKlk[b][a] = k; } } @@ -850,7 +857,7 @@ public void initCPAMatrix(int type) { for (int a = 0; a < p; a++) { double s = 0.0; for (int b = 0; b < p; b++) { - s += workKlk[a][b] * workKsi[b]; + s += workKlk[a][b] * workKsi[b]; } workKlkKsi[a] = s; } @@ -858,7 +865,7 @@ public void initCPAMatrix(int type) { // Build reduced Hessian for XV linear system for (int a = 0; a < p; a++) { for (int b = 0; b < p; b++) { - workHess[a][b] = -workKlk[a][b]; + workHess[a][b] = -workKlk[a][b]; } workHess[a][a] -= typeMult[a] * workM[a] / (workKsi[a] * workKsi[a]); } @@ -867,7 +874,7 @@ public void initCPAMatrix(int type) { for (int a = 0; a < p; a++) { workXV[a] = fV * workKlkKsi[a]; } - solveLinearSystem(workHess, workXV, p); + LinearAlgebraOps.solveLinearSystemInPlace(workHess, workXV, p); // Compute scalar quantities double dotKsiKlkKsi = 0.0; @@ -887,7 +894,7 @@ public void initCPAMatrix(int type) { for (int a = 0; a < p; a++) { double s = 0.0; for (int b = 0; b < p; b++) { - s += workKlk[a][b] * workXV[b]; + s += workKlk[a][b] * workXV[b]; } dotXVKlkXV += workXV[a] * s; } @@ -899,61 +906,15 @@ public void initCPAMatrix(int type) { sumXV2 += typeMult[a] * workXV[a] * workXV[a]; } - dFCPAdVdVdV = -0.5 * fVVV * dotKsiKlkKsi - 3.0 * fVV * dotKlkKsiXV - 3.0 * fV * dotXVKlkXV + sumQXV * sumXV2; - } - - /** - * Solve the linear system A*x = b in-place (b overwritten with solution) using Gaussian elimination with partial - * pivoting. - * - * @param a coefficient matrix (modified in-place) - * @param b right-hand side vector (overwritten with solution) - * @param n system dimension - */ - private static void solveLinearSystem(double[][] a, double[] b, int n) { - for (int col = 0; col < n; col++) { - int maxRow = col; - double maxVal = Math.abs(a[col][col]); - for (int row = col + 1; row < n; row++) { - double val = Math.abs(a[row][col]); - if (val > maxVal) { - maxVal = val; - maxRow = row; - } - } - if (maxRow != col) { - double[] tmpRow = a[col]; - a[col] = a[maxRow]; - a[maxRow] = tmpRow; - double tmpVal = b[col]; - b[col] = b[maxRow]; - b[maxRow] = tmpVal; - } - double pivot = a[col][col]; - if (Math.abs(pivot) < 1.0e-30) { - continue; - } - for (int row = col + 1; row < n; row++) { - double factor = a[row][col] / pivot; - for (int k = col + 1; k < n; k++) { - a[row][k] -= factor * a[col][k]; - } - b[row] -= factor * b[col]; - } - } - for (int row = n - 1; row >= 0; row--) { - double s = b[row]; - for (int k = row + 1; k < n; k++) { - s -= a[row][k] * b[k]; - } - b[row] = (Math.abs(a[row][row]) > 1.0e-30) ? s / a[row][row] : 0.0; - } + dFCPAdVdVdV = -0.5 * fVVV * dotKsiKlkKsi - 3.0 * fVV * dotKlkKsiXV - 3.0 * fV * dotXVKlkXV + + sumQXV * sumXV2; } /** {@inheritDoc} */ @Override - public double molarVolumeChangePhase(double pressure, double temperature, double A, double B, PhaseType pt) - throws neqsim.util.exception.IsNaNException, neqsim.util.exception.TooManyIterationsException { + public double molarVolumeChangePhase(double pressure, double temperature, double A, double B, + PhaseType pt) throws neqsim.util.exception.IsNaNException, + neqsim.util.exception.TooManyIterationsException { return super.molarVolumeChangePhase(pressure, temperature, A, B, pt); } diff --git a/src/main/java/neqsim/thermo/phase/PhaseSrkCPAs.java b/src/main/java/neqsim/thermo/phase/PhaseSrkCPAs.java index 285098d8a7..26f3612d79 100644 --- a/src/main/java/neqsim/thermo/phase/PhaseSrkCPAs.java +++ b/src/main/java/neqsim/thermo/phase/PhaseSrkCPAs.java @@ -1,7 +1,5 @@ package neqsim.thermo.phase; -// import org.ejml.data.DenseMatrix64F; - import neqsim.thermo.component.ComponentSrkCPA; import neqsim.thermo.component.ComponentSrkCPAs; diff --git a/src/main/java/neqsim/thermo/phase/PhaseUMRCPA.java b/src/main/java/neqsim/thermo/phase/PhaseUMRCPA.java index 961af7b8c8..f52f88b182 100644 --- a/src/main/java/neqsim/thermo/phase/PhaseUMRCPA.java +++ b/src/main/java/neqsim/thermo/phase/PhaseUMRCPA.java @@ -2,18 +2,14 @@ import org.apache.logging.log4j.LogManager; import org.apache.logging.log4j.Logger; -import org.ejml.data.DMatrixRMaj; -import org.ejml.dense.row.CommonOps_DDRM; -import org.ejml.dense.row.NormOps_DDRM; -import org.ejml.dense.row.factory.LinearSolverFactory_DDRM; -import org.ejml.interfaces.linsol.LinearSolverDense; -import org.ejml.simple.SimpleMatrix; -// import org.ejml.data.DenseMatrix64F; +import org.ojalgo.matrix.decomposition.LU; import neqsim.thermo.component.ComponentCPAInterface; import neqsim.thermo.component.ComponentUMRCPA; import neqsim.thermo.mixingrule.CPAMixingRuleHandler; import neqsim.thermo.mixingrule.CPAMixingRulesInterface; import neqsim.thermo.mixingrule.MixingRuleTypeInterface; +import neqsim.util.math.LinearAlgebraOps; +import neqsim.util.math.LinearAlgebraOps.DenseMatrix; /** *

@@ -43,7 +39,7 @@ public class PhaseUMRCPA extends PhasePrEos implements PhaseCPAInterface { double dFCPAdVdV = 0.0; double dFCPAdVdVdV = 0.0; double gcpav = 0.0; - private double[] dFdNtemp = { 0, 0 }; + private double[] dFdNtemp = {0, 0}; int cpaon = 1; int oldTotalNumberOfAccociationSites = 0; int totalNumberOfAccociationSites = 0; @@ -59,31 +55,31 @@ public class PhaseUMRCPA extends PhasePrEos implements PhaseCPAInterface { private double[][] deltadT = null; private double[][] deltadTdT = null; double[][][] Klkni = null; - private SimpleMatrix KlkTVMatrix = null; - private SimpleMatrix KlkTTMatrix = null; - private SimpleMatrix KlkTMatrix = null; - private SimpleMatrix udotTimesmMatrix = null; - private SimpleMatrix mVector = null; - private SimpleMatrix udotMatrix = null; - private SimpleMatrix uMatrix = null; - private SimpleMatrix QMatksiksiksi = null; - private SimpleMatrix KlkVVVMatrix = null; - private SimpleMatrix KlkVVMatrix = null; - private SimpleMatrix udotTimesmiMatrix = null; - private SimpleMatrix ksiMatrix = null; - private SimpleMatrix KlkMatrix = null; - private SimpleMatrix hessianMatrix = null; - private SimpleMatrix hessianInvers = null; + private DenseMatrix KlkTVMatrix = null; + private DenseMatrix KlkTTMatrix = null; + private DenseMatrix KlkTMatrix = null; + private DenseMatrix udotTimesmMatrix = null; + private DenseMatrix mVector = null; + private DenseMatrix udotMatrix = null; + private DenseMatrix uMatrix = null; + private DenseMatrix QMatksiksiksi = null; + private DenseMatrix KlkVVVMatrix = null; + private DenseMatrix KlkVVMatrix = null; + private DenseMatrix udotTimesmiMatrix = null; + private DenseMatrix ksiMatrix = null; + private DenseMatrix KlkMatrix = null; + private DenseMatrix hessianMatrix = null; + private DenseMatrix hessianInvers = null; /** Cached LU factorization of {@code hessianMatrix} for repeated Hessian backsolves. */ - private transient LinearSolverDense hessianLU = null; + private transient LU hessianLU = null; /** Scratch matrix passed to EJML LU because the solver may decompose its input in place. */ - private transient DMatrixRMaj hessianLUinput = null; + private transient DenseMatrix hessianLUinput = null; /** Matrix size associated with the cached Hessian LU factorization. */ private transient int hessianLUSize = -1; - private SimpleMatrix KlkVMatrix = null; - private DMatrixRMaj corr2Matrix = null; - private DMatrixRMaj corr3Matrix = null; - private DMatrixRMaj corr4Matrix = null; + private DenseMatrix KlkVMatrix = null; + private DenseMatrix corr2Matrix = null; + private DenseMatrix corr3Matrix = null; + private DenseMatrix corr4Matrix = null; /** *

@@ -105,7 +101,8 @@ public PhaseUMRCPA clone() { } if (activeAccosComp != null) { clonedPhase.activeAccosComp = activeAccosComp.clone(); - System.arraycopy(this.activeAccosComp, 0, clonedPhase.activeAccosComp, 0, activeAccosComp.length); + System.arraycopy(this.activeAccosComp, 0, clonedPhase.activeAccosComp, 0, + activeAccosComp.length); } clonedPhase.hessianLU = null; clonedPhase.hessianLUinput = null; @@ -119,92 +116,106 @@ public PhaseUMRCPA clone() { /** {@inheritDoc} */ @Override - public void init(double totalNumberOfMoles, int numberOfComponents, int initType, PhaseType pt, double beta) { + public void init(double totalNumberOfMoles, int numberOfComponents, int initType, PhaseType pt, + double beta) { boolean changedAssosiationStatus = false; if (initType == 0) { activeAccosComp = new int[numberOfComponents]; for (int i = 0; i < numberOfComponents; i++) { - if (componentArray[i].getNumberOfmoles() < 1e-50) { - componentArray[i].setNumberOfAssociationSites(0); - if (activeAccosComp[i] == 1) { - activeAccosComp[i] = 0; - changedAssosiationStatus = true; - } - } else { - if (activeAccosComp[i] == 0) { - changedAssosiationStatus = true; - activeAccosComp[i] = 1; - } - } + if (componentArray[i].getNumberOfmoles() < 1e-50) { + componentArray[i].setNumberOfAssociationSites(0); + if (activeAccosComp[i] == 1) { + activeAccosComp[i] = 0; + changedAssosiationStatus = true; + } + } else { + if (activeAccosComp[i] == 0) { + changedAssosiationStatus = true; + activeAccosComp[i] = 1; + } + } } if (changedAssosiationStatus || lngi == null) { - setTotalNumberOfAccociationSites(0); - selfAccociationScheme = new int[numberOfComponents][0][0]; - crossAccociationScheme = new int[numberOfComponents][numberOfComponents][0][0]; - for (int i = 0; i < numberOfComponents; i++) { - if (componentArray[i].getNumberOfmoles() < 1e-50) { - componentArray[i].setNumberOfAssociationSites(0); - } else { - componentArray[i].setNumberOfAssociationSites(componentArray[i].getOrginalNumberOfAssociationSites()); - setTotalNumberOfAccociationSites( - getTotalNumberOfAccociationSites() + componentArray[i].getNumberOfAssociationSites()); - selfAccociationScheme[i] = cpaSelect.setAssociationScheme(i, this); - for (int j = 0; j < numberOfComponents; j++) { - crossAccociationScheme[i][j] = cpaSelect.setCrossAssociationScheme(i, j, this); - } - } - } + setTotalNumberOfAccociationSites(0); + selfAccociationScheme = new int[numberOfComponents][0][0]; + crossAccociationScheme = new int[numberOfComponents][numberOfComponents][0][0]; + for (int i = 0; i < numberOfComponents; i++) { + if (componentArray[i].getNumberOfmoles() < 1e-50) { + componentArray[i].setNumberOfAssociationSites(0); + } else { + componentArray[i].setNumberOfAssociationSites( + componentArray[i].getOrginalNumberOfAssociationSites()); + setTotalNumberOfAccociationSites(getTotalNumberOfAccociationSites() + + componentArray[i].getNumberOfAssociationSites()); + selfAccociationScheme[i] = cpaSelect.setAssociationScheme(i, this); + for (int j = 0; j < numberOfComponents; j++) { + crossAccociationScheme[i][j] = cpaSelect.setCrossAssociationScheme(i, j, this); + } + } + } } for (int i = 0; i < numberOfComponents; i++) { - for (int j = 0; j < componentArray[i].getNumberOfAssociationSites(); j++) { - ((ComponentUMRCPA) componentArray[i]).setXsite(j, 1.0); - ((ComponentUMRCPA) componentArray[i]).setXsitedV(j, 0.0); - ((ComponentUMRCPA) componentArray[i]).setXsitedT(j, 0.0); - } + for (int j = 0; j < componentArray[i].getNumberOfAssociationSites(); j++) { + ((ComponentUMRCPA) componentArray[i]).setXsite(j, 1.0); + ((ComponentUMRCPA) componentArray[i]).setXsitedV(j, 0.0); + ((ComponentUMRCPA) componentArray[i]).setXsitedT(j, 0.0); + } } if (changedAssosiationStatus || lngi == null || mVector == null) { - lngi = new double[numberOfComponents]; - mVector = new SimpleMatrix(getTotalNumberOfAccociationSites(), 1); - KlkMatrix = new SimpleMatrix(getTotalNumberOfAccociationSites(), getTotalNumberOfAccociationSites()); - KlkVMatrix = new SimpleMatrix(getTotalNumberOfAccociationSites(), getTotalNumberOfAccociationSites()); - KlkVVMatrix = new SimpleMatrix(getTotalNumberOfAccociationSites(), getTotalNumberOfAccociationSites()); - KlkVVVMatrix = new SimpleMatrix(getTotalNumberOfAccociationSites(), getTotalNumberOfAccociationSites()); - hessianMatrix = new SimpleMatrix(getTotalNumberOfAccociationSites(), getTotalNumberOfAccociationSites()); - KlkTMatrix = new SimpleMatrix(getTotalNumberOfAccociationSites(), getTotalNumberOfAccociationSites()); - KlkTTMatrix = new SimpleMatrix(getTotalNumberOfAccociationSites(), getTotalNumberOfAccociationSites()); - KlkTVMatrix = new SimpleMatrix(getTotalNumberOfAccociationSites(), getTotalNumberOfAccociationSites()); - corr2Matrix = new DMatrixRMaj(getTotalNumberOfAccociationSites(), 1); - corr3Matrix = new DMatrixRMaj(getTotalNumberOfAccociationSites(), 1); - corr4Matrix = new DMatrixRMaj(getTotalNumberOfAccociationSites(), 1); - Klkni = new double[numberOfComponents][getTotalNumberOfAccociationSites()][getTotalNumberOfAccociationSites()]; - ksiMatrix = new SimpleMatrix(getTotalNumberOfAccociationSites(), 1); - uMatrix = new SimpleMatrix(getTotalNumberOfAccociationSites(), 1); - udotMatrix = new SimpleMatrix(getTotalNumberOfAccociationSites(), 1); - moleculeNumber = new int[getTotalNumberOfAccociationSites()]; - assSiteNumber = new int[getTotalNumberOfAccociationSites()]; - gvector = new double[getTotalNumberOfAccociationSites()][1]; - udotTimesmMatrix = new SimpleMatrix(getTotalNumberOfAccociationSites(), 1); - delta = new double[getTotalNumberOfAccociationSites()][getTotalNumberOfAccociationSites()]; - deltaNog = new double[getTotalNumberOfAccociationSites()][getTotalNumberOfAccociationSites()]; - deltadT = new double[getTotalNumberOfAccociationSites()][getTotalNumberOfAccociationSites()]; - deltadTdT = new double[getTotalNumberOfAccociationSites()][getTotalNumberOfAccociationSites()]; - QMatksiksiksi = new SimpleMatrix(getTotalNumberOfAccociationSites(), 1); - udotTimesmiMatrix = new SimpleMatrix(numberOfComponents, getTotalNumberOfAccociationSites()); - - oldTotalNumberOfAccociationSites = getTotalNumberOfAccociationSites(); - - int temp = 0; - for (int i = 0; i < numberOfComponents; i++) { - for (int j = 0; j < componentArray[i].getNumberOfAssociationSites(); j++) { - moleculeNumber[temp + j] = i; - assSiteNumber[temp + j] = j; - } - temp += componentArray[i].getNumberOfAssociationSites(); - } + lngi = new double[numberOfComponents]; + mVector = new DenseMatrix(getTotalNumberOfAccociationSites(), 1); + KlkMatrix = + new DenseMatrix(getTotalNumberOfAccociationSites(), getTotalNumberOfAccociationSites()); + KlkVMatrix = + new DenseMatrix(getTotalNumberOfAccociationSites(), getTotalNumberOfAccociationSites()); + KlkVVMatrix = + new DenseMatrix(getTotalNumberOfAccociationSites(), getTotalNumberOfAccociationSites()); + KlkVVVMatrix = + new DenseMatrix(getTotalNumberOfAccociationSites(), getTotalNumberOfAccociationSites()); + hessianMatrix = + new DenseMatrix(getTotalNumberOfAccociationSites(), getTotalNumberOfAccociationSites()); + KlkTMatrix = + new DenseMatrix(getTotalNumberOfAccociationSites(), getTotalNumberOfAccociationSites()); + KlkTTMatrix = + new DenseMatrix(getTotalNumberOfAccociationSites(), getTotalNumberOfAccociationSites()); + KlkTVMatrix = + new DenseMatrix(getTotalNumberOfAccociationSites(), getTotalNumberOfAccociationSites()); + corr2Matrix = new DenseMatrix(getTotalNumberOfAccociationSites(), 1); + corr3Matrix = new DenseMatrix(getTotalNumberOfAccociationSites(), 1); + corr4Matrix = new DenseMatrix(getTotalNumberOfAccociationSites(), 1); + Klkni = + new double[numberOfComponents][getTotalNumberOfAccociationSites()][getTotalNumberOfAccociationSites()]; + ksiMatrix = new DenseMatrix(getTotalNumberOfAccociationSites(), 1); + uMatrix = new DenseMatrix(getTotalNumberOfAccociationSites(), 1); + udotMatrix = new DenseMatrix(getTotalNumberOfAccociationSites(), 1); + moleculeNumber = new int[getTotalNumberOfAccociationSites()]; + assSiteNumber = new int[getTotalNumberOfAccociationSites()]; + gvector = new double[getTotalNumberOfAccociationSites()][1]; + udotTimesmMatrix = new DenseMatrix(getTotalNumberOfAccociationSites(), 1); + delta = new double[getTotalNumberOfAccociationSites()][getTotalNumberOfAccociationSites()]; + deltaNog = + new double[getTotalNumberOfAccociationSites()][getTotalNumberOfAccociationSites()]; + deltadT = + new double[getTotalNumberOfAccociationSites()][getTotalNumberOfAccociationSites()]; + deltadTdT = + new double[getTotalNumberOfAccociationSites()][getTotalNumberOfAccociationSites()]; + QMatksiksiksi = new DenseMatrix(getTotalNumberOfAccociationSites(), 1); + udotTimesmiMatrix = new DenseMatrix(numberOfComponents, getTotalNumberOfAccociationSites()); + + oldTotalNumberOfAccociationSites = getTotalNumberOfAccociationSites(); + + int temp = 0; + for (int i = 0; i < numberOfComponents; i++) { + for (int j = 0; j < componentArray[i].getNumberOfAssociationSites(); j++) { + moleculeNumber[temp + j] = i; + assSiteNumber[temp + j] = j; + } + temp += componentArray[i].getNumberOfAssociationSites(); + } } } @@ -261,46 +272,47 @@ public void initCPAMatrix(int type) { double tempVar2; for (int i = 0; i < numberOfComponents; i++) { for (int j = 0; j < componentArray[i].getNumberOfAssociationSites(); j++) { - tempVar1 = ksiMatrix.get(temp + j, 0); - tempVar2 = udotMatrix.get(temp + j, 0); - uMatrix.set(temp + j, 0, Math.log(tempVar1) - tempVar1 + 1.0); - gvector[temp + j][0] = mVector.get(temp + j, 0) * tempVar2; - - if (moleculeNumber[temp + j] == i) { - udotTimesmiMatrix.set(i, temp + j, tempVar2); - } else { - udotTimesmiMatrix.set(i, temp + j, 0.0); - } + tempVar1 = ksiMatrix.unsafe_get(temp + j, 0); + tempVar2 = udotMatrix.unsafe_get(temp + j, 0); + uMatrix.unsafe_set(temp + j, 0, Math.log(tempVar1) - tempVar1 + 1.0); + gvector[temp + j][0] = mVector.unsafe_get(temp + j, 0) * tempVar2; + + if (moleculeNumber[temp + j] == i) { + udotTimesmiMatrix.unsafe_set(i, temp + j, tempVar2); + } else { + udotTimesmiMatrix.unsafe_set(i, temp + j, 0.0); + } } temp += componentArray[i].getNumberOfAssociationSites(); } if (type > 2) { for (int p = 0; p < numberOfComponents; p++) { - lngi[p] = ((ComponentUMRCPA) componentArray[p]).calc_lngi(this); + lngi[p] = ((ComponentUMRCPA) componentArray[p]).calc_lngi(this); } } for (int i = 0; i < totalNumberOfAccociationSites; i++) { for (int j = i; j < totalNumberOfAccociationSites; j++) { - delta[i][j] = deltaNog[i][j] * gcpa; - delta[j][i] = delta[i][j]; - if (type > 1) { - deltadT[i][j] = cpamix.calcDeltadT(assSiteNumber[i], assSiteNumber[j], moleculeNumber[i], moleculeNumber[j], - this, getTemperature(), getPressure(), numberOfComponents); - deltadT[j][i] = deltadT[i][j]; - - deltadTdT[i][j] = cpamix.calcDeltadTdT(assSiteNumber[i], assSiteNumber[j], moleculeNumber[i], - moleculeNumber[j], this, getTemperature(), getPressure(), numberOfComponents); - deltadTdT[j][i] = deltadTdT[i][j]; - } + delta[i][j] = deltaNog[i][j] * gcpa; + delta[j][i] = delta[i][j]; + if (type > 1) { + deltadT[i][j] = cpamix.calcDeltadT(assSiteNumber[i], assSiteNumber[j], moleculeNumber[i], + moleculeNumber[j], this, getTemperature(), getPressure(), numberOfComponents); + deltadT[j][i] = deltadT[i][j]; + + deltadTdT[i][j] = + cpamix.calcDeltadTdT(assSiteNumber[i], assSiteNumber[j], moleculeNumber[i], + moleculeNumber[j], this, getTemperature(), getPressure(), numberOfComponents); + deltadTdT[j][i] = deltadTdT[i][j]; + } } } double totalVolume = getTotalVolume(); double totalVolume2 = totalVolume * totalVolume; double totalVolume3 = totalVolume2 * totalVolume; - double gdv1 = getGcpav() - 1.0 / totalVolume; + double gdv1 = gcpav - 1.0 / totalVolume; double gdv2 = gdv1 * gdv1; double gdv3 = gdv2 * gdv1; double Klk = 0.0; @@ -308,84 +320,90 @@ public void initCPAMatrix(int type) { double tempKsiRead = 0.0; for (int i = 0; i < totalNumberOfAccociationSites; i++) { for (int j = i; j < totalNumberOfAccociationSites; j++) { - Klk = KlkMatrix.get(i, j); - tempVar = Klk * gdv1; - KlkVMatrix.set(i, j, tempVar); - KlkVMatrix.set(j, i, tempVar); - - tempVar = Klk * gdv2 + Klk * (gcpavv + 1.0 / totalVolume2); - KlkVVMatrix.set(i, j, tempVar); - KlkVVMatrix.set(j, i, tempVar); - - tempVar = Klk * gdv3 + 3.0 * Klk * (gcpav - 1.0 / totalVolume) * (gcpavv + 1.0 / (totalVolume2)) - + Klk * (gcpavvv - 2.0 / (totalVolume3)); - KlkVVVMatrix.set(i, j, tempVar); - KlkVVVMatrix.set(j, i, tempVar); - - if (type > 1) { - tempVar = deltadT[i][j] / delta[i][j]; - - if (Math.abs(tempVar) > 1e-50) { - double tempVardT = deltadTdT[i][j] / delta[i][j] - - (deltadT[i][j] * deltadT[i][j]) / (delta[i][j] * delta[i][j]); - - tempVar2 = Klk * tempVar; - KlkTMatrix.set(i, j, tempVar2); - KlkTMatrix.set(j, i, tempVar2); - - tempVar2 = Klk * tempVar * (gcpav - 1.0 / totalVolume); - KlkTVMatrix.set(i, j, tempVar2); - KlkTVMatrix.set(j, i, tempVar2); - - tempVar2 = Klk * (tempVar * tempVar + tempVardT); - KlkTTMatrix.set(i, j, tempVar2); - KlkTTMatrix.set(j, i, tempVar2); - } - - if (type > 2) { - for (int p = 0; p < numberOfComponents; p++) { - double t1 = 0.0; - double t2 = 0.0; - if (moleculeNumber[i] == p) { - t1 = 1.0 / mVector.get(i, 0); - } - if (moleculeNumber[j] == p) { - t2 = 1.0 / mVector.get(j, 0); - } - Klkni[p][i][j] = Klk * (t1 + t2 + lngi[p]); // ((ComponentSrkCPA) - // getComponent(p)).calc_lngi(this)); - Klkni[p][j][i] = Klkni[p][i][j]; - } - } - } + Klk = KlkMatrix.unsafe_get(i, j); + tempVar = Klk * gdv1; + KlkVMatrix.unsafe_set(i, j, tempVar); + KlkVMatrix.unsafe_set(j, i, tempVar); + + tempVar = Klk * gdv2 + Klk * (gcpavv + 1.0 / totalVolume2); + KlkVVMatrix.unsafe_set(i, j, tempVar); + KlkVVMatrix.unsafe_set(j, i, tempVar); + + tempVar = + Klk * gdv3 + 3.0 * Klk * (gcpav - 1.0 / totalVolume) * (gcpavv + 1.0 / totalVolume2) + + Klk * (gcpavvv - 2.0 / totalVolume3); + KlkVVVMatrix.unsafe_set(i, j, tempVar); + KlkVVVMatrix.unsafe_set(j, i, tempVar); + + if (type > 1) { + tempVar = deltadT[i][j] / delta[i][j]; + + if (Math.abs(tempVar) > 1e-50) { + double tempVardT = deltadTdT[i][j] / delta[i][j] + - (deltadT[i][j] * deltadT[i][j]) / (delta[i][j] * delta[i][j]); + + tempVar2 = Klk * tempVar; + KlkTMatrix.unsafe_set(i, j, tempVar2); + KlkTMatrix.unsafe_set(j, i, tempVar2); + + tempVar2 = Klk * tempVar * (gcpav - 1.0 / totalVolume); + KlkTVMatrix.unsafe_set(i, j, tempVar2); + KlkTVMatrix.unsafe_set(j, i, tempVar2); + + tempVar2 = Klk * (tempVar * tempVar + tempVardT); + KlkTTMatrix.unsafe_set(i, j, tempVar2); + KlkTTMatrix.unsafe_set(j, i, tempVar2); + } + + if (type > 2) { + for (int p = 0; p < numberOfComponents; p++) { + double t1 = 0.0; + double t2 = 0.0; + if (moleculeNumber[i] == p) { + t1 = 1.0 / mVector.unsafe_get(i, 0); + } + if (moleculeNumber[j] == p) { + t2 = 1.0 / mVector.unsafe_get(j, 0); + } + Klkni[p][i][j] = Klk * (t1 + t2 + lngi[p]); + Klkni[p][j][i] = Klkni[p][i][j]; + } + } + } } - tempKsiRead = ksiMatrix.get(i, 0); - QMatksiksiksi.set(i, 0, 2.0 * mVector.get(i, 0) / (tempKsiRead * tempKsiRead * tempKsiRead)); + tempKsiRead = ksiMatrix.unsafe_get(i, 0); + QMatksiksiksi.unsafe_set(i, 0, + 2.0 * mVector.unsafe_get(i, 0) / (tempKsiRead * tempKsiRead * tempKsiRead)); } - SimpleMatrix ksiMatrixTranspose = ksiMatrix.transpose(); + DenseMatrix ksiMatrixTranspose = transpose(ksiMatrix); - // dXdV - SimpleMatrix KlkVMatrixksi = KlkVMatrix.mult(ksiMatrix); - SimpleMatrix XV = applyHessianInv(KlkVMatrixksi); - SimpleMatrix XVtranspose = XV.transpose(); + DenseMatrix klkVMatrixksi = multiply(KlkVMatrix, ksiMatrix); + DenseMatrix XV = applyHessianInv(klkVMatrixksi); + DenseMatrix XVtranspose = transpose(XV); - FCPA = mVector.transpose().mult(uMatrix.minus(ksiMatrix.elementMult(udotMatrix).scale(0.5))).get(0, 0); // QCPA.get(0, - // 0); - // //*0.5; + DenseMatrix qCpa = multiply(transpose(mVector), + subtract(uMatrix, scale(elementMult(ksiMatrix, udotMatrix), 0.5))); + FCPA = qCpa.unsafe_get(0, 0); - dFCPAdV = ksiMatrixTranspose.mult(KlkVMatrixksi).get(0, 0) * (-0.5); - SimpleMatrix KlkVVMatrixTImesKsi = KlkVVMatrix.mult(ksiMatrix); - dFCPAdVdV = ksiMatrixTranspose.mult(KlkVVMatrixTImesKsi).scale(-0.5).minus(KlkVMatrixksi.transpose().mult(XV)) - .get(0, 0); + DenseMatrix tempMatrix = scale(multiply(ksiMatrixTranspose, klkVMatrixksi), -0.5); + dFCPAdV = tempMatrix.unsafe_get(0, 0); + DenseMatrix klkVvMatrixTimesKsi = multiply(KlkVVMatrix, ksiMatrix); + DenseMatrix tempMatrixVV = + subtract(scale(multiply(ksiMatrixTranspose, klkVvMatrixTimesKsi), -0.5), + multiply(transpose(klkVMatrixksi), XV)); + dFCPAdVdV = tempMatrixVV.unsafe_get(0, 0); - SimpleMatrix QVVV = ksiMatrixTranspose.mult(KlkVVVMatrix.mult(ksiMatrix)); // .scale(-0.5); - SimpleMatrix QVVksi = KlkVVMatrixTImesKsi.scale(-1.0); - SimpleMatrix QksiVksi = KlkVMatrix.scale(-1.0); + DenseMatrix qVvv = multiply(ksiMatrixTranspose, multiply(KlkVVVMatrix, ksiMatrix)); + DenseMatrix qVvksi = scale(klkVvMatrixTimesKsi, -1.0); + DenseMatrix qKsiVksi = scale(KlkVMatrix, -1.0); - dFCPAdVdVdV = -0.5 * QVVV.get(0, 0) + QVVksi.transpose().mult(XV).get(0, 0) * 3.0 - + XVtranspose.mult(QksiVksi.mult(XV)).get(0, 0) * 3.0 - + XVtranspose.mult(QMatksiksiksi.mult(XVtranspose)).mult(XV).get(0, 0); + DenseMatrix mat1 = scale(multiply(transpose(qVvksi), XV), 3.0); + DenseMatrix mat2 = scale(multiply(XVtranspose, multiply(qKsiVksi, XV)), 3.0); + DenseMatrix mat4 = multiply(multiply(XVtranspose, multiply(QMatksiksiksi, XVtranspose)), XV); + + dFCPAdVdVdV = -0.5 * qVvv.unsafe_get(0, 0) + mat1.unsafe_get(0, 0) + mat2.unsafe_get(0, 0) + + mat4.unsafe_get(0, 0); if (type == 1) { return; @@ -394,36 +412,30 @@ public void initCPAMatrix(int type) { temp = 0; for (int p = 0; p < numberOfComponents; p++) { for (int kk = 0; kk < getComponent(p).getNumberOfAssociationSites(); kk++) { - ((ComponentCPAInterface) getComponent(p)).setXsitedV(kk, XV.get(temp + kk, 0)); + ((ComponentCPAInterface) getComponent(p)).setXsitedV(kk, XV.unsafe_get(temp + kk, 0)); } temp += getComponent(p).getNumberOfAssociationSites(); } - // KlkTMatrix = new SimpleMatrix(KlkdT); - SimpleMatrix KlkTMatrixTImesKsi = KlkTMatrix.mult(ksiMatrix); - // dQdT - SimpleMatrix tempMatrix2 = ksiMatrixTranspose.mult(KlkTMatrixTImesKsi); // .scale(-0.5); - dFCPAdT = tempMatrix2.get(0, 0) * (-0.5); - - // SimpleMatrix KlkTVMatrix = new SimpleMatrix(KlkdTdV); - // SimpleMatrix tempMatrixTV = - // ksiMatrixTranspose.mult(KlkTVMatrix.mult(ksiMatrix)).scale(-0.5).minus(KlkTMatrixTImesKsi.transpose().mult(XV)); - // dFCPAdTdV = tempMatrixTV.get(0, 0); - // dXdT - SimpleMatrix XT = applyHessianInv(KlkTMatrixTImesKsi); - // dQdTdT - SimpleMatrix tempMatrixTT = ksiMatrixTranspose.mult(KlkTTMatrix.mult(ksiMatrix)).scale(-0.5) - .minus(KlkTMatrixTImesKsi.transpose().mult(XT)); - dFCPAdTdT = tempMatrixTT.get(0, 0); - - SimpleMatrix tempMatrixTV = ksiMatrixTranspose.mult(KlkTVMatrix.mult(ksiMatrix)).scale(-0.5) - .minus(KlkTMatrixTImesKsi.transpose().mult(XV)); - dFCPAdTdV = tempMatrixTV.get(0, 0); + DenseMatrix klkTMatrixTimesKsi = multiply(KlkTMatrix, ksiMatrix); + DenseMatrix tempMatrix2 = scale(multiply(ksiMatrixTranspose, klkTMatrixTimesKsi), -0.5); + dFCPAdT = tempMatrix2.unsafe_get(0, 0); + + DenseMatrix XT = applyHessianInv(klkTMatrixTimesKsi); + DenseMatrix tempMatrixTT = + subtract(scale(multiply(ksiMatrixTranspose, multiply(KlkTTMatrix, ksiMatrix)), -0.5), + multiply(transpose(klkTMatrixTimesKsi), XT)); + dFCPAdTdT = tempMatrixTT.unsafe_get(0, 0); + + DenseMatrix tempMatrixTV = + subtract(scale(multiply(ksiMatrixTranspose, multiply(KlkTVMatrix, ksiMatrix)), -0.5), + multiply(transpose(klkTMatrixTimesKsi), XV)); + dFCPAdTdV = tempMatrixTV.unsafe_get(0, 0); temp = 0; for (int p = 0; p < numberOfComponents; p++) { for (int kk = 0; kk < getComponent(p).getNumberOfAssociationSites(); kk++) { - ((ComponentCPAInterface) getComponent(p)).setXsitedT(kk, XT.get(temp + kk, 0)); + ((ComponentCPAInterface) getComponent(p)).setXsitedT(kk, XT.unsafe_get(temp + kk, 0)); } temp += getComponent(p).getNumberOfAssociationSites(); } @@ -432,51 +444,20 @@ public void initCPAMatrix(int type) { return; } - // int assSites = 0; - // if(true) return; for (int p = 0; p < numberOfComponents; p++) { - SimpleMatrix KiMatrix = new SimpleMatrix(Klkni[p]); - // KiMatrix.print(10,10); - // Matrix dQdniMatrix = - // (ksiMatrix.transpose().times(KiMatrix.times(ksiMatrix)).times(-0.5)); // this - // methods misses one part of .... - // dQdniMatrix.print(10,10); - // KiMatrix.print(10, 10); - // miMatrix.getMatrix(assSites, assSites, 0, totalNumberOfAccociationSites - - // 1).print(10, 10); - // Matrix tempMatrix20 = miMatrix.getMatrix(assSites, assSites, 0, - // totalNumberOfAccociationSites - - // 1).times(uMatrix).minus(ksiMatrix.transpose().times(KiMatrix.times(ksiMatrix)).times(-0.5)); - // - // ksiMatrix.transpose().times(KlkTMatrix.times(ksiMatrix)).times(-0.5); - // System.out.println("dQdn "); - // tempMatrix20.print(10, 10); - SimpleMatrix tempMatrix4 = KiMatrix.mult(ksiMatrix); - // udotTimesmiMatrix.getMatrix(assSites, assSites, 0, - // totalNumberOfAccociationSites - 1).print(10, 10); - SimpleMatrix tempMatrix5 = udotTimesmiMatrix.extractVector(true, p).transpose().minus(tempMatrix4); - // tempMki[0] = mki[p]; - // Matrix amatrix = new Matrix(croeneckerProduct(tempMki, - // udotMatrix.getArray())); - // System.out.println("aMatrix "); - // amatrix.transpose().print(10, 10); - // System.out.println("temp4 matrix"); - // tempMatrix4.print(10, 10); - // Matrix tempMatrix5 = amatrix.minus(tempMatrix4); - SimpleMatrix tempMatrix6 = applyHessianInv(tempMatrix5); // .scale(-1.0); - // System.out.println("dXdni"); - // tempMatrix4.print(10, 10); - // tempMatrix5.print(10, 10); - // System.out.println("dXdn "); - // tempMatrix6.print(10, 10); + DenseMatrix kiMatrix = new DenseMatrix(Klkni[p]); + DenseMatrix tempMatrix4 = multiply(kiMatrix, ksiMatrix); + DenseMatrix tempMatrix5 = + subtract(transpose(extractVector(udotTimesmiMatrix, true, p)), tempMatrix4); + DenseMatrix tempMatrix6 = applyHessianInv(tempMatrix5); int temp2 = 0; for (int compp = 0; compp < numberOfComponents; compp++) { - for (int kk = 0; kk < getComponent(compp).getNumberOfAssociationSites(); kk++) { - ((ComponentCPAInterface) getComponent(compp)).setXsitedni(kk, p, -1.0 * tempMatrix6.get(temp2 + kk, 0)); - } - temp2 += getComponent(compp).getNumberOfAssociationSites(); + for (int kk = 0; kk < getComponent(compp).getNumberOfAssociationSites(); kk++) { + ((ComponentCPAInterface) getComponent(compp)).setXsitedni(kk, p, + -1.0 * tempMatrix6.unsafe_get(temp2 + kk, 0)); + } + temp2 += getComponent(compp).getNumberOfAssociationSites(); } - // assSites += getComponent(p).getNumberOfAssociationSites(); } } @@ -497,9 +478,9 @@ public void setMixingRule(MixingRuleTypeInterface mr) { public void calcDelta() { for (int i = 0; i < getTotalNumberOfAccociationSites(); i++) { for (int j = i; j < getTotalNumberOfAccociationSites(); j++) { - deltaNog[i][j] = cpamix.calcDeltaNog(assSiteNumber[i], assSiteNumber[j], moleculeNumber[i], moleculeNumber[j], - this, getTemperature(), getPressure(), numberOfComponents); - deltaNog[j][i] = deltaNog[i][j]; + deltaNog[i][j] = cpamix.calcDeltaNog(assSiteNumber[i], assSiteNumber[j], moleculeNumber[i], + moleculeNumber[j], this, getTemperature(), getPressure(), numberOfComponents); + deltaNog[j][i] = deltaNog[i][j]; } } } @@ -564,10 +545,10 @@ public double dFdTdT() { */ public double FCPA() { /* - * double tot = 0.0; double ans = 0.0; for (int i = 0; i < numberOfComponents; i++) { tot = 0.0; for (int j = 0; j < - * componentArray[i].getNumberOfAssociationSites(); j++) { double xai = ((ComponentSrkCPA) - * componentArray[i]).getXsite()[j]; tot += (Math.log(xai) - 1.0 / 2.0 * xai + 1.0 / 2.0); } ans += - * componentArray[i].getNumberOfMolesInPhase() * tot; } return ans; + * double tot = 0.0; double ans = 0.0; for (int i = 0; i < numberOfComponents; i++) { tot = 0.0; + * for (int j = 0; j < componentArray[i].getNumberOfAssociationSites(); j++) { double xai = + * ((ComponentSrkCPA) componentArray[i]).getXsite()[j]; tot += (Math.log(xai) - 1.0 / 2.0 * xai + * + 1.0 / 2.0); } ans += componentArray[i].getNumberOfMolesInPhase() * tot; } return ans; */ return FCPA; } @@ -623,12 +604,12 @@ public double dFCPAdVdVdV() { */ public double dFCPAdT() { /* - * double tot = 0.0; double ans = 0.0; for (int i = 0; i < numberOfComponents; i++) { tot = 0.0; for (int j = 0; j < - * componentArray[i].getNumberOfAssociationSites(); j++) { double xai = ((ComponentSrkCPA) - * componentArray[i]).getXsite()[j]; double xaidT = ((ComponentSrkCPA) componentArray[i]).getXsitedT()[j]; tot += - * 1.0 / xai * xaidT - 0.5 * xaidT; // - 1.0 / 2.0 * xai + 1.0 / 2.0); } ans += - * componentArray[i].getNumberOfMolesInPhase() * tot; } System.out.println("dFCPAdT1 " + ans + " dfcpa2 " - * +dFCPAdT); return ans; + * double tot = 0.0; double ans = 0.0; for (int i = 0; i < numberOfComponents; i++) { tot = 0.0; + * for (int j = 0; j < componentArray[i].getNumberOfAssociationSites(); j++) { double xai = + * ((ComponentSrkCPA) componentArray[i]).getXsite()[j]; double xaidT = ((ComponentSrkCPA) + * componentArray[i]).getXsitedT()[j]; tot += 1.0 / xai * xaidT - 0.5 * xaidT; // - 1.0 / 2.0 * + * xai + 1.0 / 2.0); } ans += componentArray[i].getNumberOfMolesInPhase() * tot; } + * System.out.println("dFCPAdT1 " + ans + " dfcpa2 " +dFCPAdT); return ans; */ return dFCPAdT; } @@ -656,17 +637,18 @@ public double dFCPAdTdV() { // getTotalVolume()) * (1.0 - getTotalVolume() * getGcpav()) * hcpatotdT)); return dFCPAdTdV; /* - * if (totalNumberOfAccociationSites > 0) { return 1.0 / (2.0 * getTotalVolume()) * (1.0 - getTotalVolume() * - * getGcpav()) * hcpatotdT; } else { return 0; } + * if (totalNumberOfAccociationSites > 0) { return 1.0 / (2.0 * getTotalVolume()) * (1.0 - + * getTotalVolume() * getGcpav()) * hcpatotdT; } else { return 0; } */ } /** {@inheritDoc} */ @Override public double molarVolume(double pressure, double temperature, double A, double B, PhaseType pt) - throws neqsim.util.exception.IsNaNException, neqsim.util.exception.TooManyIterationsException { + throws neqsim.util.exception.IsNaNException, + neqsim.util.exception.TooManyIterationsException { double BonV = pt == PhaseType.GAS ? pressure * getB() / (numberOfMolesInPhase * temperature * R) - : 2.0 / (2.0 + temperature / getPseudoCriticalTemperature()); + : 2.0 / (2.0 + temperature / getPseudoCriticalTemperature()); BonV = Math.max(1.0e-8, Math.min(1.0 - 1.0e-8, BonV)); double BonVold; double BonV2; @@ -687,8 +669,8 @@ public double molarVolume(double pressure, double temperature, double A, double iterations++; gcpa = calc_g(); if (gcpa < 0) { - setMolarVolume(1.0 / Btemp / numberOfMolesInPhase); - gcpa = calc_g(); + setMolarVolume(1.0 / Btemp / numberOfMolesInPhase); + gcpa = calc_g(); } // lngcpa = @@ -698,69 +680,73 @@ public double molarVolume(double pressure, double temperature, double A, double gcpavvv = calc_lngVVV(); if (totalNumberOfAccociationSites > 0) { - solveX(); + solveX(); } initCPAMatrix(1); BonV2 = BonV * BonV; BonVold = BonV; - h = BonV - Btemp / numberOfMolesInPhase * dFdV() - pressure * Btemp / (numberOfMolesInPhase * R * temperature); + h = BonV - Btemp / numberOfMolesInPhase * dFdV() + - pressure * Btemp / (numberOfMolesInPhase * R * temperature); dh = 1.0 + Btemp / (BonV2) * (Btemp / numberOfMolesInPhase * dFdVdV()); dhh = -2.0 * Btemp / (BonV2 * BonV) * (Btemp / numberOfMolesInPhase * dFdVdV()) - - (Btemp * Btemp) / (BonV2 * BonV2) * (Btemp / numberOfMolesInPhase * dFdVdVdV()); + - (Btemp * Btemp) / (BonV2 * BonV2) * (Btemp / numberOfMolesInPhase * dFdVdVdV()); d1 = -h / dh; d2 = -dh / dhh; // System.out.println("h " + h + " iter " + iterations + " " + d1 + " d2 " + d2 // + " d1 / d2 " + (d1 / d2)); if (Math.abs(d1 / d2) <= 1.0) { - BonV += d1 * (1.0 + 0.5 * d1 / d2); + BonV += d1 * (1.0 + 0.5 * d1 / d2); } else if (d1 / d2 < -1) { - BonV += 0.5 * d1; + BonV += 0.5 * d1; } else if (d1 > d2) { - return molarVolumeChangePhase(pressure, temperature, A, B, pt); - // BonV += d2; - // double hnew = h + d2 * dh; - // if (Math.abs(hnew) > Math.abs(h)) { - // BonV = pt == 1 ? 2.0 / (2.0 + temperature / - // getPseudoCriticalTemperature()) : pressure * getB() / (numberOfMolesInPhase * - // temperature * R); - // } + return molarVolumeChangePhase(pressure, temperature, A, B, pt); + // BonV += d2; + // double hnew = h + d2 * dh; + // if (Math.abs(hnew) > Math.abs(h)) { + // BonV = pt == 1 ? 2.0 / (2.0 + temperature / + // getPseudoCriticalTemperature()) : pressure * getB() / (numberOfMolesInPhase * + // temperature * R); + // } } else { - BonV += 0.5 * d1; + BonV += 0.5 * d1; } if (Math.abs((BonV - BonVold) / BonV) > 0.1) { - BonV = BonVold + 0.1 * (BonV - BonVold); + BonV = BonVold + 0.1 * (BonV - BonVold); } if (BonV < 0) { - if (iterations < 10) { - // System.out.println(iterations + " BonV " + BonV); - BonV = (BonVold + BonV) / 2.0; - } else { - return molarVolumeChangePhase(pressure, temperature, A, B, pt); - } + if (iterations < 10) { + // System.out.println(iterations + " BonV " + BonV); + BonV = (BonVold + BonV) / 2.0; + } else { + return molarVolumeChangePhase(pressure, temperature, A, B, pt); + } } if (BonV >= 1.0) { - if (iterations < 10) { - BonV = (BonVold + BonV) / 2.0; - } else { - return molarVolumeChangePhase(pressure, temperature, A, B, pt); - } + if (iterations < 10) { + BonV = (BonVold + BonV) / 2.0; + } else { + return molarVolumeChangePhase(pressure, temperature, A, B, pt); + } } /* - * if (BonV > 0.9999) { if (iterations < 10) { BonV = (BonVold + BonV) / 2.0; } else { // BonV = - * calcRootVolFinder(pt); // BonV = molarVolumeChangePhase(pressure, temperature, A, B, pt); // BonV = 0.9999; // - * BonV = pt == 1 ? 2.0 / (2.0 + temperature / getPseudoCriticalTemperature()) : pressure * getB() / - * (numberOfMolesInPhase * temperature * R); } } else if (BonV < 0) { if (iterations < 10) { BonV = - * Math.abs(BonVold + BonV) / 2.0; } else { // BonV = calcRootVolFinder(pt); // return - * molarVolumeChangePhase(pressure, temperature, A, B, pt); // BonV = pt == 1 ? 2.0 / (2.0 + temperature / - * getPseudoCriticalTemperature()) : pressure * getB() / (numberOfMolesInPhase * temperature * R); } } + * if (BonV > 0.9999) { if (iterations < 10) { BonV = (BonVold + BonV) / 2.0; } else { // BonV + * = calcRootVolFinder(pt); // BonV = molarVolumeChangePhase(pressure, temperature, A, B, pt); + * // BonV = 0.9999; // BonV = pt == 1 ? 2.0 / (2.0 + temperature / + * getPseudoCriticalTemperature()) : pressure * getB() / (numberOfMolesInPhase * temperature * + * R); } } else if (BonV < 0) { if (iterations < 10) { BonV = Math.abs(BonVold + BonV) / 2.0; + * } else { // BonV = calcRootVolFinder(pt); // return molarVolumeChangePhase(pressure, + * temperature, A, B, pt); // BonV = pt == 1 ? 2.0 / (2.0 + temperature / + * getPseudoCriticalTemperature()) : pressure * getB() / (numberOfMolesInPhase * temperature * + * R); } } */ setMolarVolume(1.0 / BonV * Btemp / numberOfMolesInPhase); Z = pressure * getMolarVolume() / (R * temperature); - } while ((Math.abs((BonV - BonVold) / BonV) > 1.0e-10 || Math.abs(h) > 1e-12) && iterations < maxIterations); + } while ((Math.abs((BonV - BonVold) / BonV) > 1.0e-10 || Math.abs(h) > 1e-12) + && iterations < maxIterations); // System.out.println("h failed " + h + " Z" + Z + " iterations " + iterations + // " BonV " + BonV); @@ -819,13 +805,13 @@ private double[] calcdFdNtemp() { // temp = ((ComponentSrkCPA) getComponent(k)).calc_lngi(this); // temp2 = ((ComponentSrkCPA) getComponent(k)).calc_lngidV(this); for (int i = 0; i < getComponent(k).getNumberOfAssociationSites(); i++) { - tot2 -= 1.0 * ((ComponentUMRCPA) getComponent(k)).getXsitedV()[i]; - tot3 += (1.0 - ((ComponentUMRCPA) getComponent(k)).getXsite()[i]) * 1.0; + tot2 -= 1.0 * ((ComponentUMRCPA) getComponent(k)).getXsitedV()[i]; + tot3 += (1.0 - ((ComponentUMRCPA) getComponent(k)).getXsite()[i]) * 1.0; } tot1 += 1.0 / 2.0 * tot2 * getComponent(k).getNumberOfMolesInPhase(); tot4 += 0.5 * getComponent(k).getNumberOfMolesInPhase() * tot3; } - return new double[] { -tot1, -tot4 }; + return new double[] {-tot1, -tot4}; } /** @@ -840,21 +826,21 @@ public void calcXsitedV() { } /** - * Applies the current Hessian inverse to a right-hand side without forming the inverse when a cached LU factorization - * is available. + * Applies the current Hessian inverse to a right-hand side without forming the inverse when a + * cached LU factorization is available. * * @param rhs right-hand-side matrix with one row per association site * @return solution of {@code hessianMatrix * x = rhs} */ - private SimpleMatrix applyHessianInv(SimpleMatrix rhs) { + private DenseMatrix applyHessianInv(DenseMatrix rhs) { if (hessianInvers != null) { - return hessianInvers.mult(rhs); + return multiply(hessianInvers, rhs); } if (hessianLU != null && hessianLUSize == totalNumberOfAccociationSites) { - DMatrixRMaj rhsMat = rhs.getDDRM(); - DMatrixRMaj out = new DMatrixRMaj(rhsMat.numRows, rhsMat.numCols); - hessianLU.solve(rhsMat, out); - return SimpleMatrix.wrap(out); + DenseMatrix out = new DenseMatrix(rhs.numRows, rhs.numCols); + LinearAlgebraOps.solveLu(hessianLU, rhs.numRows, rhs.numCols, (i, j) -> rhs.unsafe_get(i, j), + (i, j, value) -> out.unsafe_set(i, j, value)); + return out; } throw new IllegalStateException("Hessian factorization has not been initialized"); } @@ -873,8 +859,8 @@ public boolean solveX() { boolean solvedX = solveX2(15); - DMatrixRMaj mVectorMat = mVector.getMatrix(); - DMatrixRMaj ksiMatrixMat = ksiMatrix.getMatrix(); + DenseMatrix mVectorMat = mVector; + DenseMatrix ksiMatrixMat = ksiMatrix; // ksiMatrix.print(); // second order method not working correctly and not used t the moment b ecause of numerical @@ -883,12 +869,12 @@ public boolean solveX() { int iter = 0; for (int i = 0; i < numberOfComponents; i++) { for (int j = 0; j < componentArray[i].getNumberOfAssociationSites(); j++) { - mVectorMat.unsafe_set(temp + j, 0, componentArray[i].getNumberOfMolesInPhase()); + mVectorMat.unsafe_set(temp + j, 0, componentArray[i].getNumberOfMolesInPhase()); } temp += componentArray[i].getNumberOfAssociationSites(); } - DMatrixRMaj mat1 = KlkMatrix.getMatrix(); + DenseMatrix mat1 = KlkMatrix; double Klk = 0.0; double totvolume = getTotalVolume(); double tempVari; @@ -896,14 +882,13 @@ public boolean solveX() { for (int i = 0; i < getTotalNumberOfAccociationSites(); i++) { tempVari = mVectorMat.unsafe_get(i, 0); for (int j = i; j < getTotalNumberOfAccociationSites(); j++) { - tempVarj = mVectorMat.unsafe_get(j, 0); - Klk = tempVari * tempVarj / totvolume * delta[i][j]; - mat1.unsafe_set(i, j, Klk); - mat1.unsafe_set(j, i, Klk); + tempVarj = mVectorMat.unsafe_get(j, 0); + Klk = tempVari * tempVarj / totvolume * delta[i][j]; + mat1.unsafe_set(i, j, Klk); + mat1.unsafe_set(j, i, Klk); } } boolean solved = true; - // SimpleMatrix corrMatrix = null; do { solved = true; iter++; @@ -913,84 +898,80 @@ public boolean solveX() { double temp1; double temp2; for (int i = 0; i < numberOfComponents; i++) { - temp1 = componentArray[i].getNumberOfMolesInPhase(); - for (int j = 0; j < componentArray[i].getNumberOfAssociationSites(); j++) { - ksi = ((ComponentUMRCPA) componentArray[i]).getXsite()[j]; - ksiMatrixMat.unsafe_set(temp + j, 0, ksi); - // ksiMatrix.getMatrix().unsafe_set(temp + j, 0, - // ksiMatrix.getMatrix().unsafe_get(temp + j, 0)); - tempVari = 1.0 / ksi - 1.0; - udotMatrix.set(temp + j, 0, tempVari); - udotTimesmMatrix.set(temp + j, 0, temp1 * tempVari); - } - temp += componentArray[i].getNumberOfAssociationSites(); + temp1 = componentArray[i].getNumberOfMolesInPhase(); + for (int j = 0; j < componentArray[i].getNumberOfAssociationSites(); j++) { + ksi = ((ComponentUMRCPA) componentArray[i]).getXsite()[j]; + ksiMatrixMat.unsafe_set(temp + j, 0, ksi); + // ksiMatrix.getMatrix().unsafe_set(temp + j, 0, + // ksiMatrix.getMatrix().unsafe_get(temp + j, 0)); + tempVari = 1.0 / ksi - 1.0; + udotMatrix.unsafe_set(temp + j, 0, tempVari); + udotTimesmMatrix.unsafe_set(temp + j, 0, temp1 * tempVari); + } + temp += componentArray[i].getNumberOfAssociationSites(); } int krondelt; for (int i = 0; i < getTotalNumberOfAccociationSites(); i++) { - temp1 = mVectorMat.unsafe_get(i, 0); - temp2 = ksiMatrix.get(i, 0); - for (int j = i; j < getTotalNumberOfAccociationSites(); j++) { - krondelt = 0; - if (i == j) { - krondelt = 1; - } - tempVari = -temp1 / (temp2 * temp2) * krondelt - mat1.unsafe_get(i, j); - hessianMatrix.set(i, j, tempVari); - hessianMatrix.set(j, i, tempVari); - } + temp1 = mVectorMat.unsafe_get(i, 0); + temp2 = ksiMatrix.unsafe_get(i, 0); + for (int j = i; j < getTotalNumberOfAccociationSites(); j++) { + krondelt = 0; + if (i == j) { + krondelt = 1; + } + tempVari = -temp1 / (temp2 * temp2) * krondelt - mat1.unsafe_get(i, j); + hessianMatrix.unsafe_set(i, j, tempVari); + hessianMatrix.unsafe_set(j, i, tempVari); + } } - // ksiMatrix = new SimpleMatrix(ksi); - // SimpleMatrix hessianMatrix = new SimpleMatrix(hessian); int n = totalNumberOfAccociationSites; if (hessianLU == null || hessianLUSize != n) { - hessianLU = LinearSolverFactory_DDRM.lu(n); - hessianLUinput = new DMatrixRMaj(n, n); - hessianLUSize = n; + hessianLU = LU.PRIMITIVE.make(n, n); + hessianLUinput = new DenseMatrix(n, n); + hessianLUSize = n; } - System.arraycopy(hessianMatrix.getDDRM().getData(), 0, hessianLUinput.getData(), 0, n * n); - if (!hessianLU.setA(hessianLUinput)) { - return false; + System.arraycopy(hessianMatrix.getData(), 0, hessianLUinput.getData(), 0, n * n); + if (!LinearAlgebraOps.decomposeLu(hessianLU, n, (i, j) -> hessianLUinput.unsafe_get(i, j))) { + return false; } hessianInvers = null; if (solvedX) { - // System.out.println("solvedX "); - return true; + // System.out.println("solvedX "); + return true; } - DMatrixRMaj mat2 = ksiMatrix.getMatrix(); - CommonOps_DDRM.mult(mat1, mat2, corr2Matrix); - CommonOps_DDRM.subtract(udotTimesmMatrix.getDDRM(), corr2Matrix, corr3Matrix); - hessianLU.solve(corr3Matrix, corr4Matrix); - // SimpleMatrix gMatrix = udotTimesmMatrix.minus(KlkMatrix.mult(ksiMatrix)); + DenseMatrix mat2 = ksiMatrix; + LinearAlgebraOps.mult(mat1, mat2, corr2Matrix); + LinearAlgebraOps.subtract(udotTimesmMatrix, corr2Matrix, corr3Matrix); + LinearAlgebraOps.solveLu(hessianLU, corr3Matrix.numRows, corr3Matrix.numCols, + (i, j) -> corr3Matrix.unsafe_get(i, j), + (i, j, value) -> corr4Matrix.unsafe_set(i, j, value)); // corrMatrix = // hessianInvers.mult(udotTimesmMatrix.minus(KlkMatrix.mult(ksiMatrix))); // //.scale(-1.0); temp = 0; - // System.out.println("print SimpleMatrix ..."); // corrMatrix.print(10, 10); - // SimpleMatrix simp = new SimpleMatrix(corr4Matrix); // System.out.println("print CommonOps ..."); - // simp.print(10,10); double newX; for (int i = 0; i < numberOfComponents; i++) { - for (int j = 0; j < componentArray[i].getNumberOfAssociationSites(); j++) { - newX = ksiMatrix.get(temp + j, 0) - corr4Matrix.unsafe_get((temp + j), 0); - if (newX < 0) { - newX = 1e-10; - solved = false; - } - ((ComponentCPAInterface) componentArray[i]).setXsite(j, newX); - } - temp += componentArray[i].getNumberOfAssociationSites(); + for (int j = 0; j < componentArray[i].getNumberOfAssociationSites(); j++) { + newX = ksiMatrix.unsafe_get(temp + j, 0) - corr4Matrix.unsafe_get((temp + j), 0); + if (newX < 0) { + newX = 1e-10; + solved = false; + } + ((ComponentCPAInterface) componentArray[i]).setXsite(j, newX); + } + temp += componentArray[i].getNumberOfAssociationSites(); } // System.out.println("corrmatrix error " ); - // System.out.println("error " + NormOps_DDRM.normF(corr4Matrix)); - } while ((NormOps_DDRM.normF(corr4Matrix) > 1e-12 || !solved) && iter < 100); + // System.out.println("error " + LinearAlgebraOps.normF(corr4Matrix)); + } while ((LinearAlgebraOps.normF(corr4Matrix) > 1e-12 || !solved) && iter < 100); // System.out.println("iter " + iter + " error " + - // NormOps_DDRM.normF(corr4Matrix)); // corrMatrix.print(10, 10); + // LinearAlgebraOps.normF(corr4Matrix)); // corrMatrix.print(10, 10); // ksiMatrix.print(10, 10); return true; } @@ -1016,15 +997,15 @@ public boolean solveX2(int maxIter) { iter++; err = 0.0; for (int i = 0; i < totalNumberOfAccociationSites; i++) { - old = ((ComponentUMRCPA) componentArray[moleculeNumber[i]]).getXsite()[assSiteNumber[i]]; - neeval = 0.0; - for (int j = 0; j < totalNumberOfAccociationSites; j++) { - neeval += componentArray[moleculeNumber[j]].getNumberOfMolesInPhase() * delta[i][j] - * ((ComponentUMRCPA) componentArray[moleculeNumber[j]]).getXsite()[assSiteNumber[j]]; - } - neeval = 1.0 / (1.0 + 1.0 / totalVolume * neeval); - ((ComponentUMRCPA) componentArray[moleculeNumber[i]]).setXsite(assSiteNumber[i], neeval); - err += Math.abs((old - neeval) / neeval); + old = ((ComponentUMRCPA) componentArray[moleculeNumber[i]]).getXsite()[assSiteNumber[i]]; + neeval = 0.0; + for (int j = 0; j < totalNumberOfAccociationSites; j++) { + neeval += componentArray[moleculeNumber[j]].getNumberOfMolesInPhase() * delta[i][j] + * ((ComponentUMRCPA) componentArray[moleculeNumber[j]]).getXsite()[assSiteNumber[j]]; + } + neeval = 1.0 / (1.0 + 1.0 / totalVolume * neeval); + ((ComponentUMRCPA) componentArray[moleculeNumber[i]]).setXsite(assSiteNumber[i], neeval); + err += Math.abs((old - neeval) / neeval); } } while (Math.abs(err) > 1e-12 && iter < maxIter); // System.out.println("iter " + iter); @@ -1099,28 +1080,29 @@ public double calcRootVolFinder(PhaseType pt) { int solveXAttempts = 0; while (!solveX() && solveXAttempts < 50) { - solveXAttempts++; + solveXAttempts++; } if (solveXAttempts >= 50) { - // solveX failed to converge, skip this BonV value - oldh = h; - continue; + // solveX failed to converge, skip this BonV value + oldh = h; + continue; } - h = BonV - Btemp / numberOfMolesInPhase * dFdV() - pressure * Btemp / (numberOfMolesInPhase * R * temperature); + h = BonV - Btemp / numberOfMolesInPhase * dFdV() + - pressure * Btemp / (numberOfMolesInPhase * R * temperature); if (Math.signum(h) * Math.signum(oldh) < 0 && i > 2) { - if (solvedBonVlow < 1e-3) { - solvedBonVlow = (BonV + BonVold) / 2.0; - if (pt == PhaseType.GAS) { - break; - } - } else { - solvedBonVHigh = (BonV + BonVold) / 2.0; - if (pt == PhaseType.LIQUID) { - break; - } - } + if (solvedBonVlow < 1e-3) { + solvedBonVlow = (BonV + BonVold) / 2.0; + if (pt == PhaseType.GAS) { + break; + } + } else { + solvedBonVHigh = (BonV + BonVold) / 2.0; + if (pt == PhaseType.LIQUID) { + break; + } + } } solvedBonVHigh = (BonV + BonVold) / 2.0; oldh = h; @@ -1156,10 +1138,11 @@ public double calcRootVolFinder(PhaseType pt) { * @throws neqsim.util.exception.IsNaNException if any. * @throws neqsim.util.exception.TooManyIterationsException if any. */ - public double molarVolumeChangePhase(double pressure, double temperature, double A, double B, PhaseType pt) - throws neqsim.util.exception.IsNaNException, neqsim.util.exception.TooManyIterationsException { + public double molarVolumeChangePhase(double pressure, double temperature, double A, double B, + PhaseType pt) throws neqsim.util.exception.IsNaNException, + neqsim.util.exception.TooManyIterationsException { double BonV = pt == PhaseType.GAS ? 2.0 / (2.0 + temperature / getPseudoCriticalTemperature()) - : pressure * getB() / (numberOfMolesInPhase * temperature * R); + : pressure * getB() / (numberOfMolesInPhase * temperature * R); // double BonV = calcRootVolFinder(pt); // double BonVInit = BonV; if (BonV < 0) { @@ -1190,8 +1173,8 @@ public double molarVolumeChangePhase(double pressure, double temperature, double iterations++; gcpa = calc_g(); if (gcpa < 0) { - setMolarVolume(1.0 / Btemp / numberOfMolesInPhase); - gcpa = calc_g(); + setMolarVolume(1.0 / Btemp / numberOfMolesInPhase); + gcpa = calc_g(); } // lngcpa = @@ -1205,48 +1188,49 @@ public double molarVolumeChangePhase(double pressure, double temperature, double initCPAMatrix(1); double BonV2 = BonV * BonV; BonVold = BonV; - h = BonV - Btemp / numberOfMolesInPhase * dFdV() - pressure * Btemp / (numberOfMolesInPhase * R * temperature); + h = BonV - Btemp / numberOfMolesInPhase * dFdV() + - pressure * Btemp / (numberOfMolesInPhase * R * temperature); dh = 1.0 + Btemp / (BonV2) * (Btemp / numberOfMolesInPhase * dFdVdV()); dhh = -2.0 * Btemp / (BonV2 * BonV) * (Btemp / numberOfMolesInPhase * dFdVdV()) - - (Btemp * Btemp) / (BonV2 * BonV2) * (Btemp / numberOfMolesInPhase * dFdVdVdV()); + - (Btemp * Btemp) / (BonV2 * BonV2) * (Btemp / numberOfMolesInPhase * dFdVdVdV()); d1 = -h / dh; d2 = -dh / dhh; // System.out.println("d1" + d1 + " d2 " + d2 + " d1 / d2 " + (d1 / d2)); if (Math.abs(d1 / d2) <= 1.0) { - BonV += d1 * (1.0 + 0.5 * d1 / d2); + BonV += d1 * (1.0 + 0.5 * d1 / d2); } else if (d1 / d2 < -1) { - BonV += 0.5 * d1; + BonV += 0.5 * d1; } else if (d1 > d2) { - BonV += d2; - double hnew = h + d2 * dh; - if (Math.abs(hnew) > Math.abs(h)) { - BonV = pt == PhaseType.GAS ? 2.0 / (2.0 + temperature / getPseudoCriticalTemperature()) - : pressure * getB() / (numberOfMolesInPhase * temperature * R); - } + BonV += d2; + double hnew = h + d2 * dh; + if (Math.abs(hnew) > Math.abs(h)) { + BonV = pt == PhaseType.GAS ? 2.0 / (2.0 + temperature / getPseudoCriticalTemperature()) + : pressure * getB() / (numberOfMolesInPhase * temperature * R); + } } else { - BonV += 0.5 * d1; + BonV += 0.5 * d1; } if (Math.abs((BonV - BonVold) / BonVold) > 0.1) { - BonV = BonVold + 0.1 * (BonV - BonVold); + BonV = BonVold + 0.1 * (BonV - BonVold); } if (BonV > 1.1) { - if (iterations < 3) { - BonV = (BonVold + BonV) / 2.0; - } else { - BonV = pt == PhaseType.GAS ? 2.0 / (2.0 + temperature / getPseudoCriticalTemperature()) - : pressure * getB() / (numberOfMolesInPhase * temperature * R); - } + if (iterations < 3) { + BonV = (BonVold + BonV) / 2.0; + } else { + BonV = pt == PhaseType.GAS ? 2.0 / (2.0 + temperature / getPseudoCriticalTemperature()) + : pressure * getB() / (numberOfMolesInPhase * temperature * R); + } } if (BonV < 0) { - if (iterations < 3) { - BonV = Math.abs(BonVold + BonV) / 2.0; - } else { - BonV = pt == PhaseType.GAS ? 2.0 / (2.0 + temperature / getPseudoCriticalTemperature()) - : pressure * getB() / (numberOfMolesInPhase * temperature * R); - } + if (iterations < 3) { + BonV = Math.abs(BonVold + BonV) / 2.0; + } else { + BonV = pt == PhaseType.GAS ? 2.0 / (2.0 + temperature / getPseudoCriticalTemperature()) + : pressure * getB() / (numberOfMolesInPhase * temperature * R); + } } setMolarVolume(1.0 / BonV * Btemp / numberOfMolesInPhase); @@ -1257,8 +1241,8 @@ public double molarVolumeChangePhase(double pressure, double temperature, double } while ((Math.abs((BonV - BonVold) / BonV) > 1.0e-10) && iterations < 100); /* - * if (Math.abs(h) > 1e-8) { if (pt == 0) { molarVolume(pressure, temperature, A, B, 1); } else { - * molarVolume(pressure, temperature, A, B, 0); } return getMolarVolume(); } + * if (Math.abs(h) > 1e-8) { if (pt == 0) { molarVolume(pressure, temperature, A, B, 1); } else + * { molarVolume(pressure, temperature, A, B, 0); } return getMolarVolume(); } */ // System.out.println("Z" + Z + " iterations " + iterations + " BonV " + BonV); // System.out.println("pressure " + Z*R*temperature/getMolarVolume()); @@ -1272,7 +1256,8 @@ public double molarVolumeChangePhase(double pressure, double temperature, double // System.out.println("BonV: " + BonV + " "+" itert: " + iterations +" " +h + " " +dh + " B // " + Btemp + " gv" + gV() + " fv " + fv() + " fvv" + fVV()); if (Double.isNaN(getMolarVolume())) { - throw new neqsim.util.exception.IsNaNException(this, "molarVolumeChangePhase", "Molar volume"); + throw new neqsim.util.exception.IsNaNException(this, "molarVolumeChangePhase", + "Molar volume"); // System.out.println("BonV: " + BonV + " "+" itert: " + iterations +" " +h + " // " +dh + " B " + Btemp + " D " + Dtemp + " gv" + gV() + " fv " + fv() + " fvv" // + fVV()); @@ -1347,11 +1332,11 @@ public double[][] croeneckerProduct(double[][] a, double[][] b) { double[][] result = new double[aLength * bLength][(aCols) * (bCols)]; for (int z = 0; z < aLength; z++) { for (int i = 0; i < aCols; i++) { - for (int j = 0; j < bLength; j++) { - for (int k = 0; k < bCols; k++) { - result[j + (z * bLength)][k + (i * bCols)] = a[z][i] * b[j][k]; - } - } + for (int j = 0; j < bLength; j++) { + for (int k = 0; k < bCols; k++) { + result[j + (z * bLength)][k + (i * bCols)] = a[z][i] * b[j][k]; + } + } } } return result; @@ -1380,65 +1365,78 @@ public void setTotalNumberOfAccociationSites(int totalNumberOfAccociationSites) * @param pt a int * @param beta a double */ - public void initOld2(double totalNumberOfMoles, int numberOfComponents, int type, PhaseType pt, double beta) { + public void initOld2(double totalNumberOfMoles, int numberOfComponents, int type, PhaseType pt, + double beta) { // type = 0 start init, type = 1 gi ny betingelser if (type == 0) { setTotalNumberOfAccociationSites(0); selfAccociationScheme = new int[numberOfComponents][0][0]; crossAccociationScheme = new int[numberOfComponents][numberOfComponents][0][0]; for (int i = 0; i < numberOfComponents; i++) { - if (componentArray[i].getNumberOfmoles() < 1e-50) { - componentArray[i].setNumberOfAssociationSites(0); - } else { - componentArray[i].setNumberOfAssociationSites(componentArray[i].getOrginalNumberOfAssociationSites()); - setTotalNumberOfAccociationSites( - getTotalNumberOfAccociationSites() + componentArray[i].getNumberOfAssociationSites()); - selfAccociationScheme[i] = cpaSelect.setAssociationScheme(i, this); - for (int j = 0; j < numberOfComponents; j++) { - crossAccociationScheme[i][j] = cpaSelect.setCrossAssociationScheme(i, j, this); - } - } + if (componentArray[i].getNumberOfmoles() < 1e-50) { + componentArray[i].setNumberOfAssociationSites(0); + } else { + componentArray[i] + .setNumberOfAssociationSites(componentArray[i].getOrginalNumberOfAssociationSites()); + setTotalNumberOfAccociationSites( + getTotalNumberOfAccociationSites() + componentArray[i].getNumberOfAssociationSites()); + selfAccociationScheme[i] = cpaSelect.setAssociationScheme(i, this); + for (int j = 0; j < numberOfComponents; j++) { + crossAccociationScheme[i][j] = cpaSelect.setCrossAssociationScheme(i, j, this); + } + } } // had to remove if below - dont understand why.. Even // if (getTotalNumberOfAccociationSites() != oldTotalNumberOfAccociationSites) { - mVector = new SimpleMatrix(getTotalNumberOfAccociationSites(), 1); - KlkMatrix = new SimpleMatrix(getTotalNumberOfAccociationSites(), getTotalNumberOfAccociationSites()); - KlkVMatrix = new SimpleMatrix(getTotalNumberOfAccociationSites(), getTotalNumberOfAccociationSites()); - KlkVVMatrix = new SimpleMatrix(getTotalNumberOfAccociationSites(), getTotalNumberOfAccociationSites()); - KlkVVVMatrix = new SimpleMatrix(getTotalNumberOfAccociationSites(), getTotalNumberOfAccociationSites()); - hessianMatrix = new SimpleMatrix(getTotalNumberOfAccociationSites(), getTotalNumberOfAccociationSites()); - KlkTMatrix = new SimpleMatrix(getTotalNumberOfAccociationSites(), getTotalNumberOfAccociationSites()); - KlkTTMatrix = new SimpleMatrix(getTotalNumberOfAccociationSites(), getTotalNumberOfAccociationSites()); - KlkTVMatrix = new SimpleMatrix(getTotalNumberOfAccociationSites(), getTotalNumberOfAccociationSites()); - corr2Matrix = new DMatrixRMaj(getTotalNumberOfAccociationSites(), 1); - corr3Matrix = new DMatrixRMaj(getTotalNumberOfAccociationSites(), 1); - corr4Matrix = new DMatrixRMaj(getTotalNumberOfAccociationSites(), 1); - Klkni = new double[numberOfComponents][getTotalNumberOfAccociationSites()][getTotalNumberOfAccociationSites()]; - ksiMatrix = new SimpleMatrix(getTotalNumberOfAccociationSites(), 1); - uMatrix = new SimpleMatrix(getTotalNumberOfAccociationSites(), 1); - udotMatrix = new SimpleMatrix(getTotalNumberOfAccociationSites(), 1); + mVector = new DenseMatrix(getTotalNumberOfAccociationSites(), 1); + KlkMatrix = + new DenseMatrix(getTotalNumberOfAccociationSites(), getTotalNumberOfAccociationSites()); + KlkVMatrix = + new DenseMatrix(getTotalNumberOfAccociationSites(), getTotalNumberOfAccociationSites()); + KlkVVMatrix = + new DenseMatrix(getTotalNumberOfAccociationSites(), getTotalNumberOfAccociationSites()); + KlkVVVMatrix = + new DenseMatrix(getTotalNumberOfAccociationSites(), getTotalNumberOfAccociationSites()); + hessianMatrix = + new DenseMatrix(getTotalNumberOfAccociationSites(), getTotalNumberOfAccociationSites()); + KlkTMatrix = + new DenseMatrix(getTotalNumberOfAccociationSites(), getTotalNumberOfAccociationSites()); + KlkTTMatrix = + new DenseMatrix(getTotalNumberOfAccociationSites(), getTotalNumberOfAccociationSites()); + KlkTVMatrix = + new DenseMatrix(getTotalNumberOfAccociationSites(), getTotalNumberOfAccociationSites()); + corr2Matrix = new DenseMatrix(getTotalNumberOfAccociationSites(), 1); + corr3Matrix = new DenseMatrix(getTotalNumberOfAccociationSites(), 1); + corr4Matrix = new DenseMatrix(getTotalNumberOfAccociationSites(), 1); + Klkni = + new double[numberOfComponents][getTotalNumberOfAccociationSites()][getTotalNumberOfAccociationSites()]; + ksiMatrix = new DenseMatrix(getTotalNumberOfAccociationSites(), 1); + uMatrix = new DenseMatrix(getTotalNumberOfAccociationSites(), 1); + udotMatrix = new DenseMatrix(getTotalNumberOfAccociationSites(), 1); moleculeNumber = new int[getTotalNumberOfAccociationSites()]; assSiteNumber = new int[getTotalNumberOfAccociationSites()]; gvector = new double[getTotalNumberOfAccociationSites()][1]; - udotTimesmMatrix = new SimpleMatrix(getTotalNumberOfAccociationSites(), 1); + udotTimesmMatrix = new DenseMatrix(getTotalNumberOfAccociationSites(), 1); delta = new double[getTotalNumberOfAccociationSites()][getTotalNumberOfAccociationSites()]; deltaNog = new double[getTotalNumberOfAccociationSites()][getTotalNumberOfAccociationSites()]; deltadT = new double[getTotalNumberOfAccociationSites()][getTotalNumberOfAccociationSites()]; - deltadTdT = new double[getTotalNumberOfAccociationSites()][getTotalNumberOfAccociationSites()]; - QMatksiksiksi = new SimpleMatrix(getTotalNumberOfAccociationSites(), 1); + deltadTdT = + new double[getTotalNumberOfAccociationSites()][getTotalNumberOfAccociationSites()]; + QMatksiksiksi = new DenseMatrix(getTotalNumberOfAccociationSites(), 1); // } - udotTimesmiMatrix = new SimpleMatrix(getNumberOfComponents(), getTotalNumberOfAccociationSites()); + udotTimesmiMatrix = + new DenseMatrix(getNumberOfComponents(), getTotalNumberOfAccociationSites()); oldTotalNumberOfAccociationSites = getTotalNumberOfAccociationSites(); int temp = 0; for (int i = 0; i < numberOfComponents; i++) { - for (int j = 0; j < componentArray[i].getNumberOfAssociationSites(); j++) { - moleculeNumber[temp + j] = i; - assSiteNumber[temp + j] = j; - } - temp += componentArray[i].getNumberOfAssociationSites(); + for (int j = 0; j < componentArray[i].getNumberOfAssociationSites(); j++) { + moleculeNumber[temp + j] = i; + assSiteNumber[temp + j] = j; + } + temp += componentArray[i].getNumberOfAssociationSites(); } } if (cpamix == null) { @@ -1472,7 +1470,8 @@ public void initOld2(double totalNumberOfMoles, int numberOfComponents, int type /** {@inheritDoc} */ @Override public double molarVolume2(double pressure, double temperature, double A, double B, PhaseType pt) - throws neqsim.util.exception.IsNaNException, neqsim.util.exception.TooManyIterationsException { + throws neqsim.util.exception.IsNaNException, + neqsim.util.exception.TooManyIterationsException { Z = pt == PhaseType.LIQUID ? 1.0 : 1.0e-5; setMolarVolume(Z * R * temperature / pressure); // super.molarVolume(pressure,temperature, A, B, phase); @@ -1495,7 +1494,8 @@ public double molarVolume2(double pressure, double temperature, double A, double // System.out.println("pressure " + -R * temperature * dFdV + " " + R * // temperature / getMolarVolume()); // -pressure; - dErrdV = -R * temperature * dFdVdV - R * temperature * numberOfMolesInPhase / Math.pow(getVolume(), 2.0); + dErrdV = -R * temperature * dFdVdV + - R * temperature * numberOfMolesInPhase / Math.pow(getVolume(), 2.0); // System.out.println("errdV " + dErrdV); // System.out.println("err " + err); @@ -1505,8 +1505,8 @@ public double molarVolume2(double pressure, double temperature, double A, double Z = pressure * getMolarVolume() / (R * temperature); if (Z < 0) { - Z = 1e-6; - setMolarVolume(Z * R * temperature / pressure); + Z = 1e-6; + setMolarVolume(Z * R * temperature / pressure); } // System.out.println("Z " + Z); } while (Math.abs(err) > 1.0e-8 || iterations < 100); @@ -1521,231 +1521,7 @@ public double molarVolume2(double pressure, double temperature, double A, double * @param type a int */ public void initCPAMatrixOld(int type) { - if (getTotalNumberOfAccociationSites() == 0) { - FCPA = 0.0; - dFCPAdTdV = 0.0; - dFCPAdTdT = 0.0; - dFCPAdT = 0; - dFCPAdV = 0; - dFCPAdVdV = 0.0; - dFCPAdVdVdV = 0.0; - - return; - } - - int temp = 0; - for (int i = 0; i < numberOfComponents; i++) { - for (int j = 0; j < componentArray[i].getNumberOfAssociationSites(); j++) { - uMatrix.set(temp + j, 0, Math.log(ksiMatrix.get(temp + j, 0)) - ksiMatrix.get(temp + j, 0) + 1.0); - gvector[temp + j][0] = mVector.get(temp + j, 0) * udotMatrix.get(temp + j, 0); - } - temp += componentArray[i].getNumberOfAssociationSites(); - } - for (int i = 0; i < getNumberOfComponents(); i++) { - for (int j = 0; j < getTotalNumberOfAccociationSites(); j++) { - if (moleculeNumber[j] == i) { - udotTimesmiMatrix.set(i, j, udotMatrix.get(j, 0)); - } else { - udotTimesmiMatrix.set(i, j, 0.0); - } - } - } - - for (int i = 0; i < getTotalNumberOfAccociationSites(); i++) { - for (int j = i; j < getTotalNumberOfAccociationSites(); j++) { - delta[i][j] = deltaNog[i][j] * getGcpa(); - delta[j][i] = delta[i][j]; - if (type > 1) { - deltadT[i][j] = cpamix.calcDeltadT(assSiteNumber[i], assSiteNumber[j], moleculeNumber[i], moleculeNumber[j], - this, getTemperature(), getPressure(), numberOfComponents); - deltadT[j][i] = deltadT[i][j]; - - deltadTdT[i][j] = cpamix.calcDeltadTdT(assSiteNumber[i], assSiteNumber[j], moleculeNumber[i], - moleculeNumber[j], this, getTemperature(), getPressure(), numberOfComponents); - deltadTdT[j][i] = deltadTdT[i][j]; - } - } - } - - double totalVolume = getTotalVolume(); - double totalVolume2 = totalVolume * totalVolume; - double totalVolume3 = totalVolume2 * totalVolume; - double gdv1 = getGcpav() - 1.0 / totalVolume; - double gdv2 = gdv1 * gdv1; - double gdv3 = gdv2 * gdv1; - // double Klk = 0.0; - for (int i = 0; i < getTotalNumberOfAccociationSites(); i++) { - for (int j = i; j < getTotalNumberOfAccociationSites(); j++) { - KlkVMatrix.set(i, j, KlkMatrix.get(i, j) * gdv1); - KlkVMatrix.set(j, i, KlkVMatrix.get(i, j)); - - KlkVVMatrix.set(i, j, - KlkMatrix.get(i, j) * gdv2 + KlkMatrix.get(i, j) * (gcpavv + 1.0 / totalVolume / totalVolume)); - KlkVVMatrix.set(j, i, KlkVVMatrix.get(i, j)); - - KlkVVVMatrix.set(i, j, - KlkMatrix.get(i, j) * gdv3 - + 3.0 * KlkMatrix.get(i, j) * (getGcpav() - 1.0 / totalVolume) * (gcpavv + 1.0 / (totalVolume2)) - + KlkMatrix.get(i, j) * (gcpavvv - 2.0 / (totalVolume3))); - KlkVVVMatrix.set(j, i, KlkVVVMatrix.get(i, j)); - - if (type > 1) { - double tempVar = deltadT[i][j] / delta[i][j]; - double tempVardT = deltadTdT[i][j] / delta[i][j] - - (deltadT[i][j] * deltadT[i][j]) / (delta[i][j] * delta[i][j]); - - if (!Double.isNaN(tempVar)) { - // KlkdT[i][j] = KlkMatrix.getMatrix().unsafe_get(i, j) * tempVar; - // KlkdT[j][i] = KlkdT[i][j]; - - KlkTMatrix.set(i, j, KlkMatrix.get(i, j) * tempVar); - KlkTMatrix.set(j, i, KlkTMatrix.get(i, j)); - - KlkTVMatrix.set(i, j, KlkMatrix.get(i, j) * tempVar * (gcpav - 1.0 / totalVolume)); - KlkTVMatrix.set(j, i, KlkTVMatrix.get(i, j)); - - KlkTTMatrix.set(i, j, KlkMatrix.get(i, j) * (tempVar * tempVar + tempVardT)); - KlkTTMatrix.set(j, i, KlkTTMatrix.get(i, j)); - } - - if (type > 2) { - for (int p = 0; p < numberOfComponents; p++) { - double t1 = 0.0; - double t2 = 0.0; - if (moleculeNumber[i] == p) { - t1 = 1.0 / mVector.get(i, 0); - } - if (moleculeNumber[j] == p) { - t2 = 1.0 / mVector.get(j, 0); - } - Klkni[p][i][j] = KlkMatrix.get(i, j) * (t1 + t2 + ((ComponentUMRCPA) getComponent(p)).calc_lngi(this)); - Klkni[p][j][i] = Klkni[p][i][j]; - } - } - } - } - QMatksiksiksi.set(i, 0, - 2.0 * mVector.get(i, 0) / (ksiMatrix.get(i, 0) * ksiMatrix.get(i, 0) * ksiMatrix.get(i, 0))); - } - - SimpleMatrix ksiMatrixTranspose = ksiMatrix.transpose(); - - // dXdV - SimpleMatrix KlkVMatrixksi = KlkVMatrix.mult(ksiMatrix); - SimpleMatrix XV = hessianInvers.mult(KlkVMatrixksi); - SimpleMatrix XVtranspose = XV.transpose(); - - SimpleMatrix QCPA = mVector.transpose().mult(uMatrix.minus(ksiMatrix.elementMult(udotMatrix).scale(0.5))); - FCPA = QCPA.get(0, 0); - - SimpleMatrix tempMatrix = ksiMatrixTranspose.mult(KlkVMatrixksi).scale(-0.5); - dFCPAdV = tempMatrix.get(0, 0); - SimpleMatrix KlkVVMatrixTImesKsi = KlkVVMatrix.mult(ksiMatrix); - SimpleMatrix tempMatrixVV = ksiMatrixTranspose.mult(KlkVVMatrixTImesKsi).scale(-0.5) - .minus(KlkVMatrixksi.transpose().mult(XV)); - dFCPAdVdV = tempMatrixVV.get(0, 0); - - SimpleMatrix QVVV = ksiMatrixTranspose.mult(KlkVVVMatrix.mult(ksiMatrix)).scale(-0.5); - SimpleMatrix QVVksi = KlkVVMatrixTImesKsi.scale(-1.0); - SimpleMatrix QksiVksi = KlkVMatrix.scale(-1.0); - - SimpleMatrix mat1 = QVVksi.transpose().mult(XV).scale(3.0); - SimpleMatrix mat2 = XVtranspose.mult(QksiVksi.mult(XV)).scale(3.0); - SimpleMatrix mat4 = XVtranspose.mult(QMatksiksiksi.mult(XVtranspose)).mult(XV); - - SimpleMatrix dFCPAdVdVdVMatrix = QVVV.plus(mat1).plus(mat2).plus(mat2).plus(mat4); - dFCPAdVdVdV = dFCPAdVdVdVMatrix.get(0, 0); - temp = 0; - - if (type == 1) { - return; - } - for (int p = 0; p < numberOfComponents; p++) { - for (int kk = 0; kk < getComponent(p).getNumberOfAssociationSites(); kk++) { - ((ComponentCPAInterface) getComponent(p)).setXsitedV(kk, XV.get(temp + kk, 0)); - } - temp += getComponent(p).getNumberOfAssociationSites(); - } - - // KlkTMatrix = new SimpleMatrix(KlkdT); - SimpleMatrix KlkTMatrixTImesKsi = KlkTMatrix.mult(ksiMatrix); - // dQdT - SimpleMatrix tempMatrix2 = ksiMatrixTranspose.mult(KlkTMatrixTImesKsi).scale(-0.5); - dFCPAdT = tempMatrix2.get(0, 0); - - // SimpleMatrix KlkTVMatrix = new SimpleMatrix(KlkdTdV); - // SimpleMatrix tempMatrixTV = - // ksiMatrixTranspose.mult(KlkTVMatrix.mult(ksiMatrix)).scale(-0.5).minus(KlkTMatrixTImesKsi.transpose().mult(XV)); - // dFCPAdTdV = tempMatrixTV.get(0, 0); - // dXdT - SimpleMatrix XT = hessianInvers.mult(KlkTMatrixTImesKsi); - // dQdTdT - SimpleMatrix tempMatrixTT = ksiMatrixTranspose.mult(KlkTTMatrix.mult(ksiMatrix)).scale(-0.5) - .minus(KlkTMatrixTImesKsi.transpose().mult(XT)); - dFCPAdTdT = tempMatrixTT.get(0, 0); - - SimpleMatrix tempMatrixTV = ksiMatrixTranspose.mult(KlkTVMatrix.mult(ksiMatrix)).scale(-0.5) - .minus(KlkTMatrixTImesKsi.transpose().mult(XV)); - dFCPAdTdV = tempMatrixTV.get(0, 0); - - temp = 0; - for (int p = 0; p < numberOfComponents; p++) { - for (int kk = 0; kk < getComponent(p).getNumberOfAssociationSites(); kk++) { - ((ComponentCPAInterface) getComponent(p)).setXsitedT(kk, XT.get(temp + kk, 0)); - } - temp += getComponent(p).getNumberOfAssociationSites(); - } - - if (type == 2) { - return; - } - - // int assSites = 0; - // if(true) return; - for (int p = 0; p < numberOfComponents; p++) { - SimpleMatrix KiMatrix = new SimpleMatrix(Klkni[p]); - // KiMatrix.print(10,10); - // Matrix dQdniMatrix = - // (ksiMatrix.transpose().times(KiMatrix.times(ksiMatrix)).times(-0.5)); // this - // methods misses one part of .... - // dQdniMatrix.print(10,10); - // KiMatrix.print(10, 10); - // miMatrix.getMatrix(assSites, assSites, 0, totalNumberOfAccociationSites - - // 1).print(10, 10); - // Matrix tempMatrix20 = miMatrix.getMatrix(assSites, assSites, 0, - // totalNumberOfAccociationSites - - // 1).times(uMatrix).minus(ksiMatrix.transpose().times(KiMatrix.times(ksiMatrix)).times(-0.5)); - // - // ksiMatrix.transpose().times(KlkTMatrix.times(ksiMatrix)).times(-0.5); - // System.out.println("dQdn "); - // tempMatrix20.print(10, 10); - SimpleMatrix tempMatrix4 = KiMatrix.mult(ksiMatrix); - // udotTimesmiMatrix.getMatrix(assSites, assSites, 0, - // totalNumberOfAccociationSites - 1).print(10, 10); - SimpleMatrix tempMatrix5 = udotTimesmiMatrix.extractVector(true, p).transpose().minus(tempMatrix4); - // tempMki[0] = mki[p]; - // Matrix amatrix = new Matrix(croeneckerProduct(tempMki, - // udotMatrix.getArray())); - // System.out.println("aMatrix "); - // amatrix.transpose().print(10, 10); - // System.out.println("temp4 matrix"); - // tempMatrix4.print(10, 10); - // Matrix tempMatrix5 = amatrix.minus(tempMatrix4); - SimpleMatrix tempMatrix6 = hessianInvers.mult(tempMatrix5); // .scale(-1.0); - // System.out.println("dXdni"); - // tempMatrix4.print(10, 10); - // tempMatrix5.print(10, 10); - // System.out.println("dXdn "); - // tempMatrix6.print(10, 10); - int temp2 = 0; - for (int compp = 0; compp < numberOfComponents; compp++) { - for (int kk = 0; kk < getComponent(compp).getNumberOfAssociationSites(); kk++) { - ((ComponentCPAInterface) getComponent(compp)).setXsitedni(kk, p, -1.0 * tempMatrix6.get(temp2 + kk, 0)); - } - temp2 += getComponent(compp).getNumberOfAssociationSites(); - } - // assSites += getComponent(p).getNumberOfAssociationSites(); - } + initCPAMatrix(type); } /** @@ -1756,109 +1532,7 @@ public void initCPAMatrixOld(int type) { * @return a boolean */ public boolean solveXOld() { - if (getTotalNumberOfAccociationSites() == 0) { - return true; - } - - boolean solvedX = solveX2(5); - if (solvedX) { - // return true; - } - - DMatrixRMaj mat1 = KlkMatrix.getMatrix(); - DMatrixRMaj mat2 = ksiMatrix.getMatrix(); - // second order method not working correctly and not used t the moment b ecause of numerical - // stability - int temp = 0; - int iter = 0; - for (int i = 0; i < numberOfComponents; i++) { - for (int j = 0; j < componentArray[i].getNumberOfAssociationSites(); j++) { - mVector.set(temp + j, 0, componentArray[i].getNumberOfMolesInPhase()); - } - temp += componentArray[i].getNumberOfAssociationSites(); - } - double Klk = 0.0; - double totalVolume = getTotalVolume(); - for (int i = 0; i < getTotalNumberOfAccociationSites(); i++) { - for (int j = i; j < getTotalNumberOfAccociationSites(); j++) { - Klk = mVector.get(i, 0) * mVector.get(j, 0) / totalVolume * delta[i][j]; - KlkMatrix.set(i, j, Klk); - KlkMatrix.set(j, i, Klk); - } - } - boolean solved = true; - // SimpleMatrix corrMatrix = null; - do { - solved = true; - iter++; - temp = 0; - for (int i = 0; i < numberOfComponents; i++) { - for (int j = 0; j < componentArray[i].getNumberOfAssociationSites(); j++) { - ksiMatrix.set(temp + j, 0, ((ComponentUMRCPA) componentArray[i]).getXsite()[j]); - // ksiMatrix.getMatrix().unsafe_set(temp + j, 0, - // ksiMatrix.getMatrix().unsafe_get(temp + j, 0)); - udotMatrix.set(temp + j, 0, 1.0 / ksiMatrix.get(temp + j, 0) - 1.0); - udotTimesmMatrix.set(temp + j, 0, mVector.get(temp + j, 0) * udotMatrix.get(temp + j, 0)); - } - temp += componentArray[i].getNumberOfAssociationSites(); - } - - for (int i = 0; i < getTotalNumberOfAccociationSites(); i++) { - for (int j = i; j < getTotalNumberOfAccociationSites(); j++) { - int krondelt = 0; - if (i == j) { - krondelt = 1; - } - hessianMatrix.set(i, j, - -mVector.get(i, 0) / (ksiMatrix.get(i, 0) * ksiMatrix.get(i, 0)) * krondelt - KlkMatrix.get(i, j)); - hessianMatrix.set(j, i, hessianMatrix.get(i, j)); - } - } - - int n = totalNumberOfAccociationSites; - if (hessianLU == null || hessianLUSize != n) { - hessianLU = LinearSolverFactory_DDRM.lu(n); - hessianLUinput = new DMatrixRMaj(n, n); - hessianLUSize = n; - } - System.arraycopy(hessianMatrix.getDDRM().getData(), 0, hessianLUinput.getData(), 0, n * n); - if (!hessianLU.setA(hessianLUinput)) { - return false; - } - hessianInvers = null; - - CommonOps_DDRM.mult(mat1, mat2, corr2Matrix); - CommonOps_DDRM.subtract(udotTimesmMatrix.getDDRM(), corr2Matrix, corr3Matrix); - hessianLU.solve(corr3Matrix, corr4Matrix); - // SimpleMatrix gMatrix = udotTimesmMatrix.minus(KlkMatrix.mult(ksiMatrix)); - // corrMatrix = - // hessianInvers.mult(udotTimesmMatrix.minus(KlkMatrix.mult(ksiMatrix))); - // //.scale(-1.0); - temp = 0; - // System.out.println("print SimpleMatrix ..."); - // corrMatrix.print(10, 10); - // SimpleMatrix simp = new SimpleMatrix(corr4Matrix); - // System.out.println("print CommonOps ..."); - // simp.print(10,10); - for (int i = 0; i < numberOfComponents; i++) { - for (int j = 0; j < componentArray[i].getNumberOfAssociationSites(); j++) { - double newX = ksiMatrix.get(temp + j, 0) - corr4Matrix.unsafe_get((temp + j), 0); - if (newX < 0) { - newX = 1e-10; - solved = false; - } - ((ComponentCPAInterface) componentArray[i]).setXsite(j, newX); - } - temp += componentArray[i].getNumberOfAssociationSites(); - } - // System.out.println("corrmatrix error " ); - // System.out.println("error " + corrMatrix.norm1()); - } while ((NormOps_DDRM.normF(corr4Matrix) > 1e-12 || !solved) && iter < 100); - - // System.out.println("iter " + iter + " error " + NormOps.normF(corr4Matrix)); - // // corrMatrix.print(10, 10); - // ksiMatrix.print(10, 10); - return true; + return solveX(); } /** @@ -1870,37 +1544,7 @@ public boolean solveXOld() { * @return a boolean */ public boolean solveX2Old(int maxIter) { - double err = .0; - int iter = 0; - // if (delta == null) { - // initCPAMatrix(1); - double old = 0.0; - double neeval = 0.0; - double totalVolume = getTotalVolume(); - // } - do { - iter++; - err = 0.0; - for (int i = 0; i < getTotalNumberOfAccociationSites(); i++) { - old = ((ComponentUMRCPA) getComponent(moleculeNumber[i])).getXsite()[assSiteNumber[i]]; - neeval = 0; - for (int j = 0; j < getTotalNumberOfAccociationSites(); j++) { - neeval += getComponent(moleculeNumber[j]).getNumberOfMolesInPhase() * delta[i][j] - * ((ComponentUMRCPA) getComponent(moleculeNumber[j])).getXsite()[assSiteNumber[j]]; - } - neeval = 1.0 / (1.0 + 1.0 / totalVolume * neeval); - ((ComponentCPAInterface) getComponent(moleculeNumber[i])).setXsite(assSiteNumber[i], neeval); - err += Math.abs((old - neeval) / neeval); - } - } while (Math.abs(err) > 1e-10 && iter < maxIter); - // System.out.println("iter " + iter); - // if (Math.abs(err) - // < 1e-12) { - // return true; - // } else { - // System.out.println("did not solve for Xi in iterations: " + iter); - // System.out.println("error: " + err); - return false; + return solveX2(maxIter); } /** @@ -1917,10 +1561,12 @@ public boolean solveX2Old(int maxIter) { * @throws neqsim.util.exception.IsNaNException if any. * @throws neqsim.util.exception.TooManyIterationsException if any. */ - public double molarVolumeOld(double pressure, double temperature, double A, double B, PhaseType pt) - throws neqsim.util.exception.IsNaNException, neqsim.util.exception.TooManyIterationsException { - double BonV = pt == PhaseType.LIQUID ? 2.0 / (2.0 + temperature / getPseudoCriticalTemperature()) - : pressure * getB() / (numberOfMolesInPhase * temperature * R); + public double molarVolumeOld(double pressure, double temperature, double A, double B, + PhaseType pt) throws neqsim.util.exception.IsNaNException, + neqsim.util.exception.TooManyIterationsException { + double BonV = + pt == PhaseType.LIQUID ? 2.0 / (2.0 + temperature / getPseudoCriticalTemperature()) + : pressure * getB() / (numberOfMolesInPhase * temperature * R); // if (pressure > 1000) { // BonV = 0.9999; // } @@ -1954,8 +1600,8 @@ public double molarVolumeOld(double pressure, double temperature, double A, doub iterations++; gcpa = calc_g(); if (gcpa < 0) { - setMolarVolume(1.0 / Btemp / numberOfMolesInPhase); - gcpa = calc_g(); + setMolarVolume(1.0 / Btemp / numberOfMolesInPhase); + gcpa = calc_g(); } // lngcpa = @@ -1965,62 +1611,64 @@ public double molarVolumeOld(double pressure, double temperature, double A, doub gcpavvv = calc_lngVVV(); if (getTotalNumberOfAccociationSites() > 0) { - solveX(); + solveX(); } initCPAMatrix(1); double BonV2 = BonV * BonV; BonVold = BonV; - h = BonV - Btemp / numberOfMolesInPhase * dFdV() - pressure * Btemp / (numberOfMolesInPhase * R * temperature); + h = BonV - Btemp / numberOfMolesInPhase * dFdV() + - pressure * Btemp / (numberOfMolesInPhase * R * temperature); dh = 1.0 + Btemp / (BonV2) * (Btemp / numberOfMolesInPhase * dFdVdV()); dhh = -2.0 * Btemp / (BonV2 * BonV) * (Btemp / numberOfMolesInPhase * dFdVdV()) - - (Btemp * Btemp) / (BonV2 * BonV2) * (Btemp / numberOfMolesInPhase * dFdVdVdV()); + - (Btemp * Btemp) / (BonV2 * BonV2) * (Btemp / numberOfMolesInPhase * dFdVdVdV()); d1 = -h / dh; d2 = -dh / dhh; // System.out.println("h " + h + " iter " + iterations + " " + d1 + " d2 " + d2 // + " d1 / d2 " + (d1 / d2)); if (Math.abs(d1 / d2) <= 1.0) { - BonV += d1 * (1.0 + 0.5 * d1 / d2); + BonV += d1 * (1.0 + 0.5 * d1 / d2); } else if (d1 / d2 < -1) { - BonV += 0.5 * d1; + BonV += 0.5 * d1; } else if (d1 > d2) { - BonV += d2; - double hnew = h + d2 * dh; - if (Math.abs(hnew) > Math.abs(h)) { - BonV = pt == PhaseType.GAS ? 2.0 / (2.0 + temperature / getPseudoCriticalTemperature()) - : pressure * getB() / (numberOfMolesInPhase * temperature * R); - } + BonV += d2; + double hnew = h + d2 * dh; + if (Math.abs(hnew) > Math.abs(h)) { + BonV = pt == PhaseType.GAS ? 2.0 / (2.0 + temperature / getPseudoCriticalTemperature()) + : pressure * getB() / (numberOfMolesInPhase * temperature * R); + } } else { - BonV += 0.5 * d1; + BonV += 0.5 * d1; } if (Math.abs((BonV - BonVold) / BonVold) > 0.1) { - BonV = BonVold + 0.1 * (BonV - BonVold); + BonV = BonVold + 0.1 * (BonV - BonVold); } if (BonV > 0.9999) { - if (iterations < 3) { - BonV = (BonVold + BonV) / 2.0; - } else { - // return molarVolumeChangePhase(pressure, temperature, A, B, pt); - // BonV = 0.9999; - // BonV = pt == 1 ? 2.0 / (2.0 + temperature / - // getPseudoCriticalTemperature()) : pressure * getB() / (numberOfMolesInPhase * - // temperature * R); - } + if (iterations < 3) { + BonV = (BonVold + BonV) / 2.0; + } else { + // return molarVolumeChangePhase(pressure, temperature, A, B, pt); + // BonV = 0.9999; + // BonV = pt == 1 ? 2.0 / (2.0 + temperature / + // getPseudoCriticalTemperature()) : pressure * getB() / (numberOfMolesInPhase * + // temperature * R); + } } else if (BonV < 0) { - if (iterations < 3) { - BonV = Math.abs(BonVold + BonV) / 2.0; - } else { - // return molarVolumeChangePhase(pressure, temperature, A, B, pt); - // BonV = pt == 1 ? 2.0 / (2.0 + temperature / - // getPseudoCriticalTemperature()) : pressure * getB() / (numberOfMolesInPhase * - // temperature * R); - } + if (iterations < 3) { + BonV = Math.abs(BonVold + BonV) / 2.0; + } else { + // return molarVolumeChangePhase(pressure, temperature, A, B, pt); + // BonV = pt == 1 ? 2.0 / (2.0 + temperature / + // getPseudoCriticalTemperature()) : pressure * getB() / (numberOfMolesInPhase * + // temperature * R); + } } setMolarVolume(1.0 / BonV * Btemp / numberOfMolesInPhase); Z = pressure * getMolarVolume() / (R * temperature); - } while ((Math.abs((BonV - BonVold) / BonV) > 1.0e-10 || Math.abs(h) > 1e-12) && iterations < 100); + } while ((Math.abs((BonV - BonVold) / BonV) > 1.0e-10 || Math.abs(h) > 1e-12) + && iterations < 100); if (Math.abs(h) > 1e-12) { // System.out.println("h failed " + "Z" + Z + " iterations " + iterations + " @@ -2047,4 +1695,120 @@ public double molarVolumeOld(double pressure, double temperature, double A, doub } return getMolarVolume(); } + + /** + * Returns the transpose of a dense matrix. + * + * @param matrix input matrix + * @return transposed matrix + */ + private static DenseMatrix transpose(DenseMatrix matrix) { + DenseMatrix out = new DenseMatrix(matrix.numCols, matrix.numRows); + for (int i = 0; i < matrix.numRows; i++) { + for (int j = 0; j < matrix.numCols; j++) { + out.unsafe_set(j, i, matrix.unsafe_get(i, j)); + } + } + return out; + } + + /** + * Multiplies two dense matrices. + * + * @param left left matrix + * @param right right matrix + * @return matrix product + */ + private static DenseMatrix multiply(DenseMatrix left, DenseMatrix right) { + DenseMatrix out = new DenseMatrix(left.numRows, right.numCols); + LinearAlgebraOps.mult(left, right, out); + return out; + } + + /** + * Adds two dense matrices. + * + * @param left left matrix + * @param right right matrix + * @return sum matrix + */ + private static DenseMatrix add(DenseMatrix left, DenseMatrix right) { + DenseMatrix out = new DenseMatrix(left.numRows, left.numCols); + for (int i = 0; i < left.numRows; i++) { + for (int j = 0; j < left.numCols; j++) { + out.unsafe_set(i, j, left.unsafe_get(i, j) + right.unsafe_get(i, j)); + } + } + return out; + } + + /** + * Subtracts two dense matrices. + * + * @param left left matrix + * @param right right matrix + * @return difference matrix + */ + private static DenseMatrix subtract(DenseMatrix left, DenseMatrix right) { + DenseMatrix out = new DenseMatrix(left.numRows, left.numCols); + LinearAlgebraOps.subtract(left, right, out); + return out; + } + + /** + * Scales all elements in a dense matrix. + * + * @param matrix input matrix + * @param factor scale factor + * @return scaled matrix + */ + private static DenseMatrix scale(DenseMatrix matrix, double factor) { + DenseMatrix out = new DenseMatrix(matrix.numRows, matrix.numCols); + for (int i = 0; i < matrix.numRows; i++) { + for (int j = 0; j < matrix.numCols; j++) { + out.unsafe_set(i, j, matrix.unsafe_get(i, j) * factor); + } + } + return out; + } + + /** + * Element-wise multiplication of two dense matrices. + * + * @param left left matrix + * @param right right matrix + * @return Hadamard product + */ + private static DenseMatrix elementMult(DenseMatrix left, DenseMatrix right) { + DenseMatrix out = new DenseMatrix(left.numRows, left.numCols); + for (int i = 0; i < left.numRows; i++) { + for (int j = 0; j < left.numCols; j++) { + out.unsafe_set(i, j, left.unsafe_get(i, j) * right.unsafe_get(i, j)); + } + } + return out; + } + + /** + * Extracts one row or column as a matrix view copy. + * + * @param matrix source matrix + * @param extractRow true to extract a row, false to extract a column + * @param index row/column index + * @return extracted row/column matrix + */ + private static DenseMatrix extractVector(DenseMatrix matrix, boolean extractRow, int index) { + if (extractRow) { + DenseMatrix row = new DenseMatrix(1, matrix.numCols); + for (int j = 0; j < matrix.numCols; j++) { + row.unsafe_set(0, j, matrix.unsafe_get(index, j)); + } + return row; + } + DenseMatrix col = new DenseMatrix(matrix.numRows, 1); + for (int i = 0; i < matrix.numRows; i++) { + col.unsafe_set(i, 0, matrix.unsafe_get(i, index)); + } + return col; + } } diff --git a/src/main/java/neqsim/thermodynamicoperations/flashops/CriticalPointFlash.java b/src/main/java/neqsim/thermodynamicoperations/flashops/CriticalPointFlash.java index 2699167844..c114efe3a2 100644 --- a/src/main/java/neqsim/thermodynamicoperations/flashops/CriticalPointFlash.java +++ b/src/main/java/neqsim/thermodynamicoperations/flashops/CriticalPointFlash.java @@ -2,16 +2,17 @@ import org.apache.logging.log4j.LogManager; import org.apache.logging.log4j.Logger; -import org.ejml.data.Complex_F64; -import org.ejml.data.DMatrixRMaj; -import org.ejml.dense.row.factory.DecompositionFactory_DDRM; -import org.ejml.interfaces.decomposition.EigenDecomposition_F64; -import org.ejml.simple.SimpleMatrix; +import org.ojalgo.array.Array1D; +import org.ojalgo.matrix.decomposition.Eigenvalue; +import org.ojalgo.matrix.store.MatrixStore; +import org.ojalgo.matrix.store.Primitive64Store; +import org.ojalgo.scalar.ComplexNumber; import neqsim.thermo.system.SystemInterface; +import neqsim.util.math.LinearAlgebraOps; /** *

- * CriticalPointFlash class. + * Critical-point flash calculation using the Heidemann and Khalil criterion. *

* * @author esol @@ -23,9 +24,7 @@ public class CriticalPointFlash extends Flash { /** Logger object for class. */ static Logger logger = LogManager.getLogger(CriticalPointFlash.class); - SimpleMatrix Mmatrix = null; - SimpleMatrix Nmatrix = null; - SimpleMatrix fmatrix = null; + private Primitive64Store Mmatrix = null; int numberOfComponents; double Vc0; double Tc0; @@ -39,11 +38,8 @@ public class CriticalPointFlash extends Flash { */ public CriticalPointFlash(SystemInterface system) { this.system = system; - // clonedsystem = system.clone(); numberOfComponents = system.getPhase(0).getNumberOfComponents(); - Mmatrix = new SimpleMatrix(numberOfComponents, numberOfComponents); - Nmatrix = new SimpleMatrix(numberOfComponents, numberOfComponents); - fmatrix = new SimpleMatrix(numberOfComponents, 1); + Mmatrix = Primitive64Store.FACTORY.make(numberOfComponents, numberOfComponents); } /** @@ -71,55 +67,79 @@ public void calcMmatrix() { + (system.getPhase(0).getComponent(i).getdfugdn(j) + system.getPhase(0).getComponent(i).getdfugdp() * system.getPhase(0).getComponent(j).getVoli() * system.getPhase(0).getdPdVTn() * -1.0)); - Mmatrix.set(i, j, - Math.sqrt(system.getPhase(0).getComponent(i).getz() * system.getPhase(0).getComponent(j).getz()) * tempJ); + Mmatrix.set(i, j, Math.sqrt( + system.getPhase(0).getComponent(i).getz() * system.getPhase(0).getComponent(j).getz()) + * tempJ); } } - // Q is theoretically symmetric; symmetrize to guarantee real eigenvalues/eigenvectors. - Mmatrix = Mmatrix.plus(Mmatrix.transpose()).scale(0.5); + LinearAlgebraOps.symmetriseMmatrix(Mmatrix); } /** *

- * Returns the eigenvector associated with the eigenvalue of {@link #Mmatrix} that is closest to zero. + * Returns the eigenvector associated with the eigenvalue of {@link #Mmatrix} that is closest to + * zero. *

* *

- * At a mixture critical point the smallest eigenvalue of the Q matrix vanishes, and the corresponding eigenvector - * defines the direction of the critical composition perturbation (Heidemann & Khalil, 1980). Selecting the - * eigenvector by smallest eigenvalue magnitude — rather than by a fixed index — ensures the correct - * critical direction is used and avoids returning a null (complex) eigenvector. + * At a mixture critical point the smallest eigenvalue of the Q matrix vanishes, and the + * corresponding eigenvector defines the direction of the critical composition perturbation + * (Heidemann & Khalil, 1980). Selecting the eigenvector by smallest eigenvalue magnitude + * — rather than by a fixed index — ensures the correct critical direction is used and + * avoids returning a null (complex) eigenvector. *

* - * @return the eigenvector of the smallest-magnitude eigenvalue, or {@code null} if no real eigenvector is available + * @return the eigenvector of the smallest-magnitude eigenvalue, or {@code null} if no eigenvector + * is available */ - public SimpleMatrix getCriticalEigenVector() { - // Use the dedicated symmetric eigen decomposition. The Q matrix is symmetrized in - // calcMmatrix(), so a symmetric solver is guaranteed to return real eigenvalues and - // non-null (real) eigenvectors, unlike the generic SimpleMatrix.eig() which can flag - // near-zero imaginary parts as complex and then return null eigenvectors. - EigenDecomposition_F64 evd = DecompositionFactory_DDRM.eig(numberOfComponents, true, true); - if (!evd.decompose(Mmatrix.getMatrix().copy())) { + public double[] getCriticalEigenVector() { + Eigenvalue evd = Eigenvalue.PRIMITIVE.make(numberOfComponents, true); + if (!evd.decompose(Mmatrix.copy())) { return null; } - int n = evd.getNumberOfEigenvalues(); + + Array1D eigenvalues = evd.getEigenvalues(); + MatrixStore eigenvectors = evd.getV(); + int n = (int) eigenvalues.count(); int bestIndex = -1; double smallestMagnitude = Double.POSITIVE_INFINITY; + for (int idx = 0; idx < n; idx++) { - if (evd.getEigenVector(idx) == null) { - continue; - } - Complex_F64 eigenvalue = evd.getEigenvalue(idx); + ComplexNumber eigenvalue = eigenvalues.get(idx); double magnitude = Math.abs(eigenvalue.getReal()); if (magnitude < smallestMagnitude) { - smallestMagnitude = magnitude; - bestIndex = idx; + smallestMagnitude = magnitude; + bestIndex = idx; } } if (bestIndex < 0) { return null; } - return SimpleMatrix.wrap(evd.getEigenVector(bestIndex)); + + double[] vector = new double[numberOfComponents]; + for (int row = 0; row < numberOfComponents; row++) { + vector[row] = eigenvectors.doubleValue(row, bestIndex); + } + return vector; + } + + /** + * Calculates the quadratic form $v^T M v$ for a vector and matrix. + * + * @param vector perturbation direction vector + * @param matrix matrix used in the quadratic form + * @return scalar value of $v^T M v$ + */ + private double quadraticForm(double[] vector, Primitive64Store matrix) { + double result = 0.0; + for (int i = 0; i < vector.length; i++) { + double rowSum = 0.0; + for (int j = 0; j < vector.length; j++) { + rowSum += matrix.doubleValue(i, j) * vector[j]; + } + result += vector[i] * rowSum; + } + return result; } /** @@ -130,8 +150,8 @@ public SimpleMatrix getCriticalEigenVector() { * @return a double */ public double calcdpd() { - double[] oldz = system.getMolarRate(); - SimpleMatrix eigenVector = getCriticalEigenVector(); + final double[] oldz = system.getMolarRate(); + double[] eigenVector = getCriticalEigenVector(); if (eigenVector == null) { return Double.NaN; } @@ -141,24 +161,20 @@ public double calcdpd() { double sperturb = 1e-3; for (int ii = 0; ii < numberOfComponents; ii++) { newz1[ii] = system.getPhase(0).getComponent(ii).getz() - + sperturb * eigenVector.get(ii) * Math.sqrt(system.getPhase(0).getComponent(ii).getz()); + + sperturb * eigenVector[ii] * Math.sqrt(system.getPhase(0).getComponent(ii).getz()); newz2[ii] = system.getPhase(0).getComponent(ii).getz() - - sperturb * eigenVector.get(ii) * Math.sqrt(system.getPhase(0).getComponent(ii).getz()); + - sperturb * eigenVector[ii] * Math.sqrt(system.getPhase(0).getComponent(ii).getz()); } system.setMolarComposition(newz1); system.init(3); calcMmatrix(); - // eigenVector = Mmatrix.eig().getEigenVector(0); - SimpleMatrix evalMatrix = eigenVector.transpose().mult(Mmatrix).mult(eigenVector); - double perturb1 = evalMatrix.get(0, 0); + final double perturb1 = quadraticForm(eigenVector, Mmatrix); system.setMolarComposition(newz2); system.init(3); calcMmatrix(); - // eigenVector = Mmatrix.eig().getEigenVector(0); - evalMatrix = eigenVector.transpose().mult(Mmatrix).mult(eigenVector); - double perturb2 = evalMatrix.get(0, 0); + double perturb2 = quadraticForm(eigenVector, Mmatrix); system.setMolarComposition(oldz); system.init(3); @@ -188,16 +204,13 @@ public void run() { // system.display(); for (int k = 0; k < 13; k++) { - double detM; - double olddetM; - double ddetdT; double dT = 0.1; calcMmatrix(); - SimpleMatrix eigenVector = getCriticalEigenVector(); + double[] eigenVector = getCriticalEigenVector(); if (eigenVector == null) { - break; + break; } - SimpleMatrix evalMatrix = eigenVector.transpose().mult(Mmatrix).mult(eigenVector); + double evalMatrix = quadraticForm(eigenVector, Mmatrix); // Heidemann & Khalil (1980): the temperature is adjusted to drive the smallest eigenvalue of // the symmetric Q matrix to zero. The Rayleigh quotient eigenVector' Q eigenVector equals // that @@ -205,67 +218,66 @@ public void run() { // the // determinant (a product of all eigenvalues) and avoids the spurious roots the determinant // has. - detM = evalMatrix.get(0, 0); + double detM = evalMatrix; int iter = 0; system.setTemperature(system.getTemperature() + dT); do { - system.init(3); - iter++; - olddetM = detM; - calcMmatrix(); - eigenVector = getCriticalEigenVector(); - if (eigenVector == null) { - break; - } - evalMatrix = eigenVector.transpose().mult(Mmatrix).mult(eigenVector); - detM = evalMatrix.get(0, 0); - ddetdT = (detM - olddetM) / dT; - if (ddetdT == 0.0 || Double.isNaN(ddetdT)) { - break; - } - dT = -detM / ddetdT; - // Limit the Newton step so the search does not jump into a non-physical region where the - // EOS returns NaN properties (which would make the Q matrix non-decomposable). - if (Math.abs(dT) > 5.0) { - dT = Math.signum(dT) * 5.0; - } - double oldTemp = system.getTemperature(); - system.setTemperature(oldTemp + dT); - logger.info("Temperature " + oldTemp + " dT " + dT + " evalMatrix " + evalMatrix.get(0, 0)); + system.init(3); + iter++; + double olddetM = detM; + calcMmatrix(); + eigenVector = getCriticalEigenVector(); + if (eigenVector == null) { + break; + } + evalMatrix = quadraticForm(eigenVector, Mmatrix); + detM = evalMatrix; + double ddetdT = (detM - olddetM) / dT; + if (ddetdT == 0.0 || Double.isNaN(ddetdT)) { + break; + } + dT = -detM / ddetdT; + // Limit the Newton step so the search does not jump into a non-physical region where the + // EOS returns NaN properties (which would make the Q matrix non-decomposable). + if (Math.abs(dT) > 5.0) { + dT = Math.signum(dT) * 5.0; + } + double oldTemp = system.getTemperature(); + system.setTemperature(oldTemp + dT); + logger.info("Temperature " + oldTemp + " dT " + dT + " evalMatrix " + evalMatrix); } while (Math.abs(dT) > 1e-8 && iter < 112); double dVc = Vc0 / 100.0; - double ddetdV; - double oldVal; system.init(3); double valstart = calcdpd(); if (Double.isNaN(valstart)) { - break; + break; } iter = 0; system.getPhase(0).setTotalVolume(system.getPhase(0).getTotalVolume() + dVc); double dVOld = 1111110; do { - oldVal = valstart; - system.init(3); - iter++; - valstart = calcdpd(); - if (Double.isNaN(valstart)) { - break; - } - ddetdV = (valstart - oldVal) / dVc; - if (ddetdV == 0.0 || Double.isNaN(ddetdV)) { - break; - } - dVOld = dVc; - dVc = -valstart / ddetdV; - system.getPhase(0).setTotalVolume(system.getPhase(0).getVolume() + 0.5 * dVc); - logger.info("Volume " + system.getPhase(0).getVolume() + " dVc " + dVc + " tddpp " + valstart + " pressure " - + system.getPressure()); + double oldVal = valstart; + system.init(3); + iter++; + valstart = calcdpd(); + if (Double.isNaN(valstart)) { + break; + } + double ddetdV = (valstart - oldVal) / dVc; + if (ddetdV == 0.0 || Double.isNaN(ddetdV)) { + break; + } + dVOld = dVc; + dVc = -valstart / ddetdV; + system.getPhase(0).setTotalVolume(system.getPhase(0).getVolume() + 0.5 * dVc); + logger.info("Volume " + system.getPhase(0).getVolume() + " dVc " + dVc + " tddpp " + + valstart + " pressure " + system.getPressure()); } while (Math.abs(dVc) > 1e-5 && iter < 112 && (Math.abs(dVc) < Math.abs(dVOld) || iter < 3)); } system.setUseTVasIndependentVariables(false); system.init(3); } } + diff --git a/src/main/java/neqsim/thermodynamicoperations/flashops/SysNewtonRhapsonTPflash.java b/src/main/java/neqsim/thermodynamicoperations/flashops/SysNewtonRhapsonTPflash.java index f34f8a6e0a..b3e75e51af 100644 --- a/src/main/java/neqsim/thermodynamicoperations/flashops/SysNewtonRhapsonTPflash.java +++ b/src/main/java/neqsim/thermodynamicoperations/flashops/SysNewtonRhapsonTPflash.java @@ -1,24 +1,24 @@ package neqsim.thermodynamicoperations.flashops; -import org.ejml.data.DMatrixRMaj; -import org.ejml.dense.row.factory.LinearSolverFactory_DDRM; -import org.ejml.interfaces.linsol.LinearSolverDense; +import org.ojalgo.matrix.decomposition.LU; +import org.ojalgo.matrix.store.Primitive64Store; import neqsim.thermo.system.SystemInterface; +import neqsim.util.math.LinearAlgebraOps; /** * Newton-Raphson solver for two-phase TP flash using Michelsen's u-variable formulation. * *

- * Variables: u[i] = beta * y[i] (Michelsen 1982b). Residual: g[i] = ln(f_gas_i) - ln(f_liq_i). Jacobian: Hessian of the - * reduced Gibbs energy Q(u). Uses Armijo backtracking line search on Q for global convergence (Michelsen & Mollerup - * 2007, Ch. 12). + * Variables: u[i] = beta * y[i] (Michelsen 1982b). Residual: g[i] = ln(f_gas_i) - ln(f_liq_i). + * Jacobian: Hessian of the reduced Gibbs energy Q(u). Uses Armijo backtracking line search on Q for + * global convergence (Michelsen & Mollerup 2007, Ch. 12). *

* *

* Performance optimizations vs standard implementation: *

*
    - *
  • EJML dense solver instead of JAMA (~2x faster linear solve for n>=10)
  • + *
  • ojAlgo dense LU solver instead of JAMA (~2x faster linear solve for n>=10)
  • *
  • Pre-allocated solver and work buffers (zero allocation in solve loop)
  • *
* @@ -44,18 +44,21 @@ public class SysNewtonRhapsonTPflash implements java.io.Serializable { /** Cached feed compositions (constant during flash). */ private double[] z; - // Pre-allocated EJML matrices for zero-allocation Newton steps + // Pre-allocated matrices for zero-allocation Newton steps /** Jacobian matrix (Hessian of Q). */ - private DMatrixRMaj jacMatrix; + private Primitive64Store jacMatrix; /** Residual vector (gradient of Q). */ - private DMatrixRMaj fvecVector; + private double[] fvecVector; + + /** Pre-allocated right-hand-side vector for linear solve. */ + private Primitive64Store rhsVector; /** Newton step vector. */ - private DMatrixRMaj dxVector; + private double[] dxVector; /** Work copy of Jacobian for LU decomposition. */ - private DMatrixRMaj jacWork; + private Primitive64Store jacWork; /** u-variable array: u[i] = beta * y[i]. */ private double[] uVector; @@ -63,8 +66,8 @@ public class SysNewtonRhapsonTPflash implements java.io.Serializable { /** Trial u-vector used by the Armijo line search. */ private double[] uTrialVector; - /** Pre-allocated EJML LU solver. */ - private transient LinearSolverDense linearSolver; + /** Pre-allocated ojAlgo LU solver. */ + private transient LU linearSolver; /** *

@@ -75,19 +78,21 @@ public class SysNewtonRhapsonTPflash implements java.io.Serializable { * @param numberOfPhases a int * @param numberOfComponents a int */ - public SysNewtonRhapsonTPflash(SystemInterface system, int numberOfPhases, int numberOfComponents) { + public SysNewtonRhapsonTPflash(SystemInterface system, int numberOfPhases, + int numberOfComponents) { this.system = system; this.numberOfComponents = numberOfComponents; neq = numberOfComponents; - // Pre-allocate EJML matrices - jacMatrix = new DMatrixRMaj(neq, neq); - fvecVector = new DMatrixRMaj(neq, 1); - dxVector = new DMatrixRMaj(neq, 1); - jacWork = new DMatrixRMaj(neq, neq); + // Pre-allocate matrix and vector workspaces + jacMatrix = Primitive64Store.FACTORY.make(neq, neq); + fvecVector = new double[neq]; + rhsVector = Primitive64Store.FACTORY.make(neq, 1); + dxVector = new double[neq]; + jacWork = Primitive64Store.FACTORY.make(neq, neq); uVector = new double[neq]; uTrialVector = new double[neq]; - linearSolver = LinearSolverFactory_DDRM.lu(neq); + linearSolver = LU.PRIMITIVE.make(neq, neq); setu(); z = new double[numberOfComponents]; @@ -97,8 +102,8 @@ public SysNewtonRhapsonTPflash(SystemInterface system, int numberOfPhases, int n } /** - * Number of components this solver was sized for. Used by the caller to detect when the cached instance is stale - * (e.g. after solid precipitation removed a component). + * Number of components this solver was sized for. Used by the caller to detect when the cached + * instance is stale (e.g. after solid precipitation removed a component). * * @return number of components */ @@ -107,8 +112,9 @@ public int getNumberOfComponents() { } /** - * Re-bind this solver to a (possibly updated) system reference and refresh the cached feed composition, without - * re-allocating the EJML matrices. The component count must match — call {@link #getNumberOfComponents()} first. + * Re-bind this solver to a (possibly updated) system reference and refresh the cached feed + * composition, without re-allocating the matrix workspaces. The component count must match — call + * {@link #getNumberOfComponents()} first. * * @param system system to solve */ @@ -129,10 +135,11 @@ public void setSystem(SystemInterface system) { */ public void setfvec() { for (int i = 0; i < numberOfComponents; i++) { - fvecVector.set(i, 0, Math - .log(system.getPhase(0).getComponent(i).getFugacityCoefficient() * system.getPhase(0).getComponent(i).getx()) - - Math.log( - system.getPhase(1).getComponent(i).getFugacityCoefficient() * system.getPhase(1).getComponent(i).getx())); + fvecVector[i] = Math + .log(system.getPhase(0).getComponent(i).getFugacityCoefficient() + * system.getPhase(0).getComponent(i).getx()) + - Math.log(system.getPhase(1).getComponent(i).getFugacityCoefficient() + * system.getPhase(1).getComponent(i).getx()); } } @@ -150,10 +157,11 @@ public void setJac() { double invYi = 1.0 / system.getPhase(0).getComponent(i).getx(); double invXi = 1.0 / system.getPhase(1).getComponent(i).getx(); for (int j = 0; j < numberOfComponents; j++) { - dij = i == j ? 1.0 : 0.0; - tempJ = invBeta * (dij * invYi - 1.0 + system.getPhase(0).getComponent(i).getdfugdx(j)) - + invOneMinusBeta * (dij * invXi - 1.0 + system.getPhase(1).getComponent(i).getdfugdx(j)); - jacMatrix.set(i, j, tempJ); + dij = i == j ? 1.0 : 0.0; + tempJ = invBeta * (dij * invYi - 1.0 + system.getPhase(0).getComponent(i).getdfugdx(j)) + + invOneMinusBeta + * (dij * invXi - 1.0 + system.getPhase(1).getComponent(i).getdfugdx(j)); + jacMatrix.set(i, j, tempJ); } } } @@ -184,8 +192,8 @@ public void init() { for (int i = 0; i < numberOfComponents; i++) { system.getPhase(0).getComponent(i).setx(uVector[i] / betaSum); system.getPhase(1).getComponent(i).setx((z[i] - uVector[i]) / (1.0 - betaSum)); - system.getPhase(0).getComponent(i) - .setK(system.getPhase(0).getComponent(i).getx() / system.getPhase(1).getComponent(i).getx()); + system.getPhase(0).getComponent(i).setK( + system.getPhase(0).getComponent(i).getx() / system.getPhase(1).getComponent(i).getx()); system.getPhase(1).getComponent(i).setK(system.getPhase(0).getComponent(i).getK()); } @@ -194,8 +202,8 @@ public void init() { } /** - * Compute Michelsen's reduced Gibbs energy Q(u). Q = sum[u_i * ln(y_i * phi_gas_i) + (z_i - u_i) * ln(x_i * - * phi_liq_i)]. Gradient of Q equals fvec; Hessian of Q equals Jac. + * Compute Michelsen's reduced Gibbs energy Q(u). Q = sum[u_i * ln(y_i * phi_gas_i) + (z_i - u_i) + * * ln(x_i * phi_liq_i)]. Gradient of Q equals fvec; Hessian of Q equals Jac. * * @return Q value */ @@ -221,7 +229,7 @@ private boolean isFeasible(double[] uTrial) { double betaTrial = 0.0; for (int i = 0; i < numberOfComponents; i++) { if (uTrial[i] < 1e-15 || uTrial[i] > z[i] - 1e-15) { - return false; + return false; } betaTrial += uTrial[i]; } @@ -229,8 +237,8 @@ private boolean isFeasible(double[] uTrial) { } /** - * Set compositions from trial u vector and compute fugacities only (init level 1). Used for line search Q evaluation - * at trial points. + * Set compositions from trial u vector and compute fugacities only (init level 1). Used for line + * search Q evaluation at trial points. * * @param uTrial trial u vector */ @@ -249,11 +257,11 @@ private void setTrialAndComputeFugacities(double[] uTrial) { } /** - * Lazily initializes the EJML solver after deserialization. + * Lazily initializes the ojAlgo LU solver after deserialization. */ private void ensureSolverInitialized() { if (linearSolver == null) { - linearSolver = LinearSolverFactory_DDRM.lu(neq); + linearSolver = LU.PRIMITIVE.make(neq, neq); } if (uTrialVector == null || uTrialVector.length != numberOfComponents) { uTrialVector = new double[numberOfComponents]; @@ -286,15 +294,24 @@ public double solve() { jacMatrix.set(i, i, jacMatrix.get(i, i) + lambda); } - // Solve J * dx = fvec using EJML (pre-allocated work buffer) - jacWork.setTo(jacMatrix); - linearSolver.setA(jacWork); - linearSolver.solve(fvecVector, dxVector); + // Solve J * dx = fvec using ojAlgo LU (pre-allocated work buffer) + for (int i = 0; i < neq; i++) { + for (int j = 0; j < neq; j++) { + jacWork.set(i, j, jacMatrix.get(i, j)); + } + } + for (int i = 0; i < neq; i++) { + rhsVector.set(i, 0, fvecVector[i]); + } + + if (!LinearAlgebraOps.solveLinearSystem(jacWork, rhsVector, dxVector, linearSolver)) { + throw new IllegalStateException("Failed to decompose Jacobian matrix in Newton solver"); + } // Directional derivative: slope = fvec^T * dx = grad(Q)^T * (Jac^-1 * grad(Q)) > 0 double slope = 0.0; for (int i = 0; i < neq; i++) { - slope += fvecVector.get(i, 0) * dxVector.get(i, 0); + slope += fvecVector[i] * dxVector[i]; } // Armijo backtracking line search on Michelsen Q function @@ -303,20 +320,20 @@ public double solve() { int maxBacktrack = 8; for (int bt = 0; bt < maxBacktrack; bt++) { for (int i = 0; i < numberOfComponents; i++) { - uTrialVector[i] = uVector[i] - alpha * dxVector.get(i, 0); + uTrialVector[i] = uVector[i] - alpha * dxVector[i]; } if (isFeasible(uTrialVector)) { - try { - setTrialAndComputeFugacities(uTrialVector); - double qTrial = computeQ(); - // Armijo condition: Q_trial <= Q_current - c1 * alpha * slope - if (qTrial <= qCurrent - c1 * alpha * slope) { - break; - } - } catch (Exception ex) { - // Cubic solver failed at trial point — try shorter step - } + try { + setTrialAndComputeFugacities(uTrialVector); + double qTrial = computeQ(); + // Armijo condition: Q_trial <= Q_current - c1 * alpha * slope + if (qTrial <= qCurrent - c1 * alpha * slope) { + break; + } + } catch (Exception ex) { + // Cubic solver failed at trial point — try shorter step + } } alpha *= 0.5; @@ -326,7 +343,7 @@ public double solve() { double stepNormSq = 0.0; double uNormSq = 0.0; for (int i = 0; i < numberOfComponents; i++) { - double step = alpha * dxVector.get(i, 0); + double step = alpha * dxVector[i]; uVector[i] -= step; stepNormSq += step * step; uNormSq += uVector[i] * uVector[i]; diff --git a/src/main/java/neqsim/thermodynamicoperations/flashops/TPmultiflash.java b/src/main/java/neqsim/thermodynamicoperations/flashops/TPmultiflash.java index 29ca2a74cb..fe532321cb 100644 --- a/src/main/java/neqsim/thermodynamicoperations/flashops/TPmultiflash.java +++ b/src/main/java/neqsim/thermodynamicoperations/flashops/TPmultiflash.java @@ -10,12 +10,10 @@ import java.util.ArrayList; import org.apache.logging.log4j.LogManager; import org.apache.logging.log4j.Logger; -import org.ejml.data.DMatrixRMaj; -import org.ejml.dense.row.CommonOps_DDRM; -import org.ejml.simple.SimpleMatrix; import neqsim.thermo.component.ComponentInterface; import neqsim.thermo.phase.PhaseType; import neqsim.thermo.system.SystemInterface; +import neqsim.util.math.LinearAlgebraOps; /** *

@@ -81,8 +79,7 @@ public TPmultiflash(SystemInterface system, boolean checkForSolids) { * calcMultiPhaseBeta. *

*/ - public void calcMultiPhaseBeta() { - } + public void calcMultiPhaseBeta() {} /** *

@@ -106,35 +103,35 @@ public void setXY() { boolean isAqueous = system.getPhase(k).getType() == PhaseType.AQUEOUS; for (int i = 0; i < system.getPhase(0).getNumberOfComponents(); i++) { - if (system.getPhase(0).getComponent(i).getz() > 1e-100) { - // Check for ions - ions can only exist in aqueous phases - // This check must happen regardless of isChemicalSystem() status - if (system.getPhase(0).getComponent(i).getIonicCharge() != 0 - || system.getPhase(0).getComponent(i).isIsIon()) { - // Ions only exist in aqueous phases, near-zero in gas/oil - if (isAqueous) { - // In aqueous phase, calculate ion x from moles - double totalMoles = system.getPhase(k).getNumberOfMolesInPhase(); - if (totalMoles > 1e-100) { - system.getPhase(k).getComponent(i) - .setx(system.getPhase(k).getComponent(i).getNumberOfmoles() / totalMoles); - } else { - system.getPhase(k).getComponent(i).setx(system.getPhase(0).getComponent(i).getz()); - } - } else { - // No ions in gas or oil phases - system.getPhase(k).getComponent(i).setx(1e-50); - } - } else { - // Non-ionic components: normal flash calculation - double newX = system.getPhase(0).getComponent(i).getz() / Erow[i] - / system.getPhase(k).getComponent(i).getFugacityCoefficient(); - if (!Double.isFinite(newX) || newX <= 0.0) { - newX = Math.max(system.getPhase(0).getComponent(i).getz(), 1.0e-30); - } - system.getPhase(k).getComponent(i).setx(newX); - } - } + if (system.getPhase(0).getComponent(i).getz() > 1e-100) { + // Check for ions - ions can only exist in aqueous phases + // This check must happen regardless of isChemicalSystem() status + if (system.getPhase(0).getComponent(i).getIonicCharge() != 0 + || system.getPhase(0).getComponent(i).isIsIon()) { + // Ions only exist in aqueous phases, near-zero in gas/oil + if (isAqueous) { + // In aqueous phase, calculate ion x from moles + double totalMoles = system.getPhase(k).getNumberOfMolesInPhase(); + if (totalMoles > 1e-100) { + system.getPhase(k).getComponent(i) + .setx(system.getPhase(k).getComponent(i).getNumberOfmoles() / totalMoles); + } else { + system.getPhase(k).getComponent(i).setx(system.getPhase(0).getComponent(i).getz()); + } + } else { + // No ions in gas or oil phases + system.getPhase(k).getComponent(i).setx(1e-50); + } + } else { + // Non-ionic components: normal flash calculation + double newX = system.getPhase(0).getComponent(i).getz() / Erow[i] + / system.getPhase(k).getComponent(i).getFugacityCoefficient(); + if (!Double.isFinite(newX) || newX <= 0.0) { + newX = Math.max(system.getPhase(0).getComponent(i).getz(), 1.0e-30); + } + system.getPhase(k).getComponent(i).setx(newX); + } + } } system.getPhase(k).normalize(); @@ -151,14 +148,15 @@ public void calcE() { for (int i = 0; i < system.getPhase(0).getNumberOfComponents(); i++) { Erow[i] = 0.0; for (int k = 0; k < system.getNumberOfPhases(); k++) { - Erow[i] += system.getPhase(k).getBeta() / system.getPhase(k).getComponent(i).getFugacityCoefficient(); + Erow[i] += system.getPhase(k).getBeta() + / system.getPhase(k).getComponent(i).getFugacityCoefficient(); } if (Erow[i] < 1e-100) { - Erow[i] = 1e-100; + Erow[i] = 1e-100; } if (Double.isNaN(Erow[i])) { - logger.error("Erow is NaN for component " + system.getPhase(0).getComponent(i).getName()); - Erow[i] = 1e-100; + logger.error("Erow is NaN for component " + system.getPhase(0).getComponent(i).getName()); + Erow[i] = 1e-100; } } } @@ -189,30 +187,31 @@ public double calcQ() { for (int k = 0; k < system.getNumberOfPhases(); k++) { dQdbeta[k][0] = 1.0; for (int i = 0; i < system.getPhase(0).getNumberOfComponents(); i++) { - dQdbeta[k][0] -= multTerm[i] / system.getPhase(k).getComponent(i).getFugacityCoefficient(); + dQdbeta[k][0] -= multTerm[i] / system.getPhase(k).getComponent(i).getFugacityCoefficient(); } } for (int i = 0; i < system.getNumberOfPhases(); i++) { for (int j = 0; j < system.getNumberOfPhases(); j++) { - Qmatrix[i][j] = 0.0; - for (int k = 0; k < system.getPhase(0).getNumberOfComponents(); k++) { - Qmatrix[i][j] += multTerm2[k] / (system.getPhase(j).getComponent(k).getFugacityCoefficient() - * system.getPhase(i).getComponent(k).getFugacityCoefficient()); - } - if (i == j) { - double reg = 1.0e-3; - if (shouldApplyEnhancedMultiPhaseCheck()) { - double absDiag = Math.abs(Qmatrix[i][j]); - double beta = Math.abs(system.getPhase(i).getBeta()); - // Keep strong regularization for near-singular small-beta phases, - // but reduce bias in well-conditioned enhanced-mode cases. - if (beta > 1.0e-8 && absDiag > 1.0e-8) { - reg = Math.max(1.0e-12, absDiag * 1.0e-8); - } - } - Qmatrix[i][j] += reg; - } + Qmatrix[i][j] = 0.0; + for (int k = 0; k < system.getPhase(0).getNumberOfComponents(); k++) { + Qmatrix[i][j] += + multTerm2[k] / (system.getPhase(j).getComponent(k).getFugacityCoefficient() + * system.getPhase(i).getComponent(k).getFugacityCoefficient()); + } + if (i == j) { + double reg = 1.0e-3; + if (shouldApplyEnhancedMultiPhaseCheck()) { + double absDiag = Math.abs(Qmatrix[i][j]); + double beta = Math.abs(system.getPhase(i).getBeta()); + // Keep strong regularization for near-singular small-beta phases, + // but reduce bias in well-conditioned enhanced-mode cases. + if (beta > 1.0e-8 && absDiag > 1.0e-8) { + reg = Math.max(1.0e-12, absDiag * 1.0e-8); + } + } + Qmatrix[i][j] += reg; + } } } return Q; @@ -226,67 +225,75 @@ public double calcQ() { * @return a double */ public double solveBeta() { - SimpleMatrix betaMatrix = new SimpleMatrix(1, system.getNumberOfPhases()); - SimpleMatrix ans = null; + double[] betaVector = new double[system.getNumberOfPhases()]; + double[] ans = new double[system.getNumberOfPhases()]; double err = 1.0; double gradResidual = 1.0; int iter = 1; do { iter++; for (int k = 0; k < system.getNumberOfPhases(); k++) { - betaMatrix.set(0, k, system.getPhase(k).getBeta()); + betaVector[k] = system.getPhase(k).getBeta(); } calcQ(); - SimpleMatrix dQM = new SimpleMatrix(dQdbeta); - gradResidual = dQM.normF(); - SimpleMatrix dQdBM = new SimpleMatrix(Qmatrix); + gradResidual = LinearAlgebraOps.columnNorm(dQdbeta); + double[] rhs = new double[system.getNumberOfPhases()]; + for (int k = 0; k < system.getNumberOfPhases(); k++) { + rhs[k] = dQdbeta[k][0]; + } try { - ans = dQdBM.solve(dQM).transpose(); + if (!LinearAlgebraOps.solveLinearSystem(Qmatrix, rhs, ans)) { + throw new RuntimeException("LU decomposition failed in solveBeta"); + } } catch (Exception ex) { - if (shouldApplyEnhancedMultiPhaseCheck()) { - for (int kk = 0; kk < system.getNumberOfPhases(); kk++) { - Qmatrix[kk][kk] += 1.0e-2; - } - dQdBM = new SimpleMatrix(Qmatrix); - try { - ans = dQdBM.solve(dQM).transpose(); - } catch (Exception ex2) { - logger.error(ex2.getMessage()); - break; - } - } else { - logger.error(ex.getMessage()); - break; - } - } - - betaMatrix = betaMatrix.minus(ans.scale(iter / (iter + 3.0))); + if (shouldApplyEnhancedMultiPhaseCheck()) { + for (int kk = 0; kk < system.getNumberOfPhases(); kk++) { + Qmatrix[kk][kk] += 1.0e-2; + } + try { + if (!LinearAlgebraOps.solveLinearSystem(Qmatrix, rhs, ans)) { + throw new RuntimeException("Regularized LU decomposition failed in solveBeta"); + } + } catch (Exception ex2) { + logger.error(ex2.getMessage()); + break; + } + } else { + logger.error(ex.getMessage()); + break; + } + } + + double damping = iter / (iter + 3.0); + for (int k = 0; k < betaVector.length; k++) { + betaVector[k] -= ans[k] * damping; + } removePhase = false; for (int k = 0; k < system.getNumberOfPhases(); k++) { - double currBeta = betaMatrix.get(0, k); - if (currBeta < phaseFractionMinimumLimit) { - system.setBeta(k, phaseFractionMinimumLimit); - if (checkOneRemove) { - if (system.getPhase(k).getType() == PhaseType.GAS) { - system.setPhaseType(k, PhaseType.LIQUID); - } - checkOneRemove = false; - removePhase = true; - } - checkOneRemove = true; - } else if (currBeta > (1.0 - phaseFractionMinimumLimit)) { - system.setBeta(k, 1.0 - phaseFractionMinimumLimit); - } else { - system.setBeta(k, currBeta); - } + double currBeta = betaVector[k]; + if (currBeta < phaseFractionMinimumLimit) { + system.setBeta(k, phaseFractionMinimumLimit); + if (checkOneRemove) { + if (system.getPhase(k).getType() == PhaseType.GAS) { + system.setPhaseType(k, PhaseType.LIQUID); + } + checkOneRemove = false; + removePhase = true; + } + checkOneRemove = true; + } else if (currBeta > (1.0 - phaseFractionMinimumLimit)) { + system.setBeta(k, 1.0 - phaseFractionMinimumLimit); + } else { + system.setBeta(k, currBeta); + } } system.normalizeBeta(); system.init(1); calcE(); setXY(); system.init(1); - err = ans.normF(); + err = LinearAlgebraOps.vectorNorm(ans); } while (((err > 1e-12 || gradResidual > 1e-10) && iter < 50) || iter < 3); // logger.info("iterations " + iter); return err; @@ -309,11 +316,12 @@ private void requestBoundedRerun() { } /** - * Remove a duplicate phase while conserving its mass by merging its phase fraction into the surviving - * (near-identical) phase before removal. Two phases flagged as numerical duplicates have essentially identical - * mole-fraction vectors, so the merged phase fraction is simply the sum of the two betas. Without this merge the - * removed phase's mass leaks into the remaining phases through {@code normalizeBeta()}, which halves trace liquid - * dropout (see UMR-PRU trace oil regression). + * Remove a duplicate phase while conserving its mass by merging its phase fraction into the + * surviving (near-identical) phase before removal. Two phases flagged as numerical duplicates + * have essentially identical mole-fraction vectors, so the merged phase fraction is simply the + * sum of the two betas. Without this merge the removed phase's mass leaks into the remaining + * phases through {@code normalizeBeta()}, which halves trace liquid dropout (see UMR-PRU trace + * oil regression). * * @param keepPhase index of the phase to keep * @param removePhase2 index of the duplicate phase to remove @@ -331,7 +339,8 @@ private void mergeAndRemoveDuplicatePhase(int keepPhase, int removePhase2) { @Override public void stabilityAnalysis() { double[] logWi = new double[system.getPhase(0).getNumberOfComponents()]; - double[][] Wi = new double[system.getPhase(0).getNumberOfComponents()][system.getPhase(0).getNumberOfComponents()]; + double[][] Wi = new double[system.getPhase(0).getNumberOfComponents()][system.getPhase(0) + .getNumberOfComponents()]; double[] deltalogWi = new double[system.getPhases()[0].getNumberOfComponents()]; double[] oldDeltalogWi = new double[system.getPhases()[0].getNumberOfComponents()]; @@ -341,7 +350,8 @@ public void stabilityAnalysis() { double[] oldoldlogw = new double[system.getPhases()[0].getNumberOfComponents()]; double[] oldoldoldlogw = new double[system.getPhases()[0].getNumberOfComponents()]; double[] d = new double[system.getPhase(0).getNumberOfComponents()]; - double[][] x = new double[system.getPhase(0).getNumberOfComponents()][system.getPhase(0).getNumberOfComponents()]; + double[][] x = new double[system.getPhase(0).getNumberOfComponents()][system.getPhase(0) + .getNumberOfComponents()]; tm = new double[system.getPhase(0).getNumberOfComponents()]; double[] alpha = null; @@ -354,50 +364,55 @@ public void stabilityAnalysis() { clonedSystem.add(system.clone()); /* * for (int i = 0; i < system.getPhase(0).getNumberOfComponents(); i++) { if - * (system.getPhase(0).getComponent(i).getx() < 1e-100) { clonedSystem.add(null); continue; } double numb = 0; - * clonedSystem.add(system.clone()); + * (system.getPhase(0).getComponent(i).getx() < 1e-100) { clonedSystem.add(null); continue; } + * double numb = 0; clonedSystem.add(system.clone()); * * // (clonedSystem.get(i)).init(0); commented out sept 2005, Even S. for (int j = 0; j < - * system.getPhase(0).getNumberOfComponents(); j++) { numb = i == j ? 1.0 : 1.0e-12; // set to 0 by Even Solbraa - * 23.01.2013 - chaged back to 1.0e-12 27.04.13 if (system.getPhase(0).getComponent(j).getz() < 1e-100) { numb = 0; - * } ( clonedSystem.get(i)).getPhase(1).getComponent(j).setx(numb); } if - * (system.getPhase(0).getComponent(i).getIonicCharge() == 0) { ( clonedSystem.get(i)).init(1); } } + * system.getPhase(0).getNumberOfComponents(); j++) { numb = i == j ? 1.0 : 1.0e-12; // set to 0 + * by Even Solbraa 23.01.2013 - chaged back to 1.0e-12 27.04.13 if + * (system.getPhase(0).getComponent(j).getz() < 1e-100) { numb = 0; } ( + * clonedSystem.get(i)).getPhase(1).getComponent(j).setx(numb); } if + * (system.getPhase(0).getComponent(i).getIonicCharge() == 0) { ( clonedSystem.get(i)).init(1); + * } } */ lowestGibbsEnergyPhase = 0; /* * // logger.info("low gibbs phase " + lowestGibbsEnergyPhase); for (int k = 0; k < * minimumGibbsEnergySystem.getPhase(0).getNumberOfComponents(); k++) { for (int i = 0; i < - * minimumGibbsEnergySystem.getPhase(0).getNumberOfComponents(); i++) { if (!(( clonedSystem.get(k)) == null)) { - * sumw[k] += ( clonedSystem.get(k)).getPhase(1).getComponent(i).getx(); } } } + * minimumGibbsEnergySystem.getPhase(0).getNumberOfComponents(); i++) { if (!(( + * clonedSystem.get(k)) == null)) { sumw[k] += ( + * clonedSystem.get(k)).getPhase(1).getComponent(i).getx(); } } } * - * for (int k = 0; k < minimumGibbsEnergySystem.getPhase(0).getNumberOfComponents(); k++) { for (int i = 0; i < - * minimumGibbsEnergySystem.getPhase(0).getNumberOfComponents(); i++) { if (!(( clonedSystem.get(k)) == null) && - * system.getPhase(0).getComponent(k).getx() > 1e-100) { ( clonedSystem.get(k)).getPhase(1).getComponent(i).setx(( + * for (int k = 0; k < minimumGibbsEnergySystem.getPhase(0).getNumberOfComponents(); k++) { for + * (int i = 0; i < minimumGibbsEnergySystem.getPhase(0).getNumberOfComponents(); i++) { if (!(( + * clonedSystem.get(k)) == null) && system.getPhase(0).getComponent(k).getx() > 1e-100) { ( + * clonedSystem.get(k)).getPhase(1).getComponent(i).setx(( * clonedSystem.get(k)).getPhase(1).getComponent(i).getx() / sumw[k]); } logger.info("x: " + ( - * clonedSystem.get(k)).getPhase(0).getComponent(i).getx()); } if (system.getPhase(0).getComponent(k).getx() > - * 1e-100) { d[k] = Math.log(system.getPhase(0).getComponent(k).getx()) + + * clonedSystem.get(k)).getPhase(0).getComponent(i).getx()); } if + * (system.getPhase(0).getComponent(k).getx() > 1e-100) { d[k] = + * Math.log(system.getPhase(0).getComponent(k).getx()) + * system.getPhase(0).getComponent(k).getLogFugacityCoefficient(); - * if(minimumGibbsEnergySystem.getPhases()[lowestGibbsEnergyPhase].getComponents ()[k].getIonicCharge()!=0) d[k]=0; - * } //logger.info("dk: " + d[k]); } + * if(minimumGibbsEnergySystem.getPhases()[lowestGibbsEnergyPhase].getComponents + * ()[k].getIonicCharge()!=0) d[k]=0; } //logger.info("dk: " + d[k]); } */ // Calculate reference fugacities d[k] = ln(x_k) + ln(phi_k) for feed phase for (int k = 0; k < minimumGibbsEnergySystem.getPhase(0).getNumberOfComponents(); k++) { if (system.getPhase(0).getComponent(k).getx() > 1e-100) { - d[k] = Math.log(system.getPhase(0).getComponent(k).getx()) - + system.getPhase(0).getComponent(k).getLogFugacityCoefficient(); - // if(minimumGibbsEnergySystem.getPhases()[lowestGibbsEnergyPhase].getComponent(k).getIonicCharge()!=0) - // d[k]=0; + d[k] = Math.log(system.getPhase(0).getComponent(k).getx()) + + system.getPhase(0).getComponent(k).getLogFugacityCoefficient(); + // if(minimumGibbsEnergySystem.getPhases()[lowestGibbsEnergyPhase].getComponent(k).getIonicCharge()!=0) + // d[k]=0; } } // Initialize logWi array (will be overwritten for each trial) for (int j = 0; j < minimumGibbsEnergySystem.getPhase(0).getNumberOfComponents(); j++) { if (system.getPhase(0).getComponent(j).getz() > 1e-100) { - logWi[j] = 0.0; + logWi[j] = 0.0; } else { - logWi[j] = -10000.0; + logWi[j] = -10000.0; } } @@ -412,20 +427,20 @@ public void stabilityAnalysis() { for (int i = 0; i < numComp; i++) { double z = system.getPhase(0).getComponent(i).getz(); boolean isIon = system.getPhase(0).getComponent(i).getIonicCharge() != 0 - || system.getPhase(0).getComponent(i).isIsIon(); + || system.getPhase(0).getComponent(i).isIsIon(); validComp[i] = z > 1e-100 && !isIon; if (validComp[i]) { - double tc = system.getPhase(0).getComponent(i).getTC(); - double pc = system.getPhase(0).getComponent(i).getPC(); - double omega = system.getPhase(0).getComponent(i).getAcentricFactor(); - double kVal = (pc / presBar) * Math.exp(5.373 * (1.0 + omega) * (1.0 - tc / tempK)); - wilsonK[i] = Math.max(kVal, 1e-20); - double absLogK = Math.abs(Math.log(wilsonK[i])); - if (absLogK > maxAbsLogK) { - maxAbsLogK = absLogK; - } + double tc = system.getPhase(0).getComponent(i).getTC(); + double pc = system.getPhase(0).getComponent(i).getPC(); + double omega = system.getPhase(0).getComponent(i).getAcentricFactor(); + double kVal = (pc / presBar) * Math.exp(5.373 * (1.0 + omega) * (1.0 - tc / tempK)); + wilsonK[i] = Math.max(kVal, 1e-20); + double absLogK = Math.abs(Math.log(wilsonK[i])); + if (absLogK > maxAbsLogK) { + maxAbsLogK = absLogK; + } } else { - wilsonK[i] = 1.0; + wilsonK[i] = 1.0; } } @@ -443,20 +458,21 @@ public void stabilityAnalysis() { for (int trial = 0; !skipWilsonKTrials && trial < 2; trial++) { // Initialize trial composition from Wilson K for (int i = 0; i < numComp; i++) { - if (validComp[i]) { - double z = system.getPhase(0).getComponent(i).getz(); - // trial 0 = liquid-like (z/K), trial 1 = vapor-like (K*z) - double wVal = (trial == 0) ? z / wilsonK[i] : wilsonK[i] * z; - logWi[i] = Math.log(Math.max(wVal, 1e-100)); - } else { - logWi[i] = -10000.0; - } + if (validComp[i]) { + double z = system.getPhase(0).getComponent(i).getz(); + // trial 0 = liquid-like (z/K), trial 1 = vapor-like (K*z) + double wVal = (trial == 0) ? z / wilsonK[i] : wilsonK[i] * z; + logWi[i] = Math.log(Math.max(wVal, 1e-100)); + } else { + logWi[i] = -10000.0; + } } // Set trial phase composition (unnormalized, same as pure-component trials) for (int i = 0; i < numComp; i++) { - if (clonedSystem.get(0).isPhase(1)) { - clonedSystem.get(0).getPhase(1).getComponent(i).setx(validComp[i] ? safeExp(logWi[i]) : 1e-50); - } + if (clonedSystem.get(0).isPhase(1)) { + clonedSystem.get(0).getPhase(1).getComponent(i) + .setx(validComp[i] ? safeExp(logWi[i]) : 1e-50); + } } // Successive substitution with Wegstein acceleration (same as pure-component trials) @@ -467,66 +483,68 @@ public void stabilityAnalysis() { boolean trialInitFailed = false; int maxiter = 50; do { - errOld = err; - iter++; - err = 0; - - for (int i = 0; i < numComp; i++) { - oldoldoldlogw[i] = oldoldlogw[i]; - oldoldlogw[i] = oldlogw[i]; - oldlogw[i] = logWi[i]; - oldoldDeltalogWi[i] = oldoldlogw[i] - oldoldoldlogw[i]; - oldDeltalogWi[i] = oldlogw[i] - oldoldlogw[i]; - } - try { - clonedSystem.get(0).init(1, 1); - } catch (Exception ex) { - trialInitFailed = true; - break; - } - for (int i = 0; i < numComp; i++) { - if (validComp[i] - && !Double.isInfinite(clonedSystem.get(0).getPhase(1).getComponent(i).getLogFugacityCoefficient())) { - logWi[i] = d[i] - clonedSystem.get(0).getPhase(1).getComponent(i).getLogFugacityCoefficient(); - } - deltalogWi[i] = logWi[i] - oldlogw[i]; - err += Math.abs(deltalogWi[i]); - } - - // Wegstein acceleration every 7th iteration - if (iter % 7 == 0 && iter > 7 && useAccel && err < errOld) { - double prod1 = 0.0; - double prod2 = 0.0; - for (int i = 0; i < numComp; i++) { - if (validComp[i]) { - prod1 += deltalogWi[i] * oldDeltalogWi[i]; - prod2 += oldDeltalogWi[i] * oldDeltalogWi[i]; - } - } - if (prod2 > 1e-20) { - double lambda = prod1 / prod2; - if (lambda > 0.0 && lambda < 1.0) { - double accelFactor = lambda / (1.0 - lambda); - for (int i = 0; i < numComp; i++) { - if (validComp[i]) { - logWi[i] += accelFactor * deltalogWi[i]; - } - } - } - } - } - if (iter > 2 && err > errOld) { - useAccel = false; - } - - // Update trial phase composition - for (int i = 0; i < numComp; i++) { - clonedSystem.get(0).getPhase(1).getComponent(i).setx(validComp[i] ? safeExp(logWi[i]) : 1e-50); - } + errOld = err; + iter++; + err = 0; + + for (int i = 0; i < numComp; i++) { + oldoldoldlogw[i] = oldoldlogw[i]; + oldoldlogw[i] = oldlogw[i]; + oldlogw[i] = logWi[i]; + oldoldDeltalogWi[i] = oldoldlogw[i] - oldoldoldlogw[i]; + oldDeltalogWi[i] = oldlogw[i] - oldoldlogw[i]; + } + try { + clonedSystem.get(0).init(1, 1); + } catch (Exception ex) { + trialInitFailed = true; + break; + } + for (int i = 0; i < numComp; i++) { + if (validComp[i] && !Double.isInfinite( + clonedSystem.get(0).getPhase(1).getComponent(i).getLogFugacityCoefficient())) { + logWi[i] = + d[i] - clonedSystem.get(0).getPhase(1).getComponent(i).getLogFugacityCoefficient(); + } + deltalogWi[i] = logWi[i] - oldlogw[i]; + err += Math.abs(deltalogWi[i]); + } + + // Wegstein acceleration every 7th iteration + if (iter % 7 == 0 && iter > 7 && useAccel && err < errOld) { + double prod1 = 0.0; + double prod2 = 0.0; + for (int i = 0; i < numComp; i++) { + if (validComp[i]) { + prod1 += deltalogWi[i] * oldDeltalogWi[i]; + prod2 += oldDeltalogWi[i] * oldDeltalogWi[i]; + } + } + if (prod2 > 1e-20) { + double lambda = prod1 / prod2; + if (lambda > 0.0 && lambda < 1.0) { + double accelFactor = lambda / (1.0 - lambda); + for (int i = 0; i < numComp; i++) { + if (validComp[i]) { + logWi[i] += accelFactor * deltalogWi[i]; + } + } + } + } + } + if (iter > 2 && err > errOld) { + useAccel = false; + } + + // Update trial phase composition + for (int i = 0; i < numComp; i++) { + clonedSystem.get(0).getPhase(1).getComponent(i) + .setx(validComp[i] ? safeExp(logWi[i]) : 1e-50); + } } while (!trialInitFailed && (Math.abs(err) > 1e-9 || err > errOld) && iter < maxiter); if (trialInitFailed) { - continue; + continue; } // Calculate tangent plane distance and check for instability @@ -535,44 +553,44 @@ public void stabilityAnalysis() { double xTrivialCheck1 = 0.0; double[] xTrial = new double[numComp]; for (int i = 0; i < numComp; i++) { - if (validComp[i]) { - tmVal -= safeExp(logWi[i]); - } - xTrial[i] = clonedSystem.get(0).getPhase(1).getComponent(i).getx(); - xTrivialCheck0 += Math.abs(xTrial[i] - system.getPhase(0).getComponent(i).getx()); - xTrivialCheck1 += Math.abs(xTrial[i] - system.getPhase(1).getComponent(i).getx()); + if (validComp[i]) { + tmVal -= safeExp(logWi[i]); + } + xTrial[i] = clonedSystem.get(0).getPhase(1).getComponent(i).getx(); + xTrivialCheck0 += Math.abs(xTrial[i] - system.getPhase(0).getComponent(i).getx()); + xTrivialCheck1 += Math.abs(xTrial[i] - system.getPhase(1).getComponent(i).getx()); } boolean isTrivial = Math.abs(xTrivialCheck0) < 1e-4 || Math.abs(xTrivialCheck1) < 1e-4; if (!isTrivial && tmVal < -1e-8 && iter < maxiter) { - // Unstable — add new phase and return - system.addPhase(); - int newPhaseIdx = system.getNumberOfPhases() - 1; - for (int i = 0; i < numComp; i++) { - system.getPhase(newPhaseIdx).getComponent(i).setx(xTrial[i]); - } - system.getPhases()[newPhaseIdx].normalize(); - multiPhaseTest = true; - int dominantComp = 0; - double maxX = 0; - for (int i = 0; i < numComp; i++) { - if (xTrial[i] > maxX) { - maxX = xTrial[i]; - dominantComp = i; - } - } - system.setBeta(newPhaseIdx, system.getPhase(0).getComponent(dominantComp).getz()); - try { - system.init(1); - } catch (Exception ex) { - logger.warn("K-value trial addPhase init failed: " + ex.getMessage()); - system.removePhaseKeepTotalComposition(newPhaseIdx); - multiPhaseTest = false; - return; - } - system.normalizeBeta(); - return; + // Unstable — add new phase and return + system.addPhase(); + int newPhaseIdx = system.getNumberOfPhases() - 1; + for (int i = 0; i < numComp; i++) { + system.getPhase(newPhaseIdx).getComponent(i).setx(xTrial[i]); + } + system.getPhases()[newPhaseIdx].normalize(); + multiPhaseTest = true; + int dominantComp = 0; + double maxX = 0; + for (int i = 0; i < numComp; i++) { + if (xTrial[i] > maxX) { + maxX = xTrial[i]; + dominantComp = i; + } + } + system.setBeta(newPhaseIdx, system.getPhase(0).getComponent(dominantComp).getz()); + try { + system.init(1); + } catch (Exception ex) { + logger.warn("K-value trial addPhase init failed: " + ex.getMessage()); + system.removePhaseKeepTotalComposition(newPhaseIdx); + multiPhaseTest = false; + return; + } + system.normalizeBeta(); + return; } } @@ -589,70 +607,72 @@ public void stabilityAnalysis() { double Mmin = 1e10; for (int i = 0; i < minimumGibbsEnergySystem.getPhase(0).getNumberOfComponents(); i++) { if (minimumGibbsEnergySystem.getPhase(0).getComponent(i).isHydrocarbon()) { - if ((minimumGibbsEnergySystem.getPhase(0).getComponent(i).getMolarMass()) > Mmax) { - Mmax = minimumGibbsEnergySystem.getPhase(0).getComponent(i).getMolarMass(); - } - if ((minimumGibbsEnergySystem.getPhase(0).getComponent(i).getMolarMass()) < Mmin) { - Mmin = minimumGibbsEnergySystem.getPhase(0).getComponent(i).getMolarMass(); - } + if ((minimumGibbsEnergySystem.getPhase(0).getComponent(i).getMolarMass()) > Mmax) { + Mmax = minimumGibbsEnergySystem.getPhase(0).getComponent(i).getMolarMass(); + } + if ((minimumGibbsEnergySystem.getPhase(0).getComponent(i).getMolarMass()) < Mmin) { + Mmin = minimumGibbsEnergySystem.getPhase(0).getComponent(i).getMolarMass(); + } } } for (int i = 0; i < minimumGibbsEnergySystem.getPhase(0).getNumberOfComponents(); i++) { if (minimumGibbsEnergySystem.getPhase(0).getComponent(i).isHydrocarbon() - && minimumGibbsEnergySystem.getPhase(0).getComponent(i).getz() > 1e-50) { - if (Math.abs((minimumGibbsEnergySystem.getPhase(0).getComponent(i).getMolarMass()) - Mmax) < 1e-5) { - hydrocarbonTestCompNumb = i; - // logger.info("CHECKING heavy component " + hydrocarbonTestCompNumb); - } + && minimumGibbsEnergySystem.getPhase(0).getComponent(i).getz() > 1e-50) { + if (Math.abs( + (minimumGibbsEnergySystem.getPhase(0).getComponent(i).getMolarMass()) - Mmax) < 1e-5) { + hydrocarbonTestCompNumb = i; + // logger.info("CHECKING heavy component " + hydrocarbonTestCompNumb); + } } if (minimumGibbsEnergySystem.getPhase(0).getComponent(i).isHydrocarbon() - && minimumGibbsEnergySystem.getPhase(0).getComponent(i).getz() > 1e-50) { - if (Math.abs((minimumGibbsEnergySystem.getPhase(0).getComponent(i).getMolarMass()) - Mmin) < 1e-5) { - lightTestCompNumb = i; - // logger.info("CHECKING light component " + lightTestCompNumb); - } + && minimumGibbsEnergySystem.getPhase(0).getComponent(i).getz() > 1e-50) { + if (Math.abs( + (minimumGibbsEnergySystem.getPhase(0).getComponent(i).getMolarMass()) - Mmin) < 1e-5) { + lightTestCompNumb = i; + // logger.info("CHECKING light component " + lightTestCompNumb); + } } } // boolean checkdForHCmix = false; for (int j = system.getPhase(0).getNumberOfComponents() - 1; j >= 0; j--) { if (minimumGibbsEnergySystem.getPhase(0).getComponent(j).getx() < 1e-100 - || (minimumGibbsEnergySystem.getPhase(0).getComponent(j).getIonicCharge() != 0) - || (minimumGibbsEnergySystem.getPhase(0).getComponent(j).isHydrocarbon() && j != hydrocarbonTestCompNumb - && j != lightTestCompNumb)) { - continue; + || (minimumGibbsEnergySystem.getPhase(0).getComponent(j).getIonicCharge() != 0) + || (minimumGibbsEnergySystem.getPhase(0).getComponent(j).isHydrocarbon() + && j != hydrocarbonTestCompNumb && j != lightTestCompNumb)) { + continue; } double nomb = 0.0; for (int cc = 0; cc < system.getPhase(0).getNumberOfComponents(); cc++) { - // Pure component trial phase: component j = 1.0, others = trace - nomb = cc == j ? 1.0 : 1.0e-12; - if (system.getPhase(0).getComponent(cc).getz() < 1e-100) { - nomb = 0.0; - } - - // Initialize logWi to match pure component trial phase (Michelsen's algorithm) - // For pure component j trial: Wi[j] = 1.0, Wi[others] = trace - // So logWi[j] = 0, logWi[others] = log(1e-12) ≈ -27.6 - if (system.getPhase(0).getComponent(cc).getz() > 1e-100) { - logWi[cc] = Math.log(Math.max(nomb, 1e-100)); - } else { - logWi[cc] = -10000.0; - } - - if (clonedSystem.get(0).isPhase(1)) { - try { - clonedSystem.get(0).getPhase(1).getComponent(cc).setx(nomb); - /* - * if (system.getPhase(1).getType() == PhaseType.AQUEOUS && !checkdForHCmix) { - * clonedSystem.get(0).getPhase(1).getComponent(cc) - * .setx(clonedSystem.get(0).getPhase(0).getComponent(cc).getK() / - * clonedSystem.get(0).getPhase(0).getComponent(cc).getx()); } else { - * clonedSystem.get(0).getPhase(1).getComponent(cc).setx(nomb); } - */ - } catch (Exception ex) { - logger.warn(ex.getMessage()); - } - } + // Pure component trial phase: component j = 1.0, others = trace + nomb = cc == j ? 1.0 : 1.0e-12; + if (system.getPhase(0).getComponent(cc).getz() < 1e-100) { + nomb = 0.0; + } + + // Initialize logWi to match pure component trial phase (Michelsen's algorithm) + // For pure component j trial: Wi[j] = 1.0, Wi[others] = trace + // So logWi[j] = 0, logWi[others] = log(1e-12) ≈ -27.6 + if (system.getPhase(0).getComponent(cc).getz() > 1e-100) { + logWi[cc] = Math.log(Math.max(nomb, 1e-100)); + } else { + logWi[cc] = -10000.0; + } + + if (clonedSystem.get(0).isPhase(1)) { + try { + clonedSystem.get(0).getPhase(1).getComponent(cc).setx(nomb); + /* + * if (system.getPhase(1).getType() == PhaseType.AQUEOUS && !checkdForHCmix) { + * clonedSystem.get(0).getPhase(1).getComponent(cc) + * .setx(clonedSystem.get(0).getPhase(0).getComponent(cc).getK() / + * clonedSystem.get(0).getPhase(0).getComponent(cc).getx()); } else { + * clonedSystem.get(0).getPhase(1).getComponent(cc).setx(nomb); } + */ + } catch (Exception ex) { + logger.warn(ex.getMessage()); + } + } } // if (system.getPhase(1).getType() == PhaseType.AQUEOUS && !checkdForHCmix) { @@ -671,155 +691,158 @@ public void stabilityAnalysis() { int maxsucssubiter = 150; int maxiter = 200; - // Pre-allocate Newton matrices outside the iteration loop to avoid GC pressure + // Pre-allocate Newton arrays outside the iteration loop to avoid GC pressure int nc = system.getPhase(0).getNumberOfComponents(); - DMatrixRMaj newtonF = new DMatrixRMaj(nc, 1); - DMatrixRMaj newtonJ = new DMatrixRMaj(nc, nc); - DMatrixRMaj newtonDx = new DMatrixRMaj(nc, 1); + double[] newtonF = new double[nc]; + double[][] newtonJ = new double[nc][nc]; + double[] newtonDx = new double[nc]; do { - errOld = err; - iter++; - err = 0; - - if (iter <= maxsucssubiter || !system.isImplementedCompositionDeriativesofFugacity()) { - // DEM acceleration every 5th iteration (Michelsen 1982b, Risnes et al. 1981) - // Uses dominant eigenvalue estimate: λ = (Δg_n · Δg_{n-1}) / (Δg_{n-1} · Δg_{n-1}) - if (iter % 5 == 0 && iter > 5 && useaccsubst) { - double prod1 = 0.0; - double prod2 = 0.0; - for (int i = 0; i < nc; i++) { - // Correct DEM formula: λ = Σ(Δg_n · Δg_{n-1}) / Σ(Δg_{n-1}²) - prod1 += deltalogWi[i] * oldDeltalogWi[i]; - prod2 += oldDeltalogWi[i] * oldDeltalogWi[i]; - } - - if (prod2 > 1e-20) { - double lambda = prod1 / prod2; - // Only accelerate if 0 < λ < 1 (convergent regime) - if (lambda > 0.0 && lambda < 1.0) { - double accelFactor = lambda / (1.0 - lambda); - for (int i = 0; i < nc; i++) { - logWi[i] += accelFactor * deltalogWi[i]; - Wi[j][i] = safeExp(logWi[i]); - } - } - } - // Must still update compositions after acceleration - for (int i = 0; i < nc; i++) { - err += Math.abs(logWi[i] - oldlogw[i]); - } - } else { - for (int i = 0; i < nc; i++) { - oldoldoldlogw[i] = oldoldlogw[i]; - oldoldlogw[i] = oldlogw[i]; - oldlogw[i] = logWi[i]; - oldoldDeltalogWi[i] = oldoldlogw[i] - oldoldoldlogw[i]; - oldDeltalogWi[i] = oldlogw[i] - oldoldlogw[i]; - } - try { - clonedSystem.get(0).init(1, 1); - } catch (Exception ex) { - pureTrialInitFailed = true; - break; - } - for (int i = 0; i < nc; i++) { - if (!Double.isInfinite(clonedSystem.get(0).getPhase(1).getComponent(i).getLogFugacityCoefficient()) - && system.getPhase(0).getComponent(i).getz() > 1e-100) { - logWi[i] = d[i] - clonedSystem.get(0).getPhase(1).getComponent(i).getLogFugacityCoefficient(); - if (clonedSystem.get(0).getPhase(1).getComponent(i).getIonicCharge() != 0) { - logWi[i] = -1000.0; - } - } - deltalogWi[i] = logWi[i] - oldlogw[i]; - err += Math.abs(logWi[i] - oldlogw[i]); - Wi[j][i] = safeExp(logWi[i]); - } - if (iter > 2 && err > errOld) { - useaccsubst = false; - } - } - } else { - // Second-order (Newton) method using Michelsen's α-substitution - // α_i = 2√(W_i), which ensures W_i ≥ 0 (Michelsen 1982a) - for (int i = 0; i < nc; i++) { - oldoldoldlogw[i] = oldoldlogw[i]; - oldoldlogw[i] = oldlogw[i]; - oldlogw[i] = logWi[i]; - } - // Newton needs fugcoef + composition derivatives - try { - clonedSystem.get(0).init(3, 1); - } catch (Exception ex) { - pureTrialInitFailed = true; - break; - } - alpha = new double[nc]; - - for (int i = 0; i < nc; i++) { - alpha[i] = 2.0 * Math.sqrt(Wi[j][i]); - } - - // Build gradient and Jacobian using raw EJML (no SimpleMatrix allocation) - for (int i = 0; i < nc; i++) { - if (system.getPhase(0).getComponent(i).getz() > 1e-100) { - newtonF.set(i, 0, Math.sqrt(Wi[j][i]) * (Math.log(Wi[j][i]) - + clonedSystem.get(0).getPhases()[1].getComponent(i).getLogFugacityCoefficient() - d[i])); - } else { - newtonF.set(i, 0, 0.0); - } - for (int k = 0; k < nc; k++) { - double kronDelt = (i == k) ? 1.0 : 0.0; - if (system.getPhase(0).getComponent(i).getz() > 1e-100) { - newtonJ.set(i, k, kronDelt - + Math.sqrt(Wi[j][k] * Wi[j][i]) * clonedSystem.get(0).getPhases()[1].getComponent(i).getdfugdn(k)); - } else { - newtonJ.set(i, k, 0.0); - } - } - } - - // Solve J·dx = -f using raw EJML - boolean solved = CommonOps_DDRM.solve(newtonJ, newtonF, newtonDx); - if (!solved) { - // Regularize: add small diagonal and retry - for (int i = 0; i < nc; i++) { - newtonJ.add(i, i, 0.1); - } - solved = CommonOps_DDRM.solve(newtonJ, newtonF, newtonDx); - } - - if (solved) { - for (int i = 0; i < nc; i++) { - double alphaNew = alpha[i] - newtonDx.get(i, 0); - Wi[j][i] = Math.pow(alphaNew / 2.0, 2.0); - if (system.getPhase(0).getComponent(i).getz() > 1e-100) { - logWi[i] = Math.log(Wi[j][i]); - } - if (system.getPhase(0).getComponent(i).getIonicCharge() != 0 - || system.getPhase(0).getComponent(i).isIsIon()) { - logWi[i] = -1000.0; - } - err += Math.abs((logWi[i] - oldlogw[i]) / oldlogw[i]); - } - } - } - // logger.info("err: " + err); - - for (int i = 0; i < system.getPhase(0).getNumberOfComponents(); i++) { - if (system.getPhase(0).getComponent(i).getz() > 1e-100) { - clonedSystem.get(0).getPhase(1).getComponent(i).setx(safeExp(logWi[i])); - } - if (system.getPhase(0).getComponent(i).getIonicCharge() != 0 - || system.getPhase(0).getComponent(i).isIsIon()) { - clonedSystem.get(0).getPhase(1).getComponent(i).setx(1e-50); - } - } + errOld = err; + iter++; + err = 0; + + if (iter <= maxsucssubiter || !system.isImplementedCompositionDeriativesofFugacity()) { + // DEM acceleration every 5th iteration (Michelsen 1982b, Risnes et al. 1981) + // Uses dominant eigenvalue estimate: λ = (Δg_n · Δg_{n-1}) / (Δg_{n-1} · Δg_{n-1}) + if (iter % 5 == 0 && iter > 5 && useaccsubst) { + double prod1 = 0.0; + double prod2 = 0.0; + for (int i = 0; i < nc; i++) { + // Correct DEM formula: λ = Σ(Δg_n · Δg_{n-1}) / Σ(Δg_{n-1}²) + prod1 += deltalogWi[i] * oldDeltalogWi[i]; + prod2 += oldDeltalogWi[i] * oldDeltalogWi[i]; + } + + if (prod2 > 1e-20) { + double lambda = prod1 / prod2; + // Only accelerate if 0 < λ < 1 (convergent regime) + if (lambda > 0.0 && lambda < 1.0) { + double accelFactor = lambda / (1.0 - lambda); + for (int i = 0; i < nc; i++) { + logWi[i] += accelFactor * deltalogWi[i]; + Wi[j][i] = safeExp(logWi[i]); + } + } + } + // Must still update compositions after acceleration + for (int i = 0; i < nc; i++) { + err += Math.abs(logWi[i] - oldlogw[i]); + } + } else { + for (int i = 0; i < nc; i++) { + oldoldoldlogw[i] = oldoldlogw[i]; + oldoldlogw[i] = oldlogw[i]; + oldlogw[i] = logWi[i]; + oldoldDeltalogWi[i] = oldoldlogw[i] - oldoldoldlogw[i]; + oldDeltalogWi[i] = oldlogw[i] - oldoldlogw[i]; + } + try { + clonedSystem.get(0).init(1, 1); + } catch (Exception ex) { + pureTrialInitFailed = true; + break; + } + for (int i = 0; i < nc; i++) { + if (!Double.isInfinite( + clonedSystem.get(0).getPhase(1).getComponent(i).getLogFugacityCoefficient()) + && system.getPhase(0).getComponent(i).getz() > 1e-100) { + logWi[i] = d[i] + - clonedSystem.get(0).getPhase(1).getComponent(i).getLogFugacityCoefficient(); + if (clonedSystem.get(0).getPhase(1).getComponent(i).getIonicCharge() != 0) { + logWi[i] = -1000.0; + } + } + deltalogWi[i] = logWi[i] - oldlogw[i]; + err += Math.abs(logWi[i] - oldlogw[i]); + Wi[j][i] = safeExp(logWi[i]); + } + if (iter > 2 && err > errOld) { + useaccsubst = false; + } + } + } else { + // Second-order (Newton) method using Michelsen's α-substitution + // α_i = 2√(W_i), which ensures W_i ≥ 0 (Michelsen 1982a) + for (int i = 0; i < nc; i++) { + oldoldoldlogw[i] = oldoldlogw[i]; + oldoldlogw[i] = oldlogw[i]; + oldlogw[i] = logWi[i]; + } + // Newton needs fugcoef + composition derivatives + try { + clonedSystem.get(0).init(3, 1); + } catch (Exception ex) { + pureTrialInitFailed = true; + break; + } + alpha = new double[nc]; + + for (int i = 0; i < nc; i++) { + alpha[i] = 2.0 * Math.sqrt(Wi[j][i]); + } + + // Build gradient and Jacobian using pre-allocated arrays + for (int i = 0; i < nc; i++) { + if (system.getPhase(0).getComponent(i).getz() > 1e-100) { + newtonF[i] = Math.sqrt(Wi[j][i]) * (Math.log(Wi[j][i]) + + clonedSystem.get(0).getPhases()[1].getComponent(i).getLogFugacityCoefficient() + - d[i]); + } else { + newtonF[i] = 0.0; + } + for (int k = 0; k < nc; k++) { + double kronDelt = (i == k) ? 1.0 : 0.0; + if (system.getPhase(0).getComponent(i).getz() > 1e-100) { + newtonJ[i][k] = kronDelt + Math.sqrt(Wi[j][k] * Wi[j][i]) + * clonedSystem.get(0).getPhases()[1].getComponent(i).getdfugdn(k); + } else { + newtonJ[i][k] = 0.0; + } + } + } + + // Solve J·dx = f using ojAlgo LU + boolean solved = LinearAlgebraOps.solveLinearSystem(newtonJ, newtonF, newtonDx); + if (!solved) { + // Regularize: add small diagonal and retry + for (int i = 0; i < nc; i++) { + newtonJ[i][i] += 0.1; + } + solved = LinearAlgebraOps.solveLinearSystem(newtonJ, newtonF, newtonDx); + } + + if (solved) { + for (int i = 0; i < nc; i++) { + double alphaNew = alpha[i] - newtonDx[i]; + Wi[j][i] = Math.pow(alphaNew / 2.0, 2.0); + if (system.getPhase(0).getComponent(i).getz() > 1e-100) { + logWi[i] = Math.log(Wi[j][i]); + } + if (system.getPhase(0).getComponent(i).getIonicCharge() != 0 + || system.getPhase(0).getComponent(i).isIsIon()) { + logWi[i] = -1000.0; + } + err += Math.abs((logWi[i] - oldlogw[i]) / oldlogw[i]); + } + } + } + // logger.info("err: " + err); + + for (int i = 0; i < system.getPhase(0).getNumberOfComponents(); i++) { + if (system.getPhase(0).getComponent(i).getz() > 1e-100) { + clonedSystem.get(0).getPhase(1).getComponent(i).setx(safeExp(logWi[i])); + } + if (system.getPhase(0).getComponent(i).getIonicCharge() != 0 + || system.getPhase(0).getComponent(i).isIsIon()) { + clonedSystem.get(0).getPhase(1).getComponent(i).setx(1e-50); + } + } } while (!pureTrialInitFailed && (Math.abs(err) > 1e-9 || err > errOld) && iter < maxiter); if (pureTrialInitFailed) { - tm[j] = 10.0; - continue; + tm[j] = 10.0; + continue; } // logger.info("err: " + err + " ITER " + iter); @@ -829,56 +852,57 @@ public void stabilityAnalysis() { tm[j] = 1.0; for (int i = 0; i < system.getPhase(1).getNumberOfComponents(); i++) { - // Use getz() so heavy HCs (with near-zero x in gas phase 0) still contribute to tm - if (system.getPhase(0).getComponent(i).getz() > 1e-100) { - tm[j] -= safeExp(logWi[i]); - } - x[j][i] = clonedSystem.get(0).getPhase(1).getComponent(i).getx(); - // logger.info("txji: " + x[j][i]); + // Use getz() so heavy HCs (with near-zero x in gas phase 0) still contribute to tm + if (system.getPhase(0).getComponent(i).getz() > 1e-100) { + tm[j] -= safeExp(logWi[i]); + } + x[j][i] = clonedSystem.get(0).getPhase(1).getComponent(i).getx(); + // logger.info("txji: " + x[j][i]); - xTrivialCheck0 += Math.abs(x[j][i] - system.getPhase(0).getComponent(i).getx()); - xTrivialCheck1 += Math.abs(x[j][i] - system.getPhase(1).getComponent(i).getx()); + xTrivialCheck0 += Math.abs(x[j][i] - system.getPhase(0).getComponent(i).getx()); + xTrivialCheck1 += Math.abs(x[j][i] - system.getPhase(1).getComponent(i).getx()); } if (iter >= maxiter) { - // logger.info("iter > maxiter multiphase stability "); - // logger.info("error " + Math.abs(err)); - // logger.info("tm: " + tm[j]); + // logger.info("iter > maxiter multiphase stability "); + // logger.info("error " + Math.abs(err)); + // logger.info("tm: " + tm[j]); } if (Math.abs(xTrivialCheck0) < 1e-4 || Math.abs(xTrivialCheck1) < 1e-4) { - tm[j] = 10.0; + tm[j] = 10.0; } if (tm[j] < -1e-8) { - break; + break; } } int unstabcomp = 0; for (int k = system.getPhase(0).getNumberOfComponents() - 1; k >= 0; k--) { if (tm[k] < -1e-8 && !(Double.isNaN(tm[k]))) { - system.addPhase(); - unstabcomp = k; - for (int i = 0; i < system.getPhase(1).getNumberOfComponents(); i++) { - system.getPhase(system.getNumberOfPhases() - 1).getComponent(i).setx(x[k][i]); - } - system.getPhases()[system.getNumberOfPhases() - 1].normalize(); - multiPhaseTest = true; - system.setBeta(system.getNumberOfPhases() - 1, system.getPhase(0).getComponent(unstabcomp).getz()); - try { - system.init(1); - } catch (Exception ex) { - logger.warn("stabilityAnalysis addPhase init failed: " + ex.getMessage()); - system.removePhaseKeepTotalComposition(system.getNumberOfPhases() - 1); - multiPhaseTest = false; - return; - } - system.normalizeBeta(); - - // logger.info("STABILITY ANALYSIS: "); - // logger.info("tm1: " + k + " "+ tm[k]); - // system.display(); - return; + system.addPhase(); + unstabcomp = k; + for (int i = 0; i < system.getPhase(1).getNumberOfComponents(); i++) { + system.getPhase(system.getNumberOfPhases() - 1).getComponent(i).setx(x[k][i]); + } + system.getPhases()[system.getNumberOfPhases() - 1].normalize(); + multiPhaseTest = true; + system.setBeta(system.getNumberOfPhases() - 1, + system.getPhase(0).getComponent(unstabcomp).getz()); + try { + system.init(1); + } catch (Exception ex) { + logger.warn("stabilityAnalysis addPhase init failed: " + ex.getMessage()); + system.removePhaseKeepTotalComposition(system.getNumberOfPhases() - 1); + multiPhaseTest = false; + return; + } + system.normalizeBeta(); + + // logger.info("STABILITY ANALYSIS: "); + // logger.info("tm1: " + k + " "+ tm[k]); + // system.display(); + return; } } @@ -889,9 +913,9 @@ public void stabilityAnalysis() { } /** - * Enhanced stability analysis that uses Wilson K-values for initial guesses and tests multiple trial phase - * compositions. This method is more robust for detecting liquid-liquid equilibria and three-phase systems (e.g., - * CO2/H2S/hydrocarbon mixtures). + * Enhanced stability analysis that uses Wilson K-values for initial guesses and tests multiple + * trial phase compositions. This method is more robust for detecting liquid-liquid equilibria and + * three-phase systems (e.g., CO2/H2S/hydrocarbon mixtures). * *

* Key improvements over basic stabilityAnalysis(): @@ -899,8 +923,9 @@ public void stabilityAnalysis() { *

    *
  • Uses Wilson K-value correlation for vapor-liquid equilibrium (VLE) detection
  • *
  • Tests vapor-like trial (K), liquid-like trial (1/K), and LLE-specific trial phases
  • - *
  • LLE trial uses acentric factor-based perturbation (polarity proxy) since Wilson K-values are derived from vapor - * pressure correlations and may not capture activity coefficient-driven liquid-liquid splits
  • + *
  • LLE trial uses acentric factor-based perturbation (polarity proxy) since Wilson K-values + * are derived from vapor pressure correlations and may not capture activity coefficient-driven + * liquid-liquid splits
  • *
  • Does not skip non-hydrocarbon components (important for CO2, H2S systems)
  • *
  • Tests stability against all existing phases, not just phase 0
  • *
  • Includes Wegstein acceleration for faster convergence
  • @@ -940,16 +965,16 @@ public void stabilityAnalysisEnhanced() { boolean isIon = system.getPhase(0).getComponent(j).getIonicCharge() != 0; validComponent[j] = z > 1e-100 && !isIon; if (validComponent[j]) { - validCount++; - double tc = system.getPhase(0).getComponent(j).getTC(); - double pc = system.getPhase(0).getComponent(j).getPC(); - double omega = system.getPhase(0).getComponent(j).getAcentricFactor(); - double kVal = (pc / presBar) * Math.exp(5.373 * (1.0 + omega) * (1.0 - tc / tempK)); - wilsonK[j] = Math.max(kVal, 1e-20); - logWilsonK[j] = Math.log(wilsonK[j]); + validCount++; + double tc = system.getPhase(0).getComponent(j).getTC(); + double pc = system.getPhase(0).getComponent(j).getPC(); + double omega = system.getPhase(0).getComponent(j).getAcentricFactor(); + double kVal = (pc / presBar) * Math.exp(5.373 * (1.0 + omega) * (1.0 - tc / tempK)); + wilsonK[j] = Math.max(kVal, 1e-20); + logWilsonK[j] = Math.log(wilsonK[j]); } else { - wilsonK[j] = 1.0; - logWilsonK[j] = 0.0; + wilsonK[j] = 1.0; + logWilsonK[j] = 0.0; } } @@ -965,10 +990,11 @@ public void stabilityAnalysisEnhanced() { double[][] dRef = new double[numPhases][numComponents]; for (int refPhase = 0; refPhase < numPhases; refPhase++) { for (int k = 0; k < numComponents; k++) { - double xk = system.getPhase(refPhase).getComponent(k).getx(); - if (xk > 1e-100) { - dRef[refPhase][k] = Math.log(xk) + system.getPhase(refPhase).getComponent(k).getLogFugacityCoefficient(); - } + double xk = system.getPhase(refPhase).getComponent(k).getx(); + if (xk > 1e-100) { + dRef[refPhase][k] = + Math.log(xk) + system.getPhase(refPhase).getComponent(k).getLogFugacityCoefficient(); + } } } @@ -984,180 +1010,181 @@ public void stabilityAnalysisEnhanced() { // but LLE is driven by activity coefficient differences (polarity, H-bonding), // so we use a different initialization strategy for LLE detection. for (int trialType = 1; trialType >= -1; trialType--) { - // Initialize logWi based on trial type - for (int j = 0; j < numComponents; j++) { - if (!validComponent[j]) { - logWi[j] = -10000.0; - Wi[j] = 0.0; - } else if (trialType == 1) { - // Vapor-like: use Wilson K (volatile components enriched) - logWi[j] = logWilsonK[j]; - Wi[j] = Math.exp(logWi[j]); - } else if (trialType == -1) { - // Liquid-like: use 1/K (heavy components enriched) - logWi[j] = -logWilsonK[j]; - Wi[j] = Math.exp(logWi[j]); - } else { - // LLE trial (trialType == 0): perturb based on hydrocarbon vs non-HC nature - // Non-HCs (water, CO2, H2S, MEG) are enriched; HCs are depleted in the - // polar-rich trial phase — physically correct for aqueous LLE detection. - double z = system.getPhase(0).getComponent(j).getz(); - double perturbFactor = system.getPhase(0).getComponent(j).isHydrocarbon() ? 0.5 : 2.0; - Wi[j] = z * perturbFactor; - logWi[j] = Math.log(Math.max(Wi[j], 1e-100)); - } - oldlogw[j] = logWi[j]; - oldoldlogw[j] = logWi[j]; - deltalogWi[j] = 0.0; - oldDeltalogWi[j] = 0.0; - } - - // Force correct EOS root for the trial phase type before evaluating fugacities. - // Without this, a clone inheriting a GAS phase type would pick the vapor root - // even for liquid-like and LLE trials, giving wrong fugacity coefficients. - if (clonedSystem.isPhase(1)) { - PhaseType trialPhaseType = (trialType == 1) ? PhaseType.GAS : PhaseType.LIQUID; - clonedSystem.setPhaseType(1, trialPhaseType); - } - - // Set initial trial phase composition - for (int cc = 0; cc < numComponents; cc++) { - if (clonedSystem.isPhase(1)) { - clonedSystem.getPhase(1).getComponent(cc).setx(validComponent[cc] ? Wi[cc] : 1e-50); - } - } - - // Successive substitution iterations with acceleration - int iter = 0; - double err = 1.0e10; - double errOld = 1.0e100; - int maxiter = 150; // Reduced from 200 - Wilson init converges faster - boolean useAcceleration = true; - - do { - errOld = err; - iter++; - err = 0; - - // Store old values for acceleration - for (int i = 0; i < numComponents; i++) { - oldoldlogw[i] = oldlogw[i]; - oldlogw[i] = logWi[i]; - oldDeltalogWi[i] = deltalogWi[i]; - } - - try { - clonedSystem.init(1, 1); - } catch (RuntimeException ex) { - // Molar volume calculation failed for this trial phase composition - // Skip this trial - it's not a physically meaningful phase - logger.debug("Enhanced stability trial init failed: " + ex.getMessage()); - break; - } - - // Update logWi from fugacity coefficients - for (int i = 0; i < numComponents; i++) { - if (validComponent[i]) { - double logFugCoeff = clonedSystem.getPhase(1).getComponent(i).getLogFugacityCoefficient(); - if (!Double.isInfinite(logFugCoeff)) { - logWi[i] = d[i] - logFugCoeff; - } - } - deltalogWi[i] = logWi[i] - oldlogw[i]; - err += Math.abs(deltalogWi[i]); - Wi[i] = safeExp(logWi[i]); - } - - // Wegstein/GDEM acceleration every 7th iteration - if (iter % 7 == 0 && iter > 7 && useAcceleration && err < errOld) { - double prod1 = 0.0; - double prod2 = 0.0; - for (int i = 0; i < numComponents; i++) { - if (validComponent[i]) { - double vec1 = deltalogWi[i] * oldDeltalogWi[i]; - double vec2 = oldDeltalogWi[i] * oldDeltalogWi[i]; - prod1 += vec1; - prod2 += vec2; - } - } - if (prod2 > 1e-20) { - double lambda = prod1 / prod2; - if (lambda > 0.0 && lambda < 1.0) { - double accelFactor = lambda / (1.0 - lambda); - for (int i = 0; i < numComponents; i++) { - if (validComponent[i]) { - logWi[i] += accelFactor * deltalogWi[i]; - Wi[i] = safeExp(logWi[i]); - } - } - } - } - } - - // Disable acceleration if error is increasing - if (iter > 2 && err > errOld) { - useAcceleration = false; - } - - // Update trial phase compositions - for (int i = 0; i < numComponents; i++) { - clonedSystem.getPhase(1).getComponent(i).setx(validComponent[i] ? Wi[i] : 1e-50); - } - } while ((Math.abs(err) > 1e-9 || err > errOld) && iter < maxiter); - - // Calculate tangent plane distance - double tmVal = 1.0; - for (int i = 0; i < numComponents; i++) { - if (validComponent[i]) { - tmVal -= Wi[i]; - } - x[i] = clonedSystem.getPhase(1).getComponent(i).getx(); - } - - // Check for trivial solution (trial phase same as any existing phase) - boolean isTrivial = false; - for (int existingPhase = 0; existingPhase < numPhases; existingPhase++) { - double xTrivialCheck = 0.0; - for (int i = 0; i < numComponents; i++) { - xTrivialCheck += Math.abs(x[i] - system.getPhase(existingPhase).getComponent(i).getx()); - } - if (xTrivialCheck < 1e-4) { - isTrivial = true; - break; - } - } - - // If unstable and non-trivial, add new phase and return - if (!isTrivial && tmVal < -1e-8) { - system.addPhase(); - int newPhaseIdx = system.getNumberOfPhases() - 1; - for (int i = 0; i < numComponents; i++) { - system.getPhase(newPhaseIdx).getComponent(i).setx(x[i]); - } - system.getPhases()[newPhaseIdx].normalize(); - multiPhaseTest = true; - - // Set initial beta based on dominant component - int dominantComp = 0; - double maxX = 0; - for (int i = 0; i < numComponents; i++) { - if (x[i] > maxX) { - maxX = x[i]; - dominantComp = i; - } - } - system.setBeta(newPhaseIdx, system.getPhase(0).getComponent(dominantComp).getz()); - try { - system.init(1); - } catch (Exception ex) { - logger.warn("Enhanced K-value trial addPhase init failed: " + ex.getMessage()); - system.removePhaseKeepTotalComposition(newPhaseIdx); - multiPhaseTest = false; - return; - } - system.normalizeBeta(); - return; - } + // Initialize logWi based on trial type + for (int j = 0; j < numComponents; j++) { + if (!validComponent[j]) { + logWi[j] = -10000.0; + Wi[j] = 0.0; + } else if (trialType == 1) { + // Vapor-like: use Wilson K (volatile components enriched) + logWi[j] = logWilsonK[j]; + Wi[j] = Math.exp(logWi[j]); + } else if (trialType == -1) { + // Liquid-like: use 1/K (heavy components enriched) + logWi[j] = -logWilsonK[j]; + Wi[j] = Math.exp(logWi[j]); + } else { + // LLE trial (trialType == 0): perturb based on hydrocarbon vs non-HC nature + // Non-HCs (water, CO2, H2S, MEG) are enriched; HCs are depleted in the + // polar-rich trial phase — physically correct for aqueous LLE detection. + double z = system.getPhase(0).getComponent(j).getz(); + double perturbFactor = system.getPhase(0).getComponent(j).isHydrocarbon() ? 0.5 : 2.0; + Wi[j] = z * perturbFactor; + logWi[j] = Math.log(Math.max(Wi[j], 1e-100)); + } + oldlogw[j] = logWi[j]; + oldoldlogw[j] = logWi[j]; + deltalogWi[j] = 0.0; + oldDeltalogWi[j] = 0.0; + } + + // Force correct EOS root for the trial phase type before evaluating fugacities. + // Without this, a clone inheriting a GAS phase type would pick the vapor root + // even for liquid-like and LLE trials, giving wrong fugacity coefficients. + if (clonedSystem.isPhase(1)) { + PhaseType trialPhaseType = (trialType == 1) ? PhaseType.GAS : PhaseType.LIQUID; + clonedSystem.setPhaseType(1, trialPhaseType); + } + + // Set initial trial phase composition + for (int cc = 0; cc < numComponents; cc++) { + if (clonedSystem.isPhase(1)) { + clonedSystem.getPhase(1).getComponent(cc).setx(validComponent[cc] ? Wi[cc] : 1e-50); + } + } + + // Successive substitution iterations with acceleration + int iter = 0; + double err = 1.0e10; + double errOld = 1.0e100; + int maxiter = 150; // Reduced from 200 - Wilson init converges faster + boolean useAcceleration = true; + + do { + errOld = err; + iter++; + err = 0; + + // Store old values for acceleration + for (int i = 0; i < numComponents; i++) { + oldoldlogw[i] = oldlogw[i]; + oldlogw[i] = logWi[i]; + oldDeltalogWi[i] = deltalogWi[i]; + } + + try { + clonedSystem.init(1, 1); + } catch (RuntimeException ex) { + // Molar volume calculation failed for this trial phase composition + // Skip this trial - it's not a physically meaningful phase + logger.debug("Enhanced stability trial init failed: " + ex.getMessage()); + break; + } + + // Update logWi from fugacity coefficients + for (int i = 0; i < numComponents; i++) { + if (validComponent[i]) { + double logFugCoeff = + clonedSystem.getPhase(1).getComponent(i).getLogFugacityCoefficient(); + if (!Double.isInfinite(logFugCoeff)) { + logWi[i] = d[i] - logFugCoeff; + } + } + deltalogWi[i] = logWi[i] - oldlogw[i]; + err += Math.abs(deltalogWi[i]); + Wi[i] = safeExp(logWi[i]); + } + + // Wegstein/GDEM acceleration every 7th iteration + if (iter % 7 == 0 && iter > 7 && useAcceleration && err < errOld) { + double prod1 = 0.0; + double prod2 = 0.0; + for (int i = 0; i < numComponents; i++) { + if (validComponent[i]) { + double vec1 = deltalogWi[i] * oldDeltalogWi[i]; + double vec2 = oldDeltalogWi[i] * oldDeltalogWi[i]; + prod1 += vec1; + prod2 += vec2; + } + } + if (prod2 > 1e-20) { + double lambda = prod1 / prod2; + if (lambda > 0.0 && lambda < 1.0) { + double accelFactor = lambda / (1.0 - lambda); + for (int i = 0; i < numComponents; i++) { + if (validComponent[i]) { + logWi[i] += accelFactor * deltalogWi[i]; + Wi[i] = safeExp(logWi[i]); + } + } + } + } + } + + // Disable acceleration if error is increasing + if (iter > 2 && err > errOld) { + useAcceleration = false; + } + + // Update trial phase compositions + for (int i = 0; i < numComponents; i++) { + clonedSystem.getPhase(1).getComponent(i).setx(validComponent[i] ? Wi[i] : 1e-50); + } + } while ((Math.abs(err) > 1e-9 || err > errOld) && iter < maxiter); + + // Calculate tangent plane distance + double tmVal = 1.0; + for (int i = 0; i < numComponents; i++) { + if (validComponent[i]) { + tmVal -= Wi[i]; + } + x[i] = clonedSystem.getPhase(1).getComponent(i).getx(); + } + + // Check for trivial solution (trial phase same as any existing phase) + boolean isTrivial = false; + for (int existingPhase = 0; existingPhase < numPhases; existingPhase++) { + double xTrivialCheck = 0.0; + for (int i = 0; i < numComponents; i++) { + xTrivialCheck += Math.abs(x[i] - system.getPhase(existingPhase).getComponent(i).getx()); + } + if (xTrivialCheck < 1e-4) { + isTrivial = true; + break; + } + } + + // If unstable and non-trivial, add new phase and return + if (!isTrivial && tmVal < -1e-8) { + system.addPhase(); + int newPhaseIdx = system.getNumberOfPhases() - 1; + for (int i = 0; i < numComponents; i++) { + system.getPhase(newPhaseIdx).getComponent(i).setx(x[i]); + } + system.getPhases()[newPhaseIdx].normalize(); + multiPhaseTest = true; + + // Set initial beta based on dominant component + int dominantComp = 0; + double maxX = 0; + for (int i = 0; i < numComponents; i++) { + if (x[i] > maxX) { + maxX = x[i]; + dominantComp = i; + } + } + system.setBeta(newPhaseIdx, system.getPhase(0).getComponent(dominantComp).getz()); + try { + system.init(1); + } catch (Exception ex) { + logger.warn("Enhanced K-value trial addPhase init failed: " + ex.getMessage()); + system.removePhaseKeepTotalComposition(newPhaseIdx); + multiPhaseTest = false; + return; + } + system.normalizeBeta(); + return; + } } } @@ -1171,7 +1198,8 @@ public void stabilityAnalysisEnhanced() { */ public void stabilityAnalysis3() { double[] logWi = new double[system.getPhase(0).getNumberOfComponents()]; - double[][] Wi = new double[system.getPhase(0).getNumberOfComponents()][system.getPhase(0).getNumberOfComponents()]; + double[][] Wi = new double[system.getPhase(0).getNumberOfComponents()][system.getPhase(0) + .getNumberOfComponents()]; double[] deltalogWi = new double[system.getPhases()[0].getNumberOfComponents()]; double[] oldDeltalogWi = new double[system.getPhases()[0].getNumberOfComponents()]; @@ -1182,7 +1210,8 @@ public void stabilityAnalysis3() { double[] oldoldlogw = new double[system.getPhases()[0].getNumberOfComponents()]; double[] oldoldoldlogw = new double[system.getPhases()[0].getNumberOfComponents()]; double[] d = new double[system.getPhase(0).getNumberOfComponents()]; - double[][] x = new double[system.getPhase(0).getNumberOfComponents()][system.getPhase(0).getNumberOfComponents()]; + double[][] x = new double[system.getPhase(0).getNumberOfComponents()][system.getPhase(0) + .getNumberOfComponents()]; tm = new double[system.getPhase(0).getNumberOfComponents()]; double[] alpha = null; @@ -1195,47 +1224,52 @@ public void stabilityAnalysis3() { clonedSystem.add(system.clone()); /* * for (int i = 0; i < system.getPhase(0).getNumberOfComponents(); i++) { if - * (system.getPhase(0).getComponent(i).getx() < 1e-100) { clonedSystem.add(null); continue; } double numb = 0; - * clonedSystem.add(system.clone()); + * (system.getPhase(0).getComponent(i).getx() < 1e-100) { clonedSystem.add(null); continue; } + * double numb = 0; clonedSystem.add(system.clone()); * * // (clonedSystem.get(i)).init(0); commented out sept 2005, Even S. for (int j = 0; j < - * system.getPhase(0).getNumberOfComponents(); j++) { numb = i == j ? 1.0 : 1.0e-12; // set to 0 by Even Solbraa - * 23.01.2013 - chaged back to 1.0e-12 27.04.13 if (system.getPhase(0).getComponent(j).getz() < 1e-100) { numb = 0; - * } ( clonedSystem.get(i)).getPhase(1).getComponent(j).setx(numb); } if - * (system.getPhase(0).getComponent(i).getIonicCharge() == 0) { ( clonedSystem.get(i)).init(1); } } + * system.getPhase(0).getNumberOfComponents(); j++) { numb = i == j ? 1.0 : 1.0e-12; // set to 0 + * by Even Solbraa 23.01.2013 - chaged back to 1.0e-12 27.04.13 if + * (system.getPhase(0).getComponent(j).getz() < 1e-100) { numb = 0; } ( + * clonedSystem.get(i)).getPhase(1).getComponent(j).setx(numb); } if + * (system.getPhase(0).getComponent(i).getIonicCharge() == 0) { ( clonedSystem.get(i)).init(1); + * } } */ lowestGibbsEnergyPhase = 0; /* * // logger.info("low gibbs phase " + lowestGibbsEnergyPhase); for (int k = 0; k < * minimumGibbsEnergySystem.getPhase(0).getNumberOfComponents(); k++) { for (int i = 0; i < - * minimumGibbsEnergySystem.getPhase(0).getNumberOfComponents(); i++) { if (!(( clonedSystem.get(k)) == null)) { - * sumw[k] += ( clonedSystem.get(k)).getPhase(1).getComponent(i).getx(); } } } + * minimumGibbsEnergySystem.getPhase(0).getNumberOfComponents(); i++) { if (!(( + * clonedSystem.get(k)) == null)) { sumw[k] += ( + * clonedSystem.get(k)).getPhase(1).getComponent(i).getx(); } } } * - * for (int k = 0; k < minimumGibbsEnergySystem.getPhase(0).getNumberOfComponents(); k++) { for (int i = 0; i < - * minimumGibbsEnergySystem.getPhase(0).getNumberOfComponents(); i++) { if (!(( clonedSystem.get(k)) == null) && - * system.getPhase(0).getComponent(k).getx() > 1e-100) { ( clonedSystem.get(k)).getPhase(1).getComponent(i).setx(( + * for (int k = 0; k < minimumGibbsEnergySystem.getPhase(0).getNumberOfComponents(); k++) { for + * (int i = 0; i < minimumGibbsEnergySystem.getPhase(0).getNumberOfComponents(); i++) { if (!(( + * clonedSystem.get(k)) == null) && system.getPhase(0).getComponent(k).getx() > 1e-100) { ( + * clonedSystem.get(k)).getPhase(1).getComponent(i).setx(( * clonedSystem.get(k)).getPhase(1).getComponent(i).getx() / sumw[k]); } logger.info("x: " + ( - * clonedSystem.get(k)).getPhase(0).getComponent(i).getx()); } if (system.getPhase(0).getComponent(k).getx() > - * 1e-100) { d[k] = Math.log(system.getPhase(0).getComponent(k).getx()) + + * clonedSystem.get(k)).getPhase(0).getComponent(i).getx()); } if + * (system.getPhase(0).getComponent(k).getx() > 1e-100) { d[k] = + * Math.log(system.getPhase(0).getComponent(k).getx()) + * system.getPhase(0).getComponent(k).getLogFugacityCoefficient(); - * if(minimumGibbsEnergySystem.getPhases()[lowestGibbsEnergyPhase].getComponents ()[k].getIonicCharge()!=0) d[k]=0; - * } //logger.info("dk: " + d[k]); } + * if(minimumGibbsEnergySystem.getPhases()[lowestGibbsEnergyPhase].getComponents + * ()[k].getIonicCharge()!=0) d[k]=0; } //logger.info("dk: " + d[k]); } */ for (int k = 0; k < minimumGibbsEnergySystem.getPhase(0).getNumberOfComponents(); k++) { if (system.getPhase(0).getComponent(k).getx() > 1e-100) { - d[k] = Math.log(system.getPhase(0).getComponent(k).getx()) - + system.getPhase(0).getComponent(k).getLogFugacityCoefficient(); - // if(minimumGibbsEnergySystem.getPhases()[lowestGibbsEnergyPhase].getComponent(k).getIonicCharge()!=0) - // d[k]=0; + d[k] = Math.log(system.getPhase(0).getComponent(k).getx()) + + system.getPhase(0).getComponent(k).getLogFugacityCoefficient(); + // if(minimumGibbsEnergySystem.getPhases()[lowestGibbsEnergyPhase].getComponent(k).getIonicCharge()!=0) + // d[k]=0; } } for (int j = 0; j < minimumGibbsEnergySystem.getPhase(0).getNumberOfComponents(); j++) { if (system.getPhase(0).getComponent(j).getz() > 1e-100) { - logWi[j] = 1.0; + logWi[j] = 1.0; } else { - logWi[j] = -10000.0; + logWi[j] = -10000.0; } } @@ -1245,55 +1279,57 @@ public void stabilityAnalysis3() { double Mmin = 1e10; for (int i = 0; i < minimumGibbsEnergySystem.getPhase(0).getNumberOfComponents(); i++) { if (minimumGibbsEnergySystem.getPhase(0).getComponent(i).isHydrocarbon()) { - if ((minimumGibbsEnergySystem.getPhase(0).getComponent(i).getMolarMass()) > Mmax) { - Mmax = minimumGibbsEnergySystem.getPhase(0).getComponent(i).getMolarMass(); - } - if ((minimumGibbsEnergySystem.getPhase(0).getComponent(i).getMolarMass()) < Mmin) { - Mmin = minimumGibbsEnergySystem.getPhase(0).getComponent(i).getMolarMass(); - } + if ((minimumGibbsEnergySystem.getPhase(0).getComponent(i).getMolarMass()) > Mmax) { + Mmax = minimumGibbsEnergySystem.getPhase(0).getComponent(i).getMolarMass(); + } + if ((minimumGibbsEnergySystem.getPhase(0).getComponent(i).getMolarMass()) < Mmin) { + Mmin = minimumGibbsEnergySystem.getPhase(0).getComponent(i).getMolarMass(); + } } } for (int i = 0; i < minimumGibbsEnergySystem.getPhase(0).getNumberOfComponents(); i++) { if (minimumGibbsEnergySystem.getPhase(0).getComponent(i).isHydrocarbon() - && minimumGibbsEnergySystem.getPhase(0).getComponent(i).getz() > 1e-50) { - if (Math.abs((minimumGibbsEnergySystem.getPhase(0).getComponent(i).getMolarMass()) - Mmax) < 1e-5) { - hydrocarbonTestCompNumb = i; - // logger.info("CHECKING heavy component " + hydrocarbonTestCompNumb); - } + && minimumGibbsEnergySystem.getPhase(0).getComponent(i).getz() > 1e-50) { + if (Math.abs( + (minimumGibbsEnergySystem.getPhase(0).getComponent(i).getMolarMass()) - Mmax) < 1e-5) { + hydrocarbonTestCompNumb = i; + // logger.info("CHECKING heavy component " + hydrocarbonTestCompNumb); + } } if (minimumGibbsEnergySystem.getPhase(0).getComponent(i).isHydrocarbon() - && minimumGibbsEnergySystem.getPhase(0).getComponent(i).getz() > 1e-50) { - if (Math.abs((minimumGibbsEnergySystem.getPhase(0).getComponent(i).getMolarMass()) - Mmin) < 1e-5) { - lightTestCompNumb = i; - // logger.info("CHECKING light component " + lightTestCompNumb); - } + && minimumGibbsEnergySystem.getPhase(0).getComponent(i).getz() > 1e-50) { + if (Math.abs( + (minimumGibbsEnergySystem.getPhase(0).getComponent(i).getMolarMass()) - Mmin) < 1e-5) { + lightTestCompNumb = i; + // logger.info("CHECKING light component " + lightTestCompNumb); + } } } for (int j = 0; j < system.getNumberOfComponents(); j++) { if (minimumGibbsEnergySystem.getPhase(0).getComponent(j).getx() < 1e-100 - || (minimumGibbsEnergySystem.getPhase(0).getComponent(j).getIonicCharge() != 0) - || (minimumGibbsEnergySystem.getPhase(0).getComponent(j).isHydrocarbon() && j != hydrocarbonTestCompNumb - && j != lightTestCompNumb)) { - continue; + || (minimumGibbsEnergySystem.getPhase(0).getComponent(j).getIonicCharge() != 0) + || (minimumGibbsEnergySystem.getPhase(0).getComponent(j).isHydrocarbon() + && j != hydrocarbonTestCompNumb && j != lightTestCompNumb)) { + continue; } double nomb = 0.0; for (int cc = 0; cc < system.getPhase(0).getNumberOfComponents(); cc++) { - nomb = cc == j ? 1.0 : 1.0e-12; - if (system.getPhase(0).getComponent(cc).getz() < 1e-100) { - nomb = 0.0; - } - - if (clonedSystem.get(0).isPhase(1)) { - try { - clonedSystem.get(0).getPhase(1).getComponent(cc).setx(nomb); - } catch (Exception ex) { - logger.warn(ex.getMessage()); - } - } + nomb = cc == j ? 1.0 : 1.0e-12; + if (system.getPhase(0).getComponent(cc).getz() < 1e-100) { + nomb = 0.0; + } + + if (clonedSystem.get(0).isPhase(1)) { + try { + clonedSystem.get(0).getPhase(1).getComponent(cc).setx(nomb); + } catch (Exception ex) { + logger.warn(ex.getMessage()); + } + } } // if(minimumGibbsEnergySystem.getPhase(0).getComponent(j).getName().equals("water") // && minimumGibbsEnergySystem.isChemicalSystem()) continue; @@ -1307,175 +1343,210 @@ public void stabilityAnalysis3() { int maxsucssubiter = 150; int maxiter = 200; do { - errOld = err; - iter++; - err = 0; - - if (iter <= maxsucssubiter || !system.isImplementedCompositionDeriativesofFugacity()) { - if (iter % 7 == 0 && useaccsubst) { - double vec1 = 0.0; - - double vec2 = 0.0; - double prod1 = 0.0; - double prod2 = 0.0; - for (i = 0; i < system.getPhase(0).getNumberOfComponents(); i++) { - vec1 = oldDeltalogWi[i] * oldoldDeltalogWi[i]; - vec2 = Math.pow(oldoldDeltalogWi[i], 2.0); - prod1 += vec1 * vec2; - prod2 += vec2 * vec2; - } - - double lambda = prod1 / prod2; - // logger.info("lambda " + lambda); - for (i = 0; i < system.getPhase(0).getNumberOfComponents(); i++) { - logWi[i] += lambda / (1.0 - lambda) * deltalogWi[i]; - err += Math.abs((logWi[i] - oldlogw[i]) / oldlogw[i]); - Wi[j][i] = safeExp(logWi[i]); - } - } else { - for (int i = 0; i < system.getPhase(0).getNumberOfComponents(); i++) { - oldoldoldlogw[i] = oldoldlogw[i]; - oldoldlogw[i] = oldlogw[i]; - oldlogw[i] = logWi[i]; - oldoldDeltalogWi[i] = oldoldlogw[i] - oldoldoldlogw[i]; - oldDeltalogWi[i] = oldlogw[i] - oldoldlogw[i]; - } - try { - clonedSystem.get(0).init(1, 1); - } catch (Exception ex) { - enhancedTrialInitFailed = true; - break; - } - for (int i = 0; i < system.getPhase(0).getNumberOfComponents(); i++) { - // oldlogw[i] = logWi[i]; - if (!Double.isInfinite(clonedSystem.get(0).getPhase(1).getComponent(i).getLogFugacityCoefficient()) - && system.getPhase(0).getComponent(i).getx() > 1e-100) { - logWi[i] = d[i] - clonedSystem.get(0).getPhase(1).getComponent(i).getLogFugacityCoefficient(); - if (clonedSystem.get(0).getPhase(1).getComponent(i).getIonicCharge() != 0) { - logWi[i] = -1000.0; - } - } - deltalogWi[i] = logWi[i] - oldlogw[i]; - err += Math.abs(logWi[i] - oldlogw[i]); - Wi[j][i] = safeExp(logWi[i]); - useaccsubst = true; - } - if (iter > 2 && err > errOld) { - useaccsubst = false; - } - } - } else { - SimpleMatrix f = new SimpleMatrix(system.getPhases()[0].getNumberOfComponents(), 1); - SimpleMatrix df = null; - SimpleMatrix identitytimesConst = null; - // if (!secondOrderStabilityAnalysis) { - for (int i = 0; i < system.getPhase(0).getNumberOfComponents(); i++) { - oldoldoldlogw[i] = oldoldlogw[i]; - oldoldlogw[i] = oldlogw[i]; - oldlogw[i] = logWi[i]; - oldoldDeltalogWi[i] = oldoldlogw[i] - oldoldoldlogw[i]; - oldDeltalogWi[i] = oldlogw[i] - oldoldlogw[i]; - } - try { - clonedSystem.get(0).init(3, 1); - } catch (Exception ex) { - enhancedTrialInitFailed = true; - break; - } - alpha = new double[clonedSystem.get(0).getPhases()[0].getNumberOfComponents()]; - df = new SimpleMatrix(system.getPhases()[0].getNumberOfComponents(), - system.getPhases()[0].getNumberOfComponents()); - identitytimesConst = SimpleMatrix.identity(system.getPhases()[0].getNumberOfComponents()); - - for (int i = 0; i < clonedSystem.get(0).getPhases()[0].getNumberOfComponents(); i++) { - alpha[i] = 2.0 * Math.sqrt(Wi[j][i]); - } - - for (int i = 0; i < system.getPhase(0).getNumberOfComponents(); i++) { - if (system.getPhase(0).getComponent(i).getz() > 1e-100) { - f.set(i, 0, Math.sqrt(Wi[j][i]) * (Math.log(Wi[j][i]) - + clonedSystem.get(0).getPhases()[1].getComponent(i).getLogFugacityCoefficient() - d[i])); - } - for (int k = 0; k < clonedSystem.get(0).getPhases()[0].getNumberOfComponents(); k++) { - double kronDelt = (i == k) ? 1.0 : 0.0; - if (system.getPhase(0).getComponent(i).getz() > 1e-100) { - df.set(i, k, kronDelt - + Math.sqrt(Wi[j][k] * Wi[j][i]) * clonedSystem.get(0).getPhases()[1].getComponent(i).getdfugdn(k)); - } else { - df.set(i, k, 0); - } - } - } - - SimpleMatrix dx = null; - try { - // Check if the determinant is close to zero - double determinant = df.determinant(); - if (Math.abs(determinant) < 1e-10) { - logger.warn("Matrix is nearly singular. Determinant: " + determinant); - // Add a small regularization term to stabilize the solution - dx = df.plus(identitytimesConst.scale(1e-6)).solve(f).negative(); - } else { - dx = df.plus(identitytimesConst).solve(f).negative(); - } - } catch (Exception e) { - logger.error("Error solving matrix equation: " + e.getMessage()); - logger.debug("Attempting fallback with scaled regularization..."); - try { - // Fallback: Add a larger regularization term and retry - dx = df.plus(identitytimesConst.scale(0.5)).solve(f).negative(); - } catch (Exception ex) { - logger.error("Fallback matrix solve failed: " + ex.getMessage()); - logger.debug("Attempting pseudo-inverse fallback..."); - try { - DMatrixRMaj pinv = new DMatrixRMaj(df.numCols(), df.numRows()); - CommonOps_DDRM.pinv(df.getDDRM(), pinv); - DMatrixRMaj result = new DMatrixRMaj(df.numCols(), 1); - CommonOps_DDRM.mult(pinv, f.getDDRM(), result); - dx = SimpleMatrix.wrap(result).negative(); - logger.warn("Used pseudo-inverse matrix solve."); - } catch (Exception ex2) { - logger.error("Pseudo-inverse fallback failed: " + ex2.getMessage()); - logger.warn("Setting dx to zero matrix as a last resort."); - dx = new SimpleMatrix(f.numRows(), f.numCols()); - } - } - } - - for (int i = 0; i < system.getPhase(0).getNumberOfComponents(); i++) { - double alphaNew = alpha[i] + dx.get(i, 0); - Wi[j][i] = Math.pow(alphaNew / 2.0, 2.0); - if (system.getPhase(0).getComponent(i).getz() > 1e-100) { - logWi[i] = Math.log(Wi[j][i]); - } - if (system.getPhase(0).getComponent(i).getIonicCharge() != 0 - || system.getPhase(0).getComponent(i).isIsIon()) { - logWi[i] = -1000.0; - } - err += Math.abs((logWi[i] - oldlogw[i]) / oldlogw[i]); - } - } - // logger.info("err: " + err); - sumw[j] = 0; - - for (int i = 0; i < system.getPhase(0).getNumberOfComponents(); i++) { - sumw[j] += safeExp(logWi[i]); - } - - for (int i = 0; i < system.getPhase(0).getNumberOfComponents(); i++) { - if (system.getPhase(0).getComponent(i).getx() > 1e-100) { - clonedSystem.get(0).getPhase(1).getComponent(i).setx(safeExp(logWi[i]) / sumw[j]); - } - if (system.getPhase(0).getComponent(i).getIonicCharge() != 0 - || system.getPhase(0).getComponent(i).isIsIon()) { - clonedSystem.get(0).getPhase(1).getComponent(i).setx(1e-50); - } - } - } while (!enhancedTrialInitFailed && (Math.abs(err) > 1e-9 || err > errOld) && iter < maxiter); + errOld = err; + iter++; + err = 0; + + if (iter <= maxsucssubiter || !system.isImplementedCompositionDeriativesofFugacity()) { + if (iter % 7 == 0 && useaccsubst) { + double vec1 = 0.0; + + double vec2 = 0.0; + double prod1 = 0.0; + double prod2 = 0.0; + for (i = 0; i < system.getPhase(0).getNumberOfComponents(); i++) { + vec1 = oldDeltalogWi[i] * oldoldDeltalogWi[i]; + vec2 = Math.pow(oldoldDeltalogWi[i], 2.0); + prod1 += vec1 * vec2; + prod2 += vec2 * vec2; + } + + double lambda = prod1 / prod2; + // logger.info("lambda " + lambda); + for (i = 0; i < system.getPhase(0).getNumberOfComponents(); i++) { + logWi[i] += lambda / (1.0 - lambda) * deltalogWi[i]; + err += Math.abs((logWi[i] - oldlogw[i]) / oldlogw[i]); + Wi[j][i] = safeExp(logWi[i]); + } + } else { + for (int i = 0; i < system.getPhase(0).getNumberOfComponents(); i++) { + oldoldoldlogw[i] = oldoldlogw[i]; + oldoldlogw[i] = oldlogw[i]; + oldlogw[i] = logWi[i]; + oldoldDeltalogWi[i] = oldoldlogw[i] - oldoldoldlogw[i]; + oldDeltalogWi[i] = oldlogw[i] - oldoldlogw[i]; + } + try { + clonedSystem.get(0).init(1, 1); + } catch (Exception ex) { + enhancedTrialInitFailed = true; + break; + } + for (int i = 0; i < system.getPhase(0).getNumberOfComponents(); i++) { + // oldlogw[i] = logWi[i]; + if (!Double.isInfinite( + clonedSystem.get(0).getPhase(1).getComponent(i).getLogFugacityCoefficient()) + && system.getPhase(0).getComponent(i).getx() > 1e-100) { + logWi[i] = d[i] + - clonedSystem.get(0).getPhase(1).getComponent(i).getLogFugacityCoefficient(); + if (clonedSystem.get(0).getPhase(1).getComponent(i).getIonicCharge() != 0) { + logWi[i] = -1000.0; + } + } + deltalogWi[i] = logWi[i] - oldlogw[i]; + err += Math.abs(logWi[i] - oldlogw[i]); + Wi[j][i] = safeExp(logWi[i]); + useaccsubst = true; + } + if (iter > 2 && err > errOld) { + useaccsubst = false; + } + } + } else { + int nComp = system.getPhases()[0].getNumberOfComponents(); + double[] f = new double[nComp]; + double[][] df = new double[nComp][nComp]; + // if (!secondOrderStabilityAnalysis) { + for (int i = 0; i < system.getPhase(0).getNumberOfComponents(); i++) { + oldoldoldlogw[i] = oldoldlogw[i]; + oldoldlogw[i] = oldlogw[i]; + oldlogw[i] = logWi[i]; + oldoldDeltalogWi[i] = oldoldlogw[i] - oldoldoldlogw[i]; + oldDeltalogWi[i] = oldlogw[i] - oldoldlogw[i]; + } + try { + clonedSystem.get(0).init(3, 1); + } catch (Exception ex) { + enhancedTrialInitFailed = true; + break; + } + alpha = new double[clonedSystem.get(0).getPhases()[0].getNumberOfComponents()]; + + for (int i = 0; i < clonedSystem.get(0).getPhases()[0].getNumberOfComponents(); i++) { + alpha[i] = 2.0 * Math.sqrt(Wi[j][i]); + } + + for (int i = 0; i < system.getPhase(0).getNumberOfComponents(); i++) { + if (system.getPhase(0).getComponent(i).getz() > 1e-100) { + f[i] = Math.sqrt(Wi[j][i]) * (Math.log(Wi[j][i]) + + clonedSystem.get(0).getPhases()[1].getComponent(i).getLogFugacityCoefficient() + - d[i]); + } + for (int k = 0; k < clonedSystem.get(0).getPhases()[0].getNumberOfComponents(); k++) { + double kronDelt = (i == k) ? 1.0 : 0.0; + if (system.getPhase(0).getComponent(i).getz() > 1e-100) { + df[i][k] = kronDelt + Math.sqrt(Wi[j][k] * Wi[j][i]) + * clonedSystem.get(0).getPhases()[1].getComponent(i).getdfugdn(k); + } else { + df[i][k] = 0.0; + } + } + } + + double[] dx = new double[nComp]; + try { + // Check if the determinant is close to zero + double determinant = LinearAlgebraOps.determinant(df); + if (Math.abs(determinant) < 1e-10) { + logger.warn("Matrix is nearly singular. Determinant: " + determinant); + // Add a small regularization term to stabilize the solution + double[][] regularized = new double[nComp][nComp]; + for (int r = 0; r < nComp; r++) { + for (int c = 0; c < nComp; c++) { + regularized[r][c] = df[r][c]; + } + regularized[r][r] += 1.0e-6; + } + if (!LinearAlgebraOps.solveLinearSystem(regularized, f, dx)) { + throw new RuntimeException("Regularized LU solve failed"); + } + } else { + double[][] regularized = new double[nComp][nComp]; + for (int r = 0; r < nComp; r++) { + for (int c = 0; c < nComp; c++) { + regularized[r][c] = df[r][c]; + } + regularized[r][r] += 1.0; + } + if (!LinearAlgebraOps.solveLinearSystem(regularized, f, dx)) { + throw new RuntimeException("LU solve failed"); + } + } + for (int r = 0; r < nComp; r++) { + dx[r] = -dx[r]; + } + } catch (Exception e) { + logger.error("Error solving matrix equation: " + e.getMessage()); + logger.debug("Attempting fallback with scaled regularization..."); + try { + // Fallback: Add a larger regularization term and retry + double[][] regularized = new double[nComp][nComp]; + for (int r = 0; r < nComp; r++) { + for (int c = 0; c < nComp; c++) { + regularized[r][c] = df[r][c]; + } + regularized[r][r] += 0.5; + } + if (!LinearAlgebraOps.solveLinearSystem(regularized, f, dx)) { + throw new RuntimeException("Fallback LU solve failed"); + } + for (int r = 0; r < nComp; r++) { + dx[r] = -dx[r]; + } + } catch (Exception ex) { + logger.error("Fallback matrix solve failed: " + ex.getMessage()); + logger.debug("Attempting pseudo-inverse fallback..."); + try { + if (!LinearAlgebraOps.pseudoInverseSolve(df, f, dx)) { + throw new IllegalStateException("ojAlgo SVD decomposition failed"); + } + for (int r = 0; r < nComp; r++) { + dx[r] = -dx[r]; + } + logger.warn("Used pseudo-inverse matrix solve."); + } catch (Exception ex2) { + logger.error("Pseudo-inverse fallback failed: " + ex2.getMessage()); + logger.warn("Setting dx to zero matrix as a last resort."); + dx = new double[nComp]; + } + } + } + + for (int i = 0; i < system.getPhase(0).getNumberOfComponents(); i++) { + double alphaNew = alpha[i] + dx[i]; + Wi[j][i] = Math.pow(alphaNew / 2.0, 2.0); + if (system.getPhase(0).getComponent(i).getz() > 1e-100) { + logWi[i] = Math.log(Wi[j][i]); + } + if (system.getPhase(0).getComponent(i).getIonicCharge() != 0 + || system.getPhase(0).getComponent(i).isIsIon()) { + logWi[i] = -1000.0; + } + err += Math.abs((logWi[i] - oldlogw[i]) / oldlogw[i]); + } + } + // logger.info("err: " + err); + sumw[j] = 0; + + for (int i = 0; i < system.getPhase(0).getNumberOfComponents(); i++) { + sumw[j] += safeExp(logWi[i]); + } + + for (int i = 0; i < system.getPhase(0).getNumberOfComponents(); i++) { + if (system.getPhase(0).getComponent(i).getx() > 1e-100) { + clonedSystem.get(0).getPhase(1).getComponent(i).setx(safeExp(logWi[i]) / sumw[j]); + } + if (system.getPhase(0).getComponent(i).getIonicCharge() != 0 + || system.getPhase(0).getComponent(i).isIsIon()) { + clonedSystem.get(0).getPhase(1).getComponent(i).setx(1e-50); + } + } + } while (!enhancedTrialInitFailed && (Math.abs(err) > 1e-9 || err > errOld) + && iter < maxiter); if (enhancedTrialInitFailed) { - tm[j] = 10.0; - continue; + tm[j] = 10.0; + continue; } // logger.info("err: " + err + " ITER " + iter); @@ -1485,55 +1556,57 @@ public void stabilityAnalysis3() { tm[j] = 1.0; for (int i = 0; i < system.getPhase(1).getNumberOfComponents(); i++) { - if (system.getPhase(0).getComponent(i).getx() > 1e-100) { - tm[j] -= safeExp(logWi[i]); - } - x[j][i] = clonedSystem.get(0).getPhase(1).getComponent(i).getx(); - // logger.info("txji: " + x[j][i]); + if (system.getPhase(0).getComponent(i).getx() > 1e-100) { + tm[j] -= safeExp(logWi[i]); + } + x[j][i] = clonedSystem.get(0).getPhase(1).getComponent(i).getx(); + // logger.info("txji: " + x[j][i]); - xTrivialCheck0 += Math.abs(x[j][i] - system.getPhase(0).getComponent(i).getx()); - xTrivialCheck1 += Math.abs(x[j][i] - system.getPhase(1).getComponent(i).getx()); + xTrivialCheck0 += Math.abs(x[j][i] - system.getPhase(0).getComponent(i).getx()); + xTrivialCheck1 += Math.abs(x[j][i] - system.getPhase(1).getComponent(i).getx()); } if (iter >= maxiter - 1) { - // logger.info("iter > maxiter multiphase stability "); - // logger.info("error " + Math.abs(err)); - // logger.info("tm: " + tm[j]); + // logger.info("iter > maxiter multiphase stability "); + // logger.info("error " + Math.abs(err)); + // logger.info("tm: " + tm[j]); } if (Math.abs(xTrivialCheck0) < 1e-4 || Math.abs(xTrivialCheck1) < 1e-4) { - tm[j] = 10.0; + tm[j] = 10.0; } if (tm[j] < -1e-8) { - break; + break; } } int unstabcomp = 0; for (int k = system.getPhase(0).getNumberOfComponents() - 1; k >= 0; k--) { if (tm[k] < -1e-8 && !(Double.isNaN(tm[k]))) { - system.addPhase(); - unstabcomp = k; - for (int i = 0; i < system.getPhase(1).getNumberOfComponents(); i++) { - system.getPhase(system.getNumberOfPhases() - 1).getComponent(i).setx(x[k][i]); - } - system.getPhases()[system.getNumberOfPhases() - 1].normalize(); - multiPhaseTest = true; - system.setBeta(system.getNumberOfPhases() - 1, system.getPhase(0).getComponent(unstabcomp).getz()); - try { - system.init(1); - } catch (Exception ex) { - logger.warn("stabilityAnalysisEnhanced pure-comp addPhase init failed: " + ex.getMessage()); - system.removePhaseKeepTotalComposition(system.getNumberOfPhases() - 1); - multiPhaseTest = false; - return; - } - system.normalizeBeta(); - - // logger.info("STABILITY ANALYSIS: "); - // logger.info("tm1: " + k + " "+ tm[k]); - // system.display(); - return; + system.addPhase(); + unstabcomp = k; + for (int i = 0; i < system.getPhase(1).getNumberOfComponents(); i++) { + system.getPhase(system.getNumberOfPhases() - 1).getComponent(i).setx(x[k][i]); + } + system.getPhases()[system.getNumberOfPhases() - 1].normalize(); + multiPhaseTest = true; + system.setBeta(system.getNumberOfPhases() - 1, + system.getPhase(0).getComponent(unstabcomp).getz()); + try { + system.init(1); + } catch (Exception ex) { + logger + .warn("stabilityAnalysisEnhanced pure-comp addPhase init failed: " + ex.getMessage()); + system.removePhaseKeepTotalComposition(system.getNumberOfPhases() - 1); + multiPhaseTest = false; + return; + } + system.normalizeBeta(); + + // logger.info("STABILITY ANALYSIS: "); + // logger.info("tm1: " + k + " "+ tm[k]); + // system.display(); + return; } } @@ -1550,7 +1623,8 @@ public void stabilityAnalysis3() { */ public void stabilityAnalysis2() { double[] logWi = new double[system.getPhase(0).getNumberOfComponents()]; - double[][] Wi = new double[system.getPhase(0).getNumberOfComponents()][system.getPhase(0).getNumberOfComponents()]; + double[][] Wi = new double[system.getPhase(0).getNumberOfComponents()][system.getPhase(0) + .getNumberOfComponents()]; double[] deltalogWi = new double[system.getPhases()[0].getNumberOfComponents()]; double[] oldDeltalogWi = new double[system.getPhases()[0].getNumberOfComponents()]; @@ -1561,7 +1635,8 @@ public void stabilityAnalysis2() { double[] oldoldlogw = new double[system.getPhases()[0].getNumberOfComponents()]; double[] oldoldoldlogw = new double[system.getPhases()[0].getNumberOfComponents()]; double[] d = new double[system.getPhase(0).getNumberOfComponents()]; - double[][] x = new double[system.getPhase(0).getNumberOfComponents()][system.getPhase(0).getNumberOfComponents()]; + double[][] x = new double[system.getPhase(0).getNumberOfComponents()][system.getPhase(0) + .getNumberOfComponents()]; tm = new double[system.getPhase(0).getNumberOfComponents()]; double[] alpha = null; @@ -1573,22 +1648,22 @@ public void stabilityAnalysis2() { minimumGibbsEnergySystem = system; for (int i = 0; i < system.getPhase(0).getNumberOfComponents(); i++) { if (system.getPhase(0).getComponent(i).getx() < 1e-100) { - clonedSystem.add(null); - continue; + clonedSystem.add(null); + continue; } double numb = 0; clonedSystem.add(system.clone()); // (clonedSystem.get(i)).init(0); commented out sept 2005, Even // S. for (int j = 0; j < system.getPhase(0).getNumberOfComponents(); j++) { - numb = i == j ? 1.0 : 1.0e-12; - if (system.getPhase(0).getComponent(j).getz() < 1e-100) { - numb = 0; - } - (clonedSystem.get(i)).getPhase(1).getComponent(j).setx(numb); + numb = i == j ? 1.0 : 1.0e-12; + if (system.getPhase(0).getComponent(j).getz() < 1e-100) { + numb = 0; + } + (clonedSystem.get(i)).getPhase(1).getComponent(j).setx(numb); } if (system.getPhase(0).getComponent(i).getIonicCharge() == 0) { - (clonedSystem.get(i)).init(1); + (clonedSystem.get(i)).init(1); } } @@ -1597,35 +1672,36 @@ public void stabilityAnalysis2() { // logger.info("low gibbs phase " + lowestGibbsEnergyPhase); for (int k = 0; k < minimumGibbsEnergySystem.getPhase(0).getNumberOfComponents(); k++) { for (int i = 0; i < minimumGibbsEnergySystem.getPhase(0).getNumberOfComponents(); i++) { - if (!((clonedSystem.get(k)) == null)) { - sumw[k] += (clonedSystem.get(k)).getPhase(1).getComponent(i).getx(); - } + if (!((clonedSystem.get(k)) == null)) { + sumw[k] += (clonedSystem.get(k)).getPhase(1).getComponent(i).getx(); + } } } for (int k = 0; k < minimumGibbsEnergySystem.getPhase(0).getNumberOfComponents(); k++) { for (int i = 0; i < minimumGibbsEnergySystem.getPhase(0).getNumberOfComponents(); i++) { - if (!((clonedSystem.get(k)) == null) && system.getPhase(0).getComponent(k).getx() > 1e-100) { - (clonedSystem.get(k)).getPhase(1).getComponent(i) - .setx((clonedSystem.get(k)).getPhase(1).getComponent(i).getx() / sumw[k]); - } - // logger.info("x: " + ( - // clonedSystem.get(k)).getPhase(0).getComponent(i).getx()); + if (!((clonedSystem.get(k)) == null) + && system.getPhase(0).getComponent(k).getx() > 1e-100) { + (clonedSystem.get(k)).getPhase(1).getComponent(i) + .setx((clonedSystem.get(k)).getPhase(1).getComponent(i).getx() / sumw[k]); + } + // logger.info("x: " + ( + // clonedSystem.get(k)).getPhase(0).getComponent(i).getx()); } if (system.getPhase(0).getComponent(k).getx() > 1e-100) { - d[k] = Math.log(system.getPhase(0).getComponent(k).getx()) - + system.getPhase(0).getComponent(k).getLogFugacityCoefficient(); - // if(minimumGibbsEnergySystem.getPhases()[lowestGibbsEnergyPhase].getComponent(k).getIonicCharge()!=0) - // d[k]=0; + d[k] = Math.log(system.getPhase(0).getComponent(k).getx()) + + system.getPhase(0).getComponent(k).getLogFugacityCoefficient(); + // if(minimumGibbsEnergySystem.getPhases()[lowestGibbsEnergyPhase].getComponent(k).getIonicCharge()!=0) + // d[k]=0; } // logger.info("dk: " + d[k]); } for (int j = 0; j < minimumGibbsEnergySystem.getPhase(0).getNumberOfComponents(); j++) { if (system.getPhase(0).getComponent(j).getz() > 1e-100) { - logWi[j] = 1.0; + logWi[j] = 1.0; } else { - logWi[j] = -10000.0; + logWi[j] = -10000.0; } } @@ -1635,38 +1711,40 @@ public void stabilityAnalysis2() { double Mmin = 1e10; for (int i = 0; i < minimumGibbsEnergySystem.getPhase(0).getNumberOfComponents(); i++) { if (minimumGibbsEnergySystem.getPhase(0).getComponent(i).isHydrocarbon()) { - if ((minimumGibbsEnergySystem.getPhase(0).getComponent(i).getMolarMass()) > Mmax) { - Mmax = minimumGibbsEnergySystem.getPhase(0).getComponent(i).getMolarMass(); - } - if ((minimumGibbsEnergySystem.getPhase(0).getComponent(i).getMolarMass()) < Mmin) { - Mmin = minimumGibbsEnergySystem.getPhase(0).getComponent(i).getMolarMass(); - } + if ((minimumGibbsEnergySystem.getPhase(0).getComponent(i).getMolarMass()) > Mmax) { + Mmax = minimumGibbsEnergySystem.getPhase(0).getComponent(i).getMolarMass(); + } + if ((minimumGibbsEnergySystem.getPhase(0).getComponent(i).getMolarMass()) < Mmin) { + Mmin = minimumGibbsEnergySystem.getPhase(0).getComponent(i).getMolarMass(); + } } } for (int i = 0; i < minimumGibbsEnergySystem.getPhase(0).getNumberOfComponents(); i++) { if (minimumGibbsEnergySystem.getPhase(0).getComponent(i).isHydrocarbon() - && minimumGibbsEnergySystem.getPhase(0).getComponent(i).getz() > 1e-50) { - if (Math.abs((minimumGibbsEnergySystem.getPhase(0).getComponent(i).getMolarMass()) - Mmax) < 1e-5) { - hydrocarbonTestCompNumb = i; - // logger.info("CHECKING heavy component " + hydrocarbonTestCompNumb); - } + && minimumGibbsEnergySystem.getPhase(0).getComponent(i).getz() > 1e-50) { + if (Math.abs( + (minimumGibbsEnergySystem.getPhase(0).getComponent(i).getMolarMass()) - Mmax) < 1e-5) { + hydrocarbonTestCompNumb = i; + // logger.info("CHECKING heavy component " + hydrocarbonTestCompNumb); + } } if (minimumGibbsEnergySystem.getPhase(0).getComponent(i).isHydrocarbon() - && minimumGibbsEnergySystem.getPhase(0).getComponent(i).getz() > 1e-50) { - if (Math.abs((minimumGibbsEnergySystem.getPhase(0).getComponent(i).getMolarMass()) - Mmin) < 1e-5) { - lightTestCompNumb = i; - // logger.info("CHECKING light component " + lightTestCompNumb); - } + && minimumGibbsEnergySystem.getPhase(0).getComponent(i).getz() > 1e-50) { + if (Math.abs( + (minimumGibbsEnergySystem.getPhase(0).getComponent(i).getMolarMass()) - Mmin) < 1e-5) { + lightTestCompNumb = i; + // logger.info("CHECKING light component " + lightTestCompNumb); + } } } for (int j = system.getPhase(0).getNumberOfComponents() - 1; j >= 0; j--) { if (minimumGibbsEnergySystem.getPhase(0).getComponent(j).getx() < 1e-100 - || (minimumGibbsEnergySystem.getPhase(0).getComponent(j).getIonicCharge() != 0) - || (minimumGibbsEnergySystem.getPhase(0).getComponent(j).isHydrocarbon() && j != hydrocarbonTestCompNumb - && j != lightTestCompNumb)) { - continue; + || (minimumGibbsEnergySystem.getPhase(0).getComponent(j).getIonicCharge() != 0) + || (minimumGibbsEnergySystem.getPhase(0).getComponent(j).isHydrocarbon() + && j != hydrocarbonTestCompNumb && j != lightTestCompNumb)) { + continue; } // if(minimumGibbsEnergySystem.getPhase(0).getComponent(j).getName().equals("water") // && minimumGibbsEnergySystem.isChemicalSystem()) continue; @@ -1676,162 +1754,195 @@ public void stabilityAnalysis2() { int iter = 0; double errOld = 1.0e100; do { - errOld = err; - iter++; - err = 0; - - if (iter <= 150 || !system.isImplementedCompositionDeriativesofFugacity()) { - if (iter % 7 == 0) { - double vec1 = 0.0; - - double vec2 = 0.0; - double prod1 = 0.0; - double prod2 = 0.0; - for (i = 0; i < system.getPhase(0).getNumberOfComponents(); i++) { - vec1 = oldDeltalogWi[i] * oldoldDeltalogWi[i]; - vec2 = Math.pow(oldoldDeltalogWi[i], 2.0); - prod1 += vec1 * vec2; - prod2 += vec2 * vec2; - } - - double lambda = prod1 / prod2; - // logger.info("lambda " + lambda); - for (i = 0; i < system.getPhase(0).getNumberOfComponents(); i++) { - logWi[i] += lambda / (1.0 - lambda) * deltalogWi[i]; - err += Math.abs((logWi[i] - oldlogw[i]) / oldlogw[i]); - Wi[j][i] = safeExp(logWi[i]); - } - } else { - for (int i = 0; i < system.getPhase(0).getNumberOfComponents(); i++) { - oldoldoldlogw[i] = oldoldlogw[i]; - oldoldlogw[i] = oldlogw[i]; - oldlogw[i] = logWi[i]; - oldoldDeltalogWi[i] = oldoldlogw[i] - oldoldoldlogw[i]; - oldDeltalogWi[i] = oldlogw[i] - oldoldlogw[i]; - } - (clonedSystem.get(j)).init(1, 1); - for (int i = 0; i < system.getPhase(0).getNumberOfComponents(); i++) { - // oldlogw[i] = logWi[i]; - if (!Double.isInfinite((clonedSystem.get(j)).getPhase(1).getComponent(i).getLogFugacityCoefficient()) - && system.getPhase(0).getComponent(i).getx() > 1e-100) { - logWi[i] = d[i] - (clonedSystem.get(j)).getPhase(1).getComponent(i).getLogFugacityCoefficient(); - if ((clonedSystem.get(j)).getPhase(1).getComponent(i).getIonicCharge() != 0) { - logWi[i] = -1000.0; - } - } - deltalogWi[i] = logWi[i] - oldlogw[i]; - err += Math.abs(logWi[i] - oldlogw[i]); - Wi[j][i] = safeExp(logWi[i]); - } - } - } else { - SimpleMatrix f = new SimpleMatrix(system.getPhases()[0].getNumberOfComponents(), 1); - SimpleMatrix df = null; - SimpleMatrix identitytimesConst = null; - // if (!secondOrderStabilityAnalysis) { - for (int i = 0; i < system.getPhase(0).getNumberOfComponents(); i++) { - oldoldoldlogw[i] = oldoldlogw[i]; - oldoldlogw[i] = oldlogw[i]; - oldlogw[i] = logWi[i]; - oldoldDeltalogWi[i] = oldoldlogw[i] - oldoldoldlogw[i]; - oldDeltalogWi[i] = oldlogw[i] - oldoldlogw[i]; - } - (clonedSystem.get(j)).init(3, 1); - alpha = new double[(clonedSystem.get(j)).getPhases()[0].getNumberOfComponents()]; - df = new SimpleMatrix(system.getPhases()[0].getNumberOfComponents(), - system.getPhases()[0].getNumberOfComponents()); - identitytimesConst = SimpleMatrix.identity(system.getPhases()[0].getNumberOfComponents()); - // , system.getPhases()[0].getNumberOfComponents()); - // secondOrderStabilityAnalysis = true; - // } - - for (int i = 0; i < (clonedSystem.get(j)).getPhases()[0].getNumberOfComponents(); i++) { - alpha[i] = 2.0 * Math.sqrt(Wi[j][i]); - } - - for (int i = 0; i < system.getPhase(0).getNumberOfComponents(); i++) { - if (system.getPhase(0).getComponent(i).getz() > 1e-100) { - f.set(i, 0, Math.sqrt(Wi[j][i]) * (Math.log(Wi[j][i]) - + (clonedSystem.get(j)).getPhases()[1].getComponent(i).getLogFugacityCoefficient() - d[i])); - } - for (int k = 0; k < (clonedSystem.get(j)).getPhases()[0].getNumberOfComponents(); k++) { - double kronDelt = (i == k) ? 1.0 : 0.0; - if (system.getPhase(0).getComponent(i).getz() > 1e-100) { - df.set(i, k, kronDelt + Math.sqrt(Wi[j][k] * Wi[j][i]) - * (clonedSystem.get(j)).getPhases()[1].getComponent(i).getdfugdn(k)); - // * - // clonedSystem.getPhases()[j].getNumberOfMolesInPhase()); - } else { - df.set(i, k, 0); - // * - // clonedSystem.getPhases()[j].getNumberOfMolesInPhase()); - } - } - } - // f.print(10, 10); - // df.print(10, 10); - SimpleMatrix dx = null; - try { - // Check if the determinant is close to zero - double determinant = df.determinant(); - if (Math.abs(determinant) < 1e-10) { - logger.warn("Matrix is nearly singular. Determinant: " + determinant); - // Add a small regularization term to stabilize the solution - dx = df.plus(identitytimesConst.scale(1e-6)).solve(f).negative(); - } else { - dx = df.plus(identitytimesConst).solve(f).negative(); - } - } catch (Exception e) { - logger.error("Error solving matrix equation: " + e.getMessage()); - logger.debug("Attempting fallback with scaled regularization..."); - try { - // Fallback: Add a larger regularization term and retry - dx = df.plus(identitytimesConst.scale(0.5)).solve(f).negative(); - } catch (Exception ex) { - logger.error("Fallback matrix solve failed: " + ex.getMessage()); - throw new RuntimeException("Matrix solve failed after fallback attempts", ex); - } - } - - // dx.print(10, 10); - - for (int i = 0; i < system.getPhase(0).getNumberOfComponents(); i++) { - double alphaNew = alpha[i] + dx.get(i, 0); - Wi[j][i] = Math.pow(alphaNew / 2.0, 2.0); - if (system.getPhase(0).getComponent(i).getz() > 1e-100) { - logWi[i] = Math.log(Wi[j][i]); - } - if (system.getPhase(0).getComponent(i).getIonicCharge() != 0 - || system.getPhase(0).getComponent(i).isIsIon()) { - logWi[i] = -1000.0; - } - err += Math.abs((logWi[i] - oldlogw[i]) / oldlogw[i]); - } - - // logger.info("err newton " + err); - } - // logger.info("err: " + err); - sumw[j] = 0; - - for (int i = 0; i < system.getPhase(0).getNumberOfComponents(); i++) { - sumw[j] += safeExp(logWi[i]); - } - - for (int i = 0; i < system.getPhase(0).getNumberOfComponents(); i++) { - if (system.getPhase(0).getComponent(i).getx() > 1e-100) { - (clonedSystem.get(j)).getPhase(1).getComponent(i).setx(safeExp(logWi[i]) / sumw[j]); - } - if (system.getPhase(0).getComponent(i).getIonicCharge() != 0 - || system.getPhase(0).getComponent(i).isIsIon()) { - (clonedSystem.get(j)).getPhase(1).getComponent(i).setx(1e-50); - } - } + errOld = err; + iter++; + err = 0; + + if (iter <= 150 || !system.isImplementedCompositionDeriativesofFugacity()) { + if (iter % 7 == 0) { + double vec1 = 0.0; + + double vec2 = 0.0; + double prod1 = 0.0; + double prod2 = 0.0; + for (i = 0; i < system.getPhase(0).getNumberOfComponents(); i++) { + vec1 = oldDeltalogWi[i] * oldoldDeltalogWi[i]; + vec2 = Math.pow(oldoldDeltalogWi[i], 2.0); + prod1 += vec1 * vec2; + prod2 += vec2 * vec2; + } + + double lambda = prod1 / prod2; + // logger.info("lambda " + lambda); + for (i = 0; i < system.getPhase(0).getNumberOfComponents(); i++) { + logWi[i] += lambda / (1.0 - lambda) * deltalogWi[i]; + err += Math.abs((logWi[i] - oldlogw[i]) / oldlogw[i]); + Wi[j][i] = safeExp(logWi[i]); + } + } else { + for (int i = 0; i < system.getPhase(0).getNumberOfComponents(); i++) { + oldoldoldlogw[i] = oldoldlogw[i]; + oldoldlogw[i] = oldlogw[i]; + oldlogw[i] = logWi[i]; + oldoldDeltalogWi[i] = oldoldlogw[i] - oldoldoldlogw[i]; + oldDeltalogWi[i] = oldlogw[i] - oldoldlogw[i]; + } + (clonedSystem.get(j)).init(1, 1); + for (int i = 0; i < system.getPhase(0).getNumberOfComponents(); i++) { + // oldlogw[i] = logWi[i]; + if (!Double.isInfinite( + (clonedSystem.get(j)).getPhase(1).getComponent(i).getLogFugacityCoefficient()) + && system.getPhase(0).getComponent(i).getx() > 1e-100) { + logWi[i] = d[i] + - (clonedSystem.get(j)).getPhase(1).getComponent(i).getLogFugacityCoefficient(); + if ((clonedSystem.get(j)).getPhase(1).getComponent(i).getIonicCharge() != 0) { + logWi[i] = -1000.0; + } + } + deltalogWi[i] = logWi[i] - oldlogw[i]; + err += Math.abs(logWi[i] - oldlogw[i]); + Wi[j][i] = safeExp(logWi[i]); + } + } + } else { + int nComp = system.getPhases()[0].getNumberOfComponents(); + double[] f = new double[nComp]; + double[][] df = new double[nComp][nComp]; + // if (!secondOrderStabilityAnalysis) { + for (int i = 0; i < system.getPhase(0).getNumberOfComponents(); i++) { + oldoldoldlogw[i] = oldoldlogw[i]; + oldoldlogw[i] = oldlogw[i]; + oldlogw[i] = logWi[i]; + oldoldDeltalogWi[i] = oldoldlogw[i] - oldoldoldlogw[i]; + oldDeltalogWi[i] = oldlogw[i] - oldoldlogw[i]; + } + (clonedSystem.get(j)).init(3, 1); + alpha = new double[(clonedSystem.get(j)).getPhases()[0].getNumberOfComponents()]; + // , system.getPhases()[0].getNumberOfComponents()); + // secondOrderStabilityAnalysis = true; + // } + + for (int i = 0; i < (clonedSystem.get(j)).getPhases()[0].getNumberOfComponents(); i++) { + alpha[i] = 2.0 * Math.sqrt(Wi[j][i]); + } + + for (int i = 0; i < system.getPhase(0).getNumberOfComponents(); i++) { + if (system.getPhase(0).getComponent(i).getz() > 1e-100) { + f[i] = Math.sqrt(Wi[j][i]) * (Math.log(Wi[j][i]) + + (clonedSystem.get(j)).getPhases()[1].getComponent(i).getLogFugacityCoefficient() + - d[i]); + } + for (int k = 0; k < (clonedSystem.get(j)).getPhases()[0].getNumberOfComponents(); k++) { + double kronDelt = (i == k) ? 1.0 : 0.0; + if (system.getPhase(0).getComponent(i).getz() > 1e-100) { + df[i][k] = kronDelt + Math.sqrt(Wi[j][k] * Wi[j][i]) + * (clonedSystem.get(j)).getPhases()[1].getComponent(i).getdfugdn(k); + // * + // clonedSystem.getPhases()[j].getNumberOfMolesInPhase()); + } else { + df[i][k] = 0.0; + // * + // clonedSystem.getPhases()[j].getNumberOfMolesInPhase()); + } + } + } + // f.print(10, 10); + // df.print(10, 10); + double[] dx = new double[nComp]; + try { + // Check if the determinant is close to zero + double determinant = LinearAlgebraOps.determinant(df); + if (Math.abs(determinant) < 1e-10) { + logger.warn("Matrix is nearly singular. Determinant: " + determinant); + // Add a small regularization term to stabilize the solution + double[][] regularized = new double[nComp][nComp]; + for (int r = 0; r < nComp; r++) { + for (int c = 0; c < nComp; c++) { + regularized[r][c] = df[r][c]; + } + regularized[r][r] += 1.0e-6; + } + if (!LinearAlgebraOps.solveLinearSystem(regularized, f, dx)) { + throw new RuntimeException("Regularized LU solve failed"); + } + } else { + double[][] regularized = new double[nComp][nComp]; + for (int r = 0; r < nComp; r++) { + for (int c = 0; c < nComp; c++) { + regularized[r][c] = df[r][c]; + } + regularized[r][r] += 1.0; + } + if (!LinearAlgebraOps.solveLinearSystem(regularized, f, dx)) { + throw new RuntimeException("LU solve failed"); + } + } + for (int r = 0; r < nComp; r++) { + dx[r] = -dx[r]; + } + } catch (Exception e) { + logger.error("Error solving matrix equation: " + e.getMessage()); + logger.debug("Attempting fallback with scaled regularization..."); + try { + // Fallback: Add a larger regularization term and retry + double[][] regularized = new double[nComp][nComp]; + for (int r = 0; r < nComp; r++) { + for (int c = 0; c < nComp; c++) { + regularized[r][c] = df[r][c]; + } + regularized[r][r] += 0.5; + } + if (!LinearAlgebraOps.solveLinearSystem(regularized, f, dx)) { + throw new RuntimeException("Fallback LU solve failed"); + } + for (int r = 0; r < nComp; r++) { + dx[r] = -dx[r]; + } + } catch (Exception ex) { + logger.error("Fallback matrix solve failed: " + ex.getMessage()); + throw new RuntimeException("Matrix solve failed after fallback attempts", ex); + } + } + + // dx.print(10, 10); + + for (int i = 0; i < system.getPhase(0).getNumberOfComponents(); i++) { + double alphaNew = alpha[i] + dx[i]; + Wi[j][i] = Math.pow(alphaNew / 2.0, 2.0); + if (system.getPhase(0).getComponent(i).getz() > 1e-100) { + logWi[i] = Math.log(Wi[j][i]); + } + if (system.getPhase(0).getComponent(i).getIonicCharge() != 0 + || system.getPhase(0).getComponent(i).isIsIon()) { + logWi[i] = -1000.0; + } + err += Math.abs((logWi[i] - oldlogw[i]) / oldlogw[i]); + } + + // logger.info("err newton " + err); + } + // logger.info("err: " + err); + sumw[j] = 0; + + for (int i = 0; i < system.getPhase(0).getNumberOfComponents(); i++) { + sumw[j] += safeExp(logWi[i]); + } + + for (int i = 0; i < system.getPhase(0).getNumberOfComponents(); i++) { + if (system.getPhase(0).getComponent(i).getx() > 1e-100) { + (clonedSystem.get(j)).getPhase(1).getComponent(i).setx(safeExp(logWi[i]) / sumw[j]); + } + if (system.getPhase(0).getComponent(i).getIonicCharge() != 0 + || system.getPhase(0).getComponent(i).isIsIon()) { + (clonedSystem.get(j)).getPhase(1).getComponent(i).setx(1e-50); + } + } } while ((Math.abs(err) > 1e-9 || err > errOld) && iter < 200); if (iter > 198) { - // System.out.println("too many iterations....." + err + " temperature " - // + system.getTemperature("C") + " C " + system.getPressure("bara") + " bara"); - throw new RuntimeException( - new neqsim.util.exception.TooManyIterationsException(this, "stabilityAnalysis2", 200)); + // System.out.println("too many iterations....." + err + " temperature " + // + system.getTemperature("C") + " C " + system.getPressure("bara") + " bara"); + throw new RuntimeException( + new neqsim.util.exception.TooManyIterationsException(this, "stabilityAnalysis2", 200)); } // logger.info("err: " + err + " ITER " + iter); double xTrivialCheck0 = 0.0; @@ -1840,55 +1951,56 @@ public void stabilityAnalysis2() { tm[j] = 1.0; for (int i = 0; i < system.getPhase(1).getNumberOfComponents(); i++) { - if (system.getPhase(0).getComponent(i).getx() > 1e-100) { - tm[j] -= safeExp(logWi[i]); - } - x[j][i] = (clonedSystem.get(j)).getPhase(1).getComponent(i).getx(); - // logger.info("txji: " + x[j][i]); + if (system.getPhase(0).getComponent(i).getx() > 1e-100) { + tm[j] -= safeExp(logWi[i]); + } + x[j][i] = (clonedSystem.get(j)).getPhase(1).getComponent(i).getx(); + // logger.info("txji: " + x[j][i]); - xTrivialCheck0 += Math.abs(x[j][i] - system.getPhase(0).getComponent(i).getx()); - xTrivialCheck1 += Math.abs(x[j][i] - system.getPhase(1).getComponent(i).getx()); + xTrivialCheck0 += Math.abs(x[j][i] - system.getPhase(0).getComponent(i).getx()); + xTrivialCheck1 += Math.abs(x[j][i] - system.getPhase(1).getComponent(i).getx()); } if (iter >= 199) { - logger.info("iter > maxiter multiphase stability "); - logger.info("error " + Math.abs(err)); - logger.info("tm: " + tm[j]); + logger.info("iter > maxiter multiphase stability "); + logger.info("error " + Math.abs(err)); + logger.info("tm: " + tm[j]); } if (Math.abs(xTrivialCheck0) < 1e-6 || Math.abs(xTrivialCheck1) < 1e-6) { - tm[j] = 10.0; + tm[j] = 10.0; } if (tm[j] < -1e-8) { - break; + break; } } int unstabcomp = 0; for (int k = system.getPhase(0).getNumberOfComponents() - 1; k >= 0; k--) { if (tm[k] < -1e-8 && !(Double.isNaN(tm[k]))) { - system.addPhase(); - unstabcomp = k; - for (int i = 0; i < system.getPhase(1).getNumberOfComponents(); i++) { - system.getPhase(system.getNumberOfPhases() - 1).getComponent(i).setx(x[k][i]); - } - system.getPhases()[system.getNumberOfPhases() - 1].normalize(); - multiPhaseTest = true; - system.setBeta(system.getNumberOfPhases() - 1, system.getPhase(0).getComponent(unstabcomp).getz()); - try { - system.init(1); - } catch (Exception ex) { - logger.warn("stabilityAnalysis3 addPhase init failed: " + ex.getMessage()); - system.removePhaseKeepTotalComposition(system.getNumberOfPhases() - 1); - multiPhaseTest = false; - return; - } - system.normalizeBeta(); - - // logger.info("STABILITY ANALYSIS: "); - // logger.info("tm1: " + k + " "+ tm[k]); - // system.display(); - return; + system.addPhase(); + unstabcomp = k; + for (int i = 0; i < system.getPhase(1).getNumberOfComponents(); i++) { + system.getPhase(system.getNumberOfPhases() - 1).getComponent(i).setx(x[k][i]); + } + system.getPhases()[system.getNumberOfPhases() - 1].normalize(); + multiPhaseTest = true; + system.setBeta(system.getNumberOfPhases() - 1, + system.getPhase(0).getComponent(unstabcomp).getz()); + try { + system.init(1); + } catch (Exception ex) { + logger.warn("stabilityAnalysis3 addPhase init failed: " + ex.getMessage()); + system.removePhaseKeepTotalComposition(system.getNumberOfPhases() - 1); + multiPhaseTest = false; + return; + } + system.normalizeBeta(); + + // logger.info("STABILITY ANALYSIS: "); + // logger.info("tm1: " + k + " "+ tm[k]); + // system.display(); + return; } } system.normalizeBeta(); @@ -1908,10 +2020,10 @@ private boolean seedAdditionalPhaseFromFeed() { for (int phase = 0; phase < system.getNumberOfPhases(); phase++) { PhaseType type = system.getPhase(phase).getType(); if (type == PhaseType.GAS && system.getPhase(phase).getBeta() > 1.0e-6) { - return false; + return false; } if (type == PhaseType.AQUEOUS) { - hasAqueous = true; + hasAqueous = true; } } if (!hasAqueous) { @@ -1922,10 +2034,10 @@ private boolean seedAdditionalPhaseFromFeed() { waterZ = system.getComponent("water").getz(); } catch (Exception ex) { for (int comp = 0; comp < system.getPhase(0).getNumberOfComponents(); comp++) { - if ("water".equals(system.getPhase(0).getComponent(comp).getComponentName())) { - waterZ = system.getPhase(0).getComponent(comp).getz(); - break; - } + if ("water".equals(system.getPhase(0).getComponent(comp).getComponentName())) { + waterZ = system.getPhase(0).getComponent(comp).getz(); + break; + } } } if (waterZ < 1.0e-4) { @@ -1934,9 +2046,9 @@ private boolean seedAdditionalPhaseFromFeed() { boolean hasHydrocarbon = false; for (int comp = 0; comp < system.getPhase(0).getNumberOfComponents(); comp++) { if (system.getPhase(0).getComponent(comp).isHydrocarbon() - && system.getPhase(0).getComponent(comp).getz() > 1.0e-4) { - hasHydrocarbon = true; - break; + && system.getPhase(0).getComponent(comp).getz() > 1.0e-4) { + hasHydrocarbon = true; + break; } } if (!hasHydrocarbon) { @@ -1963,11 +2075,11 @@ private boolean seedAdditionalPhaseFromFeed() { } /** - * Ensures only one aqueous phase exists in the system. The aqueous phase is the one with the highest aqueous - * component content (water, MEG, TEG, DEG, methanol, ethanol, and ions). Other liquid phases are reclassified as OIL - * by moving their aqueous components (water, glycols, ions) to the true aqueous phase and keeping hydrocarbons in the - * oil phase. This method applies to systems with ions (where ions must be confined to the aqueous phase) or chemical - * systems. + * Ensures only one aqueous phase exists in the system. The aqueous phase is the one with the + * highest aqueous component content (water, MEG, TEG, DEG, methanol, ethanol, and ions). Other + * liquid phases are reclassified as OIL by moving their aqueous components (water, glycols, ions) + * to the true aqueous phase and keeping hydrocarbons in the oil phase. This method applies to + * systems with ions (where ions must be confined to the aqueous phase) or chemical systems. */ private void ensureSingleAqueousPhase() { // Only needed for systems with ions or chemical systems - skip for simple molecular systems @@ -1979,7 +2091,7 @@ private void ensureSingleAqueousPhase() { int aqueousCount = 0; for (int phase = 0; phase < system.getNumberOfPhases(); phase++) { if (system.getPhase(phase).getType() == PhaseType.AQUEOUS) { - aqueousCount++; + aqueousCount++; } } @@ -1993,24 +2105,24 @@ private void ensureSingleAqueousPhase() { for (int phase = 0; phase < system.getNumberOfPhases(); phase++) { if (system.getPhase(phase).getType() == PhaseType.GAS) { - continue; + continue; } double aqueousContent = 0.0; for (int comp = 0; comp < system.getPhase(phase).getNumberOfComponents(); comp++) { - ComponentInterface component = system.getPhase(phase).getComponent(comp); - String name = component.getComponentName().toLowerCase(); - // Count water, glycols, alcohols, and ions as aqueous components - if (name.equals("water") || name.equals("meg") || name.equals("teg") || name.equals("deg") - || name.equals("methanol") || name.equals("ethanol") || component.getIonicCharge() != 0 - || component.isIsIon()) { - aqueousContent += component.getx(); - } + ComponentInterface component = system.getPhase(phase).getComponent(comp); + String name = component.getComponentName().toLowerCase(); + // Count water, glycols, alcohols, and ions as aqueous components + if (name.equals("water") || name.equals("meg") || name.equals("teg") || name.equals("deg") + || name.equals("methanol") || name.equals("ethanol") || component.getIonicCharge() != 0 + || component.isIsIon()) { + aqueousContent += component.getx(); + } } if (aqueousContent > maxAqueousContent) { - maxAqueousContent = aqueousContent; - bestAqueousPhase = phase; + maxAqueousContent = aqueousContent; + bestAqueousPhase = phase; } } @@ -2023,30 +2135,30 @@ private void ensureSingleAqueousPhase() { // This will cause init() to reclassify them as OIL for (int phase = 0; phase < system.getNumberOfPhases(); phase++) { if (phase == bestAqueousPhase || system.getPhase(phase).getType() == PhaseType.GAS) { - continue; + continue; } if (system.getPhase(phase).getType() == PhaseType.AQUEOUS) { - // This phase should become OIL - adjust compositions - // Set ions and most aqueous components to trace amounts - for (int comp = 0; comp < system.getPhase(phase).getNumberOfComponents(); comp++) { - ComponentInterface component = system.getPhase(phase).getComponent(comp); - String name = component.getComponentName().toLowerCase(); - - if (component.getIonicCharge() != 0 || component.isIsIon()) { - // Ions only in aqueous phase - component.setx(1e-50); - } else if (name.equals("water")) { - // Reduce water significantly but keep trace for solubility - component.setx(Math.min(component.getx() * 0.01, 1e-4)); - } else if (name.equals("meg") || name.equals("teg") || name.equals("deg") || name.equals("methanol") - || name.equals("ethanol")) { - // Reduce glycols/alcohols - component.setx(Math.min(component.getx() * 0.1, 1e-3)); - } - // Hydrocarbons keep their current x values - } - system.getPhase(phase).normalize(); + // This phase should become OIL - adjust compositions + // Set ions and most aqueous components to trace amounts + for (int comp = 0; comp < system.getPhase(phase).getNumberOfComponents(); comp++) { + ComponentInterface component = system.getPhase(phase).getComponent(comp); + String name = component.getComponentName().toLowerCase(); + + if (component.getIonicCharge() != 0 || component.isIsIon()) { + // Ions only in aqueous phase + component.setx(1e-50); + } else if (name.equals("water")) { + // Reduce water significantly but keep trace for solubility + component.setx(Math.min(component.getx() * 0.01, 1e-4)); + } else if (name.equals("meg") || name.equals("teg") || name.equals("deg") + || name.equals("methanol") || name.equals("ethanol")) { + // Reduce glycols/alcohols + component.setx(Math.min(component.getx() * 0.1, 1e-3)); + } + // Hydrocarbons keep their current x values + } + system.getPhase(phase).normalize(); } } @@ -2063,7 +2175,7 @@ private boolean seedHydrocarbonLiquidFromFeed() { return false; } if (system.getNumberOfPhases() >= 3 || system.hasPhaseType(PhaseType.OIL) - || !system.hasPhaseType(PhaseType.AQUEOUS)) { + || !system.hasPhaseType(PhaseType.AQUEOUS)) { return false; } @@ -2072,10 +2184,10 @@ private boolean seedHydrocarbonLiquidFromFeed() { waterZ = system.getComponent("water").getz(); } catch (Exception ex) { for (int comp = 0; comp < system.getPhase(0).getNumberOfComponents(); comp++) { - if ("water".equals(system.getPhase(0).getComponent(comp).getComponentName())) { - waterZ = system.getPhase(0).getComponent(comp).getz(); - break; - } + if ("water".equals(system.getPhase(0).getComponent(comp).getComponentName())) { + waterZ = system.getPhase(0).getComponent(comp).getz(); + break; + } } } @@ -2086,8 +2198,9 @@ private boolean seedHydrocarbonLiquidFromFeed() { double heavyHydrocarbonTotal = 0.0; for (int comp = 0; comp < system.getPhase(0).getNumberOfComponents(); comp++) { ComponentInterface component = system.getPhase(0).getComponent(comp); - if (component.isHydrocarbon() && component.getz() > 1.0e-6 && component.getMolarMass() > 0.045) { - heavyHydrocarbonTotal += component.getz(); + if (component.isHydrocarbon() && component.getz() > 1.0e-6 + && component.getMolarMass() > 0.045) { + heavyHydrocarbonTotal += component.getz(); } } // Seed oil phase if there's significant heavy hydrocarbon content @@ -2111,15 +2224,15 @@ private boolean seedHydrocarbonLiquidFromFeed() { double z = component.getz(); double x = 1.0e-16; if (component.getIonicCharge() != 0 || component.isIsIon()) { - x = 1.0e-16; + x = 1.0e-16; } else if (component.isHydrocarbon()) { - if (component.getMolarMass() > 0.045) { - x = Math.max(z, 1.0e-12); - } else { - x = Math.min(z * 1.0e-2, 1.0e-8); - } + if (component.getMolarMass() > 0.045) { + x = Math.max(z, 1.0e-12); + } else { + x = Math.min(z * 1.0e-2, 1.0e-8); + } } else if ("water".equalsIgnoreCase(component.getComponentName())) { - x = Math.min(z * 1.0e-2, 1.0e-8); + x = Math.min(z * 1.0e-2, 1.0e-8); } system.getPhase(phaseIndex).getComponent(comp).setx(x); } @@ -2155,18 +2268,19 @@ public void run() { if (hasIons) { ionicZ = new double[system.getPhase(0).getNumberOfComponents()]; for (int i = 0; i < system.getPhase(0).getNumberOfComponents(); i++) { - if (system.getPhase(0).getComponent(i).getIonicCharge() != 0 || system.getPhase(0).getComponent(i).isIsIon()) { - ionicZ[i] = system.getPhase(0).getComponent(i).getz(); - // Temporarily set ion z to near-zero for stability analysis - for (int phase = 0; phase < system.getNumberOfPhases(); phase++) { - system.getPhase(phase).getComponent(i).setz(1e-100); - } - } + if (system.getPhase(0).getComponent(i).getIonicCharge() != 0 + || system.getPhase(0).getComponent(i).isIsIon()) { + ionicZ[i] = system.getPhase(0).getComponent(i).getz(); + // Temporarily set ion z to near-zero for stability analysis + for (int phase = 0; phase < system.getNumberOfPhases(); phase++) { + system.getPhase(phase).getComponent(i).setz(1e-100); + } + } } try { - system.init(1); + system.init(1); } catch (Exception ex) { - logger.warn("Ion-stripping init failed: " + ex.getMessage()); + logger.warn("Ion-stripping init failed: " + ex.getMessage()); } } @@ -2177,9 +2291,10 @@ public void run() { // phases, try enhanced version which uses Wilson K-value initial guesses and tests both // vapor-like and liquid-like trial phases for more robust detection of liquid-liquid // equilibria (e.g., sour gas, CO2 systems) - if (shouldApplyEnhancedMultiPhaseCheck() && !multiPhaseTest && system.getNumberOfPhases() < 3) { - enhancedStabilityChecked = true; - stabilityAnalysisEnhanced(); + if (shouldApplyEnhancedMultiPhaseCheck() && !multiPhaseTest + && system.getNumberOfPhases() < 3) { + enhancedStabilityChecked = true; + stabilityAnalysisEnhanced(); } } @@ -2198,50 +2313,51 @@ public void run() { int newestPhase = system.getNumberOfPhases() - 1; double newestWaterX = 0; try { - newestWaterX = system.getPhase(newestPhase).getComponent("water").getx(); + newestWaterX = system.getPhase(newestPhase).getComponent("water").getx(); } catch (Exception ex) { - // no water component + // no water component } boolean isDuplicate = false; if (newestWaterX > 0.5) { - for (int pp = 0; pp < newestPhase && !isDuplicate; pp++) { - double existingWaterX = 0; - try { - existingWaterX = system.getPhase(pp).getComponent("water").getx(); - } catch (Exception ex) { - continue; - } - if (existingWaterX <= 0.5) { - continue; - } - // Both phases are water-dominated — compare non-ionic compositions - double maxAbsDiff = 0; - for (int k = 0; k < system.getPhase(0).getNumberOfComponents(); k++) { - if (system.getPhase(0).getComponent(k).getIonicCharge() != 0) { - continue; - } - double diff = Math - .abs(system.getPhase(newestPhase).getComponent(k).getx() - system.getPhase(pp).getComponent(k).getx()); - if (diff > maxAbsDiff) { - maxAbsDiff = diff; - } - } - if (maxAbsDiff < 0.05) { - isDuplicate = true; - } - } + for (int pp = 0; pp < newestPhase && !isDuplicate; pp++) { + double existingWaterX = 0; + try { + existingWaterX = system.getPhase(pp).getComponent("water").getx(); + } catch (Exception ex) { + continue; + } + if (existingWaterX <= 0.5) { + continue; + } + // Both phases are water-dominated — compare non-ionic compositions + double maxAbsDiff = 0; + for (int k = 0; k < system.getPhase(0).getNumberOfComponents(); k++) { + if (system.getPhase(0).getComponent(k).getIonicCharge() != 0) { + continue; + } + double diff = Math.abs(system.getPhase(newestPhase).getComponent(k).getx() + - system.getPhase(pp).getComponent(k).getx()); + if (diff > maxAbsDiff) { + maxAbsDiff = diff; + } + } + if (maxAbsDiff < 0.05) { + isDuplicate = true; + } + } } if (isDuplicate) { - // Spurious duplicate — revert to pre-stability state - logger.debug("Rejecting spurious aqueous duplicate phase from ion-stripped stability analysis"); - system.removePhaseKeepTotalComposition(newestPhase); - system.normalizeBeta(); - try { - system.init(1); - } catch (Exception ex) { - logger.warn("init after spurious phase rejection failed: " + ex.getMessage()); - } - multiPhaseTest = false; + // Spurious duplicate — revert to pre-stability state + logger.debug( + "Rejecting spurious aqueous duplicate phase from ion-stripped stability analysis"); + system.removePhaseKeepTotalComposition(newestPhase); + system.normalizeBeta(); + try { + system.init(1); + } catch (Exception ex) { + logger.warn("init after spurious phase rejection failed: " + ex.getMessage()); + } + multiPhaseTest = false; } } if (!multiPhaseTest && seedAdditionalPhaseFromFeed()) { @@ -2257,44 +2373,47 @@ public void run() { // Debug: Check phases after stability analysis (before ion restoration) if (hasIons) { - logger.debug("After stability analysis (ions removed): {} phases", system.getNumberOfPhases()); + logger.debug("After stability analysis (ions removed): {} phases", + system.getNumberOfPhases()); for (int phase = 0; phase < system.getNumberOfPhases(); phase++) { - logger.debug(" Phase {} type: {}", phase, system.getPhase(phase).getType()); + logger.debug(" Phase {} type: {}", phase, system.getPhase(phase).getType()); } } // Restore ions to aqueous phase(s) after stability analysis if (hasIons && ionicZ != null) { - aqueousPhaseNumber = system.hasPhaseType(PhaseType.AQUEOUS) ? system.getPhaseNumberOfPhase("aqueous") : -1; + aqueousPhaseNumber = + system.hasPhaseType(PhaseType.AQUEOUS) ? system.getPhaseNumberOfPhase("aqueous") : -1; for (int i = 0; i < system.getPhase(0).getNumberOfComponents(); i++) { - if ((system.getPhase(0).getComponent(i).getIonicCharge() != 0 || system.getPhase(0).getComponent(i).isIsIon()) - && ionicZ[i] > 1e-100) { - // Restore z values - for (int phase = 0; phase < system.getNumberOfPhases(); phase++) { - system.getPhase(phase).getComponent(i).setz(ionicZ[i]); - // Set ions only in aqueous phase, near-zero in others - if (system.getPhase(phase).getType() == PhaseType.AQUEOUS) { - system.getPhase(phase).getComponent(i).setx(ionicZ[i]); - } else { - system.getPhase(phase).getComponent(i).setx(1e-50); - } - } - } + if ((system.getPhase(0).getComponent(i).getIonicCharge() != 0 + || system.getPhase(0).getComponent(i).isIsIon()) && ionicZ[i] > 1e-100) { + // Restore z values + for (int phase = 0; phase < system.getNumberOfPhases(); phase++) { + system.getPhase(phase).getComponent(i).setz(ionicZ[i]); + // Set ions only in aqueous phase, near-zero in others + if (system.getPhase(phase).getType() == PhaseType.AQUEOUS) { + system.getPhase(phase).getComponent(i).setx(ionicZ[i]); + } else { + system.getPhase(phase).getComponent(i).setx(1e-50); + } + } + } } // Normalize aqueous phase and reinitialize for (int phase = 0; phase < system.getNumberOfPhases(); phase++) { - system.getPhase(phase).normalize(); + system.getPhase(phase).normalize(); } try { - system.init(1); + system.init(1); } catch (Exception ex) { - logger.warn("Ion-restore init failed: " + ex.getMessage()); + logger.warn("Ion-restore init failed: " + ex.getMessage()); } } // system.init(1); // system.display(); - aqueousPhaseNumber = system.hasPhaseType(PhaseType.AQUEOUS) ? system.getPhaseNumberOfPhase("aqueous") : -1; + aqueousPhaseNumber = + system.hasPhaseType(PhaseType.AQUEOUS) ? system.getPhaseNumberOfPhase("aqueous") : -1; if (system.isChemicalSystem() && aqueousPhaseNumber >= 0) { system.getChemicalReactionOperations().solveChemEq(aqueousPhaseNumber, 0); system.getChemicalReactionOperations().solveChemEq(aqueousPhaseNumber, 1); @@ -2310,134 +2429,138 @@ public void run() { double maxerr = 1e-12; do { - iterOut++; - if (system.isChemicalSystem() && system.hasPhaseType(PhaseType.AQUEOUS)) { - int currentAqueousPhase = system.getPhaseNumberOfPhase("aqueous"); - if (currentAqueousPhase != aqueousPhaseNumber) { - aqueousPhaseNumber = currentAqueousPhase; - system.getChemicalReactionOperations().solveChemEq(aqueousPhaseNumber, 0); - } - - if (aqueousPhaseNumber >= 0 && aqueousPhaseNumber < system.getNumberOfPhases()) { - chemdev = 0.0; - double[] xchem = new double[system.getPhase(aqueousPhaseNumber).getNumberOfComponents()]; - - for (i = 0; i < system.getPhase(0).getNumberOfComponents(); i++) { - xchem[i] = system.getPhase(aqueousPhaseNumber).getComponent(i).getx(); - } - - try { - system.init(1); - system.getChemicalReactionOperations().solveChemEq(aqueousPhaseNumber, 1); - - for (i = 0; i < system.getPhase(0).getNumberOfComponents(); i++) { - chemdev += Math.abs(xchem[i] - system.getPhase(aqueousPhaseNumber).getComponent(i).getx()); - } - } catch (Exception ex) { - logger.warn("Chemical equilibrium init failed: " + ex.getMessage()); - chemdev = 0.0; - } - } - } - setDoubleArrays(); - iterations = 0; - do { - iterations++; - // oldBeta = system.getBeta(system.getNumberOfPhases() - 1); - // system.init(1); - oldDiff = diff; - diff = this.solveBeta(); - // diff = Math.abs((system.getBeta(system.getNumberOfPhases() - 1) - oldBeta) / - // oldBeta); - // System.out.println("diff multiphase " + diff); - if (iterations % 50 == 0) { - maxerr *= 100.0; - } - } while (diff > maxerr && !removePhase && (diff < oldDiff || iterations < 50) && iterations < 200); - // this.solveBeta(true); - if (iterations >= 199) { - logger.error("error in multiphase flash..did not solve in 200 iterations"); - logger.error( - "diff " + diff + " temperaure " + system.getTemperature("C") + " pressure " + system.getPressure("bara")); - diff = this.solveBeta(); - } + iterOut++; + if (system.isChemicalSystem() && system.hasPhaseType(PhaseType.AQUEOUS)) { + int currentAqueousPhase = system.getPhaseNumberOfPhase("aqueous"); + if (currentAqueousPhase != aqueousPhaseNumber) { + aqueousPhaseNumber = currentAqueousPhase; + system.getChemicalReactionOperations().solveChemEq(aqueousPhaseNumber, 0); + } + + if (aqueousPhaseNumber >= 0 && aqueousPhaseNumber < system.getNumberOfPhases()) { + chemdev = 0.0; + double[] xchem = + new double[system.getPhase(aqueousPhaseNumber).getNumberOfComponents()]; + + for (i = 0; i < system.getPhase(0).getNumberOfComponents(); i++) { + xchem[i] = system.getPhase(aqueousPhaseNumber).getComponent(i).getx(); + } + + try { + system.init(1); + system.getChemicalReactionOperations().solveChemEq(aqueousPhaseNumber, 1); + + for (i = 0; i < system.getPhase(0).getNumberOfComponents(); i++) { + chemdev += + Math.abs(xchem[i] - system.getPhase(aqueousPhaseNumber).getComponent(i).getx()); + } + } catch (Exception ex) { + logger.warn("Chemical equilibrium init failed: " + ex.getMessage()); + chemdev = 0.0; + } + } + } + setDoubleArrays(); + iterations = 0; + do { + iterations++; + // oldBeta = system.getBeta(system.getNumberOfPhases() - 1); + // system.init(1); + oldDiff = diff; + diff = this.solveBeta(); + // diff = Math.abs((system.getBeta(system.getNumberOfPhases() - 1) - oldBeta) / + // oldBeta); + // System.out.println("diff multiphase " + diff); + if (iterations % 50 == 0) { + maxerr *= 100.0; + } + } while (diff > maxerr && !removePhase && (diff < oldDiff || iterations < 50) + && iterations < 200); + // this.solveBeta(true); + if (iterations >= 199) { + logger.error("error in multiphase flash..did not solve in 200 iterations"); + logger.error("diff " + diff + " temperaure " + system.getTemperature("C") + " pressure " + + system.getPressure("bara")); + diff = this.solveBeta(); + } } while ((Math.abs(chemdev) > 1e-10 && iterOut < 100) - || (iterOut < 3 && system.isChemicalSystem() && system.hasPhaseType(PhaseType.AQUEOUS))); + || (iterOut < 3 && system.isChemicalSystem() && system.hasPhaseType(PhaseType.AQUEOUS))); // After flash converges, check for additional phases (three-phase detection) // This is particularly important for systems like CO2/H2S/hydrocarbon mixtures // that may exhibit vapor-liquid-liquid equilibrium - if (system.doMultiPhaseCheck() && system.getNumberOfPhases() >= 2 && system.getNumberOfPhases() < 3 - && !postFlashStabilityChecked && !enhancedStabilityChecked) { - postFlashStabilityChecked = true; - int oldNumPhases = system.getNumberOfPhases(); - enhancedStabilityChecked = true; - stabilityAnalysisEnhanced(); - if (system.getNumberOfPhases() > oldNumPhases) { - // Found a third phase - re-run the flash calculation - multiPhaseTest = true; - doStabilityAnalysis = false; - requestBoundedRerun(); - } + if (system.doMultiPhaseCheck() && system.getNumberOfPhases() >= 2 + && system.getNumberOfPhases() < 3 && !postFlashStabilityChecked + && !enhancedStabilityChecked) { + postFlashStabilityChecked = true; + int oldNumPhases = system.getNumberOfPhases(); + enhancedStabilityChecked = true; + stabilityAnalysisEnhanced(); + if (system.getNumberOfPhases() > oldNumPhases) { + // Found a third phase - re-run the flash calculation + multiPhaseTest = true; + doStabilityAnalysis = false; + requestBoundedRerun(); + } } // Check if water is present and if an aqueous phase should be seeded // Only try to seed aqueous phase once per flash operation (not on recursive calls) if (system.hasComponent("water") && !aqueousPhaseSeedAttempted && system.doMultiPhaseCheck() - && !system.hasPhaseType(PhaseType.AQUEOUS)) { - aqueousPhaseSeedAttempted = true; - double waterZ = 0.0; - int waterComponentIndex = -1; - try { - waterZ = system.getComponent("water").getz(); - waterComponentIndex = system.getComponent("water").getComponentNumber(); - } catch (Exception ex) { - for (int comp = 0; comp < system.getPhase(0).getNumberOfComponents(); comp++) { - if ("water".equals(system.getPhase(0).getComponent(comp).getComponentName())) { - waterZ = system.getPhase(0).getComponent(comp).getz(); - waterComponentIndex = comp; - break; - } - } - } - - // If water content is significant (> 1e-6), seed an aqueous phase. - // Limit total active phases to a maximum of 3 (e.g. gas, liquid, aqueous) to avoid - // indexing beyond what downstream algorithms expect. Do not create a new aqueous - // phase if one already exists. - if (waterZ > 1.0e-6 && waterComponentIndex >= 0 && system.getNumberOfPhases() < 3 - && !system.hasPhaseType(PhaseType.AQUEOUS)) { - system.addPhase(); - int aquPhaseIndex = system.getNumberOfPhases() - 1; - system.setPhaseType(aquPhaseIndex, PhaseType.AQUEOUS); - - // Initialize aqueous phase with water and trace amounts of other components - for (int comp = 0; comp < system.getPhase(0).getNumberOfComponents(); comp++) { - double x = 1.0e-16; - if (comp == waterComponentIndex) { - // Concentrate water in aqueous phase - x = Math.max(waterZ, 1.0e-12); - } else if (!system.getPhase(0).getComponent(comp).isHydrocarbon() - && !system.getPhase(0).getComponent(comp).isInert()) { - // Other aqueous components get trace amounts - x = Math.min(system.getPhase(0).getComponent(comp).getz() * 1.0e-2, 1.0e-8); - } - system.getPhase(aquPhaseIndex).getComponent(comp).setx(x); - } - - system.getPhases()[aquPhaseIndex].normalize(); - double initialBeta = Math.max(1.0e-5, 10.0 * phaseFractionMinimumLimit); - system.setBeta(aquPhaseIndex, initialBeta); - system.normalizeBeta(); - try { - system.init(1); - } catch (Exception ex) { - logger.warn("Aqueous phase seeding init failed, removing phase: " + ex.getMessage()); - system.removePhaseKeepTotalComposition(aquPhaseIndex); - } - multiPhaseTest = true; - doStabilityAnalysis = false; - } + && !system.hasPhaseType(PhaseType.AQUEOUS)) { + aqueousPhaseSeedAttempted = true; + double waterZ = 0.0; + int waterComponentIndex = -1; + try { + waterZ = system.getComponent("water").getz(); + waterComponentIndex = system.getComponent("water").getComponentNumber(); + } catch (Exception ex) { + for (int comp = 0; comp < system.getPhase(0).getNumberOfComponents(); comp++) { + if ("water".equals(system.getPhase(0).getComponent(comp).getComponentName())) { + waterZ = system.getPhase(0).getComponent(comp).getz(); + waterComponentIndex = comp; + break; + } + } + } + + // If water content is significant (> 1e-6), seed an aqueous phase. + // Limit total active phases to a maximum of 3 (e.g. gas, liquid, aqueous) to avoid + // indexing beyond what downstream algorithms expect. Do not create a new aqueous + // phase if one already exists. + if (waterZ > 1.0e-6 && waterComponentIndex >= 0 && system.getNumberOfPhases() < 3 + && !system.hasPhaseType(PhaseType.AQUEOUS)) { + system.addPhase(); + int aquPhaseIndex = system.getNumberOfPhases() - 1; + system.setPhaseType(aquPhaseIndex, PhaseType.AQUEOUS); + + // Initialize aqueous phase with water and trace amounts of other components + for (int comp = 0; comp < system.getPhase(0).getNumberOfComponents(); comp++) { + double x = 1.0e-16; + if (comp == waterComponentIndex) { + // Concentrate water in aqueous phase + x = Math.max(waterZ, 1.0e-12); + } else if (!system.getPhase(0).getComponent(comp).isHydrocarbon() + && !system.getPhase(0).getComponent(comp).isInert()) { + // Other aqueous components get trace amounts + x = Math.min(system.getPhase(0).getComponent(comp).getz() * 1.0e-2, 1.0e-8); + } + system.getPhase(aquPhaseIndex).getComponent(comp).setx(x); + } + + system.getPhases()[aquPhaseIndex].normalize(); + double initialBeta = Math.max(1.0e-5, 10.0 * phaseFractionMinimumLimit); + system.setBeta(aquPhaseIndex, initialBeta); + system.normalizeBeta(); + try { + system.init(1); + } catch (Exception ex) { + logger.warn("Aqueous phase seeding init failed, removing phase: " + ex.getMessage()); + system.removePhaseKeepTotalComposition(aquPhaseIndex); + } + multiPhaseTest = true; + doStabilityAnalysis = false; + } } // For electrolyte systems: ensure only one aqueous phase - the one with most aqueous content @@ -2447,20 +2570,21 @@ public void run() { boolean hasRemovedPhase = false; for (int i = 0; i < system.getNumberOfPhases(); i++) { - if (system.getBeta(i) < 1.1 * phaseFractionMinimumLimit) { - // For systems with ions, never remove the only AQUEOUS phase — ions can only - // exist in aqueous phases. Removing it causes mass balance violations because - // setXY() forces ion x = 1e-50 in all non-aqueous phases. - if (hasIons && system.getPhase(i).getType() == PhaseType.AQUEOUS) { - logger.debug("Protecting aqueous phase {} from removal (beta={}) — ions require aqueous phase", i, - system.getBeta(i)); - continue; - } - system.removePhaseKeepTotalComposition(i); - doStabilityAnalysis = false; - hasRemovedPhase = true; - i--; // indices shift after removal — re-check the (new) phase at i - } + if (system.getBeta(i) < 1.1 * phaseFractionMinimumLimit) { + // For systems with ions, never remove the only AQUEOUS phase — ions can only + // exist in aqueous phases. Removing it causes mass balance violations because + // setXY() forces ion x = 1e-50 in all non-aqueous phases. + if (hasIons && system.getPhase(i).getType() == PhaseType.AQUEOUS) { + logger.debug( + "Protecting aqueous phase {} from removal (beta={}) — ions require aqueous phase", + i, system.getBeta(i)); + continue; + } + system.removePhaseKeepTotalComposition(i); + doStabilityAnalysis = false; + hasRemovedPhase = true; + i--; // indices shift after removal — re-check the (new) phase at i + } } // For ionic systems: if the aqueous phase survived with near-zero beta but a @@ -2469,98 +2593,103 @@ public void run() { // to let the system settle back to the 2-phase result (gas + aqueous) that the // initial TPflash found correctly. if (hasIons && !hasRemovedPhase && system.getNumberOfPhases() > 2) { - int aqIdx = system.hasPhaseType(PhaseType.AQUEOUS) ? system.getPhaseNumberOfPhase("aqueous") : -1; - if (aqIdx >= 0 && system.getBeta(aqIdx) < 10.0 * phaseFractionMinimumLimit) { - // Aqueous phase beta is very low — the 3-phase result is not converging - // properly. Remove the non-aqueous phase with the smallest beta instead. - int removeIdx = -1; - double minBeta = Double.MAX_VALUE; - for (int i = 0; i < system.getNumberOfPhases(); i++) { - if (i != aqIdx && system.getBeta(i) < minBeta) { - minBeta = system.getBeta(i); - removeIdx = i; - } - } - if (removeIdx >= 0) { - logger.debug("Removing spurious non-aqueous phase {} (beta={}) to preserve ionic aqueous phase", removeIdx, - minBeta); - system.removePhaseKeepTotalComposition(removeIdx); - doStabilityAnalysis = false; - hasRemovedPhase = true; - // Re-run beta solver with the 2-phase system to ensure convergence - setDoubleArrays(); - for (int iter2 = 0; iter2 < 50; iter2++) { - double d = this.solveBeta(); - if (d < 1e-10) { - break; - } - } - } - } + int aqIdx = + system.hasPhaseType(PhaseType.AQUEOUS) ? system.getPhaseNumberOfPhase("aqueous") : -1; + if (aqIdx >= 0 && system.getBeta(aqIdx) < 10.0 * phaseFractionMinimumLimit) { + // Aqueous phase beta is very low — the 3-phase result is not converging + // properly. Remove the non-aqueous phase with the smallest beta instead. + int removeIdx = -1; + double minBeta = Double.MAX_VALUE; + for (int i = 0; i < system.getNumberOfPhases(); i++) { + if (i != aqIdx && system.getBeta(i) < minBeta) { + minBeta = system.getBeta(i); + removeIdx = i; + } + } + if (removeIdx >= 0) { + logger.debug( + "Removing spurious non-aqueous phase {} (beta={}) to preserve ionic aqueous phase", + removeIdx, minBeta); + system.removePhaseKeepTotalComposition(removeIdx); + doStabilityAnalysis = false; + hasRemovedPhase = true; + // Re-run beta solver with the 2-phase system to ensure convergence + setDoubleArrays(); + for (int iter2 = 0; iter2 < 50; iter2++) { + double d = this.solveBeta(); + if (d < 1e-10) { + break; + } + } + } + } } boolean trivialSolution = false; for (int i = 0; i < system.getNumberOfPhases(); i++) { - for (int j = i + 1; j < system.getNumberOfPhases(); j++) { - if (Math.abs(system.getPhase(i).getDensity() - system.getPhase(j).getDensity()) < 1.1e-5) { - trivialSolution = true; - break; - } - } - if (trivialSolution) { - break; - } + for (int j = i + 1; j < system.getNumberOfPhases(); j++) { + if (Math + .abs(system.getPhase(i).getDensity() - system.getPhase(j).getDensity()) < 1.1e-5) { + trivialSolution = true; + break; + } + } + if (trivialSolution) { + break; + } } if (trivialSolution && !hasRemovedPhase) { - for (int i = 0; i < system.getNumberOfPhases() - 1; i++) { - for (int j = i + 1; j < system.getNumberOfPhases(); j++) { - if (Math.abs(system.getPhase(i).getDensity() - system.getPhase(j).getDensity()) < 1.1e-5) { - // Determine whether the two near-equal-density phases are genuine numerical - // duplicates (identical composition) or a legitimate near-critical V/L pair that - // merely shares a similar density. Only genuine duplicates may have their phase - // fractions merged; merging a real V/L pair would collapse the flash to a single - // phase (e.g. TPFlashTest.testRun5). - double maxCompDiffDup = 0.0; - for (int k = 0; k < system.getPhase(0).getNumberOfComponents(); k++) { - maxCompDiffDup = Math.max(maxCompDiffDup, - Math.abs(system.getPhase(i).getComponent(k).getx() - system.getPhase(j).getComponent(k).getx())); - } - // Merge the phase fractions (mass-conserving) only when the two phases are genuine - // composition duplicates AND the system still contains a genuine vapour (GAS) phase. - // A redundant duplicate that appears alongside a dominant vapour phase (e.g. the - // trace oil at a dew point in the UMR-PR-UMC trace oil-dropout regression) must have - // its mass merged back into its twin so the trace liquid is not halved. When NO gas - // phase is present the multiphase flash has collapsed to a vapour-less trivial - // solution (e.g. three identical liquid phases in TPFlashTest.testRun5); in that - // case discard one duplicate and let the bounded rerun re-separate the genuine - // phases. - boolean systemHasGasPhase = false; - for (int p = 0; p < system.getNumberOfPhases(); p++) { - if (system.getPhase(p).getType() == PhaseType.GAS) { - systemHasGasPhase = true; - break; - } - } - boolean genuineDuplicate = maxCompDiffDup < 1.0e-4 && systemHasGasPhase; - // Protect aqueous phase in ionic systems from trivial-solution removal - if (hasIons && system.getPhase(j).getType() == PhaseType.AQUEOUS) { - if (genuineDuplicate) { - // Remove the non-aqueous duplicate, merging its mass into the aqueous phase - mergeAndRemoveDuplicatePhase(j, i); - } else { - system.removePhaseKeepTotalComposition(i); - } - } else if (genuineDuplicate) { - mergeAndRemoveDuplicatePhase(i, j); - } else { - system.removePhaseKeepTotalComposition(j); - } - doStabilityAnalysis = false; - hasRemovedPhase = true; - } - } - } + for (int i = 0; i < system.getNumberOfPhases() - 1; i++) { + for (int j = i + 1; j < system.getNumberOfPhases(); j++) { + if (Math + .abs(system.getPhase(i).getDensity() - system.getPhase(j).getDensity()) < 1.1e-5) { + // Determine whether the two near-equal-density phases are genuine numerical + // duplicates (identical composition) or a legitimate near-critical V/L pair that + // merely shares a similar density. Only genuine duplicates may have their phase + // fractions merged; merging a real V/L pair would collapse the flash to a single + // phase (e.g. TPFlashTest.testRun5). + double maxCompDiffDup = 0.0; + for (int k = 0; k < system.getPhase(0).getNumberOfComponents(); k++) { + maxCompDiffDup = + Math.max(maxCompDiffDup, Math.abs(system.getPhase(i).getComponent(k).getx() + - system.getPhase(j).getComponent(k).getx())); + } + // Merge the phase fractions (mass-conserving) only when the two phases are genuine + // composition duplicates AND the system still contains a genuine vapour (GAS) phase. + // A redundant duplicate that appears alongside a dominant vapour phase (e.g. the + // trace oil at a dew point in the UMR-PR-UMC trace oil-dropout regression) must have + // its mass merged back into its twin so the trace liquid is not halved. When NO gas + // phase is present the multiphase flash has collapsed to a vapour-less trivial + // solution (e.g. three identical liquid phases in TPFlashTest.testRun5); in that + // case discard one duplicate and let the bounded rerun re-separate the genuine + // phases. + boolean systemHasGasPhase = false; + for (int p = 0; p < system.getNumberOfPhases(); p++) { + if (system.getPhase(p).getType() == PhaseType.GAS) { + systemHasGasPhase = true; + break; + } + } + boolean genuineDuplicate = maxCompDiffDup < 1.0e-4 && systemHasGasPhase; + // Protect aqueous phase in ionic systems from trivial-solution removal + if (hasIons && system.getPhase(j).getType() == PhaseType.AQUEOUS) { + if (genuineDuplicate) { + // Remove the non-aqueous duplicate, merging its mass into the aqueous phase + mergeAndRemoveDuplicatePhase(j, i); + } else { + system.removePhaseKeepTotalComposition(i); + } + } else if (genuineDuplicate) { + mergeAndRemoveDuplicatePhase(i, j); + } else { + system.removePhaseKeepTotalComposition(j); + } + doStabilityAnalysis = false; + hasRemovedPhase = true; + } + } + } } // Composition-based trivial solution detection: two phases of the SAME @@ -2578,34 +2707,35 @@ public void run() { String modelName = system.getModelName(); boolean isCpaModel = modelName != null && modelName.contains("CPA"); if (isCpaModel) { - for (int i = 0; i < system.getNumberOfPhases() - 1; i++) { - for (int j = i + 1; j < system.getNumberOfPhases(); j++) { - if (system.getPhase(i).getType() != system.getPhase(j).getType()) { - continue; - } - double maxCompDiff = 0.0; - for (int k = 0; k < system.getPhase(0).getNumberOfComponents(); k++) { - maxCompDiff = Math.max(maxCompDiff, - Math.abs(system.getPhase(i).getComponent(k).getx() - system.getPhase(j).getComponent(k).getx())); - } - if (maxCompDiff < 1.0e-6) { - mergeAndRemoveDuplicatePhase(i, j); - doStabilityAnalysis = false; - hasRemovedPhase = true; - j--; // adjust index after removal - } - } - } + for (int i = 0; i < system.getNumberOfPhases() - 1; i++) { + for (int j = i + 1; j < system.getNumberOfPhases(); j++) { + if (system.getPhase(i).getType() != system.getPhase(j).getType()) { + continue; + } + double maxCompDiff = 0.0; + for (int k = 0; k < system.getPhase(0).getNumberOfComponents(); k++) { + maxCompDiff = Math.max(maxCompDiff, Math.abs(system.getPhase(i).getComponent(k).getx() + - system.getPhase(j).getComponent(k).getx())); + } + if (maxCompDiff < 1.0e-6) { + mergeAndRemoveDuplicatePhase(i, j); + doStabilityAnalysis = false; + hasRemovedPhase = true; + j--; // adjust index after removal + } + } + } } /* * for (int i = 0; i < system.getNumberOfPhases()-1; i++) { if - * (Math.abs(system.getPhase(i).getDensity()-system.getPhase(i+1).getDensity())< 1e-6 && !hasRemovedPhase) { - * system.removePhase(i+1); doStabilityAnalysis=false; hasRemovedPhase = true; } } + * (Math.abs(system.getPhase(i).getDensity()-system.getPhase(i+1).getDensity())< 1e-6 && + * !hasRemovedPhase) { system.removePhase(i+1); doStabilityAnalysis=false; hasRemovedPhase = + * true; } } */ if (hasRemovedPhase && !secondTime) { - secondTime = true; - stabilityAnalysis3(); - requestBoundedRerun(); + secondTime = true; + stabilityAnalysis3(); + requestBoundedRerun(); } /* diff --git a/src/main/java/neqsim/thermodynamicoperations/phaseenvelopeops/multicomponentenvelopeops/CricondenBarFlash.java b/src/main/java/neqsim/thermodynamicoperations/phaseenvelopeops/multicomponentenvelopeops/CricondenBarFlash.java index 0ff6229e07..23b099df46 100644 --- a/src/main/java/neqsim/thermodynamicoperations/phaseenvelopeops/multicomponentenvelopeops/CricondenBarFlash.java +++ b/src/main/java/neqsim/thermodynamicoperations/phaseenvelopeops/multicomponentenvelopeops/CricondenBarFlash.java @@ -4,13 +4,15 @@ import org.apache.logging.log4j.Logger; import neqsim.thermo.phase.PhaseType; import neqsim.thermo.system.SystemInterface; +import neqsim.util.math.LinearAlgebraOps; /** * Direct cricondenbar calculation using the Michelsen simultaneous Newton method. * *

    - * Finds the maximum pressure point on the phase envelope by solving an (n+2)-dimensional Newton system simultaneously, - * following the formulation of Michelsen (1980) and Michelsen & Mollerup (2007), Chapter 12. + * Finds the maximum pressure point on the phase envelope by solving an (n+2)-dimensional Newton + * system simultaneously, following the formulation of Michelsen (1980) and Michelsen & Mollerup + * (2007), Chapter 12. *

    * *

    @@ -19,14 +21,14 @@ *

      *
    • g_i = ln K_i + ln phi_i^V(y,T,P) - ln phi_i^L(x,T,P) = 0, i=1..n (equilibrium)
    • *
    • g_{n+1} = sum_i z_i*(K_i - 1)/(1 + beta*(K_i - 1)) = 0 (Rachford-Rice summation)
    • - *
    • g_{n+2} = S_P = 0 (cricondenbar condition: dT/dP = 0 along envelope, which requires the sensitivity S_P of the - * envelope equations w.r.t. ln P specification to vanish)
    • + *
    • g_{n+2} = S_P = 0 (cricondenbar condition: dT/dP = 0 along envelope, which requires the + * sensitivity S_P of the envelope equations w.r.t. ln P specification to vanish)
    • *
    * *

    - * The full (n+2)x(n+2) Jacobian is built analytically using fugacity coefficient derivatives (d ln phi/dT, d ln phi/dP, - * d ln phi/dx_j) available from system.init(3). This gives quadratic convergence, typically converging in 3-8 - * iterations from a good initial estimate. + * The full (n+2)x(n+2) Jacobian is built analytically using fugacity coefficient derivatives (d ln + * phi/dT, d ln phi/dP, d ln phi/dx_j) available from system.init(3). This gives quadratic + * convergence, typically converging in 3-8 iterations from a good initial estimate. *

    * * @author asmund @@ -58,8 +60,8 @@ public class CricondenBarFlash extends PTphaseEnvelope { * @param cricondenBarX liquid phase mole fractions at the cricondenbar estimate * @param cricondenBarY vapor phase mole fractions at the cricondenbar estimate */ - public CricondenBarFlash(SystemInterface system, String name, double phaseFraction, double[] cricondenBar, - double[] cricondenBarX, double[] cricondenBarY) { + public CricondenBarFlash(SystemInterface system, String name, double phaseFraction, + double[] cricondenBar, double[] cricondenBarX, double[] cricondenBarY) { this.system = system; this.nc = system.getPhase(0).getNumberOfComponents(); this.cricondenBar = cricondenBar; @@ -84,9 +86,9 @@ public void run() { double[] lnK = new double[nc]; for (int i = 0; i < nc; i++) { if (cricondenBarX[i] > 1.0e-100 && cricondenBarY[i] > 1.0e-100) { - lnK[i] = Math.log(cricondenBarY[i] / cricondenBarX[i]); + lnK[i] = Math.log(cricondenBarY[i] / cricondenBarX[i]); } else { - lnK[i] = 0.0; + lnK[i] = 0.0; } } @@ -109,20 +111,21 @@ public void run() { // Check convergence on the norm of g double norm = 0.0; for (int i = 0; i < nc + 2; i++) { - norm += g[i] * g[i]; + norm += g[i] * g[i]; } norm = Math.sqrt(norm); if (norm < TOLERANCE) { - cricondenBar[0] = T; - cricondenBar[1] = P; - // Update output compositions - for (int i = 0; i < nc; i++) { - cricondenBarX[i] = system.getPhase(0).getComponent(i).getx(); - cricondenBarY[i] = system.getPhase(1).getComponent(i).getx(); - } - logger.debug("CricondenBarFlash converged in {} iterations: T={} K, P={} bar, norm={}", iter, T, P, norm); - return; + cricondenBar[0] = T; + cricondenBar[1] = P; + // Update output compositions + for (int i = 0; i < nc; i++) { + cricondenBarX[i] = system.getPhase(0).getComponent(i).getx(); + cricondenBarY[i] = system.getPhase(1).getComponent(i).getx(); + } + logger.debug("CricondenBarFlash converged in {} iterations: T={} K, P={} bar, norm={}", + iter, T, P, norm); + return; } // Build the (n+2)x(n+2) Jacobian @@ -131,42 +134,44 @@ public void run() { // Solve J * delta = -g using Gaussian elimination double[] delta = solveLinearSystem(jac, g); if (delta == null) { - logger.warn("CricondenBarFlash: singular Jacobian at iter {}. Keeping envelope estimate.", iter); - break; + logger.warn("CricondenBarFlash: singular Jacobian at iter {}. Keeping envelope estimate.", + iter); + break; } // Damp the step if too large double maxDelta = 0.0; for (int i = 0; i < nc + 2; i++) { - if (Math.abs(delta[i]) > maxDelta) { - maxDelta = Math.abs(delta[i]); - } + if (Math.abs(delta[i]) > maxDelta) { + maxDelta = Math.abs(delta[i]); + } } double damping = 1.0; if (maxDelta > MAX_STEP) { - damping = MAX_STEP / maxDelta; + damping = MAX_STEP / maxDelta; } // Apply updates for (int i = 0; i < nc; i++) { - lnK[i] += damping * delta[i]; + lnK[i] += damping * delta[i]; } lnT += damping * delta[nc]; lnP += damping * delta[nc + 1]; // Safety checks if (Math.exp(lnT) < 20.0 || Math.exp(lnT) > 2000.0) { - logger.warn("CricondenBarFlash: T out of range after update. Reverting."); - break; + logger.warn("CricondenBarFlash: T out of range after update. Reverting."); + break; } if (Math.exp(lnP) < 0.01 || Math.exp(lnP) > 5000.0) { - logger.warn("CricondenBarFlash: P out of range after update. Reverting."); - break; + logger.warn("CricondenBarFlash: P out of range after update. Reverting."); + break; } } // Did not converge - keep the best estimate from envelope tracing - logger.warn("CricondenBarFlash did not converge. Keeping envelope estimate T={} K, P={} bar", Tini, Pini); + logger.warn("CricondenBarFlash did not converge. Keeping envelope estimate T={} K, P={} bar", + Tini, Pini); cricondenBar[0] = Tini; cricondenBar[1] = Pini; } @@ -205,9 +210,9 @@ private void updateCompositions(double[] lnK, double T, double P) { * Build the (n+2) residual vector for the Michelsen cricondenbar formulation. * *

    - * g_i = ln K_i + ln phi_i^V - ln phi_i^L, i=0..n-1 (equilibrium) g_n = sum_i z_i*(K_i-1)/(1+beta*(K_i-1)) - * (Rachford-Rice) g_{n+1} = S_P = sum_i (dg_i/d(lnT)) * s_i (cricondenbar condition) where s_i = -dg_i/d(lnP) is the - * sensitivity coefficient + * g_i = ln K_i + ln phi_i^V - ln phi_i^L, i=0..n-1 (equilibrium) g_n = sum_i + * z_i*(K_i-1)/(1+beta*(K_i-1)) (Rachford-Rice) g_{n+1} = S_P = sum_i (dg_i/d(lnT)) * s_i + * (cricondenbar condition) where s_i = -dg_i/d(lnP) is the sensitivity coefficient *

    * * @param lnK array of ln(K_i) values @@ -267,7 +272,7 @@ private double[] buildResidual(double[] lnK, double T, double P) { snorm = Math.sqrt(snorm); if (snorm > 1.0e-30) { for (int i = 0; i < nc; i++) { - si[i] /= snorm; + si[i] /= snorm; } } @@ -345,34 +350,34 @@ private double[][] buildJacobian(double[] lnK, double T, double P) { // dg_i/dlnK_j for (int j = 0; j < nc; j++) { - double dlnPhiV_dyj = system.getPhase(1).getComponent(i).getdfugdx(j); - double dlnPhiL_dxj = system.getPhase(0).getComponent(i).getdfugdx(j); - - // dy_j/dlnK_j = y_j * (1 - y_j) for beta near 1 (dew point) - // dx_j/dlnK_j = -x_j * y_j for beta near 1 (from RR differentiation) - // General: dy_j/dlnK_j = K_j * z_j * (1-beta) / denom^2 = y_j * (1 - betaVal) * K_j / denom - double denomj = 1.0 - betaVal + betaVal * Ki[j]; - double dyjdlnKj = Ki[j] * zi[j] * (1.0 - betaVal) / (denomj * denomj); - double dxjdlnKj = -Ki[j] * zi[j] * betaVal * Ki[j] / (denomj * denomj); - // Wait, let me be more careful: - // y_j = z_j * K_j / (1 - beta + beta*K_j) - // dy_j/dK_j = z_j * (1 - beta) / (1 - beta + beta*K_j)^2 - // dy_j/dlnK_j = K_j * dy_j/dK_j = z_j * K_j * (1-beta) / denom^2 - // x_j = z_j / (1 - beta + beta*K_j) - // dx_j/dK_j = -z_j * beta / (1 - beta + beta*K_j)^2 - // dx_j/dlnK_j = K_j * dx_j/dK_j = -z_j * beta * K_j / denom^2 - - // Only j==j terms are non-zero for compositions (diagonal in K-space) - if (i == j) { - jac[i][j] = 1.0 + dlnPhiV_dyj * dyjdlnKj - dlnPhiL_dxj * dxjdlnKj; - } else { - // Cross-terms through composition dependence of fugacity coefficients - // dy_j/dlnK_i = 0 for j != i (each K only affects its own component directly) - // But: fugacity of component i depends on ALL mole fractions - // The cross-term is: - // dg_i/dlnK_j = dlnPhiV_i/dy_j * dy_j/dlnK_j - dlnPhiL_i/dx_j * dx_j/dlnK_j - jac[i][j] = dlnPhiV_dyj * dyjdlnKj - dlnPhiL_dxj * dxjdlnKj; - } + double dlnPhiV_dyj = system.getPhase(1).getComponent(i).getdfugdx(j); + double dlnPhiL_dxj = system.getPhase(0).getComponent(i).getdfugdx(j); + + // dy_j/dlnK_j = y_j * (1 - y_j) for beta near 1 (dew point) + // dx_j/dlnK_j = -x_j * y_j for beta near 1 (from RR differentiation) + // General: dy_j/dlnK_j = K_j * z_j * (1-beta) / denom^2 = y_j * (1 - betaVal) * K_j / denom + double denomj = 1.0 - betaVal + betaVal * Ki[j]; + double dyjdlnKj = Ki[j] * zi[j] * (1.0 - betaVal) / (denomj * denomj); + double dxjdlnKj = -Ki[j] * zi[j] * betaVal * Ki[j] / (denomj * denomj); + // Wait, let me be more careful: + // y_j = z_j * K_j / (1 - beta + beta*K_j) + // dy_j/dK_j = z_j * (1 - beta) / (1 - beta + beta*K_j)^2 + // dy_j/dlnK_j = K_j * dy_j/dK_j = z_j * K_j * (1-beta) / denom^2 + // x_j = z_j / (1 - beta + beta*K_j) + // dx_j/dK_j = -z_j * beta / (1 - beta + beta*K_j)^2 + // dx_j/dlnK_j = K_j * dx_j/dK_j = -z_j * beta * K_j / denom^2 + + // Only j==j terms are non-zero for compositions (diagonal in K-space) + if (i == j) { + jac[i][j] = 1.0 + dlnPhiV_dyj * dyjdlnKj - dlnPhiL_dxj * dxjdlnKj; + } else { + // Cross-terms through composition dependence of fugacity coefficients + // dy_j/dlnK_i = 0 for j != i (each K only affects its own component directly) + // But: fugacity of component i depends on ALL mole fractions + // The cross-term is: + // dg_i/dlnK_j = dlnPhiV_i/dy_j * dy_j/dlnK_j - dlnPhiL_i/dx_j * dx_j/dlnK_j + jac[i][j] = dlnPhiV_dyj * dyjdlnKj - dlnPhiL_dxj * dxjdlnKj; + } } } @@ -400,7 +405,7 @@ private double[][] buildJacobian(double[] lnK, double T, double P) { snorm = Math.sqrt(snorm); if (snorm > 1.0e-30) { for (int i = 0; i < nc; i++) { - si[i] /= snorm; + si[i] /= snorm; } } @@ -482,8 +487,8 @@ private double[][] buildJacobian(double[] lnK, double T, double P) { } /** - * Compute S_P = sum_i s_i * T * (dlnPhiV_i/dT - dlnPhiL_i/dT) where s_i = (y_i - x_i) normalized. This is the - * cricondenbar specification function. + * Compute S_P = sum_i s_i * T * (dlnPhiV_i/dT - dlnPhiL_i/dT) where s_i = (y_i - x_i) normalized. + * This is the cricondenbar specification function. * * @param T temperature in K * @return the value of S_P @@ -498,7 +503,7 @@ private double computeSP(double T) { snorm = Math.sqrt(snorm); if (snorm > 1.0e-30) { for (int i = 0; i < nc; i++) { - si[i] /= snorm; + si[i] /= snorm; } } @@ -520,49 +525,13 @@ private double computeSP(double T) { */ private double[] solveLinearSystem(double[][] jac, double[] g) { int n = g.length; - double[][] a = new double[n][n + 1]; + double[] rhs = new double[n]; for (int i = 0; i < n; i++) { - System.arraycopy(jac[i], 0, a[i], 0, n); - a[i][n] = -g[i]; + rhs[i] = -g[i]; } - - // Forward elimination with partial pivoting - for (int col = 0; col < n; col++) { - // Find pivot - int maxRow = col; - double maxVal = Math.abs(a[col][col]); - for (int row = col + 1; row < n; row++) { - if (Math.abs(a[row][col]) > maxVal) { - maxVal = Math.abs(a[row][col]); - maxRow = row; - } - } - if (maxVal < 1.0e-30) { - return null; // singular - } - // Swap rows - if (maxRow != col) { - double[] tmp = a[col]; - a[col] = a[maxRow]; - a[maxRow] = tmp; - } - // Eliminate - for (int row = col + 1; row < n; row++) { - double factor = a[row][col] / a[col][col]; - for (int k = col; k <= n; k++) { - a[row][k] -= factor * a[col][k]; - } - } - } - - // Back substitution double[] delta = new double[n]; - for (int i = n - 1; i >= 0; i--) { - double sum = a[i][n]; - for (int j = i + 1; j < n; j++) { - sum -= a[i][j] * delta[j]; - } - delta[i] = sum / a[i][i]; + if (!LinearAlgebraOps.solveLinearSystem(jac, rhs, delta)) { + return null; } return delta; } diff --git a/src/main/java/neqsim/thermodynamicoperations/phaseenvelopeops/multicomponentenvelopeops/CricondenThermFlash.java b/src/main/java/neqsim/thermodynamicoperations/phaseenvelopeops/multicomponentenvelopeops/CricondenThermFlash.java index 8e0df17dcc..fc8f3362b3 100644 --- a/src/main/java/neqsim/thermodynamicoperations/phaseenvelopeops/multicomponentenvelopeops/CricondenThermFlash.java +++ b/src/main/java/neqsim/thermodynamicoperations/phaseenvelopeops/multicomponentenvelopeops/CricondenThermFlash.java @@ -3,13 +3,15 @@ import org.apache.logging.log4j.LogManager; import org.apache.logging.log4j.Logger; import neqsim.thermo.system.SystemInterface; +import neqsim.util.math.LinearAlgebraOps; /** * Direct cricondentherm calculation using the Michelsen simultaneous Newton method. * *

    - * Finds the maximum temperature point on the phase envelope by solving an (n+2)-dimensional Newton system - * simultaneously, following the formulation of Michelsen (1980) and Michelsen & Mollerup (2007), Chapter 12. + * Finds the maximum temperature point on the phase envelope by solving an (n+2)-dimensional Newton + * system simultaneously, following the formulation of Michelsen (1980) and Michelsen & Mollerup + * (2007), Chapter 12. *

    * *

    @@ -18,14 +20,14 @@ *

      *
    • g_i = ln K_i + ln phi_i^V(y,T,P) - ln phi_i^L(x,T,P) = 0, i=1..n (equilibrium)
    • *
    • g_{n+1} = sum_i z_i*(K_i - 1)/(1 + beta*(K_i - 1)) = 0 (Rachford-Rice summation)
    • - *
    • g_{n+2} = S_T = 0 (cricondentherm condition: dP/dT = 0 along envelope, which requires the sensitivity S_T of the - * envelope equations w.r.t. ln T specification to vanish)
    • + *
    • g_{n+2} = S_T = 0 (cricondentherm condition: dP/dT = 0 along envelope, which requires the + * sensitivity S_T of the envelope equations w.r.t. ln T specification to vanish)
    • *
    * *

    - * The full (n+2)x(n+2) Jacobian is built analytically using fugacity coefficient derivatives (d ln phi/dT, d ln phi/dP, - * d ln phi/dx_j) available from system.init(3). This gives quadratic convergence, typically converging in 3-8 - * iterations from a good initial estimate. + * The full (n+2)x(n+2) Jacobian is built analytically using fugacity coefficient derivatives (d ln + * phi/dT, d ln phi/dP, d ln phi/dx_j) available from system.init(3). This gives quadratic + * convergence, typically converging in 3-8 iterations from a good initial estimate. *

    * * @author asmund @@ -57,8 +59,8 @@ public class CricondenThermFlash extends PTphaseEnvelope { * @param cricondenThermX liquid phase mole fractions at the cricondentherm estimate * @param cricondenThermY vapor phase mole fractions at the cricondentherm estimate */ - public CricondenThermFlash(SystemInterface system, String name, double phaseFraction, double[] cricondenTherm, - double[] cricondenThermX, double[] cricondenThermY) { + public CricondenThermFlash(SystemInterface system, String name, double phaseFraction, + double[] cricondenTherm, double[] cricondenThermX, double[] cricondenThermY) { this.system = system; this.nc = system.getPhase(0).getNumberOfComponents(); this.cricondenTherm = cricondenTherm; @@ -80,9 +82,9 @@ public void run() { double[] lnK = new double[nc]; for (int i = 0; i < nc; i++) { if (cricondenThermX[i] > 1.0e-100 && cricondenThermY[i] > 1.0e-100) { - lnK[i] = Math.log(cricondenThermY[i] / cricondenThermX[i]); + lnK[i] = Math.log(cricondenThermY[i] / cricondenThermX[i]); } else { - lnK[i] = 0.0; + lnK[i] = 0.0; } } @@ -105,20 +107,21 @@ public void run() { // Check convergence on the norm of g double norm = 0.0; for (int i = 0; i < nc + 2; i++) { - norm += g[i] * g[i]; + norm += g[i] * g[i]; } norm = Math.sqrt(norm); if (norm < TOLERANCE) { - cricondenTherm[0] = T; - cricondenTherm[1] = P; - // Update output compositions - for (int i = 0; i < nc; i++) { - cricondenThermX[i] = system.getPhase(0).getComponent(i).getx(); - cricondenThermY[i] = system.getPhase(1).getComponent(i).getx(); - } - logger.debug("CricondenThermFlash converged in {} iterations: T={} K, P={} bar, norm={}", iter, T, P, norm); - return; + cricondenTherm[0] = T; + cricondenTherm[1] = P; + // Update output compositions + for (int i = 0; i < nc; i++) { + cricondenThermX[i] = system.getPhase(0).getComponent(i).getx(); + cricondenThermY[i] = system.getPhase(1).getComponent(i).getx(); + } + logger.debug("CricondenThermFlash converged in {} iterations: T={} K, P={} bar, norm={}", + iter, T, P, norm); + return; } // Build the (n+2)x(n+2) Jacobian @@ -127,42 +130,44 @@ public void run() { // Solve J * delta = -g using Gaussian elimination double[] delta = solveLinearSystem(jac, g); if (delta == null) { - logger.warn("CricondenThermFlash: singular Jacobian at iter {}. Keeping envelope estimate.", iter); - break; + logger.warn("CricondenThermFlash: singular Jacobian at iter {}. Keeping envelope estimate.", + iter); + break; } // Damp the step if too large double maxDelta = 0.0; for (int i = 0; i < nc + 2; i++) { - if (Math.abs(delta[i]) > maxDelta) { - maxDelta = Math.abs(delta[i]); - } + if (Math.abs(delta[i]) > maxDelta) { + maxDelta = Math.abs(delta[i]); + } } double damping = 1.0; if (maxDelta > MAX_STEP) { - damping = MAX_STEP / maxDelta; + damping = MAX_STEP / maxDelta; } // Apply updates for (int i = 0; i < nc; i++) { - lnK[i] += damping * delta[i]; + lnK[i] += damping * delta[i]; } lnT += damping * delta[nc]; lnP += damping * delta[nc + 1]; // Safety checks if (Math.exp(lnT) < 20.0 || Math.exp(lnT) > 2000.0) { - logger.warn("CricondenThermFlash: T out of range after update. Reverting."); - break; + logger.warn("CricondenThermFlash: T out of range after update. Reverting."); + break; } if (Math.exp(lnP) < 0.01 || Math.exp(lnP) > 5000.0) { - logger.warn("CricondenThermFlash: P out of range after update. Reverting."); - break; + logger.warn("CricondenThermFlash: P out of range after update. Reverting."); + break; } } // Did not converge - keep the best estimate from envelope tracing - logger.warn("CricondenThermFlash did not converge. Keeping envelope estimate T={} K, P={} bar", Tini, Pini); + logger.warn("CricondenThermFlash did not converge. Keeping envelope estimate T={} K, P={} bar", + Tini, Pini); cricondenTherm[0] = Tini; cricondenTherm[1] = Pini; } @@ -201,9 +206,9 @@ private void updateCompositions(double[] lnK, double T, double P) { * Build the (n+2) residual vector for the Michelsen cricondentherm formulation. * *

    - * g_i = ln K_i + ln phi_i^V - ln phi_i^L, i=0..n-1 (equilibrium) g_n = sum_i z_i*(K_i-1)/(1+beta*(K_i-1)) - * (Rachford-Rice) g_{n+1} = S_T = sum_i s_i * dg_i/d(lnP) (cricondentherm condition: dP/dT = 0) where s_i = (y_i - - * x_i) normalized + * g_i = ln K_i + ln phi_i^V - ln phi_i^L, i=0..n-1 (equilibrium) g_n = sum_i + * z_i*(K_i-1)/(1+beta*(K_i-1)) (Rachford-Rice) g_{n+1} = S_T = sum_i s_i * dg_i/d(lnP) + * (cricondentherm condition: dP/dT = 0) where s_i = (y_i - x_i) normalized *

    * * @param lnK array of ln(K_i) values @@ -240,7 +245,7 @@ private double[] buildResidual(double[] lnK, double T, double P) { snorm = Math.sqrt(snorm); if (snorm > 1.0e-30) { for (int i = 0; i < nc; i++) { - si[i] /= snorm; + si[i] /= snorm; } } @@ -290,18 +295,18 @@ private double[][] buildJacobian(double[] lnK, double T, double P) { jac[i][nc + 1] = P * (dlnPhiV_dP - dlnPhiL_dP); for (int j = 0; j < nc; j++) { - double dlnPhiV_dyj = system.getPhase(1).getComponent(i).getdfugdx(j); - double dlnPhiL_dxj = system.getPhase(0).getComponent(i).getdfugdx(j); - - double denomj = 1.0 - betaVal + betaVal * Ki[j]; - double dyjdlnKj = Ki[j] * zi[j] * (1.0 - betaVal) / (denomj * denomj); - double dxjdlnKj = -zi[j] * betaVal * Ki[j] / (denomj * denomj); - - if (i == j) { - jac[i][j] = 1.0 + dlnPhiV_dyj * dyjdlnKj - dlnPhiL_dxj * dxjdlnKj; - } else { - jac[i][j] = dlnPhiV_dyj * dyjdlnKj - dlnPhiL_dxj * dxjdlnKj; - } + double dlnPhiV_dyj = system.getPhase(1).getComponent(i).getdfugdx(j); + double dlnPhiL_dxj = system.getPhase(0).getComponent(i).getdfugdx(j); + + double denomj = 1.0 - betaVal + betaVal * Ki[j]; + double dyjdlnKj = Ki[j] * zi[j] * (1.0 - betaVal) / (denomj * denomj); + double dxjdlnKj = -zi[j] * betaVal * Ki[j] / (denomj * denomj); + + if (i == j) { + jac[i][j] = 1.0 + dlnPhiV_dyj * dyjdlnKj - dlnPhiL_dxj * dxjdlnKj; + } else { + jac[i][j] = dlnPhiV_dyj * dyjdlnKj - dlnPhiL_dxj * dxjdlnKj; + } } } @@ -373,8 +378,8 @@ private double[][] buildJacobian(double[] lnK, double T, double P) { } /** - * Compute S_T = sum_i s_i * P * (dlnPhiV_i/dP - dlnPhiL_i/dP) where s_i = (y_i - x_i) normalized. This is the - * cricondentherm specification function. + * Compute S_T = sum_i s_i * P * (dlnPhiV_i/dP - dlnPhiL_i/dP) where s_i = (y_i - x_i) normalized. + * This is the cricondentherm specification function. * * @param P pressure in bar * @return the value of S_T @@ -389,7 +394,7 @@ private double computeST(double P) { snorm = Math.sqrt(snorm); if (snorm > 1.0e-30) { for (int i = 0; i < nc; i++) { - si[i] /= snorm; + si[i] /= snorm; } } @@ -411,46 +416,13 @@ private double computeST(double P) { */ private double[] solveLinearSystem(double[][] jac, double[] g) { int n = g.length; - double[][] a = new double[n][n + 1]; + double[] rhs = new double[n]; for (int i = 0; i < n; i++) { - System.arraycopy(jac[i], 0, a[i], 0, n); - a[i][n] = -g[i]; + rhs[i] = -g[i]; } - - // Forward elimination with partial pivoting - for (int col = 0; col < n; col++) { - int maxRow = col; - double maxVal = Math.abs(a[col][col]); - for (int row = col + 1; row < n; row++) { - if (Math.abs(a[row][col]) > maxVal) { - maxVal = Math.abs(a[row][col]); - maxRow = row; - } - } - if (maxVal < 1.0e-30) { - return null; - } - if (maxRow != col) { - double[] tmp = a[col]; - a[col] = a[maxRow]; - a[maxRow] = tmp; - } - for (int row = col + 1; row < n; row++) { - double factor = a[row][col] / a[col][col]; - for (int k = col; k <= n; k++) { - a[row][k] -= factor * a[col][k]; - } - } - } - - // Back substitution double[] delta = new double[n]; - for (int i = n - 1; i >= 0; i--) { - double sum = a[i][n]; - for (int j = i + 1; j < n; j++) { - sum -= a[i][j] * delta[j]; - } - delta[i] = sum / a[i][i]; + if (!LinearAlgebraOps.solveLinearSystem(jac, rhs, delta)) { + return null; } return delta; } diff --git a/src/main/java/neqsim/util/math/LinearAlgebraOps.java b/src/main/java/neqsim/util/math/LinearAlgebraOps.java new file mode 100644 index 0000000000..4f3bec70ad --- /dev/null +++ b/src/main/java/neqsim/util/math/LinearAlgebraOps.java @@ -0,0 +1,660 @@ +package neqsim.util.math; + +import java.util.Map; +import Jama.Matrix; +import org.apache.commons.math3.linear.Array2DRowRealMatrix; +import org.apache.commons.math3.linear.RealMatrix; +import org.apache.commons.math3.linear.SingularValueDecomposition; +import org.ojalgo.matrix.decomposition.LU; +import org.ojalgo.matrix.decomposition.SingularValue; +import org.ojalgo.matrix.store.MatrixStore; +import org.ojalgo.matrix.store.Primitive64Store; + +/** + * Shared linear algebra utility methods used across NeqSim solvers. + * + *

    + * This class centralizes dense linear algebra operations that are reused by multiple thermodynamic + * and process modules, so domain classes can focus on model-specific logic. + *

    + * + * @author Even Solbraa + * @version 1.0 + */ +public final class LinearAlgebraOps { + /** Lightweight dense matrix container for small internal linear-algebra kernels. */ + public static final class DenseMatrix { + public final int numRows; + public final int numCols; + private final double[] values; + + /** + * Creates a dense matrix with zero-initialized values. + * + * @param rows number of rows + * @param cols number of columns + */ + public DenseMatrix(int rows, int cols) { + this.numRows = rows; + this.numCols = cols; + this.values = new double[rows * cols]; + } + + /** + * Creates a dense matrix by copying a 2D array. + * + * @param matrix source matrix + */ + public DenseMatrix(double[][] matrix) { + this(matrix.length, matrix.length == 0 ? 0 : matrix[0].length); + for (int i = 0; i < numRows; i++) { + for (int j = 0; j < numCols; j++) { + unsafe_set(i, j, matrix[i][j]); + } + } + } + + /** + * Reads a matrix value without bounds checking. + * + * @param row row index + * @param col column index + * @return value at {@code (row, col)} + */ + public double unsafe_get(int row, int col) { + return values[row * numCols + col]; + } + + /** + * Writes a matrix value without bounds checking. + * + * @param row row index + * @param col column index + * @param value value to write + */ + public void unsafe_set(int row, int col, double value) { + values[row * numCols + col] = value; + } + + /** + * Gets the packed row-major matrix data. + * + * @return backing data array + */ + public double[] getData() { + return values; + } + } + + /** Functional getter for matrix elements. */ + public interface MatrixElementGetter { + /** + * Reads one element from a matrix-like structure. + * + * @param row row index + * @param col column index + * @return matrix value at {@code (row, col)} + */ + double get(int row, int col); + } + + /** Functional setter for matrix elements. */ + public interface MatrixElementSetter { + /** + * Writes one element to a matrix-like structure. + * + * @param row row index + * @param col column index + * @param value value to write + */ + void set(int row, int col, double value); + } + + /** Utility class: no instances. */ + private LinearAlgebraOps() {} + + /** + * Decomposes a square matrix into a provided LU factorization using callback-based element + * access. + * + * @param solver LU solver instance to populate + * @param size matrix dimension + * @param matrixGetter callback used to read matrix values + * @return {@code true} if decomposition succeeded, {@code false} otherwise + */ + public static boolean decomposeLu(LU solver, int size, MatrixElementGetter matrixGetter) { + Primitive64Store store = Primitive64Store.FACTORY.make(size, size); + for (int i = 0; i < size; i++) { + for (int j = 0; j < size; j++) { + store.set(i, j, matrixGetter.get(i, j)); + } + } + return solver.decompose(store); + } + + /** + * Solves {@code A X = B} from a previously decomposed LU solver using callback-based matrix + * access. + * + * @param solver LU solver with prior successful decomposition + * @param rows number of rows in {@code B} and {@code X} + * @param cols number of columns in {@code B} and {@code X} + * @param rhsGetter callback used to read right-hand-side matrix values + * @param outSetter callback used to write solution matrix values + */ + public static void solveLu(LU solver, int rows, int cols, MatrixElementGetter rhsGetter, + MatrixElementSetter outSetter) { + Primitive64Store rhsStore = Primitive64Store.FACTORY.make(rows, cols); + for (int i = 0; i < rows; i++) { + for (int j = 0; j < cols; j++) { + rhsStore.set(i, j, rhsGetter.get(i, j)); + } + } + MatrixStore sol = solver.getSolution(rhsStore); + for (int i = 0; i < rows; i++) { + for (int j = 0; j < cols; j++) { + outSetter.set(i, j, sol.get(i, j)); + } + } + } + + /** + * Builds a dense {@link Matrix} from sparse row/column map storage. + * + * @param rows number of matrix rows + * @param columns number of matrix columns + * @param sparseValues sparse matrix values keyed by row and then column + * @return dense matrix representation + */ + public static Matrix toDenseMatrix(int rows, int columns, + Map> sparseValues) { + double[][] dense = new double[rows][columns]; + for (Map.Entry> rowEntry : sparseValues.entrySet()) { + int row = rowEntry.getKey().intValue(); + for (Map.Entry columnEntry : rowEntry.getValue().entrySet()) { + dense[row][columnEntry.getKey().intValue()] = columnEntry.getValue().doubleValue(); + } + } + return new Matrix(dense); + } + + /** + * Dense matrix multiplication: {@code out = a * b}. + * + * @param a left matrix + * @param b right matrix + * @param out output matrix + */ + public static void mult(DenseMatrix a, DenseMatrix b, DenseMatrix out) { + for (int i = 0; i < a.numRows; i++) { + for (int j = 0; j < b.numCols; j++) { + double sum = 0.0; + for (int k = 0; k < a.numCols; k++) { + sum += a.unsafe_get(i, k) * b.unsafe_get(k, j); + } + out.unsafe_set(i, j, sum); + } + } + } + + /** + * Dense matrix subtraction: {@code out = a - b}. + * + * @param a left matrix + * @param b right matrix + * @param out output matrix + */ + public static void subtract(DenseMatrix a, DenseMatrix b, DenseMatrix out) { + for (int i = 0; i < a.numRows; i++) { + for (int j = 0; j < a.numCols; j++) { + out.unsafe_set(i, j, a.unsafe_get(i, j) - b.unsafe_get(i, j)); + } + } + } + + /** + * Frobenius norm of a dense matrix. + * + * @param matrix input matrix + * @return {@code ||matrix||_F} + */ + public static double normF(DenseMatrix matrix) { + return vectorNorm(matrix.getData()); + } + + /** + * Solves {@code A x = b} using LU decomposition. + * + * @param matrix coefficient matrix {@code A} with shape {@code n x n} + * @param rhs right-hand side vector {@code b} with length {@code n} + * @param solution output vector {@code x} with length {@code n} + * @return {@code true} if LU decomposition succeeded and {@code solution} was written, + * {@code false} otherwise + */ + public static boolean solveLinearSystem(double[][] matrix, double[] rhs, double[] solution) { + int n = rhs.length; + Primitive64Store aStore = Primitive64Store.FACTORY.make(n, n); + Primitive64Store bStore = Primitive64Store.FACTORY.make(n, 1); + + for (int i = 0; i < n; i++) { + bStore.set(i, 0, rhs[i]); + for (int j = 0; j < n; j++) { + aStore.set(i, j, matrix[i][j]); + } + } + + LU lu = LU.PRIMITIVE.make(n, n); + if (!lu.decompose(aStore)) { + return false; + } + + MatrixStore sol = lu.getSolution(bStore); + for (int i = 0; i < n; i++) { + solution[i] = sol.get(i, 0); + } + return true; + } + + /** + * Solves {@code A x = b} in place using Gaussian elimination with partial pivoting. + * + *

    + * The coefficient matrix {@code matrix} and right-hand side {@code rhs} are modified in place, + * and {@code rhs} is overwritten with the solution vector on return. + *

    + * + * @param matrix coefficient matrix (modified in place) + * @param rhs right-hand side vector (overwritten with solution) + * @param n system dimension + */ + public static void solveLinearSystemInPlace(double[][] matrix, double[] rhs, int n) { + for (int col = 0; col < n; col++) { + int maxRow = col; + double maxVal = Math.abs(matrix[col][col]); + for (int row = col + 1; row < n; row++) { + double val = Math.abs(matrix[row][col]); + if (val > maxVal) { + maxVal = val; + maxRow = row; + } + } + if (maxRow != col) { + double[] tmpRow = matrix[col]; + matrix[col] = matrix[maxRow]; + matrix[maxRow] = tmpRow; + double tmpVal = rhs[col]; + rhs[col] = rhs[maxRow]; + rhs[maxRow] = tmpVal; + } + double pivot = matrix[col][col]; + if (Math.abs(pivot) < 1.0e-30) { + continue; + } + for (int row = col + 1; row < n; row++) { + double factor = matrix[row][col] / pivot; + for (int k = col + 1; k < n; k++) { + matrix[row][k] -= factor * matrix[col][k]; + } + rhs[row] -= factor * rhs[col]; + } + } + for (int row = n - 1; row >= 0; row--) { + double s = rhs[row]; + for (int k = row + 1; k < n; k++) { + s -= matrix[row][k] * rhs[k]; + } + rhs[row] = s / matrix[row][row]; + } + } + + /** + * Solves {@code A X = B} for matrix right-hand sides using LU decomposition. + * + * @param matrix coefficient matrix {@code A} + * @param rhs right-hand side matrix {@code B} + * @return solution matrix {@code X} + * @throws IllegalStateException if LU decomposition fails + */ + public static MatrixStore solveLinearSystem(MatrixStore matrix, + MatrixStore rhs) { + int rows = (int) matrix.countRows(); + int cols = (int) matrix.countColumns(); + LU lu = LU.PRIMITIVE.make(rows, cols); + if (!lu.decompose(matrix)) { + throw new IllegalStateException("LU decomposition failed in linear solve"); + } + return lu.getSolution(rhs); + } + + /** + * Solves {@code A x = b} from pre-allocated ojAlgo stores. + * + * @param matrix coefficient matrix store {@code A} + * @param rhs right-hand side store {@code b} with shape {@code n x 1} + * @param solution output vector {@code x} with length {@code n} + * @param solver pre-allocated LU solver instance + * @return {@code true} if decomposition succeeded and {@code solution} was written, {@code false} + * otherwise + */ + public static boolean solveLinearSystem(Primitive64Store matrix, Primitive64Store rhs, + double[] solution, LU solver) { + if (!solver.decompose(matrix)) { + return false; + } + MatrixStore sol = solver.getSolution(rhs); + for (int i = 0; i < solution.length; i++) { + solution[i] = sol.get(i, 0); + } + return true; + } + + /** + * Copies a dense square matrix into a reusable ojAlgo work store. + * + * @param source source dense matrix + * @param target target dense matrix + * @param size matrix dimension + */ + public static void copyDenseStore(Primitive64Store source, Primitive64Store target, int size) { + for (int i = 0; i < size; i++) { + for (int j = 0; j < size; j++) { + target.set(i, j, source.get(i, j)); + } + } + } + + /** + * Enforces strict symmetry in a square dense matrix by averaging mirrored off-diagonal entries. + * + * @param matrix square dense matrix to symmetrize in place + */ + public static void symmetriseMmatrix(Primitive64Store matrix) { + int size = (int) matrix.countRows(); + for (int i = 0; i < size; i++) { + for (int j = i + 1; j < size; j++) { + double average = 0.5 * (matrix.doubleValue(i, j) + matrix.doubleValue(j, i)); + matrix.set(i, j, average); + matrix.set(j, i, average); + } + } + } + + /** + * Computes determinant of a square matrix from LU decomposition. + * + * @param matrix square matrix + * @return determinant value, or {@code 0.0} if decomposition fails + */ + public static double determinant(double[][] matrix) { + int n = matrix.length; + Primitive64Store store = Primitive64Store.FACTORY.make(n, n); + for (int i = 0; i < n; i++) { + for (int j = 0; j < n; j++) { + store.set(i, j, matrix[i][j]); + } + } + LU lu = LU.PRIMITIVE.make(n, n); + if (!lu.decompose(store)) { + return 0.0; + } + return lu.getDeterminant(); + } + + /** + * Computes inverse of a square matrix using LU decomposition. + * + * @param matrix square matrix + * @return inverse matrix + * @throws IllegalStateException if decomposition fails + */ + public static double[][] inverse(double[][] matrix) { + int n = matrix.length; + Primitive64Store store = Primitive64Store.FACTORY.make(n, n); + for (int i = 0; i < n; i++) { + for (int j = 0; j < n; j++) { + store.set(i, j, matrix[i][j]); + } + } + + LU lu = LU.PRIMITIVE.make(n, n); + if (!lu.decompose(store)) { + throw new IllegalStateException("Matrix inversion failed: LU decomposition failed"); + } + + MatrixStore inv = lu.getInverse(); + double[][] out = new double[n][n]; + for (int i = 0; i < n; i++) { + for (int j = 0; j < n; j++) { + out[i][j] = inv.get(i, j); + } + } + return out; + } + + /** + * Computes Moore-Penrose pseudo-inverse of a matrix using SVD. + * + * @param matrix input matrix + * @return pseudo-inverse matrix + * @throws IllegalStateException if SVD decomposition fails + */ + public static double[][] pseudoInverse(double[][] matrix) { + int nRows = matrix.length; + int nCols = matrix[0].length; + Primitive64Store store = Primitive64Store.FACTORY.make(nRows, nCols); + for (int i = 0; i < nRows; i++) { + for (int j = 0; j < nCols; j++) { + store.set(i, j, matrix[i][j]); + } + } + + SingularValue svd = SingularValue.PRIMITIVE.make(nRows, nCols); + if (!svd.decompose(store)) { + throw new IllegalStateException("Pseudo-inverse failed: SVD decomposition failed"); + } + + MatrixStore inv = svd.getInverse(); + double[][] out = new double[(int) inv.countRows()][(int) inv.countColumns()]; + for (int i = 0; i < out.length; i++) { + for (int j = 0; j < out[i].length; j++) { + out[i][j] = inv.get(i, j); + } + } + return out; + } + + /** + * Estimates the 2-norm condition number from SVD singular values. + * + * @param matrix input matrix + * @return estimated condition number, or {@code Double.POSITIVE_INFINITY} if decomposition fails + * or the minimum singular value is effectively zero + */ + public static double conditionP2(double[][] matrix) { + int nRows = matrix.length; + int nCols = matrix[0].length; + Primitive64Store store = Primitive64Store.FACTORY.make(nRows, nCols); + for (int i = 0; i < nRows; i++) { + for (int j = 0; j < nCols; j++) { + store.set(i, j, matrix[i][j]); + } + } + + SingularValue svd = SingularValue.PRIMITIVE.make(nRows, nCols); + if (!svd.decompose(store)) { + return Double.POSITIVE_INFINITY; + } + double maxSingular = svd.getOperatorNorm(); + double minSingular = svd.getFrobeniusNorm() / Math.max(1.0, svd.getRank()); + if (Math.abs(minSingular) < 1.0e-30) { + return Double.POSITIVE_INFINITY; + } + return maxSingular / minSingular; + } + + /** + * Multiplies two primitive matrices. + * + * @param left left matrix + * @param right right matrix + * @return product matrix + */ + public static double[][] multiply(double[][] left, double[][] right) { + Primitive64Store leftStore = Primitive64Store.FACTORY.rows(left); + Primitive64Store rightStore = Primitive64Store.FACTORY.rows(right); + MatrixStore product = leftStore.multiply(rightStore); + int nRows = (int) product.countRows(); + int nCols = (int) product.countColumns(); + double[][] out = new double[nRows][nCols]; + for (int i = 0; i < nRows; i++) { + for (int j = 0; j < nCols; j++) { + out[i][j] = product.get(i, j); + } + } + return out; + } + + /** + * Multiplies a primitive matrix and vector and scales the result. + * + * @param matrix coefficient matrix + * @param vector right-hand side vector + * @param scale scaling factor applied to the product + * @return scaled product vector + */ + public static double[] multiply(double[][] matrix, double[] vector, double scale) { + Primitive64Store matrixStore = Primitive64Store.FACTORY.rows(matrix); + Primitive64Store vectorStore = Primitive64Store.FACTORY.make(vector.length, 1); + for (int i = 0; i < vector.length; i++) { + vectorStore.set(i, 0, vector[i]); + } + MatrixStore product = matrixStore.multiply(vectorStore); + double[] out = new double[(int) product.countRows()]; + for (int i = 0; i < out.length; i++) { + out[i] = product.get(i, 0) * scale; + } + return out; + } + + /** + * Calculates Euclidean norm of a vector. + * + * @param vector input vector + * @return {@code ||vector||_2} + */ + public static double vectorNorm(double[] vector) { + double sum = 0.0; + for (int i = 0; i < vector.length; i++) { + sum += vector[i] * vector[i]; + } + return Math.sqrt(sum); + } + + /** + * Calculates Euclidean norm of a column vector represented as {@code [n][1]}. + * + * @param columnVector input column vector + * @return {@code ||columnVector||_2} + */ + public static double columnNorm(double[][] columnVector) { + double sum = 0.0; + for (int i = 0; i < columnVector.length; i++) { + sum += columnVector[i][0] * columnVector[i][0]; + } + return Math.sqrt(sum); + } + + /** + * Solves {@code A x = b} using SVD pseudo-inverse as fallback for singular systems. + * + * @param matrix coefficient matrix {@code A} + * @param rhs right-hand side vector {@code b} + * @param solution output vector {@code x} + * @return {@code true} if SVD decomposition succeeded and {@code solution} was written, + * {@code false} otherwise + */ + public static boolean pseudoInverseSolve(double[][] matrix, double[] rhs, double[] solution) { + int nRows = matrix.length; + int nCols = matrix[0].length; + Primitive64Store jacStore = Primitive64Store.FACTORY.make(nRows, nCols); + Primitive64Store rhsStore = Primitive64Store.FACTORY.make(nRows, 1); + + for (int i = 0; i < nRows; i++) { + rhsStore.set(i, 0, rhs[i]); + for (int j = 0; j < nCols; j++) { + jacStore.set(i, j, matrix[i][j]); + } + } + + SingularValue svd = SingularValue.PRIMITIVE.make(nRows, nCols); + if (!svd.decompose(jacStore)) { + return false; + } + + MatrixStore result = svd.getInverse().multiply(rhsStore); + for (int i = 0; i < solution.length; i++) { + solution[i] = result.get(i, 0); + } + return true; + } + + /** + * Solves a least-squares system {@code A X ≈ B} using the SVD pseudo-inverse. + * + *

    + * This method supports one or more right-hand sides in {@code B} and is robust for rank-deficient + * or ill-conditioned systems. + *

    + * + * @param matrix coefficient matrix {@code A} with dimensions {@code m x n} + * @param rhs right-hand side matrix {@code B} with dimensions {@code m x k} + * @return solution matrix {@code X} with dimensions {@code n x k} + * @throws IllegalStateException if SVD decomposition fails + */ + public static double[][] solveLeastSquares(double[][] matrix, double[][] rhs) { + int nRows = matrix.length; + int nCols = matrix[0].length; + int rhsCols = rhs[0].length; + + Primitive64Store aStore = Primitive64Store.FACTORY.make(nRows, nCols); + Primitive64Store bStore = Primitive64Store.FACTORY.make(nRows, rhsCols); + + for (int i = 0; i < nRows; i++) { + for (int j = 0; j < nCols; j++) { + aStore.set(i, j, matrix[i][j]); + } + for (int j = 0; j < rhsCols; j++) { + bStore.set(i, j, rhs[i][j]); + } + } + + SingularValue svd = SingularValue.PRIMITIVE.make(nRows, nCols); + if (!svd.decompose(aStore)) { + throw new IllegalStateException("Least-squares failed: SVD decomposition failed"); + } + + MatrixStore result = svd.getInverse().multiply(bStore); + double[][] out = new double[(int) result.countRows()][(int) result.countColumns()]; + for (int i = 0; i < out.length; i++) { + for (int j = 0; j < out[i].length; j++) { + out[i][j] = result.get(i, j); + } + } + return out; + } + + /** + * Computes an approximate null-space vector from the right singular vector associated with the + * smallest singular value. + * + * @param matrix input Jacobian or coefficient matrix + * @return right singular vector spanning the approximate null-space + */ + public static double[] calcNullVector(double[][] matrix) { + RealMatrix jacobian = new Array2DRowRealMatrix(matrix, false); + SingularValueDecomposition svd = new SingularValueDecomposition(jacobian); + RealMatrix v = svd.getV(); + int lastCol = v.getColumnDimension() - 1; + return v.getColumn(lastCol); + } +} diff --git a/src/test/java/neqsim/process/equipment/network/NetworkLinearSolverTest.java b/src/test/java/neqsim/process/equipment/network/NetworkLinearSolverTest.java index 72ee0b3d86..4622b0159c 100644 --- a/src/test/java/neqsim/process/equipment/network/NetworkLinearSolverTest.java +++ b/src/test/java/neqsim/process/equipment/network/NetworkLinearSolverTest.java @@ -1,6 +1,7 @@ package neqsim.process.equipment.network; import static org.junit.jupiter.api.Assertions.*; +import org.junit.jupiter.api.Tag; import org.junit.jupiter.api.Test; /** @@ -10,6 +11,7 @@ * Verifies sparse, dense, and Gaussian methods produce consistent results. *

    */ +@Tag("LinearAlgebra") class NetworkLinearSolverTest { @Test diff --git a/src/test/java/neqsim/process/equipment/network/PipelineNetworkDocExamplesTest.java b/src/test/java/neqsim/process/equipment/network/PipelineNetworkDocExamplesTest.java index a5b293083c..6184b9e9f3 100644 --- a/src/test/java/neqsim/process/equipment/network/PipelineNetworkDocExamplesTest.java +++ b/src/test/java/neqsim/process/equipment/network/PipelineNetworkDocExamplesTest.java @@ -3,6 +3,7 @@ import static org.junit.jupiter.api.Assertions.*; import java.util.List; import org.junit.jupiter.api.BeforeAll; +import org.junit.jupiter.api.Tag; import org.junit.jupiter.api.Test; import neqsim.thermo.system.SystemInterface; import neqsim.thermo.system.SystemSrkEos; @@ -10,6 +11,7 @@ /** * Verifies that every code example in docs/process/pipeline_network_optimization.md compiles and runs correctly. */ +@Tag("LinearAlgebra") class PipelineNetworkDocExamplesTest { private static SystemInterface gas; diff --git a/src/test/java/neqsim/process/equipment/reactor/GibbsReactorAlgorithmTest.java b/src/test/java/neqsim/process/equipment/reactor/GibbsReactorAlgorithmTest.java index 82da8c13fc..0c8c92f030 100644 --- a/src/test/java/neqsim/process/equipment/reactor/GibbsReactorAlgorithmTest.java +++ b/src/test/java/neqsim/process/equipment/reactor/GibbsReactorAlgorithmTest.java @@ -2,6 +2,7 @@ import java.util.List; import org.junit.jupiter.api.Assertions; +import org.junit.jupiter.api.Tag; import org.junit.jupiter.api.Test; import neqsim.process.equipment.stream.Stream; import neqsim.thermo.system.SystemInterface; @@ -21,6 +22,7 @@ * @author Even Solbraa * @version $Id: $Id */ +@Tag("LinearAlgebra") public class GibbsReactorAlgorithmTest { private static final Logger logger = LogManager.getLogger(GibbsReactorAlgorithmTest.class); diff --git a/src/test/java/neqsim/thermo/phase/GaussianEliminationTest.java b/src/test/java/neqsim/thermo/phase/GaussianEliminationTest.java index 7a27d8807a..5a094cbccc 100644 --- a/src/test/java/neqsim/thermo/phase/GaussianEliminationTest.java +++ b/src/test/java/neqsim/thermo/phase/GaussianEliminationTest.java @@ -1,14 +1,16 @@ package neqsim.thermo.phase; import static org.junit.jupiter.api.Assertions.assertTrue; -import org.ejml.simple.SimpleMatrix; -import org.junit.jupiter.api.Test; import org.apache.logging.log4j.LogManager; import org.apache.logging.log4j.Logger; +import org.junit.jupiter.api.Tag; +import org.junit.jupiter.api.Test; +import neqsim.util.math.LinearAlgebraOps; /** - * Test Gaussian elimination solver against EJML for matrix inversion. + * Test Gaussian elimination solver against ojAlgo for matrix inversion. */ +@Tag("LinearAlgebra") public class GaussianEliminationTest { private static final Logger logger = LogManager.getLogger(GaussianEliminationTest.class); @@ -20,36 +22,36 @@ private static boolean solveLinearSystem(double[][] a, double[] b, int n) { int maxRow = col; double maxVal = Math.abs(a[col][col]); for (int row = col + 1; row < n; row++) { - double val = Math.abs(a[row][col]); - if (val > maxVal) { - maxVal = val; - maxRow = row; - } + double val = Math.abs(a[row][col]); + if (val > maxVal) { + maxVal = val; + maxRow = row; + } } if (maxVal < 1.0e-30) { - return false; + return false; } if (maxRow != col) { - double[] tempRow = a[col]; - a[col] = a[maxRow]; - a[maxRow] = tempRow; - double tempB = b[col]; - b[col] = b[maxRow]; - b[maxRow] = tempB; + double[] tempRow = a[col]; + a[col] = a[maxRow]; + a[maxRow] = tempRow; + double tempB = b[col]; + b[col] = b[maxRow]; + b[maxRow] = tempB; } double pivot = a[col][col]; for (int row = col + 1; row < n; row++) { - double factor = a[row][col] / pivot; - for (int k = col + 1; k < n; k++) { - a[row][k] -= factor * a[col][k]; - } - b[row] -= factor * b[col]; + double factor = a[row][col] / pivot; + for (int k = col + 1; k < n; k++) { + a[row][k] -= factor * a[col][k]; + } + b[row] -= factor * b[col]; } } for (int row = n - 1; row >= 0; row--) { double sum = b[row]; for (int k = row + 1; k < n; k++) { - sum -= a[row][k] * b[k]; + sum -= a[row][k] * b[k]; } b[row] = sum / a[row][row]; } @@ -65,12 +67,39 @@ private static double[][] invertGE(double[][] mat, int n) { double[][] copy = new double[n][n]; double[] rhs = new double[n]; for (int i = 0; i < n; i++) { - System.arraycopy(mat[i], 0, copy[i], 0, n); - rhs[i] = (i == col) ? 1.0 : 0.0; + System.arraycopy(mat[i], 0, copy[i], 0, n); + rhs[i] = (i == col) ? 1.0 : 0.0; } solveLinearSystem(copy, rhs, n); for (int i = 0; i < n; i++) { - inv[i][col] = rhs[i]; + inv[i][col] = rhs[i]; + } + } + return inv; + } + + /** + * Solve Ax=b using ojAlgo LU decomposition. + */ + private static double[] solveOjAlgo(double[][] a, double[] b, int n) { + double[] x = new double[n]; + if (!LinearAlgebraOps.solveLinearSystem(a, b, x)) { + throw new IllegalStateException("ojAlgo LU decomposition failed"); + } + return x; + } + + /** + * Compute full inverse of nxn matrix using column-by-column ojAlgo solves. + */ + private static double[][] invertOjAlgo(double[][] mat, int n) { + double[][] inv = new double[n][n]; + for (int col = 0; col < n; col++) { + double[] rhs = new double[n]; + rhs[col] = 1.0; + double[] x = solveOjAlgo(mat, rhs, n); + for (int row = 0; row < n; row++) { + inv[row][col] = x[row]; } } return inv; @@ -79,18 +108,17 @@ private static double[][] invertGE(double[][] mat, int n) { @Test public void testSimple2x2() { // A = [[4, 7], [2, 6]], inv = [[0.6, -0.7], [-0.2, 0.4]] - double[][] a = { { 4, 7 }, { 2, 6 } }; + double[][] a = {{4, 7}, {2, 6}}; double[][] invGE = invertGE(a, 2); - - SimpleMatrix sm = new SimpleMatrix(new double[][] { { 4, 7 }, { 2, 6 } }); - SimpleMatrix invEJML = sm.invert(); + double[][] invOjAlgo = invertOjAlgo(a, 2); for (int i = 0; i < 2; i++) { for (int j = 0; j < 2; j++) { - double diff = Math.abs(invGE[i][j] - invEJML.get(i, j)); - logger.printf(org.apache.logging.log4j.Level.INFO, "inv[%d][%d]: GE=%.15e EJML=%.15e diff=%.4e%n", i, j, - invGE[i][j], invEJML.get(i, j), diff); - assertTrue(diff < 1e-12, "2x2 inv mismatch at [" + i + "][" + j + "]"); + double diff = Math.abs(invGE[i][j] - invOjAlgo[i][j]); + logger.printf(org.apache.logging.log4j.Level.INFO, + "inv[%d][%d]: GE=%.15e ojAlgo=%.15e diff=%.4e%n", i, j, invGE[i][j], invOjAlgo[i][j], + diff); + assertTrue(diff < 1e-12, "2x2 inv mismatch at [" + i + "][" + j + "]"); } } } @@ -102,7 +130,6 @@ public void testCPALikeHessian4x4() { double m = 5.55; double x = 0.3; double klk_cross = 0.5; // non-zero for ed-ea cross associations - double klk_same = 0.0; // delta pattern: sites 0,1 = ed; sites 2,3 = ea double[][] klk = new double[4][4]; @@ -118,15 +145,15 @@ public void testCPALikeHessian4x4() { double[][] hess = new double[4][4]; for (int i = 0; i < 4; i++) { for (int j = 0; j < 4; j++) { - double kron = (i == j) ? -m / (x * x) : 0.0; - hess[i][j] = kron - klk[i][j]; + double kron = (i == j) ? -m / (x * x) : 0.0; + hess[i][j] = kron - klk[i][j]; } } logger.info("Hessian matrix:"); for (int i = 0; i < 4; i++) { - logger.printf(org.apache.logging.log4j.Level.INFO, " [%.6f, %.6f, %.6f, %.6f]%n", hess[i][0], hess[i][1], - hess[i][2], hess[i][3]); + logger.printf(org.apache.logging.log4j.Level.INFO, " [%.6f, %.6f, %.6f, %.6f]%n", hess[i][0], + hess[i][1], hess[i][2], hess[i][3]); } // Compute inverse with both methods @@ -135,18 +162,17 @@ public void testCPALikeHessian4x4() { System.arraycopy(hess[i], 0, hessCopy[i], 0, 4); } double[][] invGE = invertGE(hessCopy, 4); - - SimpleMatrix sm = new SimpleMatrix(hess); - SimpleMatrix invEJML = sm.invert(); + double[][] invOjAlgo = invertOjAlgo(hess, 4); logger.info("\nInverse comparison:"); double maxDiff = 0; for (int i = 0; i < 4; i++) { for (int j = 0; j < 4; j++) { - double diff = Math.abs(invGE[i][j] - invEJML.get(i, j)); - maxDiff = Math.max(maxDiff, diff); - logger.printf(org.apache.logging.log4j.Level.INFO, "inv[%d][%d]: GE=%.15e EJML=%.15e diff=%.4e%n", i, j, - invGE[i][j], invEJML.get(i, j), diff); + double diff = Math.abs(invGE[i][j] - invOjAlgo[i][j]); + maxDiff = Math.max(maxDiff, diff); + logger.printf(org.apache.logging.log4j.Level.INFO, + "inv[%d][%d]: GE=%.15e EJML=%.15e diff=%.4e%n", i, j, invGE[i][j], invEJML.get(i, j), + diff); } } logger.printf(org.apache.logging.log4j.Level.INFO, "Max difference: %.4e%n", maxDiff); @@ -173,12 +199,12 @@ public void testSingleRHSSolve() { for (int i = 0; i < 4; i++) { for (int j = 0; j < 4; j++) { - hess[i][j] = ((i == j) ? -m / (x * x) : 0.0) - klk[i][j]; + hess[i][j] = ((i == j) ? -m / (x * x) : 0.0) - klk[i][j]; } } // RHS = KlkV * ksi (some test values) - double[] rhs = { 0.1, 0.2, -0.15, -0.25 }; + double[] rhs = {0.1, 0.2, -0.15, -0.25}; // GE solve double[][] hessCopy = new double[4][4]; @@ -189,16 +215,14 @@ public void testSingleRHSSolve() { } solveLinearSystem(hessCopy, rhsCopy, 4); - // EJML solve via inverse - SimpleMatrix sm = new SimpleMatrix(hess); - SimpleMatrix rhsSM = new SimpleMatrix(new double[][] { { 0.1 }, { 0.2 }, { -0.15 }, { -0.25 } }); - SimpleMatrix xvEJML = sm.invert().mult(rhsSM); + // ojAlgo solve + double[] xvOjAlgo = solveOjAlgo(hess, rhs, 4); logger.info("Single RHS solve comparison:"); for (int i = 0; i < 4; i++) { - double diff = Math.abs(rhsCopy[i] - xvEJML.get(i, 0)); - logger.printf(org.apache.logging.log4j.Level.INFO, " xv[%d]: GE=%.15e EJML=%.15e diff=%.4e%n", i, rhsCopy[i], - xvEJML.get(i, 0), diff); + double diff = Math.abs(rhsCopy[i] - xvOjAlgo[i]); + logger.printf(org.apache.logging.log4j.Level.INFO, + " xv[%d]: GE=%.15e ojAlgo=%.15e diff=%.4e%n", i, rhsCopy[i], xvOjAlgo[i], diff); assertTrue(diff < 1e-12, "Single RHS solve mismatch at [" + i + "]"); } } diff --git a/src/test/java/neqsim/thermodynamicoperations/flashops/NewtonSolverAnalysisTest.java b/src/test/java/neqsim/thermodynamicoperations/flashops/NewtonSolverAnalysisTest.java index d45351029a..1e5b8bf612 100644 --- a/src/test/java/neqsim/thermodynamicoperations/flashops/NewtonSolverAnalysisTest.java +++ b/src/test/java/neqsim/thermodynamicoperations/flashops/NewtonSolverAnalysisTest.java @@ -2,154 +2,154 @@ import static org.junit.jupiter.api.Assertions.assertTrue; import java.util.Random; -import org.ejml.data.DMatrixRMaj; -import org.ejml.dense.row.factory.LinearSolverFactory_DDRM; -import org.ejml.interfaces.linsol.LinearSolverDense; +import org.apache.logging.log4j.LogManager; +import org.apache.logging.log4j.Logger; +import org.junit.jupiter.api.Tag; import org.junit.jupiter.api.Test; +import org.ojalgo.matrix.decomposition.LU; +import org.ojalgo.matrix.store.Primitive64Store; import Jama.Matrix; import neqsim.thermo.system.SystemInterface; import neqsim.thermo.system.SystemSrkEos; import neqsim.thermodynamicoperations.ThermodynamicOperations; -import org.apache.logging.log4j.LogManager; -import org.apache.logging.log4j.Logger; +import neqsim.util.math.LinearAlgebraOps; /** * Analysis tests for Newton solver improvements. * *

    - * Benchmarks JAMA vs EJML linear solve, init level costs, and measures the overhead of T/P derivatives vs - * composition-only derivatives. + * Benchmarks JAMA vs ojAlgo linear solve, init level costs, and measures the overhead of T/P + * derivatives vs composition-only derivatives. *

    * * @author benchmarking * @version 1.0 */ +@Tag("LinearAlgebra") class NewtonSolverAnalysisTest { private static final Logger logger = LogManager.getLogger(NewtonSolverAnalysisTest.class); /** - * Benchmark JAMA vs EJML dense linear solve (Ax = b) for typical flash sizes. + * Benchmark JAMA vs ojAlgo dense linear solve (Ax = b) for typical flash sizes. * *

    - * JAMA uses full LU decomposition via double[][] arrays. EJML uses optimized row-major storage with native-tuned - * operations. + * JAMA uses full LU decomposition via double[][] arrays. ojAlgo uses optimized dense storage with + * native-tuned operations. *

    */ @Test void benchmarkJAMAvsEJML() { - int[] sizes = { 3, 5, 10, 15, 20, 30 }; + int[] sizes = {3, 5, 10, 15, 20, 30}; int warmup = 2000; int N = 20000; Random rng = new Random(42); - logger.info("=== JAMA vs EJML Dense Linear Solve Benchmark ==="); - logger.info(String.format("%-6s %-12s %-12s %-10s", "Size", "JAMA(ns)", "EJML(ns)", "Speedup")); + logger.info("=== JAMA vs ojAlgo Dense Linear Solve Benchmark ==="); + logger + .info(String.format("%-6s %-12s %-12s %-10s", "Size", "JAMA(ns)", "ojAlgo(ns)", "Speedup")); for (int n : sizes) { - // Create random SPD matrix (typical Jacobian is SPD) double[][] aData = new double[n][n]; double[] bData = new double[n]; for (int i = 0; i < n; i++) { - bData[i] = rng.nextDouble(); - for (int j = 0; j < n; j++) { - aData[i][j] = rng.nextDouble(); - } - aData[i][i] += n; // Make diagonally dominant + bData[i] = rng.nextDouble(); + for (int j = 0; j < n; j++) { + aData[i][j] = rng.nextDouble(); + } + aData[i][i] += n; } - // JAMA warmup Matrix jamaMat = new Matrix(aData); Matrix jamab = new Matrix(n, 1); for (int i = 0; i < n; i++) { - jamab.set(i, 0, bData[i]); + jamab.set(i, 0, bData[i]); } for (int w = 0; w < warmup; w++) { - jamaMat.solve(jamab); + jamaMat.solve(jamab); } - // EJML warmup - DMatrixRMaj ejmlMat = new DMatrixRMaj(aData); - DMatrixRMaj ejmlb = new DMatrixRMaj(n, 1); + Primitive64Store ojAlgoMat = Primitive64Store.FACTORY.rows(aData); + Primitive64Store ojAlgoB = Primitive64Store.FACTORY.make(n, 1); for (int i = 0; i < n; i++) { - ejmlb.set(i, 0, bData[i]); + ojAlgoB.set(i, 0, bData[i]); } - DMatrixRMaj ejmlx = new DMatrixRMaj(n, 1); - LinearSolverDense solver = LinearSolverFactory_DDRM.lu(n); + Primitive64Store ojAlgoWork = Primitive64Store.FACTORY.make(n, n); + LU solver = LU.PRIMITIVE.make(n, n); for (int w = 0; w < warmup; w++) { - solver.setA(ejmlMat.copy()); - solver.solve(ejmlb, ejmlx); + LinearAlgebraOps.copyDenseStore(ojAlgoMat, ojAlgoWork, n); + solver.decompose(ojAlgoWork); + solver.getSolution(ojAlgoB); } - // JAMA benchmark long start = System.nanoTime(); for (int iter = 0; iter < N; iter++) { - jamaMat.solve(jamab); + jamaMat.solve(jamab); } long jamaTime = System.nanoTime() - start; - // EJML benchmark start = System.nanoTime(); for (int iter = 0; iter < N; iter++) { - solver.setA(ejmlMat.copy()); - solver.solve(ejmlb, ejmlx); + LinearAlgebraOps.copyDenseStore(ojAlgoMat, ojAlgoWork, n); + solver.decompose(ojAlgoWork); + solver.getSolution(ojAlgoB); } - long ejmlTime = System.nanoTime() - start; + long ojAlgoTime = System.nanoTime() - start; double jamaPerCall = (double) jamaTime / N; - double ejmlPerCall = (double) ejmlTime / N; - double speedup = jamaPerCall / ejmlPerCall; + double ojAlgoPerCall = (double) ojAlgoTime / N; + double speedup = jamaPerCall / ojAlgoPerCall; - logger.info(String.format("%-6d %-12.0f %-12.0f %-10.2fx", n, jamaPerCall, ejmlPerCall, speedup)); + logger.info( + String.format("%-6d %-12.0f %-12.0f %-10.2fx", n, jamaPerCall, ojAlgoPerCall, speedup)); - // Just verify both return reasonable values assertTrue(jamaPerCall > 0); - assertTrue(ejmlPerCall > 0); + assertTrue(ojAlgoPerCall > 0); } - // Also benchmark EJML with in-place solve (no copy) - logger.info("=== EJML In-Place vs Copy Solve ==="); - for (int n : new int[] { 10, 20 }) { + + // Also benchmark ojAlgo with explicit work copy. + logger.info("=== ojAlgo Work-Copy Solve ==="); + for (int n : new int[] {10, 20}) { double[][] aData = new double[n][n]; double[] bData = new double[n]; for (int i = 0; i < n; i++) { - bData[i] = rng.nextDouble(); - for (int j = 0; j < n; j++) { - aData[i][j] = rng.nextDouble(); - } - aData[i][i] += n; + bData[i] = rng.nextDouble(); + for (int j = 0; j < n; j++) { + aData[i][j] = rng.nextDouble(); + } + aData[i][i] += n; } - DMatrixRMaj ejmlMat = new DMatrixRMaj(aData); - DMatrixRMaj ejmlb = new DMatrixRMaj(n, 1); + Primitive64Store ojAlgoMat = Primitive64Store.FACTORY.rows(aData); + Primitive64Store ojAlgoB = Primitive64Store.FACTORY.make(n, 1); for (int i = 0; i < n; i++) { - ejmlb.set(i, 0, bData[i]); + ojAlgoB.set(i, 0, bData[i]); } - DMatrixRMaj ejmlx = new DMatrixRMaj(n, 1); - DMatrixRMaj ejmlMatCopy = new DMatrixRMaj(n, n); - LinearSolverDense solver2 = LinearSolverFactory_DDRM.lu(n); + Primitive64Store ojAlgoWork = Primitive64Store.FACTORY.make(n, n); + LU solver2 = LU.PRIMITIVE.make(n, n); - // Warmup for (int w = 0; w < warmup; w++) { - ejmlMatCopy.setTo(ejmlMat); - solver2.setA(ejmlMatCopy); - solver2.solve(ejmlb, ejmlx); + LinearAlgebraOps.copyDenseStore(ojAlgoMat, ojAlgoWork, n); + solver2.decompose(ojAlgoWork); + solver2.getSolution(ojAlgoB); } - // With pre-allocated copy long start = System.nanoTime(); for (int iter = 0; iter < N; iter++) { - ejmlMatCopy.setTo(ejmlMat); - solver2.setA(ejmlMatCopy); - solver2.solve(ejmlb, ejmlx); + LinearAlgebraOps.copyDenseStore(ojAlgoMat, ojAlgoWork, n); + solver2.decompose(ojAlgoWork); + solver2.getSolution(ojAlgoB); } long time = System.nanoTime() - start; - System.out.println(String.format("n=%d EJML pre-alloc copy: %.0f ns/call", n, (double) time / N)); + System.out + .println(String.format("n=%d ojAlgo work-copy: %.0f ns/call", n, (double) time / N)); } } /** - * Benchmark init(3) vs init(1) + logfugcoefdN only, to measure cost of unnecessary T/P derivative computation. + * Benchmark init(3) vs init(1) + logfugcoefdN only, to measure cost of unnecessary T/P derivative + * computation. */ @Test void benchmarkInitLevelBreakdown() { @@ -189,10 +189,14 @@ void benchmarkInitLevelBreakdown() { logger.info("=== Init Level Cost Breakdown (10-comp SRK) ==="); logger.info(String.format("init(1) fugacities: %6.1f us", init1us)); - logger.info(String.format("init(2) + T,P derivs: %6.1f us (delta: +%.1f us)", init2us, tpDerivCost)); - logger.info(String.format("init(3) + comp derivs: %6.1f us (delta: +%.1f us)", init3us, compDerivCost)); - logger.info(String.format("T,P derivative cost: %6.1f us (%.1f%% of init(3))", tpDerivCost, wastedPercent)); - logger.info(String.format("Wasted per Newton iter: %6.1f us (logfugcoefdT + dP)", tpDerivCost)); + logger.info( + String.format("init(2) + T,P derivs: %6.1f us (delta: +%.1f us)", init2us, tpDerivCost)); + logger.info(String.format("init(3) + comp derivs: %6.1f us (delta: +%.1f us)", init3us, + compDerivCost)); + logger.info(String.format("T,P derivative cost: %6.1f us (%.1f%% of init(3))", tpDerivCost, + wastedPercent)); + logger + .info(String.format("Wasted per Newton iter: %6.1f us (logfugcoefdT + dP)", tpDerivCost)); // T/P derivatives should be a measurable fraction of init(3) assertTrue(tpDerivCost >= 0, "T,P derivative cost should be non-negative"); @@ -253,8 +257,8 @@ void benchmarkNewtonVsSSIteration() { SystemInterface sysCopy = sys.clone(); // Apply small perturbation to compositions for (int i = 0; i < sysCopy.getPhase(0).getNumberOfComponents(); i++) { - double x0 = sysCopy.getPhase(0).getComponent(i).getx(); - sysCopy.getPhase(0).getComponent(i).setx(x0 * (1.0 + 0.01 * (i % 3 - 1))); + double x0 = sysCopy.getPhase(0).getComponent(i).getx(); + sysCopy.getPhase(0).getComponent(i).setx(x0 * (1.0 + 0.01 * (i % 3 - 1))); } sysCopy.getPhase(0).normalize(); sysCopy.init(1); @@ -265,7 +269,7 @@ void benchmarkNewtonVsSSIteration() { sysCopy.init(1, 1); long elapsed = System.nanoTime() - start; if (w >= warmup) { - totalSS += elapsed; + totalSS += elapsed; } } @@ -275,14 +279,14 @@ void benchmarkNewtonVsSSIteration() { SystemInterface sysCopy = sys.clone(); sysCopy.init(1); - SysNewtonRhapsonTPflash solver = new SysNewtonRhapsonTPflash(sysCopy, 2, - sysCopy.getPhase(0).getNumberOfComponents()); + SysNewtonRhapsonTPflash solver = + new SysNewtonRhapsonTPflash(sysCopy, 2, sysCopy.getPhase(0).getNumberOfComponents()); long start = System.nanoTime(); solver.solve(); long elapsed = System.nanoTime() - start; if (w >= warmup) { - totalNewton += elapsed; + totalNewton += elapsed; } } @@ -295,7 +299,8 @@ void benchmarkNewtonVsSSIteration() { } /** - * Measure allocation overhead: JAMA creates new Matrix objects each solve. EJML can reuse pre-allocated buffers. + * Measure allocation overhead: JAMA creates new Matrix objects each solve. ojAlgo can reuse + * pre-allocated buffers. */ @Test void benchmarkAllocationOverhead() { @@ -308,7 +313,7 @@ void benchmarkAllocationOverhead() { for (int i = 0; i < n; i++) { bData[i] = rng.nextDouble(); for (int j = 0; j < n; j++) { - aData[i][j] = rng.nextDouble(); + aData[i][j] = rng.nextDouble(); } aData[i][i] += n; } @@ -327,40 +332,38 @@ void benchmarkAllocationOverhead() { long start = System.nanoTime(); for (int iter = 0; iter < N; iter++) { - Matrix result = jamaMat.solve(jamab); + jamaMat.solve(jamab); } long jamaTime = System.nanoTime() - start; - // EJML: pre-allocated solver and output buffer - DMatrixRMaj ejmlMat = new DMatrixRMaj(aData); - DMatrixRMaj ejmlb = new DMatrixRMaj(n, 1); + // ojAlgo: pre-allocated solver and work buffer + Primitive64Store ojAlgoMat = Primitive64Store.FACTORY.rows(aData); + Primitive64Store ojAlgoB = Primitive64Store.FACTORY.make(n, 1); for (int i = 0; i < n; i++) { - ejmlb.set(i, 0, bData[i]); + ojAlgoB.set(i, 0, bData[i]); } - DMatrixRMaj ejmlx = new DMatrixRMaj(n, 1); - DMatrixRMaj ejmlWork = new DMatrixRMaj(n, n); - LinearSolverDense solver = LinearSolverFactory_DDRM.lu(n); + Primitive64Store ojAlgoWork = Primitive64Store.FACTORY.make(n, n); + LU solver = LU.PRIMITIVE.make(n, n); // Warmup for (int w = 0; w < 5000; w++) { - ejmlWork.setTo(ejmlMat); - solver.setA(ejmlWork); - solver.solve(ejmlb, ejmlx); + LinearAlgebraOps.copyDenseStore(ojAlgoMat, ojAlgoWork, n); + solver.decompose(ojAlgoWork); + solver.getSolution(ojAlgoB); } - start = System.nanoTime(); + long ojAlgoStart = System.nanoTime(); for (int iter = 0; iter < N; iter++) { - ejmlWork.setTo(ejmlMat); - solver.setA(ejmlWork); - solver.solve(ejmlb, ejmlx); + LinearAlgebraOps.copyDenseStore(ojAlgoMat, ojAlgoWork, n); + solver.decompose(ojAlgoWork); + solver.getSolution(ojAlgoB); } - long ejmlTime = System.nanoTime() - start; + long ojAlgoTime = System.nanoTime() - ojAlgoStart; logger.info("=== Allocation Overhead (n=10, " + N + " solves) ==="); logger.info(String.format("JAMA (new alloc each): %.0f ns/call", (double) jamaTime / N)); - logger.info(String.format("EJML (pre-allocated): %.0f ns/call", (double) ejmlTime / N)); - logger.info(String.format("Speedup: %.2fx", (double) jamaTime / ejmlTime)); - + logger.info(String.format("ojAlgo (work-copy): %.0f ns/call", (double) ojAlgoTime / N)); + logger.info(String.format("Speedup: %.2fx", (double) jamaTime / ojAlgoTime)); } /** diff --git a/src/test/java/neqsim/thermodynamicoperations/flashops/TPmultiflashTest.java b/src/test/java/neqsim/thermodynamicoperations/flashops/TPmultiflashTest.java index dd250c6956..b238c509ca 100644 --- a/src/test/java/neqsim/thermodynamicoperations/flashops/TPmultiflashTest.java +++ b/src/test/java/neqsim/thermodynamicoperations/flashops/TPmultiflashTest.java @@ -2,6 +2,7 @@ import static org.junit.jupiter.api.Assertions.assertEquals; import static org.junit.jupiter.api.Assertions.assertTrue; +import org.junit.jupiter.api.Tag; import org.junit.jupiter.api.Test; import neqsim.thermo.mixingrule.EosMixingRulesInterface; import neqsim.thermo.system.SystemInterface; @@ -12,6 +13,7 @@ /** * @author ESOL */ +@Tag("LinearAlgebra") class TPmultiflashTest { private static final Logger logger = LogManager.getLogger(TPmultiflashTest.class);