├── .bumpversion.cfg ├── .github ├── PULL_REQUEST_TEMPLATE.md └── workflows │ ├── pycycle_test_workflow.yml │ └── release_workflow.yml ├── .gitignore ├── .travis.yml ├── LICENSE.txt ├── README.md ├── example_cycles ├── N+3ref │ ├── N3_Fan_map.py │ ├── N3_HPC_map.py │ ├── N3_HPT_map.py │ ├── N3_LPC_map.py │ ├── N3_LPT_map.py │ ├── N3_MDP.py │ ├── N3_MDP_Opt.py │ ├── N3_MDP_verif.py │ ├── N3_SPD.py │ ├── N3ref.py │ ├── benchmark_N3_MDP.py │ ├── benchmark_N3_MDP_Opt.py │ ├── benchmark_N3_MDP_verif.py │ ├── benchmark_N3_SPD.py │ ├── benchmark_N3ref.py │ └── small_core_eff_balance.py ├── __init__.py ├── afterburning_turbojet.py ├── electric_propulsor.py ├── high_bypass_turbofan.py ├── mixedflow_turbofan.py ├── multi_spool_turboshaft.py ├── simple_turbojet.py ├── single_spool_turboshaft.py ├── tab_thermo_data_generator.py ├── tests │ ├── benchmark_ab_turbojet.py │ ├── benchmark_electric_propulsor.py │ ├── benchmark_hbtf.py │ ├── benchmark_mixedflow_turbofan.py │ ├── benchmark_multi_spool_turboshaft.py │ ├── benchmark_simple_turbojet.py │ ├── benchmark_single_spool_turboshaft.py │ ├── benchmark_wet_propulsor.py │ └── benchmark_wet_simple_turbojet.py ├── wet_propulsor.py └── wet_simple_turbojet.py ├── pycycle ├── __init__.py ├── api.py ├── connect_flow.py ├── constants.py ├── docs │ ├── Makefile │ ├── _theme │ │ ├── search.html │ │ ├── static │ │ │ ├── searchtools.js │ │ │ └── style.css │ │ └── theme.conf │ ├── conf.py │ ├── examples │ │ └── index.rst │ ├── index.rst │ ├── reference_guide │ │ ├── elements │ │ │ └── index.rst │ │ ├── maps │ │ │ └── index.rst │ │ └── viewers.rst │ └── tutorials │ │ ├── images │ │ ├── turbojet_elements.pdf │ │ ├── turbojet_elements.png │ │ └── turbojet_elements.tex │ │ ├── index.rst │ │ ├── turbofan.rst │ │ └── turbojet.rst ├── element_base.py ├── elements │ ├── US1976.py │ ├── __init__.py │ ├── ambient.py │ ├── bleed_out.py │ ├── cfd_start.py │ ├── combustor.py │ ├── compressor.py │ ├── compressor_map.py │ ├── cooling.py │ ├── duct.py │ ├── flight_conditions.py │ ├── flow_start.py │ ├── gearbox.py │ ├── inlet.py │ ├── mixer.py │ ├── nozzle.py │ ├── performance.py │ ├── shaft.py │ ├── splitter.py │ ├── test │ │ ├── __init__.py │ │ ├── reg_data │ │ │ ├── ambient.csv │ │ │ ├── combustorJP7.csv │ │ │ ├── compressor.csv │ │ │ ├── compressorOD1.csv │ │ │ ├── duct.csv │ │ │ ├── flowstart.csv │ │ │ ├── inlet.csv │ │ │ ├── nozzle.csv │ │ │ ├── nozzleCD.csv │ │ │ ├── nozzleCD_CV.csv │ │ │ ├── nozzleCD_CV__Cfg.csv │ │ │ ├── nozzleCD__Cfg.csv │ │ │ ├── nozzleCV.csv │ │ │ ├── nozzleCV__Cfg.csv │ │ │ ├── shaft.csv │ │ │ ├── splitter.csv │ │ │ ├── turbine.csv │ │ │ └── turbineOD1.csv │ │ ├── test_ambient.py │ │ ├── test_bleed_out.py │ │ ├── test_cfd_start.py │ │ ├── test_combustor.py │ │ ├── test_compressor.py │ │ ├── test_compressor_map.py │ │ ├── test_compressor_od.py │ │ ├── test_cooling.py │ │ ├── test_duct.py │ │ ├── test_flightconditions.py │ │ ├── test_flow_start.py │ │ ├── test_inlet.py │ │ ├── test_mixer.py │ │ ├── test_nozzle.py │ │ ├── test_nozzle_CV_CD.py │ │ ├── test_shaft.py │ │ ├── test_splitter.py │ │ ├── test_turbine.py │ │ ├── test_turbine_map.py │ │ ├── test_turbine_od.py │ │ └── test_usatm1976.py │ ├── turbine.py │ └── turbine_map.py ├── flow_in.py ├── maps │ ├── Fan_map.py │ ├── HPC_map.py │ ├── HPT_map.py │ ├── LPC_map.py │ ├── LPT_map.py │ ├── __init__.py │ ├── axi3_2.py │ ├── axi5.py │ ├── hpt1269.py │ ├── lpt2269.py │ ├── map_data.py │ ├── ncp01.py │ └── test │ │ └── __init__.py ├── mp_cycle.py ├── passthrough.py ├── tests │ ├── __init__.py │ └── test_element.py ├── thermo │ ├── __init__.py │ ├── cea │ │ ├── __init__.py │ │ ├── chem_eq.py │ │ ├── props_calcs.py │ │ ├── props_rhs.py │ │ ├── species_data.py │ │ ├── test │ │ │ ├── __init__.py │ │ │ ├── test_chem_eq_co2.py │ │ │ ├── test_chem_eq_janaf.py │ │ │ ├── test_props_rhs_co2.py │ │ │ ├── test_species_data.py │ │ │ └── test_thermo_add_cea.py │ │ ├── thermo_add.py │ │ └── thermo_data │ │ │ ├── __init__.py │ │ │ ├── co2_co_o2.py │ │ │ ├── janaf.py │ │ │ └── wet_air.py │ ├── static_ps_calc.py │ ├── static_ps_resid.py │ ├── tabular │ │ ├── __init__.py │ │ ├── air_jetA.pkl │ │ ├── air_jetA_coarse.pkl │ │ ├── tab_cea_comparison.py │ │ ├── tabular_thermo.py │ │ ├── test │ │ │ ├── __init__.py │ │ │ ├── test_tab_thermo.py │ │ │ └── test_thermo_add.py │ │ └── thermo_add.py │ ├── test │ │ ├── NPSS_Static_CEA_Data.csv │ │ ├── __init__.py │ │ ├── test_thermo_total_static_cea.py │ │ └── test_thermo_total_static_tabulated.py │ ├── thermo.py │ └── unit_comps.py └── viewers.py ├── pyproject.toml ├── release_notes.md └── setup.py /.bumpversion.cfg: -------------------------------------------------------------------------------- 1 | [bumpversion] 2 | current_version = 4.3.1-dev 3 | commit = False 4 | tag = False 5 | parse = (?P\d+)\.(?P\d+)\.(?P\d+)(\-(?P[a-z]+))? 6 | serialize = 7 | {major}.{minor}.{patch}-{release} 8 | {major}.{minor}.{patch} 9 | 10 | [bumpversion:file:pycycle/__init__.py] 11 | search = __version__ = '{current_version}' 12 | replace = __version__ = '{new_version}' 13 | 14 | [bumpversion:part:release] 15 | optional_value = rel 16 | values = 17 | dev 18 | rel 19 | -------------------------------------------------------------------------------- /.github/PULL_REQUEST_TEMPLATE.md: -------------------------------------------------------------------------------- 1 | ### Summary 2 | 3 | Summary of PR. 4 | 5 | ### Related Issues 6 | 7 | - Resolves # 8 | 9 | ### Backwards incompatibilities 10 | 11 | None 12 | 13 | ### New Dependencies 14 | 15 | None 16 | -------------------------------------------------------------------------------- /.github/workflows/release_workflow.yml: -------------------------------------------------------------------------------- 1 | # Publish release to PyPi 2 | 3 | name: Publish Release 4 | 5 | on: 6 | # Trigger on release, to publish release packages to PyPI 7 | release: 8 | types: [published] 9 | 10 | # Run the workflow manually 11 | # This might be useful if the automated publish fails for some reason (use with CARE!!) 12 | workflow_dispatch: 13 | 14 | jobs: 15 | pypi-publish: 16 | name: Upload release to PyPI 17 | runs-on: ubuntu-latest 18 | 19 | environment: release 20 | 21 | permissions: 22 | id-token: write 23 | 24 | steps: 25 | - uses: actions/checkout@v3 26 | 27 | - uses: actions/setup-python@v4 28 | with: 29 | python-version: "3.x" 30 | 31 | - name: deps 32 | run: python -m pip install -U hatch 33 | 34 | - name: build 35 | run: hatch build 36 | 37 | - name: Publish package distributions to PyPI 38 | uses: pypa/gh-action-pypi-publish@release/v1 39 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | *.idea 2 | *.pyc 3 | .cache 4 | *~ 5 | .DS_Store 6 | .dropbox 7 | pycycle.egg-info/* 8 | /build* 9 | *testflo_report.out 10 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: python 2 | python: 3 | - "3.6" 4 | 5 | notifications: 6 | email: false 7 | 8 | install: 9 | # set up basic conda env via miniconda 10 | - sudo apt-get update 11 | - wget https://repo.continuum.io/miniconda/Miniconda3-latest-Linux-x86_64.sh -O miniconda.sh; 12 | - bash miniconda.sh -b -p $HOME/miniconda 13 | - export PATH="$HOME/miniconda/bin:$PATH" 14 | - hash -r 15 | - conda config --set always_yes yes --set changeps1 no 16 | - conda update -q conda 17 | - conda info -a 18 | # install deps 19 | - conda install numpy scipy 20 | - pip install git+https://github.com/OpenMDAO/testflo.git 21 | - pip install git+https://github.com/OpenMDAO/OpenMDAO.git; 22 | # install pycycle 23 | - pip install -e . 24 | 25 | script: 26 | # run unit test suite 27 | - testflo pycycle 28 | # run engine cycle tests on python 3.6 & only when triggered by nightly build 29 | - if [[ "$TRAVIS_PYTHON_VERSION" == "3.6" ]] && [[ "$TRAVIS_EVENT_TYPE" == "cron" ]]; then 30 | testflo -b example_cycles; 31 | fi -------------------------------------------------------------------------------- /LICENSE.txt: -------------------------------------------------------------------------------- 1 | Pycycle Open Source License: 2 | 3 | Licensed under the Apache License, Version 2.0 (the "License"); 4 | you may not use this file except in compliance with the License. 5 | You may obtain a copy of the License at 6 | 7 | http://www.apache.org/licenses/LICENSE-2.0 8 | 9 | Unless required by applicable law or agreed to in writing, software 10 | distributed under the License is distributed on an "AS IS" BASIS, 11 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | See the License for the specific language governing permissions and 13 | limitations under the License. 14 | -------------------------------------------------------------------------------- /example_cycles/N+3ref/N3_HPT_map.py: -------------------------------------------------------------------------------- 1 | import numpy as np 2 | 3 | from pycycle.maps.map_data import MapData 4 | 5 | """Python version of HBTF HPT map from NPSS""" 6 | 7 | HPTMap = MapData() 8 | 9 | 10 | # Map design point values 11 | HPTMap.defaults = {} 12 | HPTMap.defaults['alphaMap'] = 1.0 13 | HPTMap.defaults['NpMap'] = 100.0 14 | HPTMap.defaults['PRmap'] = 5.0 15 | #effMapDes = 0.9276 # = effMap for no scaling 16 | 17 | HPTMap.alphaMap = np.array([1.0, 2.0]) 18 | HPTMap.NpMap = np.array([60., 70.0, 80.0, 90.0, 100., 110.]) 19 | HPTMap.PRmap = np.array([3.000, 3.250, 3.500, 3.750, 4.000, 4.250, 4.500, 4.750, 5.000, 5.250, 5.500, 5.750, 6.000, 6.250, 6.500, 6.750, 7.000, 7.250, 7.500, 8.000]) 20 | 21 | HPTMap.effMap = np.array([[[0.8460, 0.8405, 0.8349, 0.8296, 0.8249, 0.8206, 0.8165, 0.8127, 0.8092, 0.8060, 0.8030, 0.8002, 0.7976, 0.7953, 0.7931, 0.7911, 0.7892, 0.7875, 0.7858, 0.7826], 22 | [0.8879, 0.8842, 0.8804, 0.8769, 0.8735, 0.8701, 0.8670, 0.8640, 0.8614, 0.8590, 0.8568, 0.8548, 0.8529, 0.8511, 0.8495, 0.8479, 0.8460, 0.8436, 0.8414, 0.8373], 23 | [0.9125, 0.9111, 0.9096, 0.9078, 0.9055, 0.9034, 0.9014, 0.8995, 0.8979, 0.8964, 0.8950, 0.8936, 0.8924, 0.8903, 0.8877, 0.8853, 0.8830, 0.8808, 0.8787, 0.8749], 24 | [0.9228, 0.9242, 0.9250, 0.9247, 0.9240, 0.9232, 0.9224, 0.9217, 0.9210, 0.9203, 0.9197, 0.9188, 0.9162, 0.9137, 0.9113, 0.9091, 0.9070, 0.9050, 0.9031, 0.8980], 25 | [0.9215, 0.9258, 0.9289, 0.9304, 0.9313, 0.9319, 0.9323, 0.9326, 0.9328, 0.9329, 0.9330, 0.9311, 0.9288, 0.9266, 0.9245, 0.9225, 0.9206, 0.9188, 0.9161, 0.9107], 26 | [0.9106, 0.9176, 0.9232, 0.9267, 0.9292, 0.9312, 0.9327, 0.9340, 0.9351, 0.9361, 0.9369, 0.9349, 0.9329, 0.9311, 0.9293, 0.9276, 0.9259, 0.9239, 0.9212, 0.9161]], 27 | [[0.8460, 0.8405, 0.8349, 0.8296, 0.8249, 0.8206, 0.8165, 0.8127, 0.8092, 0.8060, 0.8030, 0.8002, 0.7976, 0.7953, 0.7931, 0.7911, 0.7892, 0.7875, 0.7858, 0.7826], 28 | [0.8879, 0.8842, 0.8804, 0.8769, 0.8735, 0.8701, 0.8670, 0.8640, 0.8614, 0.8590, 0.8568, 0.8548, 0.8529, 0.8511, 0.8495, 0.8479, 0.8460, 0.8436, 0.8414, 0.8373], 29 | [0.9125, 0.9111, 0.9096, 0.9078, 0.9055, 0.9034, 0.9014, 0.8995, 0.8979, 0.8964, 0.8950, 0.8936, 0.8924, 0.8903, 0.8877, 0.8853, 0.8830, 0.8808, 0.8787, 0.8749], 30 | [0.9228, 0.9242, 0.9250, 0.9247, 0.9240, 0.9232, 0.9224, 0.9217, 0.9210, 0.9203, 0.9197, 0.9188, 0.9162, 0.9137, 0.9113, 0.9091, 0.9070, 0.9050, 0.9031, 0.8980], 31 | [0.9215, 0.9258, 0.9289, 0.9304, 0.9313, 0.9319, 0.9323, 0.9326, 0.9328, 0.9329, 0.9330, 0.9311, 0.9288, 0.9266, 0.9245, 0.9225, 0.9206, 0.9188, 0.9161, 0.9107], 32 | [0.9106, 0.9176, 0.9232, 0.9267, 0.9292, 0.9312, 0.9327, 0.9340, 0.9351, 0.9361, 0.9369, 0.9349, 0.9329, 0.9311, 0.9293, 0.9276, 0.9259, 0.9239, 0.9212, 0.9161]]]) 33 | 34 | HPTMap.WpMap = np.array([[[30.446, 30.533, 30.568, 30.572, 30.572, 30.572, 30.572, 30.572, 30.572, 30.572, 30.572, 30.572, 30.572, 30.572, 30.572, 30.572, 30.572, 30.572, 30.572, 30.572], 35 | [30.299, 30.413, 30.480, 30.516, 30.529, 30.530, 30.530, 30.530, 30.530, 30.530, 30.530, 30.530, 30.530, 30.530, 30.530, 30.530, 30.530, 30.530, 30.530, 30.530], 36 | [30.120, 30.239, 30.317, 30.368, 30.398, 30.415, 30.421, 30.421, 30.421, 30.421, 30.421, 30.421, 30.421, 30.421, 30.421, 30.421, 30.421, 30.421, 30.421, 30.421], 37 | [30.014, 30.124, 30.201, 30.253, 30.288, 30.311, 30.325, 30.333, 30.337, 30.337, 30.337, 30.337, 30.337, 30.337, 30.337, 30.337, 30.337, 30.337, 30.337, 30.337], 38 | [29.856, 29.948, 30.013, 30.059, 30.091, 30.113, 30.128, 30.139, 30.145, 30.149, 30.150, 30.150, 30.150, 30.150, 30.150, 30.150, 30.150, 30.150, 30.150, 30.150], 39 | [29.799, 29.870, 29.920, 29.955, 29.979, 29.997, 30.009, 30.017, 30.023, 30.026, 30.028, 30.029, 30.029, 30.029, 30.029, 30.029, 30.029, 30.029, 30.029, 30.029]], 40 | [[30.446, 30.533, 30.568, 30.572, 30.572, 30.572, 30.572, 30.572, 30.572, 30.572, 30.572, 30.572, 30.572, 30.572, 30.572, 30.572, 30.572, 30.572, 30.572, 30.572], 41 | [30.299, 30.413, 30.480, 30.516, 30.529, 30.530, 30.530, 30.530, 30.530, 30.530, 30.530, 30.530, 30.530, 30.530, 30.530, 30.530, 30.530, 30.530, 30.530, 30.530], 42 | [30.120, 30.239, 30.317, 30.368, 30.398, 30.415, 30.421, 30.421, 30.421, 30.421, 30.421, 30.421, 30.421, 30.421, 30.421, 30.421, 30.421, 30.421, 30.421, 30.421], 43 | [30.014, 30.124, 30.201, 30.253, 30.288, 30.311, 30.325, 30.333, 30.337, 30.337, 30.337, 30.337, 30.337, 30.337, 30.337, 30.337, 30.337, 30.337, 30.337, 30.337], 44 | [29.856, 29.948, 30.013, 30.059, 30.091, 30.113, 30.128, 30.139, 30.145, 30.149, 30.150, 30.150, 30.150, 30.150, 30.150, 30.150, 30.150, 30.150, 30.150, 30.150], 45 | [29.799, 29.870, 29.920, 29.955, 29.979, 29.997, 30.009, 30.017, 30.023, 30.026, 30.028, 30.029, 30.029, 30.029, 30.029, 30.029, 30.029, 30.029, 30.029, 30.029]]]) 46 | 47 | #HPTMap.Np_data, HPTMap.alpha_data, HPTMap.PR_data = np.meshgrid(HPTMap.Nc_vals, HPTMap.alpha_vals, HPTMap.PR_vals, sparse=False) 48 | HPTMap.Npts = HPTMap.NpMap.size 49 | 50 | HPTMap.units = {} 51 | HPTMap.units['NpMap'] = 'rpm' 52 | HPTMap.units['WpMap'] = 'lbm/s' 53 | 54 | # format for new regular grid interpolator: 55 | 56 | HPTMap.param_data = [] 57 | HPTMap.output_data = [] 58 | 59 | HPTMap.param_data.append({'name': 'alphaMap', 'values': HPTMap.alphaMap, 60 | 'default': 1, 'units': None}) 61 | HPTMap.param_data.append({'name': 'NpMap', 'values': HPTMap.NpMap, 62 | 'default': 100.0, 'units': 'rpm'}) 63 | HPTMap.param_data.append({'name': 'PRmap', 'values': HPTMap.PRmap, 64 | 'default': 5.0, 'units': None}) 65 | 66 | HPTMap.output_data.append({'name': 'WpMap', 'values': HPTMap.WpMap, 67 | 'default': np.mean(HPTMap.WpMap), 'units': 'lbm/s'}) 68 | HPTMap.output_data.append({'name': 'effMap', 'values': HPTMap.effMap, 69 | 'default': np.mean(HPTMap.effMap), 'units': None}) 70 | -------------------------------------------------------------------------------- /example_cycles/N+3ref/N3_MDP.py: -------------------------------------------------------------------------------- 1 | import numpy as np 2 | import time 3 | import pickle 4 | from pprint import pprint 5 | 6 | import openmdao.api as om 7 | 8 | import pycycle.api as pyc 9 | 10 | from N3ref import N3, viewer, MPN3 11 | 12 | def N3_MDP_model(): 13 | 14 | prob = om.Problem() 15 | 16 | prob.model = MPN3(order_add=['bal']) 17 | 18 | bal = prob.model.add_subsystem('bal', om.BalanceComp(), promotes=['RTO_T4',]) 19 | bal.add_balance('TOC_BPR', val=23.7281, units=None, eq_units='ft/s', use_mult=True) 20 | prob.model.connect('bal.TOC_BPR', 'TOC.splitter.BPR') 21 | prob.model.connect('CRZ.byp_nozz.Fl_O:stat:V', 'bal.lhs:TOC_BPR') 22 | prob.model.connect('CRZ.core_nozz.Fl_O:stat:V', 'bal.rhs:TOC_BPR') 23 | 24 | bal.add_balance('TOC_W', val=820.95, units='lbm/s', eq_units='degR', rhs_name='RTO_T4') 25 | prob.model.connect('bal.TOC_W', 'TOC.fc.W') 26 | prob.model.connect('RTO.burner.Fl_O:tot:T', 'bal.lhs:TOC_W') 27 | 28 | bal.add_balance('CRZ_Fn_target', val=5514.4, units='lbf', eq_units='lbf', use_mult=True, mult_val=0.9, ref0=5000.0, ref=7000.0) 29 | prob.model.connect('bal.CRZ_Fn_target', 'CRZ.balance.rhs:FAR') 30 | prob.model.connect('TOC.perf.Fn', 'bal.lhs:CRZ_Fn_target') 31 | prob.model.connect('CRZ.perf.Fn','bal.rhs:CRZ_Fn_target') 32 | 33 | bal.add_balance('SLS_Fn_target', val=28620.8, units='lbf', eq_units='lbf', use_mult=True, mult_val=1.2553, ref0=28000.0, ref=30000.0) 34 | prob.model.connect('bal.SLS_Fn_target', 'SLS.balance.rhs:FAR') 35 | prob.model.connect('RTO.perf.Fn', 'bal.lhs:SLS_Fn_target') 36 | prob.model.connect('SLS.perf.Fn','bal.rhs:SLS_Fn_target') 37 | 38 | # setup the optimization 39 | prob.driver = om.ScipyOptimizeDriver() 40 | prob.driver.options['optimizer'] = 'SLSQP' 41 | prob.driver.options['debug_print'] = ['desvars', 'nl_cons', 'objs'] 42 | prob.driver.opt_settings={'Major step limit': 0.05} 43 | 44 | prob.model.add_design_var('fan:PRdes', lower=1.20, upper=1.4) 45 | prob.model.add_design_var('lpc:PRdes', lower=2.0, upper=4.0) 46 | prob.model.add_design_var('TOC.balance.rhs:hpc_PR', lower=40.0, upper=70.0, ref0=40.0, ref=70.0) 47 | prob.model.add_design_var('RTO_T4', lower=3000.0, upper=3600.0, ref0=3000.0, ref=3600.0) 48 | prob.model.add_design_var('bal.mult:TOC_BPR', lower=1.35, upper=1.45, ref0=1.35, ref=1.45) 49 | prob.model.add_design_var('T4_ratio.TR', lower=0.5, upper=0.95, ref0=0.5, ref=0.95) 50 | 51 | prob.model.add_objective('TOC.perf.TSFC') 52 | 53 | prob.model.add_constraint('TOC.fan_dia.FanDia', upper=100.0, ref=100.0) 54 | 55 | recorder = om.SqliteRecorder('N3_opt.sql') 56 | prob.model.add_recorder(recorder) 57 | prob.model.recording_options['record_inputs'] = True 58 | prob.model.recording_options['record_outputs'] = True 59 | 60 | prob.model.set_input_defaults('RTO_T4', 3400.0, units='degR') 61 | 62 | return(prob) 63 | 64 | if __name__ == "__main__": 65 | 66 | prob = N3_MDP_model() 67 | 68 | prob.setup() 69 | 70 | # Define the design point 71 | prob.set_val('TOC.splitter.BPR', 23.7281) 72 | prob.set_val('TOC.balance.rhs:hpc_PR', 53.6332) 73 | 74 | # Set specific cycle parameters 75 | prob.set_val('fan:PRdes', 1.300) 76 | prob.set_val('SLS.balance.rhs:FAR', 28620.9, units='lbf') 77 | prob.set_val('CRZ.balance.rhs:FAR', 5466.5, units='lbf') 78 | prob.set_val('lpc:PRdes', 3.000), 79 | prob.set_val('T4_ratio.TR', 0.926470588) 80 | prob.set_val('bal.mult:TOC_BPR', 1.41038) 81 | prob.set_val('RTO.hpt_cooling.x_factor', 0.9) 82 | 83 | # Set initial guesses for balances 84 | prob['TOC.balance.FAR'] = 0.02650 85 | prob['bal.TOC_W'] = 820.95 86 | prob['TOC.balance.lpt_PR'] = 10.937 87 | prob['TOC.balance.hpt_PR'] = 4.185 88 | prob['TOC.fc.balance.Pt'] = 5.272 89 | prob['TOC.fc.balance.Tt'] = 444.41 90 | 91 | FAR_guess = [0.02832, 0.02541, 0.02510] 92 | W_guess = [1916.13, 2000. , 802.79] 93 | BPR_guess = [25.5620, 27.3467, 24.3233] 94 | fan_Nmech_guess = [2132.6, 1953.1, 2118.7] 95 | lp_Nmech_guess = [6611.2, 6054.5, 6567.9] 96 | hp_Nmech_guess = [22288.2, 21594.0, 20574.1] 97 | Pt_guess = [15.349, 14.696, 5.272] 98 | Tt_guess = [552.49, 545.67, 444.41] 99 | hpt_PR_guess = [4.210, 4.245, 4.197] 100 | lpt_PR_guess = [8.161, 7.001, 10.803] 101 | fan_Rline_guess = [1.7500, 1.7500, 1.9397] 102 | lpc_Rline_guess = [2.0052, 1.8632, 2.1075] 103 | hpc_Rline_guess = [2.0589, 2.0281, 1.9746] 104 | trq_guess = [52509.1, 41779.4, 22369.7] 105 | 106 | for i, pt in enumerate(prob.model.od_pts): 107 | 108 | # initial guesses 109 | prob[pt+'.balance.FAR'] = FAR_guess[i] 110 | prob[pt+'.balance.W'] = W_guess[i] 111 | prob[pt+'.balance.BPR'] = BPR_guess[i] 112 | prob[pt+'.balance.fan_Nmech'] = fan_Nmech_guess[i] 113 | prob[pt+'.balance.lp_Nmech'] = lp_Nmech_guess[i] 114 | prob[pt+'.balance.hp_Nmech'] = hp_Nmech_guess[i] 115 | prob[pt+'.fc.balance.Pt'] = Pt_guess[i] 116 | prob[pt+'.fc.balance.Tt'] = Tt_guess[i] 117 | prob[pt+'.hpt.PR'] = hpt_PR_guess[i] 118 | prob[pt+'.lpt.PR'] = lpt_PR_guess[i] 119 | prob[pt+'.fan.map.RlineMap'] = fan_Rline_guess[i] 120 | prob[pt+'.lpc.map.RlineMap'] = lpc_Rline_guess[i] 121 | prob[pt+'.hpc.map.RlineMap'] = hpc_Rline_guess[i] 122 | prob[pt+'.gearbox.trq_base'] = trq_guess[i] 123 | 124 | st = time.time() 125 | 126 | prob.set_solver_print(level=-1) 127 | prob.set_solver_print(level=2, depth=1) 128 | prob.run_model() 129 | 130 | for pt in ['TOC']+prob.model.od_pts: 131 | viewer(prob, pt) 132 | 133 | print() 134 | print('Diameter', prob['TOC.fan_dia.FanDia'][0]) 135 | print("time", time.time() - st) -------------------------------------------------------------------------------- /example_cycles/N+3ref/N3_MDP_Opt.py: -------------------------------------------------------------------------------- 1 | import numpy as np 2 | import time 3 | import pickle 4 | from pprint import pprint 5 | 6 | import openmdao.api as om 7 | import pycycle.api as pyc 8 | 9 | from N3ref import N3, viewer, MPN3 10 | 11 | def N3_MDP_Opt_model(): 12 | 13 | prob = om.Problem() 14 | prob.model = MPN3(order_add=['bal']) 15 | 16 | prob.model.pyc_add_cycle_param('ext_ratio.core_Cv', 0.9999) 17 | prob.model.pyc_add_cycle_param('ext_ratio.byp_Cv', 0.9975) 18 | 19 | bal = prob.model.add_subsystem('bal', om.BalanceComp(), promotes=['RTO_T4',]) 20 | 21 | bal.add_balance('TOC_BPR', val=23.7281, units=None, eq_units=None) 22 | prob.model.connect('bal.TOC_BPR', 'TOC.splitter.BPR') 23 | prob.model.connect('CRZ.ext_ratio.ER', 'bal.lhs:TOC_BPR') 24 | 25 | bal.add_balance('TOC_W', val=820.95, units='lbm/s', eq_units='degR', rhs_name='RTO_T4') 26 | prob.model.connect('bal.TOC_W', 'TOC.fc.W') 27 | prob.model.connect('RTO.burner.Fl_O:tot:T', 'bal.lhs:TOC_W') 28 | 29 | bal.add_balance('CRZ_Fn_target', val=5514.4, units='lbf', eq_units='lbf', use_mult=True, mult_val=0.9, ref0=5000.0, ref=7000.0) 30 | prob.model.connect('bal.CRZ_Fn_target', 'CRZ.balance.rhs:FAR') 31 | prob.model.connect('TOC.perf.Fn', 'bal.lhs:CRZ_Fn_target') 32 | prob.model.connect('CRZ.perf.Fn','bal.rhs:CRZ_Fn_target') 33 | 34 | bal.add_balance('SLS_Fn_target', val=28620.8, units='lbf', eq_units='lbf', use_mult=True, mult_val=1.2553, ref0=28000.0, ref=30000.0) 35 | prob.model.connect('bal.SLS_Fn_target', 'SLS.balance.rhs:FAR') 36 | prob.model.connect('RTO.perf.Fn', 'bal.lhs:SLS_Fn_target') 37 | prob.model.connect('SLS.perf.Fn','bal.rhs:SLS_Fn_target') 38 | 39 | # setup the optimization 40 | prob.driver = om.ScipyOptimizeDriver() 41 | prob.driver.options['optimizer'] = 'SLSQP' 42 | prob.driver.options['debug_print'] = ['desvars', 'nl_cons', 'objs'] 43 | prob.driver.opt_settings={'Major step limit': 0.05} 44 | 45 | prob.model.add_design_var('fan:PRdes', lower=1.20, upper=1.4) 46 | prob.model.add_design_var('lpc:PRdes', lower=2.5, upper=4.0) 47 | prob.model.add_design_var('TOC.balance.rhs:hpc_PR', lower=40.0, upper=70.0, ref0=40.0, ref=70.0) 48 | prob.model.add_design_var('RTO_T4', lower=3000.0, upper=3600.0, ref0=3000.0, ref=3600.0) 49 | prob.model.add_design_var('bal.rhs:TOC_BPR', lower=1.35, upper=1.45, ref0=1.35, ref=1.45) 50 | prob.model.add_design_var('T4_ratio.TR', lower=0.5, upper=0.95, ref0=0.5, ref=0.95) 51 | 52 | prob.model.add_objective('CRZ.perf.TSFC', ref0=0.4, ref=0.5) 53 | 54 | # to add the constraint to the model 55 | prob.model.add_constraint('TOC.fan_dia.FanDia', upper=100.0, ref=100.0) 56 | prob.model.add_constraint('TOC.perf.Fn', lower=5800.0, ref=6000.0) 57 | 58 | prob.model.set_input_defaults('RTO_T4', 3400.0, units='degR') 59 | 60 | return(prob) 61 | 62 | if __name__ == "__main__": 63 | 64 | prob = N3_MDP_Opt_model() 65 | 66 | prob.setup() 67 | 68 | # Define the design point 69 | prob.set_val('TOC.splitter.BPR', 23.7281) 70 | prob.set_val('TOC.balance.rhs:hpc_PR', 55.0) 71 | 72 | # Set specific cycle parameters 73 | prob.set_val('SLS.fc.MN', 0.001) 74 | prob.set_val('SLS.balance.rhs:FAR', 28620.9, units='lbf') 75 | prob.set_val('CRZ.balance.rhs:FAR', 5466.5, units='lbf') 76 | prob.set_val('bal.rhs:TOC_BPR', 1.40) 77 | prob.set_val('T4_ratio.TR', 0.926470588) 78 | prob.set_val('fan:PRdes', 1.300) 79 | prob.set_val('lpc:PRdes', 3.000) 80 | prob.set_val('RTO.hpt_cooling.x_factor', 0.9) 81 | 82 | # Set inital guesses for balances 83 | prob['TOC.balance.FAR'] = 0.02650 84 | prob['bal.TOC_W'] = 820.95 85 | prob['TOC.balance.lpt_PR'] = 10.937 86 | prob['TOC.balance.hpt_PR'] = 4.185 87 | prob['TOC.fc.balance.Pt'] = 5.272 88 | prob['TOC.fc.balance.Tt'] = 444.41 89 | 90 | FAR_guess = [0.02832, 0.02541, 0.02510] 91 | W_guess = [1916.13, 2000 , 802.79] 92 | BPR_guess = [25.5620, 27.3467, 24.3233] 93 | fan_Nmech_guess = [2132.6, 1953.1, 2118.7] 94 | lp_Nmech_guess = [6611.2, 6054.5, 6567.9] 95 | hp_Nmech_guess = [22288.2, 21594.0, 20574.1] 96 | Pt_guess = [15.349, 14.696, 5.272] 97 | Tt_guess = [552.49, 545.67, 444.41] 98 | hpt_PR_guess = [4.210, 4.245, 4.197] 99 | lpt_PR_guess = [8.161, 7.001, 10.803] 100 | fan_Rline_guess = [1.7500, 1.7500, 1.9397] 101 | lpc_Rline_guess = [2.0052, 1.8632, 2.1075] 102 | hpc_Rline_guess = [2.0589, 2.0281, 1.9746] 103 | trq_guess = [52509.1, 41779.4, 22369.7] 104 | 105 | for i, pt in enumerate(prob.model.od_pts): 106 | 107 | # initial guesses 108 | prob[pt+'.balance.FAR'] = FAR_guess[i] 109 | prob[pt+'.balance.W'] = W_guess[i] 110 | prob[pt+'.balance.BPR'] = BPR_guess[i] 111 | prob[pt+'.balance.fan_Nmech'] = fan_Nmech_guess[i] 112 | prob[pt+'.balance.lp_Nmech'] = lp_Nmech_guess[i] 113 | prob[pt+'.balance.hp_Nmech'] = hp_Nmech_guess[i] 114 | prob[pt+'.fc.balance.Pt'] = Pt_guess[i] 115 | prob[pt+'.fc.balance.Tt'] = Tt_guess[i] 116 | prob[pt+'.hpt.PR'] = hpt_PR_guess[i] 117 | prob[pt+'.lpt.PR'] = lpt_PR_guess[i] 118 | prob[pt+'.fan.map.RlineMap'] = fan_Rline_guess[i] 119 | prob[pt+'.lpc.map.RlineMap'] = lpc_Rline_guess[i] 120 | prob[pt+'.hpc.map.RlineMap'] = hpc_Rline_guess[i] 121 | prob[pt+'.gearbox.trq_base'] = trq_guess[i] 122 | 123 | st = time.time() 124 | 125 | prob.set_solver_print(level=-1) 126 | prob.set_solver_print(level=2, depth=1) 127 | prob.run_driver() 128 | 129 | for pt in ['TOC']+prob.model.od_pts: 130 | viewer(prob, pt) 131 | 132 | print() 133 | print('Diameter', prob['TOC.fan_dia.FanDia'][0]) 134 | print('ER', prob['CRZ.ext_ratio.ER']) 135 | 136 | print("time", time.time() - st) -------------------------------------------------------------------------------- /example_cycles/N+3ref/N3_MDP_verif.py: -------------------------------------------------------------------------------- 1 | import numpy as np 2 | import time 3 | import pickle 4 | from pprint import pprint 5 | 6 | from openmdao.api import DirectSolver, BoundsEnforceLS, NewtonSolver, ArmijoGoldsteinLS, LinearBlockGS, pyOptSparseDriver 7 | from openmdao.api import Problem, IndepVarComp, SqliteRecorder, CaseReader, BalanceComp, ScipyKrylov, PETScKrylov, ExecComp 8 | from openmdao.utils.units import convert_units as cu 9 | 10 | import pycycle.api as pyc 11 | 12 | from N3ref import N3, viewer, MPN3 13 | 14 | def N3_MDP_verif_model(OD_statics=True): 15 | 16 | prob = Problem() 17 | 18 | prob.model = MPN3(order_add=['bal'], statics=OD_statics) 19 | 20 | prob.model.pyc_add_cycle_param('ext_ratio.core_Cv', 0.9999) 21 | prob.model.pyc_add_cycle_param('ext_ratio.byp_Cv', 0.9975) 22 | 23 | bal = prob.model.add_subsystem('bal', BalanceComp(), promotes_inputs=['RTO_T4',]) 24 | 25 | bal.add_balance('TOC_BPR', val=23.7281, units=None, eq_units=None) 26 | prob.model.connect('bal.TOC_BPR', 'TOC.splitter.BPR') 27 | prob.model.connect('CRZ.ext_ratio.ER', 'bal.lhs:TOC_BPR') 28 | 29 | bal.add_balance('TOC_W', val=820.95, units='lbm/s', eq_units='degR', rhs_name='RTO_T4') 30 | prob.model.connect('bal.TOC_W', 'TOC.fc.W') 31 | prob.model.connect('RTO.burner.Fl_O:tot:T', 'bal.lhs:TOC_W') 32 | 33 | bal.add_balance('CRZ_Fn_target', val=5514.4, units='lbf', eq_units='lbf', use_mult=True, mult_val=0.9, ref0=5000.0, ref=7000.0) 34 | prob.model.connect('bal.CRZ_Fn_target', 'CRZ.balance.rhs:FAR') 35 | prob.model.connect('TOC.perf.Fn', 'bal.lhs:CRZ_Fn_target') 36 | prob.model.connect('CRZ.perf.Fn','bal.rhs:CRZ_Fn_target') 37 | 38 | bal.add_balance('SLS_Fn_target', val=28620.8, units='lbf', eq_units='lbf', use_mult=True, mult_val=1.2553, ref0=28000.0, ref=30000.0) 39 | prob.model.connect('bal.SLS_Fn_target', 'SLS.balance.rhs:FAR') 40 | prob.model.connect('RTO.perf.Fn', 'bal.lhs:SLS_Fn_target') 41 | prob.model.connect('SLS.perf.Fn','bal.rhs:SLS_Fn_target') 42 | 43 | prob.model.set_input_defaults('RTO_T4', 3400.0, units='degR') 44 | 45 | return(prob) 46 | 47 | if __name__ == "__main__": 48 | 49 | OD_statics = True 50 | 51 | prob = N3_MDP_verif_model(OD_statics) 52 | 53 | prob.setup() 54 | 55 | # Define the design point 56 | prob.set_val('TOC.splitter.BPR', 23.7281) 57 | prob.set_val('TOC.balance.rhs:hpc_PR', 55.0) 58 | prob.set_val('TOC.opr_calc.FPR', 1.300) 59 | prob.set_val('TOC.opr_calc.LPCPR', 3.000) 60 | 61 | # Set up specific cylce parameters 62 | prob.set_val('SLS.fc.MN', 0.001) 63 | prob.set_val('SLS.balance.rhs:FAR', 28620.9, units='lbf') 64 | prob.set_val('CRZ.balance.rhs:FAR', 5466.5, units='lbf') 65 | prob.set_val('bal.rhs:TOC_BPR', 1.40) 66 | prob.set_val('fan:PRdes', 1.300) 67 | prob.set_val('lpc:PRdes', 3.000), 68 | prob.set_val('T4_ratio.TR', 0.926470588) 69 | 70 | prob.set_val('RTO.hpt_cooling.x_factor', 0.9) 71 | 72 | # initial guesses 73 | prob['TOC.balance.FAR'] = 0.02650 74 | prob['bal.TOC_W'] = 820.95 75 | prob['TOC.balance.lpt_PR'] = 10.937 76 | prob['TOC.balance.hpt_PR'] = 4.185 77 | prob['TOC.fc.balance.Pt'] = 5.272 78 | prob['TOC.fc.balance.Tt'] = 444.41 79 | 80 | FAR_guess = [0.02832, 0.02541, 0.02510] 81 | W_guess = [1916.13, 1734.44, 802.79] 82 | BPR_guess = [25.5620, 27.3467, 24.3233] 83 | fan_Nmech_guess = [2132.6, 1953.1, 2118.7] 84 | lp_Nmech_guess = [6611.2, 6054.5, 6567.9] 85 | hp_Nmech_guess = [22288.2, 21594.0, 20574.1] 86 | Pt_guess = [15.349, 14.696, 5.272] 87 | Tt_guess = [552.49, 545.67, 444.41] 88 | hpt_PR_guess = [4.210, 4.245, 4.197] 89 | lpt_PR_guess = [8.161, 7.001, 10.803] 90 | fan_Rline_guess = [1.7500, 1.7500, 1.9397] 91 | lpc_Rline_guess = [2.0052, 1.8632, 2.1075] 92 | hpc_Rline_guess = [2.0589, 2.0281 , 1.9746] 93 | trq_guess = [52509.1, 41779.4, 22369.7] 94 | 95 | for i, pt in enumerate(prob.model.od_pts): 96 | 97 | # initial guesses 98 | prob[pt+'.balance.FAR'] = FAR_guess[i] 99 | prob[pt+'.balance.W'] = W_guess[i] 100 | prob[pt+'.balance.BPR'] = BPR_guess[i] 101 | prob[pt+'.balance.fan_Nmech'] = fan_Nmech_guess[i] 102 | prob[pt+'.balance.lp_Nmech'] = lp_Nmech_guess[i] 103 | prob[pt+'.balance.hp_Nmech'] = hp_Nmech_guess[i] 104 | prob[pt+'.fc.balance.Pt'] = Pt_guess[i] 105 | prob[pt+'.fc.balance.Tt'] = Tt_guess[i] 106 | prob[pt+'.hpt.PR'] = hpt_PR_guess[i] 107 | prob[pt+'.lpt.PR'] = lpt_PR_guess[i] 108 | prob[pt+'.fan.map.RlineMap'] = fan_Rline_guess[i] 109 | prob[pt+'.lpc.map.RlineMap'] = lpc_Rline_guess[i] 110 | prob[pt+'.hpc.map.RlineMap'] = hpc_Rline_guess[i] 111 | prob[pt+'.gearbox.trq_base'] = trq_guess[i] 112 | 113 | st = time.time() 114 | 115 | prob.set_solver_print(level=-1) 116 | prob.set_solver_print(level=2, depth=1) 117 | prob.run_model() 118 | 119 | data = prob.compute_totals(of=['TOC.perf.Fn','RTO.perf.Fn','SLS.perf.Fn','CRZ.perf.Fn', 120 | 'TOC.perf.TSFC','RTO.perf.TSFC','SLS.perf.TSFC','CRZ.perf.TSFC', 121 | # 'bal.TOC_BPR','TOC.hpc_CS.CS',], wrt=['OPR', 'RTO_T4']) 122 | 'TOC.hpc_CS.CS',], wrt=['TOC.balance.rhs:hpc_PR', 'RTO_T4']) 123 | pprint(data) 124 | 125 | with open('derivs.pkl','wb') as f: 126 | pickle.dump(data, file=f) 127 | 128 | for pt in ['TOC']+prob.model.od_pts: 129 | viewer(prob, pt) 130 | 131 | print() 132 | print('Diameter', prob['TOC.fan_dia.FanDia'][0]) 133 | print('ER', prob['CRZ.ext_ratio.ER']) 134 | print("time", time.time() - st) -------------------------------------------------------------------------------- /example_cycles/N+3ref/N3_SPD.py: -------------------------------------------------------------------------------- 1 | import numpy as np 2 | import time 3 | import pickle 4 | from pprint import pprint 5 | 6 | import openmdao.api as om 7 | from openmdao.utils.general_utils import set_pyoptsparse_opt 8 | 9 | import pycycle.api as pyc 10 | 11 | from N3ref import N3, viewer, MPN3 12 | 13 | # check that pyoptsparse is installed 14 | OPT, OPTIMIZER = set_pyoptsparse_opt('SNOPT') 15 | if OPTIMIZER: 16 | from openmdao.drivers.pyoptsparse_driver import pyOptSparseDriver 17 | 18 | 19 | def N3_SPD_model(): 20 | 21 | prob = om.Problem() 22 | 23 | prob.model = MPN3() 24 | 25 | # setup the optimization 26 | prob.driver = om.pyOptSparseDriver() 27 | prob.driver.options['optimizer'] = OPTIMIZER 28 | prob.driver.options['debug_print'] = ['desvars', 'nl_cons', 'objs'] 29 | prob.driver.opt_settings={'Major step limit': 0.05} 30 | 31 | prob.model.add_design_var('fan:PRdes', lower=1.20, upper=1.4) 32 | prob.model.add_design_var('lpc:PRdes', lower=2.0, upper=4.0) 33 | prob.model.add_design_var('TOC.balance.rhs:hpc_PR', lower=40.0, upper=70.0, ref0=40.0, ref=70.0) 34 | prob.model.add_design_var('RTO_T4', lower=3000.0, upper=3600.0, ref0=3000.0, ref=3600.0) 35 | prob.model.add_design_var('T4_ratio.TR', lower=0.5, upper=0.95, ref0=0.5, ref=0.95) 36 | 37 | prob.model.add_objective('TOC.perf.TSFC') 38 | 39 | # to add the constraint to the model 40 | prob.model.add_constraint('TOC.fan_dia.FanDia', upper=100.0, ref=100.0) 41 | 42 | recorder = om.SqliteRecorder('N3_opt.sql') 43 | prob.model.add_recorder(recorder) 44 | prob.model.recording_options['record_inputs'] = True 45 | prob.model.recording_options['record_outputs'] = True 46 | 47 | return(prob) 48 | 49 | if __name__ == "__main__": 50 | 51 | prob = N3_SPD_model() 52 | 53 | prob.setup() 54 | 55 | # prob.model.RTO.nonlinear_solver.options['maxiter'] = 0 56 | # prob.model.SLS.nonlinear_solver.options['maxiter'] = 0 57 | # prob.model.CRZ.nonlinear_solver.options['maxiter'] = 0 58 | # prob.model.nonlinear_solver.options['maxiter'] = 0 59 | 60 | # Define the design point 61 | prob.set_val('TOC.splitter.BPR', 23.94514401), 62 | prob.set_val('TOC.balance.rhs:hpc_PR', 53.6332) 63 | prob.set_val('TOC.fc.W', 820.44097898, units='lbm/s') 64 | 65 | # Set specific cycle parameters 66 | prob.set_val('fan:PRdes', 1.300), 67 | prob.set_val('lpc:PRdes', 3.000), 68 | prob.set_val('T4_ratio.TR', 0.926470588) 69 | prob.set_val('RTO_T4', 3400.0, units='degR') 70 | prob.set_val('SLS.balance.rhs:FAR', 28620.84, units='lbf') 71 | prob.set_val('CRZ.balance.rhs:FAR', 5510.72833567, units='lbf') 72 | prob.set_val('RTO.hpt_cooling.x_factor', 0.9) 73 | 74 | # Set initial guesses for balances 75 | prob['TOC.balance.FAR'] = 0.02650 76 | prob['TOC.balance.lpt_PR'] = 10.937 77 | prob['TOC.balance.hpt_PR'] = 4.185 78 | prob['TOC.fc.balance.Pt'] = 5.272 79 | prob['TOC.fc.balance.Tt'] = 444.41 80 | 81 | FAR_guess = [0.02832, 0.02541, 0.02510] 82 | W_guess = [1916.13, 1900. , 802.79] 83 | BPR_guess = [25.5620, 27.3467, 24.3233] 84 | hpc_PR_guess = [14., 14., 14.] 85 | fan_Nmech_guess = [2132.6, 1953.1, 2118.7] 86 | lp_Nmech_guess = [6611.2, 6054.5, 6567.9] 87 | hp_Nmech_guess = [22288.2, 21594.0, 20574.1] 88 | hpt_PR_guess = [4.210, 4.245, 4.197] 89 | lpt_PR_guess = [8.161, 8., 10.803] 90 | fan_Rline_guess = [1.7500, 1.7500, 1.9397] 91 | lpc_Rline_guess = [2.0052, 1.8632, 2.1075] 92 | hpc_Rline_guess = [2.0589, 2.0281, 1.9746] 93 | trq_guess = [52509.1, 41779.4, 22369.7] 94 | 95 | for i, pt in enumerate(prob.model.od_pts): 96 | 97 | # initial guesses 98 | prob[pt+'.balance.FAR'] = FAR_guess[i] 99 | prob[pt+'.balance.W'] = W_guess[i] 100 | prob[pt+'.balance.BPR'] = BPR_guess[i] 101 | prob[pt+'.balance.BPR'] = BPR_guess[i] 102 | prob[pt+'.balance.fan_Nmech'] = fan_Nmech_guess[i] 103 | prob[pt+'.balance.lp_Nmech'] = lp_Nmech_guess[i] 104 | prob[pt+'.balance.hp_Nmech'] = hp_Nmech_guess[i] 105 | prob[pt+'.hpc.PR'] = hpc_PR_guess[i] 106 | prob[pt+'.hpt.PR'] = hpt_PR_guess[i] 107 | prob[pt+'.lpt.PR'] = lpt_PR_guess[i] 108 | prob[pt+'.fan.map.RlineMap'] = fan_Rline_guess[i] 109 | prob[pt+'.lpc.map.RlineMap'] = lpc_Rline_guess[i] 110 | prob[pt+'.hpc.map.RlineMap'] = hpc_Rline_guess[i] 111 | prob[pt+'.gearbox.trq_base'] = trq_guess[i] 112 | 113 | st = time.time() 114 | 115 | prob.set_solver_print(level=-1) 116 | prob.set_solver_print(level=2, depth=1) 117 | prob.run_model() 118 | 119 | # prob.model.RTO.list_outputs(residuals=True, prom_name=True) 120 | # prob.model.RTO.hpc.ideal_flow.list_inputs(units=True, prom_name=True) 121 | # exit() 122 | 123 | for pt in ['TOC']+prob.model.od_pts: 124 | viewer(prob, pt) 125 | 126 | print("time", time.time() - st) 127 | -------------------------------------------------------------------------------- /example_cycles/N+3ref/small_core_eff_balance.py: -------------------------------------------------------------------------------- 1 | from openmdao.api import ImplicitComponent 2 | import numpy as np 3 | from scipy.interpolate import Akima1DInterpolator as Akima 4 | 5 | 6 | """ Create tables for table lookup functions """ 7 | # Small engines polytripic efficiency values 8 | Wc_SE = np.array([0, 0.205, 0.63, 1.0, 1.5, 2., 2.5, 3., 4., 5., 30., 200]) 9 | # TGL 0 - current technology level 10 | EtaPoly_SE0 =np.array([0, 0.82, 0.86, 0.871, 0.881, 0.885, 0.8875, 0.889, 0.892, 0.894, 0.895, 0.895]) 11 | # TGL 1 - next generation technology level ~2% better 12 | EtaPoly_SE1 =np.array([0, 0.84, 0.88, 0.891, 0.901, 0.905, 0.9075, 0.909, 0.912, 0.914, 0.915, 0.915 ]) 13 | # TGL 2 - beyond next generation technology level ~4% better 14 | EtaPoly_SE2 =np.array([0, 0.855, 0.900, 0.912, 0.917, 0.920, 0.922, 0.9235, 0.926, 0.930, 0.931, 0.931]) 15 | 16 | # Create continuously differentiable interpolations 17 | EtaPoly_SE0_interp = Akima(Wc_SE, EtaPoly_SE0) 18 | EtaPoly_SE1_interp = Akima(Wc_SE, EtaPoly_SE1) 19 | EtaPoly_SE2_interp = Akima(Wc_SE, EtaPoly_SE2) 20 | 21 | # gather derivatives 22 | EtaPoly_SE0_interp_deriv = EtaPoly_SE0_interp.derivative(1) 23 | EtaPoly_SE1_interp_deriv = EtaPoly_SE1_interp.derivative(1) 24 | EtaPoly_SE2_interp_deriv = EtaPoly_SE2_interp.derivative(1) 25 | 26 | class SmallCoreEffBalance(ImplicitComponent): 27 | """ Polytropic/ Adiabatic efficiency balance. """ 28 | 29 | def initialize(self): 30 | self.options.declare('tech_level', default=0, values=[0,1,2], 31 | desc='Set Technology level, 0 - current tech, 1 - next gen ~2% better, 2 - beyond next gen ~4% better') 32 | self.options.declare('eng_type', default='large', values=['large', 'small'], 33 | desc='Set engine type, which changes the polytropic eff curve') 34 | def setup(self): 35 | self.add_input('CS',val = 1.0,units='lbm/s', desc='core size or corrected mass flow on the high pressure side of the HPC') 36 | self.add_input('eta_p', val = 1.0, units=None, desc='polytropic efficiency') 37 | 38 | self.add_output('eta_a', val = 0.9, units=None, desc='adiabatic efficiency', upper=1, lower=0.8) 39 | 40 | self.declare_partials('eta_a', ['CS', 'eta_p']) 41 | 42 | def apply_nonlinear(self, inputs, outputs, residuals): 43 | """ Calulate residuals for each balance """ 44 | TGL = self.options['tech_level'] 45 | Type = self.options['eng_type'] 46 | CS = inputs['CS'] 47 | 48 | if Type == 'small': 49 | if TGL == 1: 50 | EtaPoly_Calc = EtaPoly_SE1_interp(CS) 51 | elif TGL == 2: 52 | EtaPoly_Calc = EtaPoly_SE2_interp(CS) 53 | else: 54 | EtaPoly_Calc = EtaPoly_SE0_interp(CS) 55 | else: 56 | if CS < 5.30218862: 57 | EtaPoly_Calc = -9.025e-4*(CS**4.) + 0.01816*(CS**3.) - 0.1363*(CS**2.) + 0.4549*(CS) + 0.33620 58 | else: 59 | EtaPoly_Calc = 0.91 60 | if TGL == 1: 61 | EtaPoly_Calc += 0.02 62 | elif TGL == 2: 63 | EtaPoly_Calc += 0.04 64 | EtaPoly = inputs['eta_p'] 65 | CS = inputs['CS'] 66 | 67 | residuals['eta_a'] = EtaPoly - EtaPoly_Calc 68 | 69 | def linearize(self, inputs, outputs, J): 70 | TGL = self.options['tech_level'] 71 | CS = inputs['CS'] 72 | Type = self.options['eng_type'] 73 | 74 | if Type == 'small': 75 | if TGL == 1: 76 | partl = EtaPoly_SE1_interp_deriv(CS).reshape(1,)[0] 77 | elif TGL == 2: 78 | partl = EtaPoly_SE2_interp_deriv(CS).reshape(1,)[0] 79 | else: 80 | partl = EtaPoly_SE0_interp_deriv(CS).reshape(1,)[0] 81 | else: 82 | if CS < 5.30218862: 83 | partl = -0.00361*(CS**3.) + 0.05448*(CS**2.) - 0.2726*CS + 0.4549 84 | else: 85 | partl = 0.0 86 | 87 | J['eta_a','CS'] = -partl 88 | J['eta_a','eta_p'] = 1 -------------------------------------------------------------------------------- /example_cycles/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/OpenMDAO/pyCycle/8a031800fb90dea96ea174aedb112e7d5e120b39/example_cycles/__init__.py -------------------------------------------------------------------------------- /example_cycles/tests/benchmark_electric_propulsor.py: -------------------------------------------------------------------------------- 1 | import numpy as np 2 | import unittest 3 | import os 4 | 5 | import openmdao.api as om 6 | from openmdao.utils.assert_utils import assert_near_equal 7 | 8 | import pycycle.api as pyc 9 | 10 | from example_cycles.electric_propulsor import MPpropulsor 11 | 12 | 13 | class ElectricPropulsorTestCase(unittest.TestCase): 14 | 15 | 16 | def benchmark_case1(self): 17 | 18 | prob = om.Problem() 19 | 20 | prob.model = mp_propulsor = MPpropulsor() 21 | 22 | prob.set_solver_print(level=-1) 23 | prob.set_solver_print(level=2, depth=2) 24 | 25 | prob.setup() 26 | 27 | #Define the design point 28 | prob.set_val('design.fc.alt', 10000, units='m') 29 | prob.set_val('design.fc.MN', 0.8) 30 | prob.set_val('design.inlet.MN', 0.6) 31 | prob.set_val('design.fan.PR', 1.2) 32 | prob.set_val('pwr_target', -3486.657, units='hp') 33 | prob.set_val('design.fan.eff', 0.96) 34 | prob.set_val('off_design.fc.alt', 12000, units='m') 35 | 36 | # Set initial guesses for balances 37 | prob['design.balance.W'] = 200. 38 | 39 | # initial guesses 40 | prob['off_design.fan.PR'] = 1.2 41 | prob['off_design.balance.W'] = 406.790 42 | prob['off_design.balance.Nmech'] = 1. # normalized value 43 | 44 | 45 | prob.model.design.nonlinear_solver.options['atol'] = 1e-6 46 | prob.model.design.nonlinear_solver.options['rtol'] = 1e-6 47 | 48 | prob.model.off_design.nonlinear_solver.options['atol'] = 1e-6 49 | prob.model.off_design.nonlinear_solver.options['rtol'] = 1e-6 50 | prob.model.off_design.nonlinear_solver.options['maxiter'] = 10 51 | 52 | self.prob = prob 53 | 54 | 55 | prob.run_model() 56 | 57 | tol = 3e-5 58 | assert_near_equal(prob['design.fc.Fl_O:stat:W'], 409.636, tol) 59 | assert_near_equal(prob['design.nozz.Fg'], 12139.282, tol) 60 | assert_near_equal(prob['design.fan.SMN'], 36.64057531, tol) 61 | assert_near_equal(prob['design.fan.SMW'], 29.886, tol) 62 | 63 | 64 | assert_near_equal(prob['off_design.fc.Fl_O:stat:W'], 317.36096893 , tol) 65 | assert_near_equal(prob['off_design.nozz.Fg'], 9696.45125337, tol) 66 | assert_near_equal(prob['off_design.fan.SMN'], 22.98592129, tol) 67 | assert_near_equal(prob['off_design.fan.SMW'], 19.53411898, 5e-4) # this one is a little noisy 68 | 69 | if __name__ == "__main__": 70 | unittest.main() -------------------------------------------------------------------------------- /example_cycles/tests/benchmark_mixedflow_turbofan.py: -------------------------------------------------------------------------------- 1 | import numpy as np 2 | import unittest 3 | import os 4 | 5 | from openmdao.api import Problem, Group 6 | import pycycle.api as pyc 7 | from openmdao.utils.assert_utils import assert_near_equal 8 | 9 | from example_cycles.mixedflow_turbofan import MPMixedFlowTurbofan 10 | 11 | class MixedFlowTurbofanTestCase(unittest.TestCase): 12 | 13 | def setUp(self): 14 | 15 | self.prob = prob = Problem() 16 | 17 | self.prob.model = mp_mixedflow = MPMixedFlowTurbofan() 18 | 19 | prob.setup(check=False) 20 | 21 | #design variables 22 | self.prob.set_val('DESIGN.fc.alt', 35000., units='ft') #DV 23 | self.prob.set_val('DESIGN.fc.MN', 0.8) #DV 24 | self.prob.set_val('DESIGN.balance.rhs:W', 5500.0, units='lbf') 25 | self.prob.set_val('DESIGN.balance.rhs:FAR_core', 3200, units='degR') 26 | self.prob.set_val('OD.balance.rhs:FAR_core', 3200, units='degR') 27 | 28 | #Values that should be removed when set_input_defaults is fixed 29 | self.prob.set_val('DESIGN.fan.PR', 3.3) #ADV 30 | self.prob.set_val('DESIGN.lpc.PR', 1.935) 31 | self.prob.set_val('DESIGN.hpc.PR', 4.9) 32 | self.prob.set_val('DESIGN.fan.eff', 0.8948) 33 | self.prob.set_val('DESIGN.lpc.eff', 0.9243) 34 | self.prob.set_val('DESIGN.hpc.eff', 0.8707) 35 | self.prob.set_val('DESIGN.hpt.eff', 0.8888) 36 | self.prob.set_val('DESIGN.lpt.eff', 0.8996) 37 | 38 | self.prob.set_solver_print(level=-1) 39 | self.prob.set_solver_print(level=2, depth=1) 40 | 41 | 42 | def benchmark_case1(self): 43 | ''' Runs the design point and an off design point to make sure they match perfectly ''' 44 | prob = self.prob 45 | 46 | # initial guesses 47 | self.prob['DESIGN.balance.FAR_core'] = 0.025 48 | self.prob['DESIGN.balance.FAR_ab'] = 0.025 49 | self.prob['DESIGN.balance.BPR'] = 1.0 50 | self.prob['DESIGN.balance.W'] = 100. 51 | self.prob['DESIGN.balance.lpt_PR'] = 3.5 52 | self.prob['DESIGN.balance.hpt_PR'] = 2.5 53 | self.prob['DESIGN.fc.balance.Pt'] = 5.2 54 | self.prob['DESIGN.fc.balance.Tt'] = 440.0 55 | self.prob['DESIGN.mixer.balance.P_tot']=100 56 | 57 | self.prob['OD.balance.FAR_core'] = 0.031 58 | self.prob['OD.balance.FAR_ab'] = 0.038 59 | self.prob['OD.balance.BPR'] = 2.2 60 | self.prob['OD.balance.W'] = 60 61 | 62 | # really sensitive to these initial guesses 63 | self.prob['OD.balance.HP_Nmech'] = 15000 64 | self.prob['OD.balance.LP_Nmech'] = 5000 65 | 66 | self.prob['OD.fc.balance.Pt'] = 5.2 67 | self.prob['OD.fc.balance.Tt'] = 440.0 68 | self.prob['OD.mixer.balance.P_tot']= 100 69 | self.prob['OD.hpt.PR'] = 2.5 70 | self.prob['OD.lpt.PR'] = 3.5 71 | self.prob['OD.fan.map.RlineMap'] = 2.0 72 | self.prob['OD.lpc.map.RlineMap'] = 2.0 73 | self.prob['OD.hpc.map.RlineMap'] = 2.0 74 | 75 | self.prob.run_model() 76 | 77 | tol = 1e-5 78 | 79 | reg_data = 53.833997155 80 | pyc = self.prob['DESIGN.inlet.Fl_O:stat:W'][0] 81 | print('W:', reg_data, pyc) 82 | assert_near_equal(pyc, reg_data, tol) 83 | pyc = self.prob['OD.inlet.Fl_O:stat:W'][0] 84 | assert_near_equal(pyc, reg_data, tol) 85 | 86 | reg_data = 0.0311248 87 | pyc = self.prob['DESIGN.balance.FAR_core'][0] 88 | print('Main FAR:', reg_data, pyc) 89 | assert_near_equal(pyc, reg_data, tol) 90 | pyc = self.prob['OD.balance.FAR_core'][0] 91 | assert_near_equal(pyc, reg_data, tol) 92 | 93 | reg_data = 0.0387335612 94 | pyc = self.prob['DESIGN.balance.FAR_ab'][0] 95 | print('Main FAR:', reg_data, pyc) 96 | assert_near_equal(pyc, reg_data, tol) 97 | pyc = self.prob['OD.balance.FAR_ab'][0] 98 | assert_near_equal(pyc, reg_data, tol) 99 | 100 | reg_data = 2.0430265465465354 101 | pyc = self.prob['DESIGN.balance.hpt_PR'][0] 102 | print('HPT PR:', reg_data, pyc) 103 | assert_near_equal(pyc, reg_data, tol) 104 | pyc = self.prob['OD.hpt.PR'][0] 105 | assert_near_equal(pyc, reg_data, tol) 106 | 107 | reg_data = 4.098132533864145 108 | pyc = self.prob['DESIGN.balance.lpt_PR'][0] 109 | print('HPT PR:', reg_data, pyc) 110 | assert_near_equal(pyc, reg_data, tol) 111 | pyc = self.prob['OD.lpt.PR'][0] 112 | assert_near_equal(pyc, reg_data, tol) 113 | 114 | reg_data = 6802.79655491 115 | pyc = self.prob['DESIGN.mixed_nozz.Fg'][0] 116 | print('Fg:', reg_data, pyc) 117 | assert_near_equal(pyc, reg_data, tol) 118 | pyc = self.prob['OD.mixed_nozz.Fg'][0] 119 | assert_near_equal(pyc, reg_data, tol) 120 | 121 | reg_data = 1287.084732 122 | pyc = self.prob['DESIGN.hpc.Fl_O:tot:T'][0] 123 | print('Tt3:', reg_data, pyc) 124 | assert_near_equal(pyc, reg_data, tol) 125 | pyc = self.prob['OD.hpc.Fl_O:tot:T'][0] 126 | assert_near_equal(pyc, reg_data, tol) 127 | 128 | if __name__ == "__main__": 129 | unittest.main() 130 | 131 | -------------------------------------------------------------------------------- /example_cycles/tests/benchmark_simple_turbojet.py: -------------------------------------------------------------------------------- 1 | import numpy as np 2 | import unittest 3 | import os 4 | 5 | import openmdao.api as om 6 | from openmdao.utils.assert_utils import assert_near_equal 7 | 8 | import pycycle.api as pyc 9 | 10 | from example_cycles.simple_turbojet import MPTurbojet 11 | 12 | class SimpleTurbojetTestCase(unittest.TestCase): 13 | 14 | def benchmark_case1(self): 15 | 16 | prob = om.Problem() 17 | 18 | prob.model = mp_turbojet = MPTurbojet() 19 | 20 | prob.set_solver_print(level=-1) 21 | prob.set_solver_print(level=2, depth=1) 22 | 23 | prob.setup(check=False) 24 | 25 | ##Initial Conditions 26 | prob.set_val('DESIGN.fc.alt', 0, units='ft') 27 | prob.set_val('DESIGN.fc.MN', 0.000001) 28 | prob.set_val('DESIGN.balance.Fn_target', 11800.0, units='lbf') 29 | prob.set_val('DESIGN.balance.T4_target', 2370.0, units='degR') 30 | 31 | ##Values that will go away when set_input_defaults is fixed 32 | prob.set_val('DESIGN.comp.PR', 13.5) 33 | prob.set_val('DESIGN.comp.eff', 0.83) 34 | prob.set_val('DESIGN.turb.eff', 0.86) 35 | 36 | # Set initial guesses for balances 37 | prob['DESIGN.balance.FAR'] = 0.0175506829934 38 | prob['DESIGN.balance.W'] = 168.453135137 39 | prob['DESIGN.balance.turb_PR'] = 4.46138725662 40 | prob['DESIGN.fc.balance.Pt'] = 14.6955113159 41 | prob['DESIGN.fc.balance.Tt'] = 518.665288153 42 | 43 | for i,pt in enumerate(mp_turbojet.od_pts): 44 | 45 | # initial guesses 46 | prob[pt+'.balance.W'] = 166.073 47 | prob[pt+'.balance.FAR'] = 0.01680 48 | prob[pt+'.balance.Nmech'] = 8197.38 49 | prob[pt+'.fc.balance.Pt'] = 15.703 50 | prob[pt+'.fc.balance.Tt'] = 558.31 51 | prob[pt+'.turb.PR'] = 4.6690 52 | 53 | old = np.seterr(divide='raise') 54 | 55 | try: 56 | prob.run_model() 57 | tol = 1e-5 58 | print() 59 | 60 | reg_data = 147.333 61 | ans = prob['DESIGN.inlet.Fl_O:stat:W'][0] 62 | print('W:', reg_data, ans) 63 | assert_near_equal(ans, reg_data, tol) 64 | 65 | reg_data = 13.500 66 | ans = prob['DESIGN.perf.OPR'][0] 67 | print('OPR:', reg_data, ans) 68 | assert_near_equal(ans, reg_data, tol) 69 | 70 | reg_data = 0.01776487 71 | ans = prob['DESIGN.balance.FAR'][0] 72 | print('Main FAR:', reg_data, ans) 73 | assert_near_equal(ans, reg_data, tol) 74 | 75 | reg_data = 3.8591364 76 | ans = prob['DESIGN.balance.turb_PR'][0] 77 | print('HPT PR:', reg_data, ans) 78 | assert_near_equal(ans, reg_data, tol) 79 | 80 | reg_data = 11800.00455497 81 | ans = prob['DESIGN.perf.Fg'][0] 82 | print('Fg:', reg_data, ans) 83 | assert_near_equal(ans, reg_data, tol) 84 | 85 | reg_data = 0.7985165 86 | ans = prob['DESIGN.perf.TSFC'][0] 87 | print('TSFC:', reg_data, ans) 88 | assert_near_equal(ans, reg_data, tol) 89 | 90 | reg_data = 1187.7610184 91 | ans = prob['DESIGN.comp.Fl_O:tot:T'][0] 92 | print('Tt3:', reg_data, ans) 93 | assert_near_equal(ans, reg_data, tol) 94 | 95 | reg_data = 142.786698 96 | ans = prob['OD0.inlet.Fl_O:stat:W'][0] 97 | print('W:', reg_data, ans) 98 | assert_near_equal(ans, reg_data, tol) 99 | 100 | reg_data = 12.8588424 101 | ans = prob['OD0.perf.OPR'][0] 102 | print('OPR:', reg_data, ans) 103 | assert_near_equal(ans, reg_data, tol) 104 | 105 | reg_data = 0.01676938946 106 | ans = prob['OD0.balance.FAR'][0] 107 | print('Main FAR:', reg_data, ans) 108 | assert_near_equal(ans, reg_data, tol) 109 | 110 | reg_data = 7943.9331210 111 | ans = prob['OD0.balance.Nmech'][0] 112 | print('HP Nmech:', reg_data, ans) 113 | assert_near_equal(ans, reg_data, tol) 114 | 115 | reg_data = 11000.00488519 116 | ans = prob['OD0.perf.Fg'][0] 117 | print('Fg:', reg_data, ans) 118 | assert_near_equal(ans, reg_data, tol) 119 | 120 | reg_data = 0.783636794 121 | ans = prob['OD0.perf.TSFC'][0] 122 | print('TSFC:', reg_data, ans) 123 | assert_near_equal(ans, reg_data, tol) 124 | 125 | reg_data = 1168.067147267 126 | ans = prob['OD0.comp.Fl_O:tot:T'][0] 127 | print('Tt3:', reg_data, ans) 128 | assert_near_equal(ans, reg_data, tol) 129 | 130 | print() 131 | finally: 132 | np.seterr(**old) 133 | 134 | if __name__ == "__main__": 135 | unittest.main() 136 | 137 | -------------------------------------------------------------------------------- /example_cycles/tests/benchmark_single_spool_turboshaft.py: -------------------------------------------------------------------------------- 1 | import numpy as np 2 | import unittest 3 | import os 4 | 5 | import openmdao.api as om 6 | from openmdao.utils.assert_utils import assert_near_equal 7 | 8 | import pycycle.api as pyc 9 | 10 | from example_cycles.single_spool_turboshaft import MPSingleSpool 11 | 12 | class SingleSpoolTestCase(unittest.TestCase): 13 | 14 | def benchmark_case1(self): 15 | 16 | prob = om.Problem() 17 | 18 | prob.model = mp_single_spool = MPSingleSpool() 19 | 20 | prob.setup(check=False) 21 | 22 | #Define the initial design point 23 | prob.set_val('DESIGN.fc.alt', 0.0, units='ft') 24 | prob.set_val('DESIGN.fc.MN', 0.000001) 25 | prob.set_val('DESIGN.balance.T4_target', 2370.0, units='degR') 26 | prob.set_val('DESIGN.balance.pwr_target', 4000.0, units='hp') 27 | prob.set_val('DESIGN.balance.nozz_PR_target', 1.2) 28 | prob.set_val('DESIGN.comp.PR', 13.5) 29 | prob.set_val('DESIGN.comp.eff', 0.83) 30 | prob.set_val('DESIGN.turb.eff', 0.86) 31 | prob.set_val('DESIGN.pt.eff', 0.9) 32 | 33 | # Set initial guesses for balances 34 | prob['DESIGN.balance.FAR'] = 0.0175506829934 35 | prob['DESIGN.balance.W'] = 27.265 36 | prob['DESIGN.balance.turb_PR'] = 3.8768 37 | prob['DESIGN.balance.pt_PR'] = 2.8148 38 | prob['DESIGN.fc.balance.Pt'] = 14.6955113159 39 | prob['DESIGN.fc.balance.Tt'] = 518.665288153 40 | 41 | for i,pt in enumerate(mp_single_spool.od_pts): 42 | 43 | # initial guesses 44 | prob[pt+'.balance.W'] = 27.265 45 | prob[pt+'.balance.FAR'] = 0.0175506829934 46 | prob[pt+'.balance.HP_Nmech'] = 8070.0 47 | prob[pt+'.fc.balance.Pt'] = 15.703 48 | prob[pt+'.fc.balance.Tt'] = 558.31 49 | prob[pt+'.turb.PR'] = 3.8768 50 | prob[pt+'.pt.PR'] = 2.8148 51 | 52 | 53 | prob.set_solver_print(level=-1) 54 | prob.set_solver_print(level=2, depth=1) 55 | 56 | old = np.seterr(divide='raise') 57 | 58 | try: 59 | prob.run_model() 60 | tol = 1e-5 61 | print() 62 | 63 | reg_data = 27.265344349 64 | ans = prob['DESIGN.inlet.Fl_O:stat:W'][0] 65 | print('W:', ans, reg_data) 66 | assert_near_equal(ans, reg_data, tol) 67 | 68 | reg_data = 13.5 69 | ans = prob['DESIGN.perf.OPR'][0] 70 | print('OPR:', ans, reg_data) 71 | assert_near_equal(ans, reg_data, tol) 72 | 73 | reg_data = 0.01755865988 74 | ans = prob['DESIGN.balance.FAR'][0] 75 | print('Main FAR:', ans, reg_data) 76 | assert_near_equal(ans, reg_data, tol) 77 | 78 | reg_data = 3.876811443516 79 | ans = prob['DESIGN.balance.turb_PR'][0] 80 | print('HPT PR:', ans, reg_data) 81 | assert_near_equal(ans, reg_data, tol) 82 | 83 | reg_data = 800.85349568 84 | ans = prob['DESIGN.perf.Fg'][0] 85 | print('Fg:', ans, reg_data) 86 | assert_near_equal(ans, reg_data, tol) 87 | 88 | reg_data = 2.15204967 89 | ans = prob['DESIGN.perf.TSFC'][0] 90 | print('TSFC:', ans, reg_data) 91 | assert_near_equal(ans, reg_data, tol) 92 | 93 | reg_data = 1190.1777648 94 | ans = prob['DESIGN.comp.Fl_O:tot:T'][0] 95 | print('Tt3:', ans, reg_data) 96 | assert_near_equal(ans, reg_data, tol) 97 | 98 | print('#'*10) 99 | print('# OD') 100 | print('#'*10) 101 | reg_data = 25.8972423 102 | ans = prob['OD.inlet.Fl_O:stat:W'][0] 103 | print('W:', ans, reg_data) 104 | assert_near_equal(ans, reg_data, tol) 105 | 106 | reg_data = 12.4297249 107 | ans = prob['OD.perf.OPR'][0] 108 | print('OPR:', ans, reg_data) 109 | assert_near_equal(ans, reg_data, tol) 110 | 111 | reg_data = 0.0163117040 112 | ans = prob['OD.balance.FAR'][0] 113 | print('Main FAR:', ans, reg_data) 114 | assert_near_equal(ans, reg_data, tol) 115 | 116 | reg_data = 7853.754354 117 | ans = prob['OD.balance.HP_Nmech'][0] 118 | print('HP Nmech:', ans, reg_data) 119 | assert_near_equal(ans, reg_data, tol) 120 | 121 | reg_data = 696.62110739 122 | ans = prob['OD.perf.Fg'][0] 123 | print('Fg:', ans, reg_data) 124 | assert_near_equal(ans, reg_data, tol) 125 | 126 | reg_data = 2.5063687 127 | ans = prob['OD.perf.TSFC'][0] 128 | print('TSFC:', ans, reg_data) 129 | assert_near_equal(ans, reg_data, tol) 130 | 131 | reg_data = 1158.519646 132 | ans = prob['OD.comp.Fl_O:tot:T'][0] 133 | print('Tt3:', ans, reg_data) 134 | assert_near_equal(ans, reg_data, tol) 135 | finally: 136 | np.seterr(**old) 137 | 138 | 139 | if __name__ == "__main__": 140 | unittest.main() -------------------------------------------------------------------------------- /example_cycles/tests/benchmark_wet_propulsor.py: -------------------------------------------------------------------------------- 1 | import numpy as np 2 | import unittest 3 | import os 4 | 5 | import openmdao.api as om 6 | from openmdao.utils.assert_utils import assert_near_equal 7 | 8 | import pycycle.api as pyc 9 | 10 | from example_cycles.wet_propulsor import MPWetPropulsor 11 | 12 | class WetPropulsorTestCase(unittest.TestCase): 13 | 14 | 15 | def benchmark_case1(self): 16 | 17 | prob = om.Problem() 18 | 19 | prob.model = mp_wet_propulsor = MPWetPropulsor() 20 | 21 | prob.setup() 22 | 23 | #Define the design point 24 | prob.set_val('design.fan.PR', 1.2) 25 | prob.set_val('design.fan.eff', 0.96) 26 | 27 | # Set initial guesses for balances 28 | prob['design.fc.MN'] = .8 29 | prob['design.balance.W'] = 200. 30 | 31 | prob['off_design.fc.MN'] = .8 32 | prob['off_design.balance.W'] = 406.790 33 | prob['off_design.balance.Nmech'] = 1. 34 | prob['off_design.fan.PR'] = 1.2 35 | prob['off_design.fan.map.RlineMap'] = 2.2 36 | 37 | prob.set_solver_print(level=-1) 38 | prob.set_solver_print(level=2, depth=2) 39 | 40 | prob.model.design.nonlinear_solver.options['atol'] = 1e-6 41 | prob.model.design.nonlinear_solver.options['rtol'] = 1e-6 42 | 43 | prob.model.off_design.nonlinear_solver.options['atol'] = 1e-6 44 | prob.model.off_design.nonlinear_solver.options['rtol'] = 1e-6 45 | prob.model.off_design.nonlinear_solver.options['maxiter'] = 10 46 | 47 | prob.run_model() 48 | 49 | tol = 1e-5 50 | assert_near_equal(prob['design.fc.Fl_O:stat:W'], 406.5629775, tol) 51 | assert_near_equal(prob['design.nozz.Fg'], 12066.680, tol) 52 | assert_near_equal(prob['design.fan.SMN'], 36.6405753, tol) 53 | assert_near_equal(prob['design.fan.SMW'], 29.886, tol) 54 | 55 | assert_near_equal(prob['off_design.fc.Fl_O:stat:W'], 406.5629775, tol) 56 | assert_near_equal(prob['off_design.nozz.Fg'], 12066.680, tol) 57 | assert_near_equal(prob['off_design.fan.SMN'], 36.6405753, tol) 58 | assert_near_equal(prob['off_design.fan.SMW'], 29.886, tol) 59 | 60 | if __name__ == "__main__": 61 | unittest.main() -------------------------------------------------------------------------------- /example_cycles/tests/benchmark_wet_simple_turbojet.py: -------------------------------------------------------------------------------- 1 | import numpy as np 2 | import unittest 3 | import os 4 | 5 | import openmdao.api as om 6 | from openmdao.utils.assert_utils import assert_near_equal 7 | 8 | import pycycle.api as pyc 9 | 10 | from example_cycles.wet_simple_turbojet import MPWetTurbojet 11 | 12 | class WetSimpleTurbojetTestCase(unittest.TestCase): 13 | 14 | 15 | def benchmark_case1(self): 16 | 17 | prob = om.Problem() 18 | 19 | prob.model = mp_wet_turbojet = MPWetTurbojet() 20 | 21 | 22 | prob.setup() 23 | 24 | #Define the design point 25 | prob.set_val('DESIGN.comp.PR', 13.5), 26 | prob.set_val('DESIGN.comp.eff', 0.83), 27 | prob.set_val('DESIGN.turb.eff', 0.86), 28 | 29 | # Set initial guesses for balances 30 | prob['DESIGN.balance.FAR'] = 0.0175506829934 31 | prob['DESIGN.balance.W'] = 168.453135137 32 | prob['DESIGN.balance.turb_PR'] = 4.46138725662 33 | prob['DESIGN.fc.balance.Pt'] = 14.6955113159 34 | prob['DESIGN.fc.balance.Tt'] = 518.665288153 35 | 36 | prob['OD1.balance.W'] = 166.073 37 | prob['OD1.balance.FAR'] = 0.01680 38 | prob['OD1.balance.Nmech'] = 8197.38 39 | prob['OD1.fc.balance.Pt'] = 15.703 40 | prob['OD1.fc.balance.Tt'] = 558.31 41 | prob['OD1.turb.PR'] = 4.6690 42 | 43 | prob.set_solver_print(level=-1) 44 | prob.set_solver_print(level=2, depth=1) 45 | prob.run_model() 46 | 47 | tol = 1e-5 48 | print() 49 | assert_near_equal(prob['DESIGN.inlet.Fl_O:stat:W'][0], 147.47225811, tol) 50 | assert_near_equal(prob['DESIGN.perf.OPR'][0], 13.5, tol) 51 | assert_near_equal(prob['DESIGN.balance.FAR'][0], 0.01757989383, tol) 52 | assert_near_equal(prob['DESIGN.balance.turb_PR'][0], 3.87564568, tol) 53 | assert_near_equal(prob['DESIGN.perf.Fg'][0], 11800.004971016797, tol) 54 | assert_near_equal(prob['DESIGN.perf.TSFC'][0], 0.79094647, tol) 55 | assert_near_equal(prob['DESIGN.comp.Fl_O:tot:T'][0], 1189.923682, tol) 56 | assert_near_equal(prob['OD1.inlet.Fl_O:stat:W'][0], 142.615366, tol) 57 | assert_near_equal(prob['OD1.perf.OPR'][0], 12.840813, tol) 58 | assert_near_equal(prob['OD1.balance.FAR'][0], 0.016678628, tol) 59 | assert_near_equal(prob['OD1.balance.Nmech'][0], 7936.357395342184, tol) 60 | assert_near_equal(prob['OD1.perf.Fg'][0], 11000.004883208685, tol) 61 | assert_near_equal(prob['OD1.perf.TSFC'][0], 0.778460327, tol) 62 | assert_near_equal(prob['OD1.comp.Fl_O:tot:T'][0], 1169.2669863777, tol) 63 | 64 | print() 65 | 66 | if __name__ == "__main__": 67 | unittest.main() -------------------------------------------------------------------------------- /pycycle/__init__.py: -------------------------------------------------------------------------------- 1 | __version__ = '4.3.1-dev' -------------------------------------------------------------------------------- /pycycle/api.py: -------------------------------------------------------------------------------- 1 | from pycycle.constants import (AIR_FUEL_MIX, AIR_MIX, WET_AIR_MIX, BTU_s2HP, HP_per_RPM_to_FT_LBF, 2 | R_UNIVERSAL_SI, R_UNIVERSAL_ENG, g_c, MIN_VALID_CONCENTRATION, 3 | T_STDeng, P_STDeng, P_REF, CEA_AIR_COMPOSITION, CEA_AIR_FUEL_COMPOSITION, 4 | CEA_WET_AIR_COMPOSITION, AIR_JETA_TAB_SPEC, TAB_AIR_FUEL_COMPOSITION) 5 | 6 | from pycycle.thermo.cea import species_data 7 | 8 | from pycycle.elements.flow_start import FlowStart 9 | from pycycle.elements.cfd_start import CFDStart 10 | from pycycle.elements.inlet import Inlet, MilSpecRecovery 11 | from pycycle.elements.duct import Duct 12 | from pycycle.elements.compressor import Compressor 13 | from pycycle.elements.combustor import Combustor 14 | from pycycle.elements.turbine import Turbine 15 | from pycycle.elements.nozzle import Nozzle 16 | from pycycle.elements.shaft import Shaft 17 | from pycycle.elements.performance import Performance 18 | from pycycle.elements.flight_conditions import FlightConditions 19 | from pycycle.elements.splitter import Splitter 20 | from pycycle.elements.mixer import Mixer 21 | from pycycle.elements.bleed_out import BleedOut 22 | from pycycle.elements.cooling import TurbineCooling, CombineCooling 23 | from pycycle.elements.gearbox import Gearbox 24 | 25 | 26 | from pycycle.maps.axi5 import AXI5 27 | from pycycle.maps.axi3_2 import AXI3_2 28 | from pycycle.maps.lpt2269 import LPT2269 29 | from pycycle.maps.hpt1269 import HPT1269 30 | from pycycle.maps.Fan_map import FanMap 31 | from pycycle.maps.HPC_map import HPCMap 32 | from pycycle.maps.LPC_map import LPCMap 33 | from pycycle.maps.HPT_map import HPTMap 34 | from pycycle.maps.LPT_map import LPTMap 35 | from pycycle.maps.ncp01 import NCP01 36 | 37 | from pycycle.connect_flow import connect_flow 38 | 39 | from pycycle.viewers import print_bleed, print_burner, print_compressor, print_flow_station, \ 40 | print_mixer, print_nozzle, print_shaft, print_turbine, \ 41 | plot_compressor_maps, plot_turbine_maps 42 | 43 | 44 | from pycycle.mp_cycle import MPCycle, Cycle -------------------------------------------------------------------------------- /pycycle/connect_flow.py: -------------------------------------------------------------------------------- 1 | import warnings 2 | 3 | 4 | def connect_flow(group, fl_src, fl_target, connect_stat=True, connect_tot=True, connect_w=True): 5 | """ connect flow variables """ 6 | 7 | warnings.simplefilter('always', DeprecationWarning) 8 | warnings.warn(f"Deprecation warning: `connect_flow` function is deprecated. Use the `pyc_connect_flow` method from `Cycle` class instead." ) 9 | warnings.simplefilter('ignore', DeprecationWarning) 10 | 11 | group.pyc_connect_flow(fl_src, fl_target, connect_stat, connect_tot, connect_w) -------------------------------------------------------------------------------- /pycycle/constants.py: -------------------------------------------------------------------------------- 1 | import warnings 2 | import os 3 | import os.path 4 | import pickle 5 | 6 | class DeprecatedDict(dict): 7 | 8 | def __init__(self, old_name, new_name, *args, **kwargs): 9 | self.old_name = old_name 10 | self.new_name = new_name 11 | self._has_warned = False 12 | super().__init__(*args, **kwargs) 13 | 14 | def __getitem__(self, key): 15 | if not self._has_warned: 16 | self._has_warned = True 17 | warnings.simplefilter('always', DeprecationWarning) 18 | warnings.warn(f"Deprecation warning: `{self.old_name}` will be replaced by `{self.new_name}` in pyCycle 4.0", DeprecationWarning) 19 | warnings.simplefilter('ignore', DeprecationWarning) 20 | return super().__getitem__(key) 21 | 22 | # these elemental ratios matter! 23 | CEA_AIR_FUEL_COMPOSITION = {'N': 5.39157698e-02, 'O':1.44860137e-02, 'Ar': 3.23319235e-04, 'C': 1.10132233e-05, 'H':1e-8} 24 | CEA_AIR_COMPOSITION = {'N': 5.39157698e-02, 'O':1.44860137e-02, 'Ar': 3.23319235e-04, 'C': 1.10132233e-05} 25 | CEA_WET_AIR_COMPOSITION = {'Ar':3.21320739e-04, 'C':1.09451485e-05, 'H':6.86216207e-04, 'N':5.35825063e-02, 'O':1.47395810e-02} 26 | CEA_CO2_CO_O2_COMPOSITION = {'C':0.02272237, 'O':0.04544473} 27 | 28 | TAB_AIR_FUEL_COMPOSITION = {'FAR': 0.0} 29 | # A little fancy code to find the default thermo data in the python package, wherever its installed 30 | pkg_path = os.path.dirname(os.path.realpath(__file__)) 31 | tab_spec_path = os.path.join(pkg_path, 'thermo', 'tabular', 'air_jetA.pkl') 32 | with open(tab_spec_path, 'rb') as spec_data: 33 | AIR_JETA_TAB_SPEC = pickle.load(spec_data) 34 | 35 | 36 | THERMO_DEFAULT_COMPOSITIONS = { 37 | 'CEA': CEA_AIR_COMPOSITION, 38 | 'TABULAR': TAB_AIR_FUEL_COMPOSITION 39 | } 40 | 41 | 42 | # these elemental ratios matter! 43 | AIR_FUEL_ELEMENTS = DeprecatedDict('AIR_FUEL_ELEMENTS', 'CEA_AIR_FUEL_COMPOSITION', CEA_AIR_FUEL_COMPOSITION) 44 | AIR_ELEMENTS = DeprecatedDict('AIR_ELEMENTS', 'CEA_AIR_COMPOSITION', CEA_AIR_COMPOSITION) 45 | WET_AIR_ELEMENTS = DeprecatedDict('WET_AIR_ELEMENTS', 'CEA_WET_AIR_COMPOSITION', CEA_WET_AIR_COMPOSITION) 46 | CO2_CO_O2_ELEMENTS = DeprecatedDict('CO2_CO_O2_ELEMENTS', 'CEA_CO2_CO_O2_COMPOSITION', CEA_CO2_CO_O2_COMPOSITION) 47 | 48 | 49 | AIR_FUEL_MIX = DeprecatedDict('AIR_FUEL_MIX', 'CEA_AIR_FUEL_COMPOSITION', CEA_AIR_FUEL_COMPOSITION) 50 | AIR_MIX = DeprecatedDict('AIR_MIX', 'CEA_AIR_COMPOSITION', CEA_AIR_COMPOSITION) 51 | WET_AIR_MIX = DeprecatedDict('WET_AIR_MIX', 'CEA_WET_AIR_COMPOSITION', CEA_WET_AIR_COMPOSITION) 52 | CO2_CO_O2_MIX = DeprecatedDict('CO2_CO_O2_MIX', 'CEA_CO2_CO_O2_COMPOSITION', CEA_CO2_CO_O2_COMPOSITION) 53 | 54 | 55 | BTU_s2HP = 1.4148532 56 | HP_per_RPM_to_FT_LBF = 5252.11 57 | 58 | R_UNIVERSAL_SI = 8314.4598 # (m**3 * Pa)/(mol*degK) 59 | R_UNIVERSAL_ENG = 1.9872035 # (Btu lbm)/(mol*degR) 60 | 61 | g_c = 32.174 62 | 63 | MIN_VALID_CONCENTRATION = 1e-10 64 | 65 | T_STDeng = 518.67 #degR 66 | P_STDeng = 14.695951 #psi 67 | 68 | P_REF = 1.01325 # 1 atm 69 | # P_REF = 1.0162 # Not sure why, but this seems to match the SP set to the TP better 70 | 71 | 72 | ALLOWED_THERMOS = ('CEA', 'TABULAR') -------------------------------------------------------------------------------- /pycycle/docs/Makefile: -------------------------------------------------------------------------------- 1 | # Makefile for OpenMDAO Sphinx documentation 2 | 3 | ################################### 4 | # SETUP 5 | 6 | # You can set these variables from the command line. 7 | SPHINXOPTS = 8 | SPHINXBUILD = sphinx-build 9 | BUILDDIR = _build 10 | 11 | # User-friendly check for sphinx-build 12 | ifeq ($(shell which $(SPHINXBUILD) >/dev/null 2>&1; echo $$?), 1) 13 | $(error The '$(SPHINXBUILD)' command was not found. Make sure you have Sphinx installed, then set the SPHINXBUILD environment variable to point to the full path of the '$(SPHINXBUILD)' executable. Alternatively you can add the directory with the executable to your PATH. If you don't have Sphinx installed, grab it from http://sphinx-doc.org/) 14 | endif 15 | 16 | SPHINX_VERSION := $(shell $(SPHINXBUILD) --version | grep -o -E "[0-9\.]+") 17 | ifeq ($(shell expr $(SPHINX_VERSION) \< 1.5), 1) 18 | $(error Version 1.5 or later of Sphinx is required, but $(SPHINX_VERSION) was found. Upgrade via 'pip install --upgrade sphinx' or get the latest from http://sphinx-doc.org/) 19 | endif 20 | 21 | # Internal variables. 22 | ALLSPHINXOPTS = -d $(BUILDDIR)/doctrees $(SPHINXOPTS) . 23 | 24 | .PHONY: help clean html html-update single 25 | 26 | ################################### 27 | # COMMANDS 28 | 29 | # to remake only recently-changed files, not the entire document base. 30 | # note - first item in makefile is default action of "make" 31 | html-update: mock redbaron matplotlib build 32 | 33 | # to force the rebuild a file when its dependecy (e.g. code-embed) changes, not its rst file 34 | single: touch buildone 35 | 36 | # build it all over again (note: make all == make html) 37 | all html: make_srcdocs mock redbaron matplotlib buildall post_remove 38 | 39 | # build it all on CI machines; all warnings are raised as errors. 40 | travis: make_srcdocs mock redbaron matplotlib buildalltravis post_remove 41 | 42 | clean: 43 | rm -rf $(BUILDDIR)/* 44 | rm -rf _srcdocs/* 45 | rm -rf tags 46 | rm -rf tmp 47 | rm -rf make_sourcedocs 48 | rm -rf doc_plot_*.png 49 | rm -rf *.html 50 | 51 | help: 52 | @echo "Please use 'make ' where is one of" 53 | @echo " clean to remove everything that has previously been built in the document base" 54 | @echo " html, all to build the entire document base" 55 | @echo " html-update to re-make only files that have been changed since the last make" 56 | @echo " single to re-make one html file to refresh code dependency changes" 57 | 58 | 59 | ################################### 60 | # RULES (that comprise the commands) 61 | 62 | build: 63 | $(SPHINXBUILD) -b html $(ALLSPHINXOPTS) $(BUILDDIR)/html 64 | @echo "Build finished. The HTML pages are in $(BUILDDIR)/html." 65 | 66 | buildone: 67 | @echo building $$file 68 | $(SPHINXBUILD) -b html $(ALLSPHINXOPTS) $(BUILDDIR)/html $$file 69 | @echo "Build finished. The HTML pages are in $(BUILDDIR)/html." 70 | 71 | buildall: 72 | $(SPHINXBUILD) -a -b html $(ALLSPHINXOPTS) $(BUILDDIR)/html 73 | @echo "Build finished. The HTML pages are in $(BUILDDIR)/html." 74 | 75 | buildalltravis: 76 | $(SPHINXBUILD) -a -W -b html $(ALLSPHINXOPTS) $(BUILDDIR)/html 77 | @echo "Build finished. The HTML pages are in $(BUILDDIR)/html." 78 | 79 | touch: 80 | touch $$file 81 | 82 | # source doc build indicator, to trigger conf.py 83 | make_srcdocs: 84 | touch make_sourcedocs 85 | 86 | #clean up the sourcedocs indicator 87 | post_remove: 88 | rm -rf make_sourcedocs 89 | 90 | # installation testers 91 | mock: 92 | @(python -c "import mock" >/dev/null 2>&1) || (echo "The 'mock' package \ 93 | is required to build the docs. Install with:\n pip install mock"; exit 1) 94 | 95 | redbaron: 96 | @(python -c "import redbaron" >/dev/null 2>&1) || (echo "The 'redbaron' package \ 97 | is required to build the docs. Install with:\n pip install redbaron"; exit 1) 98 | 99 | matplotlib: 100 | @(python -c "import matplotlib" >/dev/null 2>&1) || (echo "The 'matplotlib' package \ 101 | is required to build the docs. Install with:\n pip install matplotlib"; exit 1) 102 | -------------------------------------------------------------------------------- /pycycle/docs/_theme/search.html: -------------------------------------------------------------------------------- 1 | {# 2 | basic/search.html 3 | ~~~~~~~~~~~~~~~~~ 4 | 5 | Template for the search page. 6 | 7 | :copyright: Copyright 2007-2019 by the Sphinx team, see AUTHORS. 8 | :license: BSD, see LICENSE for details. 9 | #} 10 | {%- extends "layout.html" %} 11 | {% set title = _('Search') %} 12 | {%- block scripts %} 13 | {{ super() }} 14 | 15 | {%- endblock %} 16 | {% block extrahead %} 17 | 18 | {{ super() }} 19 | {% endblock %} 20 | {% block body %} 21 |

{{ _('Search') }}

22 |
23 | 24 |

25 | {% trans %}Please activate JavaScript to enable the search 26 | functionality.{% endtrans %} 27 |

28 |
29 |

30 | {% trans %}From here you can search these documents. Enter your search 31 | words into the box below and click "search". Note that the search 32 | function will automatically search for all of the words. Pages 33 | containing fewer words won't appear in the result list.{% endtrans %} 34 |

35 |
36 | 37 | 38 | Include Source Docs 39 | 40 |
41 | {% if search_performed %} 42 |

{{ _('Search Results') }}

43 | {% if not search_results %} 44 |

{{ _('Your search did not match any documents. Please make sure that all words are spelled correctly and that you\'ve selected enough categories.') }}

45 | {% endif %} 46 | {% endif %} 47 |
48 | {% if search_results %} 49 |
    50 | {% for href, caption, context in search_results %} 51 |
  • {{ caption }} 52 |
    {{ context|e }}
    53 |
  • 54 | {% endfor %} 55 |
56 | {% endif %} 57 |
58 | {% endblock %} 59 | -------------------------------------------------------------------------------- /pycycle/docs/_theme/static/style.css: -------------------------------------------------------------------------------- 1 | /* 2 | * sytle.css 3 | * ~~~~~~~~~~~~~ 4 | * 5 | * OpenMDAO stylesheet 6 | * 7 | */ 8 | 9 | @import url("sphinxdoc.css"); 10 | 11 | /* link colors */ 12 | a { 13 | color: navy; 14 | text-decoration: none; 15 | } 16 | div.body h1 a, div.body h2 a, div.body h3 a, div.body h4 a, div.body h5 a, div.body h6 a { 17 | color: navy!important; 18 | } 19 | 20 | a:visited { 21 | color: navy; 22 | text-decoration: none; 23 | } 24 | 25 | a:hover { 26 | text-decoration: underline; 27 | color: #6699FF; 28 | } 29 | 30 | div.sphinxsidebar h4, div.sphinxsidebar h3 { 31 | background-color: navy; 32 | } 33 | 34 | div.sphinxsidebar input{ 35 | border: 1px solid navy; 36 | 37 | } 38 | 39 | /* hide overflowing content in the sidebar */ 40 | div.sphinxsidebarwrapper p.topless { 41 | overflow: hidden; 42 | } 43 | 44 | /*Args bkgrd in reference sheets*/ 45 | table.field-list td, table.field-list th { 46 | border: 0 !important; 47 | } 48 | 49 | /*top links "breadcrumbs"*/ 50 | div.related ul li a { 51 | margin: 0; 52 | padding: 0 5px 0 5px; 53 | line-height: 1.75em; 54 | color: navy; 55 | } 56 | 57 | /*a very thin underline of headers*/ 58 | h1, h2, h3, h4, h5, h6 { 59 | color: navy; 60 | font-weight: normal; 61 | border-bottom: 1px solid #ccc; 62 | } 63 | 64 | /*default h3 is kind of screwy with negative border. Fix it.*/ 65 | h3 { 66 | margin: 1em 0 0.3em 0; 67 | } 68 | 69 | /*in the source docs, the header color block for "Args"*/ 70 | th { 71 | /*background-color: #BFD1D4;*/ 72 | background-color: #E6E6E9; /*light gray*/ 73 | } 74 | 75 | body { 76 | font-family: 'Lucida Grande', 'Lucida Sans Unicode', 'Geneva', 'Verdana', sans-serif; 77 | font-size: 13px; 78 | letter-spacing: -0.01em; 79 | line-height: 150%; 80 | text-align: center; 81 | /*in the main page, background-color is the color of the bars down the sides*/ 82 | /*background-color: #BFD1D4; /*blue*/ */ 83 | background-color: #66C2FF; /*blue*/ 84 | 85 | 86 | color: black; 87 | padding: 0; 88 | border: 1px solid #aaa; 89 | margin: 0px 80px 0px 80px; 90 | min-width: 740px; 91 | } 92 | 93 | div.viewcode-block:target { 94 | background-color: #BFD1D4; 95 | border-top: 1px solid #ac9; 96 | border-bottom: 1px solid #ac9; 97 | } 98 | 99 | div.footer { 100 | background-color: #BFD1D4; 101 | color: navy; 102 | padding: 3px 8px 3px 0; 103 | clear: both; 104 | font-size: 0.8em; 105 | text-align: right; 106 | } 107 | 108 | a code { 109 | border: 0; 110 | color: navy; 111 | } 112 | 113 | 114 | div.skipped pre { 115 | background-color: rgba(213, 236, 22, 0.35); 116 | border-radius: 2px; 117 | } 118 | 119 | div.failed pre { 120 | background-color: rgba(255, 0, 0, 0.35); 121 | border-radius: 2px; 122 | } 123 | 124 | 125 | /* For In/Out blocks for embedded tests */ 126 | div.input_area { 127 | border-radius: 1px; 128 | 129 | } 130 | 131 | div.input_area pre { 132 | background-color: white; 133 | border-radius: 2px; 134 | margin-top: 0px; 135 | margin-bottom: 0px; 136 | } 137 | 138 | div.output_area pre { 139 | background-color: #ddd; 140 | border-radius: 2px; 141 | margin-top: 0px; 142 | margin-bottom: 0px; 143 | } 144 | 145 | div.rosetta_left { 146 | background-color: rgba(255, 238, 238, 1.0); 147 | border-radius: 2px; 148 | display: table-cell; 149 | width: 50%; 150 | border-top: 2px solid #c9a; 151 | border-bottom: 2px solid #c9a; 152 | border-left: 1px solid #c9a; 153 | border-right: 2px solid #c9a; 154 | } 155 | 156 | div.rosetta_right { 157 | background-color: rgba(238, 255, 238, 1.0); 158 | border-radius: 2px; 159 | display: table-cell; 160 | width: 50%; 161 | border-top: 2px solid #ac9; 162 | border-bottom: 2px solid #ac9; 163 | border-left: 1px solid #ac9; 164 | border-right: 2px solid #ac9; 165 | } 166 | 167 | div.rosetta_left pre { 168 | background-color: rgba(255, 238, 238, 1.0); 169 | border: none; 170 | } 171 | 172 | div.rosetta_right pre { 173 | background-color: rgba(238, 255, 238, 1.0); 174 | border: none; 175 | } 176 | 177 | div.rosetta_outer { 178 | display: table; 179 | width: 100% 180 | } 181 | 182 | div.highlight-python { 183 | margin-top: 0px; 184 | margin-bottom: 0px; 185 | } 186 | 187 | /* Fix the Parameter block width problem */ 188 | table.field-list th { 189 | width: 80px; 190 | } 191 | -------------------------------------------------------------------------------- /pycycle/docs/_theme/theme.conf: -------------------------------------------------------------------------------- 1 | [theme] 2 | inherit = sphinxdoc 3 | stylesheet = style.css 4 | pygments_style = pygments.css 5 | 6 | [options] 7 | variable = default value 8 | -------------------------------------------------------------------------------- /pycycle/docs/conf.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | # This file is execfile()d with the current directory set to its 3 | # containing dir. 4 | import sys 5 | import os 6 | import importlib 7 | import textwrap 8 | 9 | from numpydoc.docscrape import NumpyDocString, Reader 10 | from mock import Mock 11 | 12 | from openmdao.docs.config_params import MOCK_MODULES 13 | from openmdao.docs._utils.patch import do_monkeypatch 14 | from openmdao.docs._utils.upload_doc_version import get_doc_version 15 | 16 | # Only mock the ones that don't import. 17 | for mod_name in MOCK_MODULES: 18 | try: 19 | importlib.import_module(mod_name) 20 | except ImportError: 21 | sys.modules[mod_name]=Mock() 22 | 23 | # start off running the monkeypatch to keep options/parameters 24 | # usable in docstrings via numpydoc for autodocumentation. 25 | do_monkeypatch() 26 | 27 | # If extensions (or modules to document with autodoc) are in another directory, 28 | # add these directories to sys.path here. If the directory is relative to the 29 | # documentation root, use os.path.abspath to make it absolute, like shown here. 30 | sys.path.insert(0, os.path.abspath('..')) 31 | sys.path.insert(0, os.path.abspath('.')) 32 | sys.path.insert(0, os.path.abspath('./_exts')) 33 | 34 | # -- General configuration ------------------------------------------------ 35 | 36 | # If your documentation needs a minimal Sphinx version, state it here. 37 | needs_sphinx = '1.5' 38 | 39 | # Add any Sphinx extension module names here, as strings. They can be 40 | # extensions coming with Sphinx (named 'sphinx.ext.*') or your custom 41 | # ones. 42 | extensions = [ 43 | 'sphinx.ext.autodoc', 44 | 'sphinx.ext.autosummary', 45 | 'sphinx.ext.doctest', 46 | 'sphinx.ext.todo', 47 | 'sphinx.ext.coverage', 48 | 'sphinx.ext.mathjax', 49 | 'sphinx.ext.viewcode', 50 | 'numpydoc', 51 | ] 52 | 53 | numpydoc_show_class_members = False 54 | 55 | # Add any paths that contain templates here, relative to this directory. 56 | templates_path = ['_templates'] 57 | 58 | # The suffix(es) of package filenames. 59 | # You can specify multiple suffix as a list of string: 60 | # source_suffix = ['.rst', '.md'] 61 | source_suffix = '.rst' 62 | 63 | # The master toctree document. 64 | master_doc = 'index' 65 | 66 | # General information about the project. 67 | project = u'pyCycle' 68 | copyright = u'2019 NASA Glenn' 69 | author = u'Eric S. Hendricks' 70 | 71 | # The version info for the project you're documenting, acts as replacement for 72 | # |version| and |release|, also used in various other places throughout the 73 | # built documents. 74 | import pycycle 75 | version = pycycle.__version__ 76 | # The full version, including alpha/beta/rc tags. 77 | release = pycycle.__version__ + ' Beta' 78 | 79 | 80 | 81 | # The language for content autogenerated by Sphinx. Refer to documentation 82 | # for a list of supported languages. 83 | # 84 | # This is also used if you do content translation via gettext catalogs. 85 | # Usually you set "language" from the command line for these cases. 86 | language = None 87 | 88 | 89 | # exclude_patterns is a list of patterns, relative to package directory, that match files and 90 | # directories to ignore when looking for package files. 91 | exclude_patterns = ['_build', '_srcdocs/dev'] 92 | absp = os.path.join('.', '_srcdocs') 93 | sys.path.insert(0, os.path.abspath(absp)) 94 | 95 | # #packages, in the order you want to document them 96 | # packages = [ 97 | # 'cea', 98 | # 'cea.thermo_data', 99 | # 'elements', 100 | # 'maps' , 101 | # 'maps.map_plotting' , 102 | # ] 103 | 104 | # if os.path.isfile("make_sourcedocs"): 105 | # from openmdao.docs._utils.generate_sourcedocs import generate_docs 106 | # generate_docs("..", "../..", packages) 107 | 108 | # The name of the Pygments (syntax highlighting) style to use. 109 | pygments_style = 'sphinx' 110 | 111 | # If true, `todo` and `todoList` produce output, else they produce nothing. 112 | todo_include_todos = False 113 | 114 | # -- Options for HTML output ---------------------------------------------- 115 | 116 | # The theme to use for HTML and HTML Help pages. See the documentation for 117 | # a list of builtin themes. 118 | html_theme = '_theme' 119 | 120 | # Add any paths that contain custom themes here, relative to this directory. 121 | html_theme_path = ['.'] 122 | 123 | # The name of an image file (relative to this directory) to place at the top 124 | # of the page. Replace this with your own image file 125 | # html_logo = '_static/OpenMDAO_Logo.png' 126 | 127 | # The name of an image file to use as favicon of the docs. 128 | # This file should be a Windows icon file (.ico) being 16x16 or 32x32 129 | # pixels large. 130 | # html_favicon = '_static/OpenMDAO_Favicon.ico' 131 | 132 | # If not '', a 'Last updated on:' timestamp is inserted at every page bottom, 133 | # using the given strftime format. 134 | html_last_updated_fmt = '%b %d, %Y' 135 | 136 | # Output file base name for HTML help builder. 137 | htmlhelp_basename = 'pycycledoc' 138 | 139 | #Customize sidebar 140 | html_sidebars = { 141 | '**': ['globaltoc.html', 'searchbox.html'] 142 | } 143 | # -- Options for manual page output --------------------------------------- 144 | 145 | # One entry per manual page. List of tuples 146 | # (package start file, name, description, authors, manual section). 147 | man_pages = [ 148 | (master_doc, 'pyCycle', u'pyCycle Documentation', 149 | [author], 1) 150 | ] 151 | -------------------------------------------------------------------------------- /pycycle/docs/examples/index.rst: -------------------------------------------------------------------------------- 1 | -------- 2 | Examples 3 | -------- 4 | 5 | Map plotting 6 | ------------ 7 | 8 | Turbofan with bleeds 9 | -------------------- 10 | 11 | Turbojet with afterburner 12 | ------------------------- 13 | 14 | N+3 multi-design point 15 | ---------------------- 16 | 17 | Heat exchanger duct 18 | ------------------- -------------------------------------------------------------------------------- /pycycle/docs/index.rst: -------------------------------------------------------------------------------- 1 | %%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%% 2 | Documentation for pyCycle version: |release| 3 | %%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%% 4 | 5 | pyCycle is a open-source thermodynamic cycle analysis tool designed integrating this analysis into larger multidisciplinary optimization problems. The code is written in Python and is built on top of the `OpenMDAO`_ framework. 6 | 7 | .. _OpenMDAO: https://www.openmdao.org/ 8 | 9 | The pyCycle project implements 1D thermodynamic cycle analysis equations similar to the NPSS code and has been validated against this tool. While the thermodynamic equations are similar to NPSS and other available cycle analysis tools, pyCycle is focused on applying advanced methods for supporting gradient-based optimization through the implementation of analytic derivatives. This feature enables efficient exploration of large design spaces when pyCycle is coupled with other disciplinary analysis tools. 10 | 11 | 12 | .. warning:: 13 | 14 | pyCycle is built on top of OpenMDAO and relies extensively its linear and nonlinear solver library. 15 | pyCycle was also designed with a user interface that is very similar to the NPSS cycle modeling library. 16 | If you're not comfortable with either OpenMDAO or NPSS, then you may find this library difficult to understand and use. 17 | 18 | Installation 19 | ************ 20 | Installation of pyCycle requires two pieces of software. First, if you do not have OpenMDAO you will need to install it following the instructions on the `OpenMDAO Getting Started`_ page. Second, you will need to install pyCycle by typing the following command into your python environment (we recommend Anaconda): 21 | 22 | .. code:: 23 | 24 | pip install pycycle 25 | 26 | 27 | .. _OpenMDAO Getting Started: https://openmdao.org/newdocs/versions/latest/getting_started/getting_started.html 28 | 29 | Tutorials 30 | ********** 31 | 32 | This section provides several tutorials showing how to build thermodynamic cycle models in pyCycle. 33 | For users unfamiliar with OpenMDAO, a review of the OpenMDAO User Guide is highly recommended before completing the pyCycle tutorials. 34 | The tutorials in this section will demonstrate how models are constructed and executed in pyCycle, starting with a simple turboject then moving to a more complicated turbofan model. 35 | Additional models showing more features available with pyCycle are provided in the Examples section. 36 | 37 | 38 | .. toctree:: 39 | :maxdepth: 1 40 | :name: tutorials 41 | 42 | tutorials/index.rst 43 | 44 | Reference Guide 45 | **************** 46 | 47 | The reference guide intended for users looking for explanation of a particular feature in detail or documentation of the arguments/options/settings for a specific cycle element, map or viewer. 48 | 49 | .. toctree:: 50 | :maxdepth: 1 51 | :name: reference_guide 52 | 53 | reference_guide/elements/index.rst 54 | reference_guide/maps/index.rst 55 | reference_guide/viewers.rst 56 | 57 | 58 | 59 | Examples 60 | ******** 61 | The examples in this section provide a more comprehensive demsonstration of the features of pyCycle and advanced modeling methods. 62 | 63 | .. toctree:: 64 | :maxdepth: 1 65 | :name: examples 66 | 67 | examples/index.rst 68 | -------------------------------------------------------------------------------- /pycycle/docs/reference_guide/elements/index.rst: -------------------------------------------------------------------------------- 1 | 2 | .. _element_ref: 3 | 4 | ---------- 5 | Elements 6 | ---------- 7 | 8 | Flight Conditions 9 | ----------------- 10 | Options 11 | 12 | Design 13 | 14 | Inputs 15 | 16 | Outputs 17 | 18 | Off-Design 19 | 20 | Inputs 21 | 22 | Outputs 23 | 24 | XDSM 25 | 26 | 27 | Inlet 28 | ----- 29 | 30 | Compressor 31 | ---------- 32 | 33 | Burner 34 | ------ 35 | 36 | Turbine 37 | ------- 38 | 39 | Nozzle 40 | ------ 41 | 42 | Shaft 43 | ----- 44 | 45 | Duct 46 | ---- 47 | 48 | Splitter 49 | -------- 50 | 51 | BleedOut 52 | -------- 53 | 54 | Performance 55 | ----------- 56 | 57 | 58 | 59 | -------------------------------------------------------------------------------- /pycycle/docs/reference_guide/maps/index.rst: -------------------------------------------------------------------------------- 1 | ---- 2 | Maps 3 | ---- 4 | 5 | 6 | Compressor Map 7 | -------------- 8 | 9 | Available maps 10 | 11 | Compressor map plots 12 | 13 | 14 | Turbine Map 15 | ----------- 16 | 17 | Available maps 18 | 19 | Turbine map plot 20 | 21 | -------------------------------------------------------------------------------- /pycycle/docs/reference_guide/viewers.rst: -------------------------------------------------------------------------------- 1 | ------- 2 | Viewers 3 | ------- -------------------------------------------------------------------------------- /pycycle/docs/tutorials/images/turbojet_elements.pdf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/OpenMDAO/pyCycle/8a031800fb90dea96ea174aedb112e7d5e120b39/pycycle/docs/tutorials/images/turbojet_elements.pdf -------------------------------------------------------------------------------- /pycycle/docs/tutorials/images/turbojet_elements.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/OpenMDAO/pyCycle/8a031800fb90dea96ea174aedb112e7d5e120b39/pycycle/docs/tutorials/images/turbojet_elements.png -------------------------------------------------------------------------------- /pycycle/docs/tutorials/images/turbojet_elements.tex: -------------------------------------------------------------------------------- 1 | \documentclass{article} 2 | 3 | \usepackage[latin1]{inputenc} 4 | \usepackage{tikz} 5 | \usetikzlibrary{shapes, arrows, positioning, calc} 6 | 7 | %%%< 8 | \usepackage{verbatim} 9 | \usepackage[active,tightpage]{preview} 10 | \PreviewEnvironment{tikzpicture} 11 | \setlength\PreviewBorder{5pt}% 12 | %%%> 13 | 14 | \begin{comment} 15 | :Title: Simple flow chart 16 | :Tags: Diagrams 17 | 18 | With PGF/TikZ you can draw flow charts with relative ease. This flow chart from [1]_ 19 | outlines an algorithm for identifying the parameters of an autonomous underwater vehicle model. 20 | 21 | Note that relative node 22 | placement has been used to avoid placing nodes explicitly. This feature was 23 | introduced in PGF/TikZ >= 1.09. 24 | 25 | .. [1] Bossley, K.; Brown, M. & Harris, C. Neurofuzzy identification of an autonomous underwater vehicle `International Journal of Systems Science`, 1999, 30, 901-913 26 | 27 | 28 | \end{comment} 29 | 30 | 31 | \begin{document} 32 | \pagestyle{empty} 33 | 34 | 35 | % Define block styles 36 | 37 | \tikzstyle{block} = [rectangle, draw, fill=blue!20, 38 | text width=8em, text centered, font=\LARGE, rounded corners, minimum height=4em] 39 | \tikzstyle{line} = [draw, -latex'] 40 | 41 | 42 | \begin{tikzpicture}[node distance = 2cm, auto] 43 | % Place nodes 44 | \node [block] (fc) {Flight Conditions}; 45 | \node [block] [block, right=2em of fc ] (inlet) {Inlet}; 46 | \node [block, right=2em of inlet ] (hpc) {Compressor}; 47 | \node [block, right=2em of hpc] (burner) {Burner}; 48 | \node [block, right=2em of burner] (hpt) {Turbine}; 49 | \node [block, right=2em of hpt] (nozzle) {Nozzle}; 50 | \node (inletnozzle)[block, below= of $(inlet)!0.5!(nozzle)$] (shaft) {Shaft}; 51 | 52 | % Draw edges 53 | \path [line] (fc) |- (inlet); 54 | \path [line] (inlet) |- (hpc); 55 | \path [line] (hpc) |- (burner); 56 | \path [line] (burner) |- (hpt); 57 | \path [line] (hpt) |- (nozzle); 58 | \path [line] (hpc) |- (shaft); 59 | \path [line] (hpt) |- (shaft); 60 | 61 | \end{tikzpicture} 62 | 63 | 64 | \end{document} -------------------------------------------------------------------------------- /pycycle/docs/tutorials/index.rst: -------------------------------------------------------------------------------- 1 | 2 | .. toctree:: 3 | :maxdepth: 1 4 | 5 | turbojet.rst 6 | turbofan.rst 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | -------------------------------------------------------------------------------- /pycycle/docs/tutorials/turbofan.rst: -------------------------------------------------------------------------------- 1 | -------- 2 | TurboFan 3 | -------- 4 | 5 | Introductory paragraph 6 | 7 | Image of model block diagram 8 | 9 | Initialization options 10 | 11 | More setup options for elements 12 | 13 | Design vs off-design 14 | 15 | Specifying turbomachinery performance maps 16 | 17 | Off-design balance setup 18 | 19 | Example results (page viewer) -------------------------------------------------------------------------------- /pycycle/docs/tutorials/turbojet.rst: -------------------------------------------------------------------------------- 1 | -------- 2 | TurboJet 3 | -------- 4 | 5 | The performance of all jet engines can be modeled by setting up an appropriate model of the thermodynamic cycle that a particular engine is based on. 6 | One of the simplest kinds of jet engines is the single spool turbojet engine. 7 | The first ever production turbojet engine, the Junkers Jumo 004, was a single spool turbojet that was used to power the ME-262 jet fighter at the end of world war 2. 8 | By "single spool", we mean that there is a one spinning shaft that connects a single compressor to a single turbine. 9 | 10 | The relative simplicity that is an inherent feature of the single spool turbojet makes it a good place to start learning how to model the performance of jet engines. 11 | 12 | .. figure:: images/turbojet_elements.png 13 | :align: center 14 | :width: 80% 15 | :alt: element diagram for the turbojet tutorial model 16 | 17 | 18 | 19 | 20 | Terminology 21 | ------------ 22 | 23 | Element 24 | ******* 25 | Every pyCycle model is built up from set of OpenMDAO groups that we call **Elements**. 26 | This is our term for "something that performs a thermodynamic operation". 27 | Examples of elements include: :code:`Inlet`, :code:`Compressor`, :code:`Nozzle`. 28 | Each box in the above flow diagram for a turbojet represents one Element. 29 | pyCycle ships with a :ref:`library of pre-built elements` for you to use, 30 | but you could also write your own as needed. 31 | 32 | Cycle 33 | ***** 34 | Elements are assembled within a containing OpenMDAO group, called the **Cycle**. 35 | The cycle is represented by the entire flow diagram above. 36 | Notice that there are some loops in that diagram, indicating that a nonlinear solver is needed. 37 | These data cycles create implicit relationships between the elements in the model, and they show up in nearly every kind of thermodynamic cycle model. 38 | pyCycle uses OpenMDAO's NewtonSolver and a DirectSolver to converge these implicit cycles. 39 | 40 | "on-design" vs. "off-design" 41 | **************************** 42 | We're going to walk you through a run script that builds a single spool turbojet model and runs it in both **on-design** and **off-design** modes. 43 | All pyCycle models will be run in both modes, and it is important to understand what each one is for and how the relate to each other. 44 | 45 | The **on-design** mode (i.e. :code:`design=True`) takes a given mass-flow rate (pyCycle uses the variable :code:`W` to represent mass-flow) and computes the various flow-areas and performance map-scalars for all of elements in the cycle. 46 | These calculations only need to be run one time, and you should only ever have one instance of the cycle running in on-design mode. 47 | There is an associated flight-condition with the on-design calculations, which you choose because it provides a convenient place to specify the mass-flow. 48 | For example, subsonic cycles usually use either sea-level-static or top-of-climb as their on-design condition. 49 | 50 | The **off-design** mode (i.e. :code:`design=False`) requires the areas and map-scalars as inputs, and then computes the mass-flow rate and overall performance of the cycle for any given flight condition. 51 | You will use off-design mode to get performance data about your engine cycle for any operating condition you care about. 52 | 53 | 54 | Preamble 55 | -------- 56 | Import statements 57 | 58 | Creating the turbojet model 59 | --------------------------- 60 | 61 | Adding cycle elements 62 | 63 | Setting thermodynamic properties 64 | 65 | Connecting flow stations 66 | 67 | Connecting cycle elements 68 | 69 | Setting up balances 70 | 71 | Setting execution order 72 | 73 | Add/setup Newton solver 74 | 75 | 76 | Configuring output viewer 77 | -------------------------- 78 | 79 | Executing the model 80 | ------------------- 81 | 82 | Example results (page viewer) 83 | ----------------------------- -------------------------------------------------------------------------------- /pycycle/element_base.py: -------------------------------------------------------------------------------- 1 | import numpy as np 2 | import openmdao.api as om 3 | 4 | from pycycle.flow_in import FlowIn 5 | from pycycle.thermo.thermo import Thermo, ThermoAdd 6 | from pycycle.constants import ALLOWED_THERMOS 7 | 8 | 9 | class Element(om.Group): 10 | """ 11 | Custom pyCycle group for anything that requires input or output ports 12 | """ 13 | 14 | def __init__(self, **kwargs): 15 | 16 | super().__init__(**kwargs) 17 | 18 | self.Fl_I_data = {} 19 | self.Fl_O_data = {} 20 | 21 | def initialize(self): 22 | 23 | self.options.declare('design', default=True, 24 | desc='Switch between on-design and off-design calculation.') 25 | self.options.declare('thermo_data', default=False, 26 | desc='thermodynamic data specific to this element', recordable=False) 27 | self.options.declare('thermo_method', default='CEA', values=ALLOWED_THERMOS, 28 | desc='Method for computing thermodynamic properties') 29 | 30 | def copy_flow(self, src_port, output_port): 31 | """ 32 | Copy the flow data from `src_from` port to `target_to` port 33 | 34 | src_port: str or 35 | the name of the input port to copy from, or the ThermoAdd instance to query 36 | """ 37 | 38 | if isinstance(src_port, str): 39 | self.Fl_O_data[output_port] = self.Fl_I_data[src_port] 40 | elif isinstance(src_port, ThermoAdd): 41 | self.Fl_O_data[output_port] = src_port.output_port_data() 42 | else: 43 | raise ValueError('copy_from argument must be either a string that is ' 44 | 'the name of an input port, or a ThermoAdd instance') 45 | 46 | def init_output_flow(self, port_name, port_data): 47 | """ 48 | Initialize the given output port with the pord_data 49 | """ 50 | 51 | if isinstance(port_data, ThermoAdd): 52 | self.Fl_O_data[port_name] = port_data.output_port_data() 53 | else: 54 | self.Fl_O_data[port_name] = port_data 55 | 56 | 57 | # TODO: at end of setup, compare all the ports to whats in the port data and make sure that there is nothing missing 58 | 59 | 60 | 61 | 62 | 63 | 64 | -------------------------------------------------------------------------------- /pycycle/elements/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/OpenMDAO/pyCycle/8a031800fb90dea96ea174aedb112e7d5e120b39/pycycle/elements/__init__.py -------------------------------------------------------------------------------- /pycycle/elements/ambient.py: -------------------------------------------------------------------------------- 1 | import openmdao.api as om 2 | 3 | from pycycle.elements.US1976 import USatm1976Comp 4 | 5 | class DeltaTs(om.ExplicitComponent): 6 | """Computes temperature based on delta from atmospheric""" 7 | 8 | def setup(self): 9 | 10 | # inputs 11 | self.add_input('Ts_in', val=500.0, units='degR', desc='Temperature from atmospheric model') 12 | self.add_input('dTs', val=0.0, units='degR', desc='Delta from standard day temperature') 13 | 14 | self.add_output('Ts', shape=1, units='degR', desc='Temperature with delta') 15 | 16 | self.declare_partials('Ts', ['Ts_in', 'dTs'], val=1.0) 17 | 18 | def compute(self, inputs, outputs): 19 | outputs['Ts'] = inputs['Ts_in'] + inputs['dTs'] 20 | 21 | def compute_partials(self, inputs, partials): 22 | pass 23 | 24 | 25 | class Ambient(om.Group): 26 | """Determines pressure, temperature and density base on altitude from an input standard atmosphere table""" 27 | 28 | def setup(self): 29 | readAtm = self.add_subsystem('readAtmTable', USatm1976Comp(), promotes=('alt', 'Ps', 'rhos')) 30 | 31 | self.add_subsystem('dTs', DeltaTs(), promotes=('dTs', 'Ts')) 32 | self.connect('readAtmTable.Ts', 'dTs.Ts_in') 33 | 34 | # self.set_order(['readAtmTable','dTs']) 35 | 36 | 37 | if __name__ == "__main__": 38 | 39 | from pycycle.elements.US1976 import USatm1976Data 40 | 41 | p1 = om.Problem() 42 | p1.root = Ambient() 43 | 44 | var = (('alt', 30000.0),) 45 | p1.root.add("idv", om.IndepVarComp(var), promotes=["*"]) 46 | 47 | p1.setup() 48 | 49 | p1.run() 50 | 51 | # p1.check_partials() 52 | # print('Ts: ', p1['Ts']) 53 | # print('Ps: ', p1['Ps']) 54 | # print('rhos: ', p1['rhos']) 55 | 56 | T = USatm1976Data.T 57 | P = USatm1976Data.P 58 | rho = USatm1976Data.rho 59 | 60 | for i, alt in enumerate(USatm1976Data.alt): 61 | p1['alt'] = alt 62 | p1.run() 63 | print(10*"=") 64 | print("Ts", p1['Ts'], T[i]) 65 | print("Ps", p1['Ps'], P[i]) 66 | print("rho", p1['rhos'], rho[i]) 67 | 68 | p1.model.list_states() 69 | -------------------------------------------------------------------------------- /pycycle/elements/cfd_start.py: -------------------------------------------------------------------------------- 1 | import openmdao.api as om 2 | 3 | from pycycle.constants import THERMO_DEFAULT_COMPOSITIONS 4 | from pycycle.thermo.cea import species_data 5 | from pycycle.elements.flow_start import FlowStart 6 | from pycycle.element_base import Element 7 | 8 | class CFDStart(Element): 9 | 10 | def initialize(self): 11 | self.options.declare('composition', default=None, 12 | desc='composition of the flow. If None, default for thermo package is used') 13 | super().initialize() 14 | 15 | 16 | def pyc_setup_output_ports(self): 17 | thermo_method = self.options['thermo_method'] 18 | composition = self.options['composition'] 19 | if composition is None: 20 | composition = THERMO_DEFAULT_COMPOSITIONS[thermo_method] 21 | self.init_output_flow('Fl_O', composition) 22 | 23 | 24 | def setup(self): 25 | thermo_method = self.options['thermo_method'] 26 | thermo_data = self.options['thermo_data'] 27 | 28 | composition = self.Fl_O_data['Fl_O'] 29 | 30 | 31 | fs = self.add_subsystem('fs', FlowStart(thermo_method=thermo_method,thermo_data=thermo_data, 32 | composition=composition), promotes_outputs=['Fl_O:*'],promotes_inputs=['W']) 33 | fs.pyc_setup_output_ports() 34 | 35 | 36 | balance = om.BalanceComp() 37 | balance.add_balance('P', val=10., units='psi', eq_units='psi', lhs_name='Ps_computed', rhs_name='Ps', 38 | lower=1e-1) 39 | #guess_func=lambda inputs, resids: 5.) 40 | balance.add_balance('T', val=800., units='degR', eq_units='ft/s', lhs_name='V_computed', rhs_name='V', 41 | lower=1e-1) 42 | #guess_func=lambda inputs, resids: 400.) 43 | balance.add_balance('MN', val=.3, eq_units='inch**2', lhs_name='area_computed', rhs_name='area', lower=1e-6) 44 | #guess_func=lambda inputs, resids: .6) 45 | 46 | self.add_subsystem('balance', balance, promotes_inputs=['Ps', 'V', 'area']) 47 | self.connect('Fl_O:stat:P', 'balance.Ps_computed') 48 | self.connect('Fl_O:stat:V', 'balance.V_computed') 49 | self.connect('Fl_O:stat:area', 'balance.area_computed') 50 | 51 | self.connect('balance.P', 'fs.P') 52 | self.connect('balance.T', 'fs.T') 53 | self.connect('balance.MN', 'fs.MN') 54 | 55 | newton = self.nonlinear_solver = om.NewtonSolver() 56 | newton.options['solve_subsystems'] = True 57 | newton.options['maxiter'] = 10 58 | # newton.linesearch = BoundsEnforceLS() 59 | # newton.linesearch.options['print_bound_enforce'] = True 60 | 61 | self.linear_solver = om.DirectSolver(assemble_jac=True) 62 | 63 | 64 | 65 | if __name__ == "__main__": 66 | 67 | p = om.Problem() 68 | 69 | params = p.model.add_subsystem('params', om.IndepVarComp(), promotes=['*']) 70 | params.add_output('Ps', units='Pa', val=22845.15677648) 71 | params.add_output('V', units='m/s', val=158.83851913) 72 | params.add_output('area', units='m**2', val=0.87451328) 73 | params.add_output('W', units='kg/s', val=50.2454107) 74 | 75 | p.model.add_subsystem('cfd_start', CFDStart(), promotes_inputs=['Ps', 'V', 'area', 'W']) 76 | 77 | p.setup(check=False) 78 | 79 | p.set_solver_print(level=-1) 80 | p.set_solver_print(level=2, depth=1) 81 | 82 | 83 | p.run_model() 84 | 85 | p.model.list_outputs(residuals=True) 86 | 87 | 88 | 89 | 90 | 91 | -------------------------------------------------------------------------------- /pycycle/elements/flow_start.py: -------------------------------------------------------------------------------- 1 | import numpy as np 2 | 3 | from openmdao.api import Group, ExplicitComponent 4 | 5 | from pycycle.thermo.cea import species_data 6 | from pycycle.thermo.thermo import Thermo, ThermoAdd 7 | from pycycle.constants import THERMO_DEFAULT_COMPOSITIONS 8 | from pycycle.element_base import Element 9 | 10 | 11 | class FlowStart(Element): 12 | 13 | def initialize(self): 14 | 15 | self.options.declare('composition', default=None, 16 | desc='composition of the flow. None means using the default for the thermo package') 17 | 18 | self.options.declare('reactant', default=False, types=(bool, str), 19 | desc='If False, flow matches base composition. If a string, then that reactant ' 20 | 'is mixed into the flow at at the ratio set by the `mix_ratio` input') 21 | 22 | self.options.declare('mix_ratio_name', default='mix:ratio', 23 | desc='The name of the input that governs the mix ratio of the reactant to the primary flow') 24 | 25 | super().initialize() 26 | 27 | def pyc_setup_output_ports(self): 28 | 29 | thermo_method = self.options['thermo_method'] 30 | thermo_data = self.options['thermo_data'] 31 | composition = self.options['composition'] 32 | reactant = self.options['reactant'] 33 | 34 | if reactant is not False: 35 | self.thermo_add = ThermoAdd(method=thermo_method, mix_mode='reactant', 36 | thermo_kwargs={'spec':thermo_data, 37 | 'inflow_composition':composition, 38 | 'mix_composition':reactant, }) 39 | 40 | self.init_output_flow('Fl_O', self.thermo_add) 41 | 42 | else: 43 | if composition is None: 44 | composition = THERMO_DEFAULT_COMPOSITIONS[thermo_method] 45 | self.init_output_flow('Fl_O', composition) 46 | 47 | 48 | def setup(self): 49 | thermo_method = self.options['thermo_method'] 50 | thermo_data = self.options['thermo_data'] 51 | reactant = self.options['reactant'] 52 | 53 | composition = self.Fl_O_data['Fl_O'] 54 | 55 | 56 | # inputs 57 | if reactant is not False : 58 | mix_ratio_name = self.options['mix_ratio_name'] 59 | 60 | 61 | self.add_subsystem('thermo_add', self.thermo_add, 62 | promotes_inputs=(('Fl_I:stat:W', 'W'), ('mix:ratio', mix_ratio_name)), 63 | promotes_outputs=(('composition_out', 'composition'), )) 64 | 65 | set_TP = Thermo(mode='total_TP', fl_name='Fl_O:tot', 66 | method=thermo_method, 67 | thermo_kwargs={'composition':composition, 68 | 'spec':thermo_data}) 69 | 70 | in_vars = ('T','P', 'composition') 71 | 72 | self.add_subsystem('totals', set_TP, promotes_inputs=in_vars, 73 | promotes_outputs=('Fl_O:tot:*',)) 74 | 75 | set_stat_MN = Thermo(mode='static_MN', fl_name='Fl_O:stat', 76 | method=thermo_method, 77 | thermo_kwargs={'composition':composition, 78 | 'spec':thermo_data} ) 79 | 80 | self.add_subsystem('exit_static', set_stat_MN, promotes_inputs=('MN', 'W', 'composition'), 81 | promotes_outputs=('Fl_O:stat:*', )) 82 | 83 | self.connect('totals.h','exit_static.ht') 84 | self.connect('totals.S','exit_static.S') 85 | self.connect('Fl_O:tot:P','exit_static.guess:Pt') 86 | self.connect('totals.gamma', 'exit_static.guess:gamt') 87 | 88 | super().setup() 89 | 90 | 91 | -------------------------------------------------------------------------------- /pycycle/elements/gearbox.py: -------------------------------------------------------------------------------- 1 | import openmdao.api as om 2 | 3 | 4 | class Gearbox(om.ImplicitComponent): 5 | """Gearbox component based on N+3 model""" 6 | 7 | def initialize(self): 8 | self.options.declare('design', default=True, 9 | desc='Switch between on-design and off-design calculation.') 10 | 11 | def setup(self): 12 | 13 | design = self.options['design'] 14 | 15 | self.add_input('N_in', val=1000.0, units='rpm', desc='Shaft speed entering gearbox') 16 | self.add_input('N_out', val=1000.0, units='rpm', desc='Shaft speed exiting gearbox') 17 | self.add_input('eff', val=1.0, units=None, desc='Gearbox transmission efficiency') 18 | 19 | self.add_output('trq_in', val=1.0, units='ft*lbf', desc='Torque entering gearbox') 20 | self.add_output('trq_out', val=1.0, units='ft*lbf', desc='Torque exiting gearbox') 21 | 22 | if design: 23 | 24 | self.add_input('trq_base', val=1.0, units='ft*lbf', desc='Base torque value') 25 | self.add_output('gear_ratio', val=1.0, units=None, desc='Gear ratio (N_out/N_in)') 26 | 27 | self.declare_partials('gear_ratio', ['N_in','N_out']) 28 | self.declare_partials('gear_ratio', 'gear_ratio', val=-1.0) 29 | self.declare_partials('trq_in', ['N_in','N_out']) 30 | 31 | else: 32 | self.add_input('gear_ratio', val=1.0, units=None, desc='Gear ratio (N_out/N_in)') 33 | self.add_output('trq_base', val=1.0, units='ft*lbf', desc='Base torque value') 34 | 35 | self.declare_partials('trq_base', ['N_in','gear_ratio']) 36 | self.declare_partials('trq_base', 'N_out', val=1.0) 37 | self.declare_partials('trq_in','gear_ratio') 38 | 39 | self.declare_partials('trq_in', ['trq_base','eff']) 40 | self.declare_partials('trq_in', 'trq_in', val=-1.0) 41 | self.declare_partials('trq_out', 'trq_base', val=1.0) 42 | self.declare_partials('trq_out', 'trq_out', val=-1.0) 43 | 44 | 45 | def solve_nonlinear(self, inputs, outputs): 46 | 47 | design = self.options['design'] 48 | 49 | if design: 50 | outputs['gear_ratio'] = inputs['N_out'] / inputs['N_in'] 51 | outputs['trq_in'] = -inputs['trq_base']*inputs['eff']*inputs['N_out'] / inputs['N_in'] 52 | outputs['trq_out'] = inputs['trq_base'] 53 | else: 54 | outputs['trq_in'] = -outputs['trq_base']*inputs['eff']*inputs['gear_ratio'] 55 | outputs['trq_out'] = outputs['trq_base'] 56 | 57 | def apply_nonlinear(self, inputs, outputs, resids): 58 | 59 | design = self.options['design'] 60 | 61 | if design: 62 | resids['gear_ratio'] = inputs['N_out'] / inputs['N_in'] - outputs['gear_ratio'] 63 | resids['trq_in'] = -inputs['trq_base']*inputs['eff']*inputs['N_out'] / inputs['N_in'] - outputs['trq_in'] 64 | resids['trq_out'] = inputs['trq_base'] - outputs['trq_out'] 65 | 66 | else: 67 | resids['trq_base'] = inputs['N_out'] - inputs['N_in'] * inputs['gear_ratio'] 68 | resids['trq_in'] = -outputs['trq_base']*inputs['eff']*inputs['gear_ratio'] - outputs['trq_in'] 69 | resids['trq_out'] = outputs['trq_base'] - outputs['trq_out'] 70 | 71 | def linearize(self, inputs, outputs, J): 72 | 73 | design = self.options['design'] 74 | 75 | if design: 76 | J['gear_ratio','N_in'] = -inputs['N_out']/inputs['N_in']**2 77 | J['gear_ratio','N_out'] = 1.0/inputs['N_in'] 78 | 79 | J['trq_in','trq_base'] = -inputs['eff'] * inputs['N_out'] / inputs['N_in'] 80 | J['trq_in','eff'] = -inputs['trq_base'] * inputs['N_out'] / inputs['N_in'] 81 | J['trq_in','N_in'] = inputs['trq_base'] * inputs['eff'] * inputs['N_out'] / inputs['N_in']**2 82 | J['trq_in','N_out'] = -inputs['trq_base'] * inputs['eff'] / inputs['N_in'] 83 | 84 | else: 85 | J['trq_base','N_in'] = -inputs['gear_ratio'] 86 | J['trq_base','gear_ratio'] = -inputs['N_in'] 87 | 88 | J['trq_in','trq_base'] = -inputs['eff']*inputs['gear_ratio'] 89 | J['trq_in','eff'] = -outputs['trq_base']*inputs['gear_ratio'] 90 | J['trq_in','gear_ratio'] = -outputs['trq_base']*inputs['eff'] 91 | 92 | 93 | if __name__ == "__main__": 94 | 95 | p = om.Problem() 96 | 97 | inputs = p.model.add_subsystem('inputs',om.IndepVarComp(), promotes=['*']) 98 | inputs.add_output('eff', 1.0) 99 | inputs.add_output('N_in', 6772.0, units='rpm') 100 | inputs.add_output('N_out', 2184.5, units='rpm') 101 | inputs.add_output('trq_base', 23711.1, units='ft*lbf') 102 | # inputs.add_output('gear_ratio', 0.322578263438, units=None) 103 | 104 | 105 | p.model.add_subsystem('gearbox', Gearbox(design=True), promotes=['*']) 106 | 107 | p.setup() 108 | # p['trq_base'] = 23711.1 109 | p.run_model() 110 | 111 | p.check_partials(compact_print=True) 112 | 113 | print(p['trq_in'][0]) 114 | print(p['trq_out'][0]) 115 | print(p['gear_ratio'][0]) 116 | 117 | print(p['N_in'][0]*p['trq_in'][0]/5252.0) 118 | print(p['N_out'][0]*p['trq_out'][0]/5252.0) 119 | -------------------------------------------------------------------------------- /pycycle/elements/performance.py: -------------------------------------------------------------------------------- 1 | from openmdao.api import ExplicitComponent 2 | 3 | 4 | class Performance(ExplicitComponent): 5 | """Component to calculate overall engine performance parameters""" 6 | 7 | def initialize(self): 8 | 9 | self.options.declare('num_nozzles', default=1, types=int) 10 | self.options.declare('num_burners', default=1, types=int) 11 | 12 | def setup(self): 13 | # inputs 14 | self.add_input('Pt2', val=14.696, units='lbf/inch**2', desc='pressure at the inlet of the first compressor') 15 | self.add_input('Pt3', val=14.696, units='lbf/inch**2', desc='pressure at the exit of the last compressor') 16 | # self.add_input('Wfuel', val=0.0, units='lbm/s', desc='mass flow rate of fuel to combustor') 17 | self.add_input('ram_drag', val=0.0, units='lbf', desc='ram drag from inlet') 18 | self.add_input('power', val=1.0, units='hp', desc='shaft power') 19 | 20 | num_nozzles = self.options['num_nozzles'] 21 | self.Fg_vals = [] 22 | for i in range(num_nozzles): 23 | Fg_val_name = 'Fg_{:d}'.format(i) 24 | self.add_input(Fg_val_name, val=0., units='lbf', desc='gross thrust from nozzle {:d}'.format(i)) 25 | self.Fg_vals.append(Fg_val_name) 26 | 27 | num_burners = self.options['num_burners'] 28 | self.Wfuel_vals = [] 29 | for i in range(num_burners): 30 | Wfuel_val_name = 'Wfuel_{:d}'.format(i) 31 | self.add_input(Wfuel_val_name, val=0., units='lbm/s', desc='fuel flow rate entering combustor {:d}'.format(i)) 32 | self.Wfuel_vals.append(Wfuel_val_name) 33 | 34 | # outputs 35 | self.add_output('OPR', val=1.0, desc='overall pressure ratio, Pt3/Pt2') 36 | self.add_output('Fg', val=10000.0, units='lbf', desc='gross thrust of all nozzles') 37 | self.add_output('Fn', val=10000.0, units='lbf', desc='net thrust of the engine') 38 | 39 | self.declare_partials('OPR', ['Pt3', 'Pt2']) 40 | self.declare_partials('Fg', 'Fg_*', val=1.0) 41 | self.declare_partials('Fn', 'Fg_*', val=1.0) 42 | self.declare_partials('Fn', 'ram_drag', val=-1.0) 43 | 44 | if num_burners > 0: 45 | self.add_output('TSFC', val=1.0, units='lbm/(h*lbf)', desc='thrust specific fuel consumption') 46 | self.add_output('PSFC', val=1.0, units='lbm/(h*lbf)', desc='power specific fuel consumption') 47 | self.add_output('Wfuel', val=.001, units='lbm/s', desc='mass flow rate of fuel to combustor') 48 | 49 | 50 | self.declare_partials('TSFC', ['Fg_*', 'ram_drag', 'Wfuel_*']) 51 | self.declare_partials('PSFC', ['power', 'Wfuel_*']) 52 | self.declare_partials('Wfuel', 'Wfuel_*', val=1.0) 53 | 54 | 55 | def compute(self, inputs, outputs): 56 | 57 | outputs['OPR'] = inputs['Pt3'] / inputs['Pt2'] 58 | 59 | Fg = 0.0 60 | for Fg_val in self.Fg_vals: 61 | Fg += inputs[Fg_val] 62 | outputs['Fn'] = Fn = Fg - inputs['ram_drag'] 63 | outputs['Fg'] = Fg 64 | 65 | if self.Wfuel_vals: 66 | Wfuel = 0.0 67 | for Wfuel_val in self.Wfuel_vals: 68 | Wfuel += inputs[Wfuel_val] 69 | 70 | outputs['Wfuel'] = Wfuel 71 | outputs['TSFC'] = Wfuel * 3600. / (Fn+1e-10) 72 | outputs['PSFC'] = Wfuel * 3600. / inputs['power'] 73 | 74 | def compute_partials(self, inputs, J): 75 | Pt2 = inputs['Pt2'] 76 | power = inputs['power'] 77 | 78 | J['OPR', 'Pt3'] = 1 / Pt2 79 | J['OPR', 'Pt2'] = -inputs['Pt3'] / Pt2 ** 2 80 | 81 | wfuel = 0.0 82 | for Wfuel_val in self.Wfuel_vals: 83 | wfuel += inputs[Wfuel_val] 84 | 85 | fg = 0.0 86 | for Fg_val in self.Fg_vals: 87 | fg += inputs[Fg_val] 88 | fn = fg - inputs['ram_drag'] 89 | 90 | for Fg_val in self.Fg_vals: 91 | if self.Wfuel_vals: 92 | J['TSFC', Fg_val] = -3600.0 * wfuel / fn ** 2 93 | 94 | for Wfuel_val in self.Wfuel_vals: 95 | J['TSFC', Wfuel_val] = 3600.0 / fn 96 | J['PSFC', Wfuel_val] = 3600. / power 97 | 98 | # J['TSFC', 'Wfuel'] = 3600.0/(outputs['Fg'] - inputs['ram_drag']) 99 | if self.Wfuel_vals: 100 | J['TSFC', 'ram_drag'] = 3600.0 * wfuel / fn ** 2 101 | J['PSFC', 'power'] = -3600. * wfuel / power ** 2 102 | 103 | 104 | if __name__ == "__main__": 105 | from openmdao.core.problem import Problem, IndepVarComp 106 | 107 | p = Problem() 108 | 109 | des_vars = p.model.add_subsystem('des_vars', IndepVarComp(), promotes=['*']) 110 | des_vars.add_output('power', 200.0, units='hp') 111 | des_vars.add_output('Pt2', 204.696, units='psi') 112 | des_vars.add_output('Pt3', 104.696, units='psi') 113 | des_vars.add_output('Wfuel_0', 2, units='lbm/s') 114 | des_vars.add_output('ram_drag', 100, units='lbf') 115 | des_vars.add_output('Fg_0', 1200, units='lbf') 116 | des_vars.add_output('Fg_1', 2000, units='lbf') 117 | 118 | p.model.add_subsystem('comp', Performance(num_nozzles=2, num_burners=1), promotes=['*']) 119 | # p.model.comp.fd_options['form'] = 'complex_step' 120 | 121 | p.setup(check=True) 122 | p.run_model() 123 | 124 | p.check_partials(compact_print=True) 125 | -------------------------------------------------------------------------------- /pycycle/elements/shaft.py: -------------------------------------------------------------------------------- 1 | import numpy as np 2 | 3 | from openmdao.api import ExplicitComponent 4 | 5 | 6 | class Shaft(ExplicitComponent): 7 | 8 | """Calculates power balance for shaft""" 9 | 10 | def initialize(self): 11 | self.options.declare('num_ports', default=2, 12 | desc="number shaft connections to make") 13 | 14 | def setup(self): 15 | 16 | num_ports = self.options['num_ports'] 17 | 18 | self.add_input('Nmech', val = 1000.0, units="rpm") 19 | self.add_input('HPX', val = 0.0, units='hp') 20 | self.add_input('fracLoss', val = 0.0) 21 | 22 | self.add_output('trq_in', val=1.0, units='ft*lbf') 23 | self.add_output('trq_out', val=1.0, units='ft*lbf') 24 | self.add_output('trq_net', val=1.0, units='ft*lbf') 25 | self.add_output('pwr_in', val=1.0, units='hp') 26 | self.add_output('pwr_in_real', val=1.0, units='hp') 27 | self.add_output('pwr_out', val=1.0, units='hp') 28 | self.add_output('pwr_out_real', val=1.0, units='hp') 29 | self.add_output('pwr_net', val=1.0, units='hp') 30 | 31 | HP_to_FT_LBF_per_SEC = 550 32 | self.convert = 2. * np.pi / 60. / HP_to_FT_LBF_per_SEC 33 | 34 | self.trq_vars = [] 35 | for i in range(num_ports): 36 | trq_var_name = 'trq_{:d}'.format(i) 37 | self.add_input(trq_var_name, val=0., units='ft*lbf') 38 | 39 | self.trq_vars.append(trq_var_name) 40 | 41 | self.declare_partials(['trq_in', 'trq_out', 'pwr_in', 'pwr_out'], trq_var_name) 42 | 43 | self.declare_partials('trq_net', '*') 44 | self.declare_partials('pwr_net', '*') 45 | self.declare_partials(['pwr_in', 'pwr_out', 'pwr_in_real', 'pwr_out_real'], '*') 46 | 47 | def compute(self, inputs, outputs): 48 | 49 | fracLoss = inputs['fracLoss'] 50 | HPX = inputs['HPX'] 51 | Nmech = inputs['Nmech'] 52 | 53 | trq_in = 0 54 | trq_out = 0 55 | 56 | for trq_var in self.trq_vars: 57 | trq = inputs[trq_var] 58 | if trq >= 0: 59 | trq_in += trq 60 | else: 61 | trq_out += trq 62 | 63 | trq_net = trq_in * (1. - fracLoss) + trq_out - HPX / (Nmech * self.convert) 64 | outputs['trq_net'] = trq_net 65 | 66 | outputs['trq_in'] = trq_in 67 | outputs['trq_out'] = trq_out 68 | outputs['pwr_in'] = trq_in * Nmech * self.convert 69 | outputs['pwr_out'] = trq_out * Nmech * self.convert 70 | outputs['pwr_net'] = trq_net * Nmech * self.convert 71 | outputs['pwr_in_real'] = trq_in * (1. - fracLoss) * Nmech * self.convert 72 | outputs['pwr_out_real'] = trq_out * Nmech * self.convert - HPX 73 | 74 | def compute_partials(self, inputs, J): 75 | num_ports = self.options['num_ports'] 76 | 77 | PortTrqs = [inputs['trq_%d'%i] for i in range(num_ports)] 78 | 79 | fracLoss = inputs['fracLoss'] 80 | HPX = inputs['HPX'] 81 | Nmech = inputs['Nmech'] 82 | 83 | trq_in = 0 84 | trq_out = 0 85 | 86 | for trq_var in self.trq_vars: 87 | trq = inputs[trq_var] 88 | if trq >= 0: 89 | trq_in += trq 90 | else: 91 | trq_out += trq 92 | 93 | J['trq_net', 'Nmech'] = HPX * Nmech ** (-2.) / self.convert 94 | J['trq_net', 'HPX'] = -1. / (Nmech * self.convert) 95 | J['trq_net', 'fracLoss'] = -trq_in 96 | 97 | J['pwr_in', 'Nmech'] = trq_in * self.convert 98 | 99 | J['pwr_out', 'Nmech'] = trq_out * self.convert 100 | 101 | J['pwr_in_real', 'Nmech'] = trq_in * self.convert * (1 - fracLoss) 102 | J['pwr_in_real', 'fracLoss'] = -trq_in * self.convert * Nmech 103 | 104 | J['pwr_out_real', 'Nmech'] = trq_out * self.convert 105 | J['pwr_out_real', 'HPX'] = -1 106 | 107 | J['pwr_net', 'Nmech'] = trq_in * \ 108 | (1 - fracLoss) * self.convert + trq_out * self.convert 109 | J['pwr_net', 'HPX'] = -1 110 | J['pwr_net', 'fracLoss'] = -trq_in * Nmech * self.convert 111 | 112 | for i in range(num_ports): 113 | trq_var_name = 'trq_%d'%i 114 | 115 | if PortTrqs[i] >= 0: 116 | J['trq_in', trq_var_name]= 1.0 117 | J['trq_out', trq_var_name]= 0.0 118 | J['trq_net', trq_var_name]= 1 - fracLoss 119 | J['pwr_in', trq_var_name]= Nmech * self.convert 120 | J['pwr_out', trq_var_name] = 0.0 121 | J['pwr_in_real', trq_var_name]= Nmech * self.convert * (1 - fracLoss) 122 | J['pwr_out_real', trq_var_name] = 0.0 123 | J['pwr_net', trq_var_name]= Nmech * \ 124 | self.convert * (1 - fracLoss) 125 | 126 | elif PortTrqs[i] < 0: 127 | J['trq_out', trq_var_name] = 1.0 128 | J['trq_in', trq_var_name] = 0.0 129 | J['trq_net', trq_var_name] = 1.0 130 | J['pwr_in', trq_var_name]= 0. 131 | J['pwr_out', trq_var_name] = Nmech * self.convert 132 | J['pwr_in_real', trq_var_name]= 0. 133 | J['pwr_out_real', trq_var_name] = Nmech * self.convert 134 | J['pwr_net', trq_var_name] = Nmech * self.convert 135 | 136 | 137 | if __name__ == "__main__": 138 | from openmdao.api import Problem, Group 139 | 140 | p = Problem() 141 | p.model = Group() 142 | p.model.add_subsystem("shaft", Shaft(10)) 143 | 144 | p.setup() 145 | p.run_model() 146 | 147 | #print(p['shaft.PortTrqs']) 148 | -------------------------------------------------------------------------------- /pycycle/elements/test/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/OpenMDAO/pyCycle/8a031800fb90dea96ea174aedb112e7d5e120b39/pycycle/elements/test/__init__.py -------------------------------------------------------------------------------- /pycycle/elements/test/reg_data/ambient.csv: -------------------------------------------------------------------------------- 1 | alt,MN,dTs,Pt,Ps,Tt,Ts 2 | 0.0,0.0,0.0,14.695951,14.695951,518.67,518.67 3 | 0,0.5,0,17.4329771,14.695951,544.6097206,518.67 4 | 35000,0.8,0,5.272411749,3.457820249,444.4067685,393.8544 5 | 20000,0.4,0,7.540819157,6.753207926,461.6930846,447.3468 6 | 5000,0.2,27,12.57347945,12.22764259,532.0626773,527.8392 7 | 10000,0.25,18,10.555554,10.10631068,507.2773626,501.0084 8 | 50000,1.5,-10,6.178377626,1.68200905,551.3147123,379.97 -------------------------------------------------------------------------------- /pycycle/elements/test/reg_data/combustorJP7.csv: -------------------------------------------------------------------------------- 1 | Fl_I.W,Fl_I.Pt,Fl_I.Tt,Fl_I.ht,Fl_I.s,Fl_I.MN,FAR,eff,Fl_O.MN,Fl_O.Pt,Fl_O.Tt,Fl_O.ht,Fl_O.s,Wfuel,Fl_O.Ps,Fl_O.Ts,Fl_O.hs,Fl_O.rhos,Fl_O.gams 2 | 38.8,158.428,1278.64,181.381769,1.690022166,0.3,0.02673,1,0.2,158.428,2973.240078,176.6596564,1.959495309,1.037124,154.436543,2956.729659,171.467604,0.1408453453,1.279447105 3 | 99.7,422.079,1444.83,224.5905327,1.654579613,0.3096,0.02918,1,0.2,422.079,3237.498083,218.2227917,1.921670693,2.909246,411.5126916,3220.059375,212.6046421,0.344571361,1.271102509 4 | 102.9,437.66,1459.06,228.3306076,1.654670256,0.3098,0.02953,1,0.2,437.66,3267.216324,221.7814028,1.922468719,3.038637,426.7118555,3249.681743,216.1158384,0.3540352431,1.270124567 -------------------------------------------------------------------------------- /pycycle/elements/test/reg_data/compressor.csv: -------------------------------------------------------------------------------- 1 | start.W,start.Pt,start.Tt,start.Fl_O.ht,start.Fl_O.s,start.Fl_O.MN,comp.PRdes,comp.effDes,comp.Fl_O.MN,shaft.Nmech,comp.Fl_O.Pt,comp.Fl_O.Tt,comp.Fl_O.ht,comp.Fl_O.s,comp.pwr,Fl_O.Ps,Fl_O.Ts,Fl_O.hs,Fl_O.rhos,Fl_O.gams,comp.effPoly 2 | 322.11,5.264,444.23,-24.025042,1.664764768,0.6677,1.685,0.8948,0.4201,14705.7,8.86984,524.1328405,-4.871316799,1.668642865,-8729.087084,7.855528585,506.25039,-9.160408414,0.041880322,1.400377629,0.902194264 3 | 52.76,8.828,524.88,-4.692076341,1.669309207,0.3124,1.935,0.9243,0.3059,4666.1,17.08218,642.5431523,23.58953079,1.672666607,-2111.155573,16.01021625,630.7987245,20.76060554,0.068502494,1.398046525,0.930938986 4 | 52.76,16.91,642.67,23.62009137,1.673409235,0.3563,9.369,0.8707,0.2442,4666.1,158.42979,1277.944505,181.2027957,1.689881287,-11763.1789,152.1488229,1264.232593,177.6774425,0.324819361,1.36406597,0.903031838 5 | 847.43,15.521,527.99,-3.945950956,1.632014886,0.6281,1.698,0.9253,0.4005,15859.9,26.354658,621.0552421,18.41485832,1.634721332,-26810.36447,23.59939382,601.8166096,13.78613462,0.105836797,1.398758879,0.930614737 6 | 138.7,26.231,621.41,18.5002462,1.635181543,0.2992,1.812,0.9389,0.3117,4869.9,47.530572,743.0904515,47.886958,1.637611545,-5766.8522,44.44519558,729.1131737,44.49978959,0.164524248,1.394822185,0.943728241 7 | 138.7,47.047,743.14,47.89896728,1.638329301,0.3633,9.524,0.8738,0.2524,4869.9,448.075628,1467.271118,230.4916053,1.654534574,-35832.00338,429.2787235,1450.973125,226.2044101,0.798510905,1.352967566,0.90508207 8 | -------------------------------------------------------------------------------- /pycycle/elements/test/reg_data/compressorOD1.csv: -------------------------------------------------------------------------------- 1 | comp.PRdes,comp.effDes,shaft.Nmech,comp.Fl_I.W,comp.Fl_I.Pt,comp.Fl_I.Tt,comp.Fl_I.ht,comp.Fl_I.s,comp.Fl_I.MN,comp.Fl_I.V,comp.Fl_I.A,comp.Fl_I.Ps,comp.Fl_I.Ts,comp.Fl_I.hs,comp.Fl_O.W,comp.Fl_O.Pt,comp.Fl_O.Tt,comp.Fl_O.ht,comp.Fl_O.s,comp.Fl_O.MN,comp.Fl_O.V,comp.Fl_O.A,comp.Fl_O.Ps,comp.Fl_O.Ts,comp.Fl_O.hs,comp.PR,comp.eff,comp.Nc,comp.Wc,comp.pwr,comp.RlineMap,comp.PRmap,comp.effMap,comp.NcMap,comp.WcMap,comp.s_WcDes,comp.s_PRdes,comp.s_effDes,comp.s_NcDes,comp.SMW,comp.SMN 2 | 8.1,0.942,9150,316.9623153,6.956826354,460.5145509,-20.12340407,1.654266443,0.5,513.4717993,2462.967397,5.864103216,438.5375139,-25.38870773,316.9623153,56.35029347,857.52303,75.76267118,1.660843741,0.2,284.8982643,921.4739487,54.81205928,850.9008137,74.14172037,8.1,0.942,9710.575741,630.9141628,-43000.60237,2,5.2,0.851,1,30,21.03047209,1.69047619,1.10693302,9710.575741,19.18856643,19.99995168 3 | 8.1,0.942,9241.209537,302.5561181,6.469539196,451.0439683,-22.39256083,1.654266462,0.5112067957,519.0017389,2462.967397,5.412293172,428.5884578,-27.77188672,302.5561181,53.91887169,850.6220669,74.07351306,1.661890685,0.1986258815,281.8488175,921.4739487,52.46646082,844.1366128,72.48707667,8.33426772,0.9338517405,9909.801204,640.9057404,-41294.47117,2.018518851,5.338580905,0.8436388865,1.020516339,30.47585072,21.03047209,1.69047619,1.10693302,9710.575741,17.66894475,18.26781055 4 | 8.1,0.942,8723.931613,276.912145,6.469539196,451.0439683,-22.39256083,1.654266462,0.4530826263,462.4629791,2462.967397,5.619169674,433.2149142,-26.66370444,276.912145,46.12663385,805.8545797,63.14212318,1.6593894,0.207095476,286.1550856,921.4739487,44.77585114,799.1420851,61.50683925,7.129817511,0.9522704417,9355.098773,586.5840176,-33511.63561,2.000977375,4.626089232,0.8602782865,0.9633928021,27.8921154,21.03047209,1.69047619,1.10693302,9710.575741,25.45794807,25.6912442 5 | 8.1,0.942,9226.061388,464.4633765,11.2847459,498.4865068,-11.02205955,1.64008267,0.4596552722,492.895542,2462.967397,9.76231423,478.2472504,-15.87382737,464.4633765,82.69238203,896.4826026,85.32048967,1.645437395,0.204569425,297.6864254,921.4739487,80.3360445,889.2803543,83.55075446,7.327801865,0.9507450247,9410.987593,592.9779236,-63311.26261,1.98286204,4.743206737,0.8589002293,0.9691482611,28.1962002,21.03047209,1.69047619,1.10693302,9710.575741,23.6748569,23.87481214 6 | 8.1,0.942,8859.511135,424.9984385,11.2847459,498.4865068,-11.02205955,1.64008267,0.4102877464,441.7793959,2462.967397,10.04935106,482.2280013,-14.91969434,424.9984385,71.18729312,857.050914,75.64707736,1.644673506,0.2128838971,303.0190612,921.4739487,68.99063818,849.5588494,73.81336959,6.308276125,0.9550551498,9037.090246,542.5932471,-52115.05161,2.004008249,4.140107003,0.8627939835,0.9306441232,25.80020959,21.03047209,1.69047619,1.10693302,9710.575741,29.82315252,29.38923676 7 | 8.1,0.942,9285.337383,586.0670299,15.11163595,522.8211991,-5.185972485,1.63148695,0.4388426046,482.7571591,2462.967397,13.23880762,503.4155055,-9.840200711,586.0670299,104.4687074,921.6575407,91.516925,1.636220997,0.2074157282,305.8543733,921.4739487,101.4129665,914.0745015,89.64874113,6.913130235,0.9553409441,9248.40114,572.221976,-80185.92496,1.974735328,4.497908027,0.8630521692,0.952405026,27.20918201,21.03047209,1.69047619,1.10693302,9710.575741,25.91965042,26.21123644 8 | 8.1,0.942,8930.72645,534.4067266,15.11163595,522.8211991,-5.185972485,1.63148695,0.391066769,431.8472536,2462.967397,13.59892448,507.2932595,-8.910323053,534.4067266,89.74232344,883.0626232,82.02398576,1.636116887,0.2157998638,311.6118355,921.4739487,86.90096517,875.1597293,80.08480578,5.938623967,0.9535888524,8895.200818,521.7820786,-65940.06344,2.011856173,3.92143953,0.8614693348,0.9160322781,24.81063594,21.03047209,1.69047619,1.10693302,9710.575741,31.88751917,30.81119788 9 | -------------------------------------------------------------------------------- /pycycle/elements/test/reg_data/duct.csv: -------------------------------------------------------------------------------- 1 | dPqP,Qin,Fl_I.W,Fl_I.V,Fl_I.MN,Fl_I.s,Fl_I.Pt,Fl_I.Tt,Fl_I.ht,Fl_I.rhot,Fl_I.gamt,Fl_O.MN,Fl_O.s,Fl_O.Pt,Fl_O.Tt,Fl_O.ht,Fl_O.rhot,Fl_O.gamt,Fl_O.Ps,Fl_O.Ts,Fl_O.hs,Fl_O.rhos,Fl_O.gams 2 | 0.004825,0,55.4,345.5183252,0.3107,1.668743589,8.875,524.44,-4.797632993,0.045674381,1.400149285,0.3124,1.66907543,8.832178125,524.4399853,-4.797632993,0.045454003,1.400149295,8.254276879,514.3937636,-7.207438971,0.043309519,1.400279592 3 | 0.010125,0,55.4,376.2363784,0.3059,1.672461355,17.09,642.08,23.47793581,0.071837802,1.397739772,0.3563,1.673159538,16.91696375,642.079989,23.47793581,0.071110445,1.397739772,15.49739432,626.258172,19.6673337,0.066789045,1.398165313 4 | 0.005055,0,50.2,780.3518977,0.365,1.908430562,41.522,2049.85,388.6709041,0.054671037,1.325643383,0.3063,1.908778152,41.31210629,2049.849673,388.6709041,0.054394683,1.325643418,38.83761803,2018.940548,380.0555681,0.051919429,1.326796356 5 | 0.010685,0,54.14,745.464231,0.4127,1.914748757,9.504,1444.34,224.4604837,0.017759765,1.353358116,0.4463,1.915485763,9.40244976,1444.339971,224.4604837,0.017570002,1.353358135,8.233755475,1395.00111,211.5425186,0.015930288,1.35618147 6 | 0.014892,0,281.4,495.2175839,0.4499,1.668743589,8.875,524.44,-4.797632993,0.045674381,1.400149285,0.4589,1.669772981,8.7428335,524.4399853,-4.797632993,0.044994199,1.400149295,7.567041486,503.2280338,-9.885154174,0.040584597,1.400412342 7 | 0.014892,25,281.4,495.2175839,0.4499,1.668743589,8.875,524.44,-4.797632993,0.045674381,1.400149285,0.4589,1.669942437,8.7428335,524.8103107,-4.708791486,0.044962449,1.400144289,7.567044419,503.5836031,-9.799892762,0.040555957,1.400408304 8 | -------------------------------------------------------------------------------- /pycycle/elements/test/reg_data/flowstart.csv: -------------------------------------------------------------------------------- 1 | W,MN,V,A,s,Pt,Tt,ht,rhot,gamt,Ps,Ts,hs,rhos,gams 2 | 100.,0.8,778.4815888,780.6801432,1.664686613,5.27,444.23,-24.025042,0.03201863727,1.40093045,3.456238051,393.697602,-36.12787168,0.02369414343,1.401186628 3 | 100.,0.2442,420.2597401,105.5465407,1.690022166,158.428,1278.64,181.381769,0.3344136547,1.363171493,152.1472937,1264.922228,177.8546038,0.3246390067,1.364023048 4 | 100.,0.01,11.16555077,16864.54056,1.631486818,14.697,518.68,-6.179364198,0.07647672914,1.400225285,14.69597861,518.6696023,-6.181853918,0.07647294709,1.400225418 5 | 100.,0.2518,458.8968798,41.07379012,1.654579613,422.079,1444.83,224.5905327,0.7884566888,1.353312789,404.4394862,1428.800922,220.3850056,0.7639810875,1.354220487 6 | 100.,2,1539.504957,1267.526185,1.664686613,5.27,444.23,-24.025042,0.03201863727,1.40093045,0.67428619,246.6152438,-71.35768202,0.007379457919,1.399699486 7 | 100.,0.5,544.8629807,390.4116413,1.631486818,14.697,518.68,-6.179364198,0.07647672914,1.400225285,12.38914009,493.9565518,-12.10813735,0.06769435135,1.400513361 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | 38 | 39 | 40 | 41 | 42 | 43 | 44 | 45 | 46 | 47 | 48 | 49 | 50 | -------------------------------------------------------------------------------- /pycycle/elements/test/reg_data/inlet.csv: -------------------------------------------------------------------------------- 1 | eRamBase,Fl_I.W,Fl_I.V,Fl_I.MN,Fl_I.s,Fl_I.Pt,Fl_I.Tt,Fl_I.ht,Fl_I.rhot,Fl_I.gamt,Fl_O.MN,Fl_O.s,Fl_O.Pt,Fl_O.Tt,Fl_O.ht,Fl_O.rhot,Fl_O.gamt,Fram,Fl_O.Ps,Fl_O.Ts,Fl_O.hs,Fl_O.rhos,Fl_O.gams 2 | 0.999,322.11,778.4815888,0.8,1.664686613,5.27,444.23,-24.025042,0.032018637,1.40093045,0.5,1.664755207,5.26473,444.2299258,-24.025042,0.031986624,1.400930447,7793.756533,4.437746803,423.0251092,-29.1044171,0.028313684,1.401055176 3 | 0.9937,796.93,11.16555077,0.01,1.631486818,14.697,518.68,-6.179364198,0.076476729,1.400225285,0.5,1.631920408,14.6044089,518.6799816,-6.179364198,0.075994928,1.400225289,276.5633376,12.31108889,493.9565517,-12.10813739,0.067267879,1.400513361 4 | 0.9915,847.43,334.9584708,0.3,1.631438424,15.653,528,-3.943551752,0.080013594,1.40010055,0.5,1.632024074,15.5199495,527.9999845,-3.943551752,0.07933348,1.400100553,8822.447462,13.08300361,502.8386733,-9.978517613,0.070222907,1.400416749 5 | -------------------------------------------------------------------------------- /pycycle/elements/test/reg_data/nozzle.csv: -------------------------------------------------------------------------------- 1 | Cfg,PsExh,Fl_I.W,Fl_I.MN,Fl_I.s,Fl_I.Pt,Fl_I.Tt,Fl_I.ht,Fl_I.rhot,Fl_I.gamt,Fl_O.MN,Fl_O.s,Fl_O.Pt,Fl_O.Tt,Fl_O.ht,Fl_O.rhot,Fl_O.gamt,Fl_O.Ps,Fg,Vactual,Ath,AR,PR 2 | 0.9884,3.457820249,51.45,0.4463,1.916338161,9.262,1443.35,224.2005337,0.0173194199,1.353413879,1.0,1.916338156,9.262,1443.349966,224.2005337,0.01731942031,1.353413881,4.94328977,3276.493418,1693.381407,400.8253537,1.0,2.678566071 3 | 0.9927,3.457820249,268.0,0.4589,1.670004383,8.739,524.88,-4.692076333,0.04493676743,1.400143335,1.0,1.670004395,8.739,524.8799855,-4.692076333,0.04493676867,1.400143345,4.615124769,10017.09879,1025.458884,1321.125808,1.0,2.527314715 4 | 0.9925,14.695951,132.15,0.4313,1.874505303,25.873,1608.0,267.8403035,0.043427133,1.344679719,0.9479017979,1.874505303,25.873,1607.999997,267.8403035,0.04342713307,1.34467972,14.695951,6948.085577,1704.406551,390.8229334,1.0,1.760552958 5 | 0.9926,14.695951,662.01,0.4285,1.635834599,24.526,611.28,16.06250198,0.1082895753,1.398538201,0.8875874224,1.635834597,24.526,611.2799938,16.06250198,0.1082895764,1.398538202,14.695951,20423.52541,999.9944929,1269.369205,1.0,1.668895058 6 | -------------------------------------------------------------------------------- /pycycle/elements/test/reg_data/shaft.csv: -------------------------------------------------------------------------------- 1 | trqLoad1,trqLoad2,trqLoad3,Nmech,HPX,fracLoss,trqIn,trqOut,trqNet,pwrIn,pwrOut,pwrNet 2 | -9924.5,-2378.7,12302.7,4666.1,0,0,12302.7,-12303.2,-0.5,10930.0061,-10930.45031,-0.444211681 3 | -27365.6,-5779.9,33145.4,4812.5,0,0,33145.4,-33145.5,-0.1,30371.059,-30371.15063,-0.091629786 4 | 0,-3963,4052.3,14705.7,250,0,4052.3,-3963,0.012969766,11346.27277,-11096.23646,0.03631481 5 | 0,-10698.7,10782.1,15726.5,250,0,10782.1,-10698.7,-0.091449497,32285.04256,-32035.31638,-0.273828928 6 | 0,-10698.7,10782.1,15726.5,250,0.01,10782.1,-10698.7,-107.9124495,32285.04256,-32035.31638,-323.1242545 -------------------------------------------------------------------------------- /pycycle/elements/test/reg_data/splitter.csv: -------------------------------------------------------------------------------- 1 | BPR,Fl_I.W,Fl_I.V,Fl_I.MN,Fl_I.s,Fl_I.Pt,Fl_I.Tt,Fl_I.ht,Fl_I.rhot,Fl_I.gamt,Fl_O1.MN,Fl_O1.s,Fl_O1.Pt,Fl_O1.Tt,Fl_O1.ht,Fl_O1.rhot,Fl_O1.gamt,Fl_O1.Ps,Fl_O1.Ts,Fl_O1.hs,Fl_O1.rhos,Fl_O1.gams,Fl_O1.W,Fl_O2.MN,Fl_O2.s,Fl_O2.Pt,Fl_O2.Tt,Fl_O2.ht,Fl_O2.rhot,Fl_O2.gamt,Fl_O2.Ps,Fl_O2.Ts,Fl_O2.hs,Fl_O2.rhos,Fl_O2.gams,Fl_O2.W 2 | 5.105,338.22,503.567421,0.4578,1.668743589,8.875,524.44,-4.797632993,0.045674381,1.400149285,0.3107,1.6687436,8.875,524.4399853,-4.797632993,0.045674382,1.400149295,8.30033374,514.5007587,-7.181776752,0.043542119,1.400278259,55.4004914,0.4529,1.6687436,8.875,524.4399853,-4.797632993,0.045674382,1.400149295,7.709739653,503.7575556,-9.75818074,0.041306472,1.400406324,282.8195086 3 | 8,100,534.9884574,0.5,1.656330819,9,500,-10.65917719,0.04858169,1.40044844,0.3,1.656330837,9,499.999976,-10.65917719,0.048581692,1.400448456,8.455030587,491.1480335,-12.78144634,0.046462526,1.400542378,11.11111111,0.4,1.656330837,9,499.999976,-10.65917719,0.048581692,1.400448456,8.060164638,484.4753111,-14.3810161,0.044902681,1.400608468,88.88888889 4 | 3,500,602.8131976,0.6,1.671388635,5,450,-22.64269291,0.029988698,1.400891614,0.4,1.6713886,5,449.9999434,-22.64269291,0.029988701,1.400891611,4.477747135,436.0143022,-25.99313702,0.027717806,1.400982008,125,0.5,1.6713886,5,449.9999434,-22.64269291,0.029988701,1.400891611,4.214614501,428.5213751,-27.78795483,0.026545161,1.401025415,375 5 | 0.9,200,561.0147223,0.5,1.687292341,8,550,1.33623523,0.039257931,1.399768328,0.35,1.687292349,8,549.9999915,1.33623523,0.039257932,1.399768334,7.350196468,536.850346,-1.819925991,0.036952657,1.399973418,105.2631579,0.45,1.687292349,8,549.9999915,1.33623523,0.039257932,1.399768334,6.962079481,528.5929852,-3.801277473,0.035548189,1.400092299,94.73684211 -------------------------------------------------------------------------------- /pycycle/elements/test/reg_data/turbine.csv: -------------------------------------------------------------------------------- 1 | Fl_I.W,Fl_I.Pt,Fl_I.Tt,Fl_I.ht,Fl_I.s,Fl_I.MN,Fl_O.Pt,Fl_O.Tt,Fl_O.ht,Fl_O.s,Fl_O.MN,PRdes,EffDes,Power,Nmech,Fl_O.Ps,Fl_O.Ts,Fl_O.hs,Fl_O.rhos,Fl_O.gams,effPoly 2 | 39.84,149.869,2856.85,621.2884303,1.915955031,0.1025,41.42316197,2190.357254,428.117932,1.927218221,0.365,3.618,0.8888,10888.58626,14705.7,37.96612501,2144.412539,415.1683751,0.047784707,1.322241601,0.8722458 3 | 47.7,41.219,2053.59,389.7148812,1.909442213,0.3063,9.361571656,1476.477344,232.9152493,1.921578073,0.4127,4.403,0.8996,10582.17121,4666.1,8.356245792,1433.377474,221.5837115,0.015734425,1.353977475,0.8805839 4 | 108.31,423.865,3152.42,710.1476523,1.874240943,0.1039,117.7075812,2422.889583,494.3924382,1.88433936,0.37,3.601,0.8998,33062.92119,15859.9,107.6809079,2371.909128,479.7590155,0.122530256,1.314612828,0.8850356 5 | 129.62,117.119,2282.7,454.2925546,1.867624209,0.3103,27.58987044,1660.406783,281.8968232,1.878705207,0.403,4.245,0.9054,31616.21612,4869.9,24.77368796,1615.349512,269.8067191,0.041392732,1.344314749,0.8882065 6 | -------------------------------------------------------------------------------- /pycycle/elements/test/reg_data/turbineOD1.csv: -------------------------------------------------------------------------------- 1 | turb.PRdes,turb.effDes,shaft.Nmech,burn.FAR,burn.Fl_I.W,burn.Fl_I.Pt,burn.Fl_I.Tt,burn.Fl_I.ht,burn.Fl_I.s,burn.Fl_I.MN,burn.Fl_I.V,burn.Fl_I.A,burn.Fl_I.Ps,burn.Fl_I.Ts,burn.Fl_I.hs,turb.Fl_I.W,turb.Fl_I.Pt,turb.Fl_I.Tt,turb.Fl_I.ht,turb.Fl_I.s,turb.Fl_I.MN,turb.Fl_I.V,turb.Fl_I.A,turb.Fl_I.Ps,turb.Fl_I.Ts,turb.Fl_I.hs,turb.Fl_O.W,turb.Fl_O.Pt,turb.Fl_O.Tt,turb.Fl_O.ht,turb.Fl_O.s,turb.Fl_O.MN,turb.Fl_O.V,turb.Fl_O.A,turb.Fl_O.Ps,turb.Fl_O.Ts,turb.Fl_O.hs,turb.PR,turb.eff,turb.Np,turb.Wp,turb.pwr,turb.PRmap,turb.effMap,turb.NpMap,turb.WpMap,turb.s_WpDes,turb.s_PRdes,turb.s_effDes,turb.s_NpDes 2 | 1.73736648,0.923,9150,0.03,332,56.35029345,857.5228531,75.76262786,1.660843688,0.2,284.8982329,965.19143,54.81206118,850.900637,74.14167741,341.96,56.35029345,2826.459623,73.55594938,2.017611699,0.2,497.5606032,1878.203329,54.92835946,2810.647508,68.61190682,341.96,32.43431602,2525.358965,-19.53776412,2.020704596,0.39,913.1693866,1678.918616,29.4319236,2470.732099,-36.1907808,1.73736648,0.923,172.1074624,322.6268585,45040.89681,6,0.9276,100,149.898,2.152309293,0.147473296,0.9950409659,1.721074624 3 | 1.73736648,0.923,8724.503686,0.025,291.0645769,46.40188618,806.7204256,63.35312818,1.659243097,0.2066802449,285.735344,965.19143,45.04841941,800.0281161,61.72263811,298.3411913,46.40188618,2502.112521,61.80792993,1.988973554,0.1983447057,466.4318484,1878.203329,45.23912648,2487.738244,57.46316161,298.3411913,26.45006895,2222.360764,-21.84746594,1.992097841,0.3895645599,859.4080052,1678.918616,23.98508001,2172.353059,-36.59736402,1.754320046,0.9237808656,174.4163976,321.6110806,35311.69335,6.114960244,0.9283847572,101.341566,149.4216056,2.152309293,0.147473296,0.9950409659,1.721074624 4 | 1.73736648,0.923,9054.347074,0.027,468.7460441,77.46707627,877.4505529,80.64675859,1.644642618,0.2082667212,299.8972578,965.19143,75.17900368,870.126779,78.85063912,481.4021873,77.46707627,2672.993225,78.52654195,1.975747236,0.1986293525,481.6672294,1878.203329,75.52951168,2657.918594,73.89330588,481.4021873,44.24182369,2379.380381,-10.73223761,1.978825574,0.3897748113,887.7851922,1678.918616,40.13280177,2326.879905,-26.47228457,1.750991931,0.9245669402,175.1290569,321.2845362,60795.35094,6.092392664,0.9291747494,101.7556441,149.2743488,2.152309293,0.147473296,0.9950409659,1.721074624 5 | 1.73736648,0.923,8777.426301,0.022,536.136246,83.63668797,865.5497154,77.72880142,1.636034822,0.2197209644,314.1630235,965.19143,80.89181766,857.5028706,75.75773906,547.9312434,83.63668797,2371.056091,76.05557869,1.929612858,0.1961951373,450.1396241,1878.203329,81.57620028,2357.458582,72.00903049,547.9312434,47.08608981,2095.449762,-5.074804276,1.93266688,0.3893399478,835.8372699,1678.918616,42.68329126,2047.385646,-19.02671519,1.776250445,0.9272858886,180.2587178,319.0069557,62895.7003,6.263667837,0.9319072483,104.736143,148.2153012,2.152309293,0.147473296,0.9950409659,1.721074624 -------------------------------------------------------------------------------- /pycycle/elements/test/test_ambient.py: -------------------------------------------------------------------------------- 1 | import numpy as np 2 | import unittest 3 | import os 4 | 5 | from openmdao.api import Problem 6 | from openmdao.utils.assert_utils import assert_check_partials 7 | 8 | 9 | from pycycle.elements.ambient import Ambient 10 | 11 | 12 | fpath = os.path.dirname(os.path.realpath(__file__)) 13 | ref_data = np.loadtxt(fpath + "/reg_data/ambient.csv", 14 | delimiter=",", skiprows=1) 15 | 16 | header = ['alt','MN','dTs','Pt','Ps','Tt','Ts'] 17 | 18 | h_map = dict(((v_name,i) for i,v_name in enumerate(header))) 19 | 20 | class FlowStartTestCase(unittest.TestCase): 21 | 22 | def setUp(self): 23 | 24 | self.prob = Problem() 25 | 26 | self.prob.model.add_subsystem('amb', Ambient()) 27 | self.prob.model.set_input_defaults('amb.alt', 0, units='ft') 28 | self.prob.model.set_input_defaults('amb.dTs', 0, units='degR') 29 | 30 | self.prob.setup(check=False, force_alloc_complex=True) 31 | 32 | def test_case1(self): 33 | old = np.seterr(divide='raise') 34 | try: 35 | # 6 cases to check against 36 | for i, data in enumerate(ref_data): 37 | self.prob['amb.alt'] = data[h_map['alt']] 38 | self.prob['amb.dTs'] = data[h_map['dTs']] 39 | 40 | self.prob.run_model() 41 | 42 | # check outputs 43 | tol = 1.0e-2 # seems a little generous 44 | 45 | npss = data[h_map['Ps']] 46 | pyc = self.prob['amb.Ps'] 47 | rel_err = abs(npss - pyc)/npss 48 | self.assertLessEqual(rel_err, tol) 49 | 50 | npss = data[h_map['Ts']] 51 | pyc = self.prob['amb.Ts'] 52 | rel_err = abs(npss - pyc)/npss 53 | self.assertLessEqual(rel_err, tol) 54 | 55 | partial_data = self.prob.check_partials(out_stream=None, method='cs', 56 | includes=['amb.*'], excludes=['*.base_thermo.*', 'amb.readAtmTable']) 57 | assert_check_partials(partial_data, atol=1e-8, rtol=1e-8) 58 | finally: 59 | np.seterr(**old) 60 | 61 | 62 | if __name__ == "__main__": 63 | unittest.main() -------------------------------------------------------------------------------- /pycycle/elements/test/test_bleed_out.py: -------------------------------------------------------------------------------- 1 | """ Tests the duct component. """ 2 | 3 | import unittest 4 | import os 5 | 6 | import numpy as np 7 | 8 | from openmdao.api import Problem, Group 9 | from openmdao.utils.assert_utils import assert_near_equal, assert_check_partials 10 | 11 | from pycycle.mp_cycle import Cycle 12 | from pycycle.elements.bleed_out import BleedOut 13 | from pycycle.elements.flow_start import FlowStart 14 | from pycycle import constants 15 | from pycycle.thermo.cea import species_data 16 | 17 | 18 | class BleedOutTestCase(unittest.TestCase): 19 | 20 | def test_case1(self): 21 | 22 | self.prob = Problem() 23 | cycle = self.prob.model = Cycle() 24 | cycle.options['thermo_method'] = 'CEA' 25 | cycle.options['thermo_data'] = species_data.janaf 26 | 27 | cycle.add_subsystem('flow_start', FlowStart(), promotes=['MN', 'P', 'T']) 28 | cycle.add_subsystem('bleed', BleedOut(bleed_names=['bld1', 'bld2']), promotes=['MN']) 29 | 30 | cycle.pyc_connect_flow('flow_start.Fl_O', 'bleed.Fl_I') 31 | 32 | cycle.set_input_defaults('MN', 0.5) 33 | cycle.set_input_defaults('bleed.bld1:frac_W', 0.1) 34 | cycle.set_input_defaults('bleed.bld2:frac_W', 0.1) 35 | cycle.set_input_defaults('P', 17., units='psi') 36 | cycle.set_input_defaults('T', 500., units='degR') 37 | cycle.set_input_defaults('flow_start.W', 500., units='lbm/s') 38 | 39 | self.prob.setup(check=False, force_alloc_complex=True) 40 | self.prob.set_solver_print(level=-1) 41 | 42 | self.prob.run_model() 43 | 44 | tol = 2.0e-5 45 | 46 | Tt_in = self.prob.get_val('bleed.Fl_I:tot:T', units='degR') 47 | Pt_in = self.prob.get_val('bleed.Fl_I:tot:P', units='psi') 48 | W_in = self.prob['bleed.Fl_I:stat:W'] 49 | 50 | assert_near_equal(self.prob['bleed.Fl_O:tot:T'], Tt_in, tol) 51 | assert_near_equal(self.prob['bleed.bld1:tot:T'], Tt_in, tol) 52 | assert_near_equal(self.prob['bleed.bld2:tot:T'], Tt_in, tol) 53 | 54 | assert_near_equal(self.prob['bleed.Fl_O:tot:P'], Pt_in, tol) 55 | assert_near_equal(self.prob['bleed.bld1:tot:P'], Pt_in, tol) 56 | assert_near_equal(self.prob['bleed.bld2:tot:P'], Pt_in, tol) 57 | 58 | assert_near_equal(self.prob['bleed.Fl_O:stat:W'], W_in*0.8, tol) 59 | assert_near_equal(self.prob['bleed.bld1:stat:W'], W_in*0.1, tol) 60 | assert_near_equal(self.prob['bleed.bld2:stat:W'], W_in*0.1, tol) 61 | 62 | partial_data = self.prob.check_partials(out_stream=None, method='cs', 63 | includes=['bleed.*'], excludes=['*.base_thermo.*',]) 64 | assert_check_partials(partial_data, atol=1e-8, rtol=1e-8) 65 | 66 | if __name__ == "__main__": 67 | unittest.main() 68 | -------------------------------------------------------------------------------- /pycycle/elements/test/test_cfd_start.py: -------------------------------------------------------------------------------- 1 | import unittest 2 | import numpy as np 3 | 4 | import openmdao.api as om 5 | from openmdao.utils.assert_utils import assert_near_equal 6 | 7 | 8 | from pycycle.thermo.cea.species_data import janaf 9 | from pycycle.elements.cfd_start import CFDStart 10 | from pycycle.mp_cycle import Cycle 11 | 12 | 13 | class CFDStartTestCase(unittest.TestCase): 14 | 15 | def test_case1(self): 16 | 17 | p = om.Problem() 18 | 19 | p.model = Cycle() 20 | p.model.options['thermo_method'] = 'CEA' 21 | p.model.options['thermo_data'] = janaf 22 | 23 | cfd_start = p.model.add_subsystem('cfd_start', CFDStart(), promotes=['*']) 24 | 25 | p.model.set_input_defaults('Ps', units='kPa', val=100) 26 | p.model.set_input_defaults('V', units='m/s', val=100.) 27 | p.model.set_input_defaults('area', units='m**2', val=1) 28 | p.model.set_input_defaults('W', units='kg/s', val=100.) 29 | 30 | p.setup() 31 | p.run_model() 32 | 33 | 34 | tol = 1e-4 35 | assert_near_equal(p['Fl_O:tot:P'], 15.24202341, tol) # psi 36 | assert_near_equal(p.get_val('Fl_O:stat:P', units='kPa'), 100, tol) 37 | assert_near_equal(p.get_val('Fl_O:stat:MN'), 0.26744049, tol) 38 | 39 | 40 | if __name__ == "__main__": 41 | 42 | unittest.main() -------------------------------------------------------------------------------- /pycycle/elements/test/test_combustor.py: -------------------------------------------------------------------------------- 1 | """ Tests the duct component. """ 2 | 3 | import unittest 4 | import os 5 | 6 | import numpy as np 7 | 8 | from openmdao.api import Problem, Group, IndepVarComp 9 | from openmdao.utils.assert_utils import assert_near_equal, assert_check_partials 10 | 11 | from pycycle.thermo.thermo import Thermo 12 | from pycycle.thermo.cea import species_data 13 | from pycycle.elements.combustor import Combustor 14 | from pycycle.elements.flow_start import FlowStart 15 | from pycycle.mp_cycle import Cycle 16 | 17 | 18 | fpath = os.path.dirname(os.path.realpath(__file__)) 19 | ref_data = np.loadtxt(fpath + "/reg_data/combustorJP7.csv", 20 | delimiter=",", skiprows=1) 21 | 22 | header = ['Fl_I.W', 'Fl_I.Pt', 'Fl_I.Tt', 'Fl_I.ht', 'Fl_I.s', 'Fl_I.MN', 'FAR', 'eff', 'Fl_O.MN', 23 | 'Fl_O.Pt', 'Fl_O.Tt', 'Fl_O.ht', 'Fl_O.s', 'Wfuel', 'Fl_O.Ps', 'Fl_O.Ts', 'Fl_O.hs', 24 | 'Fl_O.rhos', 'Fl_O.gams'] 25 | 26 | h_map = dict(((v_name, i) for i, v_name in enumerate(header))) 27 | 28 | 29 | class BurnerTestCase(unittest.TestCase): 30 | 31 | def test_case1(self): 32 | 33 | prob = Problem() 34 | model = prob.model = Cycle() 35 | model.options['thermo_method'] = 'CEA' 36 | model.options['thermo_data'] = species_data.janaf 37 | 38 | model.add_subsystem('ivc', IndepVarComp('in_composition', [3.23319235e-04, 1.10132233e-05, 39 | 5.39157698e-02, 1.44860137e-02])) 40 | 41 | model.add_subsystem('flow_start', FlowStart()) 42 | model.add_subsystem('combustor', Combustor()) 43 | 44 | model.pyc_connect_flow('flow_start.Fl_O', 'combustor.Fl_I') 45 | 46 | # model.set_input_defaults('Fl_I:tot:P', 100.0, units='lbf/inch**2') 47 | # model.set_input_defaults('Fl_I:tot:h', 100.0, units='Btu/lbm') 48 | # model.set_input_defaults('Fl_I:stat:W', 100.0, units='lbm/s') 49 | model.set_input_defaults('combustor.Fl_I:FAR', 0.0) 50 | model.set_input_defaults('combustor.MN', 0.5) 51 | 52 | 53 | # needed because composition is sized by connection 54 | # model.connect('ivc.in_composition', ['Fl_I:tot:composition', 'Fl_I:stat:composition', ]) 55 | 56 | 57 | prob.set_solver_print(level=2) 58 | prob.setup(check=False, force_alloc_complex=True) 59 | 60 | # 6 cases to check against 61 | for i, data in enumerate(ref_data): 62 | 63 | # input flowstation 64 | # prob['Fl_I:tot:P'] = data[h_map['Fl_I.Pt']] 65 | # prob['Fl_I:tot:h'] = data[h_map['Fl_I.ht']] 66 | # prob['Fl_I:stat:W'] = data[h_map['Fl_I.W']] 67 | # prob['Fl_I:FAR'] = data[h_map['FAR']] 68 | prob.set_val('flow_start.P', data[h_map['Fl_I.Pt']], units='psi') 69 | prob.set_val('flow_start.T', data[h_map['Fl_I.Tt']], units='degR') 70 | prob.set_val('flow_start.W', data[h_map['Fl_I.W']], units='lbm/s') 71 | prob['combustor.Fl_I:FAR'] = data[h_map['FAR']] 72 | prob['combustor.MN'] = data[h_map['Fl_O.MN']] 73 | 74 | prob.run_model() 75 | 76 | # prob.model.combustor.mix_fuel.list_inputs(print_arrays=True) 77 | # prob.model.combustor.mix_fuel.list_outputs(print_arrays=True) 78 | # print(prob['combustor.Fl_I:tot:composition']) 79 | # print(prob['combustor.Fl_I:tot:n']) 80 | print(prob['combustor.Fl_I:tot:h']) 81 | print(prob['combustor.Fl_I:tot:P']) 82 | # exit() 83 | 84 | # check outputs 85 | tol = 1.0e-2 86 | 87 | npss = data[h_map['Fl_O.Pt']] 88 | pyc = prob['combustor.Fl_O:tot:P'] 89 | rel_err = abs(npss - pyc) / npss 90 | print('Pt out:', npss, pyc, rel_err) 91 | self.assertLessEqual(rel_err, tol) 92 | 93 | npss = data[h_map['Fl_O.Tt']] 94 | pyc = prob['combustor.Fl_O:tot:T'] 95 | rel_err = abs(npss - pyc) / npss 96 | print('Tt out:', npss, pyc, rel_err) 97 | self.assertLessEqual(rel_err, tol) 98 | 99 | npss = data[h_map['Fl_O.ht']] 100 | pyc = prob['combustor.Fl_O:tot:h'] 101 | rel_err = abs(npss - pyc) / npss 102 | print('ht out:', npss, pyc, rel_err) 103 | self.assertLessEqual(rel_err, tol) 104 | 105 | npss = data[h_map['Fl_O.Ps']] 106 | pyc = prob['combustor.Fl_O:stat:P'] 107 | rel_err = abs(npss - pyc) / npss 108 | print('Ps out:', npss, pyc, rel_err) 109 | self.assertLessEqual(rel_err, tol) 110 | 111 | npss = data[h_map['Fl_O.Ts']] 112 | pyc = prob['combustor.Fl_O:stat:T'] 113 | rel_err = abs(npss - pyc) / npss 114 | print('Ts out:', npss, pyc, rel_err) 115 | self.assertLessEqual(rel_err, tol) 116 | 117 | npss = data[h_map['Wfuel']] 118 | pyc = prob['combustor.Fl_I:stat:W'] * (prob['combustor.Fl_I:FAR']) 119 | rel_err = abs(npss - pyc) / npss 120 | print('Wfuel:', npss, pyc, rel_err) 121 | self.assertLessEqual(rel_err, tol) 122 | 123 | print('') 124 | 125 | partial_data = prob.check_partials(out_stream=None, method='cs', 126 | includes=['combustor.*',], excludes=['*.base_thermo.*', '*.mix_fuel.*']) 127 | assert_check_partials(partial_data, atol=1e-8, rtol=1e-8) 128 | 129 | 130 | if __name__ == "__main__": 131 | unittest.main() 132 | -------------------------------------------------------------------------------- /pycycle/elements/test/test_compressor.py: -------------------------------------------------------------------------------- 1 | import numpy as np 2 | import unittest 3 | import os 4 | 5 | from openmdao.api import Problem 6 | from openmdao.utils.assert_utils import assert_near_equal, assert_check_partials 7 | 8 | 9 | from pycycle.mp_cycle import Cycle 10 | from pycycle.thermo.cea.species_data import janaf 11 | from pycycle.elements.compressor import Compressor 12 | from pycycle.elements.flow_start import FlowStart 13 | from pycycle import constants 14 | 15 | fpath = os.path.dirname(os.path.realpath(__file__)) 16 | ref_data = np.loadtxt(fpath + "/reg_data/compressor.csv", 17 | delimiter=",", skiprows=1) 18 | 19 | header = [ 20 | 'start.W', 21 | 'start.Pt', 22 | 'start.Tt', 23 | 'start.Fl_O.ht', 24 | 'start.Fl_O.s', 25 | 'start.Fl_O.MN', 26 | 'comp.PRdes', 27 | 'comp.effDes', 28 | 'comp.Fl_O.MN', 29 | 'shaft.Nmech', 30 | 'comp.Fl_O.Pt', 31 | 'comp.Fl_O.Tt', 32 | 'comp.Fl_O.ht', 33 | 'comp.Fl_O.s', 34 | 'comp.pwr', 35 | 'Fl_O.Ps', 36 | 'Fl_O.Ts', 37 | 'Fl_O.hs', 38 | 'Fl_O.rhos', 39 | 'Fl_O.gams', 40 | 'comp.effPoly'] 41 | 42 | 43 | h_map = dict(((v_name, i) for i, v_name in enumerate(header))) 44 | 45 | 46 | class CompressorTestCase(unittest.TestCase): 47 | 48 | def setUp(self): 49 | 50 | self.prob = Problem() 51 | cycle = self.prob.model = Cycle() 52 | cycle.options['thermo_method'] = 'CEA' 53 | cycle.options['thermo_data'] = janaf 54 | 55 | cycle.add_subsystem('flow_start', FlowStart(thermo_data=janaf)) 56 | cycle.add_subsystem('compressor', Compressor(design=True)) 57 | 58 | cycle.set_input_defaults('flow_start.P', 17., units='psi') 59 | cycle.set_input_defaults('flow_start.T', 500., units='degR') 60 | cycle.set_input_defaults('compressor.MN', 0.5) 61 | cycle.set_input_defaults('flow_start.W', 10., units='lbm/s') 62 | cycle.set_input_defaults('compressor.PR', 6.) 63 | cycle.set_input_defaults('compressor.eff', 0.9) 64 | 65 | cycle.pyc_connect_flow("flow_start.Fl_O", "compressor.Fl_I") 66 | 67 | self.prob.set_solver_print(level=-1) 68 | self.prob.setup(check=False, force_alloc_complex=True) 69 | 70 | def test_case1(self): 71 | old = np.seterr(divide='raise') 72 | try: 73 | # 6 cases to check against 74 | for i, data in enumerate(ref_data): 75 | self.prob['compressor.PR'] = data[h_map['comp.PRdes']] 76 | self.prob['compressor.eff'] = data[h_map['comp.effDes']] 77 | self.prob['compressor.MN'] = data[h_map['comp.Fl_O.MN']] 78 | 79 | # input flowstation 80 | self.prob['flow_start.P'] = data[h_map['start.Pt']] 81 | self.prob['flow_start.T'] = data[h_map['start.Tt']] 82 | self.prob['flow_start.W'] = data[h_map['start.W']] 83 | self.prob.run_model() 84 | 85 | tol = 1e-3 86 | 87 | npss = data[h_map['comp.Fl_O.Pt']] 88 | pyc = self.prob['compressor.Fl_O:tot:P'][0] 89 | print('Pt out:', npss, pyc) 90 | assert_near_equal(pyc, npss, tol) 91 | 92 | npss = data[h_map['comp.Fl_O.Tt']] 93 | pyc = self.prob['compressor.Fl_O:tot:T'][0] 94 | print('Tt out:', npss, pyc) 95 | assert_near_equal(pyc, npss, tol) 96 | 97 | npss = data[h_map['comp.Fl_O.ht']] - data[h_map['start.Fl_O.ht']] 98 | pyc = self.prob['compressor.Fl_O:tot:h'] - self.prob['flow_start.Fl_O:tot:h'] 99 | print('delta h:', npss, pyc) 100 | assert_near_equal(pyc, npss, tol) 101 | 102 | npss = data[h_map['start.Fl_O.s']] 103 | pyc = self.prob['flow_start.Fl_O:tot:S'][0] 104 | print('S in:', npss, pyc) 105 | assert_near_equal(pyc, npss, tol) 106 | 107 | npss = data[h_map['comp.Fl_O.s']] 108 | pyc = self.prob['compressor.Fl_O:tot:S'][0] 109 | print('S out:', npss, pyc) 110 | assert_near_equal(pyc, npss, tol) 111 | 112 | npss = data[h_map['comp.pwr']] 113 | pyc = self.prob['compressor.power'][0] 114 | print('Power:', npss, pyc) 115 | assert_near_equal(pyc, npss, tol) 116 | 117 | npss = data[h_map['Fl_O.Ps']] 118 | pyc = self.prob['compressor.Fl_O:stat:P'][0] 119 | print('Ps out:', npss, pyc) 120 | assert_near_equal(pyc, npss, tol) 121 | 122 | npss = data[h_map['Fl_O.Ts']] 123 | pyc = self.prob['compressor.Fl_O:stat:T'][0] 124 | print('Ts out:', npss, pyc) 125 | assert_near_equal(pyc, npss, tol) 126 | 127 | npss = data[h_map['comp.effPoly']] 128 | pyc = self.prob['compressor.eff_poly'][0] 129 | print('effPoly:', npss, pyc) 130 | assert_near_equal(pyc, npss, tol) 131 | print("") 132 | 133 | partial_data = self.prob.check_partials(out_stream=None, method='cs', 134 | includes=['compressor.*'], excludes=['*.base_thermo.*',]) 135 | assert_check_partials(partial_data, atol=1e-8, rtol=1e-8) 136 | finally: 137 | np.seterr(**old) 138 | 139 | if __name__ == "__main__": 140 | unittest.main() 141 | -------------------------------------------------------------------------------- /pycycle/elements/test/test_flightconditions.py: -------------------------------------------------------------------------------- 1 | import numpy as np 2 | import unittest 3 | import os 4 | 5 | import openmdao.api as om 6 | from openmdao.utils.assert_utils import assert_near_equal, assert_check_partials 7 | 8 | from pycycle.thermo.cea.species_data import janaf 9 | from pycycle.elements.flight_conditions import FlightConditions 10 | from pycycle.mp_cycle import Cycle 11 | 12 | 13 | fpath = os.path.dirname(os.path.realpath(__file__)) 14 | ref_data = np.loadtxt(fpath + "/reg_data/ambient.csv", 15 | delimiter=",", skiprows=1) 16 | 17 | header = ['alt', 'MN', 'dTs', 'Pt', 'Ps', 'Tt', 'Ts'] 18 | 19 | h_map = dict(((v_name, i) for i, v_name in enumerate(header))) 20 | 21 | 22 | class FlightConditionsTestCase(unittest.TestCase): 23 | 24 | def setUp(self): 25 | self.prob = om.Problem() 26 | 27 | self.prob.model = Cycle() 28 | self.prob.model.options['thermo_method'] = 'CEA' 29 | self.prob.model.options['thermo_data'] = janaf 30 | 31 | self.prob.model.set_input_defaults('fc.MN', 0.0) 32 | self.prob.model.set_input_defaults('fc.alt', 0.0, units="ft") 33 | self.prob.model.set_input_defaults('fc.dTs', 0.0, units='degR') 34 | 35 | fc = self.prob.model.add_subsystem('fc', FlightConditions()) 36 | 37 | self.prob.setup(check=False, force_alloc_complex=True) 38 | self.prob.set_solver_print(level=-1) 39 | 40 | def test_case1(self): 41 | 42 | # 6 cases to check against 43 | for i, data in enumerate(ref_data): 44 | self.prob['fc.alt'] = data[h_map['alt']] 45 | self.prob['fc.MN'] = data[h_map['MN']] 46 | self.prob['fc.dTs'] = data[h_map['dTs']] 47 | 48 | if self.prob['fc.MN'] < 1e-10: 49 | self.prob['fc.MN'] += 1e-6 50 | 51 | self.prob.run_model() 52 | 53 | # check outputs 54 | Pt = data[h_map['Pt']] 55 | Pt_c = self.prob['fc.Fl_O:tot:P'] 56 | 57 | Ps = data[h_map['Ps']] 58 | Ps_c = self.prob['fc.Fl_O:stat:P'] 59 | 60 | Tt = data[h_map['Tt']] 61 | Tt_c = self.prob['fc.Fl_O:tot:T'] 62 | 63 | Ts = data[h_map['Ts']] 64 | Ts_c = self.prob['fc.Fl_O:stat:T'] 65 | 66 | tol = 1e-4 67 | assert_near_equal(Pt_c, Pt, tol) 68 | assert_near_equal(Ps_c, Ps, tol) 69 | assert_near_equal(Tt_c, Tt, tol) 70 | assert_near_equal(Ps_c, Ps, tol) 71 | 72 | # NOTE: CHeck partials not needed, since no new components are here 73 | # partial_data = self.prob.check_partials(out_stream=None, method='cs', 74 | # includes=['fc.*'], excludes=['*.base_thermo.*', 'fc.ambient.readAtmTable', '*.exit_static*']) 75 | # assert_check_partials(partial_data, atol=1e-8, rtol=1e-8) 76 | 77 | if __name__ == "__main__": 78 | unittest.main() 79 | -------------------------------------------------------------------------------- /pycycle/elements/test/test_inlet.py: -------------------------------------------------------------------------------- 1 | """ Tests the inlet component. """ 2 | 3 | import unittest 4 | import os 5 | 6 | import numpy as np 7 | 8 | from openmdao.api import Problem, Group 9 | from openmdao.utils.assert_utils import assert_near_equal, assert_check_partials 10 | 11 | from pycycle.mp_cycle import Cycle 12 | from pycycle.thermo.cea.species_data import janaf 13 | from pycycle.elements.inlet import Inlet, MilSpecRecovery 14 | from pycycle.elements.flow_start import FlowStart 15 | from pycycle.constants import AIR_JETA_TAB_SPEC, TAB_AIR_FUEL_COMPOSITION 16 | 17 | 18 | fpath = os.path.dirname(os.path.realpath(__file__)) 19 | ref_data = np.loadtxt(fpath + "/reg_data/inlet.csv", delimiter=",", skiprows=1) 20 | 21 | header = [ 22 | 'eRamBase', 23 | 'Fl_I.W', 24 | 'Fl_I.V', 25 | 'Fl_I.MN', 26 | 'Fl_I.s', 27 | 'Fl_I.Pt', 28 | 'Fl_I.Tt', 29 | 'Fl_I.ht', 30 | 'Fl_I.rhot', 31 | 'Fl_I.gamt', 32 | 'Fl_O.MN', 33 | 'Fl_O.s', 34 | 'Fl_O.Pt', 35 | 'Fl_O.Tt', 36 | 'Fl_O.ht', 37 | 'Fl_O.rhot', 38 | 'Fl_O.gamt', 39 | 'Fram', 40 | 'Fl_O.Ps', 41 | 'Fl_O.Ts', 42 | 'Fl_O.hs', 43 | 'Fl_O.rhos', 44 | 'Fl_O.gams'] 45 | 46 | h_map = dict(((v_name, i) for i, v_name in enumerate(header))) 47 | 48 | class MilSpecTestCase(unittest.TestCase): 49 | 50 | def test(self): 51 | 52 | prob = Problem() 53 | 54 | prob.model.add_subsystem('mil_spec', MilSpecRecovery(), promotes=['*']) 55 | 56 | prob.setup(check=False, force_alloc_complex=True) 57 | 58 | prob.set_val('MN', 0.8) 59 | prob.set_val('ram_recovery_base', 0.9) 60 | 61 | prob.run_model() 62 | 63 | tol = 1e-4 64 | assert_near_equal(prob['ram_recovery'], 0.9, tol) 65 | 66 | partial_data = prob.check_partials(out_stream=None, method='cs') 67 | assert_check_partials(partial_data, atol=1e-8, rtol=1e-8) 68 | 69 | prob.set_val('MN', 2.0) 70 | 71 | prob.run_model() 72 | assert_near_equal(prob['ram_recovery'], 0.8325, tol) 73 | 74 | partial_data = prob.check_partials(out_stream=None, method='cs') 75 | assert_check_partials(partial_data, atol=1e-8, rtol=1e-8) 76 | 77 | prob.set_val('MN', 6.0) 78 | 79 | prob.run_model() 80 | assert_near_equal(prob['ram_recovery'], 0.35858, tol) 81 | 82 | partial_data = prob.check_partials(out_stream=None, method='cs') 83 | assert_check_partials(partial_data, atol=1e-8, rtol=1e-8) 84 | 85 | 86 | class InletTestCase(unittest.TestCase): 87 | 88 | def setUp(self): 89 | 90 | self.prob = Problem() 91 | cycle = self.prob.model = Cycle() 92 | cycle.options['thermo_method'] = 'CEA' 93 | cycle.options['thermo_data'] = janaf 94 | 95 | # cycle.options['thermo_method'] = 'TABULAR' 96 | # cycle.options['thermo_data'] = AIR_JETA_TAB_SPEC 97 | 98 | cycle.set_input_defaults('flow_start.P', 17, units='psi') 99 | cycle.set_input_defaults('flow_start.T', 500.0, units='degR') 100 | cycle.set_input_defaults('inlet.MN', 0.5) 101 | cycle.set_input_defaults('inlet.Fl_I:stat:V', 1., units='ft/s') 102 | cycle.set_input_defaults('flow_start.W', 1., units='lbm/s') 103 | 104 | cycle.add_subsystem('flow_start', FlowStart()) 105 | cycle.add_subsystem('inlet', Inlet()) 106 | 107 | # total and static 108 | fl_src = "flow_start.Fl_O" 109 | fl_target = "inlet.Fl_I" 110 | cycle.pyc_connect_flow("flow_start.Fl_O", "inlet.Fl_I") 111 | 112 | self.prob.set_solver_print(level=-1) 113 | self.prob.setup(check=False, force_alloc_complex=True) 114 | 115 | def test_case1(self): 116 | # 4 cases to check against 117 | for i, data in enumerate(ref_data): 118 | self.prob['inlet.ram_recovery'] = data[h_map['eRamBase']] 119 | 120 | # input flowstation 121 | self.prob['flow_start.P'] = data[h_map['Fl_I.Pt']] 122 | self.prob['flow_start.T'] = data[h_map['Fl_I.Tt']] 123 | self.prob['inlet.MN'] = data[h_map['Fl_O.MN']] 124 | self.prob['flow_start.MN'] = data[h_map['Fl_I.MN']] 125 | self.prob['flow_start.W'] = data[h_map['Fl_I.W']] 126 | self.prob['inlet.Fl_I:stat:V'] = data[h_map['Fl_I.V']] 127 | 128 | self.prob.run_model() 129 | 130 | # check outputs 131 | 132 | pt, ht, Fram, ps, ts = data[h_map['Fl_O.Pt']], data[h_map['Fl_O.ht']], data[ 133 | h_map['Fram']], data[h_map['Fl_O.Ps']], data[h_map['Fl_O.Ts']] 134 | pt_computed = self.prob['inlet.Fl_O:tot:P'] 135 | ht_computed = self.prob['inlet.Fl_O:tot:h'] 136 | Fram_computed = self.prob['inlet.F_ram'] 137 | ps_computed = self.prob['inlet.Fl_O:stat:P'] 138 | ts_computed = self.prob['inlet.Fl_O:stat:T'] 139 | 140 | tol = 1e-4 141 | assert_near_equal(pt_computed, pt, tol) 142 | assert_near_equal(ht_computed, ht, tol) 143 | assert_near_equal(Fram_computed, Fram, tol) 144 | assert_near_equal(ps_computed, ps, tol) 145 | assert_near_equal(ts_computed, ts, tol) 146 | 147 | partial_data = self.prob.check_partials(out_stream=None, method='cs', includes=['inlet.*'], excludes=['*.base_thermo.*']) 148 | assert_check_partials(partial_data, atol=1e-8, rtol=1e-8) 149 | 150 | if __name__ == "__main__": 151 | unittest.main() 152 | -------------------------------------------------------------------------------- /pycycle/elements/test/test_mixer.py: -------------------------------------------------------------------------------- 1 | """ Tests the duct component. """ 2 | 3 | import unittest 4 | import os 5 | 6 | import numpy as np 7 | 8 | import openmdao.api as om 9 | 10 | from openmdao.api import Problem, Group 11 | 12 | from openmdao.utils.assert_utils import assert_near_equal, assert_check_partials 13 | 14 | from pycycle.constants import CEA_AIR_COMPOSITION, CEA_AIR_FUEL_COMPOSITION 15 | from pycycle.mp_cycle import Cycle 16 | from pycycle.elements.mixer import Mixer 17 | from pycycle.elements.flow_start import FlowStart 18 | from pycycle.connect_flow import connect_flow 19 | from pycycle.thermo.cea.species_data import janaf 20 | 21 | 22 | class MixerTestcase(unittest.TestCase): 23 | 24 | def test_mix_same(self): 25 | # mix two identical streams and make sure you get twice the area and the same total pressure 26 | 27 | p = Problem() 28 | 29 | cycle = p.model = Cycle() 30 | cycle.options['thermo_method'] = 'CEA' 31 | cycle.options['thermo_data'] = janaf 32 | 33 | cycle.set_input_defaults('P', 17., units='psi') 34 | cycle.set_input_defaults('T', 500., units='degR') 35 | cycle.set_input_defaults('MN', 0.5) 36 | cycle.set_input_defaults('W', 100., units='lbm/s') 37 | 38 | cycle.add_subsystem('start1', FlowStart(), promotes=['P', 'T', 'MN', 'W']) 39 | cycle.add_subsystem('start2', FlowStart(), promotes=['P', 'T', 'MN', 'W']) 40 | 41 | cycle.add_subsystem('mixer', Mixer(design=True)) 42 | 43 | cycle.pyc_connect_flow('start1.Fl_O', 'mixer.Fl_I1') 44 | cycle.pyc_connect_flow('start2.Fl_O', 'mixer.Fl_I2') 45 | 46 | p.set_solver_print(level=-1) 47 | 48 | p.setup() 49 | 50 | p.run_model() 51 | 52 | tol = 1e-6 53 | assert_near_equal(p['mixer.Fl_O:stat:area'], 2*p['start1.Fl_O:stat:area'], tolerance=tol) 54 | assert_near_equal(p['mixer.Fl_O:tot:P'], p['P'], tolerance=tol) 55 | assert_near_equal(p['mixer.ER'], 1, tolerance=tol) 56 | 57 | def test_mix_diff(self): 58 | # mix two identical streams and make sure you get twice the area and the same total pressure 59 | 60 | p = Problem() 61 | cycle = p.model = Cycle() 62 | cycle.options['thermo_method'] = 'CEA' 63 | cycle.options['thermo_data'] = janaf 64 | 65 | cycle.set_input_defaults('start1.P', 17., units='psi') 66 | cycle.set_input_defaults('start2.P', 15., units='psi') 67 | cycle.set_input_defaults('T', 500., units='degR') 68 | cycle.set_input_defaults('MN', 0.5) 69 | cycle.set_input_defaults('W', 100., units='lbm/s') 70 | 71 | cycle.add_subsystem('start1', FlowStart(), promotes=['MN', 'T', 'W']) 72 | cycle.add_subsystem('start2', FlowStart(), promotes=['MN', 'T', 'W']) 73 | 74 | cycle.add_subsystem('mixer', Mixer(design=True)) 75 | 76 | cycle.pyc_connect_flow('start1.Fl_O', 'mixer.Fl_I1') 77 | cycle.pyc_connect_flow('start2.Fl_O', 'mixer.Fl_I2') 78 | 79 | p.set_solver_print(level=-1) 80 | 81 | p.setup() 82 | p.run_model() 83 | tol = 1e-6 84 | assert_near_equal(p['mixer.Fl_O:stat:area'], 653.26524074, tolerance=tol) 85 | assert_near_equal(p['mixer.Fl_O:tot:P'], 15.89206597, tolerance=tol) 86 | assert_near_equal(p['mixer.ER'], 1.1333333333, tolerance=tol) 87 | 88 | def test_mix_air_with_airfuel(self): 89 | 90 | p = Problem() 91 | 92 | cycle = p.model = Cycle() 93 | cycle.options['thermo_method'] = 'CEA' 94 | cycle.options['thermo_data'] = janaf 95 | 96 | cycle.set_input_defaults('start1.P', 9.218, units='psi') 97 | cycle.set_input_defaults('start1.T', 1524.32, units='degR') 98 | cycle.set_input_defaults('start1.MN', 0.4463) 99 | cycle.set_input_defaults('start1.W', 161.49, units='lbm/s') 100 | 101 | cycle.set_input_defaults('start2.P', 8.68, units='psi') 102 | cycle.set_input_defaults('start2.T', 524., units='degR') 103 | cycle.set_input_defaults('start2.MN', 0.4463) 104 | cycle.set_input_defaults('start2.W', 158., units='lbm/s') 105 | 106 | cycle.add_subsystem('start1', FlowStart(composition=CEA_AIR_FUEL_COMPOSITION)) 107 | cycle.add_subsystem('start2', FlowStart(composition=CEA_AIR_COMPOSITION)) 108 | 109 | cycle.add_subsystem('mixer', Mixer(design=True, designed_stream=1)) 110 | 111 | cycle.pyc_connect_flow('start1.Fl_O', 'mixer.Fl_I1') 112 | cycle.pyc_connect_flow('start2.Fl_O', 'mixer.Fl_I2') 113 | 114 | p.setup(force_alloc_complex=True) 115 | 116 | p.set_solver_print(level=-1) 117 | 118 | p.run_model() 119 | 120 | tol = 1e-6 121 | assert_near_equal(p['mixer.Fl_O:stat:area'], 2786.86877031, tolerance=tol) 122 | assert_near_equal(p['mixer.Fl_O:tot:P'], 8.87520497, tolerance=tol) 123 | assert_near_equal(p['mixer.ER'], 1.06198157, tolerance=tol) 124 | 125 | partials = p.check_partials(includes=['mixer.area_calc*', 'mixer.mix_flow*', 'mixer.imp_out*'], out_stream=None, method='cs') 126 | assert_check_partials(partials, atol=1e-8, rtol=1e-8) 127 | 128 | 129 | if __name__ == "__main__": 130 | unittest.main() -------------------------------------------------------------------------------- /pycycle/elements/test/test_nozzle.py: -------------------------------------------------------------------------------- 1 | """ Tests the Nozzle component. """ 2 | 3 | import unittest 4 | import os 5 | 6 | import numpy as np 7 | 8 | from openmdao.api import Problem, Group 9 | from openmdao.utils.assert_utils import assert_near_equal, assert_check_partials 10 | 11 | from pycycle.mp_cycle import Cycle 12 | from pycycle.thermo.cea.species_data import janaf 13 | from pycycle.elements.flow_start import FlowStart 14 | from pycycle.elements.nozzle import Nozzle 15 | 16 | fpath = os.path.dirname(os.path.realpath(__file__)) 17 | ref_data = np.loadtxt(fpath + "/reg_data/nozzle.csv", delimiter=",", skiprows=1) 18 | 19 | header = ['Cfg', 'PsExh', 'Fl_I.W', 'Fl_I.MN', 'Fl_I.s', 'Fl_I.Pt', 'Fl_I.Tt', 'Fl_I.ht', 20 | 'Fl_I.rhot', 'Fl_I.gamt', 'Fl_O.MN', 'Fl_O.s', 'Fl_O.Pt', 'Fl_O.Tt', 'Fl_O.ht', 21 | 'Fl_O.rhot', 'Fl_O.gamt', 'Fl_O.Ps', 'Fg', 'Vactual', 'Ath', 'AR', 'PR'] 22 | h_map = dict(((v_name, i) for i, v_name in enumerate(header))) 23 | 24 | 25 | class NozzleTestCase(unittest.TestCase): 26 | 27 | def test_case1(self): 28 | 29 | self.prob = Problem() 30 | cycle = self.prob.model = Cycle() 31 | cycle.options['thermo_method'] = 'CEA' 32 | cycle.options['thermo_data'] = janaf 33 | 34 | cycle.add_subsystem('flow_start', FlowStart()) 35 | cycle.add_subsystem('nozzle', Nozzle(lossCoef='Cfg', internal_solver=True)) 36 | 37 | cycle.set_input_defaults('nozzle.Ps_exhaust', 10.0, units='lbf/inch**2') 38 | cycle.set_input_defaults('flow_start.MN', 0.0) 39 | cycle.set_input_defaults('flow_start.T', 500.0, units='degR') 40 | cycle.set_input_defaults('flow_start.P', 17.0, units='psi') 41 | cycle.set_input_defaults('flow_start.W', 100.0, units='lbm/s') 42 | 43 | cycle.pyc_connect_flow("flow_start.Fl_O", "nozzle.Fl_I") 44 | 45 | self.prob.setup(check=False, force_alloc_complex=True) 46 | 47 | # 4 cases to check against 48 | for i, data in enumerate(ref_data): 49 | 50 | self.prob['nozzle.Cfg'] = data[h_map['Cfg']] 51 | self.prob['nozzle.Ps_exhaust'] = data[h_map['PsExh']] 52 | # input flowstation 53 | 54 | self.prob['flow_start.P'] = data[h_map['Fl_I.Pt']] 55 | self.prob['flow_start.T'] = data[h_map['Fl_I.Tt']] 56 | self.prob['flow_start.W'] = data[h_map['Fl_I.W']] 57 | self.prob['flow_start.MN'] = data[h_map['Fl_I.MN']] 58 | 59 | self.prob.run_model() 60 | 61 | # check outputs 62 | Fg, V, PR = data[h_map['Fg']], data[ 63 | h_map['Vactual']], data[h_map['PR']] 64 | MN = data[h_map['Fl_O.MN']] 65 | Ath = data[h_map['Ath']] 66 | Pt = data[h_map['Fl_O.Pt']] 67 | MN_computed = self.prob['nozzle.Fl_O:stat:MN'] 68 | Fg_computed = self.prob['nozzle.Fg'] 69 | V_computed = self.prob['nozzle.Fl_O:stat:V'] 70 | PR_computed = self.prob['nozzle.PR'] 71 | Ath_computed = self.prob['nozzle.Fl_O:stat:area'] 72 | Pt_computed = self.prob['nozzle.Fl_O:tot:P'] 73 | 74 | # Used for all 75 | tol = 5.e-3 76 | 77 | assert_near_equal(MN_computed, MN, tol) 78 | 79 | assert_near_equal(Fg_computed, Fg, tol) 80 | assert_near_equal(V_computed, V, tol) 81 | assert_near_equal(Pt_computed, Pt, tol) 82 | 83 | assert_near_equal(PR_computed, PR, tol) 84 | assert_near_equal(Ath_computed, Ath, tol) 85 | 86 | partial_data = self.prob.check_partials(out_stream=None, method='cs', 87 | includes=['nozzle.*'], excludes=['*.base_thermo.*',]) 88 | assert_check_partials(partial_data, atol=1e-8, rtol=1e-8) 89 | 90 | if __name__ == "__main__": 91 | unittest.main() 92 | -------------------------------------------------------------------------------- /pycycle/elements/test/test_shaft.py: -------------------------------------------------------------------------------- 1 | import numpy as np 2 | import unittest 3 | import os 4 | 5 | from openmdao.api import Problem, Group 6 | from openmdao.utils.assert_utils import assert_near_equal, assert_check_partials 7 | 8 | from pycycle.elements.shaft import Shaft 9 | 10 | fpath = os.path.dirname(os.path.realpath(__file__)) 11 | ref_data = np.loadtxt(fpath + "/reg_data/shaft.csv", 12 | delimiter=",", skiprows=1) 13 | 14 | header = [ 15 | 'trqLoad1', 16 | 'trqLoad2', 17 | 'trqLoad3', 18 | 'Nmech', 19 | 'HPX', 20 | 'fracLoss', 21 | 'trqIn', 22 | 'trqOut', 23 | 'trqNet', 24 | 'pwrIn', 25 | 'pwrOut', 26 | 'pwrNet'] 27 | h_map = dict(((v_name, i) for i, v_name in enumerate(header))) 28 | 29 | 30 | class ShaftTestCase(unittest.TestCase): 31 | 32 | def setUp(self): 33 | self.prob = Problem() 34 | self.prob.model = Group() 35 | self.prob.model.add_subsystem("shaft", Shaft(num_ports=3), promotes=["*"]) 36 | 37 | self.prob.model.set_input_defaults('trq_0', 17., units='ft*lbf') 38 | self.prob.model.set_input_defaults('trq_1', 17., units='ft*lbf') 39 | self.prob.model.set_input_defaults('trq_2', 17., units='ft*lbf') 40 | self.prob.model.set_input_defaults('Nmech', 17., units='rpm') 41 | self.prob.model.set_input_defaults('HPX', 17., units='hp') 42 | self.prob.model.set_input_defaults('fracLoss', 17.) 43 | 44 | self.prob.setup(check=False, force_alloc_complex=True) 45 | 46 | def test_case1(self): 47 | # 6 cases to check against 48 | for i, data in enumerate(ref_data): 49 | # input torques 50 | self.prob['trq_0'] = data[h_map['trqLoad1']] 51 | self.prob['trq_1'] = data[h_map['trqLoad2']] 52 | self.prob['trq_2'] = data[h_map['trqLoad3']] 53 | 54 | # shaft inputs 55 | self.prob['Nmech'] = data[h_map['Nmech']] 56 | self.prob['HPX'] = data[h_map['HPX']] 57 | self.prob['fracLoss'] = data[h_map['fracLoss']] 58 | self.prob.run_model() 59 | 60 | # check outputs 61 | trqIn, trqOut, trqNet = data[ 62 | h_map['trqIn']], data[ 63 | h_map['trqOut']], data[ 64 | h_map['trqNet']] 65 | pwrIn, pwrOut, pwrNet = data[ 66 | h_map['pwrIn']], data[ 67 | h_map['pwrOut']], data[ 68 | h_map['pwrNet']] 69 | trqIn_comp = self.prob['trq_in'] 70 | trqOut_comp = self.prob['trq_out'] 71 | trqNet_comp = self.prob['trq_net'] 72 | pwrIn_comp = self.prob['pwr_in'] 73 | pwrOut_comp = self.prob['pwr_out'] 74 | pwrNet_comp = self.prob['pwr_net'] 75 | 76 | tol = 1.0e-4 77 | assert_near_equal(trqIn_comp, trqIn, tol) 78 | assert_near_equal(trqOut_comp, trqOut, tol) 79 | assert_near_equal(trqNet_comp, trqNet, tol) 80 | assert_near_equal(pwrIn_comp, pwrIn, tol) 81 | assert_near_equal(pwrOut_comp, pwrOut, tol) 82 | assert_near_equal(pwrNet_comp, pwrNet, tol) 83 | 84 | partial_data = self.prob.check_partials(out_stream=None, method='cs') 85 | assert_check_partials(partial_data, atol=1e-8, rtol=1e-8) 86 | 87 | if __name__ == "__main__": 88 | unittest.main() 89 | -------------------------------------------------------------------------------- /pycycle/elements/test/test_splitter.py: -------------------------------------------------------------------------------- 1 | import numpy as np 2 | import unittest 3 | import os 4 | 5 | from openmdao.api import Problem 6 | from openmdao.utils.assert_utils import assert_near_equal, assert_check_partials 7 | 8 | 9 | from pycycle.mp_cycle import Cycle 10 | from pycycle.thermo.cea.species_data import janaf 11 | 12 | from pycycle.elements.splitter import Splitter 13 | from pycycle.elements.flow_start import FlowStart 14 | 15 | 16 | fpath = os.path.dirname(os.path.realpath(__file__)) 17 | ref_data = np.loadtxt(fpath + "/reg_data/splitter.csv", delimiter=",", skiprows=1) 18 | 19 | header = [ 20 | 'BPR', 21 | 'Fl_I.W', 22 | 'Fl_I.V', 23 | 'Fl_I.MN', 24 | 'Fl_I.s', 25 | 'Fl_I.Pt', 26 | 'Fl_I.Tt', 27 | 'Fl_I.ht', 28 | 'Fl_I.rhot', 29 | 'Fl_I.gamt', 30 | 'Fl_O1.MN', 31 | 'Fl_O1.s', 32 | 'Fl_O1.Pt', 33 | 'Fl_O1.Tt', 34 | 'Fl_O1.ht', 35 | 'Fl_O1.rhot', 36 | 'Fl_O1.gamt', 37 | 'Fl_O1.Ps', 38 | 'Fl_O1.Ts', 39 | 'Fl_O1.hs', 40 | 'Fl_O1.rhos', 41 | 'Fl_O1.gams', 42 | 'Fl_O1.W', 43 | 'Fl_O2.MN', 44 | 'Fl_O2.s', 45 | 'Fl_O2.Pt', 46 | 'Fl_O2.Tt', 47 | 'Fl_O2.ht', 48 | 'Fl_O2.rhot', 49 | 'Fl_O2.gamt', 50 | 'Fl_O2.Ps', 51 | 'Fl_O2.Ts', 52 | 'Fl_O2.hs', 53 | 'Fl_O2.rhos', 54 | 'Fl_O2.gams', 55 | 'Fl_O2.W'] 56 | h_map = dict(((v_name, i) for i, v_name in enumerate(header))) 57 | 58 | 59 | class splitterTestCase(unittest.TestCase): 60 | 61 | def setUp(self): 62 | 63 | self.prob = Problem() 64 | cycle = self.prob.model = Cycle() 65 | cycle.options['thermo_method'] = 'CEA' 66 | cycle.options['thermo_data'] = janaf 67 | 68 | cycle.add_subsystem('flow_start', FlowStart()) 69 | cycle.add_subsystem('splitter', Splitter()) 70 | 71 | cycle.set_input_defaults('flow_start.P', 17., units='psi') 72 | cycle.set_input_defaults('flow_start.T', 500., units='degR') 73 | cycle.set_input_defaults('splitter.MN1', 0.5) 74 | cycle.set_input_defaults('splitter.MN2', 0.5) 75 | cycle.set_input_defaults('flow_start.W', 10., units='lbm/s') 76 | 77 | cycle.pyc_connect_flow('flow_start.Fl_O', 'splitter.Fl_I') 78 | 79 | self.prob.set_solver_print(level=-1) 80 | self.prob.setup(check=False, force_alloc_complex=True) 81 | 82 | def test_case1(self): 83 | # 4 cases to check against 84 | for i, data in enumerate(ref_data): 85 | self.prob['splitter.BPR'] = data[h_map['BPR']] 86 | 87 | # input flowstation 88 | self.prob['flow_start.P'] = data[h_map['Fl_I.Pt']] 89 | self.prob['flow_start.T'] = data[h_map['Fl_I.Tt']] 90 | self.prob['flow_start.W'] = data[h_map['Fl_I.W']] 91 | self.prob['splitter.MN1'] = data[h_map['Fl_O1.MN']] 92 | self.prob['splitter.MN2'] = data[h_map['Fl_O2.MN']] 93 | self.prob['splitter.Fl_I:stat:V'] = data[h_map['Fl_I.V']] 94 | self.prob.run_model() 95 | 96 | # check flow1 outputs 97 | pt1, ht1, ps1, ts1 = data[ 98 | h_map['Fl_O2.Pt']], data[ 99 | h_map['Fl_O1.ht']], data[ 100 | h_map['Fl_O1.Ps']], data[ 101 | h_map['Fl_O1.Ts']] 102 | pt1_computed = self.prob['splitter.Fl_O1:tot:P'] 103 | ht1_computed = self.prob['splitter.Fl_O1:tot:h'] 104 | ps1_computed = self.prob['splitter.Fl_O1:stat:P'] 105 | ts1_computed = self.prob['splitter.Fl_O1:stat:T'] 106 | 107 | tol = 1e-4 108 | assert_near_equal(pt1_computed, pt1, tol) 109 | assert_near_equal(ht1_computed, ht1, tol) 110 | assert_near_equal(ps1_computed, ps1, tol) 111 | assert_near_equal(ts1_computed, ts1, tol) 112 | 113 | # check flow2 outputs 114 | pt2, ht2, ps2, ts2 = data[ 115 | h_map['Fl_O2.Pt']], data[ 116 | h_map['Fl_O2.ht']], data[ 117 | h_map['Fl_O2.Ps']], data[ 118 | h_map['Fl_O2.Ts']] 119 | pt2_computed = self.prob['splitter.Fl_O2:tot:P'] 120 | ht2_computed = self.prob['splitter.Fl_O2:tot:h'] 121 | ps2_computed = self.prob['splitter.Fl_O2:stat:P'] 122 | ts2_computed = self.prob['splitter.Fl_O2:stat:T'] 123 | 124 | assert_near_equal(pt2_computed, pt2, tol) 125 | assert_near_equal(ht2_computed, ht2, tol) 126 | assert_near_equal(ps2_computed, ps2, tol) 127 | assert_near_equal(ts2_computed, ts2, tol) 128 | 129 | partial_data = self.prob.check_partials(out_stream=None, method='cs', 130 | includes=['splitter.*'], excludes=['*.base_thermo.*',]) 131 | assert_check_partials(partial_data, atol=1e-8, rtol=1e-8) 132 | 133 | if __name__ == "__main__": 134 | unittest.main() 135 | -------------------------------------------------------------------------------- /pycycle/elements/test/test_usatm1976.py: -------------------------------------------------------------------------------- 1 | import numpy as np 2 | import unittest 3 | import os 4 | 5 | from openmdao.api import Problem 6 | from openmdao.utils.assert_utils import assert_check_partials 7 | 8 | from pycycle.elements.US1976 import USatm1976Comp 9 | 10 | class TestCase1976(unittest.TestCase): 11 | 12 | def test_derivs(self): 13 | 14 | p = Problem() 15 | p.model.add_subsystem('std_1976', USatm1976Comp()) 16 | 17 | p.setup(force_alloc_complex=True) 18 | p.final_setup() 19 | 20 | data = p.check_partials(out_stream=None) 21 | 22 | assert_check_partials(data, atol=1e-4, rtol=1e-1) 23 | 24 | 25 | if __name__ == "__main__": 26 | unittest.main() 27 | 28 | -------------------------------------------------------------------------------- /pycycle/flow_in.py: -------------------------------------------------------------------------------- 1 | """ FlowIN component which serves as an input flowstation for cycle components. 2 | """ 3 | import numpy as np 4 | 5 | import openmdao.api as om 6 | 7 | 8 | class FlowIn(om.ExplicitComponent): 9 | """ 10 | Provides a central place to connect flow information to in a component 11 | but doesn't actually do anything on its own 12 | """ 13 | 14 | def initialize(self): 15 | self.options.declare('fl_name', default='flow', 16 | desc='thermodynamic data set') 17 | 18 | def setup(self): 19 | fl_name = self.options['fl_name'] 20 | 21 | self.add_output('foo', val=1., 22 | desc="dummy output that is NOT used for anything other than to keep the framework happy. ") 23 | 24 | self.add_input('%s:tot:h'%fl_name, val=1.0, desc='total enthalpy', units='Btu/lbm') 25 | self.add_input('%s:tot:T'%fl_name, val=518., desc='total temperature', units='degR') 26 | self.add_input('%s:tot:P'%fl_name, val=1., desc='total pressure', units='lbf/inch**2') 27 | self.add_input('%s:tot:rho'%fl_name, val=1.0, desc='total density', units='lbm/ft**3') 28 | self.add_input('%s:tot:gamma'%fl_name, val=1.4, desc='total gamma') 29 | self.add_input('%s:tot:Cp'%fl_name, val=1.0, desc='total Specific heat at constant pressure', units='Btu/(lbm*degR)') 30 | self.add_input('%s:tot:Cv'%fl_name, val=1.0, desc='total Specific heat at constant volume', units='Btu/(lbm*degR)') 31 | self.add_input('%s:tot:S'%fl_name, val=1.0, desc='total entropy', units='Btu/(lbm*degR)') 32 | self.add_input('%s:tot:R'%fl_name, val=1.0, desc='total gas constant', units='Btu/(lbm*degR)') 33 | self.add_input('%s:tot:composition'%fl_name, shape_by_conn=True, desc='flow composition vector') 34 | 35 | self.add_input('%s:stat:h'%fl_name, val=1.0, desc='static enthalpy', units='Btu/lbm') 36 | self.add_input('%s:stat:T'%fl_name, val=518., desc='static temperature', units='degR') 37 | self.add_input('%s:stat:P'%fl_name, val=1.0, desc='static pressure', units='lbf/inch**2') 38 | self.add_input('%s:stat:rho'%fl_name, val=1.0, desc='static density', units='lbm/ft**3') 39 | self.add_input('%s:stat:gamma'%fl_name, val=1.4, desc='static gamma') 40 | self.add_input('%s:stat:Cp'%fl_name, val=1.0, desc='static Specific heat at constant pressure', units='Btu/(lbm*degR)') 41 | self.add_input('%s:stat:Cv'%fl_name, val=1.0, desc='static Specific heat at constant volume', units='Btu/(lbm*degR)') 42 | self.add_input('%s:stat:S'%fl_name, val= 0.0, desc='static entropy', units='Btu/(lbm*degR)') 43 | self.add_input('%s:stat:R'%fl_name, val=1.0, desc='static gas constant', units='Btu/(lbm*degR)') 44 | self.add_input('%s:stat:composition'%fl_name, shape_by_conn=True, desc='flow composition vector') 45 | 46 | # TODO takes these out of static (keep them top level) 47 | self.add_input('%s:stat:V'%fl_name, val=1.0, desc='Velocity', units='ft/s') 48 | self.add_input('%s:stat:Vsonic'%fl_name, val=1.0, desc='Speed of sound', units='ft/s') 49 | self.add_input('%s:stat:MN'%fl_name, val=1.0, desc='Mach number') 50 | self.add_input('%s:stat:area'%fl_name, val=1.0, desc='flow area', units='inch**2') 51 | self.add_input('%s:stat:Wc'%fl_name, val=1.0, desc='corrected weight flow', units='lbm/s') 52 | self.add_input('%s:stat:W'%fl_name, val= 0.0, desc='weight flow', units='lbm/s') 53 | self.add_input('%s:FAR'%fl_name, val=0.0, desc='fuel to air ratio') 54 | # self.add_input('%s:WAR'%fl_name, val = 0.0, desc='water to air ratio') 55 | # self.add_input('%s:nu', %nameval=1.0, desc='dynamic viscosity', units='lbm/(s*ft)') 56 | 57 | def compute(self, inputs, outputs): 58 | pass 59 | -------------------------------------------------------------------------------- /pycycle/maps/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/OpenMDAO/pyCycle/8a031800fb90dea96ea174aedb112e7d5e120b39/pycycle/maps/__init__.py -------------------------------------------------------------------------------- /pycycle/maps/map_data.py: -------------------------------------------------------------------------------- 1 | # stupid hack so I can create data containers in python 2 | 3 | class MapData(object): 4 | pass -------------------------------------------------------------------------------- /pycycle/maps/test/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/OpenMDAO/pyCycle/8a031800fb90dea96ea174aedb112e7d5e120b39/pycycle/maps/test/__init__.py -------------------------------------------------------------------------------- /pycycle/passthrough.py: -------------------------------------------------------------------------------- 1 | import numpy as np 2 | 3 | from openmdao.api import ExplicitComponent 4 | 5 | 6 | class PassThrough(ExplicitComponent): 7 | """ 8 | Helper component that is needed when variables must be passed directly from 9 | input to output of a cycle element with no other component in between 10 | """ 11 | 12 | def __init__(self, i_var, o_var, val, units=None): 13 | super(PassThrough, self).__init__() 14 | self.i_var = i_var 15 | self.o_var = o_var 16 | self.units = units 17 | self.val = val 18 | 19 | if isinstance(val, (float, int)) or np.isscalar(val): 20 | size=1 21 | else: 22 | size = np.prod(val.shape) 23 | 24 | self.size = size 25 | 26 | def setup(self): 27 | if self.units is None: 28 | self.add_input(self.i_var, self.val) 29 | self.add_output(self.o_var, self.val) 30 | else: 31 | self.add_input(self.i_var, self.val, units=self.units) 32 | self.add_output(self.o_var, self.val, units=self.units) 33 | 34 | #partial derivs setup 35 | row_col = np.arange(self.size) 36 | self.declare_partials(of=self.o_var, wrt=self.i_var, 37 | val=np.ones(self.size), rows=row_col, cols=row_col) 38 | 39 | def compute(self, inputs, outputs): 40 | 41 | outputs[self.o_var] = inputs[self.i_var] 42 | 43 | def compute_partials(self, inputs, J): 44 | pass 45 | 46 | 47 | if __name__ == "__main__": 48 | 49 | from openmdao.api import Problem, IndepVarComp 50 | 51 | p = Problem() 52 | 53 | indeps = p.model.add_subsystem('indeps', IndepVarComp(), promotes=['*']) 54 | indeps.add_output('foo', val=np.ones(4)) 55 | 56 | p.model.add_subsystem('pt', PassThrough("foo", "bar", val=np.ones(4)), promotes=['*']) 57 | 58 | p.setup() 59 | p.run_model() 60 | 61 | p.check_partial_derivatives() 62 | -------------------------------------------------------------------------------- /pycycle/tests/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/OpenMDAO/pyCycle/8a031800fb90dea96ea174aedb112e7d5e120b39/pycycle/tests/__init__.py -------------------------------------------------------------------------------- /pycycle/tests/test_element.py: -------------------------------------------------------------------------------- 1 | import unittest -------------------------------------------------------------------------------- /pycycle/thermo/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/OpenMDAO/pyCycle/8a031800fb90dea96ea174aedb112e7d5e120b39/pycycle/thermo/__init__.py -------------------------------------------------------------------------------- /pycycle/thermo/cea/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/OpenMDAO/pyCycle/8a031800fb90dea96ea174aedb112e7d5e120b39/pycycle/thermo/cea/__init__.py -------------------------------------------------------------------------------- /pycycle/thermo/cea/test/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/OpenMDAO/pyCycle/8a031800fb90dea96ea174aedb112e7d5e120b39/pycycle/thermo/cea/test/__init__.py -------------------------------------------------------------------------------- /pycycle/thermo/cea/test/test_chem_eq_co2.py: -------------------------------------------------------------------------------- 1 | import unittest 2 | 3 | from openmdao.api import Problem, Group 4 | 5 | from openmdao.utils.assert_utils import assert_near_equal 6 | 7 | from pycycle.thermo.cea.chem_eq import ChemEq 8 | from pycycle.thermo.cea import species_data 9 | from pycycle import constants 10 | 11 | 12 | class ChemEqTestCase(unittest.TestCase): 13 | 14 | def setUp(self): 15 | self.thermo = species_data.Properties(species_data.co2_co_o2, init_elements=constants.CO2_CO_O2_ELEMENTS) 16 | p = self.p = Problem(model=Group()) 17 | p.model.suppress_solver_output = True 18 | p.model.set_input_defaults('P', 1.034210, units="bar") 19 | 20 | def test_set_total_tp(self): 21 | p = self.p 22 | p.model.add_subsystem('ceq', ChemEq(thermo=self.thermo), promotes=["*"]) 23 | p.model.set_input_defaults('T', 1500., units='degK') 24 | p.setup(check=False) 25 | p.run_model() 26 | 27 | tol = 6e-4 28 | 29 | assert_near_equal(p['n'], [8.15344263e-06, 2.27139552e-02, 4.07672148e-06], tol) 30 | 31 | 32 | if __name__ == "__main__": 33 | 34 | unittest.main() 35 | -------------------------------------------------------------------------------- /pycycle/thermo/cea/test/test_chem_eq_janaf.py: -------------------------------------------------------------------------------- 1 | import unittest 2 | import numpy as np 3 | 4 | from openmdao.api import Problem, Group 5 | 6 | from openmdao.utils.assert_utils import assert_near_equal 7 | 8 | from pycycle.thermo.cea.chem_eq import ChemEq 9 | from pycycle.thermo.cea import species_data 10 | from pycycle import constants 11 | 12 | 13 | class ChemEqTestCase(unittest.TestCase): 14 | 15 | def setUp(self): 16 | self.thermo = species_data.Properties(species_data.janaf, init_elements=constants.AIR_ELEMENTS) 17 | p = self.p = Problem(model=Group()) 18 | p.model.suppress_solver_output = True 19 | p.model.set_input_defaults('P', 1.034210, units="bar") 20 | 21 | def test_set_total_tp(self): 22 | p = self.p 23 | p.model.add_subsystem('ceq', ChemEq(thermo=self.thermo), promotes=["*"]) 24 | p.model.set_input_defaults('T', 1500., units='degK') 25 | p.setup(check=False) 26 | p.run_model() 27 | 28 | check_val = np.array([3.23319236e-04, 1.00000000e-10, 1.10138429e-05, 1.00000000e-10, 29 | 1.72853915e-08, 6.76015824e-09, 1.00000000e-10, 2.69578737e-02, 30 | 4.80653071e-09, 7.23197634e-03]) 31 | 32 | tol = 6e-4 33 | 34 | print(p['n']) 35 | print(check_val) 36 | assert_near_equal(p['n'], check_val, tol) 37 | 38 | 39 | if __name__ == "__main__": 40 | 41 | unittest.main() -------------------------------------------------------------------------------- /pycycle/thermo/cea/test/test_props_rhs_co2.py: -------------------------------------------------------------------------------- 1 | import unittest 2 | 3 | import numpy as np 4 | 5 | from openmdao.api import Problem, Group 6 | 7 | from openmdao.utils.assert_utils import assert_near_equal 8 | 9 | from pycycle.thermo.cea.props_rhs import PropsRHS 10 | from pycycle.thermo.cea.props_calcs import PropsCalcs 11 | from pycycle.thermo.cea import species_data 12 | from pycycle import constants 13 | 14 | 15 | class PropsRHSTestCase(unittest.TestCase): 16 | 17 | def setUp(self): 18 | 19 | self.thermo = species_data.Properties(species_data.co2_co_o2, init_elements=constants.CO2_CO_O2_ELEMENTS) 20 | 21 | p = self.prob = Problem() 22 | p.model = Group() 23 | p.model.suppress_solver_output = True 24 | 25 | p.model.add_subsystem('props_rhs', PropsRHS(thermo=self.thermo), 26 | promotes=['T', 'n', 'composition', 'rhs_T', 'rhs_P', 'lhs_TP', 'n_moles']) 27 | p.model.set_input_defaults('T', 4000., units='degK') 28 | 29 | n = np.array([0.02040741, 0.0023147, 0.0102037]) 30 | p.model.set_input_defaults('n', n) 31 | 32 | b = np.array([0.02272211, 0.04544422]) 33 | p.model.set_input_defaults('composition', b) 34 | p.model.set_input_defaults('n_moles', 0.03292581) 35 | 36 | p.setup(check=False) 37 | p['n_moles'] = 0.03292581 38 | 39 | def test_total_rhs(self): 40 | 41 | p = self.prob 42 | p.run_model() 43 | 44 | goal_rhs_T = np.array([0.00016837, 0.07307206, 0.04281455]) 45 | goal_rhs_P = np.array([0.02272211, 0.04544422, 0.03292581]) 46 | goal_lhs_TP = np.array([[0.02272211, 0.02503681, 0.02272211], 47 | [0.02503681, 0.07048103, 0.04544422], 48 | [0.02272211, 0.04544422, 0.]]) 49 | 50 | tol = 1e-5 51 | assert_near_equal(p['rhs_T'], goal_rhs_T, tol) 52 | assert_near_equal(p['rhs_P'], goal_rhs_P, tol) 53 | assert_near_equal(p['lhs_TP'], goal_lhs_TP, tol) 54 | 55 | class PropsCalcsTestCase(unittest.TestCase): 56 | 57 | def setUp(self): 58 | 59 | self.thermo = species_data.Properties(species_data.co2_co_o2, init_elements=constants.CO2_CO_O2_ELEMENTS) 60 | 61 | p = self.prob = Problem() 62 | p.model = Group() 63 | p.model.suppress_solver_output = True 64 | 65 | p.model.add_subsystem('props', PropsCalcs(thermo=self.thermo), promotes=['*']) 66 | 67 | p.model.set_input_defaults('T', 4000., units='degK') 68 | p.model.set_input_defaults('P', 1.034210, units='bar') 69 | 70 | n = np.array([0.02040741, 0.0023147, 0.0102037]) 71 | p.model.set_input_defaults('n', n) 72 | 73 | result_T = np.array([-1.74791977, 1.81604241, -0.24571810]) 74 | p.model.set_input_defaults('result_T', result_T) 75 | 76 | result_P = np.array([0.48300853, 0.48301125, -0.01522548]) 77 | p.model.set_input_defaults('result_P', result_P) 78 | 79 | p.setup(check=False) 80 | p['n_moles'] = 0.03292581 81 | 82 | def test_total_calcs(self): 83 | 84 | p = self.prob 85 | p.run_model() 86 | 87 | tol = 1e-4 88 | assert_near_equal(p['Cp'], 0.579647062532, tol) 89 | assert_near_equal(p['gamma'], 1.19039, tol) 90 | assert_near_equal(p['h'], 340.324938088, tol) 91 | assert_near_equal(p['S'], 2.35850919305, tol) 92 | assert_near_equal(p['rho'], 9.44448e-5, tol) 93 | 94 | p['T'] = 1500. 95 | p['n'] = np.array([8.15344274e-06, 2.27139552e-02, 4.07672137e-06]) 96 | p['result_T'] = np.array([-1.48684061e+01, -5.86384040e+00, -2.68839475e-03]) 97 | p['result_P'] = np.array([3.33393139e-01, 3.33393139e-01, -5.97840423e-05]) 98 | p['n_moles'] = 0.022726185333 99 | p.run_model() 100 | 101 | assert_near_equal(p['Cp'], 0.322460071411, tol) 102 | assert_near_equal(p['gamma'], 1.16380, tol) 103 | assert_near_equal(p['h'], -1801.35777129, tol) 104 | assert_near_equal(p['S'], 1.58630171846, tol) 105 | assert_near_equal(p['rho'], 0.0003648856, tol) 106 | 107 | 108 | if __name__ == "__main__": 109 | unittest.main() 110 | -------------------------------------------------------------------------------- /pycycle/thermo/cea/thermo_data/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/OpenMDAO/pyCycle/8a031800fb90dea96ea174aedb112e7d5e120b39/pycycle/thermo/cea/thermo_data/__init__.py -------------------------------------------------------------------------------- /pycycle/thermo/cea/thermo_data/co2_co_o2.py: -------------------------------------------------------------------------------- 1 | """CO2, CO, O2 reactants, used for testing and development, a subset of the janaf thermo fit set""" 2 | 3 | import numpy as np 4 | from collections import OrderedDict 5 | 6 | 7 | products = OrderedDict([ 8 | ('CO',{ 9 | 'coeffs': [ 10 | [ 1.489045326e+04, -2.922285939e+02, 5.724527170e+00, -8.176235030e-03, # 200 - 1000 11 | 1.456903469e-05, -1.087746302e-08, 3.027941827e-12, -1.303131878e+04, -7.859241350e+00], 12 | [ 4.619197250e+05, -1.944704863e+03,5.916714180e+00,-5.664282830e-04, # 1000 - 6000 13 | 1.398814540e-07, -1.787680361e-11,9.620935570e-16,-2.466261084e+03, -1.387413108e+01], 14 | [ 8.868662960e+08, -7.500377840e+05, 2.495474979e+02, -3.956351100e-02, # 6000 - 20000 15 | 3.297772080e-06, -1.318409933e-10, 1.998937948e-15, 5.701421130e+06, -2.060704786e+03], 16 | ], 17 | 'ranges': [200,1000,6000,20000], 18 | 'wt': 28.01, 19 | 'elements': OrderedDict([('C',1), ('O',1)]), 20 | }), 21 | ('CO2',{ 22 | 'coeffs': [ 23 | [ 4.943650540e+04, -6.264116010e+02, 5.301725240e+00, 2.503813816e-03, # 200 - 1000 24 | -2.127308728e-07, -7.689988780e-10, 2.849677801e-13, -4.528198460e+04, -7.048279440e+00, ], 25 | [ 1.176962419e+05, -1.788791477e+03,8.291523190e+00,-9.223156780e-05, # 1000 - 6000 26 | 4.863676880e-09, -1.891053312e-12,6.330036590e-16,-3.908350590e+04, -2.652669281e+01], 27 | [ -1.544423287e+09, 1.016847056e+06, -2.561405230e+02, 3.369401080e-02, # 6000 - 20000 28 | -2.181184337e-06, 6.991420840e-11, -8.842351500e-16, -8.043214510e+06, 2.254177493e+03] 29 | ], 30 | 'ranges': [201,1000,6000,20000], 31 | 'wt': 44.01, 32 | 'elements': OrderedDict([('C',1), ('O',2)]), 33 | }), 34 | ('O2',{ 35 | 'coeffs':[ 36 | [ -3.425563420e+04, 4.847000970e+02, 1.119010961e+00, 4.293889240e-03, # 200 - 1000 37 | -6.836300520e-07, -2.023372700e-09, 1.039040018e-12, -3.391454870e+03, 1.849699470e+01], 38 | [ -1.037939022e+06,2.344830282e+03,1.819732036e+00,1.267847582e-03, # 1000 - 6000 39 | -2.188067988e-07,2.053719572e-11,-8.193467050e-16,-1.689010929e+04, 1.738716506e+01], 40 | [ 4.975294300e+08, -2.866106874e+05, 6.690352250e+01, -6.169959020e-03, # 6000 - 20000 41 | 3.016396027e-07, -7.421416600e-12, 7.278175770e-17, 2.293554027e+06, -5.530621610e+02] 42 | ], 43 | 'ranges': [200,999,6000,20000], 44 | 'wt': 32, 45 | 'elements': {'O':2}, 46 | }) 47 | ]) 48 | 49 | # init_prod_amounts = OrderedDict([ # initial value used to set the atomic fractions in the mixture 50 | # ('CO', 0), 51 | # ('CO2', 1), 52 | # ('O2', 0), 53 | # ]) 54 | 55 | element_wts = { 56 | 'C':12.01, 'O': 16.0, 57 | } 58 | 59 | # default_elements = {'C':0.02272237, 'O':0.04544473} -------------------------------------------------------------------------------- /pycycle/thermo/cea/thermo_data/wet_air.py: -------------------------------------------------------------------------------- 1 | import numpy as np 2 | 3 | from collections import OrderedDict 4 | 5 | from pycycle.thermo.cea.thermo_data import janaf 6 | 7 | big_range = janaf.big_range 8 | small_range = janaf.small_range 9 | 10 | 11 | products = dict(janaf.products) 12 | # remove these, because they cause numerical 13 | products.pop('CH4') 14 | products.pop('C2H4') 15 | 16 | element_wts = janaf.element_wts 17 | 18 | reactants = janaf.reactants -------------------------------------------------------------------------------- /pycycle/thermo/static_ps_calc.py: -------------------------------------------------------------------------------- 1 | import numpy as np 2 | 3 | from openmdao.api import ExplicitComponent 4 | 5 | from pycycle.constants import R_UNIVERSAL_SI 6 | 7 | class PsCalc(ExplicitComponent): 8 | """Mach number, Area calculation for when Ps is known""" 9 | 10 | def setup(self): 11 | 12 | self.add_input('gamma', val=1.4) 13 | self.add_input('R', val=0.0, units='J/kg/degK') 14 | self.add_input('Ts', val=518., units="degK", desc="Static temp") 15 | self.add_input('ht', val=0., units="J/kg", desc="Total enthalpy reference condition") 16 | self.add_input('hs', val=0., units="J/kg", desc="Static enthalpy") 17 | self.add_input('W', val=0.0, desc="mass flow rate", units="kg/s") 18 | self.add_input('rho', val=1.0, desc="density", units="kg/m**3") 19 | 20 | self.add_output('MN', val=1.0, desc="computed mach number") 21 | self.add_output('V', val=1.0, units="m/s", desc="computed speed", res_ref=1e3) 22 | self.add_output('Vsonic', val=1.0, units="m/s", desc="computed speed of sound", res_ref=1e3) 23 | self.add_output('area', val=1.0, units="m**2", desc="computed area") 24 | 25 | self.declare_partials('V', ['ht', 'hs']) 26 | self.declare_partials('Vsonic', ['gamma', 'R', 'Ts']) 27 | self.declare_partials('MN', ['gamma', 'R', 'Ts', 'hs', 'ht']) 28 | self.declare_partials('area', ['rho', 'W', 'hs', 'ht']) 29 | 30 | def compute(self, inputs, outputs): 31 | 32 | outputs['Vsonic'] = Vsonic = np.sqrt(inputs['gamma'] * inputs['R'] * inputs['Ts']) 33 | 34 | # If ht < hs then V will be imaginary, so use an inverse relationship to allow solution process to continue 35 | if inputs['ht'] >= inputs['hs']: 36 | outputs['V'] = V = np.sqrt(2.0 * (inputs['ht'] - inputs['hs'])) 37 | else: 38 | # print('Warning: in', self.pathname, 'ht < hs, inverting relationship to get a real velocity, ht = ', inputs['ht'], 'hs = ', inputs['hs']) 39 | outputs['V'] = V = np.sqrt(2.0 * (inputs['hs'] - inputs['ht'])) 40 | 41 | outputs['MN'] = V / Vsonic 42 | outputs['area'] = inputs['W'] / (inputs['rho'] * V) 43 | 44 | 45 | def compute_partials(self, inputs, J): 46 | 47 | Vsonic = np.sqrt(inputs['gamma'] * inputs['R'] * inputs['Ts']) 48 | 49 | J['Vsonic','gamma'] = Vsonic / (2.0 * inputs['gamma']) 50 | J['Vsonic','R'] = Vsonic / (2.0 * inputs['R']) 51 | J['Vsonic','Ts'] = Vsonic / (2.0 * inputs['Ts']) 52 | 53 | if inputs['ht'] >= inputs['hs']: 54 | V = np.sqrt(2.0 * (inputs['ht'] - inputs['hs'])) 55 | J['V','ht'] = 1.0 / V 56 | J['V','hs'] = -1.0 / V 57 | else: 58 | V = np.sqrt(2.0 * (inputs['hs'] - inputs['ht'])) 59 | J['V','hs'] = 1.0 / V 60 | J['V','ht'] = -1.0 / V 61 | 62 | J['MN','ht'] = 1.0 / Vsonic * J['V','ht'] 63 | J['MN','hs'] = 1.0 / Vsonic * J['V','hs'] 64 | J['MN','gamma'] = -V / Vsonic**2 * J['Vsonic','gamma'] 65 | J['MN','R'] = -V / Vsonic**2 * J['Vsonic','R'] 66 | J['MN','Ts'] = -V / Vsonic**2 * J['Vsonic','Ts'] 67 | 68 | J['area','W'] = 1.0 / (inputs['rho'] * V) 69 | J['area','rho'] = -inputs['W'] / (inputs['rho']**2 * V) 70 | J['area','ht'] = -inputs['W'] / (inputs['rho'] * V**2) * J['V','ht'] 71 | J['area','hs'] = -inputs['W'] / (inputs['rho'] * V**2) * J['V','hs'] 72 | 73 | 74 | 75 | 76 | 77 | 78 | -------------------------------------------------------------------------------- /pycycle/thermo/tabular/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/OpenMDAO/pyCycle/8a031800fb90dea96ea174aedb112e7d5e120b39/pycycle/thermo/tabular/__init__.py -------------------------------------------------------------------------------- /pycycle/thermo/tabular/air_jetA.pkl: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/OpenMDAO/pyCycle/8a031800fb90dea96ea174aedb112e7d5e120b39/pycycle/thermo/tabular/air_jetA.pkl -------------------------------------------------------------------------------- /pycycle/thermo/tabular/air_jetA_coarse.pkl: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/OpenMDAO/pyCycle/8a031800fb90dea96ea174aedb112e7d5e120b39/pycycle/thermo/tabular/air_jetA_coarse.pkl -------------------------------------------------------------------------------- /pycycle/thermo/tabular/tab_cea_comparison.py: -------------------------------------------------------------------------------- 1 | import numpy as np 2 | import openmdao.api as om 3 | import pickle 4 | 5 | from pycycle.thermo.cea import chem_eq as cea_thermo 6 | from pycycle.thermo.tabular import tabular_thermo as tab_thermo 7 | from pycycle.thermo.tabular import tab_thermo_gen as tab_thermo_gen 8 | from pycycle.thermo.cea.species_data import janaf 9 | from pycycle.constants import TAB_AIR_FUEL_COMPOSITION 10 | 11 | p = om.Problem() 12 | p.model = om.Group() 13 | 14 | p.model.add_subsystem('tab', tab_thermo.SetTotalTP(thermo_spec='air_jetA.pkl', composition=TAB_AIR_FUEL_COMPOSITION), promotes_inputs=['*']) 15 | 16 | p.model.add_subsystem('cea', tab_thermo_gen.TabThermoGenAirFuel(thermo_data=janaf, thermo_method='CEA'), promotes_inputs=['*']) 17 | 18 | p.set_solver_print(level=-1) 19 | p.setup() 20 | 21 | p['FAR'] = 0.00 22 | p['P'] = 101325 #7857143.1 23 | p['T'] = 500 #1977.8 24 | 25 | 26 | temp = np.random.rand(10,3) 27 | 28 | for i, row in enumerate(temp): 29 | p['FAR'] = row[0]*(0.05) 30 | p['P'] = row[1]*1e6 31 | p['T'] = row[2]*(2500-150)+150 32 | print(p['FAR'], p['P'], p['T']) 33 | 34 | p.run_model() 35 | 36 | tab = p.get_val('tab.h')[0] 37 | cea = p.get_val('cea.flow:h', units='J/kg')[0] 38 | print('h:', abs((tab-cea)/cea)) 39 | 40 | tab = p.get_val('tab.S')[0] 41 | cea = p.get_val('cea.flow:S', units='J/kg/degK')[0] 42 | print('S:', abs((tab-cea)/cea)) 43 | 44 | tab = p.get_val('tab.gamma')[0] 45 | cea = p.get_val('cea.flow:gamma')[0] 46 | print('gamma:', abs((tab-cea)/cea)) 47 | 48 | tab = p.get_val('tab.Cp')[0] 49 | cea = p.get_val('cea.flow:Cp', units='J/kg/degK')[0] 50 | print('Cp:', abs((tab-cea)/cea)) 51 | 52 | tab = p.get_val('tab.Cv')[0] 53 | cea = p.get_val('cea.flow:Cv', units='J/kg/degK')[0] 54 | print('Cv:', abs((tab-cea)/cea)) 55 | 56 | tab = p.get_val('tab.rho')[0] 57 | cea = p.get_val('cea.flow:rho', units='kg/m**3')[0] 58 | print('rho:', abs((tab-cea)/cea)) 59 | 60 | tab = p.get_val('tab.R')[0] 61 | cea = p.get_val('cea.flow:R', units='J/kg/degK')[0] 62 | print('R:', abs((tab-cea)/cea)) 63 | print() -------------------------------------------------------------------------------- /pycycle/thermo/tabular/tabular_thermo.py: -------------------------------------------------------------------------------- 1 | import numpy as np 2 | import openmdao.api as om 3 | 4 | from pycycle.constants import TAB_AIR_FUEL_COMPOSITION, AIR_JETA_TAB_SPEC 5 | 6 | 7 | class SetTotalTP(om.Group): 8 | 9 | def initialize(self): 10 | self.options.declare('interp_method', default='slinear') 11 | self.options.declare('spec', recordable=False) 12 | self.options.declare('composition') 13 | 14 | def setup(self): 15 | interp_method = self.options['interp_method'] 16 | spec = self.options['spec'] 17 | composition = self.options['composition'] 18 | 19 | if composition is None: 20 | composition = TAB_AIR_FUEL_COMPOSITION 21 | 22 | sorted_compo = sorted(composition.keys()) 23 | 24 | interp = om.MetaModelStructuredComp(method=interp_method, extrapolate=True) 25 | self.add_subsystem('tab', interp, promotes_inputs=['P', 'T'], 26 | promotes_outputs=['h', 'S', 'gamma', 'Cp', 'Cv', 'rho', 'R']) 27 | 28 | for i, param in enumerate(sorted_compo): 29 | interp.add_input(param, composition[param], training_data=spec[param]) 30 | self.promotes('tab', inputs=[(param, 'composition')], src_indices=[i,]) 31 | self.set_input_defaults('composition', src_shape=len(composition)) 32 | 33 | interp.add_input('P', 101325.0, units='Pa', training_data=spec['P']) 34 | interp.add_input('T', 273.0, units='degK', training_data=spec['T']) 35 | 36 | if len(sorted_compo) == 1 and interp_method == 'slinear': 37 | interp.options['method'] = '3D-slinear' 38 | 39 | interp.add_output('h', 1.0, units='J/kg', training_data=spec['h']) 40 | interp.add_output('S', 1.0, units='J/kg/degK', training_data=spec['S']) 41 | interp.add_output('gamma', 1.4, units=None, training_data=spec['gamma']) 42 | interp.add_output('Cp', 1.0, units='J/kg/degK', training_data=spec['Cp']) 43 | interp.add_output('Cv', 1.0, units='J/kg/degK', training_data=spec['Cv']) 44 | interp.add_output('rho', 1.0, units='kg/m**3', training_data=spec['rho']) 45 | interp.add_output('R', 287.0, units='J/kg/degK', training_data=spec['R']) 46 | 47 | # required part of the SetTotalTP API for flow setup 48 | # use a sorted list of keys, so dictionary hash ordering doesn't bite us 49 | # loop over keys and create a vector of mass fractions 50 | self.composition = [composition[k] for k in sorted_compo] 51 | -------------------------------------------------------------------------------- /pycycle/thermo/tabular/test/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/OpenMDAO/pyCycle/8a031800fb90dea96ea174aedb112e7d5e120b39/pycycle/thermo/tabular/test/__init__.py -------------------------------------------------------------------------------- /pycycle/thermo/tabular/test/test_tab_thermo.py: -------------------------------------------------------------------------------- 1 | import unittest 2 | 3 | import numpy as np 4 | 5 | import openmdao.api as om 6 | from openmdao.utils.assert_utils import assert_near_equal 7 | 8 | from pycycle.constants import AIR_JETA_TAB_SPEC, TAB_AIR_FUEL_COMPOSITION 9 | from pycycle.thermo.tabular.tabular_thermo import SetTotalTP 10 | 11 | class TabThermoUnitTest(unittest.TestCase): 12 | 13 | def test_tab_thermo(self): 14 | 15 | p = om.Problem() 16 | 17 | p.model = SetTotalTP(spec=AIR_JETA_TAB_SPEC, composition=TAB_AIR_FUEL_COMPOSITION) 18 | 19 | p.setup() 20 | 21 | p['composition'] = 0.04 22 | p['P'] = 101325*3 23 | p['T'] = 1000 24 | 25 | p.run_model() 26 | 27 | TOL = 5e-4 28 | assert_near_equal(p.get_val('h'), -940746.85004758, tolerance=TOL) 29 | assert_near_equal(p.get_val('S'), 7967.73852287, tolerance=TOL) 30 | assert_near_equal(p.get_val('gamma'), 1.3094714, tolerance=TOL) 31 | assert_near_equal(p.get_val('Cp'), 1214.3836316, tolerance=TOL) 32 | assert_near_equal(p.get_val('Cv'), 927.3899091, tolerance=TOL) 33 | assert_near_equal(p.get_val('rho'), 1.05951464, tolerance=TOL) 34 | assert_near_equal(p.get_val('R'), 286.9948147750743, tolerance=TOL) 35 | 36 | 37 | if __name__ == "__main__": 38 | 39 | unittest.main() -------------------------------------------------------------------------------- /pycycle/thermo/test/NPSS_Static_CEA_Data.csv: -------------------------------------------------------------------------------- 1 | W,MN,V,A,s,Pt,Tt,ht,rhot,gamt,Ps,Ts,hs,rhos,gams 2 | 100,0.8,778.4815888,780.6801432,1.664686613,5.27,444.23,-24.025042,0.032018637,1.40093045,3.456238051,393.697602,-36.12787168,0.023694143,1.401186628 3 | 100,0.5,544.8629807,390.4116413,1.631486818,14.697,518.68,-6.179364198,0.076476729,1.400225285,12.38914009,493.9565518,-12.10813735,0.067694351,1.400513361 4 | 100,0.2518,458.8968798,41.07379012,1.654579613,422.079,1444.83,224.5905327,0.788456689,1.353312789,404.4394862,1428.800922,220.3850056,0.763981088,1.354220487 5 | 100,0.2442,420.2597401,105.5465407,1.690022166,158.428,1278.64,181.381769,0.334413655,1.363171493,152.1472937,1264.922228,177.8546038,0.324639007,1.364023048 6 | 100,0.01,11.16555077,16864.54056,1.631486818,14.697,518.68,-6.179364198,0.076476729,1.400225285,14.69597861,518.6696023,-6.181853918,0.076472947,1.400225418 7 | 100,2,1539.504957,1267.526185,1.664686613,5.27,444.23,-24.025042,0.032018637,1.40093045,0.67428619,246.6152438,-71.35768202,0.007379458,1.399699486 8 | -------------------------------------------------------------------------------- /pycycle/thermo/test/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/OpenMDAO/pyCycle/8a031800fb90dea96ea174aedb112e7d5e120b39/pycycle/thermo/test/__init__.py -------------------------------------------------------------------------------- /pycycle/thermo/unit_comps.py: -------------------------------------------------------------------------------- 1 | import inspect 2 | import numpy as np 3 | 4 | from openmdao.api import ExplicitComponent 5 | 6 | _full_out_args = inspect.getfullargspec(ExplicitComponent.add_output) 7 | _allowed_out_args = set(_full_out_args.args[3:] + _full_out_args.kwonlyargs) 8 | 9 | 10 | class UnitCompBase(ExplicitComponent): 11 | 12 | def initialize(self): 13 | self.options.declare('fl_name') 14 | 15 | def setup_io(self): 16 | rel2meta = self._var_rel2meta 17 | 18 | fl_name = self.options['fl_name'] 19 | 20 | for in_name in self._var_rel_names['input']: 21 | 22 | out_name = '{0}:{1}'.format(fl_name, in_name) 23 | 24 | meta = rel2meta[in_name] 25 | new_meta = {k:v for k, v in meta.items() if k in _allowed_out_args} 26 | meta_val = meta['val'] 27 | if isinstance(meta_val, float): 28 | val = meta_val 29 | else: 30 | val = meta_val.copy() 31 | self.add_output(out_name, val=val, **new_meta) 32 | 33 | rel2meta = self._var_rel2meta 34 | 35 | for in_name, out_name in zip(self._var_rel_names['input'], self._var_rel_names['output']): 36 | 37 | shape = rel2meta[in_name]['shape'] 38 | if shape is not None: 39 | size = np.prod(shape) 40 | row_col = np.arange(size, dtype=int) 41 | 42 | self.declare_partials(of=out_name, wrt=in_name, 43 | val=np.ones(size), rows=row_col, cols=row_col) 44 | else: 45 | self.declare_partials(of=out_name, wrt=in_name, val=1) 46 | 47 | def compute(self, inputs, outputs): 48 | outputs._data[:] = inputs._data 49 | 50 | class EngUnitProps(UnitCompBase): 51 | """only job is to provide flow in english units""" 52 | 53 | def setup_io(self, composition): 54 | 55 | self.add_input('T', val=284., units="degR", desc="Temperature") 56 | self.add_input('P', val=1., units='lbf/inch**2', desc="Pressure") 57 | self.add_input('h', val=1., units="Btu/lbm", desc="enthalpy") 58 | self.add_input('S', val=1., units="Btu/(lbm*degR)", desc="entropy") 59 | self.add_input('gamma', val=1.4, desc="ratio of specific heats") 60 | self.add_input('Cp', val=1., units="Btu/(lbm*degR)", desc="Specific heat at constant pressure") 61 | self.add_input('Cv', val=1., units="Btu/(lbm*degR)", desc="Specific heat at constant volume") 62 | self.add_input('rho', val=1., units="lbm/ft**3", desc="density") 63 | self.add_input('R', val=1.0, units="Btu/(lbm*degR)", desc='Total specific gas constant') 64 | self.add_input('composition', val=composition, desc='moles of atoms present for each element') 65 | 66 | super().setup_io() 67 | 68 | 69 | class EngUnitStaticProps(UnitCompBase): 70 | 71 | def setup_io(self): 72 | 73 | self.add_input('area', val=1.0, units="inch**2") 74 | self.add_input('W', val=1.0, units="lbm/s") 75 | self.add_input('V', val=1.0, units="ft/s") 76 | self.add_input('Vsonic', val=1.0, units="ft/s") 77 | self.add_input('MN', val=0.5) 78 | 79 | super().setup_io() 80 | 81 | 82 | if __name__ == "__main__": 83 | 84 | from openmdao.api import Problem, Group, IndepVarComp 85 | from pycycle.cea import species_data 86 | 87 | thermo = species_data.Properties(species_data.co2_co_o2) 88 | 89 | p = Problem() 90 | model = p.model = Group() 91 | indep = model.add_subsystem('indep', IndepVarComp(), promotes=['*']) 92 | # indep.add_output('T', val=100., units='degK') 93 | # indep.add_output('P', val=1., units='bar') 94 | indep.add_output('T', val=100., units='degR') 95 | indep.add_output('P', val=1., units='psi') 96 | 97 | model.add_subsystem('units', EngUnitProps(thermo=thermo), promotes=['*']) 98 | 99 | p.setup() 100 | 101 | p.run_model() 102 | 103 | p.model.run_linearize() 104 | jac = p.model.get_subsystem('units').jacobian._subjacs 105 | 106 | for pair in jac: 107 | print(pair) 108 | print(jac[pair]) 109 | print() -------------------------------------------------------------------------------- /pyproject.toml: -------------------------------------------------------------------------------- 1 | [build-system] 2 | build-backend = "setuptools.build_meta" 3 | requires = ["setuptools"] 4 | -------------------------------------------------------------------------------- /setup.py: -------------------------------------------------------------------------------- 1 | import re 2 | import sys 3 | 4 | from setuptools import setup 5 | 6 | __version__ = re.findall( 7 | r"""__version__ = ["']+([0-9\.\-dev]*)["']+""", 8 | open('pycycle/__init__.py').read(), 9 | )[0] 10 | 11 | # optional dependencies, by category (currently just 'test') 12 | optional_dependencies = { 13 | 'test': [ 14 | 'testflo>=1.3.6', 15 | ] 16 | } 17 | 18 | # Add an optional dependency that concatenates all others 19 | optional_dependencies['all'] = sorted([ 20 | dependency 21 | for dependencies in optional_dependencies.values() 22 | for dependency in dependencies 23 | ]) 24 | 25 | setup(name='om-pycycle', 26 | version=__version__, 27 | description="pyCycle -- Thermodynamic Cycle modeling library", 28 | long_description="""pyCycle is an open-source library for modeling of turbine based propulsion and power generation systems. 29 | It is a modular library, allowing you to build up a turbine based system from basic blocks like `inlet`, `compressor`, `turbine`, and `nozzle`. 30 | """, 31 | 32 | packages=[ 33 | 'pycycle', 34 | 'pycycle.elements', 35 | 'pycycle.elements.test', 36 | 'pycycle.maps', 37 | 'pycycle.maps.test', 38 | 'pycycle.thermo', 39 | 'pycycle.thermo.cea', 40 | 'pycycle.thermo.cea.test', 41 | 'pycycle.thermo.cea.thermo_data', 42 | 'pycycle.thermo.tabular', 43 | 'pycycle.thermo.tabular.test', 44 | 'pycycle.thermo.test', 45 | 'pycycle.tests', 46 | ], 47 | install_requires=[ 48 | 'openmdao>=3.10.0', 49 | ], 50 | package_data={ 51 | 'pycycle.elements.test': ['reg_data/*.csv'], 52 | 'pycycle.thermo.test': ['*.csv'], 53 | 'pycycle.thermo.tabular': ['*.pkl'], 54 | }, 55 | extras_require=optional_dependencies, 56 | ) 57 | --------------------------------------------------------------------------------