def plot_condensation_curve(AR_min, AR_max, p_c, fp, T_ref): gamma = fp.get_specific_heat_ratio(T=T_ref, p=p_c) # [-] Specific heat ratio # Determine maximum Mach number at exit M_exit_max = IRT.Mach_from_area_ratio( AR=AR_max, gamma=gamma) # [-] Max. Mach number at exit M_exit_min = IRT.Mach_from_area_ratio( AR=AR_min, gamma=gamma) # [-] Min. Mach number at exit M_exit = np.linspace(start=M_exit_min, stop=M_exit_max, num=50) # [-] Range of exit Mach numbers to evalulate # Determine pressure at exit PR_exit = IRT.pressure_ratio(M=M_exit, gamma=gamma) # [-] Pressure ratio at exit p_exit = p_c / PR_exit # [-] Exit pressure # Determine saturation temperature at exit T_sat_exit = fp.get_saturation_temperature( p=p_exit) # [K] Saturation temperature at exit # print(p_exit) # print(T_sat_exit) TR_exit = IRT.temperature_ratio( M=M_exit, gamma=gamma) # [-] Temperature ratio at exit T_chamber = T_sat_exit * TR_exit # [K] Chamber temperature that would result precisely in saturation temperature at nozzle exit AR = IRT.area_ratio( M=M_exit, gamma=gamma ) # [-] Area ratios corresponding to all specified mach numbers plt.plot( AR, T_chamber, label="$p_c ={:2.0f}$ bar , $T_c={:3.0f}$ K, $\\gamma={:1.3f}$".format( p_c * 1e-5, T_ref, gamma))
def test_TRP_example(self): # Slide 57-51 from Ideal Rocket Motor lecture from Thermal Rocket Propulsion course are used as example (Zandbergen) F_expected = 401.8e3 u_exit_expected = 3021 m_dot_expected = 132.9 gamma = 1.3 R = 390.4 p_chamber = 5.6e6 p_back = 6.03e3 T_chamber = 3400 AR_exit = 49 d_exit = 1.6 A_exit = 0.25 * math.pi * d_exit**2 A_throat = A_exit / AR_exit NS = "isentropically expanded (margin: 0.01)" res = IRT.get_engine_performance(p_chamber=p_chamber, T_chamber=T_chamber, A_throat=A_throat, AR_exit=AR_exit, p_back=p_back, gamma=gamma, R=R) self.assertAlmostEqual(res['thrust'], F_expected, -3) self.assertAlmostEqual(res['m_dot'], m_dot_expected, 0) self.assertAlmostEqual(res['u_exit'], u_exit_expected, -1) self.assertEqual(res['nozzle_status'], NS)
def test_simple_input(self): # Expected exit velocity and mass flow # Should be sonic throat too, due to vacuum at exit u_exit_expected = 1.0801234497346432 m_dot_expected = 0.6847314563772704 # Should be equal to Gamma(1.4) (vandankerkhove function) p_exit = 0.5282817877171743 # Should be exit pressure for M=1 AR_exit = 1 T_chamber = 1 R = 1 gamma = 1.4 p_chamber = 1 A_throat = 1 AR_exit = 1 p_back = 0 # Expected thrust F_expected = m_dot_expected * u_exit_expected + p_exit * A_throat * AR_exit res = IRT.thrust(p_chamber=p_chamber, T_chamber=T_chamber, A_throat=A_throat, AR_exit=AR_exit, p_back=p_back, gamma=gamma, R=R) self.assertEqual(res, F_expected)
def test_simple_input(self): u_exit_expected = 1.0801234497346432 m_dot_expected = 0.6847314563772704 # Should be equal to Gamma(1.4) (vandankerkhove function) p_exit = 0.5282817877171743 # Should be exit pressure for M=1 AR_exit = 1 T_chamber = 1 R = 1 gamma = 1.4 p_chamber = 1 A_throat = 1 AR_exit = 1 p_back = 0 # Expected thrust F_expected = m_dot_expected * u_exit_expected + p_exit * A_throat * AR_exit # Since the throat is M=1 and the back pressure is vacuum, it is clearly underexpanded NS = "underexpanded" res = IRT.get_engine_performance(p_chamber=p_chamber, T_chamber=T_chamber, A_throat=A_throat, AR_exit=AR_exit, p_back=p_back, gamma=gamma, R=R) self.assertEqual(res['thrust'], F_expected) self.assertEqual(res['m_dot'], m_dot_expected) self.assertEqual(res['u_exit'], u_exit_expected) self.assertEqual(res['nozzle_status'], NS)
def test_table_from_TRP(self): # Page 51 from the TRP reader has a table with in and outputs. It is used to verify the function here in_out = ((1.05, 0.6177), (1.10, 0.6284), (1.12, 0.6325), (1.19, 0.6466), (1.23, 0.6543), (1.31, 0.6691), (1.38, 0.6813), (1.65, 0.7238)) for input, output in in_out: res = IRT.vdk(input) self.assertEqual(round(res, 4), output)
def test_Anderson_example(self): # Example 10.5 from Anderson2016 (recalculated manually due to different formula, calculation approach) expected_result = 586.1 # [kg/s] res = IRT.mass_flow(p_chamber=3.03e6, A_throat=0.4, R=520, T_chamber=3500, gamma=1.22) self.assertEqual(round(res, 1), expected_result)
def testAndersonTable(self): # Test values from Appendix B from Anderson gamma = 1.4 in_out = ((0.102e1, 0.1047e1, 3), (0.158e1, 0.2746e1, 3), (0.345e1, 0.1372e2, 2), (0.61e1, 0.4324e2, 2), (0.36e2, 0.1512e4, 0)) for M, PR, places in in_out: res = IRT.pressure_ratio_shockwave(M=M, gamma=gamma) self.assertAlmostEqual(res, PR, places)
def test_TRP_example(self): # Slide 57-51 from Ideal Rocket Motor lecture from Thermal Rocket Propulsion course are used as example (Zandbergen) expected = 3021 gamma = 1.3 R = 390.4 T_chamber = 3400 AR_exit = 49 res = IRT.exit_velocity(AR_exit=AR_exit, T_chamber=T_chamber, R=R, gamma=gamma) self.assertAlmostEqual( res, expected, -1) # 4 digits accuracy (-1, rounding difference)
def testAndersonTable(self): # Appendix A and B from Anderson2016 # For this case the pressure ratios of two tables must be multiplied. Accuracy unknown after multiplication # Lets guess 1 digit loss is fine gamma = 1.4 in_out = ( (0.1760e1, 0.8458e1, 0.4736e1, 3), # M = 2.05 (0.1395e2, 0.2247e3, 0.2140e2, 2) # M = 4.3 ) for AR, PR_exit, PR_shockwave, places in in_out: res = IRT.exit_pressure_ratio_shockwave(exit_area_ratio=AR, gamma=gamma) self.assertAlmostEqual(res, PR_exit / PR_shockwave, places)
def plot_pressure_curve(AR_min, AR_max, p_c, fp, T_ref): gamma = fp.get_specific_heat_ratio(T=T_ref, p=p_c) # [-] Specific heat ratio # Determine maximum Mach number at exit M_exit_max = IRT.Mach_from_area_ratio( AR=AR_max, gamma=gamma) # [-] Max. Mach number at exit M_exit_min = IRT.Mach_from_area_ratio( AR=AR_min, gamma=gamma) # [-] Min. Mach number at exit M_exit = np.linspace(start=M_exit_min, stop=M_exit_max, num=50) # [-] Range of exit Mach numbers to evalulate # Determine pressure at exit PR_exit = IRT.pressure_ratio(M=M_exit, gamma=gamma) # [-] Pressure ratio at exit p_exit = p_c / PR_exit # [-] Exit pressure AR = IRT.area_ratio( M=M_exit, gamma=gamma ) # [-] Area ratios corresponding to all specified mach numbers plt.plot(AR, p_exit * 1e-5, label="{:2.0f} bar , $\\gamma={:1.2f}$".format(p_c * 1e-5, gamma))
def testAndersonTable(self): # use Anderson2016 table Appendix for gamma-1.4 in order to verify function workings in_out = ( (0.2894e2, 0.2e-1, False, 5), (0.2708e1, 0.22e0, False, 4), (0.1213e1, 0.58e0, False, 4), #(0.1000e1, 0.98e0, False, 2), # Function is extremely insensitive to results close to M=1 #(0.1000e1, 0.102e1, True, 3), (0.1126e1, 0.1420e1, True, 3), (0.2763e1, 0.2550e1, True, 3), (0.3272e3, 0.9e1, True, 3), (0.1538e5, 0.2e2, True, 2)) for AR, expected_M, supersonic, M_places in in_out: res_M = IRT.Mach_from_area_ratio(AR=AR, gamma=1.4, supersonic=supersonic) self.assertAlmostEqual(res_M, expected_M, places=M_places)
def testAndersonTable(self): # Using table from Anderon to test for different back pressures # Taking line for M=0.5 first for subsonic test p_chamber = 1.186e6 exit_area_ratio = 1.340 gamma = 1.4 p_exit = 1e6 res = IRT.nozzle_status(p_chamber=p_chamber, p_back=p_exit, AR_exit=exit_area_ratio, gamma=gamma) self.assertEqual(res, 'subsonic') p_chamber = 1.54e6 # Slight below value for which it will not have a shock in the nozzle res = IRT.nozzle_status(p_chamber=p_chamber, p_back=p_exit, AR_exit=exit_area_ratio, gamma=gamma) self.assertEqual(res, 'shock in nozzle') # Now test the same but with slightly higher pressure to see if it returns underexpanded p_chamber = 1.55e6 # Slight above value for which it will not have a shock in the nozzle res = IRT.nozzle_status(p_chamber=p_chamber, p_back=p_exit, AR_exit=exit_area_ratio, gamma=gamma) self.assertEqual(res, 'overexpanded') # Now isentropic expansion (tolerance is set 0.01) # Area ratio must be updated to a value present in Anderson table for M>1 # M = 2.1 is selected exit_area_ratio = 1.837 p_chamber = 9.145e6 res = IRT.nozzle_status(p_chamber=p_chamber, p_back=p_exit, AR_exit=exit_area_ratio, gamma=gamma) self.assertEqual(res, 'isentropically expanded (margin: 0.01)') # Same but with exit pressure slightly below margin set, so it is overexpanded p_exit = 0.9899e6 res = IRT.nozzle_status(p_chamber=p_chamber, p_back=p_exit, AR_exit=exit_area_ratio, gamma=gamma) self.assertEqual(res, 'underexpanded') # Same but slightly higher so it is underexpanded p_exit = 1.01e6 res = IRT.nozzle_status(p_chamber=p_chamber, p_back=p_exit, AR_exit=exit_area_ratio, gamma=gamma) self.assertEqual(res, 'overexpanded')
def test_TRP_example(self): # Slide 57-51 from Ideal Rocket Motor lecture from Thermal Rocket Propulsion course are used as example (Zandbergen) expected = 401.8e3 gamma = 1.3 R = 390.4 p_chamber = 5.6e6 p_back = 6.03e3 T_chamber = 3400 AR_exit = 49 d_exit = 1.6 A_exit = 0.25 * math.pi * d_exit**2 A_throat = A_exit / AR_exit res = IRT.thrust(p_chamber=p_chamber, T_chamber=T_chamber, A_throat=A_throat, AR_exit=AR_exit, p_back=p_back, gamma=gamma, R=R) self.assertAlmostEqual( expected, res, -3) # Slight rounding error again and 1 digit accuracy less
def testOne(self): # Mach is 1 input should return 1 expected = 1 res = IRT.pressure_ratio_shockwave(M=1, gamma=1.4) self.assertEqual(res, expected)
fp = FluidProperties(td['propellant']) # Object to access fluid properties with p_c = 5e5#td['p_inlet'] # [bar] Chamber pressure T_c = 600#td['T_chamber_guess'] # [K] Chamber temperature h_channel = td['h_channel'] # [m] Channel/nozzle depth w_throat = td['w_throat'] # [m] Throat width AR_exit = 10 #td['AR_exit'] # [-] Exit area ratio p_back = 0# td['p_back'] # [Pa] Atmospheric pressire print("Chamber temperature: {:3.2f} K".format(T_c)) # Calculate throat area, and propellant properties A_throat = h_channel*w_throat # [m^2] Thrpat area gamma = fp.get_specific_heat_ratio(T=T_c, p=p_c) # [-] Get gamma at specified gas constant R = fp.get_specific_gas_constant() # [J/(kg*K)] Specific gas constant ep = IRT.get_engine_performance(p_chamber=p_c, T_chamber=T_c, A_throat=A_throat, AR_exit=AR_exit, p_back=p_back, gamma=gamma, R=R) print("\n --- IRT predictions --- ") print("Isp: {:3.2f} s".format(ep['Isp'])) print("Gamma: {:1.3f} ".format(gamma)) print("Mass flow: {:2.3f} mg/s".format(ep['m_dot']*1e6)) print("Thrust: {:2.3f} mN".format(ep['thrust']*1e3)) zeta_CF = td['F']/ep['thrust'] Isp_real = td['F']/td['m_dot']/g0 zeta_Isp = Isp_real/ep['Isp'] discharge_factor = td['m_dot']/ep['m_dot'] print('\n --- Experimental values ---') print("Isp: {:3.2f} s (zeta_Isp = {:1.3f})".format(Isp_real, zeta_Isp)) print("Mass flow: {:2.3f} mg/s (Cd = {:1.3f})".format(td['m_dot']*1e6, discharge_factor)) print("Thrust: {:2.3f} mN (zeta_CF ={:1.3f})".format(td['F'], zeta_CF))
def run(F_desired, T_chamber, channel_amount, settings, x_guess): """ Algorithm to determine the thruster with the best specific impulse for the given thrust and power requirements Args: F_desired (N): Required thrust T_chamber (K): Chamber temperature determines mass flow and Isp and ideal power consumption channel_amount (-): Amount of channels. Technically, this must be optmized, but using built-in optimization really doesn't work well for integers In settings {dict}: FDP (dict): Fixed design parameters bounds (dict of 2-tuples): The constraints on the design parameters to be optimized NR (dict of functions): Nusselt relations that are used in various phases in the channel (liquid, two-phase, dry-out,gas) PDR (dict of functions): Pressure drop relations that are used in various phases in the channel (friction: liquid, two-phase, gas; contraction) steps (dict of ints): Fidelity of the temperature-steps in the various sections of the channel """ print("\n ----- OPTIMIZING THRUSTER FOR POWER CONSUMPTION -----") print("\n HIGH-LEVEL INPUTS") print("--- Desired thrust: {:3.1f} mN".format(F_desired * 1e3)) print("--- Chamber temperature: {:4.1f} K".format(T_chamber)) ## First determine ideal engine performance, and load settings for that T_inlet = settings['FDP'][ 'T_inlet'] # [K] Inlet temperature (at heating channel inlet manifold) p_inlet = settings['FDP'][ 'p_inlet'] # [Pa] Chamber pressure assumed equal to inlet pressure AR_exit = settings['FDP']['AR_exit'] # [-] Exit area ratio of nozzle p_back = settings['FDP']['p_back'] # [Pa] assert p_back == 0 # Nozzle correction does not work for back pressure fp = settings['FDP'][ 'fp'] # [object] FluidProperties object containing thermodynamic parameters for propellant print("--- Inlet pressure: {:2.1f} bar".format(p_inlet * 1e-5)) print("--- Exit area ratio {:2.1f}".format(AR_exit)) # Check if the inputs are correct assert (T_chamber > T_inlet) ep_ideal = IRT.engine_performance_from_F_and_T(F_desired=F_desired, p_chamber=p_inlet, T_chamber=T_chamber, AR_exit=AR_exit, p_back=p_back, fp=fp) # Calculate ideal power consumption m_dot = ep_ideal['m_dot'] # [kg/s] Mass flow through chamber Isp = ep_ideal['Isp'] # [s] Specific impulse P_ideal = chamber.ideal_power_consumption( # [W] Ideal power consumption mass_flow=m_dot, T_inlet=T_inlet, p_inlet=p_inlet, T_outlet=T_chamber, p_outlet=p_inlet, fp=fp) print("\n IDEAL PERFORMANCE") print("--- Nozzle status: {}".format(ep_ideal['nozzle_status'])) print("--- Mass flow: {:3.2f} mg/s".format(m_dot * 1e6)) print("--- Isp: {:3.1f} s".format(Isp)) print("--- Power consumption: {:2.1f} W".format(P_ideal)) # Calculate pre-calculated values of heating channel that are constant through entire optimization prepared_values = oneD.full_homogenous_preparation( T_inlet=T_inlet, # [K] Inlet temperature of the channel T_outlet= T_chamber, # [K] Outlet of the channel is equal to chamber temperature in IRT m_dot=m_dot / channel_amount, # [kg/s] Mass flow in a single channel p_ref= p_inlet, # [Pa] The pressure in the channel is assumed to be constant for heat flow calculation purposes steps_l=settings['steps'] ['steps_l'], # [-] Fidelity of simulation in liquid section of channel steps_tp=settings['steps'] ['steps_tp'], # [-] Fidelity of simulation in two-phase section of channel steps_g=settings['steps'] ['steps_g'], # [-] Fidelity of simulation in gas section of channel fp= fp, # [object] Object to access thermodynamic parameters of propellant with ) ### Define function to optimize here, and load settings into fixed design parameters ### IMPORTANT NOTE: if the order of arguments of f() is changed, the order of bounds must be changed too. # The bounds are defined a bit early here, so one does not forgot this after changing either. b = Bounds( [ # Lower bounds settings['bounds']['w_channel'][0], settings['bounds']['w_channel_spacing'][0], settings['bounds']['T_wall_superheat'][0] + T_chamber, ], [ # Higher bounds settings['bounds']['w_channel'][1], settings['bounds']['w_channel_spacing'][1], settings['bounds']['T_wall_superheat'][1] + T_chamber, ]) # Initial guess (just make it the higher bound (arbitary)) NOTE: SAME ORDER APPLIES ABOUNDS AND AS f() below x0 = None # Set the first guess based on a guess being provided or not if (x_guess is None): x0 = [ settings['bounds']['w_channel'][0], settings['bounds']['w_channel_spacing'][0], settings['bounds']['T_wall_superheat'][0] + T_chamber, ] else: x0 = x_guess # The function to optimize def f(x, return_full_results=False ): # NOTE: change bounds above if argument change if (return_full_results): print("Going to return full results.") w_channel = x[0] w_channel_spacing = x[1] T_wall = x[2] # Wall arguments need to be encapsulated in a dictionary so they can be conveniently passed to the section of code calcuating wall effects # and the actual bottom wall temperature wall_args = { 'kappa_wall': settings['FDP'] ['kappa_wall'], # [W/(m*K)] Thermal conductivity of chip wall 'h_channel': settings['FDP']['h_channel'], # [m] Channel depth, channel height 'w_channel': w_channel, # [m] Channel width 'w_channel_spacing': w_channel_spacing, # [m] Spacing between channels/wall thickness 'emissivity_chip_bottom': settings['FDP'] ['emissivity_chip_bottom'], # [m] Emissivity of the bottom of the chip } # Exit manifold length is zero based on existing VLM design, so should be zero in settings assert settings['FDP'][ 'l_exit_manifold'] == 0 # It is merely a remnant from previous code choices # The function that returns the eventual power output res_P = optim_P_total( # CAPITALIZED COMMENTS ARE FOR OPTIMIZED VARIABLES channel_amount=channel_amount, # [-] AMOUNT OF CHANNELS w_channel=w_channel, # [m] CHANNEL WIDTH h_channel=settings['FDP'] ['h_channel'], # [m] Channel depth, channel height inlet_manifold_length_factor=settings['FDP'] ['inlet_manifold_length_factor'], # [-] Linear factor to determine inlet manifold length inlet_manifold_width_factor=settings['FDP'] ['inlet_manifold_width_factor'], # [-] Linear factor to determine inlet manifold width l_exit_manifold=settings['FDP'] ['l_exit_manifold'], # [-] NOTE: old part of design. Is set to zero. w_channel_spacing= w_channel_spacing, # [m] SPACING BETWEEN CHANNELS/WALL THICKNESS w_outer_margin=settings['FDP'] ['w_outer_margin'], # [m] Margin around edge of heating channels, or nozzle width T_wall=T_wall, # [K] WALL TEMPERATURE p_ref=p_inlet, # [Pa] Pressure through channel m_dot=m_dot, # [kg/s] Heat flow through channel prepared_values= prepared_values, # {dict} Dictionary with many parameters that are costly to calcuate but are cosntant throughout optimization Nusselt_relations=settings[ 'Nusselt_relations'], # {dict} Heat transfer relations used for optimization pressure_drop_relations=settings[ 'pressure_drop_relations'], # {dict} Pressure drops used for optimization convergent_half_angle=settings['FDP'] ['convergent_half_angle'], # [rad] Half-angle of convergent part of 2D-conical nozzle divergent_half_angle=settings['FDP'] ['divergent_half_angle'], # [rad] Half-angle of divergent part of 2D-conical nozzle F_desired= F_desired, # [N] Desired thrust. Still required to correct for pressure drop p_back= p_back, # [Pa] Back pressure. NOTE: Nozzle correction depends on this being 0. AR_exit= AR_exit, # [-] Exit area ratio to correct nozzle after pressure drop emissivity_top=settings['FDP'] ['emissivity_chip_top'], # [-] Emissivity of top chip (black-body-radiation) fp= fp, # [object] Object to access thermodynamic parameters of propellant with wall_args= wall_args, # {dict} Arguments about wall design. See aboves ) # Return the total power consumption. The variable of interest P_total = P_ideal + res_P['P_loss'] #print(x) if math.isnan(P_total): #print("A constraint violated.") # Punish the objective function by outputting high power consumptions for invalid solutions # The punishment must however not be constant, or it will converge at the boundary where it is invalid. # Since the punishment stems from the pressure drop being higher than the initial p_chamber value, the lower the negative final pressure is the higher the punishment if return_full_results: print("Converged on invalid result!") print(res_P['pressure_drop_punishment']) return (P_ideal * (2 + 1 * res_P['pressure_drop_punishment']) ) * settings['function_scaling'] else: #print("P_total: {:2.5f} W".format(P_total)) # Scipy minimize can't handle multiple returns for the objective function, so the extensive final results must be pulled out afterwards if return_full_results: print("Returning full results!") print(res_P['P_loss']) return res_P else: return P_total * settings['function_scaling'] ## OPTIMIZATION ALGORITHM # First bounds must be set in the same order that arguments are given in f() # The order is: channel_amount, w_channel,w_channel_spacing,T_wall optimization_method = 'L-BFGS-B' optimization_options = { 'ftol': 2.220446049250313e-20, 'gtol': 1e-10, 'disp': False, } # Pressure drop constraint con_pressure_drop = NonlinearConstraint(f, 0, 2 * P_ideal) minimize_results = minimize(fun=f, x0=x0, bounds=b, method=optimization_method, options=optimization_options) # print(minimize_results.x) print(minimize_results.success) print(minimize_results.status) print(minimize_results.message) print(minimize_results.nit) print("Trying to return full results") res_final = f(minimize_results.x, return_full_results=True) print(res_final['P_loss']) optim_results = { #'full_res': res_final['res'], #'full_prepared_values': res_final['prepared_values'], 'minimize_results': minimize_results, 'w_channel': minimize_results.x[0], 'w_channel_spacing': minimize_results.x[1], 'T_wall_top': minimize_results.x[2], 'P_total': minimize_results.fun / settings['function_scaling'], 'P_loss': res_final['P_loss'], 'P_ideal': P_ideal, 'l_channel': res_final['l_channel'], 'h_channel': res_final['h_channel'], 'hydraulic_diameter': res_final['hydraulic_diameter'], 'l_inlet': res_final['l_inlet'], 'l_total': res_final['l_total'], 'A_chip': res_final['A_chip'], 'l_outlet': res_final['l_outlet'], 'l_convergent': res_final['l_convergent'], 'l_divergent': res_final['l_divergent'], 'l_channel_l': res_final['l_channel_l'], 'l_channel_tp': res_final['l_channel_tp'], 'l_channel_g': res_final['l_channel_g'], 'T_wall_bottom': res_final['T_wall_bottom'], 'w_total': res_final['w_total'], 'w_channels_total': res_final['w_channels_total'], 'w_nozzle': res_final['w_nozzle'], 'w_inlet': res_final['w_inlet'], 'pressure_drop': res_final['pressure_drop'], 'Re_channel_l': res_final['Re_channel_l'], 'Re_channel_tp': res_final['Re_channel_tp'], 'Re_channel_g': res_final['Re_channel_g'], 'M_channel_exit_after_dP': res_final['M_channel_exit_after_dP'], 'Re_channel_exit_after_dP': res_final['Re_channel_exit_after_dP'], 'm_dot': ep_ideal['m_dot'], 'Isp': ep_ideal['Isp'], 'p_inlet': p_inlet, 'w_throat_new': res_final['w_throat_new'], 'Re_throat_new': res_final['Re_throat_new'], } #print(optim_results) return optim_results
def test_zero_two(self): expected_result = 0.769800358919501 res = IRT.mass_flow(p_chamber=1, A_throat=1, R=1, T_chamber=1, gamma=2) self.assertEqual(res, expected_result)
def test_simple_input(self): expected = 1.0801234497346432 # Manually calculated # area ratio of 1 means the mach number is 1 res = IRT.exit_velocity(AR_exit=1, T_chamber=1, R=1, gamma=1.4) self.assertEqual(res, expected)
def testAreaRatioOne(self): expected_result = 1.0 input = 1 res = IRT.Mach_from_area_ratio(AR=input, gamma=1.4) self.assertEqual(res, expected_result)
def Rajeev_complete(p_chamber, T_chamber, w_throat, h_throat, throat_roc, AR_exit, p_back, divergence_half_angle, fp: FluidProperties, is_cold_flow): """ Function that implements all corrections proposed by Makhan2018 Args: p_chamber (Pa): Chamber pressure T_chamber (K): Chamber temperature w_throat (m): Throat width h_throat (m): Throat heigh (or channel depth) throat_roc (m): Throat radius of curvature AR_exit (-): Area ratio of nozzle exit area divided by throat area p_back (Pa): Back pressure divergence_half_angle (rad): Divergence half angle of nozzle fp (FluidProperties): Object to access fluid properties is_cold_flow (bool): Reynolds number is adjusted depending on whether the chamber is heated or cooled Raises: ValueError: Is raised for hot flow, since no verification is done yet on that equation """ # Get the (assumed to be) constant fluid properties gamma = fp.get_specific_heat_ratio(T=T_chamber, p=p_chamber) # [-] Specific heat ratio R = fp.get_specific_gas_constant() # [J/kg] Specific gas constant # Report calculated values for verification and comparison purposes print("Gamma: {:1.4f}".format(gamma)) print("R: {:3.2f} J/kg\n".format(R)) # Calculate basic peformance parameters A_throat = w_throat * h_throat # [m] Throat area ## IDEAL PERFORMANCE # First get ideal performance, and check if the nozzle is properly expanded. ep = IRT.get_engine_performance(p_chamber=p_chamber, T_chamber=T_chamber, A_throat=A_throat, AR_exit=AR_exit, p_back=p_back, gamma=gamma, R=R) # Report ideal performance print("Thrust: {:.2f} mN".format(ep['thrust'] * 1e3)) print("Isp_ideal: {:.1f} s".format(ep['thrust'] / ep['m_dot'] / 9.80655)) print("Mass flow: {:.3f} mg/s".format(ep['m_dot'] * 1e6)) m_dot_ideal = ep['m_dot'] # [kg/s] Ideal mass flow #F_ideal = ep['thrust'] # [N] Ideal thrust ## CALCULATING THE CORRECTION FACTORS # Calculate the divergence loss and report it CF_divergence_loss = divergence_loss_conical_2D( alpha=divergence_half_angle) print("\n -- DIVERGENCE LOSS for {:2.2f} deg divergence half-angle".format( math.degrees(divergence_half_angle))) print( " Divergence loss (2D concical): {:.5f} ".format(CF_divergence_loss)) # Calculate the viscous loss # To determine the Reynolds number at the throat, the hydraulic diameter at the throat and nozzle conditions must be determined # Get hydraulic diameter of the nozzle from the wetted perimeter and nozzle area wetted_perimeter_throat = 2 * (w_throat + h_throat ) # [m] Wetted perimeter throat Dh_throat = hydraulic_diameter(A=A_throat, wetted_perimeter=wetted_perimeter_throat ) # [m] Hydraulic diameter at throat p_throat = p_chamber / IRT.pressure_ratio( M=1, gamma=gamma) # [Pa] pressure in throat T_throat = T_chamber / IRT.temperature_ratio( M=1, gamma=gamma) # [K] Temperature in throat viscosity_throat = fp.get_viscosity(T=T_throat, p=p_throat) # Throat reynolds based on ideal mass flow? Re_throat = reynolds(m_dot=m_dot_ideal, A=A_throat, D_hydraulic=Dh_throat, viscosity=viscosity_throat) if is_cold_flow: Re_throat_wall = Reynolds_throat_wall_cold(reynolds_throat=Re_throat) else: Re_throat_wall = Reynolds_throat_wall_hot(reynolds_throat=Re_throat) print("\n-- THROAT CONDITIONS --") print(" p = {:2.4f} bar, T = {:4.2f} K".format( p_throat * 1e-5, T_throat)) print(" mu = {:2.4f} [microPa*s] Dh = {:3.4f} [microm]".format( viscosity_throat * 1e6, Dh_throat * 1e6)) print(" Reynolds: {:6.6f} ".format(Re_throat)) CF_viscous_loss = viscous_loss(area_ratio=AR_exit, reynolds_throat_wall=Re_throat_wall) print(" CF_viscous_loss: {:1.5f}".format(CF_viscous_loss)) # Calculating throat boundary layer loss, which causes a reduction in effective throat area/mass flow Cd_throat_boundary_loss = throat_boundary_loss(gamma=gamma, reynolds_throat=Re_throat, throat_radius=0.5 * Dh_throat, throat_roc=throat_roc) print("\n-- DISCHARGE FACTOR --") print(" Throat boundary layer: {:1.4f}".format(Cd_throat_boundary_loss)) ## APPLYING THE CORRECTION FACTORS # Now all these loss factors must be combined into a new "real" thrust # The divergence loss only applies to the jet/momentum thrust and not the pressure, so jet thrust is needed # This is equal to the exit velocity times corrected mass flow. The returned exit velocity does not include pressure terms! # First we must know the corrected mass flow m_dot_real = ep['m_dot'] * Cd_throat_boundary_loss # [kg/s] # Secondly, we must know the pressure thrust to add to the jet thrust again F_pressure = IRT.pressure_thrust(p_chamber=p_chamber, p_back=p_back, A_throat=A_throat, AR=AR_exit, gamma=gamma) F_divergence = m_dot_real * ep[ 'u_exit'] * CF_divergence_loss + F_pressure # [N] Thrust decreased by divergence loss, pressure term must be added again, since divergence only applies to jet thrust # This jet thrust is then again corrected by viscous losses, which are subtracted from the current CF CF_jet_divergence = F_divergence / ( p_chamber * A_throat ) # [-] Thrust coefficient after taking into account discharge factor and divergence loss CF_real_final = CF_jet_divergence - CF_viscous_loss # [-] The final thrust coefficient, also taking into account viscous loss F_real = CF_real_final * p_chamber * A_throat # [N] Real thrust, after taking into account of all the three proposed correction factors # Report "real" results print("\n === CORRECTED PERFORMANCE PARAMETERS === ") print(" Real mass flow: {:3.4f} mg/s".format(m_dot_real * 1e6)) print(" CF with divergence loss {:1.5f}".format(CF_jet_divergence)) print(" Real CF: {:1.5f}".format(CF_real_final)) print(" Real thrust: {:2.4f} mN".format(F_real * 1e3)) print(" Real Isp: {:3.2f}".format(F_real / m_dot_real / 9.80655)) return { 'm_dot_real': m_dot_real, 'm_dot_ideal': ep['m_dot'], 'F_real': CF_real_final }
def testOne(self): # Should return pressure ratio for M=1, as shockwave is infinitely weak # Value taken from Anderson2006 Appendix A again expected = 0.1893e1 res = IRT.exit_pressure_ratio_shockwave(exit_area_ratio=1, gamma=1.4) self.assertAlmostEqual(res, expected, 3)
def test_one(self): # Mach number is 1 in the throat so area ratio must be 1. expected_result = 1 res = IRT.area_ratio(M=1, gamma=1.4) self.assertEqual(res, expected_result)
mu_throat = np.zeros_like(m_dot) # [Pa*s] Dynamic viscosity in the throat # Results to check condensation in nozzle exit T_exit = np.zeros_like(m_dot) # [K] p_exit = np.zeros_like(m_dot) # [Pa] it_AR = np.nditer(AR_exit, flags=['c_index']) # Iterate over all possible combinations of area ratio and chamber temperature to find the mass flow and area ratio that finds according to basic IRT for AR in it_AR: it_T = np.nditer(T_chamber, flags=['c_index']) for T in it_T: # Store mass flow, throat area and other results ep = IRT.engine_performance_from_F_and_T(F_desired=F, p_chamber=p_chamber, T_chamber=float(T), AR_exit=float(AR), p_back=p_back, fp=fp) m_dot[it_AR.index][it_T.index] = ep['m_dot'] # [kg/s] Mass flow A_throat[it_AR.index][it_T.index] = ep['A_throat'] # [m^2] Throat area Isp[it_AR.index][it_T.index] = ep['Isp'] # [s] Specific impulse h_chamber[it_AR.index][it_T.index] = fp.get_enthalpy( T=float(T), p=p_chamber) # [J/kg] Enthalpy at chamber inlet # Throat stuff T_throat[it_AR.index][it_T.index] = ep[ 'T_throat'] # [K] Throat temperature p_throat[it_AR.index][it_T.index] = ep[ 'p_throat'] # [Pa] Throat pressure u_throat[it_AR.index][it_T.index] = ep[ 'u_throat'] # [K] Throat velocity Pr_throat[it_AR.index][it_T.index] = fp.get_Prandtl(