├── proptools ├── __init__.py ├── units.py ├── constants.py ├── electric │ ├── __init__.py │ ├── electric_test.py │ └── generic.py ├── valve_test.py ├── turbopump_test.py ├── isentropic_test.py ├── isentropic.py ├── tank_structure_test.py ├── valve.py ├── solid_test.py ├── nozzle_test.py ├── solid.py ├── convection_test.py ├── nonsimple_comp_flow.py ├── tank_structure.py ├── turbopump.py ├── convection.py └── nozzle.py ├── docs ├── source │ ├── examples │ │ ├── expansion_ratio_output.txt │ │ ├── exit_velocity_output.txt │ │ ├── electric │ │ │ ├── isp_opt_output.txt │ │ │ ├── power_output.txt │ │ │ ├── thrust_isp_output.txt │ │ │ ├── thrust_isp.py │ │ │ ├── isp_opt.py │ │ │ ├── power.py │ │ │ └── plots │ │ │ │ └── thrust_power_isp.py │ │ ├── expansion_ratio_inverse_output.txt │ │ ├── c_star_ideal_output.txt │ │ ├── choked_output.txt │ │ ├── solid │ │ │ ├── pressure_and_thrust_output.txt │ │ │ ├── pressure_and_thrust.py │ │ │ └── plots │ │ │ │ ├── thrust_curve.py │ │ │ │ └── equilibrium_pressure.py │ │ ├── thrust_isp_output.txt │ │ ├── expansion_ratio.py │ │ ├── c_star_ideal.py │ │ ├── expansion_ratio_inverse.py │ │ ├── plots │ │ │ ├── mach_area.py │ │ │ ├── isentropic_relations.py │ │ │ ├── thrust_pc.py │ │ │ ├── exp_ratio_cf.py │ │ │ ├── thrust_pa.py │ │ │ └── cf_alt.py │ │ ├── run_examples.py │ │ ├── exit_velocity.py │ │ ├── choked.py │ │ ├── thrust_isp.py │ │ └── convection │ │ │ └── plots │ │ │ └── film_cooling.py │ ├── modules.rst │ ├── figures │ │ ├── nozzle_cutaway.jpg │ │ ├── solid │ │ │ ├── example_grain.png │ │ │ ├── thrust_curves.png │ │ │ ├── flame_structure.png │ │ │ └── composite_propellant.png │ │ └── sea_level_vs_vacuum_engine.png │ ├── proptools.solid.rst │ ├── proptools.units.rst │ ├── proptools.nozzle.rst │ ├── proptools.constants.rst │ ├── proptools.electric.rst │ ├── proptools.turbopump.rst │ ├── proptools.isentropic.rst │ ├── proptools.nozzle_test.rst │ ├── proptools.tank_structure.rst │ ├── proptools.turbopump_test.rst │ ├── proptools.isentropic_test.rst │ ├── proptools.nonsimple_comp_flow.rst │ ├── proptools.tank_structure_test.rst │ ├── proptools.rst │ ├── index.rst │ ├── conf.py │ ├── electric_tutorial.rst │ ├── solid_tutorial.rst │ └── nozzle_tutorial.rst ├── Makefile └── make.bat ├── pytest.ini ├── .coveragerc ├── .github └── workflows │ └── lint_and_test.yml ├── .gitignore ├── LICENSE ├── README.md ├── setup.py └── .pylintrc /proptools/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /docs/source/examples/expansion_ratio_output.txt: -------------------------------------------------------------------------------- 1 | Expansion ratio = 11.9 2 | -------------------------------------------------------------------------------- /docs/source/examples/exit_velocity_output.txt: -------------------------------------------------------------------------------- 1 | Exit velocity = 2832 m s**-1 2 | -------------------------------------------------------------------------------- /docs/source/examples/electric/isp_opt_output.txt: -------------------------------------------------------------------------------- 1 | Optimal specific impulse = 1482 s 2 | -------------------------------------------------------------------------------- /docs/source/examples/expansion_ratio_inverse_output.txt: -------------------------------------------------------------------------------- 1 | Exit pressure = 100 kPa 2 | -------------------------------------------------------------------------------- /docs/source/examples/c_star_ideal_output.txt: -------------------------------------------------------------------------------- 1 | Ideal characteristic velocity = 1722 m s**-1 2 | -------------------------------------------------------------------------------- /docs/source/examples/choked_output.txt: -------------------------------------------------------------------------------- 1 | The flow is choked 2 | Mass flow = 45.6 kg s**-1 3 | -------------------------------------------------------------------------------- /docs/source/modules.rst: -------------------------------------------------------------------------------- 1 | proptools 2 | ========= 3 | 4 | .. toctree:: 5 | :maxdepth: 4 6 | 7 | proptools 8 | -------------------------------------------------------------------------------- /docs/source/examples/electric/power_output.txt: -------------------------------------------------------------------------------- 1 | Jet power = 857 W 2 | Total efficiency = 0.703 3 | Input power = 1219 W 4 | -------------------------------------------------------------------------------- /docs/source/examples/solid/pressure_and_thrust_output.txt: -------------------------------------------------------------------------------- 1 | Chamber pressure = 6.9 MPa 2 | Thrust (sea level) = 9.1 kN 3 | -------------------------------------------------------------------------------- /docs/source/examples/thrust_isp_output.txt: -------------------------------------------------------------------------------- 1 | Specific impulse = 288.7 s 2 | Thrust = 129.2 kN 3 | Mass flow = 45.6 kg s**-1 4 | -------------------------------------------------------------------------------- /docs/source/figures/nozzle_cutaway.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mvernacc/proptools/HEAD/docs/source/figures/nozzle_cutaway.jpg -------------------------------------------------------------------------------- /docs/source/examples/electric/thrust_isp_output.txt: -------------------------------------------------------------------------------- 1 | Thrust = 52.2 mN 2 | Specific Impulse = 3336 s 3 | Mass flow = 1.59 mg s^-1 4 | -------------------------------------------------------------------------------- /docs/source/figures/solid/example_grain.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mvernacc/proptools/HEAD/docs/source/figures/solid/example_grain.png -------------------------------------------------------------------------------- /docs/source/figures/solid/thrust_curves.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mvernacc/proptools/HEAD/docs/source/figures/solid/thrust_curves.png -------------------------------------------------------------------------------- /docs/source/figures/solid/flame_structure.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mvernacc/proptools/HEAD/docs/source/figures/solid/flame_structure.png -------------------------------------------------------------------------------- /pytest.ini: -------------------------------------------------------------------------------- 1 | [pytest] 2 | testpaths = 3 | proptools 4 | addopts = 5 | --cov=proptools 6 | --cov-report=term 7 | --cov-fail-under=75 8 | -------------------------------------------------------------------------------- /docs/source/figures/sea_level_vs_vacuum_engine.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mvernacc/proptools/HEAD/docs/source/figures/sea_level_vs_vacuum_engine.png -------------------------------------------------------------------------------- /docs/source/figures/solid/composite_propellant.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mvernacc/proptools/HEAD/docs/source/figures/solid/composite_propellant.png -------------------------------------------------------------------------------- /docs/source/proptools.solid.rst: -------------------------------------------------------------------------------- 1 | proptools.solid module 2 | ====================== 3 | 4 | .. automodule:: proptools.solid 5 | :members: 6 | :undoc-members: 7 | :show-inheritance: 8 | -------------------------------------------------------------------------------- /docs/source/proptools.units.rst: -------------------------------------------------------------------------------- 1 | proptools.units module 2 | ====================== 3 | 4 | .. automodule:: proptools.units 5 | :members: 6 | :undoc-members: 7 | :show-inheritance: 8 | -------------------------------------------------------------------------------- /docs/source/proptools.nozzle.rst: -------------------------------------------------------------------------------- 1 | proptools.nozzle module 2 | ======================= 3 | 4 | .. automodule:: proptools.nozzle 5 | :members: 6 | :undoc-members: 7 | :show-inheritance: 8 | 9 | -------------------------------------------------------------------------------- /docs/source/proptools.constants.rst: -------------------------------------------------------------------------------- 1 | proptools.constants module 2 | ========================== 3 | 4 | .. automodule:: proptools.constants 5 | :members: 6 | :undoc-members: 7 | :show-inheritance: 8 | -------------------------------------------------------------------------------- /docs/source/proptools.electric.rst: -------------------------------------------------------------------------------- 1 | proptools.electric package 2 | ========================== 3 | 4 | .. automodule:: proptools.electric 5 | :members: 6 | :undoc-members: 7 | :show-inheritance: 8 | -------------------------------------------------------------------------------- /docs/source/proptools.turbopump.rst: -------------------------------------------------------------------------------- 1 | proptools.turbopump module 2 | ========================== 3 | 4 | .. automodule:: proptools.turbopump 5 | :members: 6 | :undoc-members: 7 | :show-inheritance: 8 | -------------------------------------------------------------------------------- /docs/source/proptools.isentropic.rst: -------------------------------------------------------------------------------- 1 | proptools.isentropic module 2 | =========================== 3 | 4 | .. automodule:: proptools.isentropic 5 | :members: 6 | :undoc-members: 7 | :show-inheritance: 8 | -------------------------------------------------------------------------------- /docs/source/proptools.nozzle_test.rst: -------------------------------------------------------------------------------- 1 | proptools.nozzle_test module 2 | ============================ 3 | 4 | .. automodule:: proptools.nozzle_test 5 | :members: 6 | :undoc-members: 7 | :show-inheritance: 8 | -------------------------------------------------------------------------------- /docs/source/proptools.tank_structure.rst: -------------------------------------------------------------------------------- 1 | proptools.tank_structure module 2 | =============================== 3 | 4 | .. automodule:: proptools.tank_structure 5 | :members: 6 | :undoc-members: 7 | :show-inheritance: 8 | -------------------------------------------------------------------------------- /docs/source/proptools.turbopump_test.rst: -------------------------------------------------------------------------------- 1 | proptools.turbopump_test module 2 | =============================== 3 | 4 | .. automodule:: proptools.turbopump_test 5 | :members: 6 | :undoc-members: 7 | :show-inheritance: 8 | -------------------------------------------------------------------------------- /docs/source/proptools.isentropic_test.rst: -------------------------------------------------------------------------------- 1 | proptools.isentropic_test module 2 | ================================ 3 | 4 | .. automodule:: proptools.isentropic_test 5 | :members: 6 | :undoc-members: 7 | :show-inheritance: 8 | -------------------------------------------------------------------------------- /docs/source/proptools.nonsimple_comp_flow.rst: -------------------------------------------------------------------------------- 1 | proptools.nonsimple_comp_flow module 2 | ==================================== 3 | 4 | .. automodule:: proptools.nonsimple_comp_flow 5 | :members: 6 | :undoc-members: 7 | :show-inheritance: 8 | -------------------------------------------------------------------------------- /docs/source/proptools.tank_structure_test.rst: -------------------------------------------------------------------------------- 1 | proptools.tank_structure_test module 2 | ==================================== 3 | 4 | .. automodule:: proptools.tank_structure_test 5 | :members: 6 | :undoc-members: 7 | :show-inheritance: 8 | -------------------------------------------------------------------------------- /.coveragerc: -------------------------------------------------------------------------------- 1 | [run] 2 | branch = True 3 | omit = 4 | tests* 5 | */__init__.py 6 | [report] 7 | show_missing = True 8 | # Regexes for lines to exclude from consideration 9 | exclude_lines = 10 | # Have to re-enable the standard pragma 11 | pragma: no cover 12 | 13 | # Don't complain if tests don't hit defensive assertion code: 14 | raise NotImplementedError 15 | -------------------------------------------------------------------------------- /docs/source/examples/expansion_ratio.py: -------------------------------------------------------------------------------- 1 | """Compute the expansion ratio for a given pressure ratio.""" 2 | from proptools import nozzle 3 | 4 | p_c = 10e6 # Chamber pressure [units: pascal] 5 | p_e = 100e3 # Exit pressure [units: pascal] 6 | gamma = 1.2 # Exhaust heat capacity ratio [units: dimensionless] 7 | 8 | # Solve for the expansion ratio [units: dimensionless] 9 | exp_ratio = nozzle.er_from_p(p_c, p_e, gamma) 10 | 11 | print 'Expansion ratio = {:.1f}'.format(exp_ratio) 12 | -------------------------------------------------------------------------------- /docs/source/examples/c_star_ideal.py: -------------------------------------------------------------------------------- 1 | """Ideal characteristic velocity.""" 2 | from proptools import nozzle 3 | 4 | gamma = 1.2 # Exhaust heat capacity ratio [units: dimensionless] 5 | m_molar = 20e-3 # Exhaust molar mass [units: kilogram mole**1] 6 | T_c = 3000. # Chamber temperature [units: kelvin] 7 | 8 | # Compute the characteristic velocity [units: meter second**-1] 9 | c_star = nozzle.c_star(gamma, m_molar, T_c) 10 | 11 | print 'Ideal characteristic velocity = {:.0f} m s**-1'.format(c_star) 12 | -------------------------------------------------------------------------------- /proptools/units.py: -------------------------------------------------------------------------------- 1 | ''' Unit conversions. 2 | ''' 3 | 4 | from __future__ import division 5 | 6 | 7 | def meter2inch(x): 8 | return x / 0.0254 9 | 10 | 11 | def inch2meter(x): 12 | return x * 0.0254 13 | 14 | def psi2pascal(x): 15 | return x * 6895 16 | 17 | def pascal2psi(x): 18 | return x / 6895 19 | 20 | def lbf2newton(x): 21 | return x * 4.448 22 | 23 | def newton2lbf(x): 24 | return x / 4.448 25 | 26 | def lbm2kilogram(x): 27 | return x * 0.4536 28 | 29 | def kilogram2lbm(x): 30 | return x / 0.4536 -------------------------------------------------------------------------------- /docs/source/examples/expansion_ratio_inverse.py: -------------------------------------------------------------------------------- 1 | """Compute the pressure ratio from a given expansion ratio.""" 2 | from scipy.optimize import fsolve 3 | from proptools import nozzle 4 | 5 | p_c = 10e6 # Chamber pressure [units: pascal] 6 | gamma = 1.2 # Exhaust heat capacity ratio [units: dimensionless] 7 | exp_ratio = 11.9 # Expansion ratio [units: dimensionless] 8 | 9 | # Solve for the exit pressure [units: pascal]. 10 | p_e = p_c * nozzle.pressure_from_er(exp_ratio, gamma) 11 | 12 | print 'Exit pressure = {:.0f} kPa'.format(p_e * 1e-3) 13 | -------------------------------------------------------------------------------- /docs/source/examples/electric/thrust_isp.py: -------------------------------------------------------------------------------- 1 | """Example electric propulsion thrust and specific impulse calculation.""" 2 | from proptools import electric, constants 3 | 4 | V_b = 1000. # Beam voltage [units: volt]. 5 | I_b = 1. # Beam current [units: ampere]. 6 | F = electric.thrust(I_b, V_b, electric.m_Xe) 7 | I_sp = electric.specific_impulse(V_b, electric.m_Xe) 8 | m_dot = F / (I_sp * constants.g) 9 | print 'Thrust = {:.1f} mN'.format(F * 1e3) 10 | print 'Specific Impulse = {:.0f} s'.format(I_sp) 11 | print 'Mass flow = {:.2f} mg s^-1'.format(m_dot * 1e6) 12 | -------------------------------------------------------------------------------- /docs/source/examples/electric/isp_opt.py: -------------------------------------------------------------------------------- 1 | """Example calculation of optimal specific impulse for an electrically propelled spacecraft.""" 2 | from proptools import electric 3 | 4 | dv = 2e3 # Delta-v [units: meter second**-1]. 5 | t_m = 100 * 24 * 60 * 60 # Thrust duration [units: second]. 6 | eta_T = 0.7 # Total efficiency [units: dimensionless]. 7 | specific_mass = 50e-3 # Specific mass of poower supply [units: kilogram watt**-1]. 8 | I_sp = electric.optimal_isp_delta_v(dv, eta_T, t_m, specific_mass) 9 | 10 | print 'Optimal specific impulse = {:.0f} s'.format(I_sp) 11 | -------------------------------------------------------------------------------- /docs/source/examples/plots/mach_area.py: -------------------------------------------------------------------------------- 1 | """Plot the Mach-Area relation.""" 2 | import numpy as np 3 | from matplotlib import pyplot as plt 4 | from proptools import nozzle 5 | 6 | M_1 = np.linspace(0.1, 10) # Mach number [units: dimensionless] 7 | gamma = 1.2 # Exhaust heat capacity ratio [units: dimensionless] 8 | 9 | area_ratio = nozzle.area_from_mach(M_1, gamma) 10 | 11 | plt.loglog(M_1, area_ratio) 12 | plt.xlabel('Mach number $M_1$ [-]') 13 | plt.ylabel('Area ratio $A_1 / A_2$ [-]') 14 | plt.title('Mach-Area relation for $M_2 = 1$') 15 | plt.ylim([1, 1e3]) 16 | plt.show() 17 | -------------------------------------------------------------------------------- /docs/source/examples/run_examples.py: -------------------------------------------------------------------------------- 1 | """Run all examples and save their output.""" 2 | 3 | import subprocess 4 | import os 5 | 6 | filenames = [f for f in os.listdir('.') if os.path.isfile(f)] 7 | 8 | for filename in filenames: 9 | if filename == __file__: 10 | # Skip this file 11 | continue 12 | if os.path.splitext(filename)[1] == '.py': 13 | print 'Running ' + filename 14 | output = subprocess.check_output(['python', filename]) 15 | with open(os.path.splitext(filename)[0] + '_output.txt', 'w') as out_file: 16 | out_file.write(output) 17 | -------------------------------------------------------------------------------- /docs/source/examples/exit_velocity.py: -------------------------------------------------------------------------------- 1 | """Estimate exit velocity.""" 2 | from proptools import isentropic 3 | 4 | # Declare engine design parameters 5 | p_c = 10e6 # Chamber pressure [units: pascal] 6 | p_e = 100e3 # Exit pressure [units: pascal] 7 | gamma = 1.2 # Exhaust heat capacity ratio [units: dimensionless] 8 | m_molar = 20e-3 # Exhaust molar mass [units: kilogram mole**1] 9 | T_c = 3000. # Chamber temperature [units: kelvin] 10 | 11 | # Compute the exit velocity 12 | v_e = isentropic.velocity(v_1=0, p_1=p_c, T_1=T_c, p_2=p_e, gamma=gamma, m_molar=m_molar) 13 | 14 | print 'Exit velocity = {:.0f} m s**-1'.format(v_e) 15 | -------------------------------------------------------------------------------- /docs/source/proptools.rst: -------------------------------------------------------------------------------- 1 | proptools package 2 | ================= 3 | 4 | Submodules 5 | ---------- 6 | 7 | .. toctree:: 8 | 9 | proptools.constants 10 | proptools.isentropic 11 | proptools.electric 12 | proptools.isentropic_test 13 | proptools.nonsimple_comp_flow 14 | proptools.nozzle 15 | proptools.nozzle_test 16 | proptools.solid 17 | proptools.tank_structure 18 | proptools.tank_structure_test 19 | proptools.turbopump 20 | proptools.turbopump_test 21 | proptools.units 22 | 23 | Module contents 24 | --------------- 25 | 26 | .. automodule:: proptools 27 | :members: 28 | :undoc-members: 29 | :show-inheritance: 30 | -------------------------------------------------------------------------------- /docs/Makefile: -------------------------------------------------------------------------------- 1 | # Minimal makefile for Sphinx documentation 2 | # 3 | 4 | # You can set these variables from the command line. 5 | SPHINXOPTS = 6 | SPHINXBUILD = sphinx-build 7 | SPHINXPROJ = proptools 8 | SOURCEDIR = source 9 | BUILDDIR = build 10 | 11 | # Put it first so that "make" without argument is like "make help". 12 | help: 13 | @$(SPHINXBUILD) -M help "$(SOURCEDIR)" "$(BUILDDIR)" $(SPHINXOPTS) $(O) 14 | 15 | .PHONY: help Makefile 16 | 17 | # Catch-all target: route all unknown targets to Sphinx using the new 18 | # "make mode" option. $(O) is meant as a shortcut for $(SPHINXOPTS). 19 | %: Makefile 20 | @$(SPHINXBUILD) -M $@ "$(SOURCEDIR)" "$(BUILDDIR)" $(SPHINXOPTS) $(O) -------------------------------------------------------------------------------- /docs/source/examples/plots/isentropic_relations.py: -------------------------------------------------------------------------------- 1 | """Plot isentropic relations.""" 2 | import numpy as np 3 | from matplotlib import pyplot as plt 4 | from proptools import isentropic 5 | 6 | M = np.logspace(-1, 1) 7 | gamma = 1.2 8 | 9 | plt.loglog(M, 1 / isentropic.stag_temperature_ratio(M, gamma), label='$T / T_0$') 10 | plt.loglog(M, 1 / isentropic.stag_pressure_ratio(M, gamma), label='$p / p_0$') 11 | plt.loglog(M, 1 / isentropic.stag_density_ratio(M, gamma), label='$\\rho / \\rho_0$') 12 | plt.xlabel('Mach number [-]') 13 | plt.ylabel('Static / stagnation [-]') 14 | plt.title('Isentropic flow relations for $\gamma={:.2f}$'.format(gamma)) 15 | plt.xlim(0.1, 10) 16 | plt.ylim([1e-3, 1.1]) 17 | plt.axvline(x=1, color='grey', linestyle=':') 18 | plt.legend(loc='lower left') 19 | plt.show() -------------------------------------------------------------------------------- /docs/source/examples/plots/thrust_pc.py: -------------------------------------------------------------------------------- 1 | """Plot thrust vs chmaber pressure.""" 2 | 3 | import numpy as np 4 | from matplotlib import pyplot as plt 5 | from proptools import nozzle 6 | 7 | p_c = np.linspace(1e6, 20e6) # Chamber pressure [units: pascal] 8 | p_e = 100e3 # Exit pressure [units: pascal] 9 | p_a = 100e3 # Ambient pressure [units: pascal] 10 | gamma = 1.2 # Exhaust heat capacity ratio [units: dimensionless] 11 | A_t = np.pi * (0.1 / 2)**2 # Throat area [units: meter**2] 12 | 13 | # Compute thrust [units: newton] 14 | F = nozzle.thrust(A_t, p_c, p_e, gamma) 15 | 16 | plt.plot(p_c * 1e-6, F * 1e-3) 17 | plt.xlabel('Chamber pressure $p_c$ [MPa]') 18 | plt.ylabel('Thrust $F$ [kN]') 19 | plt.title('Thrust vs chamber pressure at $p_e = p_a = {:.0f}$ kPa'.format(p_e * 1e-3)) 20 | plt.show() 21 | -------------------------------------------------------------------------------- /docs/source/examples/electric/power.py: -------------------------------------------------------------------------------- 1 | """Example electric propulsion power calculation.""" 2 | import numpy as np 3 | from proptools import electric 4 | 5 | F = 52.2e-3 # Thrust [units: newton]. 6 | m_dot = 1.59e-6 # Mass flow [units: kilogram second**-1]. 7 | 8 | # Compute the jet power [units: watt]. 9 | P_jet = electric.jet_power(F, m_dot) 10 | 11 | # Compute the total efficiency [units: dimensionless]. 12 | eta_T = electric.total_efficiency( 13 | divergence_correction=np.cos(np.deg2rad(10)), 14 | double_fraction=0.1, 15 | mass_utilization=0.9, 16 | electrical_efficiency=0.85) 17 | 18 | # Compute the input power [units: watt]. 19 | P_in = P_jet / eta_T 20 | 21 | print 'Jet power = {:.0f} W'.format(P_jet) 22 | print 'Total efficiency = {:.3f}'.format(eta_T) 23 | print 'Input power = {:.0f} W'.format(P_in) 24 | -------------------------------------------------------------------------------- /docs/source/examples/choked.py: -------------------------------------------------------------------------------- 1 | """Check that the nozzle is choked and find the mass flow.""" 2 | from math import pi 3 | from proptools import nozzle 4 | 5 | # Declare engine design parameters 6 | p_c = 10e6 # Chamber pressure [units: pascal] 7 | p_e = 100e3 # Exit pressure [units: pascal] 8 | gamma = 1.2 # Exhaust heat capacity ratio [units: dimensionless] 9 | m_molar = 20e-3 # Exhaust molar mass [units: kilogram mole**1] 10 | T_c = 3000. # Chamber temperature [units: kelvin] 11 | A_t = pi * (0.1 / 2)**2 # Throat area [units: meter**2] 12 | 13 | # Check choking 14 | if nozzle.is_choked(p_c, p_e, gamma): 15 | print 'The flow is choked' 16 | 17 | # Compute the mass flow [units: kilogram second**-1] 18 | m_dot = nozzle.mass_flow(A_t, p_c, T_c, gamma, m_molar) 19 | 20 | print 'Mass flow = {:.1f} kg s**-1'.format(m_dot) 21 | -------------------------------------------------------------------------------- /proptools/constants.py: -------------------------------------------------------------------------------- 1 | ''' Physical Constants. 2 | 3 | Matt Vernacchia 4 | proptools 5 | 2016 Apr 15 6 | ''' 7 | 8 | # Universal gas constant [units: joule kelvin**-1 mole**-1]. 9 | R_univ = 8.314 10 | 11 | # Acceleration due to gravity at Earth's surface [units; meter second**-2] 12 | g = 9.81 13 | 14 | # Standard temperature [kelvin] 15 | T_stp = 273.15 16 | 17 | # Elementary charge [units: coulomb]. 18 | charge = 1.602176e-19 19 | 20 | # unified atomic mass unit equivalent in kilograms. 21 | amu_kg = 1.660539e-27 22 | 23 | # One standard cubic foot of gas in moles [units: mole]. 24 | # See https://en.wikipedia.org/wiki/Standard_cubic_foot#Definitions 25 | scf_mole = 1.19804 26 | 27 | # Molar amss of dry air [units: kilogram mole**-1]. 28 | # See https://www.engineeringtoolbox.com/molecular-mass-air-d_679.html 29 | m_molar_air = 28.9647e-3 30 | -------------------------------------------------------------------------------- /proptools/electric/__init__.py: -------------------------------------------------------------------------------- 1 | """Electric propulsion design tools. 2 | 3 | .. autosummary:: 4 | 5 | m_Xe 6 | m_Kr 7 | thrust 8 | jet_power 9 | double_ion_thrust_correction 10 | specific_impulse 11 | total_efficiency 12 | thrust_per_power 13 | stuhlinger_velocity 14 | optimal_isp_thrust_time 15 | optimal_isp_delta_v 16 | """ 17 | 18 | from proptools.constants import amu_kg 19 | 20 | from proptools.electric.generic import ( 21 | thrust, 22 | jet_power, 23 | double_ion_thrust_correction, 24 | specific_impulse, 25 | total_efficiency, 26 | thrust_per_power, 27 | stuhlinger_velocity, 28 | optimal_isp_thrust_time, 29 | optimal_isp_delta_v 30 | ) 31 | 32 | m_Xe = 131.29 * amu_kg 33 | """Xenon atom mass [units: kilogram].""" 34 | 35 | m_Kr = 83.798 * amu_kg 36 | """Krypton atom mass [units: kilogram].""" 37 | -------------------------------------------------------------------------------- /.github/workflows/lint_and_test.yml: -------------------------------------------------------------------------------- 1 | name: Lint and test 2 | 3 | on: 4 | pull_request: [] 5 | 6 | jobs: 7 | lint_and_test: 8 | runs-on: ubuntu-22.04 9 | strategy: 10 | matrix: 11 | python-version: ['3.9', '3.10'] 12 | 13 | steps: 14 | - uses: actions/checkout@v2 15 | - name: Set up Python ${{ matrix.python-version }} 16 | uses: actions/setup-python@v1 17 | with: 18 | python-version: ${{ matrix.python-version }} 19 | - name: Setup 20 | run: | 21 | # Set up pip and a virtual env 22 | cd .. 23 | python -m pip install --upgrade pip==22.3 24 | python -m venv ./env 25 | source ./env/bin/activate 26 | cd proptools 27 | pip install -e ".[test]" 28 | # TODO lint 29 | - name: Test 30 | run: | 31 | source ../env/bin/activate 32 | pytest 33 | -------------------------------------------------------------------------------- /docs/make.bat: -------------------------------------------------------------------------------- 1 | @ECHO OFF 2 | 3 | pushd %~dp0 4 | 5 | REM Command file for Sphinx documentation 6 | 7 | if "%SPHINXBUILD%" == "" ( 8 | set SPHINXBUILD=sphinx-build 9 | ) 10 | set SOURCEDIR=source 11 | set BUILDDIR=build 12 | set SPHINXPROJ=proptools 13 | 14 | if "%1" == "" goto help 15 | 16 | %SPHINXBUILD% >NUL 2>NUL 17 | if errorlevel 9009 ( 18 | echo. 19 | echo.The 'sphinx-build' command was not found. Make sure you have Sphinx 20 | echo.installed, then set the SPHINXBUILD environment variable to point 21 | echo.to the full path of the 'sphinx-build' executable. Alternatively you 22 | echo.may add the Sphinx directory to PATH. 23 | echo. 24 | echo.If you don't have Sphinx installed, grab it from 25 | echo.http://sphinx-doc.org/ 26 | exit /b 1 27 | ) 28 | 29 | %SPHINXBUILD% -M %1 %SOURCEDIR% %BUILDDIR% %SPHINXOPTS% 30 | goto end 31 | 32 | :help 33 | %SPHINXBUILD% -M help %SOURCEDIR% %BUILDDIR% %SPHINXOPTS% 34 | 35 | :end 36 | popd 37 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Byte-compiled / optimized / DLL files 2 | __pycache__/ 3 | *.py[cod] 4 | 5 | # C extensions 6 | *.so 7 | 8 | # Distribution / packaging 9 | .Python 10 | env/ 11 | build/ 12 | develop-eggs/ 13 | dist/ 14 | downloads/ 15 | eggs/ 16 | .eggs/ 17 | lib/ 18 | lib64/ 19 | parts/ 20 | sdist/ 21 | var/ 22 | *.egg-info/ 23 | .installed.cfg 24 | *.egg 25 | 26 | # PyInstaller 27 | # Usually these files are written by a python script from a template 28 | # before PyInstaller builds the exe, so as to inject date/other infos into it. 29 | *.manifest 30 | *.spec 31 | 32 | # Installer logs 33 | pip-log.txt 34 | pip-delete-this-directory.txt 35 | 36 | # Unit test / coverage reports 37 | htmlcov/ 38 | .tox/ 39 | .coverage 40 | .coverage.* 41 | .cache 42 | nosetests.xml 43 | coverage.xml 44 | *,cover 45 | 46 | # Translations 47 | *.mo 48 | *.pot 49 | 50 | # Django stuff: 51 | *.log 52 | 53 | # Sphinx documentation 54 | docs/_build/ 55 | 56 | # PyBuilder 57 | target/ 58 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2017 Matthew Vernacchia 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /docs/source/examples/thrust_isp.py: -------------------------------------------------------------------------------- 1 | """Estimate specific impulse, thrust and mass flow.""" 2 | from math import pi 3 | from proptools import nozzle 4 | 5 | # Declare engine design parameters 6 | p_c = 10e6 # Chamber pressure [units: pascal] 7 | p_e = 100e3 # Exit pressure [units: pascal] 8 | gamma = 1.2 # Exhaust heat capacity ratio [units: dimensionless] 9 | m_molar = 20e-3 # Exhaust molar mass [units: kilogram mole**1] 10 | T_c = 3000. # Chamber temperature [units: kelvin] 11 | A_t = pi * (0.1 / 2)**2 # Throat area [units: meter**2] 12 | 13 | # Predict engine performance 14 | C_f = nozzle.thrust_coef(p_c, p_e, gamma) # Thrust coefficient [units: dimensionless] 15 | c_star = nozzle.c_star(gamma, m_molar, T_c) # Characteristic velocity [units: meter second**-1] 16 | I_sp = C_f * c_star / nozzle.g # Specific impulse [units: second] 17 | F = A_t * p_c * C_f # Thrust [units: newton] 18 | m_dot = A_t * p_c / c_star # Propellant mass flow [units: kilogram second**-1] 19 | 20 | print 'Specific impulse = {:.1f} s'.format(I_sp) 21 | print 'Thrust = {:.1f} kN'.format(F * 1e-3) 22 | print 'Mass flow = {:.1f} kg s**-1'.format(m_dot) 23 | -------------------------------------------------------------------------------- /docs/source/examples/solid/pressure_and_thrust.py: -------------------------------------------------------------------------------- 1 | """Find the chamber pressure and thrust of a solid rocket motor.""" 2 | from proptools import solid, nozzle 3 | 4 | # Propellant properties 5 | gamma = 1.26 # Exhaust gas ratio of specific heats [units: dimensionless]. 6 | rho_solid = 1510. # Solid propellant density [units: kilogram meter**-3]. 7 | n = 0.5 # Propellant burn rate exponent [units: dimensionless]. 8 | a = 2.54e-3 * (6.9e6)**(-n) # Burn rate coefficient, such that the propellant 9 | # burns at 2.54 mm s**-1 at 6.9 MPa [units: meter second**-1 pascal**-n]. 10 | c_star = 1209. # Characteristic velocity [units: meter second**-1]. 11 | 12 | # Motor geometry 13 | A_t = 839e-6 # Throat area [units: meter**2]. 14 | A_b = 1.25 # Burn area [units: meter**2]. 15 | 16 | # Nozzle exit pressure [units: pascal]. 17 | p_e = 101e3 18 | 19 | # Compute the chamber pressure [units: pascal]. 20 | p_c = solid.chamber_pressure(A_b / A_t, a, n, rho_solid, c_star) 21 | 22 | # Compute the sea level thrust [units: newton]. 23 | F = nozzle.thrust(A_t, p_c, p_e, gamma) 24 | 25 | print 'Chamber pressure = {:.1f} MPa'.format(p_c * 1e-6) 26 | print 'Thrust (sea level) = {:.1f} kN'.format(F * 1e-3) 27 | -------------------------------------------------------------------------------- /docs/source/examples/electric/plots/thrust_power_isp.py: -------------------------------------------------------------------------------- 1 | """Plot the thrust, power, Isp trade-off.""" 2 | from matplotlib import pyplot as plt 3 | import numpy as np 4 | from proptools import electric 5 | 6 | eta_T = 0.7 # Total efficiency [units: dimensionless]. 7 | I_sp = np.linspace(1e3, 7e3) # Specific impulse [units: second]. 8 | 9 | ax1 = plt.subplot(111) 10 | for P_in in [2e3, 1e3, 500]: 11 | T = P_in * electric.thrust_per_power(I_sp, eta_T) 12 | plt.plot(I_sp, T * 1e3, label='$P_{{in}} = {:.1f}$ kW'.format(P_in * 1e-3)) 13 | 14 | plt.xlim([1e3, 7e3]) 15 | plt.ylim([0, 290]) 16 | plt.xlabel('Specific impulse [s]') 17 | plt.ylabel('Thrust [mN]') 18 | plt.legend() 19 | plt.suptitle('Thrust, Power and $I_{sp}$ (70% efficient thruster)') 20 | plt.grid(True) 21 | 22 | # Add thrust/power on second axis. 23 | ax2 = ax1.twiny() 24 | new_tick_locations = ax1.get_xticks() 25 | ax2.set_xlim(ax1.get_xlim()) 26 | ax2.set_xticks(new_tick_locations) 27 | ax2.set_xticklabels(['{:.0f}'.format(tp * 1e6) 28 | for tp in electric.thrust_per_power(new_tick_locations, eta_T)]) 29 | ax2.set_xlabel('Thrust/power [$\\mathrm{mN \\, kW^{-1}}$]') 30 | ax2.tick_params(axis='y', direction='in', pad=-25) 31 | plt.subplots_adjust(top=0.8) 32 | 33 | plt.show() 34 | -------------------------------------------------------------------------------- /docs/source/examples/convection/plots/film_cooling.py: -------------------------------------------------------------------------------- 1 | """Film cooting example""" 2 | import numpy as np 3 | from matplotlib import pyplot as plt 4 | 5 | from proptools import convection 6 | 7 | T_aw = 3200 # Adiabatic wall temperature of core flow [units: kelvin]. 8 | T_f = 1600 # Film temperature [units: kelvin]. 9 | T_w = 700 # Wall temperature [units: kelvin]. 10 | x = np.linspace(0, 1) # Distance downstream [units: meter]. 11 | D = 0.5 # Diameter [units: meter]. 12 | m_dot_core = np.pi / 4 * D**2 * 5.33 * 253 # Core mass flow [units: kilogram second**-1]. 13 | m_dot_film = (1./99) * m_dot_core 14 | mu_core = 2e-5 / 0.66 # Dynamic viscosity of the core fluid [units: pascal second]. 15 | Pr_film = 0.8 16 | film_param = 1.265 17 | cp_ratio = 0.8 18 | 19 | eta = np.array([ 20 | convection.film_efficiency(x_, D, m_dot_core, m_dot_film, 21 | mu_core, Pr_film, film_param, cp_ratio) 22 | for x_ in x]) 23 | T_aw_f = convection.film_adiabatic_wall_temperature(eta, T_aw, T_f) 24 | q_ratio = (T_aw_f - T_w) / (T_aw - T_w) 25 | 26 | plt.plot(x, eta, label='film eff. $\eta$') 27 | plt.plot(x, q_ratio, label='heat flux reduction $q_{w,f} / q_{w,0}$') 28 | plt.xlabel('Distane from injection [m]') 29 | plt.ylabel('[-]') 30 | plt.title('Film cooling in a typical liquid rocket engine') 31 | plt.legend() 32 | plt.grid(True) 33 | 34 | plt.show() 35 | -------------------------------------------------------------------------------- /docs/source/examples/plots/exp_ratio_cf.py: -------------------------------------------------------------------------------- 1 | """Effect of expansion ratio on thrust coefficient.""" 2 | import numpy as np 3 | from matplotlib import pyplot as plt 4 | from proptools import nozzle 5 | 6 | p_c = 10e6 # Chamber pressure [units: pascal] 7 | p_a = 100e3 # Ambient pressure [units: pascal] 8 | gamma = 1.2 # Exhaust heat capacity ratio [units: dimensionless] 9 | p_e = np.linspace(0.4 * p_a, 2 * p_a) # Exit pressure [units: pascal] 10 | 11 | # Compute the expansion ratio and thrust coefficient for each p_e 12 | exp_ratio = nozzle.er_from_p(p_c, p_e, gamma) 13 | C_F = nozzle.thrust_coef(p_c, p_e, gamma, p_a=p_a, er=exp_ratio) 14 | 15 | # Compute the matched (p_e = p_a) expansion ratio 16 | exp_ratio_matched = nozzle.er_from_p(p_c, p_a, gamma) 17 | 18 | plt.plot(exp_ratio, C_F) 19 | plt.axvline(x=exp_ratio_matched, color='grey') 20 | plt.annotate('matched $p_e = p_a$, $\epsilon = {:.1f}$'.format(exp_ratio_matched), 21 | xy=(exp_ratio_matched - 0.7, 1.62), 22 | xytext=(exp_ratio_matched - 0.7, 1.62), 23 | color='black', 24 | fontsize=10, 25 | rotation=90 26 | ) 27 | plt.xlabel('Expansion ratio $\\epsilon = A_e / A_t$ [-]') 28 | plt.ylabel('Thrust coefficient $C_F$ [-]') 29 | plt.title('$C_F$ vs expansion ratio at $p_c = {:.0f}$ MPa, $p_a = {:.0f}$ kPa'.format( 30 | p_c *1e-6, p_a * 1e-3)) 31 | plt.show() 32 | -------------------------------------------------------------------------------- /docs/source/index.rst: -------------------------------------------------------------------------------- 1 | proptools: Rocket Propulsion Design Tools 2 | ========================================= 3 | 4 | Proptools is a Python package for preliminary design of rocket propulsion systems. 5 | 6 | Proptools provides implementations of equations for nozzle flow, turbo-machinery and rocket structures. 7 | The project aims to cover most of the commonly used equations in 8 | *Rocket Propulsion Elements* by George Sutton and Oscar Biblarz and 9 | *Modern Engineering for Design of Liquid-Propellant Rocket Engines* by Dieter Huzel and David Huang. 10 | 11 | Proptools can be used as a desktop calculator: 12 | 13 | .. code-block:: python 14 | 15 | >> from proptools import nozzle 16 | >> p_c = 10e6; p_e = 100e3; gamma = 1.2; m_molar = 20e-3; T_c = 3000. 17 | >> C_f = nozzle.thrust_coef(p_c, p_e, gamma) 18 | >> c_star = nozzle.c_star(gamma, m_molar, T_c) 19 | >> I_sp = C_f * c_star / nozzle.g 20 | >> print "The engine's ideal sea level specific impulse is {:.1f} seconds.".format(I_sp) 21 | The engine's ideal sea level specific impulse is 288.7 seconds. 22 | 23 | Proptools can also be used as a library in other propulsion design and analysis software. It is distributed under a `MIT License `_ and can be used in commercial projects. 24 | 25 | Tutorials 26 | ========= 27 | 28 | .. toctree:: 29 | :maxdepth: 2 30 | 31 | nozzle_tutorial 32 | solid_tutorial 33 | electric_tutorial 34 | 35 | 36 | Package contents 37 | ================ 38 | 39 | .. toctree:: 40 | :maxdepth: 1 41 | 42 | modules 43 | 44 | * :ref:`genindex` 45 | 46 | -------------------------------------------------------------------------------- /docs/source/examples/plots/thrust_pa.py: -------------------------------------------------------------------------------- 1 | """Plot thrust vs ambient pressure.""" 2 | import numpy as np 3 | from matplotlib import pyplot as plt 4 | import skaero.atmosphere.coesa as atmo 5 | from proptools import nozzle 6 | 7 | p_c = 10e6 # Chamber pressure [units: pascal] 8 | p_e = 100e3 # Exit pressure [units: pascal] 9 | p_a = np.linspace(0, 100e3) # Ambient pressure [units: pascal] 10 | gamma = 1.2 # Exhaust heat capacity ratio [units: dimensionless] 11 | A_t = np.pi * (0.1 / 2)**2 # Throat area [units: meter**2] 12 | 13 | # Compute thrust [units: newton] 14 | F = nozzle.thrust(A_t, p_c, p_e, gamma, 15 | p_a=p_a, er=nozzle.er_from_p(p_c, p_e, gamma)) 16 | 17 | ax1 = plt.subplot(111) 18 | plt.plot(p_a * 1e-3, F * 1e-3) 19 | plt.xlabel('Ambient pressure $p_a$ [kPa]') 20 | plt.ylabel('Thrust $F$ [kN]') 21 | plt.suptitle('Thrust vs ambient pressure at $p_c = {:.0f}$ MPa, $p_e = {:.0f}$ kPa'.format( 22 | p_c *1e-6, p_e * 1e-3)) 23 | 24 | # Add altitude on second axis 25 | ylim = plt.ylim() 26 | ax2 = ax1.twiny() 27 | new_tick_locations = np.array([100, 75, 50, 25, 1]) 28 | ax2.set_xlim(ax1.get_xlim()) 29 | ax2.set_xticks(new_tick_locations) 30 | 31 | 32 | def tick_function(p): 33 | """Map atmospheric pressure [units: kilopascal] to altitude [units: kilometer].""" 34 | h_table = np.linspace(84e3, 0) # altitude [units: meter] 35 | p_table = atmo.pressure(h_table) # atmo pressure [units: pascal] 36 | return np.interp(p * 1e3, p_table, h_table) * 1e-3 37 | 38 | 39 | ax2.set_xticklabels(['{:.0f}'.format(h) for h in tick_function(new_tick_locations)]) 40 | ax2.set_xlabel('Altitude [km]') 41 | ax2.tick_params(axis='y', direction='in', pad=-25) 42 | plt.subplots_adjust(top=0.8) 43 | plt.show() 44 | -------------------------------------------------------------------------------- /docs/source/examples/plots/cf_alt.py: -------------------------------------------------------------------------------- 1 | """Plot C_F vs altitude.""" 2 | import numpy as np 3 | from matplotlib import pyplot as plt 4 | import skaero.atmosphere.coesa as atmo 5 | from proptools import nozzle 6 | 7 | p_c = 10e6 # Chamber pressure [units: pascal] 8 | gamma = 1.2 # Exhaust heat capacity ratio [units: dimensionless] 9 | p_e_1 = 100e3 # Nozzle exit pressure, 1st stage [units: pascal] 10 | exp_ratio_1 = nozzle.er_from_p(p_c, p_e_1, gamma) # Nozzle expansion ratio [units: dimensionless] 11 | p_e_2 = 15e3 # Nozzle exit pressure, 2nd stage [units: pascal] 12 | exp_ratio_2 = nozzle.er_from_p(p_c, p_e_2, gamma) # Nozzle expansion ratio [units: dimensionless] 13 | 14 | alt = np.linspace(0, 84e3) # Altitude [units: meter] 15 | p_a = atmo.pressure(alt) # Ambient pressure [units: pascal] 16 | 17 | # Compute the thrust coeffieicient of the fixed-area nozzle, 1st stage [units: dimensionless] 18 | C_F_fixed_1 = nozzle.thrust_coef(p_c, p_e_1, gamma, p_a=p_a, er=exp_ratio_1) 19 | 20 | # Compute the thrust coeffieicient of the fixed-area nozzle, 2nd stage [units: dimensionless] 21 | C_F_fixed_2 = nozzle.thrust_coef(p_c, p_e_2, gamma, p_a=p_a, er=exp_ratio_2) 22 | 23 | # Compute the thrust coeffieicient of a variable-area matched nozzle [units: dimensionless] 24 | C_F_matched = nozzle.thrust_coef(p_c, p_a, gamma) 25 | 26 | plt.plot(alt * 1e-3, C_F_fixed_1, label='1st stage $\\epsilon_1 = {:.1f}$'.format(exp_ratio_1)) 27 | plt.plot(alt[0.4 * p_a < p_e_2] * 1e-3, C_F_fixed_2[0.4 * p_a < p_e_2], 28 | label='2nd stage $\\epsilon_2 = {:.1f}$'.format(exp_ratio_2)) 29 | plt.plot(alt * 1e-3, C_F_matched, label='matched', color='grey', linestyle=':') 30 | plt.xlabel('Altitude [km]') 31 | plt.ylabel('Thrust coefficient $C_F$ [-]') 32 | plt.title('Effect of altitude on nozzle performance') 33 | plt.legend() 34 | plt.show() 35 | -------------------------------------------------------------------------------- /docs/source/examples/solid/plots/thrust_curve.py: -------------------------------------------------------------------------------- 1 | """Plot the thrust curve of a solid rocket motor with a cylindrical propellant grain.""" 2 | 3 | from matplotlib import pyplot as plt 4 | import numpy as np 5 | from proptools import solid 6 | 7 | 8 | # Grain geometry (Clinder with circular port) 9 | r_in = 0.15 # Grain inner radius [units: meter]. 10 | r_out = 0.20 # Grain outer radius [units: meter]. 11 | length = 1.0 # Grain length [units: meter]. 12 | 13 | # Propellant properties 14 | gamma = 1.26 # Exhaust gas ratio of specific heats [units: dimensionless]. 15 | rho_solid = 1510. # Solid propellant density [units: kilogram meter**-3]. 16 | n = 0.5 # Propellant burn rate exponent [units: dimensionless]. 17 | a = 2.54e-3 * (6.9e6)**(-n) # Burn rate coefficient, such that the propellant 18 | # burns at 2.54 mm s**-1 at 6.9 MPa [units: meter second**-1 pascal**-n]. 19 | c_star = 1209. # Characteristic velocity [units: meter second**-1]. 20 | 21 | # Nozzle geometry 22 | A_t = 839e-6 # Throat area [units: meter**2]. 23 | A_e = 8 * A_t # Exit area [units: meter**2]. 24 | p_a = 101e3 # Ambeint pressure during motor firing [units: pascal]. 25 | 26 | # Burning surface evolution 27 | x = np.linspace(0, r_out - r_in) # Flame front progress steps [units: meter]. 28 | A_b = 2 * np.pi * (r_in + x) * length # Burn area at each flame progress step [units: meter**2]. 29 | 30 | # Compute thrust curve. 31 | t, p_c, F = solid.thrust_curve(A_b, x, A_t, A_e, p_a, a, n, rho_solid, c_star, gamma) 32 | 33 | # Plot results. 34 | ax1 = plt.subplot(2, 1, 1) 35 | plt.plot(t, p_c * 1e-6) 36 | plt.ylabel('Chamber pressure [MPa]') 37 | 38 | ax2 = plt.subplot(2, 1, 2) 39 | plt.plot(t, F * 1e-3) 40 | plt.ylabel('Thrust, sea level [kN]') 41 | plt.xlabel('Time [s]') 42 | plt.setp(ax1.get_xticklabels(), visible=False) 43 | 44 | plt.tight_layout() 45 | plt.subplots_adjust(hspace=0) 46 | plt.show() 47 | -------------------------------------------------------------------------------- /docs/source/examples/solid/plots/equilibrium_pressure.py: -------------------------------------------------------------------------------- 1 | """Illustrate the chamber pressure equilibrium of a solid rocket motor.""" 2 | 3 | from matplotlib import pyplot as plt 4 | import numpy as np 5 | 6 | p_c = np.linspace(1e6, 10e6) # Chamber pressure [units: pascal]. 7 | 8 | # Propellant properties 9 | gamma = 1.26 # Exhaust gas ratio of specific heats [units: dimensionless]. 10 | rho_solid = 1510. # Solid propellant density [units: kilogram meter**-3]. 11 | n = 0.5 # Propellant burn rate exponent [units: dimensionless]. 12 | a = 2.54e-3 * (6.9e6)**(-n) # Burn rate coefficient, such that the propellant 13 | # burns at 2.54 mm s**-1 at 6.9 MPa [units: meter second**-1 pascal**-n]. 14 | c_star = 1209. # Characteristic velocity [units: meter second**-1]. 15 | 16 | # Motor geometry 17 | A_t = 839e-6 # Throat area [units: meter**2]. 18 | A_b = 1.25 # Burn area [units: meter**2]. 19 | 20 | # Compute the nozzle mass flow rate at each chamber pressure. 21 | # [units: kilogram second**-1]. 22 | m_dot_nozzle = p_c * A_t / c_star 23 | 24 | # Compute the combustion mass addition rate at each chamber pressure. 25 | # [units: kilogram second**-1]. 26 | m_dot_combustion = A_b * rho_solid * a * p_c**n 27 | 28 | # Plot the mass rates 29 | plt.plot(p_c * 1e-6, m_dot_nozzle, label='Nozzle') 30 | plt.plot(p_c * 1e-6, m_dot_combustion, label='Combustion') 31 | plt.xlabel('Chamber pressure [MPa]') 32 | plt.ylabel('Mass rate [kg / s]') 33 | 34 | # Find where the mass rates are equal (e.g. the equilibrium). 35 | i_equil = np.argmin(abs(m_dot_combustion - m_dot_nozzle)) 36 | m_dot_equil = m_dot_nozzle[i_equil] 37 | p_c_equil = p_c[i_equil] 38 | 39 | # Plot the equilibrium point. 40 | plt.scatter(p_c_equil * 1e-6, m_dot_equil, marker='o', color='black', label='Equilibrium') 41 | plt.axvline(x=p_c_equil * 1e-6, color='grey', linestyle='--') 42 | 43 | plt.title('Chamber pressure: stable equilibrium, $n =$ {:.1f}'.format(n)) 44 | plt.legend() 45 | plt.show() 46 | -------------------------------------------------------------------------------- /proptools/valve_test.py: -------------------------------------------------------------------------------- 1 | """Unit tests for valve flow.""" 2 | 3 | import unittest 4 | from proptools import valve 5 | 6 | class TestValveGasCv(unittest.TestCase): 7 | def test_methane_example(self): 8 | """Test the methane example from 9 | http://www.idealvalve.com/pdf/Flow-Calculation-for-Gases.pdf 10 | """ 11 | # Setup 12 | p_1 = 790.83e3 # Inlet pressure, 100 psig [units: pascal] 13 | p_2 = 101e3 # Outlet pressure [units: pascal] 14 | T = 294.3 # gas temperature, 70 F [units: kelvin] 15 | m_molar = 0.01604 # Methane molar mass [units: kilogram mole**-1] 16 | flow_scfh = 600 17 | m_dot = valve.scfh_to_m_dot(flow_scfh, m_molar) 18 | cv_given = 0.1098 # Flow coefficient Cv given in example. 19 | 20 | # Action 21 | cv_calc = valve.valve_gas_cv(m_dot, p_1, p_2, m_molar, T) 22 | 23 | # Verification 24 | self.assertAlmostEqual(cv_given, cv_calc, places=3) 25 | 26 | 27 | class TestValveGasMassFlow(unittest.TestCase): 28 | def test_methane_example(self): 29 | """Test the methane example from 30 | http://www.idealvalve.com/pdf/Flow-Calculation-for-Gases.pdf 31 | """ 32 | # Setup 33 | p_1 = 790.83e3 # Inlet pressure, 100 psig [units: pascal] 34 | p_2 = 101e3 # Outlet pressure [units: pascal] 35 | T = 294.3 # gas temperature, 70 F [units: kelvin] 36 | m_molar = 0.01604 # Methane molar mass [units: kilogram mole**-1] 37 | flow_scfh_given = 600 # Flow in scfh given in example 38 | m_dot_given = valve.scfh_to_m_dot(flow_scfh_given, m_molar) 39 | cv = 0.1098 # Flow coefficient Cv 40 | 41 | # Action 42 | m_dot_calc = valve.valve_gas_mass_flow(cv, p_1, p_2, m_molar, T) 43 | 44 | # Verification 45 | self.assertAlmostEqual(m_dot_given, m_dot_calc, places=3) 46 | 47 | 48 | if __name__ == '__main__': 49 | unittest.main() 50 | -------------------------------------------------------------------------------- /proptools/turbopump_test.py: -------------------------------------------------------------------------------- 1 | import unittest 2 | from proptools import turbopump 3 | 4 | class TestStringMethods(unittest.TestCase): 5 | 6 | def test_m_dot2gpm(self): 7 | # 100 kg s**-1, 1000 kg m**-3 --> 1584 gal min**-1 8 | self.assertAlmostEqual(1584, turbopump.m_dot2gpm(100, 1000), delta=2) 9 | 10 | 11 | def test_gpm_m_dot_inverse(self): 12 | for m_dot in [1, 100, 1000, 2434]: 13 | for rho in [1, 100, 1000, 2000]: 14 | self.assertAlmostEqual(m_dot, turbopump.gpm2m_dot( 15 | turbopump.m_dot2gpm(m_dot, rho), rho)) 16 | 17 | 18 | def test_rpm_radsec_inverse(self): 19 | for rpm in [1, 1e3, 1e6]: 20 | self.assertAlmostEqual(rpm, turbopump.radsec2rpm( 21 | turbopump.rpm2radsec(rpm))) 22 | 23 | def test_sample61(self): 24 | # Check against sample problem 6-1 from Huzel and Huang. 25 | rho_ox = 1143 # 71.38 lbm ft**-3 26 | rho_fuel = 808.1 # 50.45 lbm ft**-3 27 | dp_ox = 9.997e6 # 1450 psi 28 | dp_fuel = 11.55e6 # 1675 psi 29 | m_dot_ox = 894 # 1971 lbm s**-1 30 | m_dot_fuel = 405 # 892 lbm s**-1 31 | N = turbopump.rpm2radsec(7000) 32 | 33 | # Check speed 34 | self.assertAlmostEqual(7000, turbopump.radsec2rpm(N)) 35 | 36 | # Check pump head 37 | self.assertAlmostEqual(2930, turbopump.dp2head(dp_ox, rho_ox), delta=10) 38 | self.assertAlmostEqual(4790, turbopump.dp2head(dp_fuel, rho_fuel), delta=10) 39 | 40 | # Check volume flow rate 41 | self.assertAlmostEqual(12420, turbopump.m_dot2gpm(m_dot_ox, rho_ox), delta=40) 42 | self.assertAlmostEqual(7960, turbopump.m_dot2gpm(m_dot_fuel, rho_fuel), delta=20) 43 | 44 | # Check specific speed 45 | self.assertAlmostEqual(1980, turbopump.pump_specific_speed_us( 46 | dp_ox, m_dot_ox, rho_ox, N), 47 | delta=30) 48 | self.assertAlmostEqual(1083, turbopump.pump_specific_speed_us( 49 | dp_fuel, m_dot_fuel, rho_fuel, N), 50 | delta=20) 51 | 52 | 53 | if __name__ == '__main__': 54 | unittest.main() -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # proptools 2 | 3 | ![Lint and test](https://github.com/mvernacc/proptools/actions/workflows/lint_and_test.yml/badge.svg) 4 | 5 | 6 | 7 | Rocket propulsion design calculation tools. 8 | 9 | ## Installation 10 | 11 | ```bash 12 | pip install proptools-rocket 13 | ``` 14 | 15 | Or install from source: 16 | 17 | ```bash 18 | git clone https://github.com/mvernacc/proptools.git 19 | cd proptools 20 | pip install -e . 21 | ``` 22 | 23 | ## Overview 24 | 25 | This project provides python implementations of equations used in the preliminary design of rocket propulsion systems. 26 | Most of the equations are taken from from [*Modern Engineering for Design of Liquid-Propellant Rocket Engines*](https://arc.aiaa.org/doi/book/10.2514/4.866197) by Dieter Huzel and David Huang or [*Rocket Propulsion Elements*](http://www.wiley.com/WileyCDA/WileyTitle/productCd-1118753658.html) by George Sutton and Oscar Biblarz. 27 | 28 | 29 | ## Author 30 | 31 | This software is written and maintained by Matthew Vernacchia, a graduate researcher in the department of Aeronautics and Astronautics at MIT. Please notify me of bugs or feature requests by opening an issue ticket or emailing mvernacc at mit dot edu. 32 | 33 | 34 | ## No Warranty 35 | 36 | THIS SOFTWARE IS PROVIDED BY THE AUTHOR ``AS IS'' AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. 37 | 38 | Further, the design tools implemented herein are suitable for preliminary design only. More detailed analysis and throughout verification testing is necessary before a rocket propulsion system can be safely operated or its performance guaranteed. 39 | -------------------------------------------------------------------------------- /setup.py: -------------------------------------------------------------------------------- 1 | '''A setuptools based installer for proptools. 2 | 3 | Based on https://github.com/pypa/sampleproject/blob/master/setup.py 4 | 5 | Matt Vernacchia 6 | proptools 7 | 2016 Sept 21 8 | ''' 9 | 10 | # Always prefer setuptools over distutils 11 | from setuptools import setup, find_packages 12 | # To use a consistent encoding 13 | from codecs import open 14 | from os import path 15 | 16 | here = path.abspath(path.dirname(__file__)) 17 | 18 | INSTALL_REQUIRES = [ 19 | 'numpy', 20 | 'matplotlib', 21 | 'scikit-aero', 22 | 'scipy', 23 | ] 24 | TEST_REQUIRES = [ 25 | 'pytest', 26 | 'coverage', 27 | 'pytest-cov', 28 | ] 29 | DOCS_REQUIRES = [ 30 | 'sphinx_rtd_theme', 31 | ] 32 | 33 | with open("README.md", "r") as fh: 34 | long_description = fh.read() 35 | 36 | setup( 37 | name='proptools-rocket', 38 | 39 | version='0.0.2', 40 | 41 | description='Rocket propulsion design calculation tools.', 42 | 43 | author='Matt Vernacchia', 44 | author_email='mvernacc@mit.edu', 45 | 46 | # See https://pypi.python.org/pypi?%3Aaction=list_classifiers 47 | classifiers=[ 48 | # How mature is this project? Common values are 49 | # 3 - Alpha 50 | # 4 - Beta 51 | # 5 - Production/Stable 52 | 'Development Status :: 3 - Alpha', 53 | 54 | # Indicate who your project is intended for 55 | 'Intended Audience :: Science/Research', 56 | 'Topic :: Scientific/Engineering', 57 | 58 | # Specify the Python versions you support here. In particular, ensure 59 | # that you indicate whether you support Python 2, Python 3 or both. 60 | 'Programming Language :: Python :: 3.7', 61 | ], 62 | 63 | # What does your project relate to? 64 | keywords='rocket propulsion engineering aerospace', 65 | 66 | long_description=long_description, 67 | long_description_content_type="text/markdown", 68 | 69 | project_urls={ 70 | 'Documentation': 'https://proptools.readthedocs.io/en/latest/', 71 | 'Source Code': 'https://github.com/mvernacc/proptools', 72 | }, 73 | 74 | install_requires=INSTALL_REQUIRES, 75 | extras_require={ 76 | 'test': TEST_REQUIRES + INSTALL_REQUIRES, 77 | 'docs': DOCS_REQUIRES + INSTALL_REQUIRES, 78 | }, 79 | 80 | packages=find_packages(), 81 | 82 | scripts=[], 83 | ) 84 | -------------------------------------------------------------------------------- /proptools/isentropic_test.py: -------------------------------------------------------------------------------- 1 | """Unit test for isentropic relations.""" 2 | import unittest 3 | from proptools import isentropic 4 | from proptools.constants import R_univ 5 | 6 | 7 | class TestStagTemperatureRatio(unittest.TestCase): 8 | """Unit tests for isentropic.stag_temperature_ratio""" 9 | 10 | def test_still(self): 11 | """Check that the ratio is 1 when Mach=0.""" 12 | self.assertEqual(1, isentropic.stag_temperature_ratio(0, 1.2)) 13 | 14 | def test_sonic(self): 15 | """Check the ratio when Mach=1.""" 16 | # Rocket Propulsion Elements, 8th edition, page 59. 17 | self.assertAlmostEqual(1 / 0.91, isentropic.stag_temperature_ratio(1, 1.2), places=2) 18 | 19 | 20 | class TestStagPressureRatio(unittest.TestCase): 21 | """Unit tests for isentropic.stag_pressure_ratio""" 22 | 23 | def test_still(self): 24 | """Check that the ratio is 1 when Mach=0.""" 25 | self.assertEqual(1, isentropic.stag_pressure_ratio(0, 1.2)) 26 | 27 | def test_sonic(self): 28 | """Check the ratio when Mach=1.""" 29 | # Rocket Propulsion Elements, 8th edition, page 59. 30 | self.assertAlmostEqual(1 / 0.56, isentropic.stag_pressure_ratio(1, 1.2), places=1) 31 | 32 | 33 | class TestStagDesnityRatio(unittest.TestCase): 34 | """Unit tests for isentropic.stag_density_ratio""" 35 | 36 | def test_still(self): 37 | """Check that the ratio is 1 when Mach=0.""" 38 | self.assertEqual(1, isentropic.stag_density_ratio(0, 1.2)) 39 | 40 | def test_sonic(self): 41 | """Check the ratio when Mach=1.""" 42 | # Rocket Propulsion Elements, 8th edition, page 59. 43 | self.assertAlmostEqual(1.61, isentropic.stag_density_ratio(1, 1.2), places=2) 44 | 45 | 46 | class TestVelocity(unittest.TestCase): 47 | """Unit tests for isentropic.velocity.""" 48 | 49 | def test_equal_pressure(self): 50 | """Test that the velocity is the same if the pressure is the same.""" 51 | v_1 = 100. 52 | p = 1e6 53 | v_2 = isentropic.velocity(v_1, p_1=p, T_1=300, p_2=p, gamma=1.2, m_molar=20e-3) 54 | self.assertEqual(v_1, v_2) 55 | 56 | def test_rpe_3_2(self): 57 | """Test against example problem 3-2 from Rocket Propulsion Elements.""" 58 | v_1 = 0 59 | p_1 = 2.068e6 60 | T_1 = 2222. 61 | gamma = 1.3 62 | m_molar = R_univ / 345.7 63 | p_2 = 101e3 64 | v_2 = isentropic.velocity(v_1, p_1, T_1, p_2, gamma, m_molar) 65 | self.assertAlmostEqual(v_2, 1828., places=0) 66 | 67 | 68 | if __name__ == '__main__': 69 | unittest.main() 70 | -------------------------------------------------------------------------------- /proptools/isentropic.py: -------------------------------------------------------------------------------- 1 | """Isentropic relations. 2 | 3 | See :ref:`isentropic-relations-tutorial-label` for a 4 | physical explaination of the isentropic relations. 5 | 6 | .. autosummary:: 7 | 8 | stag_temperature_ratio 9 | stag_pressure_ratio 10 | stag_density_ratio 11 | velocity 12 | """ 13 | from proptools.constants import R_univ 14 | 15 | 16 | def stag_temperature_ratio(M, gamma): 17 | """Stagnation temperature / static temperature ratio. 18 | 19 | Arguments: 20 | M (scalar): Mach number [units: dimensionless]. 21 | gamma (scalar): Heat capacity ratio [units: dimensionless]. 22 | 23 | Returns: 24 | scalar: the stagnation temperature ratio :math:`T_0 / T` [units: dimensionless]. 25 | """ 26 | return 1 + (gamma - 1) / 2 * M**2 27 | 28 | 29 | def stag_pressure_ratio(M, gamma): 30 | """Stagnation pressure / static pressure ratio. 31 | 32 | Arguments: 33 | M (scalar): Mach number [units: dimensionless]. 34 | gamma (scalar): Heat capacity ratio [units: dimensionless]. 35 | 36 | Returns: 37 | scalar: the stagnation pressure ratio :math:`p_0 / p` [units: dimensionless]. 38 | """ 39 | return (1 + (gamma - 1) / 2 * M**2)**(gamma / (gamma - 1)) 40 | 41 | 42 | def stag_density_ratio(M, gamma): 43 | """Stagnation density / static density ratio. 44 | 45 | Arguments: 46 | M (scalar): Mach number [units: dimensionless]. 47 | gamma (scalar): Heat capacity ratio [units: dimensionless]. 48 | 49 | Returns: 50 | scalar: the stagnation density ratio :math:`\\rho_0 / \\rho` [units: dimensionless]. 51 | """ 52 | return (1 + (gamma - 1) / 2 * M**2)**(1 / (gamma - 1)) 53 | 54 | 55 | def velocity(v_1, p_1, T_1, p_2, gamma, m_molar): # pylint: disable=too-many-arguments 56 | """Velocity relation between two points in an isentropic flow. 57 | 58 | Given the velocity, pressure, and temperature at station 1 and the pressure at station 2, 59 | find the velocity at station 2. See Rocket Propulsion Elements, 8th edition, equation 3-15b. 60 | 61 | Arguments: 62 | v_1 (scalar): Velocity at station 1 [units: meter second**-1]. 63 | p_1 (scalar): Pressure at station 1 [units: pascal]. 64 | T_1 (scalar): Temperature at station 1 [units kelvin]. 65 | p_2 (scalar): Pressure at station 2 [units: pascal]. 66 | gamma (scalar): Gas ratio of specific heats [units: dimensionless]. 67 | m_molar (scalar): Gas mean molar mass [units: kilogram mole**-1]. 68 | 69 | Returns: 70 | scalar: velocity at station 2 [units: meter second**-1]. 71 | """ 72 | return ((2 * gamma) / (gamma - 1) * R_univ * T_1 / m_molar 73 | * (1 - (p_2 / p_1)**((gamma - 1) / gamma)) + v_1**2)**0.5 74 | -------------------------------------------------------------------------------- /proptools/tank_structure_test.py: -------------------------------------------------------------------------------- 1 | import unittest 2 | from proptools import tank_structure as ts 3 | from proptools.units import inch2meter, psi2pascal, lbf2newton, lbm2kilogram 4 | 5 | 6 | class TestStringMethods(unittest.TestCase): 7 | 8 | def test_sample_8_3(self): 9 | # Do sample problem 8-3 from Huzel and Huang. 10 | stress = psi2pascal(38e3) 11 | a = inch2meter(41.0) 12 | b = inch2meter(29.4) 13 | l_c = inch2meter(46.9) 14 | E = psi2pascal(10.4e6) 15 | v = 0.36 16 | weld_eff = 1.0 17 | p_to = psi2pascal(180) # Oxidizer max pressure 180 psi 18 | p_tf = psi2pascal(170) # fuel max pressure 170 psi 19 | rho = 0.101 * lbm2kilogram(1) / inch2meter(1)**3 20 | 21 | # knuckle factor K = 0.80 22 | self.assertAlmostEqual(0.8, ts.knuckle_factor(a / b), delta=0.02) 23 | 24 | # Ox tank knuckle 0.155 inch thick. 25 | self.assertAlmostEqual(inch2meter(0.155), ts.knuckle_thickness( 26 | p_to, a, b, stress, weld_eff), delta=inch2meter(0.005)) 27 | 28 | # Ox tank crown 0.135 inch thick. 29 | self.assertAlmostEqual(inch2meter(0.135), ts.crown_thickness( 30 | p_to, (a / b) * a, stress, weld_eff), delta=inch2meter(0.005)) 31 | 32 | # Fuel tank cylinder 0.183 inch thick. 33 | self.assertAlmostEqual(inch2meter(0.183), ts.cylinder_thickness( 34 | p_tf, a, stress, weld_eff), delta=inch2meter(0.005)) 35 | 36 | # Ellipse design factor E' = 4.56 37 | self.assertAlmostEqual(4.56, ts.ellipse_design_factor(a / b), delta = 0.005) 38 | 39 | # Oxidizer tank end weighs 126.4 lmb 40 | self.assertAlmostEqual(2 * lbm2kilogram(126.4), ts.ellipse_mass( 41 | a, b, inch2meter(0.145), rho), delta=lbm2kilogram(0.2)) 42 | 43 | # Cylindrical section weighs 223.3 lbm 44 | self.assertAlmostEqual(lbm2kilogram(223.3), ts.cylinder_mass( 45 | a, inch2meter(0.183), l_c, rho), delta=lbm2kilogram(0.2)) 46 | 47 | # Critical external pressure for ox tank ends = 13.4 psi. 48 | self.assertAlmostEqual(psi2pascal(13.4), ts.cr_ex_press_ellipse_end( 49 | a, b, inch2meter(0.145), E, C_b=0.10), delta=1e3) 50 | 51 | # Critical external loading for fuel tank cylinder 10.8 psi 52 | self.assertAlmostEqual(psi2pascal(10.8), ts.cr_ex_press_cylinder( 53 | a, inch2meter(0.183), l_c, E, v), delta=1e3) 54 | 55 | 56 | def test_sample_8_4(self): 57 | # Do sample problem 8-4 from Huzel and Huang. 58 | a = inch2meter(41.0) 59 | l_c = inch2meter(46.9) 60 | E = psi2pascal(10.4e6) 61 | t_c = inch2meter(0.183) 62 | 63 | self.assertAlmostEqual(lbf2newton(823900), ts.max_axial_load( 64 | 0, a, t_c, l_c, E), delta=100) 65 | 66 | if __name__ == '__main__': 67 | unittest.main() 68 | -------------------------------------------------------------------------------- /proptools/valve.py: -------------------------------------------------------------------------------- 1 | """Valve flow calculations. 2 | 3 | 4 | References: 5 | [1] "Flow Calculation for Gases", Ideal Valve, Inc. 6 | http://www.idealvalve.com/pdf/Flow-Calculation-for-Gases.pdf 7 | """ 8 | import numpy as np 9 | import proptools.constants 10 | 11 | 12 | def m_dot_to_scfh(m_dot, m_molar): 13 | """Convert mass flow rate to standard cubic feet per hour. 14 | 15 | Arguments: 16 | m_dot (scalar): Mass flow rate [units: kilogram second**-1]. 17 | m_molar (scalar): Gas molar mass [units: kilogram mole**-1]. 18 | """ 19 | flow_mole = m_dot / m_molar # units: mole second**-1 20 | flow_scfs = flow_mole / proptools.constants.scf_mole # units: scf second**-1 21 | flow_scfh = flow_scfs * 3600 22 | return flow_scfh 23 | 24 | 25 | def scfh_to_m_dot(flow_scfh, m_molar): 26 | """Convert standard cubic feet per hour to mass flow rate.""" 27 | flow_scfs = flow_scfh / 3600 28 | flow_mole = flow_scfs * proptools.constants.scf_mole 29 | m_dot = flow_mole * m_molar 30 | return m_dot 31 | 32 | 33 | def valve_gas_cv(m_dot, p_1, p_2, m_molar, T): 34 | """Find the required valve Cv for a given mass flow and pressure drop. 35 | Assumes that a compressible gas is flowing through the valve. 36 | 37 | Arguments: 38 | m_dot (scalar): Mass flow rate [units: kilogram second**-1]. 39 | p_1 (scalar): Inlet pressure [units: pascal]. 40 | p_2 (scalar): Outlet pressure [units: pascal]. 41 | m_molar (scalar): Gas molar mass [units: kilogram mole**-1]. 42 | T (scalar): Gas temperature [units: kelvin]. 43 | 44 | Returns: 45 | scalar: Valve flow coefficient Cv [units: gallon minute**-1 psi**-1]. 46 | """ 47 | # Specific gravity of the gas [units: dimensionless]: 48 | spec_grav = m_molar / proptools.constants.m_molar_air 49 | 50 | # Convert gas flow to standard cubic feet per hour 51 | flow_scfh = m_dot_to_scfh(m_dot, m_molar) 52 | 53 | # Determine if the flow is choked. 54 | # Checking if `p_1 >= 2 * p_2` is suggested by [1]. 55 | # There is a more accurate choked flow criterion which depends 56 | # on the ratio of specific heats. 57 | choked = p_1 >= 2 * p_2 58 | 59 | if choked: 60 | cv = flow_scfh / 0.08821 * (spec_grav * T)**0.5 / p_1 61 | else: 62 | cv = flow_scfh / 0.1040 * (spec_grav * T / (p_1**2 - p_2**2))**0.5 63 | return cv 64 | 65 | 66 | def valve_gas_pressure(cv, m_dot, p_1, m_molar, T): 67 | # TODO, use fsolve on valve gas mass flow 68 | pass 69 | 70 | 71 | def valve_gas_mass_flow(cv, p_1, p_2, m_molar, T): 72 | # Specific gravity of the gas [units: dimensionless]: 73 | spec_grav = m_molar / proptools.constants.m_molar_air 74 | 75 | # Determine if the flow is choked. 76 | # Checking if `p_1 >= 2 * p_2` is suggested by [1]. 77 | # There is a more accurate choked flow criterion which depends 78 | # on the ratio of specific heats. 79 | choked = p_1 >= 2 * p_2 80 | 81 | if choked: 82 | flow_scfh = cv * 0.08821 * p_1 / (spec_grav * T)**0.5 83 | else: 84 | flow_scfh = cv * 0.1040 * ((p_1**2 - p_2**2) / (spec_grav * T))**0.5 85 | 86 | m_dot = scfh_to_m_dot(flow_scfh, m_molar) 87 | return m_dot 88 | 89 | 90 | def demo_plots(): 91 | """Demonstrate by plotting mass flow vs pressure.""" 92 | cv = 0.1 93 | p_1 = np.linspace(101e3, 500e3) # units: pascal 94 | p_2 = 101e3 # units: pascal 95 | m_molar = proptools.constants.m_molar_air 96 | T = 300 # units: kelvin 97 | m_dot = np.array([valve_gas_mass_flow(cv, p, p_2, m_molar, T) for p in p_1]) 98 | plt.plot(p_1 * 1e-3, m_dot, label='Cv = {:.2f}'.format(cv)) 99 | plt.xlabel('Inlet pressure [kPa]') 100 | plt.ylabel('Mass flow [kg/s]') 101 | plt.legend() 102 | plt.show() 103 | 104 | 105 | if __name__ == '__main__': 106 | import matplotlib.pyplot as plt 107 | demo_plots() 108 | -------------------------------------------------------------------------------- /proptools/solid_test.py: -------------------------------------------------------------------------------- 1 | """Unit tests for solid rocket motor equations.""" 2 | import unittest 3 | import numpy as np 4 | from proptools import solid 5 | 6 | 7 | class TestBurnAreaRatio(unittest.TestCase): 8 | """Unit tests for solid.burn_area_ratio.""" 9 | 10 | def test_ex_12_3(self): 11 | """Test against example problem 12-3 from Rocket Propulsion Elements.""" 12 | gamma = 1.26 # Given ratio of specific heats [units: dimensionless]. 13 | rho_solid = 1510. # Given solid propellant density [units: kilogram meter**-3]. 14 | p_c = 6.895e6 # Given chamber pressure [units: pascal]. 15 | n = 0.5 # Burn rate exponent, guess 16 | a = 2.54e-3 * (p_c)**(-n) # Burn rate coefficient, from given burn rate of 17 | # 0.1 inch second**-1 at 1000 psi [units: meter second**-1 pascal**-n]. 18 | c_star = 1209. # Given characteristic velocity [units: meter second**-1]. 19 | K_RPE = 1933. / 1.30 # Given burn area ratio [units: dimensionless]. 20 | 21 | K = solid.burn_area_ratio(p_c, a, n, rho_solid, c_star) 22 | 23 | self.assertTrue(abs(K - K_RPE) < 0.1) 24 | 25 | 26 | class TestBurnAndThroatArea(unittest.TestCase): 27 | """Unit tests for solid.burn_and_throat_area.""" 28 | 29 | def test_ex_12_3(self): 30 | """Test against example problem 12-3 from Rocket Propulsion Elements.""" 31 | gamma = 1.26 # Given ratio of specific heats [units: dimensionless]. 32 | rho_solid = 1510. # Given solid propellant density [units: kilogram meter**-3]. 33 | p_c = 6.895e6 # Given chamber pressure [units: pascal]. 34 | n = 0.5 # Burn rate exponent, guess 35 | a = 2.54e-3 * (p_c)**(-n) # Burn rate coefficient, from given burn rate of 36 | # 0.1 inch second**-1 at 1000 psi [units: meter second**-1 pascal**-n]. 37 | c_star = 1209. # Given characteristic velocity [units: meter second**-1]. 38 | F = 8.9e3 # Given thrust [units: newton]. 39 | p_e = 101e3 # Exit pressure [units: pascal]. 40 | 41 | A_t_RPE = 839e-6 # Given throat area [units: meter**2]. 42 | A_b_RPE = 1.25 # Given burn area [units: meter**2]. 43 | 44 | A_b, A_t = solid.burn_and_throat_area(F, p_c, p_e, a, n, rho_solid, c_star, gamma) 45 | 46 | self.assertTrue(np.isclose(A_t, A_t_RPE, rtol=5e-2)) 47 | self.assertTrue(np.isclose(A_b, A_b_RPE, rtol=5e-2)) 48 | 49 | 50 | class TestThrustCurve(unittest.TestCase): 51 | """Unit tests for solid.thrust_curve.""" 52 | 53 | def test_ex_12_3_const(self): 54 | """Test against example problem 12-3 from Rocket Propulsion Elements. 55 | 56 | Constant burn area and thrust.""" 57 | gamma = 1.26 # Given ratio of specific heats [units: dimensionless]. 58 | rho_solid = 1510. # Given solid propellant density [units: kilogram meter**-3]. 59 | p_c = 6.895e6 # Given chamber pressure [units: pascal]. 60 | n = 0.5 # Burn rate exponent, guess 61 | a = 2.54e-3 * (p_c)**(-n) # Burn rate coefficient, from given burn rate of 62 | # 0.1 inch second**-1 at 1000 psi [units: meter second**-1 pascal**-n]. 63 | c_star = 1209. # Given characteristic velocity [units: meter second**-1]. 64 | A_t = 839e-6 # Throat area [units: meter**2]. 65 | A_e = 8 * A_t # Exit area [units: meter**2]. 66 | p_a = 101e3 # Ambeint pressure during motor firing [units: pascal]. 67 | x = np.array([0, 2.54e-3, 2 * 2.54e-3]) # Flame front progress distance. 68 | A_b = 1.25 * np.ones(3) # Burn area [units: meter**2]. 69 | 70 | F_RPE = 8.9e3 # Given thrust [units: newton]. 71 | p_c_RPE = 6.9e6 # Given chamber pressure [units: pascal]. 72 | t_expected = np.array([0, 1, 2]) # Expected times to reach each step [units: second]. 73 | 74 | t, p_c, F = solid.thrust_curve(A_b, x, A_t, A_e, p_a, a, n, rho_solid, c_star, gamma) 75 | 76 | for i in range(3): 77 | self.assertTrue(abs(p_c[i] - p_c_RPE) < 0.1e6) 78 | self.assertTrue(abs(F[i] - F_RPE) < 0.3e3) 79 | self.assertTrue(abs(t[i] - t_expected[i]) < 1e-2) 80 | 81 | 82 | if __name__ == '__main__': 83 | unittest.main() 84 | -------------------------------------------------------------------------------- /proptools/nozzle_test.py: -------------------------------------------------------------------------------- 1 | """Unit tests for nozzle flow.""" 2 | import unittest 3 | import numpy as np 4 | from proptools import nozzle 5 | from proptools.constants import R_univ 6 | 7 | 8 | class TestMassFlow(unittest.TestCase): 9 | """Unit tests for nozzle.mass_flow.""" 10 | 11 | def test_rpe_3_3(self): 12 | """Test against example problem 3-3 from Rocket Propulsion Elements.""" 13 | T_c = 2800. 14 | gamma = 1.2 15 | m_molar = R_univ / 360. 16 | p_c = 2.039e6 17 | A_t = 13.32e-4 18 | 19 | m_dot = nozzle.mass_flow(A_t, p_c, T_c, gamma, m_molar) 20 | self.assertAlmostEqual(1.754, m_dot, places=3) 21 | 22 | 23 | class TestThrust(unittest.TestCase): 24 | """Unit tests for nozzle.thrust.""" 25 | 26 | def test_rpe_3_3(self): 27 | """Test against example problem 3-3 from Rocket Propulsion Elements.""" 28 | T_c = 2800. 29 | gamma = 1.2 30 | m_molar = R_univ / 360. 31 | p_c = 2.039e6 32 | A_t = 13.32e-4 33 | p_e = 2.549e3 34 | 35 | F = nozzle.thrust(A_t, p_c, p_e, gamma) 36 | self.assertAlmostEqual(5001., F, places=0) 37 | 38 | 39 | class TestThrustCoef(unittest.TestCase): 40 | """Unit tests for nozzle.thrust_coef.""" 41 | 42 | def test_hh_1_3(self): 43 | """Test against example problem 1-3 from Huzel and Huang.""" 44 | # Inputs 45 | T_c = 3633 # = 6540 R 46 | p_c = 6.895e6 # = 1000 psia 47 | p_e = 67.9e3 # = 9.85 psia 48 | p_a = 101e3 # = 14.7 psia 49 | m_molar = 22.67e-3 50 | gamma = 1.2 51 | er = 12 52 | 53 | # Correct results from Huzel and Huang 54 | C_F_hh = 1.5918 55 | 56 | C_F = nozzle.thrust_coef(p_c, p_e, gamma, p_a, er) 57 | 58 | self.assertTrue(abs(C_F - C_F_hh) < 0.01) 59 | 60 | 61 | class TestCStar(unittest.TestCase): 62 | """Unit tests for nozzle.c_star.""" 63 | 64 | def test_hh_1_3(self): 65 | """Test against example problem 1-3 from Huzel and Huang.""" 66 | # Inputs 67 | T_c = 3633 # = 6540 R 68 | p_c = 6.895e6 # = 1000 psia 69 | p_e = 67.9e3 # = 9.85 psia 70 | p_a = 101e3 # = 14.7 psia 71 | m_molar = 22.67e-3 72 | gamma = 1.2 73 | er = 12 74 | 75 | # Correct results from Huzel and Huang 76 | c_star_hh = 1777 # = 5830 ft s**-1 77 | 78 | c_star = nozzle.c_star(gamma, m_molar, T_c) 79 | 80 | self.assertTrue(abs(c_star - c_star_hh) < 5) 81 | 82 | 83 | class TestMachFormEr(unittest.TestCase): 84 | """Unit tests for nozzle.mach_from_er.""" 85 | 86 | def test_hh_1_12(self): 87 | """Test mach_from_er against H&H figure 1-12""" 88 | gamma = 1.4 89 | er = 4 90 | M_hh = 3.0 91 | M = nozzle.mach_from_er(er, gamma) 92 | self.assertTrue(abs(M - M_hh) < 0.1) 93 | 94 | 95 | class TestMachFromPr(unittest.TestCase): 96 | """Unit tests for nozzle.mach_from_pr.""" 97 | 98 | def test_rpe_3_1(self): 99 | """Test mach_from_pr against RPE figure 3-1.""" 100 | M = nozzle.mach_from_pr(1, 1, 1.3) 101 | M_RPE = 0. 102 | self.assertTrue(abs(M - M_RPE) < 0.05) 103 | M = nozzle.mach_from_pr(1, 0.8, 1.3) 104 | M_RPE = 0.6 105 | self.assertTrue(abs(M - M_RPE) < 0.05) 106 | M = nozzle.mach_from_pr(1, 0.1, 1.3) 107 | M_RPE = 2.2 108 | self.assertTrue(abs(M - M_RPE) < 0.05) 109 | 110 | 111 | class TestMachArea(unittest.TestCase): 112 | """Unit tests for nozzle.area_from_mach.""" 113 | 114 | def test_gamma_sonic(self): 115 | """Test that the sonic area ratio is 1, across range of gamma.""" 116 | for gamma in np.linspace(1.1, 1.6, 10): 117 | self.assertTrue(np.isclose(nozzle.area_from_mach(1, gamma), 1)) 118 | 119 | def test_inverse(self): 120 | """Test that mach_from_area_subsonic is the inverse of area_from_mach.""" 121 | for gamma in np.linspace(1.1, 1.6, 10): 122 | for Ar in np.linspace(1.1, 10, 10): 123 | M = nozzle.mach_from_area_subsonic(Ar, gamma) 124 | self.assertTrue(np.isclose(nozzle.area_from_mach(M, gamma), Ar)) 125 | 126 | 127 | class PressureFromEr(unittest.TestCase): 128 | """Unit tests for nozzle.pressure_from_er and nozzle.er_from_p""" 129 | 130 | def test_rpe_3_3(self): 131 | """Test against example problem 3-3 from Rocket Propulsion Elements.""" 132 | gamma = 1.20 # Given ratio of specific heats [units: dimensionless]. 133 | er_rpe = 60. # Given area expansion ratio [units: dimensionless]. 134 | p_c_rpe = 2.039e6 # Given chamber pressure [units: pascal]. 135 | p_e_rpe = 2.549e3 # Given exit pressure [units: pascal]. 136 | 137 | er = nozzle.er_from_p(p_c_rpe, p_e_rpe, gamma) 138 | self.assertTrue(np.isclose(er, er_rpe, rtol=1e-2)) 139 | 140 | pr = nozzle.pressure_from_er(er_rpe, gamma) 141 | self.assertTrue(np.isclose(pr, p_e_rpe / p_c_rpe, rtol=1e-2)) 142 | 143 | def test_inverse(self): 144 | """Test that pressure_from_er is the inverse of area_from_mach.""" 145 | for gamma in np.linspace(1.1, 1.6, 5): 146 | for er in np.linspace(2, 400, 10): 147 | pr = nozzle.pressure_from_er(er, gamma) 148 | p_e = 1. 149 | p_c = p_e / pr 150 | er_calc = nozzle.er_from_p(p_c, p_e, gamma) 151 | self.assertTrue(np.isclose(er_calc, er, rtol=1e-3)) 152 | 153 | 154 | if __name__ == '__main__': 155 | unittest.main() 156 | -------------------------------------------------------------------------------- /docs/source/conf.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | # 3 | # proptools documentation build configuration file, created by 4 | # sphinx-quickstart on Sun Aug 20 15:41:02 2017. 5 | # 6 | # This file is execfile()d with the current directory set to its 7 | # containing dir. 8 | # 9 | # Note that not all possible configuration values are present in this 10 | # autogenerated file. 11 | # 12 | # All configuration values have a default; values that are commented out 13 | # serve to show the default. 14 | 15 | # If extensions (or modules to document with autodoc) are in another directory, 16 | # add these directories to sys.path here. If the directory is relative to the 17 | # documentation root, use os.path.abspath to make it absolute, like shown here. 18 | # 19 | import os 20 | import sys 21 | sys.path.insert(0, os.path.abspath('../proptools')) 22 | 23 | 24 | # -- General configuration ------------------------------------------------ 25 | 26 | # If your documentation needs a minimal Sphinx version, state it here. 27 | # 28 | # needs_sphinx = '1.0' 29 | 30 | # Add any Sphinx extension module names here, as strings. They can be 31 | # extensions coming with Sphinx (named 'sphinx.ext.*') or your custom 32 | # ones. 33 | extensions = ['sphinx.ext.autodoc', 34 | 'sphinx.ext.napoleon', 35 | 'sphinx.ext.doctest', 36 | 'sphinx.ext.todo', 37 | 'sphinx.ext.coverage', 38 | 'sphinx.ext.mathjax', 39 | 'sphinx.ext.viewcode', 40 | 'matplotlib.sphinxext.plot_directive', 41 | 'sphinx.ext.autosummary'] 42 | 43 | # Add any paths that contain templates here, relative to this directory. 44 | templates_path = ['_templates'] 45 | 46 | # The suffix(es) of source filenames. 47 | # You can specify multiple suffix as a list of string: 48 | # 49 | # source_suffix = ['.rst', '.md'] 50 | source_suffix = '.rst' 51 | 52 | # The master toctree document. 53 | master_doc = 'index' 54 | 55 | # General information about the project. 56 | project = u'proptools' 57 | copyright = u'2017, Matthew Vernacchia' 58 | author = u'Matthew Vernacchia' 59 | 60 | # The version info for the project you're documenting, acts as replacement for 61 | # |version| and |release|, also used in various other places throughout the 62 | # built documents. 63 | # 64 | # The short X.Y version. 65 | version = u'0.0.0' 66 | # The full version, including alpha/beta/rc tags. 67 | release = u'0.0.0' 68 | 69 | # The language for content autogenerated by Sphinx. Refer to documentation 70 | # for a list of supported languages. 71 | # 72 | # This is also used if you do content translation via gettext catalogs. 73 | # Usually you set "language" from the command line for these cases. 74 | language = None 75 | 76 | # List of patterns, relative to source directory, that match files and 77 | # directories to ignore when looking for source files. 78 | # This patterns also effect to html_static_path and html_extra_path 79 | exclude_patterns = [] 80 | 81 | # The name of the Pygments (syntax highlighting) style to use. 82 | pygments_style = 'sphinx' 83 | 84 | # If true, `todo` and `todoList` produce output, else they produce nothing. 85 | todo_include_todos = True 86 | 87 | 88 | # -- Options for HTML output ---------------------------------------------- 89 | 90 | # The theme to use for HTML and HTML Help pages. See the documentation for 91 | # a list of builtin themes. 92 | # 93 | html_theme = 'sphinx_rtd_theme' 94 | 95 | # Theme options are theme-specific and customize the look and feel of a theme 96 | # further. For a list of options available for each theme, see the 97 | # documentation. 98 | # 99 | # html_theme_options = {} 100 | 101 | # Add any paths that contain custom static files (such as style sheets) here, 102 | # relative to this directory. They are copied after the builtin static files, 103 | # so a file named "default.css" will overwrite the builtin "default.css". 104 | html_static_path = ['_static'] 105 | 106 | 107 | # -- Options for HTMLHelp output ------------------------------------------ 108 | 109 | # Output file base name for HTML help builder. 110 | htmlhelp_basename = 'proptoolsdoc' 111 | 112 | 113 | # -- Options for LaTeX output --------------------------------------------- 114 | 115 | latex_elements = { 116 | # The paper size ('letterpaper' or 'a4paper'). 117 | # 118 | # 'papersize': 'letterpaper', 119 | 120 | # The font size ('10pt', '11pt' or '12pt'). 121 | # 122 | # 'pointsize': '10pt', 123 | 124 | # Additional stuff for the LaTeX preamble. 125 | # 126 | # 'preamble': '', 127 | 128 | # Latex figure (float) alignment 129 | # 130 | # 'figure_align': 'htbp', 131 | } 132 | 133 | # Grouping the document tree into LaTeX files. List of tuples 134 | # (source start file, target name, title, 135 | # author, documentclass [howto, manual, or own class]). 136 | latex_documents = [ 137 | (master_doc, 'proptools.tex', u'proptools Documentation', 138 | u'Matthew Vernacchia', 'manual'), 139 | ] 140 | 141 | 142 | # -- Options for manual page output --------------------------------------- 143 | 144 | # One entry per manual page. List of tuples 145 | # (source start file, name, description, authors, manual section). 146 | man_pages = [ 147 | (master_doc, 'proptools', u'proptools Documentation', 148 | [author], 1) 149 | ] 150 | 151 | 152 | # -- Options for Texinfo output ------------------------------------------- 153 | 154 | # Grouping the document tree into Texinfo files. List of tuples 155 | # (source start file, target name, title, author, 156 | # dir menu entry, description, category) 157 | texinfo_documents = [ 158 | (master_doc, 'proptools', u'proptools Documentation', 159 | author, 'proptools', 'One line description of project.', 160 | 'Miscellaneous'), 161 | ] 162 | 163 | -------------------------------------------------------------------------------- /proptools/solid.py: -------------------------------------------------------------------------------- 1 | """ Solid rocket motor equations. 2 | 3 | .. autosummary:: 4 | 5 | chamber_pressure 6 | burn_area_ratio 7 | burn_and_throat_area 8 | thrust_curve 9 | """ 10 | 11 | from scipy.integrate import cumtrapz 12 | 13 | from proptools import nozzle 14 | 15 | 16 | def chamber_pressure(K, a, n, rho_solid, c_star): 17 | """Chamber pressure due to solid propellant combustion. 18 | 19 | Reference: Equation 12-6 in Rocket Propulsion Elements, 8th edition. 20 | 21 | Args: 22 | K (scalar): Ratio of burning area to throat area, :math:`A_b/A_t` [units: dimensionless]. 23 | a (scalar): Propellant burn rate coefficient [units: meter second**-1 pascal**-n]. 24 | n (scalar): Propellant burn rate exponent [units: dimensionless]. 25 | rho_solid (scalar): Solid propellant density [units: kilogram meter**-3]. 26 | c_star (scalar): Propellant combustion characteristic velocity [units: meter second**-1]. 27 | 28 | Returns: 29 | scalar: Chamber pressure [units: pascal]. 30 | """ 31 | return (K * rho_solid * a * c_star) ** (1 / (1 - n)) 32 | 33 | 34 | def burn_area_ratio(p_c, a, n, rho_solid, c_star): 35 | """Get the burn area ratio, given chamber pressure and propellant properties. 36 | 37 | Reference: Equation 12-6 in Rocket Propulsion Elements, 8th edition. 38 | 39 | Arguments: 40 | p_c (scalar): Chamber pressure [units: pascal]. 41 | a (scalar): Propellant burn rate coefficient [units: meter second**-1 pascal**-n]. 42 | n (scalar): Propellant burn rate exponent [units: none]. 43 | rho_solid (scalar): Solid propellant density [units: kilogram meter**-3]. 44 | c_star (scalar): Propellant combustion characteristic velocity [units: meter second**-1]. 45 | 46 | Returns: 47 | scalar: Ratio of burning area to throat area, :math:`K = A_b/A_t` [units: dimensionless]. 48 | """ 49 | return p_c**(1 - n) / (rho_solid * a * c_star) 50 | 51 | 52 | def burn_and_throat_area(F, p_c, p_e, a, n, rho_solid, c_star, gamma): 53 | """Given thrust and chamber pressure, and propellant properties, find the burn area and throat area. 54 | 55 | Assumes that the exit pressure is matched (:math:`p_e = p_a`). 56 | 57 | Arguments: 58 | F (scalar): Thrust force [units: newton]. 59 | p_c (scalar): Chamber pressure [units: pascal]. 60 | p_e (scalar): Nozzle exit pressure [units: pascal]. 61 | a (scalar): Propellant burn rate coefficient [units: meter second**-1 pascal**-n]. 62 | n (scalar): Propellant burn rate exponent [units: none]. 63 | rho_solid (scalar): Solid propellant density [units: kilogram meter**-3]. 64 | c_star (scalar): Propellant combustion characteristic velocity [units: meter second**-1]. 65 | gamma (scalar): Exhaust gas ratio of specific heats [units: dimensionless]. 66 | 67 | Returns: 68 | tuple: tuple containing: 69 | 70 | A_b (scalar): Burn area [units: meter**2]. 71 | 72 | A_t (scalar): Throat area [units: meter**2]. 73 | """ 74 | C_F = nozzle.thrust_coef(p_c, p_e, gamma) 75 | A_t = F / (C_F * p_c) 76 | A_b = A_t * burn_area_ratio(p_c, a, n, rho_solid, c_star) 77 | return (A_b, A_t) 78 | 79 | 80 | def thrust_curve(A_b, x, A_t, A_e, p_a, a, n, rho_solid, c_star, gamma): 81 | """Thrust vs time curve for a solid rocket motor. 82 | 83 | Given information about the evolution of the burning surface of the propellant grain, 84 | this function predicts the time-varying thrust of a solid rocket motor. 85 | 86 | The evolution of the burning surface is described by two lists, ``A_b`` and ``x``. 87 | Each element in the lists describes a step in the (discretized) evolution of the burning 88 | surface. ``x[i]`` is the distance which the flame front must progress (normal to the burning 89 | surface) to reach step ``i``. ``A_b[i]`` is the burn area at step ``i``. 90 | 91 | Arguments: 92 | A_b (list): Burn area at each step [units: meter**-2]. 93 | x (list): flame front progress distance at each step [units: meter]. 94 | A_t (scalar): Nozzle throat area [units: meter**2]. 95 | A_e (scalar): Nozzle exit area [units: meter**2]. 96 | p_a (scalar): Ambient pressure during motor firing [units: pascal]. 97 | a (scalar): Propellant burn rate coefficient [units: meter second**-1 pascal**-n]. 98 | n (scalar): Propellant burn rate exponent [units: none]. 99 | rho_solid (scalar): Solid propellant density [units: kilogram meter**-3]. 100 | c_star (scalar): Propellant combustion characteristic velocity [units: meter second**-1]. 101 | gamma (scalar): Exhaust gas ratio of specific heats [units: dimensionless]. 102 | 103 | Returns: 104 | tuple: tuple containing: 105 | 106 | t (list): time at each step [units: second]. 107 | 108 | p_c (list): Chamber pressure at each step [units: pascal]. 109 | 110 | F (list): Thrust at each step [units: newton]. 111 | """ 112 | # Compute chamber pressure and exit pressure each flame progress distance x 113 | # [units: pascal]. 114 | p_c = chamber_pressure(A_b / A_t, a, n, rho_solid, c_star) 115 | p_e = p_c * nozzle.pressure_from_er(A_e / A_t, gamma) 116 | 117 | # Compute the burn rate for each flame progress distance x [units: meter second**-1] 118 | r = a * p_c**n 119 | 120 | # Compute the thrust for each flame progress distance x [units: newton] 121 | F = nozzle.thrust(A_t, p_c, p_e, gamma, p_a, A_e / A_t) 122 | 123 | # Compute the time to reach each flame progress distance x [units: second] 124 | t = cumtrapz(1 / r, x, initial=0) 125 | 126 | return (t, p_c, F) 127 | -------------------------------------------------------------------------------- /proptools/convection_test.py: -------------------------------------------------------------------------------- 1 | """Unit tests for convection models.""" 2 | import unittest 3 | from math import pi 4 | from proptools import convection, nozzle 5 | 6 | 7 | class TestTaw(unittest.TestCase): 8 | """Unit tests for convection.adiabatic_wall_temperature""" 9 | 10 | def test_ssme(self): 11 | """Test against the SSME example from 16.512 Lecture 7.""" 12 | # The T_aw given in the example (3400 K) does not seem to be correct. 13 | # For now, I am not going to implement this test, for want of a reference 14 | # example to compare to. 15 | pass 16 | 17 | class TestLongTube(unittest.TestCase): 18 | """Unit tests for convection.long_tube_coeff.""" 19 | 20 | def test_1(self): 21 | """No examples are given in Hill & Peterson, I had to come up with this.""" 22 | # Setup 23 | Re = 1e6 24 | Pr = 0.7 25 | mass_flux = 1. 26 | mu = 2e-5 27 | c_p = 1000. 28 | k = mu * c_p / Pr 29 | D = Re * mu / mass_flux 30 | 31 | # Action 32 | h = convection.long_tube_coeff(mass_flux, D, c_p, mu, k) 33 | 34 | # Verification 35 | # Lefthand side of equation 11.35 from H & P 36 | # = h / (c_p mass_flux) 37 | # units: dimensionless 38 | lhs = 0.023 * Re**-0.2 * Pr**-0.67 39 | self.assertTrue(abs(lhs - h / (c_p * mass_flux)) < 1e-6) 40 | 41 | 42 | class TestBartz(unittest.TestCase): 43 | """Unit tests for convection.bartz.""" 44 | 45 | def test_ssme(self): 46 | """Test agaisnt the SSME example from 16.512 Lecture 7.""" 47 | T_e = 3200. 48 | T_w = 1000. 49 | T_avg = (T_e + T_w) / 2 50 | sigma = convection.bartz_sigma_sanchez(T_e, T_avg) 51 | p_c = 22e6 52 | c_star = 2600. 53 | c_p = 2800. 54 | mu_e = 3e-5 55 | Pr = 1 56 | D_t = 0.25 # Not given in example, taken from another reference. 57 | 58 | # Note: the value for h_g given in the lecture notes, 160e3 W m**-2 K**-1 59 | # appears to be incorrect. Kelly Mathesius and Matt Vernacchia re-did 60 | # the calculation, and instead got 22e3 W m**-2 K**-1. 61 | h_g_exp = 22e3 # Answer from the example 62 | 63 | h_g = convection.bartz(p_c, c_star, D_t, D_t, c_p, mu_e, Pr, sigma) 64 | 65 | self.assertTrue(abs(h_g - h_g_exp) < 0.1e3) 66 | 67 | def test_huzel_43_a1_throat(self): 68 | """Test against Huzel and Huang example problem 4-3, for the A-1 engine 69 | at the throat.""" 70 | # Values given in the problem statement 71 | p_c = 6.9e6 # 1000 psi 72 | c_star = 1725. # 5660 ft second**-1 73 | D_t = 0.633 # 24.9 inch 74 | c_p = 2031. # 0.485 BTU lbm**-1 rankine**-1 75 | mu_e = 7.47e-5 # 4.18e-6 lbm inch**-1 second**-1 76 | Pr = 0.816 # Prandtl number 77 | sigma = 1.0 # Correction factor 78 | 79 | # Answer given for the convection coefficient 80 | h_g_huzel = 7950. # 0.0027 BTU inch**-2 second**-1 fahrenheit**-1 81 | 82 | h_g = convection.bartz(p_c, c_star, D_t, D_t, c_p, mu_e, Pr, sigma) 83 | 84 | self.assertTrue(abs(h_g - h_g_huzel) < 1e3) 85 | 86 | def test_huzel_43_a1_exit(self): 87 | """Test against Huzel and Huang example problem 4-3, for the A-1 engine 88 | at the nozzle exit.""" 89 | # Values given in the problem statement 90 | p_c = 6.9e6 # 1000 psi 91 | c_star = 1725. # 5660 ft second**-1 92 | D_t = 0.633 # 24.9 inch 93 | D = 5**0.5 * D_t # Expansion ratio of 5 94 | c_p = 2031. # 0.485 BTU lbm**-1 rankine**-1 95 | mu_e = 7.47e-5 # 4.18e-6 lbm inch**-1 second**-1 96 | Pr = 0.816 # Prandtl number 97 | sigma = 0.8 # Correction factor 98 | 99 | # Answer given for the convection coefficient 100 | h_g_huzel = 1492. # 0.000507 BTU inch**-2 second**-1 fahrenheit**-1 101 | 102 | h_g = convection.bartz(p_c, c_star, D_t, D, c_p, mu_e, Pr, sigma) 103 | 104 | self.assertTrue(abs(h_g - h_g_huzel) < 0.1e3) 105 | 106 | 107 | class TestBartzSigmaHuzel(unittest.TestCase): 108 | """Unit tests for convection.bartz_sigma_huzel.""" 109 | 110 | def test_huzel_43_a1_throat(self): 111 | """Test against Huzel and Huang example problem 4-3, for the A-1 engine 112 | at the throat.""" 113 | # Values given in the problem statement 114 | T_c = 3411. # 6140 rankine 115 | T_w = 0.8 * T_c # "Since the carbon-deposit approached to the gas temperature, 116 | # a T_w/T_c value of 0.8 is used to determine the sigma values." 117 | M = 1. # Mach=1 at throat 118 | gamma = 1.2 119 | 120 | # Answer given for the correction factor 121 | sigma_huzel = 1. 122 | 123 | sigma = convection.bartz_sigma_huzel(T_c, T_w, M, gamma) 124 | 125 | self.assertTrue(abs(sigma - sigma_huzel) < 0.05) 126 | 127 | def test_huzel_43_a1_exit(self): 128 | """Test against Huzel and Huang example problem 4-3, for the A-1 engine 129 | at the exit.""" 130 | # Values given in the problem statement 131 | T_c = 3411. # 6140 rankine 132 | T_w = 0.8 * T_c # "Since the carbon-deposit approached to the gas temperature, 133 | # a T_w/T_c value of 0.8 is used to determine the sigma values." 134 | gamma = 1.2 135 | M = nozzle.mach_from_er(5., gamma) # Expansion ratio of 5 136 | 137 | # Answer given for the correction factor 138 | sigma_huzel = 0.8 139 | 140 | sigma = convection.bartz_sigma_huzel(T_c, T_w, M, gamma) 141 | 142 | self.assertTrue(abs(sigma - sigma_huzel) < 0.05) 143 | 144 | 145 | class TestFilmCooling(unittest.TestCase): 146 | """Test the film cooling functions.""" 147 | 148 | def test_ms_lec10_1(self): 149 | """Test against the example from Martinez-Sanchez Lecture 10, 150 | with 1% film mass flow.""" 151 | x = 0.5 # Distance downstream [units: meter]. 152 | D = 0.5 # Diameter [units: meter]. 153 | m_dot_core = pi / 4 * D**2 * 5.33 * 253 # Core mass flow [units: kilogram second**-1]. 154 | m_dot_film = (1./99) * m_dot_core 155 | mu_core = 2e-5 / 0.66 # Dynamic viscosity of the core fluid [units: pascal second]. 156 | Pr_film = 0.8 157 | film_param = 1.265 158 | cp_ratio = 0.8 159 | 160 | # Answer from lecture notes 161 | eta_ms = 0.361 162 | 163 | eta = convection.film_efficiency(x, D, m_dot_core, m_dot_film, 164 | mu_core, Pr_film, film_param, cp_ratio) 165 | self.assertTrue(abs(eta - eta_ms) < 0.05) 166 | 167 | def test_ms_lec10_10(self): 168 | """Test against the example from Martinez-Sanchez Lecture 10, 169 | with 10% film mass flow.""" 170 | x = 0.5 # Distance downstream [units: meter]. 171 | D = 0.5 # Diameter [units: meter]. 172 | m_dot_core = pi / 4 * D**2 * 5.33 * 253 # Core mass flow [units: kilogram second**-1]. 173 | m_dot_film = (1./9) * m_dot_core 174 | mu_core = 2e-5 / 0.66 # Dynamic viscosity of the core fluid [units: pascal second]. 175 | Pr_film = 0.8 176 | film_param = 1.265 177 | cp_ratio = 0.8 178 | 179 | # Answer from lecture notes 180 | eta_ms = 1.0 181 | 182 | eta = convection.film_efficiency(x, D, m_dot_core, m_dot_film, 183 | mu_core, Pr_film, film_param, cp_ratio) 184 | self.assertTrue(abs(eta - eta_ms) < 0.05) 185 | 186 | def test_film_adiabatic_wall_temperature(self): 187 | """Test against the example from Martinez-Sanchez Lecture 10""" 188 | T_aw_f = convection.film_adiabatic_wall_temperature(1, 3200, 1600) 189 | self.assertEqual(T_aw_f, 1600) 190 | 191 | T_aw_f = convection.film_adiabatic_wall_temperature(0.361, 3200, 1600) 192 | # Error in notes, correct value is 2622 K, not 2296 K (In the notes, 193 | # 700 K, the wall temp, is incorrectly used in place of 1600 K, the film temp) 194 | self.assertTrue(abs(2622 - T_aw_f) < 1) 195 | 196 | 197 | if __name__ == '__main__': 198 | unittest.main() 199 | -------------------------------------------------------------------------------- /docs/source/electric_tutorial.rst: -------------------------------------------------------------------------------- 1 | Electric Propulsion Basics 2 | ************************** 3 | 4 | Electrostatic Thrust Generation 5 | =============================== 6 | Like all rockets, electric thrusters generate thrust by expelling mass at high velocity. In electric (electrostatic) propulsion, a high-velocity jet is produced by accelerating charged particles through an electric potential. 7 | 8 | The propellant medium in electrostatic thrusters is a plasma consisting of electrons and positively charged ions. The plasma is usually produced by ionizing a gas via electron bombardment. The electric fields within the thruster are configured such that there is a large potential drop between the plasma generation region and the thruster exit. When ions exit the thruster, the are accelerated to high velocities by the electrostatic force. If the ions fall through a beam potential of :math:`V_b`, they will leave the thruster with a velocity 9 | 10 | .. math:: 11 | 12 | v_e = \sqrt{2 V_b \frac{q}{m_i}} 13 | 14 | where :math:`q` is the ion charge and :math:`m_i` is the ion mass. The thrust produced by the ion flow is 15 | 16 | .. math:: 17 | 18 | F &= v_e \dot{m} 19 | 20 | &= \left( \sqrt{2 V_b \frac{q}{m_i}} \right) \left( \frac{m_i}{q} I_b \right) 21 | 22 | &= I_b \sqrt{2 V_b \frac{m_i}{q}} 23 | 24 | where :math:`I_b` is the ion beam current. 25 | 26 | The ideal specific impulse of an electrostatic thruster is: 27 | 28 | .. math:: 29 | 30 | I_{sp} &= \frac{v_e}{g_0} 31 | 32 | &= \frac{1}{g_0} \sqrt{2 V_b \frac{q}{m_i}} 33 | 34 | 35 | As an example, use ``proptools`` to compute the thrust and specific impulse of singly charged Xenon ions with a beam voltage of 1000 V and a beam current of 1 A: 36 | 37 | .. literalinclude:: examples/electric/thrust_isp.py 38 | 39 | .. literalinclude:: examples/electric/thrust_isp_output.txt 40 | 41 | This example illustrates the typical performance of electric propulsion systems: low thrust, high specific impulse, and low mass flow. 42 | 43 | 44 | Advantages over Chemical Propulsion 45 | =================================== 46 | Electric propulsion is appealing because it enables higher specific impulse than chemical propulsion. The specific impulse of a chemical rocket depends on velocity to which a nozzle flow can be accelerated. This velocity is limited by the finite energy content of the working gas. In contrast, the particles leaving an electric thruster can be accelerated to very high velocities if sufficient electrical power is available. 47 | 48 | In a chemical rocket, the kinetic energy of the exhaust gas is supplied by thermal energy released in a combustion reaction. For example, in the stoichiometric combustion of H\ :sub:`2` and O\ :sub:`2` releases an energy per particle of :math:`4.01 \times 10^{-19}` J, or 2.5 eV. If all of the released energy were converted into kinetic energy of the exhaust jet, the maximum possible exhaust velocity would be: 49 | 50 | .. math:: 51 | 52 | v_e = \sqrt{2 \frac{E}{m_{H_2O}}} = 5175 \, \mathrm{m s^{-1}} 53 | 54 | corresponding to a maximum specific impulse of about 530 s. In practice, the most efficient flight engines have specific impulses of 450 to 500 s. 55 | 56 | With electric propulsion, much higher energies per particle are possible. If we accelerate (singly) charged particles through a potential of 1000 V, the jet kinetic energy will be 1000 eV per ion. This is 400 times more energy per particle than is possible with chemical propulsion (or, for similar particle masses, 20 times higher specific impulse). Specific impulse in excess of 10,000 s is feasible with electric propulsion. 57 | 58 | 59 | Power and Efficiency 60 | ==================== 61 | 62 | In an electric thruster, the energy to accelerate the particles in the jet must be supplied by an external source. Typically solar panels supply this electrical power, but some future concepts might use nuclear reactors. The available power limits the thrust and specific impulse of an electric thruster. 63 | 64 | The kinetic power of the jet is given by: 65 | 66 | .. math:: 67 | 68 | P_{jet} = \frac{F^2}{2 \dot{m}} 69 | 70 | where :math:`\dot{m}` is the jet mass flow. Increasing thrust with increasing mass flow (i.e. increasing :math:`I_{sp}`) will increase the kinetic power of the jet. 71 | 72 | The power input required by the thruster (:math:`P_{in}`) is somewhat higher than the jet power. The ratio of the jet and input power is the total efficiency of the thruster, :math:`\eta_T`: 73 | 74 | .. math:: 75 | 76 | \eta_T \equiv \frac{P_{jet}}{P_{in}} 77 | 78 | The total efficiency depends on several factors: 79 | 80 | #. Thrust losses due to beam divergence. This loss is proportional to the cosine of the average beam divergence half-angle. 81 | #. Thrust losses due to doubly charged ions in the beam. This loss is a function of the doubles-to-singles current ratio, :math:`I^{++}/I^+` 82 | #. Thrust losses due to propellant gas escaping the thruster without being ionized. The fraction of the propellant mass flow which is ionized and enters the beam is the mass utilization efficiency, :math:`\eta_m`. 83 | #. Electrical losses incurred in ion generation, power conversion, and powering auxiliary thruster components. These losses are captured by the electrical efficiency, :math:`\eta_e = \frac{I_b V_b}{P_{in}}` 84 | 85 | Use ``proptools`` to compute the efficiency and required power of the example thruster. Assume that the beam divergence is :math:`\cos(10 \unicode{xb0})`, the double ion current fraction is 10%, the mass utilization efficiency is 90%, and the electrical efficiency is 85%: 86 | 87 | .. literalinclude:: examples/electric/power.py 88 | 89 | .. literalinclude:: examples/electric/power_output.txt 90 | 91 | The overall efficiency of the thruster is about 70%. The required input power could be supplied by a few square meters of solar panels (at 1 AU from the sun). 92 | 93 | The power, thrust, and specific impulse of a thruster are related by: 94 | 95 | .. math:: 96 | 97 | \frac{F}{P_{in}} = \frac{2 \eta_T}{g_0 I_{sp}} 98 | 99 | Thus, for a power-constrained system the propulsion designer faces a trade-off between thrust and specific impulse. 100 | 101 | .. plot:: examples/electric/plots/thrust_power_isp.py 102 | :include-source: 103 | :align: center 104 | 105 | 106 | Optimal Specific Impulse 107 | ======================== 108 | 109 | For electrically propelled spacecraft, there is an optimal specific impulse which will maximize the payload mass fraction of a given mission. While increasing specific impulse decreases the required propellant mass, it also increases the required power at a particular thrust level, which increases the mass of the power supply. The optimal specific impulse minimizes the combined mass of the propellant and power supply. 110 | 111 | The optimal specific impulse depends on several factors: 112 | 113 | #. The mission thrust duration, :math:`t_m`. Longer thrust durations reduce the required thrust (if the same total impulse or :math:`\Delta v` is to be delivered), and therefore reduce the power and power supply mass at a given :math:`I_{sp}`. Therefore, longer thrust durations increase the optimal :math:`I_{sp}`. 114 | #. The specific mass of the power supply, :math:`\alpha`. This is the ratio of power supply mass to power, and is typically 20 to 200 kg kW :sup:`-1` for solar-electric systems. The specific impulse optimization assumes that power supply mass is linear with respect to power. Increasing the specific mass reduces the optimal :math:`I_{sp}`. 115 | #. The total efficiency of the thruster. 116 | #. The :math:`\Delta v` of the mission. Higher :math:`\Delta v` (in a fixed time window) requires more thrust, and therefore leads to a lower optimal :math:`I_{sp}`. 117 | 118 | 119 | Consider an example mission to circularize the orbit of a geostationary satellite launched onto a elliptical transfer orbit. Assume that the low-thrust circularization maneuver requires a :math:`\Delta v` of 2 km s :sup:`-1` over 100 days. The thruster is 70% efficient and the power supply specific mass is 50 kg kW :sup:`-1`: 120 | 121 | .. literalinclude:: examples/electric/isp_opt.py 122 | 123 | .. literalinclude:: examples/electric/isp_opt_output.txt 124 | 125 | 126 | For the mathematical details of specific impulse optimization, see [Lozano]_. 127 | 128 | .. [Lozano] P. Lozano, *16.522 Lecture Notes*, Lecture 3-4 Mission Analysis for Electric Propulsion. Online: https://ocw.mit.edu/courses/aeronautics-and-astronautics/16-522-space-propulsion-spring-2015/lecture-notes/MIT16_522S15_Lecture3-4.pdf 129 | -------------------------------------------------------------------------------- /proptools/electric/electric_test.py: -------------------------------------------------------------------------------- 1 | """Unit tests for proptools.electric.""" 2 | 3 | import unittest 4 | import random 5 | import numpy as np 6 | from proptools import electric 7 | import proptools.constants 8 | 9 | class TestThrust(unittest.TestCase): 10 | """Unit tests for electric.thrust.""" 11 | 12 | def test_gk_239(self): 13 | """Test against the rule-of-thumb equation for Xe given in 14 | Goebel and Katz equation 2.3-9.""" 15 | random.seed(357) # Seed random for test repeatability. 16 | for i in range(10): 17 | I_b = random.random() # Beam current [units: ampere]. 18 | V_b = 100 * random.random() # Beam voltage [units: volt]. 19 | F_gk_239 = 1.65 * I_b * V_b**0.5 * 1e-3 20 | F = electric.thrust(I_b, V_b, electric.m_Xe) 21 | self.assertAlmostEqual(F, F_gk_239, places=1) 22 | 23 | class TestJetPower(unittest.TestCase): 24 | """Unit tests for electric.jet_power.""" 25 | 26 | def test_unity(self): 27 | """Power should be 1/2 if F=1 and m_dot=1.""" 28 | self.assertEqual(0.5, electric.jet_power(1, 1)) 29 | 30 | def test_ten(self): 31 | """Power should be 50 if F=10 and m_dot=1.""" 32 | self.assertEqual(50, electric.jet_power(10, 1)) 33 | 34 | class TestThrustCorrection(unittest.TestCase): 35 | """Unit test for electric.double_ion_thrust_correction.""" 36 | 37 | def test_gk_example_gamma(self): 38 | """Test against the example value of gamma given in Goebel and Katz page 24. 39 | 40 | "For example, assuming an ion thruster with a 10-deg half-angle beam divergence and 41 | # a 10% doubles-to-singles ratio results in gamma=0.958." 42 | """ 43 | gamma_gk = 0.958 44 | alpha = electric.double_ion_thrust_correction(0.1) 45 | self.assertAlmostEqual(gamma_gk, alpha * np.cos(np.deg2rad(10.)), places=2) 46 | 47 | def test_exception(self): 48 | """Check that the function raises a value error for invalid double_fraction.""" 49 | with self.assertRaises(ValueError): 50 | electric.double_ion_thrust_correction(-1) 51 | 52 | 53 | class TestIsp(unittest.TestCase): 54 | """Unit test for electric.specific_impusle.""" 55 | 56 | def test_gk_example(self): 57 | """Test against the example values given in Goebel and Katz page 27. 58 | 59 | "Using our previous example of a 10-deg half-angle beam divergence and a 10% 60 | doubles-to-singles ratio with a 90% propellant utilization of xenon at [...] 1500 V, 61 | the Isp is [...] 4127 s" 62 | """ 63 | I_sp_gk = 4127 # Correct I_sp value from Goebel and Katz [units: second]. 64 | V_b = 1500 # Beam voltage [units: volt]. 65 | divergence_correction = np.cos(np.deg2rad(10.)) # Beam divergence correction factor. 66 | I_sp = electric.specific_impulse(V_b, electric.m_Xe, 67 | divergence_correction=divergence_correction, 68 | double_fraction=0.1, 69 | mass_utilization=0.9) 70 | self.assertLess(abs(I_sp - I_sp_gk), 3) 71 | 72 | 73 | def test_exception(self): 74 | """Check that the function raises a value error for invalid inputs.""" 75 | with self.assertRaises(ValueError): 76 | electric.specific_impulse(1, 1, divergence_correction=-1) 77 | with self.assertRaises(ValueError): 78 | electric.specific_impulse(1, 1, mass_utilization=-1) 79 | 80 | 81 | class TestTotalEffciency(unittest.TestCase): 82 | """Unit tests for electric.total_efficiency""" 83 | 84 | def test_gk_example(self): 85 | """Test against the example values given in Goebel and Katz page 29. 86 | 87 | "Using our previous example of an ion thruster with 10-deg half-angle divergence, 88 | 10% double ion current, 90% mass utilization efficiency and [...] beam at 1500 V 89 | [...] the electrical efficiency is [...] 0.857 [...] and the total efficiency is 90 | [...] 0.708" 91 | """ 92 | eta_t_gk = 0.708 # Correct total efficiency value from Goebel and Katz [units: dimensionless]. 93 | V_b = 1500 # Beam voltage [units: volt]. 94 | divergence_correction = np.cos(np.deg2rad(10.)) # Beam divergence correction factor. 95 | eta_t = electric.total_efficiency(divergence_correction=divergence_correction, 96 | double_fraction=0.1, 97 | mass_utilization=0.9, 98 | electrical_efficiency=0.857) 99 | self.assertAlmostEqual(eta_t_gk, eta_t, places=2) 100 | 101 | def test_exception(self): 102 | """Check that the function raises a value error for invalid inputs.""" 103 | with self.assertRaises(ValueError): 104 | electric.total_efficiency(divergence_correction=-1) 105 | with self.assertRaises(ValueError): 106 | electric.total_efficiency(mass_utilization=-1) 107 | with self.assertRaises(ValueError): 108 | electric.total_efficiency(electrical_efficiency=-1) 109 | 110 | 111 | class TestThrustPerPower(unittest.TestCase): 112 | """Unit tests for electric.thrust_per_power.""" 113 | 114 | def test_gk_example(self): 115 | """Test against the example values given in Goebel and Katz. 116 | "thrust produced is 122.4 mN [...] 2 A beam at 1500 V [...] dissipated power of 528.3 W" 117 | """ 118 | eta_t = 0.708 # Total efficiency [units: dimensionless]. 119 | I_sp = 4127 # Specific impulse [units: seconds]. 120 | P_in = 2 * 1500 + 528.3 # Input power [units: watt]. 121 | F = 122.4e-3 # Thrust force [units: watt]. 122 | fp = electric.thrust_per_power(I_sp, eta_t) 123 | self.assertAlmostEqual(fp, F / P_in, places=6) 124 | 125 | def test_exception(self): 126 | """Check that the function raises a value error for invalid inputs.""" 127 | with self.assertRaises(ValueError): 128 | electric.thrust_per_power(1, total_efficiency=-1) 129 | 130 | 131 | class TestStuhlingerVelocity(unittest.TestCase): 132 | """Unit tests for electric.stuhlinger_velocity.""" 133 | 134 | def test_example(self): 135 | """Prof Lozano's notes do not provide numerical examples; I made this up.""" 136 | # Compare to https://www.wolframalpha.com/input/?i=(2+*+0.708+*+(116+days)+%2F+(100+kilogram%2Fkilowatt))%5E0.5 137 | eta_t = 0.708 # Total efficiency [units: dimensionless]. 138 | t_m = 10e6 # Mission time (about 116 days) [units: seconds]. 139 | specific_mass = 100e-3 # Propulsion+power specific mass [units: kilogram watt**-1]. 140 | v_ch = electric.stuhlinger_velocity(eta_t, t_m, specific_mass) 141 | self.assertLess(abs(v_ch - 11.9e3), 30) 142 | 143 | def test_exception(self): 144 | """Check that the function raises a value error for invalid inputs.""" 145 | with self.assertRaises(ValueError): 146 | electric.stuhlinger_velocity(-1, 1, 1) 147 | 148 | 149 | class TestOptimalIspThrustTime(unittest.TestCase): 150 | """Unit tests for electric.optimal_isp_thrust_time.""" 151 | 152 | def test_example(self): 153 | """Prof Lozano's notes do not provide numerical examples; I made this up.""" 154 | # Compare to https://www.wolframalpha.com/input/?i=(2+*+0.708+*+(116+days)+%2F+(100+kilogram%2Fkilowatt))%5E0.5+%2F+(9.81+meter+*+second%5E-2) 155 | eta_t = 0.708 # Total efficiency [units: dimensionless]. 156 | t_m = 10e6 # Mission time (about 116 days) [units: seconds]. 157 | specific_mass = 100e-3 # Propulsion+power specific mass [units: kilogram watt**-1]. 158 | I_sp_opt = electric.optimal_isp_thrust_time(eta_t, t_m, specific_mass) 159 | self.assertLess(abs(I_sp_opt - 1214), 5) 160 | 161 | class TestOptimalIspDeltaV(unittest.TestCase): 162 | """Unit tests for electric.optimal_isp_delta_v.""" 163 | 164 | def test_const_eff(self): 165 | """Test at constant efficiency.""" 166 | # Compare to Lozano 16.522 note lect 3 figure 1. 167 | eta_t = 0.708 # Total efficiency [units: dimensionless]. 168 | t_m = 10e6 # Mission time (about 116 days) [units: seconds]. 169 | specific_mass = 100e-3 # Propulsion+power specific mass [units: kilogram watt**-1]. 170 | v_ch = electric.stuhlinger_velocity(eta_t, t_m, specific_mass) 171 | dv = 0.2 * v_ch # Delta-v [units: meter second**-1]. 172 | 173 | # Optimal I_sp read off figure 1 for dv/v_ch = 0.2. 174 | I_sp_opt_fig1 = 0.90 * v_ch / proptools.constants.g 175 | 176 | I_sp_opt = electric.optimal_isp_delta_v(dv, eta_t, t_m, specific_mass) 177 | 178 | self.assertLess(abs(I_sp_opt - I_sp_opt_fig1), 20) 179 | 180 | def test_variable_efficiency(self): 181 | """Test that introducing the discharge loss increases the optimal specific impulse.""" 182 | eta_t = 0.708 # Total efficiency [units: dimensionless]. 183 | t_m = 10e6 # Mission time (about 116 days) [units: seconds]. 184 | specific_mass = 100e-3 # Propulsion+power specific mass [units: kilogram watt**-1]. 185 | v_ch = electric.stuhlinger_velocity(eta_t, t_m, specific_mass) 186 | dv = 0.2 * v_ch # Delta-v [units: meter second**-1]. 187 | 188 | I_sp_opt_const = electric.optimal_isp_delta_v(dv, eta_t, t_m, specific_mass) 189 | I_sp_opt_var = electric.optimal_isp_delta_v(dv, eta_t, t_m, specific_mass, 190 | discharge_loss=200, 191 | m_ion=electric.m_Xe) 192 | self.assertGreater(I_sp_opt_var, I_sp_opt_const) 193 | 194 | 195 | if __name__ == '__main__': 196 | unittest.main() 197 | -------------------------------------------------------------------------------- /proptools/nonsimple_comp_flow.py: -------------------------------------------------------------------------------- 1 | ''' Non-simple compressible flow. 2 | 3 | Calculate quasi-1D compressible flow properties with varying area, friction, and heat addition. 4 | "One-dimensional compressible flows of calorically perfect gases in which only a single driving potential is 5 | present are called simple flows" [1]. This module implements a numerical solution for non-simple flows, 6 | i.e. flows with multiple driving potentials. 7 | 8 | References: 9 | [1] L. Pekker, "One-Dimensional Compressible Flow in Variable Area Duct with Heat Addition," 10 | Air Force Research Laboratory, Edwards, CA, Rep. AFRL-RZ-ED-JA-2010-303, 2010. 11 | Online: http://www.dtic.mil/dtic/tr/fulltext/u2/a524450.pdf. 12 | 13 | [2] A. Bandyopadhyay and A. Majumdar, "Modeling of Compressible Flow with Friction and Heat 14 | Transfer using the Generalized Fluid System Simulation Program (GFSSP)," 15 | Thermal Fluid Analysis Workshop, Cleveland, OH, 2007. 16 | Online: https://tfaws.nasa.gov/TFAWS07/Proceedings/TFAWS07-1016.pdf 17 | 18 | [3] J. D. Anderson, Modern Compressible Flow with Historical Perspective, 2nd ed. 19 | New York, NY: McGraw-Hill, 1990. 20 | 21 | Matt Vernacchia 22 | proptools 23 | 2016 Oct 3 24 | ''' 25 | 26 | from scipy.misc import derivative 27 | from scipy.integrate import ode 28 | import numpy as np 29 | 30 | from proptools import nozzle 31 | 32 | 33 | def differential(x, state, mdot, c_p, gamma, f_f, f_q, f_A): 34 | ''' Differential equation for Mach number in non-simple duct flow. 35 | 36 | Note: This method will not be accurate (and may divide by zero) for flows which 37 | contain a region at Mach 1, e.g. a choked convergent-divergent nozzle. 38 | 39 | Arguments: 40 | state (2-vector): Stagnation temperature [units: kelvin], 41 | Mach number [units: none]. 42 | x (scalar): Distance from the duct inlet [units: meter]. 43 | mdot (scalar): The mass flow through the duct [units: kilogram second**-1]. 44 | c_p (scalar): Fluid heat capacity at constant pressure [units: joule kilogram**-1 kelvin**-1]. 45 | gamma (scalar): Fluid ratio of specific heats [units: none]. 46 | f_f (function mapping scalar->scalar): The Fanning friction factor 47 | as a function of distance from the inlet [units: none]. 48 | f_q (function mapping scalar->scalar): The heat transfer into the fluid 49 | per unit wall area as a function of distance from the inlet 50 | [units: joule meter**-2]. 51 | f_A (function mapping scalar->scalar): The duct area as a function of 52 | distance from the inlet [units: meter**2]. 53 | 54 | Returns: 55 | d state / dx 56 | ''' 57 | T_o = state[0] 58 | M = state[1] 59 | 60 | # Duct area 61 | A = f_A(x) 62 | # Duct diameter 63 | D = (A / np.pi)**0.5 64 | # Duct area derivative 65 | dA_dx = derivative(f_A, x, dx=1e-6) 66 | 67 | # Use Equation 4 from Bandyopadhyay to find dT_o/dx. 68 | dT_o_dx = f_q(x) * np.pi * D / (mdot * c_p) 69 | 70 | # Use Equation 3 from Bandyopadhyay to find dM/dx. 71 | # Note: There is an error in Bandyopadhyay's equation. The area 72 | # term should not be multiplied by gamma * M**2. Pekker's equation 10 73 | # shows the correct area term; but lacks a friction term. 74 | # Note: Compared to Anderson's Eqn 3.96, Bandyopadhyay's equation 75 | # is missing a factor of 2 on the friction term. I use Anderson's 76 | # friction term here. 77 | dM_dx = M * (1 + (gamma - 1) / 2 * M**2) / (1 - M**2) \ 78 | * ( \ 79 | gamma * M**2 * 2 * f_f(x) / D \ 80 | + (1 + gamma * M**2) / (2 * T_o) * dT_o_dx \ 81 | - dA_dx / A 82 | ) 83 | return [dT_o_dx, dM_dx] 84 | 85 | 86 | def solve_nonsimple(x, M_in, T_o_in, mdot, c_p, gamma, f_f, f_q, f_A): 87 | ''' Solve a non-simple flow case 88 | 89 | Arguments: 90 | state (2-vector): Stagnation temperature [units: kelvin], 91 | Mach number [units: none]. 92 | x (array): Distances from the duct inlet at which to return solution [units: meter]. 93 | T_o_in (scalar): Inlet stagnation temperature [units: kelvin]. 94 | M_in (scalar): Inlet Mach number [units: none]. 95 | mdot (scalar): The mass flow through the duct [units: kilogram second**-1]. 96 | c_p (scalar): Fluid heat capacity at constant pressure [units: joule kilogram**-1 kelvin**-1]. 97 | gamma (scalar): Fluid ratio of specific heats [units: none]. 98 | f_f (function mapping scalar->scalar): The Fanning friction factor 99 | as a function of distance from the inlet [units: none]. 100 | f_q (function mapping scalar->scalar): The heat transfer into the fluid 101 | per unit wall area as a function of distance from the inlet 102 | [units: joule meter**-2]. 103 | f_A (function mapping scalar->scalar): The duct area as a function of 104 | distance from the inlet [units: meter**2]. 105 | 106 | Returns: 107 | T_o (array of length len(x)): The stagnation temperature at each station in x [units: none]. 108 | M (array of length len(x)): The Mach number at each station in x [units: none]. 109 | choked (boolean): True if the flow chokes at M=1 in the duct. M and T_o for x past the 110 | choke point will be nan. Choking can cause shocks or upstream effects which this model 111 | does not capture; therefore results for choked scenarios may not be accurate. 112 | ''' 113 | T_o = np.full(len(x), np.nan) 114 | M = np.full(len(x), np.nan) 115 | solver = ode(differential).set_f_params(mdot, c_p, gamma, f_f, f_q, f_A) 116 | solver.set_initial_value([T_o_in, M_in], 0) 117 | T_o[0] = T_o_in 118 | M[0] = M_in 119 | i = 1 120 | while solver.successful() and i < len(x): 121 | y = solver.integrate(x[i]) 122 | T_o[i] = y[0] 123 | M[i] = y[1] 124 | i += 1 125 | choked = False 126 | if not solver.successful() and abs(solver.y[1] - 1) < 1e-3: 127 | choked = True 128 | return (T_o, M, choked) 129 | 130 | 131 | def main(): 132 | def f_f(x): 133 | return 0 134 | def f_q(x): 135 | return 0 136 | def f_A(x): 137 | return 1 + x 138 | mdot = 100 139 | c_p = 2000 140 | gamma = 1.4 141 | R = c_p * (1 - 1 / gamma) 142 | x = np.linspace(0, 1) 143 | T_o_in = 300 144 | 145 | for M_in in [0.2, 0.8, 1.2, 2]: 146 | T_in = T_o_in * (1 + (gamma - 1) / 2 * M_in**2)**-1 147 | v_in = M_in * (gamma * R * T_in)**0.5 148 | rho_in = mdot / (v_in * f_A(0)) 149 | p_in = rho_in * R * T_in 150 | p_o_in = p_in * (T_o_in / T_in)**(gamma / (gamma -1)) 151 | 152 | 153 | A_t = nozzle.throat_area(mdot, p_o_in, T_o_in, gamma, nozzle.R_univ / R) 154 | 155 | if M_in < 1: 156 | M_area = [nozzle.mach_from_area_subsonic(f_A(xi) / A_t, gamma) for xi in x] 157 | else: 158 | M_area = [nozzle.mach_from_er(f_A(xi) / A_t, gamma) for xi in x] 159 | 160 | T_o, M, choked = solve_nonsimple(x, M_in, T_o_in, mdot, c_p, gamma, f_f, f_q, f_A) 161 | 162 | plt.subplot(2,1,1) 163 | plt.plot(x, T_o) 164 | plt.xlabel('x [m]') 165 | plt.ylabel('T_o [K]') 166 | 167 | plt.subplot(2,1,2) 168 | plt.plot(x, M_area, label='Simple area solution $M_{{in}}$={:.1f}'.format(M_in), 169 | marker='x') 170 | plt.plot(x, M, label='Non-simple solution $M_{{in}}$={:.1f}'.format(M_in), 171 | marker='+') 172 | plt.xlabel('x [m]') 173 | plt.ylabel('M [-]') 174 | plt.legend() 175 | plt.suptitle('Demonstration with linear area variation, no heat addition, no friction') 176 | 177 | # Fanno Flow demo 178 | plt.figure() 179 | f = 0.005 180 | def f_f(x): 181 | return f 182 | def f_q(x): 183 | return 0 184 | def f_A(x): 185 | return 1 186 | 187 | for M_in in [0.2, 0.8, 1.2, 2]: 188 | # Duct diameter 189 | D = (f_A(0) / np.pi)**0.5 190 | # Choking length 191 | # Anderson Modern Compressible Flow Equation 3.107 192 | L = D / (4 * f) * ((1 - M_in**2) / (gamma * M_in**2) + (gamma + 1) / (2 * gamma) \ 193 | * np.log((gamma + 1) * M_in**2 / (2 + (gamma - 1) * M_in**2))) 194 | x = np.linspace(0, L) 195 | T_in = T_o_in * (1 + (gamma - 1) / 2 * M_in**2)**-1 196 | v_in = M_in * (gamma * R * T_in)**0.5 197 | rho_in = mdot / (v_in * f_A(0)) 198 | p_in = rho_in * R * T_in 199 | p_o_in = p_in * (T_o_in / T_in)**(gamma / (gamma -1)) 200 | 201 | T_o, M, choked = solve_nonsimple(x, M_in, T_o_in, mdot, c_p, gamma, f_f, f_q, f_A) 202 | 203 | plt.subplot(2,1,1) 204 | plt.plot(x, T_o) 205 | plt.xlabel('x [m]') 206 | plt.ylabel('T_o [K]') 207 | 208 | plt.subplot(2,1,2) 209 | plt.plot(x, M, label='$M_{{in}}$={:.1f}, {:s}'.format(M_in, 'choked' if choked else 'not choked'), 210 | marker='+') 211 | plt.xlabel('x [m]') 212 | plt.ylabel('M [-]') 213 | plt.axhline(y=1, color='black') 214 | plt.legend() 215 | plt.suptitle('Demonstration of Fanno Flow') 216 | 217 | # Rayleigh Flow demo 218 | plt.figure() 219 | q = 1e5 220 | def f_f(x): 221 | return 0 222 | def f_q(x): 223 | return q 224 | def f_A(x): 225 | return 1 226 | 227 | for M_in in [0.2, 0.8, 1.2, 2]: 228 | x = np.linspace(0, 100) 229 | 230 | T_o, M, choked = solve_nonsimple(x, M_in, T_o_in, mdot, c_p, gamma, f_f, f_q, f_A) 231 | 232 | plt.subplot(2,1,1) 233 | plt.plot(x, T_o) 234 | plt.xlabel('x [m]') 235 | plt.ylabel('T_o [K]') 236 | 237 | plt.subplot(2,1,2) 238 | plt.plot(x, M, label='$M_{{in}}$={:.1f}, {:s}'.format(M_in, 'choked' if choked else 'not choked'), 239 | marker='+') 240 | plt.xlabel('x [m]') 241 | plt.ylabel('M [-]') 242 | plt.axhline(y=1, color='black') 243 | plt.legend() 244 | plt.suptitle('Demonstration of Rayleigh Flow') 245 | plt.show() 246 | 247 | 248 | 249 | if __name__ == '__main__': 250 | from matplotlib import pyplot as plt 251 | main() 252 | -------------------------------------------------------------------------------- /proptools/tank_structure.py: -------------------------------------------------------------------------------- 1 | ''' Tank structure calculations. 2 | 3 | Matt Vernacchia 4 | proptools 5 | 2015 April 17 6 | ''' 7 | 8 | from math import pi 9 | import numpy as np 10 | 11 | def crown_thickness(p_t, R, stress, weld_eff): 12 | ''' Crown thickness of a spherical or ellipsoidal tank end. 13 | 14 | Implements eqn 8-16 from Huzel and Huang. The crown is the center of the 15 | tank end, see figure 8-6 in Huzel and Huang. 16 | 17 | Arguments: 18 | p_t: Tank internal pressure (less the ambient pressure) [units: pascal]. 19 | R: Crown radius [units: meter]. 20 | stress: Max allowable stress in thank wall material [units: pascal]. 21 | weld_eff: Weld efficiency, scalar in [0, 1] [units: none]. 22 | 23 | Returns: 24 | crown thickness [units: meter]. 25 | ''' 26 | return p_t * R / (2 * stress * weld_eff) 27 | 28 | 29 | def knuckle_thickness(p_t, a, b, stress, weld_eff): 30 | ''' Knuckle thickness of a ellipsoidal tank end. 31 | 32 | Implements eqn 8-15 from Huzel and Huang. The knuckle is the transition from 33 | the cylindrical section to the tank end, see figure 8-6 in Huzel and Huang. 34 | 35 | Arguments: 36 | p_t: Tank internal pressure (less the ambient pressure) [units: pascal]. 37 | a: Tank radius [units: meter]. 38 | a: Semiminor axis [units: meter]. 39 | stress: Max allowable stress in thank wall material [units: pascal]. 40 | weld_eff: Weld efficiency, scalar in [0, 1] [units: none]. 41 | 42 | Returns: 43 | knuckle thickness [units: meter]. 44 | ''' 45 | # K is the stress factor, see figure 8-7 in Huzel and Huang. Its value is 46 | # 0.67 for spherical ends, and increases for ellipsoidal ends. 47 | K = knuckle_factor(a / b) 48 | return K * p_t * a / (stress * weld_eff) 49 | 50 | 51 | def cylinder_thickness(p_t, a, stress, weld_eff): 52 | ''' Thickness of a cylindrical tank section. 53 | 54 | Implements eqn 8-28 from Huzel and Huang. 55 | 56 | Arguments: 57 | p_t: Tank internal pressure (less the ambient pressure) [units: pascal]. 58 | a: Tank radius [units: meter]. 59 | stress: Max allowable stress in thank wall material [units: pascal]. 60 | weld_eff: Weld efficiency, scalar in [0, 1] [units: none]. 61 | 62 | Returns: 63 | cylinder thickness [units: meter]. 64 | ''' 65 | return p_t * a / (stress * weld_eff) 66 | 67 | 68 | def sphere_thickness(p_t, a, stress, weld_eff): 69 | ''' Thickness of a spherical tank. 70 | 71 | Implements eqn 8-9 from Huzel and Huang. 72 | 73 | Arguments: 74 | p_t: Tank internal pressure (less the ambient pressure) [units: pascal]. 75 | a: Tank radius [units: meter]. 76 | stress: Max allowable stress in thank wall material [units: pascal]. 77 | weld_eff: Weld efficiency, scalar in [0, 1] [units: none]. 78 | 79 | Returns: 80 | sphere thickness [units: meter]. 81 | ''' 82 | return p_t * a / (2 * stress * weld_eff) 83 | 84 | 85 | def max_axial_load(p_t, a, t_c, l_c, E): 86 | '''Maximum compressive axial load that a cylindrical section can support. 87 | 88 | Implements eqn 8-33 from Huzel and Huang. 89 | 90 | Arguments: 91 | p_t: Tank internal pressure (less the ambient pressure) [units: pascal]. 92 | a: Tank radius [units: meter]. 93 | t_c: Cylinder wall thickness [units: meter]. 94 | l_c: Cylinder length [units: meter]. 95 | E: Wall material modulus of elasticity [units: pascal]. 96 | 97 | Returns: 98 | Critical compressive axial load [units: newtons]. 99 | ''' 100 | # Critical axial stress (unpressurized) [units: pascal]. 101 | S_c = (9 * (t_c / a)**1.6 + 0.16 * (t_c / l_c)**1.3) * E 102 | # Axial cross section area [units: meter**2]. 103 | A_section = 2 * pi * a * t_c 104 | # Critical axial compression load (unpressurized) [units: newton]. 105 | F_c = S_c * A_section 106 | # Axial tension load due to internal pressure [units: newton]. 107 | F_a = pi * a**2 * p_t 108 | # The total critical compressive load is the load supported by the tank 109 | # walls plus the load supported by the internal pressure. 110 | return F_c + F_a 111 | 112 | 113 | def cylinder_mass(a, t_c, l_c, rho): 114 | ''' Mass of a cylindrical tank section. 115 | 116 | Arguments: 117 | a: Tank radius [units: meter]. 118 | t_c: Tank wall thickness [units: meter]. 119 | l_c: Cylinder length [units: meter]. 120 | rho: Tank material density [units: kilogram meter**-3]. 121 | 122 | Returns: 123 | mass of spherical tank [kilogram]. 124 | ''' 125 | return 2 * pi * a * l_c * t_c * rho 126 | 127 | 128 | def sphere_mass(a, t, rho): 129 | ''' Mass of a spherical tank. 130 | 131 | Arguments: 132 | a: Tank radius [units: meter]. 133 | t: Tank wall thickness [units: meter]. 134 | rho: Tank material density [units: kilogram meter**-3]. 135 | 136 | Returns: 137 | mass of spherical tank [kilogram]. 138 | ''' 139 | return 4 * pi * a**2 * t * rho 140 | 141 | 142 | def ellipse_mass(a, b, t, rho): 143 | ''' Mass of a ellipsoidal tank. 144 | 145 | Arguments: 146 | a: Tank radius [units: meter]. 147 | b: Tank semimajor axis [units: meter]. 148 | t: Tank wall thickness [units: meter]. 149 | rho: Tank material density [units: kilogram meter**-3]. 150 | 151 | Returns: 152 | mass of ellipsoidal tank [kilogram]. 153 | ''' 154 | k = a / b 155 | return pi * a**2 * t * rho * ellipse_design_factor(k) / k 156 | 157 | 158 | def cr_ex_press_sphere(a, t, E, v): 159 | ''' Critical external pressure difference to buckle a spherical tank. 160 | 161 | Implements eqn 8-12 in Huzel and Huang. 162 | 163 | Arguments: 164 | a: Tank radius [units: meter]. 165 | t: Wall thickness [units: meter]. 166 | E: Wall material modulus of elasticity [units: pascal]. 167 | v: Wall material Poisson's ratio [units: none]. 168 | 169 | Returns: 170 | Critical external pressure for buckling [units: pascal]. 171 | ''' 172 | return 2 * E * t**2 / a**2 * (3 * (1 - v**2))**0.5 173 | 174 | 175 | def cr_ex_press_sphere_end(a, t, E): 176 | ''' Critical external pressure difference to buckle a spherical tank end. 177 | 178 | Implements eqn 8-26 in Huzel and Huang. 179 | 180 | Arguments: 181 | a: Tank radius [units: meter]. 182 | t: Wall thickness [units: meter]. 183 | E: Wall material modulus of elasticity [units: pascal]. 184 | 185 | Returns: 186 | Critical external pressure for buckling [units: pascal]. 187 | ''' 188 | return 0.342 * E * t**2 / a**2 189 | 190 | 191 | def cr_ex_press_ellipse_end(a, b, t, E, C_b=0.05): 192 | ''' Critical external pressure difference to buckle a ellipsoidal tank end. 193 | 194 | Implements eqn 8-25 in Huzel and Huang. 195 | 196 | Arguments: 197 | a: Tank radius [units: meter]. 198 | a: Semiminor axis [units: meter]. 199 | t: Wall thickness [units: meter]. 200 | E: Wall material modulus of elasticity [units: pascal]. 201 | Cb: Buckling coefficient [units: none]. 202 | 203 | Returns: 204 | Critical external pressure for buckling [units: pascal]. 205 | ''' 206 | return C_b * 2 * E * t**2 / (a / b * a)**2 207 | 208 | 209 | def cr_ex_press_cylinder(a, t_c, l_c, E, v): 210 | ''' Critical external pressure difference to buckle a cylindrical tank section. 211 | 212 | Implements eqn 8-12 in Huzel and Huang. 213 | 214 | Arguments: 215 | a: Tank radius [units: meter]. 216 | t_c: Tank wall thickness [units: meter]. 217 | l_c: Cylinder length [units: meter]. 218 | E: Wall material modulus of elasticity [units: pascal]. 219 | v: Wall material Poisson's ratio [units: none]. 220 | 221 | Returns: 222 | Critical external pressure for buckling [units: pascal]. 223 | ''' 224 | if l_c < 4.9 * a * (a / t_c)**0.5: 225 | # Short tank. 226 | return 0.807 * E * t_c**2 / (l_c * a) \ 227 | * ( (1 / (1 - v**2))**3 * t_c**2 / a**2)**0.25 228 | else: 229 | # Long tank. 230 | return E * t_c**3 / (4 * (1 - v**2) * a**3) 231 | 232 | 233 | def sphere_volume(a): 234 | '''Volume enclosed by a spherical tank. 235 | 236 | Arguments: 237 | a: Tank radius [units: meter]. 238 | 239 | Returns: 240 | tank volume [units: meter**3]. 241 | ''' 242 | return 4.0 / 3 * pi * a**3 243 | 244 | 245 | def ellipse_volume(a, b): 246 | '''Volume enclosed by a ellipsoidal tank. 247 | 248 | Arguments: 249 | a: Tank radius (semimajor axis) [units: meter]. 250 | a: Semiminor axis [units: meter]. 251 | 252 | Returns: 253 | tank volume [units: meter**3]. 254 | ''' 255 | return 4.0 / 3 * pi * a**2 * b 256 | 257 | 258 | def cylinder_volume(a, l_c): 259 | '''Volume enclosed by a cylindrical tank section. 260 | 261 | Arguments: 262 | a: Tank radius [units: meter]. 263 | l_c: Cylinder length [units: meter]. 264 | 265 | Returns: 266 | tank volume [units: meter**3]. 267 | ''' 268 | return pi * a**2 * l_c 269 | 270 | 271 | def knuckle_factor(ellipse_ratio): 272 | '''Get the knuckle factor K for an ellipse ratio. 273 | 274 | Implements the "Envelope Curve for K for Combined Stress" curve from 275 | figure 8-7 in Huzel and Huang. 276 | 277 | Arguments: 278 | ellipse_ratio: Ratio of major and minor axes of ellipse end [units: none]. 279 | 280 | Returns: 281 | knuckle factor K for ellipsoidal end stress calculations [units: none]. 282 | ''' 283 | # Linear fit to: 284 | # 1.0 -> 0.67 285 | # 1.75 -. 0.95 286 | return 0.67 + (ellipse_ratio - 1) * (0.95 - 0.67) / 0.75 287 | 288 | 289 | def ellipse_design_factor(ellipse_ratio): 290 | '''Get the ellipse design factor K for an ellipse ratio. 291 | 292 | Implements eqn bs-16 in Huzel and Huang. 293 | 294 | Arguments: 295 | ellipse_ratio: Ratio of major and minor axes of ellipse end [units: none]. 296 | 297 | Returns: 298 | ellipse design factor K for ellipsoidal end stress calculations [units: none]. 299 | ''' 300 | k1 = ellipse_ratio 301 | k2 = (ellipse_ratio**2 - 1)**0.5 302 | return 2 * k1 + 1 / k2 * np.log((k1 + k2) / (k1 - k2)) -------------------------------------------------------------------------------- /proptools/turbopump.py: -------------------------------------------------------------------------------- 1 | ''' Nozzle flow calculations. 2 | 3 | Matt Vernacchia 4 | proptools 5 | 2016 Apr 3 6 | ''' 7 | from __future__ import division 8 | from proptools import nozzle 9 | import math 10 | import numpy as np 11 | from scipy.interpolate import RectBivariateSpline, interp1d 12 | 13 | 14 | def pump_power(dp, m_dot, rho, eta): 15 | ''' Get the input drive power for a pump. 16 | 17 | Arguments: 18 | dp: Pump pressure rise [units: pascal]. 19 | m_dot: Pump mass flow [units: kilogram second**-1]. 20 | rho: Density of pumped fluid [units: kilogram meter**-3]. 21 | eta: Pump efficiency [units: none]. 22 | 23 | Returns: 24 | The shaft power required to drive the pump [units: watt]. 25 | ''' 26 | return 1 / eta * dp * m_dot / rho 27 | 28 | 29 | def turbine_enthalpy(p_o, p_e, T_o, gamma, c_p): 30 | ''' Get the specific enthalpy drop for a turbine. 31 | 32 | Arguments: 33 | p_o: Turbine inlet stagnation pressure [units: same as p_e]. 34 | p_e: Turbine exit pressure [units: same as p_o]. 35 | T_o: Turbine inlet stagnation temperature [units: kelvin]. 36 | gamma: Turbine working gas ratio of specific heats [units: none]. 37 | c_p: working gas heat capacity at const pressure 38 | [units: joule kilogram**-1 kelvin**-1]. 39 | 40 | Returns: 41 | The specific enthalpy drop across the turbine [units: joule kilogram**-1]. 42 | ''' 43 | # Turbine specific enthalpy drop [units: joule kilogram**-1] 44 | # Eqn. 6-13 in Huzel and Huang, assumes ideal and calorically perfect gas. 45 | return c_p * T_o * (1 - (p_e / p_o)**((gamma -1) / gamma)) 46 | 47 | 48 | def turbine_spout_velocity(p_o, p_e, T_o, gamma, c_p): 49 | ''' Get the theoretical spouting velocity for a turbine. 50 | 51 | Arguments: 52 | p_o: Turbine inlet stagnation pressure [units: same as p_e]. 53 | p_e: Turbine exit pressure [units: same as p_o]. 54 | T_o: Turbine inlet stagnation temperature [units: kelvin]. 55 | gamma: Turbine working gas ratio of specific heats [units: none]. 56 | c_p: working gas heat capacity at const pressure 57 | [units: joule kilogram**-1 kelvin**-1]. 58 | 59 | Returns: 60 | The theoretical spouting velocity c_o of the turbine [units: mater second**-1]. 61 | ''' 62 | dh_turb = turbine_enthalpy(p_o, p_e, T_o, gamma, c_p) 63 | c_o = (2 * dh_turb)**0.5 64 | return c_o 65 | 66 | 67 | def trubine_power(p_o, p_e, m_dot, T_o, eta, gamma, c_p): 68 | ''' Get the output drive power for a turbine. 69 | 70 | Arguments: 71 | p_o: Turbine inlet stagnation pressure [units: same as p_e]. 72 | p_e: Turbine exit pressure [units: same as p_o]. 73 | m_dot: Turbine working gas mass flow [units: kilogram second**-1]. 74 | T_o: Turbine inlet stagnation temperature [units: kelvin]. 75 | gamma: Turbine working gas ratio of specific heats [units: none]. 76 | c_p: working gas heat capacity at const pressure 77 | [units: joule kilogram**-1 kelvin**-1]. 78 | 79 | Returns: 80 | The shaft power produced by the turbine [units: watt]. 81 | ''' 82 | dh_turb = turbine_enthalpy(p_o, p_e, T_o, gamma, c_p) 83 | 84 | # Turbine output power [units: watt] 85 | power_turb = eta * m_dot * dh_turb 86 | return power_turb 87 | 88 | 89 | def turbine_exit_temperature(p_o, p_te, T_o, eta, gamma, c_p): 90 | '''Get the turbine exit temperature. 91 | 92 | Arguments: 93 | p_o: turbine inlet stagnation pressure [units: pascal]. 94 | p_te: turbine exit pressure [units: pascal]. 95 | T_o: turbine inlet stagnation temperature [units: kelvin]. 96 | eta: turbine efficiency. 97 | gamma: working gas ratio of specific heats [units: none]. 98 | c_p: working gas heat capacity at const pressure 99 | [units: joule kilogram**-1 kelvin**-1]. 100 | ''' 101 | # Turbine specific enthalpy drop [units: joule kilogram**-1] 102 | dh_turb_ideal = turbine_enthalpy(p_o, p_te, T_o, gamma, c_p) 103 | dh_turb = eta * dh_turb_ideal 104 | # Turbine exit temperature 105 | T_te = T_o - (dh_turb / c_p) 106 | return T_te 107 | 108 | 109 | def gg_dump_isp(p_o, p_te, p_ne, T_o, eta, gamma, c_p, m_molar): 110 | '''Get the specific impulse of a Gas Generator turbine exhaust dump. 111 | 112 | Arguments: 113 | p_o: turbine inlet stagnation pressure [units: pascal]. 114 | p_te: turbine exit pressure [units: pascal]. 115 | p_ne: Dump nozzle exit pressure [units: pascal]. 116 | T_o: turbine inlet stagnation temperature [units: kelvin]. 117 | eta: turbine efficiency. 118 | gamma: working gas ratio of specific heats [units: none]. 119 | c_p: working gas heat capacity at const pressure 120 | [units: joule kilogram**-1 kelvin**-1]. 121 | m_molar: working gas molar mass [units: kilogram mole**-1]. 122 | ''' 123 | T_te = turbine_exit_temperature(p_o, p_te, T_o, eta, gamma, c_p) 124 | 125 | # Dump nozzle thrust coefficient and characteristic velocity. 126 | # Assume optimal expansion. 127 | C_f = nozzle.thrust_coef(p_c=p_te, p_e=p_ne, gamma=gamma) 128 | c_star = nozzle.c_star(gamma=gamma, m_molar=m_molar, T_c=T_te) 129 | 130 | # Dump nozzle specific impulse [units: second] 131 | Isp = c_star * C_f / 9.81 132 | 133 | return Isp 134 | 135 | 136 | 137 | def m_dot2gpm(m_dot, rho): 138 | ''' Convert mass flow to gallons per minute. 139 | 140 | Arguments: 141 | m_dot: mass flow [units: kilogram second**-1]. 142 | rho: density [units: kilogram meter**-3]. 143 | 144 | Returns: 145 | Volume flow [gallon minute**-1]. 146 | ''' 147 | m3_per_minute = m_dot / rho * 60 148 | liter_per_minute = m3_per_minute * 1000 149 | gpm = liter_per_minute * 0.2642 150 | return gpm 151 | 152 | 153 | def gpm2m_dot(gpm, rho): 154 | ''' Convert gallons per minute to mass flow. 155 | 156 | Arguments: 157 | gpm: Volume flow [gallon minute**-1]. 158 | rho: density [units: kilogram meter**-3]. 159 | 160 | Returns: 161 | m_dot: mass flow [units: kilogram second**-1]. 162 | ''' 163 | 164 | liter_per_minute = gpm / 0.2642 165 | m3_per_minute = liter_per_minute / 1000 166 | m_dot = m3_per_minute * rho / 60 167 | return m_dot 168 | 169 | 170 | def dp2head(dp, rho): 171 | ''' Convert pump pressure rise to US-units head. 172 | 173 | Arguments: 174 | dp: Pump pressure rise [units: pascal]. 175 | rho: density [units: kilogram meter**-3]. 176 | 177 | Returns: 178 | pump head [units: feet]. 179 | ''' 180 | return dp / (rho * nozzle.g) / 0.0254 / 12 181 | 182 | 183 | def radsec2rpm(radsec): 184 | ''' Convert radian second**-1 to rpm. 185 | ''' 186 | return radsec / (2 * math.pi) * 60 187 | 188 | 189 | def rpm2radsec(rpm): 190 | ''' Convert rpm to radian second**-1. 191 | ''' 192 | return rpm * (2 * math.pi) / 60 193 | 194 | 195 | def pump_specific_speed_us(dp, m_dot, rho, N): 196 | ''' Pump specific speed N_s in US units. 197 | 198 | Arguments: 199 | dp: Pump pressure rise [units: pascal]. 200 | m_dot: Pump mass flow [units: kilogram second**-1]. 201 | rho: Density of pumped fluid [units: kilogram meter**-3]. 202 | N: Pump rotation speed [radian second**-1]. 203 | 204 | Returns: 205 | N_s [units: rpm gallon**0.5 minute**-0.5 feet**-0.75)]. 206 | ''' 207 | N_rpm = radsec2rpm(N) 208 | # Volume flow rate [units: gallon minute**-1]. 209 | Q = m_dot2gpm(m_dot, rho) 210 | # Pump head [units: feet] 211 | H = dp2head(dp, rho) 212 | N_s = N_rpm * Q**0.5 / H**0.75 213 | return N_s 214 | 215 | 216 | # Pump efficiency interpolation. 217 | # Based on figure 6-23 in Huzel and Huang. 218 | gpm = np.array([50, 150, 350, 750]) 219 | Ns_us = np.array([500, 700, 900, 1500, 3000, 5000, 7000, 9000, 15000]) 220 | eta = np.array([ 221 | [46.51, 53.74, 58.40, 64.56, 65.69, 63.91, 62.23, 60.72, 56.58], 222 | [52.58, 58.67, 62.86, 69.25, 71.00, 69.09, 67.25, 65.61, 61.32], 223 | [62.07, 66.41, 69.93, 75.66, 76.74, 74.50, 72.47, 70.69, 66.16], 224 | [67.72, 72.77, 75.82, 81.07, 82.34, 79.62, 77.22, 75.00, 69.86], 225 | ]) / 100.0 226 | 227 | pump_eff_interpolator = RectBivariateSpline(gpm, Ns_us, eta) 228 | 229 | def pump_efficiency(dp, m_dot, rho, N): 230 | ''' Pump efficiency estimate. 231 | 232 | Based on figure 6-23 in Huzel and Huang. 233 | 234 | Arguments: 235 | dp: Pump pressure rise [units: pascal]. 236 | m_dot: Pump mass flow [units: kilogram second**-1]. 237 | rho: Density of pumped fluid [units: kilogram meter**-3]. 238 | N: Pump rotation speed [radian second**-1]. 239 | 240 | Returns: 241 | pump efficiency [units: none]. 242 | ''' 243 | # Volume flow rate [units: gallon minute**-1]. 244 | Q = m_dot2gpm(m_dot, rho) 245 | # Specific speed in US units 246 | Ns_us = pump_specific_speed_us(dp, m_dot, rho, N) 247 | eta = pump_eff_interpolator(Q, Ns_us) 248 | eta = eta[0][0] 249 | return eta 250 | 251 | 252 | def pump_efficiency_demo(): 253 | plt.figure() 254 | rho = 1000 255 | m_dot = gpm2m_dot(np.array([50, 100, 150, 350]), rho) 256 | N = rpm2radsec(np.linspace(10e3, 400e3)) 257 | dp = 10e6 258 | print(dp2head(dp, rho)) 259 | for m in m_dot: 260 | eta = np.zeros(len(N)) 261 | Ns_us = np.zeros(len(N)) 262 | for i in range(len(N)): 263 | eta[i] = pump_efficiency(dp, m, rho, N[i]) 264 | Ns_us[i] = pump_specific_speed_us(dp, m, rho, N[i]) 265 | plt.semilogx(Ns_us, eta, label='Q = {0:.0f} gpm'.format(m_dot2gpm(m, rho))) 266 | plt.xlabel('Ns [US units]') 267 | plt.ylabel('$\eta$ [-]') 268 | plt.legend() 269 | plt.grid(True) 270 | 271 | 272 | # Turbine efficiency interpolation. 273 | # From figure 10-9 in Rocket Propulsion Elements. 274 | # Single-stage impulse turbine. 275 | uco = np.array([0.03, 0.04, 0.05, 0.07, 0.09, 0.11, 0.14, 0.17, 0.20, 276 | 0.25, 0.28, 0.33, 0.39, 0.45, 0.49, 0.54, 0.56]) 277 | eta = np.array([10.10, 13.22, 16.68, 20.10, 27.36, 32.01, 39.90, 278 | 47.01, 53.80, 62.62, 68.06, 74.53, 77.71, 77.53, 75.85, 72.59, 279 | 69.81]) / 100.0 280 | ssi_turbine_eff_interpolator = interp1d(uco, eta) 281 | 282 | def ssi_turbine_efficiency(uco): 283 | ''' Efficiency of a single-stage impulse turbine. 284 | 285 | Data from 10-9 in Rocket Propulsion Elements. 286 | 287 | Arguments: 288 | uco: Velocity ratio u / c_o [units: none]. 289 | 290 | Returns: 291 | turbine efficiency [units: none]. 292 | ''' 293 | return float(ssi_turbine_eff_interpolator(uco)) 294 | 295 | 296 | def turbine_efficiency_demo(): 297 | plt.figure() 298 | uco = np.logspace(np.log10(0.04), np.log10(0.5)) 299 | eta = [ssi_turbine_efficiency(u) for u in uco] 300 | plt.loglog(uco, eta) 301 | plt.xlabel('$u / c_o$') 302 | plt.ylabel('$\eta$') 303 | plt.grid(True) 304 | 305 | 306 | if __name__ == '__main__': 307 | from matplotlib import pyplot as plt 308 | pump_efficiency_demo() 309 | turbine_efficiency_demo() 310 | plt.show() 311 | -------------------------------------------------------------------------------- /proptools/convection.py: -------------------------------------------------------------------------------- 1 | """Models for convective heat transfer.""" 2 | from math import pi 3 | import numpy as np 4 | 5 | 6 | def adiabatic_wall_temperature(T_c, M, gamma, r=0.9, Pr=None): 7 | """Adiabatic wall temperature: the driving temperature for boundary layer convection. 8 | 9 | Reference: Huzel and Huang, equation 4-10-a 10 | Arguments: 11 | T_c (scalar): Stagnation temperature of the fluid external to the boundary layer 12 | [units: kelvin]. 13 | M (scalar): Mach number of the flow [units: dimensionless]. 14 | gamma (scalar): Fluid's ratio of specific heats [units: dimensionless]. 15 | r (scalar): Recovery factor [units: dimensionless]. Value of 0.9 16 | recommended for turbulent flow. 17 | Pr (scalar): Prandtl number [units: dimensionless]. If Pr is provided, 18 | Pr will be used to estimate r (instead of the value of r given). 19 | 20 | Returns: 21 | scalar: The adiabatic wall temperature :math:`T_{aw}` [units: kelvin]. 22 | """ 23 | if Pr is not None: 24 | r = Pr**0.33 # Correlation from H&H for turbulent flow. 25 | 26 | return T_c * (1 + r * (gamma - 1) / 2 * M**2) / (1 + (gamma - 1) / 2 * M**2) 27 | 28 | def long_tube_coeff(mass_flux, D, c_p, mu, k): 29 | """Convection coefficient for flow in a long tube. 30 | 31 | This model for convection was developed from experiments with fully-developed 32 | flow in long tubes. It is taken from Eq. 11.35 in Hill & Peterson: 33 | 34 | .. math:: 35 | \frac{h}{G c_p} = 0.023 (\frac{G D}{ \mu_b})^{-0.2} (\frac{\mu c_p}{k})_b^{-0.67} 36 | 37 | where :math:`G` is the average mass flux through the tube, and the subscript :math:`b` 38 | denotes properties evaluated at the bulk fluid temperature. 39 | 40 | References: 41 | [1] P. Hill and C.Peterson, "Mechanics and Thermodynamics of Propulsion", 42 | 2nd edition, 1992. 43 | 44 | Arguments: 45 | mass_flux (scalar): Mass flux through the tube 46 | [units: kilogram meter**-2 second**-1]. 47 | D (scalar): Tube (hydraulic) diameter [units: meter]. 48 | c_p (scalar): Heat capacity at constant pressure of the fluid flowing 49 | through the tube [units: joule kilogram**-1 kelvin**-1]. 50 | mu (scalar): viscosity of the fluid [units: pascal second]. 51 | k (scalar): Thermal conductivty of the fluid [units: watt meter**-1 kelvin**-1] 52 | 53 | Returns: 54 | scalar: The convection coefficient :math:`h` [units: watt meter**-2 kelvin**-1]. 55 | """ 56 | Pr = mu * c_p / k 57 | Re = mass_flux * D / mu 58 | h = 0.023 * mass_flux * c_p * Re**-0.2 * Pr**-0.67 59 | return h 60 | 61 | 62 | def bartz(p_c, c_star, D_t, D, c_p, mu_e, Pr, sigma=1.): 63 | """Bartz equation for estimation of the convection coefficient. 64 | 65 | The Bartz equation is an empirical estimate of the convection coefficient 66 | :math:`h_g` of the exhaust flow in a rocket nozzle. The derivation of the 67 | Bartz equation starts from the modified Reynolds analogy, and uses an 68 | empirical correlation for turbulent pipe flow to estimate the friction 69 | factor. 70 | 71 | References: 72 | [1] Huzel and Huang Equation 4-13. 73 | [2] M. Martinez-Sanchez, "Convective Heat Transfer: Reynolds Analogy," 74 | MIT 16.512 Lecture 7. https://ocw.mit.edu/courses/aeronautics-and-astronautics/16-512-rocket-propulsion-fall-2005/lecture-notes/lecture_7.pdf 75 | 76 | Arguments: 77 | p_c (scalar): Chamber pressure [units: pascal]. 78 | c_star (scalar): Characteristic velocity [units: meter second**-1]. 79 | D_t (scalar): Throat diameter [units: meter]. 80 | D (scalar): Nozzle diameter where convection is to be computed [units: meter]. 81 | c_p (scalar): Heat capacity at constant pressure of the fluid, 82 | at stagnation conditions [units: joule kilogram**-1 kelvin**-1]. 83 | mu_e (scalar): dynamic viscosity of the fluid, 84 | at stagnation conditions [units: pascal second]. 85 | Pr (scalar): Prandtl number of the fluid, at stagnation 86 | conditions [units: dimensionless]. 87 | sigma (scalar): Correction factor [units: dimensionless]. 88 | 89 | Returns: 90 | scalar: The convection coefficient :math:`h_g` [units: watt meter**-2 kelvin**-1]. 91 | """ 92 | # Note: this neglects the factor (D_T/R)**0.1 present in Huzel's version of the 93 | # equation, but not present in Sanchez 94 | # Note: this includes a factor of Pr**0.6, which is present in Huzel but 95 | # assumed to be unity in Sanchez. 96 | return (0.026 / D_t**0.2 97 | * (p_c / c_star)**0.8 98 | * (D_t / D)**1.8 99 | * c_p * mu_e**0.2 / Pr**0.6 100 | * sigma) 101 | 102 | 103 | def bartz_sigma_sanchez(T_e, T_avg, w=0.6): 104 | """Correction factor for the Bartz equation. 105 | 106 | Reference: 107 | [1] M. Martinez-Sanchez, "Convective Heat Transfer: Reynolds Analogy," 108 | MIT 16.512 Lecture 7. https://ocw.mit.edu/courses/aeronautics-and-astronautics/16-512-rocket-propulsion-fall-2005/lecture-notes/lecture_7.pdf 109 | 110 | Arguments: 111 | T_e (scalar): Static temperature of the fluid external to the boundary layer 112 | [units: kelvin]. 113 | T_avg (scalar): Average temperature through the boundary layer, used to adjust 114 | the effective density and viscosity of the boundary layer. Martinez-Sanchez 115 | suggests :math:`T_{avg} = (T_e - T_w)/2` for flow around Mach 1. 116 | w (scalar): Viscosity is assumed to vary as :math:`T^w`. Martinez-Sanchez 117 | suggests :math:`w = 0.6` [units: dimensionless]. 118 | 119 | Returns: 120 | scalar: The Bartz equation correction factor :math:`\sigma`. 121 | """ 122 | return (T_e / T_avg)**(0.8 - 0.2 * w) 123 | 124 | 125 | def bartz_sigma_huzel(T_c, T_w, M, gamma): 126 | """Correction factor for the Bartz equation. 127 | 128 | Reference: 129 | [1] Huzel and Huang Equation 4-14. 130 | 131 | Arguments: 132 | T_c (scalar): Chamber temperature (flow stagnation temperature) 133 | [units: kelvin]. 134 | T_w (scalar): Wall temperature [units: kelvin]. 135 | M (scalar): Mach number of the flow [units: dimensionless]. 136 | gamma (scalar): Fluid's ratio of specific heats [units: dimensionless]. 137 | 138 | Returns: 139 | scalar: The Bartz equation correction factor :math:`\sigma`. 140 | """ 141 | stag = 1 + (gamma - 1) / 2 * M**2 # Stagnation temperature ratio 142 | return ((0.5 * T_w / T_c * stag + 0.5)**0.68 143 | * (stag)**0.12)**-1 144 | 145 | 146 | def film_adiabatic_wall_temperature(eta_film, T_aw, T_f): 147 | """The effective adiabatic wall temperature in the presence of film cooling. 148 | 149 | The heat flux with film cooling can be approximated as: 150 | 151 | .. math:: 152 | q_w = h_g (T_{aw}^f - T_w) 153 | 154 | Where :math:`T_{aw}^f`is effective adiabatic wall temperature in the presence of film cooling, 155 | and :math:`h_g` is the convection coefficient computed as if there were no film. 156 | 157 | Reference: 158 | [1] M. Martinez-Sanchez, "Ablative Cooling, Film Cooling," 159 | MIT 16.512 Lecture 10. 160 | """ 161 | T_aw_f = T_aw - eta_film * (T_aw - T_f) 162 | return T_aw_f 163 | 164 | 165 | def film_efficiency(x, D, m_dot_core, m_dot_film, mu_core, Pr_film=1, film_param=1, cp_ratio=1): 166 | """The film efficiency for a film cooling layer. 167 | 168 | Arguments: 169 | x (scalar): Distance downstream from the film injection location [units: meter]. 170 | D (scalar): Passage diameter [units: meter]. 171 | m_dot_core (scalar): Core mass flow [units: kilogram second**-1]. 172 | m_dot_film (scalar): Film mass flow [units: kilogram second**-1]. 173 | mu_core (scalar): Dynamic viscosity of the core fluid [units: pascal second]. 174 | Pr_film (scalar, optional): Prandtl number of the film fluid [units: dimensionless]. 175 | film_param (scalar, optional): The film parameter, which is the ratio of the 176 | film density to the core density: :math:`M_f = \rho_f / \rho_c` 177 | [units: dimensionless]. 178 | cp_ratio (scalar, optional): The ratio of the core specific heat capacity to the film 179 | specific heat capacity, :math:`c_{p,c}/c_{p,f}` [units: dimensionless]. 180 | 181 | References: 182 | [1] M. Martinez-Sanchez, "Ablative Cooling, Film Cooling," 183 | MIT 16.512 Lecture 10. 184 | [2] W. Rohsenow, J Hartnett, and Y Cho, "Handbook of Heat Transfer," 185 | Ch 17 186 | """ 187 | # Estimate the film thickness [units: meter]. 188 | s = D / 4 * m_dot_film / m_dot_core / film_param 189 | 190 | # Film flow area [units: meter**2]. 191 | A_film = pi * D * s 192 | 193 | # Film flow mass flux [units: kilogram meter**-2 second**-1]. 194 | flux_film = m_dot_film / A_film 195 | 196 | # Zeta parameter, dimensionless distance downstream from the film injection. 197 | zeta = x / (film_param * s) * (flux_film * s / mu_core)**(-0.25) 198 | 199 | # Find the film efficiency by an empirical correlation with the zeta distance and 200 | # the Prandtl number. Martinez-Sanchez recommends this correlation, which is 201 | # from Rohsenow. 202 | eta = 1.9 * Pr_film**(0.66) / (1 + 0.329 * cp_ratio * zeta**0.8) 203 | eta = min([1.0, eta]) 204 | 205 | return eta 206 | 207 | 208 | def rannie_transpiration_cooling(cool_flux_fraction, Pr_film, Re_bulk): 209 | """Rannie's equation for transpiration cooling. 210 | 211 | Implements the "Rannie equation" for transpiration cooling, as given in [1]. 212 | Rannie's original paper [2] gives a slightly different form of the equation 213 | (eqn. 19 in [2]). 214 | 215 | Warning: Huzel [1] states this equation "predicts coolant flows slightly lower 216 | than those required experimentally", and suggests that the predicted 217 | cooling flow will be about 85% of the actual required cooling flow. 218 | 219 | Arguments: 220 | cool_flux_fraction (scalar): Coolant mass flux through the wall / 221 | hot gas mass flux parallel to the wall 222 | [units: dimensionless]. 223 | Pr_film (scalar): Mean film Prandtl number [units: dimensionless]. 224 | Re_bulk (scalar): Bulk hot-gas Reynolds number 225 | 226 | Returns: 227 | scalar: the ratio :math:`\frac{T_{aw} - T_{co}}{T_{wg} - T_{co}}`, where 228 | :math:`T_{aw}` is the adiabatic wall temperature of the gas flow, 229 | :math:`T_{wg}` is the gas-side wall temperature, 230 | :math:`T_{co}` is the coolant initial bulk temperature. 231 | 232 | 233 | References: 234 | [1] Huzel and Huang Equation 4-37. 235 | [2] Rannie, W.D.; Dunn, Louis G.; Millikan, Clark B. "A simplified theory of 236 | porous wall cooling." JPL, Pasadena 1947. 237 | Online: https://trs.jpl.nasa.gov/handle/2014/45706 238 | """ 239 | G = cool_flux_fraction 240 | assert G >= 0 # Mass fluxes must be positive 241 | assert G < 1 # Model is probably not valid 242 | R = Re_bulk**0.1 243 | temp_ratio = ((1 + (1.18 * R - 1) 244 | * (1 - np.exp(-37 * G * R))) 245 | * np.exp(37 * G * R * Pr_film)) 246 | assert temp_ratio >= 1 # If temp_ratio < 1, then T_{wg}> T_{aw}, which is not possible 247 | return temp_ratio 248 | -------------------------------------------------------------------------------- /proptools/electric/generic.py: -------------------------------------------------------------------------------- 1 | """Generic electric propulsion design equations.""" 2 | from __future__ import division 3 | import numpy as np 4 | from scipy.optimize import minimize 5 | from proptools.constants import charge, amu_kg, g 6 | 7 | 8 | def thrust(I_b, V_b, m_ion): 9 | """Thrust of an electric thruster. 10 | 11 | Compute the ideal thrust of an electric thruster from the beam current and voltage, 12 | assuming singly charged ions and no beam divergence. 13 | 14 | Reference: Goebel and Katz, equation 2.3-8. 15 | 16 | Arguments: 17 | I_b (scalar): Beam current [units: ampere]. 18 | V_b (scalar): Beam voltage [units: volt]. 19 | m_ion (scalar): Ion mass [units: kilogram]. 20 | 21 | Returns: 22 | scalar: Thrust force [units: newton]. 23 | """ 24 | return (2 * (m_ion / charge) * V_b)**0.5 * I_b 25 | 26 | 27 | def jet_power(F, m_dot): 28 | """Jet power of a rocket propulsion device. 29 | 30 | Compute the kinetic power of the jet for a given thrust level and mass flow. 31 | 32 | Reference: Goebel and Katz, equation 2.3-4. 33 | 34 | Arguments: 35 | F (scalar): Thrust force [units: newton]. 36 | m_dot (scalar): Jet mass flow rate [units: kilogram second**-1]. 37 | 38 | Returns: 39 | scalar: jet power [units: watt]. 40 | """ 41 | return F**2 / (2 * m_dot) 42 | 43 | 44 | def double_ion_thrust_correction(double_fraction): 45 | """Doubly-charged ion thrust correction factor. 46 | 47 | Compute the thrust correction factor for the presence of doubly charged ions in the beam. 48 | This factor is denoted as :math:`\\alpha` in Goebel and Katz. 49 | 50 | Reference: Goebel and Katz, equation 2.3-14. 51 | 52 | Arguments: 53 | double_fraction (scalar in [0, 1]): The doubly-charged ion current over the singly-charged 54 | ion current, :math:`I^{++} / I^+` [units: dimensionless]. 55 | 56 | Returns: 57 | scalar in (0, 1]: The thrust correction factor, :math:`\\alpha` [units: dimensionless]. 58 | """ 59 | if double_fraction < 0 or double_fraction > 1: 60 | raise ValueError('double_fraction {:.f} is not in [0, 1]'.format(double_fraction)) 61 | 62 | return (1 + (0.5)**0.5 * double_fraction) / (1 + double_fraction) 63 | 64 | 65 | def specific_impulse(V_b, m_ion, divergence_correction=1, double_fraction=1, mass_utilization=1): 66 | """Specific impulse of an electric thruster. 67 | 68 | If only ``V_b`` and ``m_ion`` are provided, the ideal specific impulse will be computed. 69 | If ``divergence_correction``, ``double_fraction``, or ``mass_utilization`` are provided, 70 | the specific impulse will be reduced by the corresponding efficiency factors. 71 | 72 | Reference: Goebel and Katz, equation 2.4-8. 73 | 74 | Arguments: 75 | V_b (scalar): Beam voltage [units: volt]. 76 | m_ion (scalar): Ion mass [units: kilogram]. 77 | divergence_correction (scalar in (0, 1])): Thrust correction factor for beam divergence 78 | [units: dimensionless]. 79 | double_fraction (scalar in [0, 1]): The doubly-charged ion current over the singly-charged 80 | ion current, :math:`I^{++} / I^+` [units: dimensionless]. 81 | mass_utilization (scalar in (0, 1])): Mass utilization efficiency [units: dimensionless]. 82 | 83 | Returns: 84 | scalar: the specific impulse [units: second]. 85 | """ 86 | # Check inputs 87 | if divergence_correction < 0 or divergence_correction > 1: 88 | raise ValueError('divergence_correction {:.f} is not in [0, 1]'.format( 89 | divergence_correction)) 90 | if mass_utilization < 0 or mass_utilization > 1: 91 | raise ValueError('mass_utilization {:.f} is not in [0, 1]'.format(mass_utilization)) 92 | 93 | # Compute the efficiency factor 94 | efficiency = (divergence_correction * double_ion_thrust_correction(double_fraction) 95 | * mass_utilization) 96 | 97 | # Compute the ideal specific impulse 98 | I_sp_ideal = 1 / g * (2 * (charge / m_ion) * V_b)**0.5 99 | 100 | return efficiency * I_sp_ideal 101 | 102 | 103 | def total_efficiency(divergence_correction=1, double_fraction=1, mass_utilization=1, 104 | electrical_efficiency=1): 105 | """Total efficiency of an electric thruster. 106 | 107 | The total efficiency is defined as the ratio of jet power to input power: 108 | 109 | :math:`\\eta_T \\equiv \\frac{P_{jet}}{P_{in}}` 110 | 111 | Reference: Goebel and Katz, equation 2.5-7. 112 | 113 | Arguments: 114 | divergence_correction (scalar in (0, 1])): Thrust correction factor for beam divergence 115 | [units: dimensionless]. 116 | double_fraction (scalar in [0, 1]): The doubly-charged ion current over the singly-charged ion 117 | current, :math:`I^{++} / I^+` [units: dimensionless]. 118 | mass_utilization (scalar in (0, 1])): Mass utilization efficiency [units: dimensionless]. 119 | electrical_efficiency (scalar in (0, 1])): Electrical efficiency [units: dimensionless]. 120 | 121 | Returns: 122 | scalar: Total efficiency [units: dimensionless]. 123 | """ 124 | # Check inputs 125 | if divergence_correction < 0 or divergence_correction > 1: 126 | raise ValueError('divergence_correction {:.f} is not in [0, 1]'.format( 127 | divergence_correction)) 128 | if mass_utilization < 0 or mass_utilization > 1: 129 | raise ValueError('mass_utilization {:.f} is not in [0, 1]'.format(mass_utilization)) 130 | if electrical_efficiency < 0 or electrical_efficiency > 1: 131 | raise ValueError('electrical_efficiency {:.f} is not in [0, 1]'.format( 132 | electrical_efficiency)) 133 | 134 | gamma = divergence_correction * double_ion_thrust_correction(double_fraction) 135 | return gamma**2 * mass_utilization * electrical_efficiency 136 | 137 | 138 | def thrust_per_power(I_sp, total_efficiency=1): 139 | """Thrust/power ratio of an electric thruster. 140 | 141 | Reference: Goebel and Katz, equation 2.5-9. 142 | 143 | Arguments: 144 | I_sp (scalar): Specific impulse [units:second]. 145 | total_efficiency (scalar in (0, 1]): The total efficiency of the thruster 146 | [units: dimensionless]. 147 | 148 | Returns: 149 | scalar: Thrust force per unit power input [units: newton watt**-1]. 150 | """ 151 | # Check inputs 152 | if total_efficiency < 0 or total_efficiency > 1: 153 | raise ValueError('total_efficiency {:.f} is not in [0, 1]'.format(total_efficiency)) 154 | 155 | return (2 / g) * (total_efficiency / I_sp) 156 | 157 | 158 | def stuhlinger_velocity(total_efficiency, t_m, specific_mass): 159 | """Stuhlinger velocity, a characteristic velocity for electric propulsion missions. 160 | 161 | The Stuhlinger velocity is a characteristic velocity for electric propulsion missions, 162 | and is used in calculation of optimal specific impulse. It is defined as: 163 | 164 | :math:`v_{ch} \\equiv \\sqrt{ \\frac{2 \\eta_T t_m}{\\alpha} }` 165 | 166 | where :math:`\\eta_T` is the thruster total efficiency, 167 | :math:`t_m` is the mission thrust duration, and 168 | :math:`\\alpha` is the propulsion and power system specific mass. 169 | 170 | The Stuhlinger velocity is also an approximate upper limit for the :math:`\\Delta v` 171 | capability of an electric propulsion spacecraft. 172 | 173 | Reference: `Lozano 16.522 notes`_, equation 3-7. 174 | 175 | Arguments: 176 | total_efficiency (scalar in (0, 1]): The total efficiency of the thruster 177 | [units: dimensionless]. 178 | t_m: mission thrust duration [units: second]. 179 | specific_mass: specific mass (per unit power) of the thruster and power system 180 | [units: kilogram watt**1]. 181 | 182 | Returns: 183 | scalar: Stuhlinger velocity [units: meter second**-1]. 184 | 185 | .. _Lozano 16.522 notes: https://ocw.mit.edu/courses/aeronautics-and-astronautics/16-522-space-propulsion-spring-2015/lecture-notes/MIT16_522S15_Lecture3-4.pdf 186 | """ 187 | # Check inputs 188 | if total_efficiency < 0 or total_efficiency > 1: 189 | raise ValueError('total_efficiency {:.f} is not in [0, 1]'.format(total_efficiency)) 190 | 191 | return (2 * total_efficiency * t_m / specific_mass)**0.5 192 | 193 | 194 | def optimal_isp_thrust_time(total_efficiency, t_m, specific_mass): 195 | """Optimal specific impulse for constant thrust / fixed time mission. 196 | 197 | Reference: `Lozano 16.522 notes`_, equation 3-5. 198 | 199 | Arguments: 200 | total_efficiency (scalar in (0, 1]): The total efficiency of the thruster 201 | [units: dimensionless]. 202 | t_m: mission thrust duration [units: second]. 203 | specific_mass (scalar): specific mass (per unit power) of the thruster and power system 204 | [units: kilogram watt**1]. 205 | 206 | Returns: 207 | scalar: Optimal specific impulse [units: second]. 208 | 209 | """ 210 | return stuhlinger_velocity(total_efficiency, t_m, specific_mass) / g 211 | 212 | 213 | def optimal_isp_delta_v(dv, total_efficiency, t_m, specific_mass, 214 | discharge_loss=None, m_ion=None): 215 | """Optimal specific impulse for a fixed :math:`\\Delta v` mission. 216 | 217 | The dependence of efficiency on specific impulse can optionally be included in the 218 | optimization. 219 | If ``discharge_loss`` and ``m_ion`` are not provided, then the total efficiency 220 | :math:`\\eta_T` has a constant value of the given ``total_efficiency``. If ``discharge_loss`` 221 | and ``m_ion`` are provided, :math:`\\eta_T` is assumed to vary with specific impulse as: 222 | 223 | :math:`\\eta_T = \\frac{\\eta_{T,0}}{1 + (v_L / I_{sp} g)^2}` 224 | 225 | where :math:`\\eta_{T,0}` is the given value of ``total_efficiency``, and :math:`v_L` is 226 | the loss velocity: 227 | 228 | :math:`v_L = \\sqrt{2 q V_d / m_i}` 229 | 230 | where :math:`V_d` is the discharge loss and :math:`m_i` is the ion mass. 231 | 232 | Reference: `Lozano 16.522 notes`_, equation 3-13. 233 | 234 | Arguments: 235 | total_efficiency (scalar in (0, 1]): The total efficiency of the thruster 236 | [units: dimensionless]. 237 | t_m: mission thrust duration [units: second]. 238 | specific_mass: specific mass (per unit power) of the thruster and power system 239 | [units: kilogram watt**1]. 240 | discharge_loss (scalar, optional): The discharge loss per ion [units: volt]. 241 | m_ion (scalar, optional): Ion mass [units: kilogram]. 242 | 243 | Returns: 244 | scalar: Optimal specific impulse [units: second]. 245 | """ 246 | v_ch = stuhlinger_velocity(total_efficiency, t_m, specific_mass) 247 | v_L = 0 248 | if discharge_loss is not None and m_ion is not None: 249 | v_L = (2 * (charge / m_ion) * discharge_loss)**0.5 250 | 251 | def mass_fraction(c): 252 | """Optimization objective function. 253 | 254 | Payload + structure mass fraction. 255 | 256 | Reference: Lozano equation 3-13 257 | 258 | Arguments: 259 | c : effective exhaust velocity, I_sp * g. 260 | """ 261 | return np.exp(-dv / c) - (c**2 + v_L**2) / (v_ch**2) * (1 - np.exp(-dv / c)) 262 | 263 | # Scale factor to avoid numerical problems with minimize. 264 | scale_factor = 10e3 265 | 266 | # Find the c which maximizes the mass fraction. 267 | result = minimize(lambda c: -mass_fraction(c * scale_factor), 1.) 268 | c = result.x[0] * scale_factor 269 | 270 | return c / g 271 | 272 | 273 | # Put functions in the electric module. 274 | thrust.__module__ = 'proptools.electric' 275 | jet_power.__module__ = 'proptools.electric' 276 | double_ion_thrust_correction.__module__ = 'proptools.electric' 277 | specific_impulse.__module__ = 'proptools.electric' 278 | total_efficiency.__module__ = 'proptools.electric' 279 | thrust_per_power.__module__ = 'proptools.electric' 280 | stuhlinger_velocity.__module__ = 'proptools.electric' 281 | optimal_isp_thrust_time.__module__ = 'proptools.electric' 282 | optimal_isp_delta_v.__module__ = 'proptools.electric' 283 | -------------------------------------------------------------------------------- /proptools/nozzle.py: -------------------------------------------------------------------------------- 1 | """Nozzle flow calculations. 2 | 3 | .. autosummary:: 4 | 5 | thrust_coef 6 | c_star 7 | er_from_p 8 | throat_area 9 | mass_flow 10 | thrust 11 | mach_from_er 12 | mach_from_pr 13 | is_choked 14 | mach_from_area_subsonic 15 | area_from_mach 16 | pressure_from_er 17 | """ 18 | 19 | import numpy as np 20 | from scipy.optimize import fsolve 21 | import warnings 22 | 23 | import proptools.constants 24 | 25 | R_univ = proptools.constants.R_univ 26 | g = proptools.constants.g # pylint: disable=invalid-name 27 | 28 | def thrust_coef(p_c, p_e, gamma, p_a=None, er=None): 29 | """Nozzle thrust coefficient, :math:`C_F`. 30 | 31 | The thrust coefficient is a figure of merit for the nozzle expansion process. 32 | See :ref:`thrust-coefficient-label` for a description of the physical meaning of the 33 | thrust coefficient. 34 | 35 | Reference: Equation 1-33a in Huzel and Huang. 36 | 37 | Arguments: 38 | p_c (scalar): Nozzle stagnation chamber pressure [units: pascal]. 39 | p_e (scalar): Nozzle exit static pressure [units: pascal]. 40 | gamma (scalar): Exhaust gas ratio of specific heats [units: dimensionless]. 41 | p_a (scalar, optional): Ambient pressure [units: pascal]. If None, 42 | then p_a = p_e. 43 | er (scalar, optional): Nozzle area expansion ratio [units: dimensionless]. If None, 44 | then p_a = p_e. 45 | 46 | Returns: 47 | scalar: The thrust coefficient, :math:`C_F` [units: dimensionless]. 48 | """ 49 | if (p_a is None and er is not None) or (er is None and p_a is not None): 50 | raise ValueError('Both p_a and er must be provided.') 51 | C_F = (2 * gamma**2 / (gamma - 1) \ 52 | * (2 / (gamma + 1))**((gamma + 1) / (gamma - 1)) \ 53 | * (1 - (p_e / p_c)**((gamma - 1) / gamma)) 54 | )**0.5 55 | if p_a is not None and er is not None: 56 | C_F += er * (p_e - p_a) / p_c 57 | return C_F 58 | 59 | 60 | def c_star(gamma, m_molar, T_c): 61 | """Characteristic velocity, :math:`c^*`. 62 | 63 | The characteristic velocity is a figure of merit for the propellants and combustion 64 | process. 65 | See :ref:`characteristic_velocity-tutorial-label` for a description of the physical 66 | meaning of the characteristic velocity. 67 | 68 | Reference: Equation 1-32a in Huzel and Huang. 69 | 70 | Arguments: 71 | gamma (scalar): Exhaust gas ratio of specific heats [units: dimensionless]. 72 | m_molar (scalar): Exhaust gas mean molar mass [units: kilogram mole**-1]. 73 | T_c (scalar): Nozzle stagnation temperature [units: kelvin]. 74 | 75 | Returns: 76 | scalar: The characteristic velocity [units: meter second**-1]. 77 | """ 78 | # Note that the g in Huzel is removed here, because Huzel uses US units while this 79 | # function uses SI. 80 | return (gamma * (R_univ / m_molar) * T_c)**0.5 \ 81 | / gamma \ 82 | / (2 / (gamma + 1))**((gamma + 1) / (2 * (gamma - 1))) 83 | 84 | 85 | def er_from_p(p_c, p_e, gamma): 86 | """Find the nozzle expansion ratio from the chamber and exit pressures. 87 | 88 | See :ref:`expansion-ratio-tutorial-label` for a physical description of the 89 | expansion ratio. 90 | 91 | Reference: Rocket Propulsion Elements, 8th Edition, Equation 3-25 92 | 93 | Arguments: 94 | p_c (scalar): Nozzle stagnation chamber pressure [units: pascal]. 95 | p_e (scalar): Nozzle exit static pressure [units: pascal]. 96 | gamma (scalar): Exhaust gas ratio of specific heats [units: dimensionless]. 97 | 98 | Returns: 99 | scalar: Expansion ratio :math:`\\epsilon = A_e / A_t` [units: dimensionless] 100 | """ 101 | AtAe = ((gamma + 1) / 2)**(1 / (gamma - 1)) \ 102 | * (p_e / p_c)**(1 / gamma) \ 103 | * ((gamma + 1) / (gamma - 1)*( 1 - (p_e / p_c)**((gamma -1) / gamma)))**0.5 104 | er = 1/AtAe 105 | return er 106 | 107 | 108 | def pressure_from_er(er, gamma): 109 | """Find the exit/chamber pressure ratio from the nozzle expansion ratio. 110 | 111 | See :ref:`expansion-ratio-tutorial-label` for a physical description of the 112 | expansion ratio. 113 | 114 | Reference: Rocket Propulsion Elements, 8th Edition, Equation 3-25 115 | 116 | Arguments: 117 | er (scalar): Expansion ratio :math:`\\epsilon = A_e / A_t` [units: dimensionless]. 118 | gamma (scalar): Exhaust gas ratio of specific heats [units: dimensionless]. 119 | 120 | Returns: 121 | scalar: Pressure ratio :math:`p_e/p_c` [units: dimensionless]. 122 | """ 123 | pressure_ratio = fsolve(lambda x: er - er_from_p(1., x, gamma), x0=1e-3 / er)[0] 124 | assert pressure_ratio < 1 125 | return pressure_ratio 126 | 127 | 128 | def throat_area(m_dot, p_c, T_c, gamma, m_molar): 129 | """Find the nozzle throat area. 130 | 131 | Given gas stagnation conditions and a mass flow rate, find the required throat area 132 | of a choked nozzle. See :ref:`choked-flow-tutorial-label` for details. 133 | 134 | Reference: Rocket Propulsion Elements, 8th Edition, Equation 3-24 135 | 136 | Arguments: 137 | m_dot (scalar): Propellant mass flow rate [units: kilogram second**-1]. 138 | p_c (scalar): Nozzle stagnation chamber pressure [units: pascal]. 139 | T_c (scalar): Nozzle stagnation temperature [units: kelvin]. 140 | gamma (scalar): Exhaust gas ratio of specific heats [units: dimensionless]. 141 | m_molar (scalar): Exhaust gas mean molar mass [units: kilogram mole**-1]. 142 | 143 | Returns: 144 | scalar: Throat area [units: meter**2]. 145 | """ 146 | R = R_univ / m_molar 147 | # Find the Throat Area require for the specified mass flow, using 148 | # Eocket Propulsion Equations 7th Ed, Equation 3-24 149 | A_t = m_dot / ( p_c * gamma \ 150 | * (2 / (gamma + 1))**((gamma + 1) / (2*gamma - 2)) \ 151 | / (gamma * R * T_c)**0.5) 152 | return A_t 153 | 154 | 155 | def mass_flow(A_t, p_c, T_c, gamma, m_molar): 156 | """Find the mass flow through a choked nozzle. 157 | 158 | Given gas stagnation conditions and a throat area, find the mass flow through a 159 | choked nozzle. See :ref:`choked-flow-tutorial-label` for details. 160 | 161 | Reference: Rocket Propulsion Elements, 8th Edition, Equation 3-24. 162 | 163 | Arguments: 164 | A_t (scalar): Nozzle throat area [units: meter**2]. 165 | p_c (scalar): Nozzle stagnation chamber pressure [units: pascal]. 166 | T_c (scalar): Nozzle stagnation temperature [units: kelvin]. 167 | gamma (scalar): Exhaust gas ratio of specific heats [units: dimensionless]. 168 | m_molar (scalar): Exhaust gas mean molar mass [units: kilogram mole**-1]. 169 | 170 | Returns: 171 | scalar: Mass flow rate :math:`\dot{m}` [units: kilogram second**-1]. 172 | """ 173 | return (A_t * p_c * gamma / (gamma * R_univ / m_molar * T_c)**0.5 174 | * (2 / (gamma + 1))**((gamma + 1) / (2 * (gamma - 1)))) 175 | 176 | 177 | def thrust(A_t, p_c, p_e, gamma, p_a=None, er=None): 178 | """Nozzle thrust force. 179 | 180 | Arguments: 181 | A_t (scalar): Nozzle throat area [units: meter**2]. 182 | p_c (scalar): Nozzle stagnation chamber pressure [units: pascal]. 183 | p_e (scalar): Nozzle exit static pressure [units: pascal]. 184 | gamma (scalar): Exhaust gas ratio of specific heats [units: dimensionless]. 185 | p_a (scalar, optional): Ambient pressure [units: pascal]. If None, 186 | then p_a = p_e. 187 | er (scalar, optional): Nozzle area expansion ratio [units: dimensionless]. If None, 188 | then p_a = p_e. 189 | 190 | Returns: 191 | scalar: Thrust force [units: newton]. 192 | """ 193 | return A_t * p_c * thrust_coef(p_c, p_e, gamma, p_a, er) 194 | 195 | 196 | def mach_from_er(er, gamma): 197 | """Find the exit Mach number from the area expansion ratio. 198 | 199 | Reference: J. Majdalani and B. A. Maickie, http://maji.utsi.edu/publications/pdf/HT02_11.pdf 200 | 201 | Arguments: 202 | er (scalar): Nozzle area expansion ratio, A_e / A_t [units: dimensionless]. 203 | gamma (scalar): Exhaust gas ratio of specific heats [units: dimensionless]. 204 | 205 | Returns: 206 | scalar: The exit Mach number [units: dimensionless]. 207 | """ 208 | n = 5 # order of the approximation 209 | X = np.zeros((n,)) 210 | M = np.zeros((n,)) 211 | 212 | e = 1/float(er) # expansion ratio 213 | y = gamma # ratio of specific heats 214 | B = (y+1)/(y-1) 215 | k = np.sqrt( 0.5*(y-1) ) 216 | u = e**(1/B) / np.sqrt( 1+k**2 ) 217 | X[0] = (u*k)**(B/(1-B)) 218 | M[0] = X[0] 219 | 220 | for i in range(1, n): 221 | lamb = 1/( 2*M[i-1]**(2/B)*(B-2) + M[i-1]**2 *B**2*k**2*u**2 ) 222 | X[i] = lamb*M[i-1]*B*( M[i-1]**(2/B) - M[i-1]**2*B*k**2*u**2 \ 223 | + ( M[i-1]**(2+2/B)*k**2*u**2*(B**2-4*B+4) \ 224 | - M[i-1]**2*B**2*k**2*u**4 + M[i-1]**(4/B)*(2*B-3) \ 225 | + 2*M[i-1]**(2/B)*u**2*(2-B) )**0.5 ) 226 | M[i] = M[i-1] + X[i] 227 | if abs( np.imag( M[n-1] ) ) > 1e-5: 228 | warnings.warn('Exit Mach Number has nonzero imaginary part!') 229 | Me = float(np.real(M[n-1])) 230 | return Me 231 | 232 | 233 | def mach_from_pr(p_c, p_e, gamma): 234 | """Find the exit Mach number from the pressure ratio. 235 | 236 | Arguments: 237 | p_c (scalar): Nozzle stagnation chamber pressure [units: pascal]. 238 | p_e (scalar): Nozzle exit static pressure [units: pascal]. 239 | gamma (scalar): Exhaust gas ratio of specific heats [units: dimensionless]. 240 | 241 | Returns: 242 | scalar: Exit Mach number [units: dimensionless]. 243 | """ 244 | return (2 / (gamma - 1) * ((p_e / p_c)**((1 - gamma) / gamma) -1))**0.5 245 | 246 | 247 | def is_choked(p_c, p_e, gamma): 248 | """Determine whether the nozzle flow is choked. 249 | 250 | See :ref:`choked-flow-tutorial-label` for details. 251 | 252 | Reference: Rocket Propulsion Elements, 8th Edition, Equation 3-20. 253 | 254 | Arguments: 255 | p_c (scalar): Nozzle stagnation chamber pressure [units: pascal]. 256 | p_e (scalar): Nozzle exit static pressure [units: pascal]. 257 | gamma (scalar): Exhaust gas ratio of specific heats [units: dimensionless]. 258 | 259 | Returns: 260 | bool: True if flow is choked, false otherwise. 261 | """ 262 | return p_e/p_c < (2 / (gamma + 1))**(gamma / (gamma - 1)) 263 | 264 | 265 | def mach_from_area_subsonic(area_ratio, gamma): 266 | """Find the Mach number as a function of area ratio for subsonic flow. 267 | 268 | Arguments: 269 | area_ratio (scalar): Area / throat area [units: dimensionless]. 270 | gamma (scalar): Ratio of specific heats [units: dimensionless]. 271 | 272 | Returns: 273 | scalar: Mach number of the flow in a passage with ``area = area_ratio * (throat area)``. 274 | """ 275 | # See https://www.grc.nasa.gov/WWW/winddocs/utilities/b4wind_guide/mach.html 276 | P = 2 / (gamma + 1) 277 | Q = 1 - P 278 | E = 1 / Q 279 | R = area_ratio**2 280 | a = P**(1 / Q) 281 | r = (R - 1) / (2 * a) 282 | X_init = 1 / ((1 + r) + (r * (r + 2))**0.5) 283 | X = fsolve( 284 | lambda X: (P + Q * X)**E - R * X, 285 | X_init 286 | ) 287 | return X[0]**0.5 288 | 289 | 290 | def area_from_mach(M, gamma): 291 | """Find the area ratio for a given Mach number. 292 | 293 | For isentropic nozzle flow, a station where the Mach number is :math:`M` will have an area 294 | :math:`A`. This function returns that area, normalized by the area of the nozzle throat 295 | :math:`A_t`. 296 | See :ref:`mach-area-tutorial-label` for a physical description of the Mach-Area relation. 297 | 298 | Reference: Rocket Propulsion Elements, 8th Edition, Equation 3-14. 299 | 300 | Arguments: 301 | M (scalar): Mach number [units: dimensionless]. 302 | gamma (scalar): Ratio of specific heats [units: dimensionless]. 303 | 304 | Returns: 305 | scalar: Area ratio :math:`A / A_t`. 306 | """ 307 | return 1 / M * (2 / (gamma + 1) * (1 + (gamma - 1) / 2 * M**2)) \ 308 | **((gamma + 1) / (2 * (gamma - 1))) 309 | -------------------------------------------------------------------------------- /docs/source/solid_tutorial.rst: -------------------------------------------------------------------------------- 1 | Solid-Propellant Rocket Motors 2 | ****************************** 3 | 4 | Solid propellant rocket motors store propellant as a solid grain within the combustion chamber. When the motor is ignited, the surfaces of the propellant grain burn and produce hot gas, which is expelled from the chamber through a nozzle to produce thrust. 5 | 6 | However, in most solid rocket motors, no mechanism exists to control the chamber pressure and thrust during flight. Rather, the chamber pressure of a solid rocket motor arises from an equilibrium between exhaust generation from combustion and exhaust discharge through the nozzle. 7 | 8 | The rest of this page reviews the fundamentals solid propellants, and demonstrates the use of ``proptools.solid`` to predict the performance of a solid rocket motor. 9 | 10 | 11 | Applications of Solid Rocket Motors 12 | =================================== 13 | 14 | Compared to liquid-propellant rocket engines, solid propellant motors are mechanically simpler, require less support equipment and time to prepare for launch, and can be stored for long times loaded and ready for launch. Therefore, solid motors are preferred for most military applications, which may need to be fired from mobile launchers (e.g. tactical missiles) or be quickly ready for launch after many years of storage (e.g. strategic missiles). The mechanical simplicity of solid motors is also favors their use in some space-launch applications. 15 | 16 | 17 | Propellant Ingredients 18 | ====================== 19 | 20 | Chemical requirements of solid propellants 21 | ------------------------------------------ 22 | 23 | A solid propellant contains both fuel and oxidizer mixed together. This is different from most other combustion systems, where the fuel and oxidizer are only mixed just before combustion (e.g. internal combustion engines, torches, liquid bi-propellant rocket engines). This poses a chemistry challenge: the propellant ingredients must react energetically with each other, but also be safely stored and handled while mixed together. Clearly, a formulation which spontaneously ignites during mixing has no practical value as a storable solid propellant. A propellant must also not ignite when exposed to mechanical shock, heat or electrostatic discharges during handling. A propellant which is resistant to these accidental ignition sources is said to have low sensitivity. In chemical terms, low sensitivity roughly requires that the combustion reaction have high activation energy. 24 | 25 | A further difference between solid rocket motors and most other combustion devices is that the motor contains all its propellant in the combustion chamber, rather than gradually injecting it as it is to be burned. This means that the rate of propellant consumption is not governed by a throttle or injector, but by the chemical dynamics of the combustion reaction. The propellant must burn at a stable and predictable rate. Ingredient sets which react very quickly may be useful as explosives, but not as propellants. 26 | 27 | In summary, the choice of ingredients must produce a solid propellant which: 28 | 29 | #. stores a large amount of chemical potential energy, and reacts to provide hot gas for propulsion 30 | #. is resistant to accidental ignition during production, storage and handling 31 | #. burns at a stable and predictable rate 32 | 33 | Ammonium perchlorate composite propellant 34 | ----------------------------------------- 35 | 36 | Ammonium perchlorate composite propellant (APCP) is the most-used solid propellant composition in space launch applications (e.g. the Space Shuttle’s Reusable Solid Rocket Motor, Orbital ATK’s Star motor series). APCP is energetic (up to ~270 seconds of specific impulse), is resistant to accidental ignition, and will burn stably in a properly designed motor. 37 | 38 | APCP contains a solid oxidizer (ammonium perchlorate) and (optionally) a powdered metal fuel, held together by a rubber-like binder. Ammonium perchlorate is a crystalline solid, which divided into small particles (10 to 500 μm) and dispersed though the propellant. During combustion, the ammonium perchlorate decomposes to produce a gas rich in oxidizing species. A polymer matrix, the binder, binds the oxidizer particles together, giving the propellant mechanical strength. The binder serves as a fuel, giving off hydrocarbon vapors during combustion. Additional fuel may be added as hot-burning metal powder dispersed in the binder. 39 | 40 | .. figure:: figures/solid/composite_propellant.png 41 | :align: center 42 | 43 | A composite propellant consists of crystalline oxidizer particles, and possibly a metal fuel powder, dispersed in a polymer binder. 44 | 45 | 46 | Combustion Process 47 | ================== 48 | 49 | The combustion process of a composite propellant has many steps, and the flame structure is complex. Although the propellant is a solid, important reactions, including combustion of the fuel with the oxidizer, occur in the gas phase. A set of flames hover over the surface of the burning propellant. These flames transfer heat to the propellant surface, causing its solid components to decompose into gases. The gaseous decomposition products contain fuel vapor and oxidizing species, which supply the flames with reactants. 50 | 51 | Importantly, the combustion process contains a feedback loop. Heat from the flames vaporizes the surface, and vapor from the surface provides fuel and oxidizer to the flames. The rate at which this process proceeds depends on chemical kinetics, mass transfer, and heat transfer within the combustion zone. Importantly, the feedback rate depends on pressure. As we will see in the next section, the rate of propellant combustion determines the chamber pressure and thrust of a solid rocket motor. 52 | 53 | .. figure:: figures/solid/flame_structure.png 54 | :align: center 55 | 56 | The typical flame structure of composite propellant combustion. Heat from the flames decomposes the ammonium perchlorate and binder, which in turn supply oxidizing (AP) and fuel (binder) gases to the flames. 57 | 58 | 59 | Effect of pressure on burn rate 60 | ------------------------------- 61 | 62 | The flame structure described above causes the propellant to burn faster at higher pressures. At higher pressures, the gas phase is denser, causing reactions and diffusion to proceed more quickly. This moves the flame structure closer to the surface. The closer flames and denser conducing medium enhance heat transfer to the surface, which drives more decomposition, increasing the burn rate. 63 | 64 | Although this dependence is complicated, it can be empirically described by Vieille’s Law, which relates the burn rate :math:`r` to the chamber pressure :math:`p_c` via two parameters: 65 | 66 | .. math:: 67 | 68 | r = a (p_c )^n 69 | 70 | :math:`r` is the rate at which the surface regresses, and has units of velocity. :math:`a` is the burn rate coefficient, which has units of [velocity (pressure) :sup:`-n`]. :math:`n` is the unitless burn rate exponent. The model parameters :math:`a, n` must be determined by combustion experiments on the propellant. 71 | 72 | 73 | Motor Internal Ballistics 74 | ========================= 75 | 76 | The study of propellant combustion and fluid dynamics within a rocket motor is called internal ballistics. Internal ballistics can be used to estimate the chamber pressure and thrust of a motor. 77 | 78 | Equilibrium chamber pressure 79 | ---------------------------- 80 | The operating chamber pressure of a solid motor is set by an equilibrium between exhaust generation from combustion and exhaust discharge through the nozzle. 81 | The chamber pressure of a solid rocket motor is related to the mass of combustion gas in the chamber by the Ideal Gas Law: 82 | 83 | .. math:: 84 | 85 | p_c = m R T_c \frac{1}{V_c} 86 | 87 | where :math:`R` is the specific gas constant of the combustion gases in the chamber, :math:`T_c` is their temperature, and :math:`V_c` is the chamber volume. Gas mass is added to the chamber by burning propellant, and mass flows out of the chamber through the nozzle. The rate of change of the chamber gas mass is: 88 | 89 | .. math:: 90 | 91 | \frac{d m}{d t} = \dot{m}_{combustion} - \dot{m}_{nozzle} 92 | 93 | Ideal nozzle theory relates the mass flow through the nozzle to the chamber pressure and the :ref:`characteristic_velocity-tutorial-label`: 94 | 95 | .. math:: 96 | 97 | \dot{m}_{nozzle} = \frac{p_c A_t}{c^*} 98 | 99 | The rate of gas addition from combustion is: 100 | 101 | .. math:: 102 | 103 | \dot{m}_{combustion} = A_b \rho_s r(p_c) 104 | 105 | where :math:'A_b' is the burn area of the propellant, :math:`\rho_s` is the density of the solid propellant, and :math:`r(p_c)` is the burn rate (a function of chamber pressure). 106 | 107 | At the equilibrium chamber pressure where the inflow and outflow rates are equal: 108 | 109 | .. math:: 110 | 111 | \frac{d m}{d t} = \dot{m}_{combustion} - \dot{m}_{nozzle} = 0 112 | 113 | .. math:: 114 | 115 | A_b \rho_s r(p_c) = \frac{p_c A_t}{c^*} 116 | 117 | .. math:: 118 | 119 | p_c &= \frac{A_b}{A_t} \rho_s c^* r(p_c) \\ 120 | &= K \rho_s c^* r(p_c) 121 | 122 | where :math:`K \equiv \frac{A_b}{A_t}` is ratio of burn area to throat area. If the propellant burn rate is well-modeled by Vieille's Law, the equilibrium chamber pressure can be solved for in closed form: 123 | 124 | .. math:: 125 | 126 | p_c = \left( K \rho_s c^* a \right)^{\frac{1}{1 - n}} 127 | 128 | Consider an example motor. The motor burns a relatively slow-burning propellant with the following properties: 129 | 130 | - Burn rate exponent of 0.5, and a burn rate of 2.54 mm s :sup:`-1` at 6.9 MPa 131 | - Exhaust ratio of specific heats of 1.26 132 | - Characteristic velocity of 1209 m s :sup:`-1` 133 | - Solid density of 1510 kg m :sup:`-3` 134 | 135 | The motor has a burn area of 1.25 m :sup:`2`, and a throat area of 839 mm :sup:`2` (diameter of 33 mm). 136 | 137 | Plot the combustion and nozzle mass flow rates versus pressure: 138 | 139 | .. plot:: examples/solid/plots/equilibrium_pressure.py 140 | :include-source: 141 | :align: center 142 | 143 | 144 | The nozzle and combustion mass flow rates are equal at 6.9 MPa: this is the equilibrium pressure of the motor. This equilibrium is stable: 145 | 146 | - At lower pressures, the combustion mass addition rate is higher than the nozzle outflow rate, so the mass of gas in the chamber will increase and the pressure will rise to the equilibrium value. 147 | - At higher pressures, the combustion mass addition rate is lower than the nozzle outflow rate, so the mass of gas in the chamber will decrease and the pressure will fall to the equilibrium value. 148 | 149 | In general, a stable equilibrium pressure will exist for propellants with :math:`n < 1` (i.e. burn rate is sub-linear in pressure). 150 | 151 | We can use proptools to quickly find the chamber pressure and thrust of the example motor: 152 | 153 | .. literalinclude:: examples/solid/pressure_and_thrust.py 154 | 155 | .. literalinclude:: examples/solid/pressure_and_thrust_output.txt 156 | 157 | 158 | Burn area evolution and thrust curves 159 | ------------------------------------- 160 | 161 | In most propellant grain geometries, the burn area of the propellant grain changes as the flame front advances and propellant is consumed. This change in burn area causes the chamber pressure and thrust to change during the burn. The variation of thrust (or chamber pressure) with time is called a thrust curve. Thrust curves are classified as regressive (decreasing with time), neutral or progressive (increasing with time). 162 | 163 | If we know how the burn area :math:`A_b` varies with the flame front progress distance :math:`x`, we can use ``proptools`` to predict the thrust curve. For example, consider a cylindrical propellant with a hollow circular core. The core radius :math:`r_{in}` is 0.15 m, the outer radius :math:`r_{out}` is 0.20 m, and the length :math:`L` is 1.0 m. The burn area is given by: 164 | 165 | .. math:: 166 | 167 | A_b(x) = 2 \pi (r_{in} + x) L 168 | 169 | .. figure:: figures/solid/example_grain.png 170 | :align: center 171 | 172 | Dimensions of the example cylindrical propellant grain. 173 | 174 | Assume that the propellant properties are the same as in the previous example. The nozzle throat area is still 839 mm :sup:`2`, and the nozzle expansion area ratio is 8. 175 | 176 | .. plot:: examples/solid/plots/thrust_curve.py 177 | :include-source: 178 | :align: center 179 | 180 | Note that the pressure and thrust increase with time (the thrust curve is progressive). This grain has a progressive thrust curve because the burn area increases with :math:`x` as the flame front moves outward from the initial core. 181 | 182 | Solid motor designers have devised a wide variety of grain geometries to achieve different thrust curves. 183 | 184 | .. figure:: figures/solid/thrust_curves.png 185 | :align: center 186 | 187 | Various grains and their thrust curves. Reprinted from `Richard Nakka's rocketry page `_. 188 | -------------------------------------------------------------------------------- /.pylintrc: -------------------------------------------------------------------------------- 1 | [MASTER] 2 | 3 | # Specify a configuration file. 4 | #rcfile= 5 | 6 | # Python code to execute, usually for sys.path manipulation such as 7 | # pygtk.require(). 8 | #init-hook= 9 | 10 | # Add files or directories to the blacklist. They should be base names, not 11 | # paths. 12 | ignore=CVS 13 | 14 | # Add files or directories matching the regex patterns to the blacklist. The 15 | # regex matches against base names, not paths. 16 | ignore-patterns= 17 | 18 | # Pickle collected data for later comparisons. 19 | persistent=yes 20 | 21 | # List of plugins (as comma separated values of python modules names) to load, 22 | # usually to register additional checkers. 23 | load-plugins= 24 | 25 | # Use multiple processes to speed up Pylint. 26 | jobs=1 27 | 28 | # Allow loading of arbitrary C extensions. Extensions are imported into the 29 | # active Python interpreter and may run arbitrary code. 30 | unsafe-load-any-extension=no 31 | 32 | # A comma-separated list of package or module names from where C extensions may 33 | # be loaded. Extensions are loading into the active Python interpreter and may 34 | # run arbitrary code 35 | extension-pkg-whitelist= 36 | 37 | # Allow optimization of some AST trees. This will activate a peephole AST 38 | # optimizer, which will apply various small optimizations. For instance, it can 39 | # be used to obtain the result of joining multiple strings with the addition 40 | # operator. Joining a lot of strings can lead to a maximum recursion error in 41 | # Pylint and this flag can prevent that. It has one side effect, the resulting 42 | # AST will be different than the one from reality. This option is deprecated 43 | # and it will be removed in Pylint 2.0. 44 | optimize-ast=no 45 | 46 | 47 | [MESSAGES CONTROL] 48 | 49 | # Only show warnings with the listed confidence levels. Leave empty to show 50 | # all. Valid levels: HIGH, INFERENCE, INFERENCE_FAILURE, UNDEFINED 51 | confidence= 52 | 53 | # Enable the message, report, category or checker with the given id(s). You can 54 | # either give multiple identifier separated by comma (,) or put this option 55 | # multiple time (only on the command line, not in the configuration file where 56 | # it should appear only once). See also the "--disable" option for examples. 57 | #enable= 58 | 59 | # Disable the message, report, category or checker with the given id(s). You 60 | # can either give multiple identifiers separated by comma (,) or put this 61 | # option multiple times (only on the command line, not in the configuration 62 | # file where it should appear only once).You can also use "--disable=all" to 63 | # disable everything first and then reenable specific checks. For example, if 64 | # you want to run only the similarities checker, you can use "--disable=all 65 | # --enable=similarities". If you want to run only the classes checker, but have 66 | # no Warning level messages displayed, use"--disable=all --enable=classes 67 | # --disable=W" 68 | disable=import-star-module-level,old-octal-literal,oct-method,print-statement,unpacking-in-except,parameter-unpacking,backtick,old-raise-syntax,old-ne-operator,long-suffix,dict-view-method,dict-iter-method,metaclass-assignment,next-method-called,raising-string,indexing-exception,raw_input-builtin,long-builtin,file-builtin,execfile-builtin,coerce-builtin,cmp-builtin,buffer-builtin,basestring-builtin,apply-builtin,filter-builtin-not-iterating,using-cmp-argument,useless-suppression,range-builtin-not-iterating,suppressed-message,no-absolute-import,old-division,cmp-method,reload-builtin,zip-builtin-not-iterating,intern-builtin,unichr-builtin,reduce-builtin,standarderror-builtin,unicode-builtin,xrange-builtin,coerce-method,delslice-method,getslice-method,setslice-method,input-builtin,round-builtin,hex-method,nonzero-method,map-builtin-not-iterating 69 | 70 | 71 | [REPORTS] 72 | 73 | # Set the output format. Available formats are text, parseable, colorized, msvs 74 | # (visual studio) and html. You can also give a reporter class, eg 75 | # mypackage.mymodule.MyReporterClass. 76 | output-format=text 77 | 78 | # Put messages in a separate file for each module / package specified on the 79 | # command line instead of printing them on stdout. Reports (if any) will be 80 | # written in a file name "pylint_global.[txt|html]". This option is deprecated 81 | # and it will be removed in Pylint 2.0. 82 | files-output=no 83 | 84 | # Tells whether to display a full report or only the messages 85 | reports=yes 86 | 87 | # Python expression which should return a note less than 10 (10 is the highest 88 | # note). You have access to the variables errors warning, statement which 89 | # respectively contain the number of errors / warnings messages and the total 90 | # number of statements analyzed. This is used by the global evaluation report 91 | # (RP0004). 92 | evaluation=10.0 - ((float(5 * error + warning + refactor + convention) / statement) * 10) 93 | 94 | # Template used to display messages. This is a python new-style format string 95 | # used to format the message information. See doc for all details 96 | #msg-template= 97 | 98 | 99 | [BASIC] 100 | 101 | # Good variable names which should always be accepted, separated by a comma 102 | good-names=i,j,k,ex,Run,_ 103 | 104 | # Bad variable names which should always be refused, separated by a comma 105 | bad-names=foo,bar,baz,toto,tutu,tata 106 | 107 | # Colon-delimited sets of names that determine each other's naming style when 108 | # the name regexes allow several styles. 109 | name-group= 110 | 111 | # Include a hint for the correct naming format with invalid-name 112 | include-naming-hint=no 113 | 114 | # List of decorators that produce properties, such as abc.abstractproperty. Add 115 | # to this list to register other decorators that produce valid properties. 116 | property-classes=abc.abstractproperty 117 | 118 | # Regular expression matching correct function names 119 | function-rgx=[a-z_][a-z0-9_]{2,30}$ 120 | 121 | # Naming hint for function names 122 | function-name-hint=[a-z_][a-z0-9_]{2,30}$ 123 | 124 | # Regular expression matching correct variable names 125 | variable-rgx=[a-z_][a-z0-9_]{2,30}$ 126 | 127 | # Naming hint for variable names 128 | variable-name-hint=[a-z_][a-z0-9_]{2,30}$ 129 | 130 | # Regular expression matching correct constant names 131 | const-rgx=(([A-Z_][A-Z0-9_]*)|(__.*__))|[a-z_][a-z0-9_]{2,30}$ 132 | 133 | # Naming hint for constant names 134 | const-name-hint=(([A-Z_][A-Z0-9_]*)|(__.*__))$ 135 | 136 | # Regular expression matching correct attribute names 137 | attr-rgx=[a-z_][a-z0-9_]{2,30}$ 138 | 139 | # Naming hint for attribute names 140 | attr-name-hint=[a-z_][a-z0-9_]{2,30}$ 141 | 142 | # Regular expression matching correct argument names 143 | argument-rgx=[a-z_][a-z0-9_]{2,30}$ 144 | 145 | # Naming hint for argument names 146 | argument-name-hint=[a-z_][a-z0-9_]{2,30}$ 147 | 148 | # Regular expression matching correct class attribute names 149 | class-attribute-rgx=([A-Za-z_][A-Za-z0-9_]{2,30}|(__.*__))$ 150 | 151 | # Naming hint for class attribute names 152 | class-attribute-name-hint=([A-Za-z_][A-Za-z0-9_]{2,30}|(__.*__))$ 153 | 154 | # Regular expression matching correct inline iteration names 155 | inlinevar-rgx=[A-Za-z_][A-Za-z0-9_]*$ 156 | 157 | # Naming hint for inline iteration names 158 | inlinevar-name-hint=[A-Za-z_][A-Za-z0-9_]*$ 159 | 160 | # Regular expression matching correct class names 161 | class-rgx=[A-Z_][a-zA-Z0-9]+$ 162 | 163 | # Naming hint for class names 164 | class-name-hint=[A-Z_][a-zA-Z0-9]+$ 165 | 166 | # Regular expression matching correct module names 167 | module-rgx=(([a-z_][a-z0-9_]*)|([A-Z][a-zA-Z0-9]+))$ 168 | 169 | # Naming hint for module names 170 | module-name-hint=(([a-z_][a-z0-9_]*)|([A-Z][a-zA-Z0-9]+))$ 171 | 172 | # Regular expression matching correct method names 173 | method-rgx=[a-z_][a-z0-9_]{2,30}$ 174 | 175 | # Naming hint for method names 176 | method-name-hint=[a-z_][a-z0-9_]{2,30}$ 177 | 178 | # Regular expression which should only match function or class names that do 179 | # not require a docstring. 180 | no-docstring-rgx=^_ 181 | 182 | # Minimum line length for functions/classes that require docstrings, shorter 183 | # ones are exempt. 184 | docstring-min-length=-1 185 | 186 | 187 | [ELIF] 188 | 189 | # Maximum number of nested blocks for function / method body 190 | max-nested-blocks=5 191 | 192 | 193 | [FORMAT] 194 | 195 | # Maximum number of characters on a single line. 196 | max-line-length=100 197 | 198 | # Regexp for a line that is allowed to be longer than the limit. 199 | ignore-long-lines=^\s*(# )??$ 200 | 201 | # Allow the body of an if to be on the same line as the test if there is no 202 | # else. 203 | single-line-if-stmt=no 204 | 205 | # List of optional constructs for which whitespace checking is disabled. `dict- 206 | # separator` is used to allow tabulation in dicts, etc.: {1 : 1,\n222: 2}. 207 | # `trailing-comma` allows a space between comma and closing bracket: (a, ). 208 | # `empty-line` allows space-only lines. 209 | no-space-check=trailing-comma,dict-separator 210 | 211 | # Maximum number of lines in a module 212 | max-module-lines=1000 213 | 214 | # String used as indentation unit. This is usually " " (4 spaces) or "\t" (1 215 | # tab). 216 | indent-string=' ' 217 | 218 | # Number of spaces of indent required inside a hanging or continued line. 219 | indent-after-paren=4 220 | 221 | # Expected format of line ending, e.g. empty (any line ending), LF or CRLF. 222 | expected-line-ending-format= 223 | 224 | 225 | [LOGGING] 226 | 227 | # Logging modules to check that the string format arguments are in logging 228 | # function parameter format 229 | logging-modules=logging 230 | 231 | 232 | [MISCELLANEOUS] 233 | 234 | # List of note tags to take in consideration, separated by a comma. 235 | notes=FIXME,XXX,TODO 236 | 237 | 238 | [SIMILARITIES] 239 | 240 | # Minimum lines number of a similarity. 241 | min-similarity-lines=4 242 | 243 | # Ignore comments when computing similarities. 244 | ignore-comments=yes 245 | 246 | # Ignore docstrings when computing similarities. 247 | ignore-docstrings=yes 248 | 249 | # Ignore imports when computing similarities. 250 | ignore-imports=no 251 | 252 | 253 | [SPELLING] 254 | 255 | # Spelling dictionary name. Available dictionaries: none. To make it working 256 | # install python-enchant package. 257 | spelling-dict= 258 | 259 | # List of comma separated words that should not be checked. 260 | spelling-ignore-words= 261 | 262 | # A path to a file that contains private dictionary; one word per line. 263 | spelling-private-dict-file= 264 | 265 | # Tells whether to store unknown words to indicated private dictionary in 266 | # --spelling-private-dict-file option instead of raising a message. 267 | spelling-store-unknown-words=no 268 | 269 | 270 | [TYPECHECK] 271 | 272 | # Tells whether missing members accessed in mixin class should be ignored. A 273 | # mixin class is detected if its name ends with "mixin" (case insensitive). 274 | ignore-mixin-members=yes 275 | 276 | # List of module names for which member attributes should not be checked 277 | # (useful for modules/projects where namespaces are manipulated during runtime 278 | # and thus existing member attributes cannot be deduced by static analysis. It 279 | # supports qualified module names, as well as Unix pattern matching. 280 | ignored-modules= 281 | 282 | # List of class names for which member attributes should not be checked (useful 283 | # for classes with dynamically set attributes). This supports the use of 284 | # qualified names. 285 | ignored-classes=optparse.Values,thread._local,_thread._local 286 | 287 | # List of members which are set dynamically and missed by pylint inference 288 | # system, and so shouldn't trigger E1101 when accessed. Python regular 289 | # expressions are accepted. 290 | generated-members= 291 | 292 | # List of decorators that produce context managers, such as 293 | # contextlib.contextmanager. Add to this list to register other decorators that 294 | # produce valid context managers. 295 | contextmanager-decorators=contextlib.contextmanager 296 | 297 | 298 | [CLASSES] 299 | 300 | # List of method names used to declare (i.e. assign) instance attributes. 301 | defining-attr-methods=__init__,__new__,setUp 302 | 303 | # List of valid names for the first argument in a class method. 304 | valid-classmethod-first-arg=cls 305 | 306 | # List of valid names for the first argument in a metaclass class method. 307 | valid-metaclass-classmethod-first-arg=mcs 308 | 309 | # List of member names, which should be excluded from the protected access 310 | # warning. 311 | exclude-protected=_asdict,_fields,_replace,_source,_make 312 | 313 | 314 | [DESIGN] 315 | 316 | # Maximum number of arguments for function / method 317 | max-args=5 318 | 319 | # Argument names that match this expression will be ignored. Default to name 320 | # with leading underscore 321 | ignored-argument-names=_.* 322 | 323 | # Maximum number of locals for function / method body 324 | max-locals=15 325 | 326 | # Maximum number of return / yield for function / method body 327 | max-returns=6 328 | 329 | # Maximum number of branch for function / method body 330 | max-branches=12 331 | 332 | # Maximum number of statements in function / method body 333 | max-statements=50 334 | 335 | # Maximum number of parents for a class (see R0901). 336 | max-parents=7 337 | 338 | # Maximum number of attributes for a class (see R0902). 339 | max-attributes=7 340 | 341 | # Minimum number of public methods for a class (see R0903). 342 | min-public-methods=2 343 | 344 | # Maximum number of public methods for a class (see R0904). 345 | max-public-methods=20 346 | 347 | # Maximum number of boolean expressions in a if statement 348 | max-bool-expr=5 349 | 350 | 351 | [IMPORTS] 352 | 353 | # Deprecated modules which should not be used, separated by a comma 354 | deprecated-modules=regsub,TERMIOS,Bastion,rexec 355 | 356 | # Create a graph of every (i.e. internal and external) dependencies in the 357 | # given file (report RP0402 must not be disabled) 358 | import-graph= 359 | 360 | # Create a graph of external dependencies in the given file (report RP0402 must 361 | # not be disabled) 362 | ext-import-graph= 363 | 364 | # Create a graph of internal dependencies in the given file (report RP0402 must 365 | # not be disabled) 366 | int-import-graph= 367 | 368 | # Force import order to recognize a module as part of the standard 369 | # compatibility libraries. 370 | known-standard-library= 371 | 372 | # Force import order to recognize a module as part of a third party library. 373 | known-third-party=enchant 374 | 375 | # Analyse import fallback blocks. This can be used to support both Python 2 and 376 | # 3 compatible code, which means that the block might have code that exists 377 | # only in one or another interpreter, leading to false positives when analysed. 378 | analyse-fallback-blocks=no 379 | 380 | 381 | [EXCEPTIONS] 382 | 383 | # Exceptions that will emit a warning when being caught. Defaults to 384 | # "Exception" 385 | overgeneral-exceptions=Exception 386 | -------------------------------------------------------------------------------- /docs/source/nozzle_tutorial.rst: -------------------------------------------------------------------------------- 1 | Nozzle Flow 2 | *********** 3 | 4 | Nozzle flow theory can predict the thrust and specific impulse of a rocket engine. The following example predicts the performance of an engine which operates at chamber pressure of 10 MPa, chamber temperature of 3000 K, and has 100 mm diameter nozzle throat. 5 | 6 | .. literalinclude:: examples/thrust_isp.py 7 | 8 | Output: 9 | 10 | .. literalinclude:: examples/thrust_isp_output.txt 11 | 12 | The rest of this page derives the nozzle flow theory, and demonstrates other features of ``proptools.nozzle``. 13 | 14 | 15 | Ideal Nozzle Flow 16 | ================= 17 | 18 | The purpose of a rocket is to generate thrust by expelling mass at high velocity. The rocket nozzle is a flow device which accelerates gas to high velocity before it is expelled from the vehicle. The nozzle accelerates the gas by converting some of the gas's thermal energy into kinetic energy. 19 | 20 | Ideal nozzle flow is a simplified model of the aero- and thermo-dynamic behavior of fluid in a nozzle. The ideal model allows us to write algebraic relations between an engine's geometry and operating conditions (e.g. throat area, chamber pressure, chamber temperature) and its performance (e.g. thrust and specific impulse). These equations are fundamental tools for the preliminary design of rocket propulsion systems. 21 | 22 | 23 | The assumptions of the ideal model are: 24 | 25 | #. The fluid flowing through the nozzle is gaseous. 26 | #. The gas is homogeneous, obeys the ideal gas law, and is calorically perfect. Its molar mass (:math:`\mathcal{M}`) and heat capacities (:math:`c_p, c_v`) are constant throughout the fluid, and do not vary with temperature. 27 | #. There is no heat transfer to or from the gas. Therefore, the flow is adiabatic. The specific enthalpy :math:`h` is constant throughout the nozzle. 28 | #. There are no viscous effects or shocks within the gas or at its boundaries. Therefore, the flow is reversible. If the flow is both adiabatic and reversible, it is isentropic: the specific entropy :math:`s` is constant throughout the nozzle. 29 | #. The flow is steady; :math:`\frac{d}{dt} = 0`. 30 | #. The flow is quasi one dimensional. The flow state varies only in the axial direction of the nozzle. 31 | #. The flow velocity is axially directed. 32 | #. The flow does not react in the nozzle. The chemical equilibrium established in the combustion chamber does not change as the gas flows through the nozzle. This assumption is known as "frozen flow". 33 | 34 | These assumptions are usually acceptably accurate for preliminary design work. Most rocket engines perform within 1% to 6% of the ideal model predictions [RPE]_. 35 | 36 | 37 | .. _isentropic-relations-tutorial-label: 38 | 39 | Isentropic Relations 40 | ==================== 41 | Under the assumption of isentropic flow and calorically perfect gas, there are several useful relations between fluid states. These relations depend on the heat capacity ratio, :math:`\gamma = c_p /c_v`. Consider two gas states, 1 and 2, which are isentropically related (:math:`s_1 = s_2`). The states' pressure, temperature and density ratios are related: 42 | 43 | .. math:: 44 | 45 | \frac{p_1}{p_2} = \left( \frac{\rho_1}{\rho_2} \right)^\gamma = \left( \frac{T_1}{T_2} \right)^{\frac{\gamma}{\gamma - 1}} 46 | 47 | Stagnation state 48 | ---------------- 49 | 50 | Now consider the relation between static and stagnation states in a moving fluid. The stagnation state is the state a moving fluid would reach if it were isentropically decelerated to zero velocity. The stagnation enthalpy :math:`h_0` is the sum of the static enthalpy and the specific kinetic energy: 51 | 52 | .. math:: 53 | 54 | h_0 = h + \frac{1}{2} v^2 55 | 56 | For a calorically perfect gas, :math:`T = h / c_p`, and the stagnation temperature is: 57 | 58 | .. math:: 59 | 60 | T_0 = T + \frac{v^2}{2 c_p} 61 | 62 | It is helpful to write the fluid properties in terms of the Mach number :math:`M`, instead of the velocity. Mach number is the velocity normalized by the local speed of sound, :math:`a = \sqrt{\gamma R T}`. In terms of Mach number, the stagnation temperature is: 63 | 64 | .. math:: 65 | T_0 = T \left( 1 + \frac{\gamma - 1}{2} M^2 \right) 66 | 67 | Because the static and stagnation states are isentropically related, :math:`\frac{p_0}{p} = \left( \frac{T_0}{T} \right)^{\frac{\gamma}{\gamma - 1}}`. Therefore, the stagnation pressure is: 68 | 69 | .. math:: 70 | p_0 = p \left( 1 + \frac{\gamma - 1}{2} M^2 \right)^{\frac{\gamma}{\gamma - 1}} 71 | 72 | Use ``proptools`` to plot the stagnation state variables against Mach number: 73 | 74 | .. plot:: examples/plots/isentropic_relations.py 75 | :include-source: 76 | :align: center 77 | 78 | 79 | Exit velocity 80 | ------------- 81 | 82 | The exit velocity of the exhaust gas is the fundamental measure of efficiency for rocket propulsion systems, as the `rocket equation `_ shows. We can now show a basic relation between the exit velocity and the combustion conditions of the rocket. First, use the conservation of energy to relate the velocity at any two points in the flow: 83 | 84 | .. math:: 85 | 86 | v_2 = \sqrt{2(h_1 - h_2) + v_1^2} 87 | 88 | We can replace the enthalpy difference with an expression of the pressures and temperatures, using the isentropic relations. 89 | 90 | .. math:: 91 | 92 | v_2 = \sqrt{\frac{2 \gamma}{\gamma - 1} R T_1 \left(1 - \left( \frac{p_2}{p_1} \right)^{\frac{\gamma - 1}{\gamma}} \right) + v_1^2} 93 | 94 | Set state 1 to be the conditions in the combustion chamber: :math:`T_1 = T_c, p_1 = p_c, v_1 \approx 0`. Set state 2 to be the state at the nozzle exit: :math:`p_2 = p_e, v_2 = v_e`. This gives the exit velocity: 95 | 96 | .. math:: 97 | 98 | v_e &= \sqrt{\frac{2 \gamma}{\gamma - 1} R T_c \left(1 - \left( \frac{p_e}{p_c} \right)^{\frac{\gamma - 1}{\gamma}} \right)} \\ 99 | &= \sqrt{\frac{2 \gamma}{\gamma - 1} \mathcal{R} \frac{T_c}{\mathcal{M}} \left(1 - \left( \frac{p_e}{p_c} \right)^{\frac{\gamma - 1}{\gamma}} \right)} 100 | 101 | where :math:`\mathcal{R} = 8.314` J mol :sup:`-1` K :sup:`-1` is the universal gas constant and :math:`\mathcal{M}` is the molar mass of the exhaust gas. Temperature and molar mass have the most significant effect on exit velocity. To maximize exit velocity, a rocket should burn propellants which yield a high flame temperature and low molar mass exhaust. This is why many rockets burn hydrogen and oxygen: they yield a high flame temperature, and the exhaust (mostly H\ :sub:`2` and H\ :sub:`2`\ O) is of low molar mass. 102 | 103 | The pressure ratio :math:`p_e / p_c` is usually quite small. As the pressure ratio goes to zero, the exit vleocity approaches its maximum for a given :math:`T_c, \mathcal{M}` and :math:`\gamma`. 104 | 105 | .. math:: 106 | \frac{p_e}{p_c} \rightarrow 0 \quad \Rightarrow \quad 1 - \left( \frac{p_e}{p_c} \right)^{\frac{\gamma - 1}{\gamma}} \rightarrow 1 107 | 108 | If :math:`p_e \ll p_c`, the pressures have a weak effect on exit velocity. 109 | 110 | The heat capacity ratio :math:`\gamma` has a weak effect on exit velocity. Decreasing :math:`\gamma` increases exit velocity. 111 | 112 | Use ``proptools`` to find the exit velocity of the example engine: 113 | 114 | .. literalinclude:: examples/exit_velocity.py 115 | 116 | .. literalinclude:: examples/exit_velocity_output.txt 117 | 118 | 119 | .. _mach-area-tutorial-label: 120 | 121 | Mach-Area Relation 122 | ================== 123 | 124 | Using the isentropic relations, we can find how the Mach number of the flow varies with the cross sectional area of the nozzle. This allows the design of a nozzle geometry which will accelerate the flow the high speeds needed for rocket propulsion. 125 | 126 | Start with the conservation of mass. Because the flow is quasi- one dimensional, the mass flow through every cross-section of the nozzle must be the same: 127 | 128 | .. math:: 129 | 130 | \dot{m} = A v \rho = \mathrm{const} 131 | 132 | where :math:`A` is the cross-sectional area of the nozzle flow passage (normal to the flow axis). To conserve mass, the ratio of areas between any two points along the nozzle axis must be: 133 | 134 | .. math:: 135 | 136 | \frac{A_1}{A_2} = \frac{v_2 \rho_2}{v_1 \rho_1} 137 | 138 | Use the isentropic relations to write the velocity and density in terms of Mach number, and simplify: 139 | 140 | .. math:: 141 | 142 | \frac{A_1}{A_2} = \frac{M_2}{M_1} \left( \frac{1 + \frac{\gamma - 1}{2} M_1^2}{1 + \frac{\gamma - 1}{2} M_2^2} \right)^{\frac{\gamma + 1}{2 (\gamma - 1)}} 143 | 144 | We can use ``proptools`` to plot Mach-Area relation. Let :math:`M_2 = 1` and plot :math:`A_1 / A_2` vs :math:`M_1`: 145 | 146 | .. plot:: examples/plots/mach_area.py 147 | :include-source: 148 | :align: center 149 | 150 | We see that the nozzle area has a minimum at :math:`M = 1`. At subsonic speeds, Mach number increases as the area is decreased. At supersonic speeds (:math:`M > 1`), Mach number increases as area increases. For a flow passage to accelerate gas from subsonic to supersonic speeds, it must first decrease in area, then increase in area. Therefore, most rocket nozzles have a convergent-divergent shape. Larger expansions of the divergent section lead to higher exit Mach numbers. 151 | 152 | .. figure:: figures/nozzle_cutaway.jpg 153 | :align: center 154 | 155 | The typical converging-diverging shape of rocket nozzles is shown in this cutaway of the Thiokol C-1 engine. Image credit: `Smithsonian Institution, National Air and Space Museum `_ 156 | 157 | .. _choked-flow-tutorial-label: 158 | 159 | Choked Flow 160 | =========== 161 | 162 | The station of minimum area in a converging-diverging nozzle is known as the *nozzle throat*. If the pressure ratio across the nozzle is at least: 163 | 164 | .. math:: 165 | 166 | \frac{p_c}{p_e} > \left( \frac{\gamma + 1}{2} \right)^{\frac{\gamma}{\gamma - 1}} \sim 1.8 167 | 168 | then the flow at the throat will be sonic (:math:`M = 1`) and the flow in the diverging section will be supersonic. The velocity at the throat is: 169 | 170 | .. math:: 171 | 172 | v_t = \sqrt{\gamma R T_t} = \sqrt{\frac{2 \gamma}{\gamma + 1} R T_c} 173 | 174 | The mass flow at the sonic throat (i.e. through the nozzle) is: 175 | 176 | .. math:: 177 | 178 | \dot{m} = A_t v_t \rho_t = A_t p_c \frac{\gamma}{\sqrt{\gamma R T_c}} \left( \frac{2}{\gamma + 1} \right)^{\frac{\gamma + 1}{2 (\gamma - 1)}} 179 | 180 | Notice that the mass flow does not depend on the exit pressure. If the exit pressure is sufficiently low to produce sonic flow at the throat, the nozzle is *choked* and further decreases in exit pressure will not alter the mass flow. Increasing the chamber pressure increases the density at the throat, and therefore will increase the mass flow which can "fit through" the throat. Increasing the chamber temperature increases the throat velocity but decreases the density by a larger amount; the net effect is to decrease mass flow as :math:`1 / \sqrt{T_c}`. 181 | 182 | Use ``proptools`` to compute the mass flow of the example engine: 183 | 184 | .. literalinclude:: examples/choked.py 185 | 186 | .. literalinclude:: examples/choked_output.txt 187 | 188 | Thrust 189 | ====== 190 | 191 | The thrust force of a rocket engine is equal to the momentum flow out of the nozzle plus a pressure force at the nozzle exit: 192 | 193 | .. math:: 194 | 195 | F = \dot{m} v_e + (p_e - p_a) A_e 196 | 197 | where :math:`p_a` is the ambient pressure and :math:`A_e` is the nozzle exit area. We can rewrite this in terms of the chamber pressure: 198 | 199 | .. math:: 200 | 201 | F = A_t p_c \sqrt{\frac{2 \gamma^2}{\gamma - 1} \left( \frac{2}{\gamma + 1}\right)^{\frac{\gamma + 1}{\gamma - 1}} \left(1 - \left( \frac{p_e}{p_c} \right)^{\frac{\gamma - 1}{\gamma}} \right)} + (p_e - p_a) A_e 202 | 203 | Note that thrust depends only on :math:`\gamma` and the nozzle pressures and areas; not chamber temperature. 204 | 205 | Use ``proptools`` to plot thrust versus chamber pressure for the example engine: 206 | 207 | .. plot:: examples/plots/thrust_pc.py 208 | :include-source: 209 | :align: center 210 | 211 | Note that thrust is almost linear in chamber pressure. 212 | 213 | We can also explore the variation of thrust with ambient pressure for fixed :math:`p_c, p_e`: 214 | 215 | .. plot:: examples/plots/thrust_pa.py 216 | :include-source: 217 | :align: center 218 | 219 | 220 | .. _thrust-coefficient-label: 221 | 222 | Thrust coefficient 223 | ================== 224 | We can normalize thrust by :math:`A_t p_c` to give a non-dimensional measure of nozzle efficiency, which is independent of engine size or power level. This is the *thrust coefficient*, :math:`C_F`: 225 | 226 | .. math:: 227 | 228 | C_F \equiv \frac{F}{A_t p_c} 229 | 230 | For an ideal nozzle, the thrust coefficient is: 231 | 232 | .. math:: 233 | 234 | C_F = \sqrt{\frac{2 \gamma^2}{\gamma - 1} \left( \frac{2}{\gamma + 1}\right)^{\frac{\gamma + 1}{\gamma - 1}} \left(1 - \left( \frac{p_e}{p_c} \right)^{\frac{\gamma - 1}{\gamma}} \right)} + \frac{p_e - p_a}{p_c} \frac{A_e}{A_t} 235 | 236 | Note that :math:`C_F` is independent of the combustion temperature or the engine size. It depends only on the heat capacity ratio, nozzle pressures, and expansion ratio (:math:`A_e / A_t`). Therefore, :math:`C_F` is a figure of merit for the nozzle expansion process. It can be used to compare the efficiency of different nozzle designs on different engines. Values of :math:`C_F` are generally between 0.8 and 2.2, with higher values indicating better nozzle performance. 237 | 238 | 239 | .. _expansion-ratio-tutorial-label: 240 | 241 | Expansion Ratio 242 | =============== 243 | 244 | The expansion ratio is an important design parameter which affects nozzle efficiency. It is the ratio of exit area to throat area: 245 | 246 | .. math:: 247 | 248 | \epsilon \equiv \frac{A_e}{A_t} 249 | 250 | The expansion ratio appears directly in the equation for thrust coefficient. The expansion ratio also allows the nozzle designer to set the exit pressure. The relation between expansion ratio and pressure ratio can be found from mass conservation and the isentropic relations: 251 | 252 | .. math:: 253 | 254 | \epsilon &= \frac{A_e}{A_t} = \frac{\rho_t v_t}{\rho_e v_e} \\ 255 | &= \left( \left( \frac{\gamma + 1}{2} \right)^{\frac{1}{\gamma - 1}} \left( \frac{p_e}{p_c} \right)^{\frac{1}{\gamma}} \sqrt{\frac{\gamma + 1}{\gamma - 1} \left(1 - \left( \frac{p_e}{p_c} \right)^{\frac{\gamma - 1}{\gamma}} \right)} \right)^{-1} 256 | 257 | This relation is implemented in ``proptools``: 258 | 259 | .. literalinclude:: examples/expansion_ratio.py 260 | 261 | .. literalinclude:: examples/expansion_ratio_output.txt 262 | 263 | We can also solve the inverse problem: 264 | 265 | .. literalinclude:: examples/expansion_ratio_inverse.py 266 | 267 | .. literalinclude:: examples/expansion_ratio_inverse_output.txt 268 | 269 | Let us plot the effect of expansion ratio on thrust coefficient: 270 | 271 | .. plot:: examples/plots/exp_ratio_cf.py 272 | :include-source: 273 | :align: center 274 | 275 | The thrust coefficient is maximized at the *matched expansion condition*, where :math:`p_e = p_a`. Therefore, nozzle designers select the expansion ratio based on the ambient pressure which the engine is expected to operate in. Small expansion ratios are used for space launch boosters or tactical missiles, which operate at low altitudes (high ambient pressure). Large expansion ratios are used for second stage or orbital maneuvering engines, which operate in the vacuum of space. 276 | 277 | .. figure:: figures/sea_level_vs_vacuum_engine.png 278 | :align: center 279 | :width: 300px 280 | 281 | This illustration shows two variants of an engine family, one designed for a first stage booster (left) and the other for a second stage (right). The first stage (e.g. sea level) engine has a smaller expansion ratio than the second stage (e.g. vacuum) engine. Image credit: `shadowmage `_. 282 | 283 | The following plot shows :math:`C_F` vs altitude for our example engine with two different nozzles: a small nozzle suited to a first stage application (blue curve) and a large nozzle for a second stage (orange curve). Compare these curves to the performance of a hypothetical matched nozzle, which expands to :math:`p_e = p_a` at every altitude. The fixed-expansion nozzles perform well at their design altitude, but have lower :math:`C_F` than a matched nozzle at all other altitudes. 284 | 285 | .. plot:: examples/plots/cf_alt.py 286 | :include-source: 287 | :align: center 288 | 289 | 290 | .. _characteristic_velocity-tutorial-label: 291 | 292 | Characteristic velocity 293 | ======================= 294 | 295 | We can define another performance parameter which captures the effects of the combustion gas which is supplied to the nozzle. This is the *characteristic velocity*, :math:`c^*`: 296 | 297 | .. math:: 298 | 299 | c^* \equiv \frac{A_t p_c}{\dot{m}} 300 | 301 | For an ideal rocket, the characteristic velocity is: 302 | 303 | .. math:: 304 | 305 | c^* = \frac{\sqrt{\gamma R T_c}}{\gamma} \left( \frac{\gamma + 1}{2} \right)^{\frac{\gamma + 1}{2 (\gamma - 1)}} 306 | 307 | The characteristic velocity depends only on the exhaust properties (:math:`\gamma, R`) and the combustion temperature. It is therefore a figure of merit for the combustion process and propellants. :math:`c^*` is independent of the nozzle expansion process. 308 | 309 | The ideal :math:`c^*` of the example engine is: 310 | 311 | .. literalinclude:: examples/c_star_ideal.py 312 | 313 | .. literalinclude:: examples/c_star_ideal_output.txt 314 | 315 | 316 | Specific Impulse 317 | ================ 318 | 319 | Finally, we arrive at *specific impulse*, the most important performance parameter of a rocket engine. The specific impulse is the ratio of thrust to the rate of propellant consumption: 320 | 321 | .. math:: 322 | 323 | I_{sp} \equiv \frac{F}{\dot{m} g_0} 324 | 325 | For historical reasons, specific impulse is normalized by the constant :math:`g_0 =` 9.807 m s :sup:`-2` , and has units of seconds. For an ideal rocket at matched exit pressure, :math:`I_{sp} = v_2 / g_0`. 326 | 327 | The specific impulse measures the "fuel efficiency" of a rocket engine. The specific impulse and propellant mass fraction together determine the delta-v capability of a rocket. 328 | 329 | Specific impulse is the product of the thrust coefficient and the characteristic velocity. The overall efficiency of the engine (:math:`I_{sp}`) depends on both the combustion gas (:math:`c^*`) and the efficiency of the nozzle expansion process (:math:`C_F`). 330 | 331 | .. math:: 332 | 333 | I_{sp} = \frac{c^* C_F}{g_0} 334 | 335 | 336 | .. [RPE] G. P. Sutton and O. Biblarz, *Rocket Propulsion Elements*, Hoboken: John Wiley & Sons, 2010. 337 | --------------------------------------------------------------------------------