def pressure_integral(T1, P1, dH): r'''Method to compute an integral of the pressure differential of an elevation difference with a base elevation defined by temperature `T1` and pressure `P1`. This is similar to subtracting the pressures at two different elevations, except it allows for local conditions (temperature and pressure) to be taken into account. This is useful for e.x. evaluating the pressure difference between the top and bottom of a natural draft cooling tower. Parameters ---------- T1 : float Temperature at the lower elevation condition, [K] P1 : float Pressure at the lower elevation condition, [Pa] dH : float Elevation difference for which to evaluate the pressure difference, [m] Returns ------- delta_P : float Pressure difference between the elevations, [Pa] ''' # Compute the elevation to obtain the pressure specified H_ref = brenth(H_for_P_ATMOSPHERE_1976_err, -610.0, 86000.0, args=(P1,)) # Compute the temperature delta dT = T1 - ATMOSPHERE_1976(H_ref).T return quad(to_int_dP_ATMOSPHERE_1976, H_ref, H_ref+dH, args=(dT,))[0]
def pressure_integral(T1, P1, dH): r'''Method to compute an integral of the pressure differential of an elevation difference with a base elevation defined by temperature `T1` and pressure `P1`. This is similar to subtracting the pressures at two different elevations, except it allows for local conditions (temperature and pressure) to be taken into account. This is useful for e.x. evaluating the pressure difference between the top and bottom of a natural draft cooling tower. Parameters ---------- T1 : float Temperature at the lower elevation condition, [K] P1 : float Pressure at the lower elevation condition, [Pa] dH : float Elevation difference for which to evaluate the pressure difference, [m] Returns ------- delta_P : float Pressure difference between the elevations, [Pa] ''' # Compute the elevation to obtain the pressure specified def to_solve(H): return ATMOSPHERE_1976(H).P - P1 H_ref = brenth(to_solve, -610.0, 86000) # Compute the temperature delta dT = T1 - ATMOSPHERE_1976(H_ref).T def to_int(Z): atm = ATMOSPHERE_1976(Z, dT=dT) return atm.g * atm.rho from scipy.integrate import quad return float(quad(to_int, H_ref, H_ref + dH)[0])
def pressure_integral(T1, P1, dH): r'''Method to compute an integral of the pressure differential of an elevation difference with a base elevation defined by temperature `T1` and pressure `P1`. This is similar to subtracting the pressures at two different elevations, except it allows for local conditions (temperature and pressure) to be taken into account. This is useful for e.x. evaluating the pressure difference between the top and bottom of a natural draft cooling tower. Parameters ---------- T1 : float Temperature at the lower elevation condition, [K] P1 : float Pressure at the lower elevation condition, [Pa] dH : float Elevation difference for which to evaluate the pressure difference, [m] Returns ------- delta_P : float Pressure difference between the elevations, [Pa] ''' # Compute the elevation to obtain the pressure specified def to_solve(H): return ATMOSPHERE_1976(H).P - P1 H_ref = brenth(to_solve, -610.0, 86000) # Compute the temperature delta dT = T1 - ATMOSPHERE_1976(H_ref).T def to_int(Z): atm = ATMOSPHERE_1976(Z, dT=dT) return atm.g*atm.rho from scipy.integrate import quad return float(quad(to_int, H_ref, H_ref+dH)[0])
def liquid_jet_pump_ancillary(rhop, rhos, Kp, Ks, d_nozzle=None, d_mixing=None, Qp=None, Qs=None, P1=None, P2=None): r'''Calculates the remaining variable in a liquid jet pump when solving for one if the inlet variables only and the rest of them are known. The equation comes from conservation of energy and momentum in the mixing chamber. The variable to be solved for must be one of `d_nozzle`, `d_mixing`, `Qp`, `Qs`, `P1`, or `P2`. .. math:: P_1 - P_2 = \frac{1}{2}\rho_pV_n^2(1+K_p) - \frac{1}{2}\rho_s V_3^2(1+K_s) Rearrange to express V3 in terms of Vn, and using the density ratio `C`, the expression becomes: .. math:: P_1 - P_2 = \frac{1}{2}\rho_p V_n^2\left[(1+K_p) - C(1+K_s) \left(\frac{MR}{1-R}\right)^2\right] Using the primary nozzle area and flow rate: .. math:: P_1 - P_2 = \frac{1}{2}\rho_p \left(\frac{Q_p}{A_n}\right)^2 \left[(1+K_p) - C(1+K_s) \left(\frac{MR}{1-R}\right)^2\right] For `P`, `P2`, `Qs`, and `Qp`, the equation can be rearranged explicitly for them. For `d_mixing` and `d_nozzle`, a bounded solver is used searching between 1E-9 m and 20 times the other diameter which was specified. Parameters ---------- rhop : float The density of the primary (motive) fluid, [kg/m^3] rhos : float The density of the secondary fluid (drawn from the vacuum chamber), [kg/m^3] Kp : float The primary nozzle loss coefficient, [-] Ks : float The secondary inlet loss coefficient, [-] d_nozzle : float, optional The inside diameter of the primary fluid's nozle, [m] d_mixing : float, optional The diameter of the mixing chamber, [m] Qp : float, optional The volumetric flow rate of the primary fluid, [m^3/s] Qs : float, optional The volumetric flow rate of the secondary fluid, [m^3/s] P1 : float, optional The pressure of the primary fluid entering its nozzle, [Pa] P2 : float, optional The pressure of the secondary fluid at the entry of the ejector, [Pa] Returns ------- solution : float The parameter not specified (one of `d_nozzle`, `d_mixing`, `Qp`, `Qs`, `P1`, or `P2`), (units of `m`, `m`, `m^3/s`, `m^3/s`, `Pa`, or `Pa` respectively) Notes ----- The following SymPy code was used to obtain the analytical formulas ( they are not shown here due to their length): >>> from sympy import * >>> A_nozzle, A_mixing, Qs, Qp, P1, P2, rhos, rhop, Ks, Kp = symbols('A_nozzle, A_mixing, Qs, Qp, P1, P2, rhos, rhop, Ks, Kp') >>> R = A_nozzle/A_mixing >>> M = Qs/Qp >>> C = rhos/rhop >>> rhs = rhop/2*(Qp/A_nozzle)**2*((1+Kp) - C*(1 + Ks)*((M*R)/(1-R))**2 ) >>> new = Eq(P1 - P2, rhs) >>> #solve(new, Qp) >>> #solve(new, Qs) >>> #solve(new, P1) >>> #solve(new, P2) Examples -------- Calculating primary fluid nozzle inlet pressure P1: >>> liquid_jet_pump_ancillary(rhop=998., rhos=1098., Ks=0.11, Kp=.04, ... P2=133600, Qp=0.01, Qs=0.01, d_mixing=0.045, d_nozzle=0.02238) 426434.60314398084 References ---------- .. [1] Ejectors and Jet Pumps. Design and Performance for Incompressible Liquid Flow. 85032. ESDU International PLC, 1985. ''' unknowns = sum(i is None for i in (d_nozzle, d_mixing, Qs, Qp, P1, P2)) if unknowns > 1: raise ValueError('Too many unknowns') elif unknowns < 1: raise ValueError('Overspecified') C = rhos / rhop if Qp is not None and Qs is not None: M = Qs / Qp if d_nozzle is not None: A_nozzle = pi / 4 * d_nozzle * d_nozzle if d_mixing is not None: A_mixing = pi / 4 * d_mixing * d_mixing R = A_nozzle / A_mixing if P1 is None: return rhop / 2 * (Qp / A_nozzle)**2 * ((1 + Kp) - C * (1 + Ks) * ((M * R) / (1 - R))**2) + P2 elif P2 is None: return -rhop / 2 * (Qp / A_nozzle)**2 * ((1 + Kp) - C * (1 + Ks) * ((M * R) / (1 - R))**2) + P1 elif Qs is None: try: return sqrt( (-2 * A_nozzle**2 * P1 + 2 * A_nozzle**2 * P2 + Kp * Qp**2 * rhop + Qp**2 * rhop) / (C * rhop * (Ks + 1))) * (A_mixing - A_nozzle) / A_nozzle except ValueError: return -1j elif Qp is None: return A_nozzle * sqrt( (2 * A_mixing**2 * P1 - 2 * A_mixing**2 * P2 - 4 * A_mixing * A_nozzle * P1 + 4 * A_mixing * A_nozzle * P2 + 2 * A_nozzle**2 * P1 - 2 * A_nozzle**2 * P2 + C * Ks * Qs**2 * rhop + C * Qs**2 * rhop) / (rhop * (Kp + 1))) / (A_mixing - A_nozzle) elif d_nozzle is None: def err(d_nozzle): return P1 - liquid_jet_pump_ancillary(rhop=rhop, rhos=rhos, Kp=Kp, Ks=Ks, d_nozzle=d_nozzle, d_mixing=d_mixing, Qp=Qp, Qs=Qs, P1=None, P2=P2) return brenth(err, 1E-9, d_mixing * 20) elif d_mixing is None: def err(d_mixing): return P1 - liquid_jet_pump_ancillary(rhop=rhop, rhos=rhos, Kp=Kp, Ks=Ks, d_nozzle=d_nozzle, d_mixing=d_mixing, Qp=Qp, Qs=Qs, P1=None, P2=P2) try: return brenth(err, 1E-9, d_nozzle * 20) except: return secant(err, d_nozzle * 2)
def flash_ideal(zs, funcs, Tcs=None, T=None, P=None, VF=None): r'''PVT flash model using ideal, composition-independent equation. Solves the various cases of composition-independent models. Capable of solving with two of `T`, `P`, and `VF` for the other one; that results in three solve modes, but for `VF=1` and `VF=0`, there are additional solvers; for a total of seven solvers implemented. The function takes a list of callables that take `T` in Kelvin as an argument, and return vapor pressure. The callables can include the effect of non-ideal pure component fugacity coefficients. For the (`T`, `P`) and (`P`, `VF`) cases, the Poynting correction factor can be easily included as well but not the (`T`, `VF`) case as the callable only takes `T` as an argument. Normally the Poynting correction factor is used with activity coefficient models with composition dependence. Both `flash_wilson` and `flash_Tb_Tc_Pc` are specialized cases of this function and have the same functionality but with the model built right in. Even when using more complicated models, this is useful for obtaining initial This model uses `flash_inner_loop` to solve the Rachford-Rice problem. Parameters ---------- zs : list[float] Mole fractions of the phase being flashed, [-] funcs : list[Callable] Functions to calculate ideal or real vapor pressures, take temperature in Kelvin and return pressure in Pa, [-] Tcs : list[float], optional Critical temperatures of all species; uses as upper bounds and only for the case that `T` is not specified; if they are needed and not given, it is assumed a method `solve_prop` exists in each of `funcs` which will accept `P` in Pa and return temperature in `K`, [K] T : float, optional Temperature, [K] P : float, optional Pressure, [Pa] VF : float, optional Molar vapor fraction, [-] Returns ------- T : float Temperature, [K] P : float Pressure, [Pa] VF : float Molar vapor fraction, [-] xs : list[float] Mole fractions of liquid phase, [-] ys : list[float] Mole fractions of vapor phase, [-] Notes ----- For the cases where `VF` is 1 or 0 and T is known, an explicit solution is used. For the same cases where `P` and `VF` are known, there is no explicit solution available. There is an internal `Tmax` parameter, set to 50000 K; which, in the event of convergence of the Secant method, is used as a bounded for a bounded solver. It is used in the PVF solvers. Examples -------- Basic case with four compounds, usingthe Antoine equation as a model and solving for vapor pressure: >>> from chemicals import Antoine, Ambrose_Walton >>> Tcs = [369.83, 425.12, 469.7, 507.6] >>> Antoine_As = [8.92828, 8.93266, 8.97786, 9.00139] >>> Antoine_Bs = [803.997, 935.773, 1064.84, 1170.88] >>> Antoine_Cs = [-26.11, -34.361, -41.136, -48.833] >>> Psat_funcs = [] >>> for i in range(4): ... def Psat_func(T, A=Antoine_As[i], B=Antoine_Bs[i], C=Antoine_Cs[i]): ... return Antoine(T, A, B, C) ... Psat_funcs.append(Psat_func) >>> zs = [.4, .3, .2, .1] >>> T, P, VF, xs, ys = flash_ideal(T=330.55, P=1e6, zs=zs, funcs=Psat_funcs, Tcs=Tcs) >>> round(VF, 10) 1.00817e-05 Similar case, using the Ambrose-Walton corresponding states method to estimate vapor pressures: >>> Tcs = [369.83, 425.12, 469.7, 507.6] >>> Pcs = [4248000.0, 3796000.0, 3370000.0, 3025000.0] >>> omegas = [0.152, 0.193, 0.251, 0.2975] >>> Psat_funcs = [] >>> for i in range(4): ... def Psat_func(T, Tc=Tcs[i], Pc=Pcs[i], omega=omegas[i]): ... return Ambrose_Walton(T, Tc, Pc, omega) ... Psat_funcs.append(Psat_func) >>> _, P, VF, xs, ys = flash_ideal(T=329.151, VF=0, zs=zs, funcs=Psat_funcs, Tcs=Tcs) >>> round(P, 3) 1000013.343 Case with fugacities in the liquid phase, vapor phase, activity coefficients in the liquid phase, and Poynting correction factors. >>> Tcs = [647.14, 514.0] >>> Antoine_As = [10.1156, 10.3368] >>> Antoine_Bs = [1687.54, 1648.22] >>> Antoine_Cs = [-42.98, -42.232] >>> gammas = [1.1, .75] >>> fugacities_gas = [.995, 0.98] >>> fugacities_liq = [.9999, .9998] >>> Poyntings = [1.000001, .999999] >>> zs = [.5, .5] >>> funcs = [] >>> for i in range(2): ... def K_over_P(T, A=Antoine_As[i], B=Antoine_Bs[i], C=Antoine_Cs[i], fl=fugacities_liq[i], ... fg=fugacities_gas[i], gamma=gammas[i], poy=Poyntings[i]): ... return Antoine(T, A, B, C)*gamma*poy*fl/fg ... funcs.append(K_over_P) >>> _, _, VF, xs, ys = flash_ideal(zs, funcs, Tcs=Tcs, P=1e5, T=364.0) >>> VF, xs, ys (0.510863971792927, [0.5573493403937615, 0.4426506596062385], [0.4450898279593881, 0.5549101720406119]) Note that while this works for PT composition independent flashes - an outer iterating loop is needed for composition dependence! ''' T_MAX = 50000.0 N = len(zs) cmps = range(N) if T is not None and P is not None: P_inv = 1.0 / P Ks = [0.0] * N for i in cmps: Ks[i] = P_inv * funcs[i](T) ans = (T, P) + flash_inner_loop(zs=zs, Ks=Ks) return ans if T is not None and VF == 0.0: ys = [0.0] * N P_bubble = 0.0 for i in cmps: v = funcs[i](T) * zs[i] P_bubble += v ys[i] = v P_inv = 1.0 / P_bubble for i in cmps: ys[i] *= P_inv return (T, P_bubble, 0.0, zs, ys) if T is not None and VF == 1.0: xs = [0.0] * N P_dew = 0. for i in cmps: v = zs[i] / funcs[i](T) P_dew += v xs[i] = v P_dew = 1. / P_dew for i in cmps: xs[i] *= P_dew return (T, P_dew, 1.0, xs, zs) elif T is not None and VF is not None: # Solve for in the middle of Pdew P_low = flash_ideal(zs, funcs, Tcs, T=T, VF=1)[1] P_high = flash_ideal(zs, funcs, Tcs, T=T, VF=0)[1] info = [] def to_solve(P, info): T_calc, P_calc, VF_calc, xs, ys = flash_ideal(zs, funcs, Tcs, T=T, P=P) info[:] = T_calc, P_calc, VF_calc, xs, ys err = VF_calc - VF return err P = brenth(to_solve, P_low, P_high, args=(info, )) return tuple(info) if Tcs is None: # numba: delete Tcs = [fi.solve_prop(1e6) for fi in funcs] # numba: delete if P is not None and VF == 1: def to_solve(T_guess): T_guess = abs(T_guess) P_dew = 0. for i in cmps: P_dew += zs[i] / funcs[i](T_guess) P_dew = 1. / P_dew return P_dew - P # 2/3 average critical point T_guess = .66666 * sum([Tcs[i] * zs[i] for i in cmps]) try: T_dew = abs(secant(to_solve, T_guess, xtol=1e-12, maxiter=50)) except Exception as e: T_dew = None if T_dew is None or T_dew > T_MAX * 5.0: # Went insanely high T, bound it with brenth T_low_guess = sum([.1 * Tcs[i] * zs[i] for i in cmps]) bound = True try: err_low = to_solve(T_low_guess) except: bound = False try: err_high = to_solve(T_MAX) except: bound = False if bound and err_low * err_high > 0.0: bound = False if bound: T_dew = brenth(to_solve, T_low_guess, T_MAX, fa=err_low, fb=err_high) else: T_dew = secant(to_solve, min(min(Tcs) * 0.9, T_guess), xtol=1e-12, maxiter=50, bisection=True, high=min(Tcs)) xs = [P] * N for i in range(N): xs[i] *= zs[i] / funcs[i](T_dew) return (T_dew, P, 1.0, xs, zs) elif P is not None and VF == 0: def to_solve(T_guess): # T_guess = abs(T_guess) P_bubble = 0.0 for i in cmps: P_bubble += zs[i] * funcs[i](T_guess) return P_bubble - P # 2/3 average critical point T_guess = sum([.55 * Tcs[i] * zs[i] for i in cmps]) try: T_bubble = abs( secant(to_solve, T_guess, maxiter=50, bisection=True, xtol=1e-12)) except: T_bubble = None if T_bubble is None or T_bubble > T_MAX * 5.0: # Went insanely high T, bound it with brenth T_low_guess = sum([.1 * Tcs[i] * zs[i] for i in cmps]) bound = True try: err_low = to_solve(T_low_guess) except: bound = False try: err_high = to_solve(T_MAX) except: bound = False if bound and err_low * err_high > 0.0: bound = False if bound: T_bubble = brenth(to_solve, T_low_guess, T_MAX, fa=err_low, fb=err_high) else: Tc_min = min(Tcs) T_bubble = secant(to_solve, min(Tc_min * 0.9, T_guess), maxiter=50, bisection=True, high=Tc_min, xtol=1e-12) P_inv = 1.0 / P ys = [0.0] * N for i in range(N): ys[i] = zs[i] * P_inv * funcs[i](T_bubble) return (T_bubble, P, 0.0, zs, ys) elif P is not None and VF is not None: bound = True try: T_low = flash_ideal(zs, funcs, Tcs, P=P, VF=1)[0] T_high = flash_ideal(zs, funcs, Tcs, P=P, VF=0)[0] except: bound = False info = [] def err(T, zs, funcs, Tcs, P, VF, info, ignore_err): try: T_calc, P_calc, VF_calc, xs, ys = flash_ideal(zs, funcs, Tcs, T=T, P=P) except: if ignore_err: return -0.5 else: raise ValueError("No solution in inner loop") info[:] = T_calc, P_calc, VF_calc, xs, ys return VF_calc - VF if bound: P = brenth(err, T_low, T_high, xtol=1e-14, args=(zs, funcs, Tcs, P, VF, info, False)) else: T_guess = .5 * sum([Tcs[i] * zs[i] for i in cmps]) Tc_min = min(Tcs) # Starting at the lowest component's Tc should guarantee starting at two phases P = secant(err, Tc_min * (1.0 - 1e-7), xtol=1e-12, high=Tc_min, bisection=True, args=(zs, funcs, Tcs, P, VF, info, True)) return tuple(info) else: raise ValueError("Provide two of P, T, and VF")
def flash_Tb_Tc_Pc(zs, Tbs, Tcs, Pcs, T=None, P=None, VF=None): r'''PVT flash model using a model published in [1]_, which provides a PT surface using only each compound's boiling temperature and critical temperature and pressure. This is useful for obtaining initial guesses for more rigorous models, or it can be used as its own model. Capable of solving with two of `T`, `P`, and `VF` for the other one; that results in three solve modes, but for `VF=1` and `VF=0`, there are additional solvers; for a total of seven solvers implemented. This model uses `flash_inner_loop` to solve the Rachford-Rice problem. .. math:: K_i = \frac{P_{c,i}^{\left(\frac{1}{T} - \frac{1}{T_{b,i}} \right) / \left(\frac{1}{T_{c,i}} - \frac{1}{T_{b,i}} \right)}}{P} Parameters ---------- zs : list[float] Mole fractions of the phase being flashed, [-] Tbs : list[float] Boiling temperatures of all species, [K] Tcs : list[float] Critical temperatures of all species, [K] Pcs : list[float] Critical pressures of all species, [Pa] T : float, optional Temperature, [K] P : float, optional Pressure, [Pa] VF : float, optional Molar vapor fraction, [-] Returns ------- T : float Temperature, [K] P : float Pressure, [Pa] VF : float Molar vapor fraction, [-] xs : list[float] Mole fractions of liquid phase, [-] ys : list[float] Mole fractions of vapor phase, [-] Notes ----- For the cases where `VF` is 1 or 0 and T is known, an explicit solution is used. For the same cases where `P` and `VF` are known, there is no explicit solution available. There is an internal `Tmax` parameter, set to 50000 K; which, in the event of convergence of the Secant method, is used as a bounded for a bounded solver. It is used in the PVF solvers. This typically allows pressures up to 2 MPa to be converged to. Failures may still occur for other conditions. This model is based on [1]_, which aims to estimate dew and bubble points using the same K value formulation as used here. While this implementation uses a numerical solver to provide an exact bubble/dew point estimate, [1]_ suggests a sequential substitution and flowchart based solver with loose tolerances. That model was also implemented, but found to be slower and less reliable than this implementation. Examples -------- >>> Tcs = [305.322, 540.13] >>> Pcs = [4872200.0, 2736000.0] >>> Tbs = [184.55, 371.53] >>> zs = [0.4, 0.6] >>> flash_Tb_Tc_Pc(zs=zs, Tcs=Tcs, Pcs=Pcs, Tbs=Tbs, T=300, P=1e5) (300, 100000.0, 0.38070407481453833, [0.03115784303656836, 0.9688421569634316], [0.9999999998827086, 1.172914188751506e-10]) References ---------- .. [1] Kandula, Vamshi Krishna, John C. Telotte, and F. Carl Knopf. "It’s Not as Easy as It Looks: Revisiting Peng—Robinson Equation of State Convergence Issues for Dew Point, Bubble Point and Flash Calculations." International Journal of Mechanical Engineering Education 41, no. 3 (July 1, 2013): 188-202. https://doi.org/10.7227/IJMEE.41.3.2. ''' T_MAX = 50000 N = len(zs) cmps = range(N) # Assume T and P to begin with if T is not None and P is not None: Ks = [ Pcs[i]**((1.0 / T - 1.0 / Tbs[i]) / (1.0 / Tcs[i] - 1.0 / Tbs[i])) / P for i in cmps ] return (T, P) + flash_inner_loop(zs=zs, Ks=Ks, check=True) if T is not None and VF == 0: P_bubble = 0.0 for i in cmps: P_bubble += zs[i] * Pcs[i]**((1.0 / T - 1.0 / Tbs[i]) / (1.0 / Tcs[i] - 1.0 / Tbs[i])) return flash_Tb_Tc_Pc(zs, Tbs, Tcs, Pcs, T=T, P=P_bubble) if T is not None and VF == 1: # Checked to be working vs. PT implementation. P_dew = 0. for i in cmps: P_dew += zs[i] / (Pcs[i]**((1.0 / T - 1.0 / Tbs[i]) / (1.0 / Tcs[i] - 1.0 / Tbs[i]))) P_dew = 1. / P_dew return flash_Tb_Tc_Pc(zs, Tbs, Tcs, Pcs, T=T, P=P_dew) elif T is not None and VF is not None: # Solve for in the middle of Pdew P_low = flash_Tb_Tc_Pc(zs, Tbs, Tcs, Pcs, T=T, VF=1)[1] P_high = flash_Tb_Tc_Pc(zs, Tbs, Tcs, Pcs, T=T, VF=0)[1] info = [] def err(P): T_calc, P_calc, VF_calc, xs, ys = flash_Tb_Tc_Pc(zs, Tbs, Tcs, Pcs, T=T, P=P) info[:] = T_calc, P_calc, VF_calc, xs, ys return VF_calc - VF P = brenth(err, P_low, P_high) return tuple(info) elif P is not None and VF == 1: checker = oscillation_checker() def to_solve(T_guess): T_guess = abs(T_guess) P_dew = 0. for i in range(len(zs)): P_dew += zs[i] / (Pcs[i]**((1.0 / T_guess - 1.0 / Tbs[i]) / (1.0 / Tcs[i] - 1.0 / Tbs[i]))) P_dew = 1. / P_dew err = P_dew - P if checker(T_guess, err): raise ValueError("Oscillation") # print(T_guess, err) return err Tc_pseudo = sum([Tcs[i] * zs[i] for i in cmps]) T_guess = 0.666 * Tc_pseudo try: T_dew = abs(secant(to_solve, T_guess, maxiter=50, ytol=1e-2)) # , high=Tc_pseudo*3 except: T_dew = None if T_dew is None or T_dew > T_MAX * 5.0: # Went insanely high T, bound it with brenth T_low_guess = sum([.1 * Tcs[i] * zs[i] for i in cmps]) checker = oscillation_checker(both_sides=True, minimum_progress=.05) try: T_dew = brenth(to_solve, T_MAX, T_low_guess) except NotBoundedError: raise Exception( "Bisecting solver could not find a solution between %g K and %g K" % (T_MAX, T_low_guess)) return flash_Tb_Tc_Pc(zs, Tbs, Tcs, Pcs, T=T_dew, P=P) elif P is not None and VF == 0: checker = oscillation_checker() def to_solve(T_guess): T_guess = abs(T_guess) P_bubble = 0.0 for i in cmps: P_bubble += zs[i] * Pcs[i]**((1.0 / T_guess - 1.0 / Tbs[i]) / (1.0 / Tcs[i] - 1.0 / Tbs[i])) err = P_bubble - P if checker(T_guess, err): raise ValueError("Oscillation") # print(T_guess, err) return err # 2/3 average critical point Tc_pseudo = sum([Tcs[i] * zs[i] for i in cmps]) T_guess = 0.55 * Tc_pseudo try: T_bubble = abs(secant(to_solve, T_guess, maxiter=50, ytol=1e-2)) # , high=Tc_pseudo*4 except Exception as e: # print(e) checker = oscillation_checker(both_sides=True, minimum_progress=.05) T_bubble = None if T_bubble is None or T_bubble > T_MAX * 5.0: # Went insanely high T (or could not converge because went too high), bound it with brenth T_low_guess = 0.1 * Tc_pseudo try: T_bubble = brenth(to_solve, T_MAX, T_low_guess) except NotBoundedError: raise Exception( "Bisecting solver could not find a solution between %g K and %g K" % (T_MAX, T_low_guess)) return flash_Tb_Tc_Pc(zs, Tbs, Tcs, Pcs, T=T_bubble, P=P) elif P is not None and VF is not None: T_low = flash_Tb_Tc_Pc(zs, Tbs, Tcs, Pcs, P=P, VF=1)[0] T_high = flash_Tb_Tc_Pc(zs, Tbs, Tcs, Pcs, P=P, VF=0)[0] info = [] def err(T): T_calc, P_calc, VF_calc, xs, ys = flash_Tb_Tc_Pc(zs, Tbs, Tcs, Pcs, T=T, P=P) info[:] = T_calc, P_calc, VF_calc, xs, ys return VF_calc - VF P = brenth(err, T_low, T_high) return tuple(info) else: raise ValueError("Provide two of P, T, and VF")
def liquid_jet_pump_ancillary(rhop, rhos, Kp, Ks, d_nozzle=None, d_mixing=None, Qp=None, Qs=None, P1=None, P2=None): r'''Calculates the remaining variable in a liquid jet pump when solving for one if the inlet variables only and the rest of them are known. The equation comes from conservation of energy and momentum in the mixing chamber. The variable to be solved for must be one of `d_nozzle`, `d_mixing`, `Qp`, `Qs`, `P1`, or `P2`. .. math:: P_1 - P_2 = \frac{1}{2}\rho_pV_n^2(1+K_p) - \frac{1}{2}\rho_s V_3^2(1+K_s) Rearrange to express V3 in terms of Vn, and using the density ratio `C`, the expression becomes: .. math:: P_1 - P_2 = \frac{1}{2}\rho_p V_n^2\left[(1+K_p) - C(1+K_s) \left(\frac{MR}{1-R}\right)^2\right] Using the primary nozzle area and flow rate: .. math:: P_1 - P_2 = \frac{1}{2}\rho_p \left(\frac{Q_p}{A_n}\right)^2 \left[(1+K_p) - C(1+K_s) \left(\frac{MR}{1-R}\right)^2\right] For `P`, `P2`, `Qs`, and `Qp`, the equation can be rearranged explicitly for them. For `d_mixing` and `d_nozzle`, a bounded solver is used searching between 1E-9 m and 20 times the other diameter which was specified. Parameters ---------- rhop : float The density of the primary (motive) fluid, [kg/m^3] rhos : float The density of the secondary fluid (drawn from the vacuum chamber), [kg/m^3] Kp : float The primary nozzle loss coefficient, [-] Ks : float The secondary inlet loss coefficient, [-] d_nozzle : float, optional The inside diameter of the primary fluid's nozle, [m] d_mixing : float, optional The diameter of the mixing chamber, [m] Qp : float, optional The volumetric flow rate of the primary fluid, [m^3/s] Qs : float, optional The volumetric flow rate of the secondary fluid, [m^3/s] P1 : float, optional The pressure of the primary fluid entering its nozzle, [Pa] P2 : float, optional The pressure of the secondary fluid at the entry of the ejector, [Pa] Returns ------- solution : float The parameter not specified (one of `d_nozzle`, `d_mixing`, `Qp`, `Qs`, `P1`, or `P2`), (units of `m`, `m`, `m^3/s`, `m^3/s`, `Pa`, or `Pa` respectively) Notes ----- The following SymPy code was used to obtain the analytical formulas ( they are not shown here due to their length): >>> from sympy import * >>> A_nozzle, A_mixing, Qs, Qp, P1, P2, rhos, rhop, Ks, Kp = symbols('A_nozzle, A_mixing, Qs, Qp, P1, P2, rhos, rhop, Ks, Kp') >>> R = A_nozzle/A_mixing >>> M = Qs/Qp >>> C = rhos/rhop >>> rhs = rhop/2*(Qp/A_nozzle)**2*((1+Kp) - C*(1 + Ks)*((M*R)/(1-R))**2 ) >>> new = Eq(P1 - P2, rhs) >>> #solve(new, Qp) >>> #solve(new, Qs) >>> #solve(new, P1) >>> #solve(new, P2) Examples -------- Calculating primary fluid nozzle inlet pressure P1: >>> liquid_jet_pump_ancillary(rhop=998., rhos=1098., Ks=0.11, Kp=.04, ... P2=133600, Qp=0.01, Qs=0.01, d_mixing=0.045, d_nozzle=0.02238) 426434.60314398084 References ---------- .. [1] Ejectors and Jet Pumps. Design and Performance for Incompressible Liquid Flow. 85032. ESDU International PLC, 1985. ''' unknowns = sum(i is None for i in (d_nozzle, d_mixing, Qs, Qp, P1, P2)) if unknowns > 1: raise Exception('Too many unknowns') elif unknowns < 1: raise Exception('Overspecified') C = rhos/rhop if Qp is not None and Qs is not None: M = Qs/Qp if d_nozzle is not None: A_nozzle = pi/4*d_nozzle*d_nozzle if d_mixing is not None: A_mixing = pi/4*d_mixing*d_mixing R = A_nozzle/A_mixing if P1 is None: return rhop/2*(Qp/A_nozzle)**2*((1+Kp) - C*(1 + Ks)*((M*R)/(1-R))**2 ) + P2 elif P2 is None: return -rhop/2*(Qp/A_nozzle)**2*((1+Kp) - C*(1 + Ks)*((M*R)/(1-R))**2 ) + P1 elif Qs is None: try: return ((-2*A_nozzle**2*P1 + 2*A_nozzle**2*P2 + Kp*Qp**2*rhop + Qp**2*rhop)/(C*rhop*(Ks + 1)))**0.5*(A_mixing - A_nozzle)/A_nozzle except ValueError: return -1j elif Qp is None: return A_nozzle*((2*A_mixing**2*P1 - 2*A_mixing**2*P2 - 4*A_mixing*A_nozzle*P1 + 4*A_mixing*A_nozzle*P2 + 2*A_nozzle**2*P1 - 2*A_nozzle**2*P2 + C*Ks*Qs**2*rhop + C*Qs**2*rhop)/(rhop*(Kp + 1)))**0.5/(A_mixing - A_nozzle) elif d_nozzle is None: def err(d_nozzle): return P1 - liquid_jet_pump_ancillary(rhop=rhop, rhos=rhos, Kp=Kp, Ks=Ks, d_nozzle=d_nozzle, d_mixing=d_mixing, Qp=Qp, Qs=Qs, P1=None, P2=P2) return brenth(err, 1E-9, d_mixing*20) elif d_mixing is None: def err(d_mixing): return P1 - liquid_jet_pump_ancillary(rhop=rhop, rhos=rhos, Kp=Kp, Ks=Ks, d_nozzle=d_nozzle, d_mixing=d_mixing, Qp=Qp, Qs=Qs, P1=None, P2=P2) try: return brenth(err, 1E-9, d_nozzle*20) except: return newton(err, d_nozzle*2)
def volume_solutions_NR_low_P(T, P, b, delta, epsilon, a_alpha): r'''Newton-Raphson based solver for cubic EOS volumes designed specifically for the low-pressure regime. Seeks only two possible solutions - an ideal gas like one, and one near the eos covolume `b` - as the initializations are `R*T/P` and `b*1.000001` . Parameters ---------- T : float Temperature, [K] P : float Pressure, [Pa] b : float Coefficient calculated by EOS-specific method, [m^3/mol] delta : float Coefficient calculated by EOS-specific method, [m^3/mol] epsilon : float Coefficient calculated by EOS-specific method, [m^6/mol^2] a_alpha : float Coefficient calculated by EOS-specific method, [J^2/mol^2/Pa] tries : int, optional Internal parameter as this function will call itself if it needs to; number of previous solve attempts, [-] Returns ------- Vs : tuple[complex] Three possible molar volumes (third one is hardcoded to 1j), [m^3/mol] Notes ----- The algorithm is NR, with some checks that will switch the solver to `brenth` some of the time. ''' P_inv = 1.0/P def err_fun(V): denom1 = 1.0/(V*(V + delta) + epsilon) denom0 = 1.0/(V-b) w0 = R*T*denom0 w1 = a_alpha*denom1 err = w0 - w1 - P return err # failed = False Vs = [R*T/P, b*1.000001] max_err, rel_err = 0.0, 0.0 for i, damping in zip((0, 1), (1.0, 1.0)): V = Vi = Vs[i] err = 0.0 for _ in range(31): denom1 = 1.0/(V*(V + delta) + epsilon) denom0 = 1.0/(V-b) w0 = R*T*denom0 w1 = a_alpha*denom1 if w0 - w1 - P == err: break # No change in error err = w0 - w1 - P derr_dV = (V + V + delta)*w1*denom1 - w0*denom0 if derr_dV != 0.0: V = V - err/derr_dV*damping rel_err = abs(err*P_inv) if rel_err < 1e-14 or V == Vi: # Conditional check probably not worth it break if i == 1 and V > 1.5*b or V < b: # try: # try: try: try: V = brenth(err_fun, b*(1.0+1e-12), b*(1.5), xtol=1e-14) except Exception as e: if a_alpha < 1e-5: V = brenth(err_fun, b*1.5, b*5.0, xtol=1e-14) else: raise e denom1 = 1.0/(V*(V + delta) + epsilon) denom0 = 1.0/(V-b) w0 = R*T*denom0 w1 = a_alpha*denom1 err = w0 - w1 - P derr_dV = (V + V + delta)*w1*denom1 - w0*denom0 V_1NR = V - err/derr_dV*damping if abs((V_1NR-V)/V) < 1e-10: V = V_1NR except: V = 1j if i == 0 and rel_err > 1e-8: V = 1j # failed = True # except: # V = brenth(err_fun, b*(1.0+1e-12), b*(1.5)) # except: # pass # print([T, P, 'fail on brenth low P root']) Vs[i] = V # max_err = max(max_err, rel_err) Vs.append(1j) # if failed: return Vs