From 184660655204ba54ea8d9214b9aca6c87f1255c0 Mon Sep 17 00:00:00 2001 From: Kenneth-T-Moore Date: Tue, 17 Jun 2025 15:04:01 -0400 Subject: [PATCH 1/2] Fix for compatibility with OpenMDAO:3.39 --- example_cycles/tests/test_all_examples.py | 123 ++++++++++++++++++++++ pycycle/api.py | 6 +- pycycle/viewers.py | 94 +++++++++++++---- 3 files changed, 197 insertions(+), 26 deletions(-) create mode 100644 example_cycles/tests/test_all_examples.py diff --git a/example_cycles/tests/test_all_examples.py b/example_cycles/tests/test_all_examples.py new file mode 100644 index 00000000..0ab01cd0 --- /dev/null +++ b/example_cycles/tests/test_all_examples.py @@ -0,0 +1,123 @@ +""" +This script is designed to test run scripts located in a specified directory. +It uses Python's unittest framework to dynamically generate test cases for each +script that ends with '.py'. +""" + +import os +import subprocess +import unittest +from pathlib import Path + +from openmdao.utils.testing_utils import use_tempdirs +from parameterized import parameterized + +# Address any issue that requires a skip. +SKIP_EXAMPLES = { + "high_bypass_turbofan.py" : "Runtime is more than 25 min." , + "tab_thermo_data_generator.py" : "Runtime is more than 25 min." , +} + + +def find_examples(): + """ + Find and return a list of run scripts in the specified directory. + + Returns + ------- + list + A list of pathlib.Path objects pointing to the run scripts. + """ + base_dir = os.path.join(os.path.dirname(os.path.dirname(os.path.abspath(__file__))), '.') + + run_files = [] + for root, _, files in os.walk(base_dir): + for file in files: + if file.endswith('.py'): + run_files.append(Path(root) / file) + + # Don't recurse into tests directory. + break + + return run_files + + +def example_name(testcase_func, param_num, param): + """ + Returns a formatted case name for unit testing with decorator @parameterized.expand(). + It is intended to be used when expand() is called with a list of strings + representing test case names. + + Parameters + ---------- + testcase_func : Any + This parameter is ignored. + param_num : Any + This parameter is ignored. + param : param + The param object containing the case name to be formatted. + """ + return 'benchmark_example_' + param.args[0].name.replace('.py', '') + + +@use_tempdirs +class RunScriptTest(unittest.TestCase): + """ + A test case class that uses unittest to run and test scripts with a timeout. + + Attributes + ---------- + base_directory : str + The base directory where the run scripts are located. + run_files : list + A list of paths to run scripts found in the base directory. + + Methods + ------- + setUpClass() + Class method to find all run scripts before tests are run. + find_run_files(base_dir) + Finds and returns all run scripts in the specified directory. + run_script(script_path) + Attempts to run a script with a timeout and handles errors. + test_run_scripts() + Generates a test for each run script with a timeout. + """ + + def run_script(self, script_path, max_allowable_time=1500): + """ + Attempt to run a script with a 1500-second timeout and handle errors. + + Parameters + ---------- + script_path : pathlib.Path + The path to the script to be run. + + Raises + ------ + Exception + Any exception other than ImportError or TimeoutExpired that occurs while running the script. + """ + with open(os.devnull, 'w') as devnull: + proc = subprocess.Popen(['python', script_path], stdout=devnull, stderr=subprocess.PIPE) + proc.wait(timeout=max_allowable_time) + (stdout, stderr) = proc.communicate() + + if proc.returncode != 0: + if 'ImportError' in str(stderr): + self.skipTest(f'Skipped {script_path.name} due to ImportError') + else: + raise Exception(f'Error running {script_path.name}:\n{stderr.decode("utf-8")}') + + @parameterized.expand(find_examples(), name_func=example_name) + def benchmark_run_scripts(self, example_path): + """Test each run script to ensure it executes without error.""" + if example_path.name in SKIP_EXAMPLES: + reason = SKIP_EXAMPLES[example_path.name] + self.skipTest(f'Skipped {example_path.name}: {reason}.') + + self.run_script(example_path) + + +if __name__ == '__main__': + unittest.main() diff --git a/pycycle/api.py b/pycycle/api.py index ea7cb46f..5e9ceb0d 100644 --- a/pycycle/api.py +++ b/pycycle/api.py @@ -1,6 +1,6 @@ -from pycycle.constants import (AIR_FUEL_MIX, AIR_MIX, WET_AIR_MIX, BTU_s2HP, HP_per_RPM_to_FT_LBF, - R_UNIVERSAL_SI, R_UNIVERSAL_ENG, g_c, MIN_VALID_CONCENTRATION, - T_STDeng, P_STDeng, P_REF, CEA_AIR_COMPOSITION, CEA_AIR_FUEL_COMPOSITION, +from pycycle.constants import (AIR_FUEL_MIX, AIR_MIX, WET_AIR_MIX, BTU_s2HP, HP_per_RPM_to_FT_LBF, + R_UNIVERSAL_SI, R_UNIVERSAL_ENG, g_c, MIN_VALID_CONCENTRATION, + T_STDeng, P_STDeng, P_REF, CEA_AIR_COMPOSITION, CEA_AIR_FUEL_COMPOSITION, CEA_WET_AIR_COMPOSITION, AIR_JETA_TAB_SPEC, TAB_AIR_FUEL_COMPOSITION) from pycycle.thermo.cea import species_data diff --git a/pycycle/viewers.py b/pycycle/viewers.py index bde0fd69..5ecca2e1 100644 --- a/pycycle/viewers.py +++ b/pycycle/viewers.py @@ -3,13 +3,36 @@ import numpy as np # protection incase env doesn't have matplotlib installed, since its not strictly required -try: +try: import matplotlib import matplotlib.pyplot as plt -except ImportError: +except ImportError: plt = None +def get_val(prob, point, element, var_name): + """ + Get the value of the requested element from the OpenMDAO model. + + Parameters + ---------- + prob: + OpenMDAO problem that contains the pycycle model. + point: str + OpenMDAO pathname of the cycle point. + element: str + Name of the pcycle element system. + var_name: str + Name of the variable to get. + """ + try: + val = prob.get_val(f"{point}.{element}.{var_name}") + except KeyError: + # This is a cycle parameter. + val = prob.get_val(f"{element}.{var_name}") + + return val[0] + def print_flow_station(prob, fs_names, file=sys.stdout): names = ['tot:P', 'tot:T', 'tot:h', 'tot:S', 'stat:P', 'stat:W', 'stat:MN', 'stat:V', 'stat:area'] @@ -83,14 +106,19 @@ def print_burner(prob, element_names, file=sys.stdout): # line_tmpl = '{:<20}| '+'{:13.3f}'*4 line_tmpl = '{:<20}| {:13.4f}{:13.2f}{:13.4f}{:13.5f}' + for e_name in element_names: - W_fuel = prob[e_name+'.Wfuel'][0] - W_tot = prob[e_name+'.Fl_O:stat:W'][0] + + point, _, element = e_name.rpartition(".") + + W_fuel = get_val(prob, point, element, 'Wfuel') + W_tot = get_val(prob, point, element, 'Fl_O:stat:W') W_air = W_tot - W_fuel FAR = W_fuel/W_air - print(line_tmpl.format(e_name, prob[e_name+'.dPqP'][0], - prob[e_name+'.Fl_O:tot:T'][0], - W_fuel, FAR), + dPqP = get_val(prob, point, element, 'dPqP') + T_tot = get_val(prob, point, element, 'Fl_O:tot:T') + + print(line_tmpl.format(e_name, dPqP, T_tot, W_fuel, FAR), file=file, flush=True) @@ -108,17 +136,28 @@ def print_turbine(prob, element_names, file=sys.stdout): line_tmpl = '{:<14}| '+'{:13.3f}'*9 for e_name in element_names: + + point, _, element = e_name.rpartition(".") + sys = prob.model._get_subsystem(e_name) if sys.options['design']: - PR_temp = prob[e_name+'.map.scalars.PR'][0] - eff_temp = prob[e_name+'.map.scalars.eff'][0] + PR_temp = get_val(prob, point, element, 'map.scalars.PR') + eff_temp = get_val(prob, point, element, 'map.scalars.eff') else: - PR_temp = prob[e_name+'.PR'][0] - eff_temp = prob[e_name+'.eff'][0] - - print(line_tmpl.format(e_name, prob[e_name+'.Wp'][0], PR_temp, - eff_temp, prob[e_name+'.eff_poly'][0], prob[e_name+'.Np'][0], prob[e_name+'.power'][0], - prob[e_name+'.map.NpMap'][0], prob[e_name+'.map.PRmap'][0], prob[e_name+'.map.alphaMap'][0]), + PR_temp = get_val(prob, point, element, 'PR') + eff_temp = get_val(prob, point, element, 'eff') + + Wp = get_val(prob, point, element, 'Wp') + eff_poly = get_val(prob, point, element, 'eff_poly') + Np = get_val(prob, point, element, 'Np') + power = get_val(prob, point, element, 'power') + NpMap = get_val(prob, point, element, 'map.NpMap') + PRmap = get_val(prob, point, element, 'map.PRmap') + alphaMap = get_val(prob, point, element, 'map.alphaMap') + + print(line_tmpl.format(e_name, Wp, PR_temp, + eff_temp, eff_poly, Np, power, + NpMap, PRmap, alphaMap), file=file, flush=True) @@ -134,21 +173,30 @@ def print_nozzle(prob, element_names, file=sys.stdout): for e_name in element_names: + + point, _, element = e_name.rpartition(".") + sys = prob.model._get_subsystem(e_name) if sys.options['lossCoef'] == 'Cv': - Cv_val = prob[e_name+'.Cv'][0] + + Cv_val = get_val(prob, point, element, 'Cv') Cfg_val = ' N/A ' line_tmpl = '{:<14}| ' + '{:13.3f}'*2 + '{}' + '{:13.3f}'*5 else: Cv_val = ' N/A ' - Cfg_val = prob[e_name+'.Cfg'][0] + Cfg_val = get_val(prob, point, element, 'Cfg') line_tmpl = '{:<14}| ' + '{:13.3f}'*1 + '{}' + '{:13.3f}'*6 - print(line_tmpl.format(e_name, prob[e_name+'.PR'][0], Cv_val, Cfg_val, - prob[e_name+'.Throat:stat:area'][0], prob[e_name+'.Throat:stat:MN'][0], - prob[e_name+'.Fl_O:stat:MN'][0], - prob[e_name+'.Fl_O:stat:V'][0], prob[e_name+'.Fg'][0]), + PR = get_val(prob, point, element, 'PR') + area = get_val(prob, point, element, 'Throat:stat:area') + throat_MN = get_val(prob, point, element, 'Throat:stat:MN') + MN = get_val(prob, point, element, 'Fl_O:stat:MN') + V = get_val(prob, point, element, 'Fl_O:stat:V') + Fg = get_val(prob, point, element, 'Fg') + + print(line_tmpl.format(e_name, PR, Cv_val, Cfg_val, + area, throat_MN, MN, V, Fg), file=file, flush=True) @@ -287,7 +335,7 @@ def plot_compressor_maps(prob, element_names, eff_vals=np.array([0,0.5,0.55,0.6, plt.ylabel('PR') plt.title(e_name) # plt.show() - plt.savefig(e_name+'.pdf') + plt.savefig(e_name+'.pdf') def plot_turbine_maps(prob, element_names, eff_vals=np.array([0,0.5,0.55,0.6,0.65,0.7,0.75,0.8,0.85,0.9,0.95,1.0]),alphas=[0]): @@ -321,5 +369,5 @@ def plot_turbine_maps(prob, element_names, eff_vals=np.array([0,0.5,0.55,0.6,0.6 plt.ylabel('PR') plt.title(e_name) # plt.show() - plt.savefig(e_name+'.pdf') + plt.savefig(e_name+'.pdf') From 284f20a000454422908d24261c4c8b78ff9c91b9 Mon Sep 17 00:00:00 2001 From: Kenneth-T-Moore Date: Mon, 23 Jun 2025 13:57:15 -0400 Subject: [PATCH 2/2] Add units to get_val method. --- pycycle/viewers.py | 12 +++++++++--- 1 file changed, 9 insertions(+), 3 deletions(-) diff --git a/pycycle/viewers.py b/pycycle/viewers.py index 5ecca2e1..f6034b41 100644 --- a/pycycle/viewers.py +++ b/pycycle/viewers.py @@ -10,7 +10,7 @@ plt = None -def get_val(prob, point, element, var_name): +def get_val(prob, point, element, var_name, units=None): """ Get the value of the requested element from the OpenMDAO model. @@ -24,12 +24,18 @@ def get_val(prob, point, element, var_name): Name of the pcycle element system. var_name: str Name of the variable to get. + units: str or None + Units for the return value. Default is None, which will return using the delared units. + + Returns + float + Value of requested element. """ try: - val = prob.get_val(f"{point}.{element}.{var_name}") + val = prob.get_val(f"{point}.{element}.{var_name}", units=units) except KeyError: # This is a cycle parameter. - val = prob.get_val(f"{element}.{var_name}") + val = prob.get_val(f"{element}.{var_name}", units=units) return val[0]