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
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(