@@ -409,6 +409,7 @@ def update(self, t):
409409import numpy as np
410410from scipy .integrate import solve_bvp
411411from scipy import constants as const
412+ from scipy .optimize import root_scalar
412413
413414
414415def solve (params ):
@@ -417,10 +418,9 @@ def solve_tritium_extraction(dimensionless_params, y_T2_in, elements):
417418 Solves the BVP for tritium extraction in a bubble column.
418419
419420 Args:
420- dimensionless_params (dict): A dictionary containing the dimensionless parameters:
421+ params (dict): A dictionary containing the dimensionless parameters:
421422 Bo_l, phi_l, Bo_g, phi_g, psi, nu.
422423 y_T2_in (float): Inlet tritium molar fraction in the gas phase, y_T2(0-).
423- elements (int): Number of mesh elements for the solver.
424424
425425 Returns:
426426 sol: The solution object from scipy.integrate.solve_bvp.
@@ -440,29 +440,32 @@ def ode_system(xi, S):
440440 S[1] = dx_T/d(xi)
441441 S[2] = y_T2 (dimensionless gas concentration)
442442 S[3] = dy_T2/d(xi)
443+
444+ x_T = c_T / c_T(L+)
445+ xi = z / L (dimensionless position)
443446 """
444447 x_T , dx_T_dxi , y_T2 , dy_T2_dxi = S
445448
446- # Dimensionless driving force theta. Eq. 8.8
447- theta = x_T - np .sqrt (np . maximum ( 0 , (1 - psi * xi ) * y_T2 / nu ))
449+ # Dimensionless driving force theta. Eq. 12
450+ theta = x_T - np .sqrt (( (1 - ( psi * xi )) / nu ) * y_T2 ) # MATCHES PAPER
448451
449452 # Equation for d(S[0])/d(xi) = d(x_T)/d(xi)
450453 dS0_dxi = dx_T_dxi
451454
452455 # Equation for d(S[1])/d(xi) = d^2(x_T)/d(xi)^2
453- dS1_dxi = Bo_l * (phi_l * theta - dx_T_dxi ) # Eq. 9.1.4
456+ dS1_dxi = Bo_l * (phi_l * theta - dx_T_dxi )
454457
455458 # Equation for d(S[2])/d(xi) = d(y_T2)/d(xi)
456459 dS2_dxi = dy_T2_dxi
457460
458461 # Equation for d(S[3])/d(xi) = d^2(y_T2)/d(xi)^2 from eq (11)
459462 # Avoid division by zero if (1 - psi * xi) is close to zero at xi=1
460- denominator = 1 - psi * xi
461- denominator = np .where (np .isclose (denominator , 0 ), 1e-9 , denominator )
462463
463- term1 = (1 + 2 * psi / Bo_g ) * dy_T2_dxi
464- term2 = phi_g * theta
465- dS3_dxi = (Bo_g / denominator ) * (term1 - term2 )
464+ term1 = (1 + 2 * psi / Bo_g ) * dy_T2_dxi # Part of Eq. 9.3.3 (fourth line)
465+ term2 = phi_g * theta # Part of Eq. 9.3.3 (fourth line)
466+ dS3_dxi = (Bo_g / (1 - psi * xi )) * (
467+ term1 - term2
468+ ) # Eq. 9.3.3 (fourth line)
466469
467470 return np .vstack ((dS0_dxi , dS1_dxi , dS2_dxi , dS3_dxi ))
468471
@@ -492,82 +495,150 @@ def boundary_conditions(Sa, Sb):
492495 # Set up the mesh and an initial guess for the solver.
493496 xi = np .linspace (0 , 1 , elements + 1 )
494497
495- # An initial guess that is physically plausible can significantly help convergence.
496- # We expect liquid concentration (x_T) to decrease from inlet (xi=1) to outlet (xi=0).
497- # We expect gas concentration (y_T2) to increase from inlet (xi=0) to outlet (xi=1).
498- # y_guess = np.zeros((4, xi.size))
499- # y_guess[0, :] = np.linspace(
500- # 0.5, 1.0, xi.size
501- # ) # Guess for x_T (linear decrease)
502- # y_guess[1, :] = -0.5 # Guess for dx_T/dxi
503- # y_guess[2, :] = np.linspace(
504- # y_T2_in, y_T2_in + 1e-4, xi.size
505- # ) # Guess for y_T2 (linear increase)
506- # y_guess[3, :] = 1e-4 # Guess for dy_T2/dxi
507-
508- # a zero initial guess works better for low concentrations
509498 y_guess = np .zeros ((4 , xi .size ))
510499
511500 # Run the BVP solver
512501 sol = solve_bvp (
513- ode_system , boundary_conditions , xi , y_guess , tol = 1e-5 , max_nodes = 10000
502+ ode_system , boundary_conditions , xi , y_guess , tol = 1e-6 , max_nodes = 10000
514503 )
515504
516505 return sol
517506
518507 # Unpack parameters
519508 c_T_inlet = params ["c_T_inlet" ]
520509 y_T2_in = params ["y_T2_in" ]
521- P_outlet = params ["P_outlet" ]
522- ρ_l = params ["ρ_l" ]
523- K_s = params ["K_s" ]
510+ P_0 = params ["P_0" ]
524511
525512 L = params ["L" ]
526513 D = params ["D" ]
527- ε_g = params ["ε_g" ]
528-
529- Q_l = params ["Q_l" ]
530- Q_g = params ["Q_g" ]
531-
532- a = params ["a" ]
533514
534- E_g = params ["E_g" ]
535- E_l = params ["E_l" ]
536-
537- h_l = params ["h_l" ]
515+ flow_l = params ["flow_l" ]
516+ u_g0 = params ["u_g0" ]
538517
539518 g = params ["g" ]
540519 T = params ["T" ]
541520
542521 elements = params ["elements" ] # Number of mesh elements for solver
543522
544- # Calculate inlet pressure hydrostatically
545- assert P_outlet > 0 , "P_outlet must be greater than zero."
546- P_0 = P_outlet + ρ_l * g * L
523+ # --- Constants ---
524+ g = const .g # m/s^2, Gravitational acceleration
525+ R = const .R # J/(mol·K), Universal gas constant
526+ N_A = const .N_A # 1/mol, Avogadro's number
527+ M_LiPb = 2.875e-25 # Kg/molecule, Lipb molecular mass
528+
529+ # Calculate empirical correlations
530+ ρ_l = 10.45e3 * (1 - 1.61e-4 * T ) # kg/m^3, Liquid (LiPb) density
531+ σ_l = 0.52 - 0.11e-3 * T # N/m, Surface tension, liquid (LiPb) - gas (He) interface
532+ μ_l = 1.87e-4 * np .exp (11640 / (R * T )) # Pa.s, Dynamic viscosity of liquid LiPb
533+ ν_l = μ_l / ρ_l # m^2/s, Kinematic viscosity of liquid LiPb
534+ D_T = 2.5e-7 * np .exp (
535+ - 27000 / (R * T )
536+ ) # m^2/s, Tritium diffusion coefficient in liquid LiPb
537+
538+ K_s = 2.32e-8 * np .exp (
539+ - 1350 / (R * T )
540+ ) # atfrac*Pa^0.5, Sievert's constant for tritium in liquid LiPb
541+ K_s = K_s * (ρ_l / (M_LiPb * N_A )) # mol/(m^3·Pa^0.5)
547542
548- # Calculate the superficial flow velocities
549543 A = np .pi * (D / 2 ) ** 2 # m^2, Cross-sectional area of the column
550- u_g0 = Q_g / A # m/s, superficial gas inlet velocity
544+
545+ # Calculate the volumetric flow rates
546+ Q_l = flow_l / ρ_l # m^3/s, Volumetric flow rate of liquid phase
547+ Q_g = u_g0 * A # m^3/s, Volumetric flow rate of gas phase
548+
549+ # Calculate the superficial flow velocities
550+ # u_g0 = Q_g / A # m/s, superficial gas inlet velocity
551551 u_l = Q_l / A # m/s, superficial liquid inlet velocity
552552
553- # Calculate dimensionless values
553+ # Calculate Bond, Galilei, Schmidt and Froude numbers
554+ Bn = (g * D ** 2 * ρ_l ) / σ_l # Bond number
555+ Ga = (g * D ** 3 ) / ν_l ** 2 # Galilei number
556+ Sc = ν_l / D_T # Schmidt number
557+ Fr = u_g0 / (g * D ) ** 0.5 # Froude number
558+
559+ # Calculate dispersion coefficients
560+ E_l = (D * u_g0 ) / (
561+ (13 * Fr ) / (1 + 6.5 * (Fr ** 0.8 ))
562+ ) # m^2/s, Effective axial dispersion coefficient, liquid phase
563+ E_g = (
564+ 0.2 * D ** 2
565+ ) * u_g0 # m^2/s, Effective axial dispersion coefficient, gas phase
566+
567+ # Calculate gas hold-up (phase fraction) & mass transfer coefficient
568+ C = 0.2 * (Bn ** (1 / 8 )) * (Ga ** (1 / 12 )) * Fr # C = ε_g / (1 - ε_g)^4
569+
570+ def solveEqn (ε_g , C ):
571+ # Define the equation to solve
572+ eqn = ε_g / (1 - ε_g ) ** 4 - C
573+ return eqn
574+
575+ ε_g_initial_guess = 0.1
576+ try :
577+ # bracket=[0.0001, 0.9999] tells it to *only* look in this range
578+ sol = root_scalar (solveEqn , args = (C ,), bracket = [0.00001 , 0.99999 ])
579+
580+ # print(f"--- Using root_scalar (robust method) ---")
581+ # print(f"C value was: {C}")
582+ # if sol.converged:
583+ # print(f"Solved gas hold-up (εg): {sol.root:.6f}")
584+ # # Verify it
585+ # verification = sol.root / (1 - sol.root)**4
586+ # print(f"Verification (should equal C): {verification:.6f}")
587+ # else:
588+ # print("Solver did not converge.")
589+
590+ except ValueError as e :
591+ print (
592+ f"Solver failed. This can happen if C is so large that no solution exists between 0 and 1."
593+ )
594+ print (f"Error: { e } " )
595+
596+ ε_g = sol .root # Gas phase fraction
554597 ε_l = 1 - ε_g # Liquid phase fraction
555- ψ = (ρ_l * g * ε_l * L ) / P_0 # Hydrostatic pressure ratio (Eq. 8.3)
556- ν = (c_T_inlet / K_s ) ** 2 / P_0 # Tritium partial pressure ratio (Eq. 8.5)
557- Bo_l = u_l * L / (ε_l * E_l ) # Bodenstein number, liquid phase (Eq. 8.9)
558- phi_l = a * h_l * L / u_l # Transfer units parameter, liquid phase (Eq. 8.11)
559- Bo_g = u_g0 * L / (ε_g * E_g ) # Bodenstein number, gas phase (Eq. 8.10)
560- phi_g = (
561- 0.5 * (const .R * T * c_T_inlet / P_0 ) * (a * h_l * L / u_g0 )
562- ) # Transfer units parameter, gas phase (Eq. 8.12)
598+
599+ # Calculate outlet pressure hydrostatically & check non-negative
600+ P_outlet = P_0 - (ρ_l * (1 - ε_g ) * g * L )
601+
602+ if P_outlet <= 0 :
603+ raise ValueError (
604+ f"Calculated gas outlet pressure P_outlet must be positive, but got { P_outlet :.2e} Pa. Check P_0, rho_l, g, and L are realistic."
605+ )
606+
607+ # Calculate interfacial area
608+ d_b = (
609+ 26 * (Bn ** - 0.5 ) * (Ga ** - 0.12 ) * (Fr ** - 0.12 )
610+ ) * D # m, Mean bubble diameter AGREES WITH PAPER
611+ a = 6 * ε_g / d_b # m^-1, Specific interfacial area, assuming spherical bubbles
612+
613+ # Calculate volumetric mass transfer coefficient, liquid-gas
614+ h_l_a = (
615+ D_T * (0.6 * Sc ** 0.5 * Bn ** 0.62 * Ga ** 0.31 * ε_g ** 1.1 ) / (D ** 2 )
616+ ) # Volumetric mass transfer coefficient, liquid-gas
617+
618+ h_l = h_l_a / a # Mass transfer coefficient
619+
620+ # Calculate dimensionless values
621+
622+ # Hydrostatic pressure ratio (Eq. 8.3) # MATCHES PAPER
623+ psi = (ρ_l * g * (1 - ε_g ) * L ) / P_0
624+ # Tritium partial pressure ratio (Eq. 8.5) # MATCHES PAPER
625+ nu = ((c_T_inlet / K_s ) ** 2 ) / P_0
626+ # Bodenstein number, liquid phase (Eq. 8.9) # MATCHES PAPER
627+ Bo_l = u_l * L / (ε_l * E_l )
628+ # Transfer units parameter, liquid phase (Eq. 8.11) # MATCHES PAPER
629+ phi_l = a * h_l * L / u_l
630+ # Bodenstein number, gas phase (Eq. 8.10) # MATCHES PAPER ASSUMING u_g0
631+ Bo_g = u_g0 * L / (ε_g * E_g )
632+ # Transfer units parameter, gas phase (Eq. 8.12) # MATCHES PAPER
633+ phi_g = 0.5 * (R * T * c_T_inlet / P_0 ) * (a * h_l * L / u_g0 )
563634
564635 dimensionless_params = {
565636 "Bo_l" : Bo_l ,
566637 "phi_l" : phi_l ,
567638 "Bo_g" : Bo_g ,
568639 "phi_g" : phi_g ,
569- "psi" : ψ ,
570- "nu" : ν ,
640+ "psi" : psi ,
641+ "nu" : nu ,
571642 }
572643
573644 # Solve the model
@@ -638,20 +709,10 @@ class GLC(pathsim.blocks.Function):
638709 More details about the model can be found in: https://doi.org/10.13182/FST95-A30485
639710
640711 Args:
641- P_outlet: Outlet operating pressure [Pa]
712+ P_0: Inlet operating pressure [Pa]
642713 L: Column height [m]
643- u_l: Superficial liquid velocity [m/s]
644714 u_g0: Superficial gas inlet velocity [m/s]
645- ε_g: Gas phase fraction [-]
646- ε_l: Liquid phase fraction [-]
647- E_g: Gas phase axial dispersion coefficient [m^2/s]
648- E_l: Liquid phase axial dispersion coefficient [m^2/s]
649- a: Specific interfacial area [m^2/m^3]
650- h_l: Liquid phase volumetric mass transfer coefficient [m/s]
651- ρ_l: Liquid density [kg/m^3]
652- K_s: Tritium solubility constant [mol/(m^3·Pa)^0.5]
653715 Q_l: Liquid volumetric flow rate [m^3/s]
654- Q_g: Gas volumetric flow rate [m^3/s]
655716 D: Column diameter [m]
656717 T: Temperature [K]
657718 g: Gravitational acceleration [m/s^2], default is 9.81
@@ -662,47 +723,27 @@ class GLC(pathsim.blocks.Function):
662723 "y_T2_in" : 1 ,
663724 }
664725 _port_map_out = {
665- "T_out_liquid " : 0 ,
666- "T_out_gas " : 1 ,
726+ "c_T_outlet " : 0 ,
727+ "P_T2_out_gas " : 1 ,
667728 "efficiency" : 2 ,
668729 }
669730
670731 def __init__ (
671732 self ,
672- P_outlet ,
733+ P_0 ,
673734 L ,
674- u_l ,
675735 u_g0 ,
676- ε_g ,
677- ε_l ,
678- E_g ,
679- E_l ,
680- a ,
681- h_l ,
682- ρ_l ,
683- K_s ,
684- Q_l ,
685- Q_g ,
736+ flow_l ,
686737 D ,
687738 T ,
688- g = 9.81 ,
739+ g = const . g ,
689740 initial_nb_of_elements = 20 ,
690741 ):
691742 self .params = {
692- "P_outlet " : P_outlet ,
743+ "P_0 " : P_0 ,
693744 "L" : L ,
694- "u_l" : u_l ,
695745 "u_g0" : u_g0 ,
696- "ε_g" : ε_g ,
697- "ε_l" : ε_l ,
698- "E_g" : E_g ,
699- "E_l" : E_l ,
700- "a" : a ,
701- "h_l" : h_l ,
702- "ρ_l" : ρ_l ,
703- "K_s" : K_s ,
704- "Q_l" : Q_l ,
705- "Q_g" : Q_g ,
746+ "flow_l" : flow_l ,
706747 "g" : g ,
707748 "D" : D ,
708749 "T" : T ,
@@ -724,4 +765,4 @@ def func(self, c_T_inlet, y_T2_inlet):
724765 n_T_out_gas = res ["tritium_out_gas [mol/s]" ]
725766 eff = res ["extraction_efficiency [%]" ]
726767
727- return n_T_out_liquid , n_T_out_gas , eff
768+ return c_T_outlet , P_T2_outlet , eff
0 commit comments