Skip to content

Commit 636b0a2

Browse files
freaking works. Let's go
1 parent 509b11a commit 636b0a2

File tree

12 files changed

+854
-465
lines changed

12 files changed

+854
-465
lines changed

src/main/java/com/rae/formicapi/math/Solvers.java

Lines changed: 36 additions & 39 deletions
Original file line numberDiff line numberDiff line change
@@ -16,54 +16,51 @@ public class Solvers {
1616
* @return the Solution if there is one or 0.
1717
*/
1818
public static float dichotomy(Function<Float, Float> function, float a, float b, float epsilon) {
19-
try {
20-
float fa = function.apply(a);
21-
float fb = function.apply(b);
19+
float fa = function.apply(a);
20+
float fb = function.apply(b);
2221

23-
if (Float.isNaN(fa) || Float.isNaN(fb)) {
24-
throw new RuntimeException("Initial values are NaN: f(a)=" + fa + ", f(b)=" + fb);
25-
}
22+
if (Float.isNaN(fa) || Float.isNaN(fb)) {
23+
throw new RuntimeException("Initial values are NaN: f(a)=" + fa + ", f(b)=" + fb);
24+
}
2625

27-
if (fa * fb > 0) {
28-
throw new RuntimeException("Wrong boundaries in dichotomy solver: a=" + a + " f(a)=" + fa + " | b=" + b + " f(b)=" + fb);
29-
}
26+
if (fa * fb > 0) {
27+
throw new RuntimeException("Wrong boundaries in dichotomy solver: a=" + a + " f(a)=" + fa + " | b=" + b + " f(b)=" + fb);
28+
}
3029

31-
float m = (a + b) / 2f;
32-
float fm = function.apply(m);
33-
34-
while (Math.abs(b - a) > epsilon) {
35-
// Check for NaN
36-
if (Float.isNaN(fm)) {
37-
// Try to adjust toward the better side
38-
float testA = function.apply(a);
39-
float testB = function.apply(b);
40-
41-
if (!Float.isNaN(testA)) {
42-
b = m;
43-
} else if (!Float.isNaN(testB)) {
44-
a = m;
45-
} else {
46-
throw new RuntimeException("Function returned NaN across entire interval: a=" + a + ", b=" + b);
47-
}
48-
} else if (fm == 0.0f) {
49-
return m;
50-
} else if (fa * fm > 0) {
30+
float m = (a + b) / 2f;
31+
float fm = function.apply(m);
32+
int maxIterations = 10000;
33+
int i = 0;
34+
while (Math.abs(b - a) > epsilon && i < maxIterations) {
35+
// Check for NaN
36+
i++;
37+
if (Float.isNaN(fm)) {
38+
// Try to adjust toward the better side
39+
float testA = function.apply(a);
40+
float testB = function.apply(b);
41+
42+
if (!Float.isNaN(testA)) {
43+
b = m;
44+
} else if (!Float.isNaN(testB)) {
5145
a = m;
52-
fa = fm;
5346
} else {
54-
b = m;
55-
fb = fm;
47+
throw new RuntimeException("Function returned NaN across entire interval: a=" + a + ", b=" + b);
5648
}
57-
58-
m = (a + b) / 2f;
59-
fm = function.apply(m);
49+
} else if (fm == 0.0f) {
50+
return m;
51+
} else if (fa * fm > 0) {
52+
a = m;
53+
fa = fm;
54+
} else {
55+
b = m;
56+
fb = fm;
6057
}
6158

62-
return m;
63-
} catch (RuntimeException e) {
64-
FormicAPI.LOGGER.error("Dichotomy solver error: " + e);
65-
return 0;
59+
m = (a + b) / 2f;
60+
fm = function.apply(m);
6661
}
62+
63+
return m;
6764
}
6865
//TODO complete a naive approach first
6966

Lines changed: 263 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,18 +1,279 @@
11
package com.rae.formicapi.thermal_utilities;
22

3+
import com.rae.formicapi.FormicAPI;
34
import net.createmod.catnip.data.Couple;
4-
import oshi.util.tuples.Pair;
55

66
import java.util.ArrayList;
7+
import java.util.Arrays;
78
import java.util.List;
89

910
public abstract class CubicEOS implements EquationOfState{
11+
12+
public static final double T_REF = 298.15;
13+
public static final double P_REF = 101325.0;
14+
protected final double M;
15+
protected final double Tc; // Critical temperature [K]
16+
protected final double Pc; // Critical pressure [Pa]
17+
18+
19+
protected CubicEOS(double m, double Tc, double Pc) {
20+
this.M = m;
21+
this.Tc = Tc;
22+
this.Pc = Pc;
23+
}
24+
1025
@Override
1126
public abstract double pressure(double temperature, double volumeMolar);
1227

1328

14-
abstract Couple<Double> findSpinodalPoints(double T);
29+
public abstract List<Double> findSpinodalPoints(double T);
30+
public List<Double> findSpinodalPoints(double T, double vMin, double vMax) {
31+
List<Double> spinodals = new ArrayList<>();
32+
33+
double previousV = vMin;
34+
double previousP = pressure(T, previousV);
35+
double previousSlope = Double.NaN;
36+
37+
for (double V = vMin; V <= vMax; V *= 1.05) {
38+
double P = pressure(T, V);
39+
double slope = (P - previousP) / (V - previousV);
40+
41+
if (!Double.isNaN(previousSlope) && previousSlope * slope <= 0) {
42+
// slope changed sign → inflection/spinodal
43+
spinodals.add(V);
44+
}
45+
46+
previousV = V;
47+
previousP = P;
48+
previousSlope = slope;
49+
}
50+
51+
return spinodals;
52+
}
53+
54+
@Override
55+
public double volumeMolar(double T, double P, double vaporFraction) {
56+
double[] roots = getZFactors(T, P);
57+
58+
if (roots.length == 0) {
59+
throw new IllegalStateException("No valid real roots for Z at T = "+ T+ " P = "+ P);
60+
}
61+
62+
double Z;
63+
if (vaporFraction <= 0.0) {
64+
Z = roots[0]; // Liquid-like root
65+
} else if (vaporFraction >= 1.0) {
66+
Z = roots[roots.length - 1]; // Vapor-like root
67+
} else if (roots.length >= 2) {
68+
double saturationPressure = saturationPressure(T);
69+
// Interpolate molar volume, not Z
70+
double Zl = roots[0];
71+
double Zv = roots[roots.length - 1];
72+
double Vl = Zl * R * T / saturationPressure;
73+
double Vv = Zv * R * T / saturationPressure;
74+
return (1 - vaporFraction) * Vl + vaporFraction * Vv;
75+
} else {
76+
// Fallback for single phase
77+
Z = roots[0];
78+
}
79+
80+
return Z * R * T / P;
81+
}
82+
1583
abstract double[] getZFactors(double T, double P);
84+
protected double idealGasEntropy(double T, double P) {
85+
if (T < 0) T = 0.1f;
86+
if (P < 0) P = 0.1f;
87+
88+
double Cp = 3.5 * R;
89+
90+
return (Cp * Math.log(T) - R * Math.log(P))/M;
91+
}
92+
abstract double saturationPressure(double T);
93+
94+
/**
95+
* @param T temperature
96+
* @param Vm molar volume -> TODO go toward a by mass volume
97+
* @return specific entropy
98+
*/
99+
public final double totalEntropy(double T, double Vm) {
100+
double SRef = idealGasEntropy(T_REF, P_REF) + residualEntropy(T_REF, P_REF, getZFactors(T_REF, P_REF)[0]);
101+
102+
// Check two-phase region
103+
if (T >= Tc ) {//we can't compute the saturation pressure past the critical temperature by definition
104+
// Not in 2-phase region, just compute entropy at T, P, Z
105+
double P = pressure(T, Vm); // From PR EOS
106+
double Z = Vm * P / (T * R);//only one roots
107+
return residualEntropy(T, P, Z) + idealGasEntropy(T,P) - SRef;
108+
}
109+
double PSat = saturationPressure(T);//if we are 2 phased then we need to use the saturation pressure
110+
111+
// Two-phase region
112+
double[] Zs = getZFactors(T, PSat);
113+
if (Zs.length == 0) {//if there is no solution we are a liquid that is too low in temperature.
114+
double P = pressure(T, Vm); // From PR EOS
115+
double Z = Vm * P / (T * R);//only one roots
116+
return residualEntropy(T, P, Z) + idealGasEntropy(T,P)- SRef;
117+
}
118+
double Z_l = Zs[0];
119+
double Z_v = Zs[Zs.length - 1];
120+
121+
// Compute molar volumes of saturated phases
122+
double v_l = Z_l * R * T / PSat;
123+
double v_v = Z_v * R * T / PSat;
124+
125+
if (Vm < v_l || Vm > v_v){//check if 2 phases or not
126+
double P = pressure(T, Vm); // From PR EOS
127+
double Z = Vm * P / (T * R);//only one roots
128+
return residualEntropy(T, P, Z) + idealGasEntropy(T,P)- SRef;
129+
}
130+
131+
// Compute vapor quality x
132+
double x = (Vm - v_l) / (v_v - v_l);
133+
x = Math.max(0.0, Math.min(1.0, x)); // Clamp between 0 and 1
134+
135+
// Residual entropy for each phase
136+
double S_l = residualEntropy(T, pressure(T, v_l), Z_l); //+ idealGasEntropy(T, PSat);
137+
double S_v = residualEntropy(T, pressure(T, v_v), Z_v);// + idealGasEntropy(T, PSat);
138+
// Mix and convert to specific entropy
139+
return (1 - x) * S_l + x * S_v - SRef + idealGasEntropy(T, PSat); // [J/kg·K]
140+
}
141+
142+
/**
143+
* Warning only use this if you know that the fluid is in the LV phase.
144+
* @param T temperature
145+
* @return return the couple Vl, Vv or the intermediate V if it's super critical.
146+
*/
147+
public Couple<Double> getSaturationVolumes(double T) {
148+
try {
149+
if (T < Tc ) {
150+
double PSat = saturationPressure(T); // try normal coexistence
151+
double[] roots = getZFactors(T, PSat);
152+
153+
if (roots.length <= 2) {
154+
throw new IllegalStateException("The Saturation Pressure is not in a 2 phase region");
155+
}
156+
double Zl = roots[0];
157+
double Zv = roots[roots.length - 1];
158+
double Vl = Zl * R * T / PSat;
159+
double Vv = Zv * R * T / PSat;
160+
return Couple.create(Vl, Vv);
161+
} else {
162+
double[] critRoot = getZFactors(Tc, Pc);
163+
164+
// Start guess: approximate ideal gas volume
165+
double Z = Arrays.stream(critRoot).max().getAsDouble();
166+
//we need to find a better interpolation function.
167+
double VmInflection = Z * EquationOfState.R * Tc / Pc;
168+
return Couple.create(VmInflection, VmInflection);
169+
}
170+
} catch (RuntimeException e) {
171+
FormicAPI.LOGGER.debug("Unexpectedly found outside LV phase {}", T);
172+
FormicAPI.LOGGER.debug(e);
173+
// Fall back to inflection point
174+
return Couple.create(Double.NaN, Double.NaN);
175+
}
176+
}
177+
178+
protected abstract double residualEntropy(double T, double P, double Z);
179+
180+
181+
/**
182+
*
183+
* @param T temperature
184+
* @return specific enthalpy
185+
*/
186+
protected double idealGasEnthalpy(double T) {
187+
double Cp = 75.0; // J/mol·K
188+
return Cp * (T)/M;
189+
}
190+
191+
protected abstract double residualEnthalpy(double T, double P, double Z);
192+
193+
public double totalEnthalpy(double T, double Vm) {
194+
double HRef = idealGasEnthalpy(T_REF) + residualEnthalpy(T_REF, P_REF, getZFactors(T_REF, P_REF)[0]);
195+
double P = pressure(T, Vm); // From PR EOS
196+
197+
// Check two-phase region
198+
if (T >= Tc ) {//we can't compute the saturation pressure past the critical temperature by definition
199+
// Not in 2-phase region, just compute entropy at T, P, Z
200+
double Z = Vm * P / (T * R);//only one roots
201+
return residualEnthalpy(T, P, Z) + idealGasEnthalpy(T) - HRef;
202+
}
203+
204+
double PSat = saturationPressure(T);//if we are 2 phased then we need to use the saturation pressure
205+
206+
// Two-phase region
207+
double[] Zs = getZFactors(T, PSat);
208+
209+
if (Zs.length == 0){//check if 2 phases or not
210+
System.out.println("weird T "+T);
211+
double Z = Vm * P / (T * R);//only one roots
212+
return residualEnthalpy(T, P, Z) + idealGasEnthalpy(T) - HRef;
213+
}
214+
215+
double Z_l = Zs[0];
216+
double Z_v = Zs[Zs.length - 1];
217+
218+
// Compute molar volumes of saturated phases
219+
double v_l = Z_l * R * T / PSat;
220+
double v_v = Z_v * R * T / PSat;
221+
222+
if (Vm < v_l || Vm > v_v){//check if 2 phases or not
223+
double Z = Vm * P / (T * R);//only one roots
224+
return residualEnthalpy(T, P, Z) + idealGasEnthalpy(T) - HRef;
225+
}
226+
227+
228+
// Compute vapor quality x
229+
double x = (Vm - v_l) / (v_v - v_l);
230+
x = Math.max(0.0, Math.min(1.0, x)); // Clamp between 0 and 1
231+
232+
// Residual entropy for each phase
233+
double H_l = residualEnthalpy(T, PSat, Z_l) + idealGasEnthalpy(T);
234+
double H_v = residualEnthalpy(T, PSat, Z_v) + idealGasEnthalpy(T);
235+
// Mix and convert to specific entropy
236+
return (1 - x) * H_l + x * H_v - HRef; // [J/kg·K]
237+
}
238+
239+
public boolean is2phases(double T, double P) {
240+
double saturationPressure = saturationPressure(T);
241+
return P >= saturationPressure * 0.99f || P <= saturationPressure * 1.01f;// do this to avoid problem with numerical error
242+
}
243+
244+
public double getEntropy(float T, float P, float x) {
245+
double[] roots1 = getZFactors(T, P);
246+
if (roots1.length == 0)
247+
return 0;//this is bad -> do it differently
248+
if (roots1.length == 1) {// this is when it's monophasique or past critical
249+
double Z1 = roots1[0];
250+
double Vm1 = Z1 * R * T / P;
251+
return totalEntropy(T, Vm1);
252+
}
253+
else {
254+
if (T < Tc ){
255+
double saturationPressure = saturationPressure(T);
256+
if (saturationPressure * 1.01 < P) {
257+
return totalEntropy(T,roots1[0]* R * T / P);
258+
}
259+
if (saturationPressure * 0.99 > P) {
260+
return totalEntropy(T,roots1[roots1.length-1]* R * T / P);
261+
}
262+
}
263+
Couple<Double> Vms = getSaturationVolumes(T);
264+
return (totalEntropy(T,Vms.getFirst()) * (1- x) +totalEntropy(T, Vms.getSecond())* x);
265+
}
266+
}
267+
268+
public double getEnthalpy(float T, float P, float x){
269+
final double R = EquationOfState.R;
270+
if (Float.isNaN(T)) T = 100;
271+
if (Float.isNaN(P)) P = 1;
16272

273+
if (T < 100) T = 100;
274+
if (P < 1) P = 1;
275+
return totalEnthalpy(T, volumeMolar(T, P, x));
276+
}
17277

278+
protected abstract double minZ(float finalT, float finalP);
18279
}

src/main/java/com/rae/formicapi/thermal_utilities/EOSLibrary.java

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,8 @@ public static PengRobinsonEOS getPRWaterEOS() {
66
double Tc = 647.1;//critical temperature in Kelvin
77
double Pc = 22.064e6;//critical pressure in Pascals
88
double omega = 0.344;//omega.... What is it exactly ?
9-
return new PengRobinsonEOS(Tc, Pc, omega);
9+
double M = 18.01528e-3f;
10+
return new PengRobinsonEOS(Tc, Pc, omega, M);
1011
}
1112

1213
public static VanDerWaalsEOS getVanDerWaalsWaterEOS() {

src/main/java/com/rae/formicapi/thermal_utilities/EquationOfState.java

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
package com.rae.formicapi.thermal_utilities;
22

33
public interface EquationOfState {
4+
double R = 8.314462618; // [J/mol·K]
45
//TODO we only wants to use the
56
/**
67
* @param temperature temperature in Kelvin

0 commit comments

Comments
 (0)